DOS アプリからはじめて Windows3.1 に移行してアプリケーションを使い始めたとき、クリップボードにデータをコピーしたり、ペーストしたりして、ことなるアプリ間でデータを交換できることを知り、感激したことを覚えている。Windows は、クリップボードを経由して交換できる標準的なデータフォーマットをいくつか予め決めている。しかし、多くのアプリケーションでは、そのアプリ独自のフォーマットを使ってクリップボードを利用している。ここでは、メッセージによるプロセス間通信 と同様なインタフェースを使って、任意のメモリブロックをクリップボードを経由して受け渡しすることを試してみよう。
クリップボードでやりとりする独自のデータフォーマットを登録するには
function RegisterClipboardFormat(lpszFormat: PChar): UINT; stdcall;
を使う。lpszFormat には、任意の文字列を設定する。この API の戻り値が、登録されたデータフォーマットの番号であり、$C000 から $FFFF までの値になる。一回のウィンドウセッションでは、同じ文字列を設定すると、どの場合にも同じデータフォーマット番号が返ってくる。したがって、二重に登録されることはない。早い者勝ちである。上記の API を同じ文字列を使って何度実行しても、登録数が増えるわけではないので安心である。さて、任意のメモリブロックをクリップボードとやり取りしなくてはならないが、それは「メモリハンドル」を通して行う。したがって、Delphi の GetMem( ) や AllocMem( ) で確保したメモリではなく、ウィンドウズの API である GlobalAlloc( ) を使ってメモリを確保する必要がある。すでに API の稿の 第17章 メモリ操作 でこれらの GlobalXXX( ) については説明している。ここでは、簡単に復習してみよう。
function GlobalAlloc(uFlags: UINT; dwBytes: DWORD): HGLOBAL; stdcall;
uFlag には、ここでは GHND = GMEM_MOVEABLE or GMEM_ZEROINIT; を設定する。dwBytes は確保すべきメモリのサイズである。この関数の戻り値が「メモリハンドル」である。これをクリップボードに渡すことにより、コピー動作が実現する。渡す前に有意なデータを書き込まなくてはならない。そのためには、ハンドルをポインタに変換する。
function GlobalLock(hMem: HGLOBAL): Pointer; stdcall;
この関数の戻り値のポインタを使ってデータを書き込んだ後、クリップボードにハンドルを渡してコピー動作が完了する。クリップボードを閉じる前に、
function GlobalUnlock(hMem: HGLOBAL): BOOL; stdcall;
を実行してアンロックする。普通のメモリ確保の場合は、メモリハンドルをつかって
function GlobalFree(hMem: HGLOBAL): HGLOBAL; stdcall;
により、メモリを破棄する。しかし、クリップボードに渡したメモリブロックを破棄してはならない。適切なタイミングでウィンドウズが自動的に破棄してくれる。
ペースト動作は、クリップボードからメモリハンドルを受け取ってポインタに変換し、自分のメモリに転写してロックを解いてクリップボードを閉じる。ハンドルからメモリのサイズを得るには、
function GlobalSize(hMem: HGLOBAL): DWORD; stdcall;
で取得できる。それはさっそくやってみよう。ここでは、Jpeg ファイルの内容を受け渡ししてみよう。Form1 に Button を二つ置いて以下のようにする。
List 1
unit Main; interface uses Windows, Messages, SysUtils, Classes, Graphics, Controls, Forms, Dialogs, StdCtrls; type TForm1 = class(TForm) Button1: TButton; Button2: TButton; procedure FormCreate(Sender: TObject); procedure Button1Click(Sender: TObject); procedure Button2Click(Sender: TObject); private { Private 宣言 } public { Public 宣言 } end; var Form1: TForm1; implementation {$R *.DFM} uses Clipbrd, Jpeg; const MyDataStr:string = 'HogeHogeData'; var MyDataFormat:UINT; procedure TForm1.FormCreate(Sender: TObject); begin MyDataFormat := RegisterClipboardFormat(PChar(MyDataStr)); end; procedure TForm1.Button1Click(Sender: TObject); var MS:TMemoryStream; hBuf:THandle; p:pointer; begin MS := TMemoryStream.Create; try MS.LoadFromFile('c:\orangecake.jpg'); hBuf := GlobalAlloc(GHND,MS.Size); p := GlobalLock(hBuf); MS.ReadBuffer(p^,MS.Size); Clipboard.Open; Clipboard.Clear; Clipboard.SetAsHandle(MyDataFormat,hBuf); GlobalUnlock(hBuf); Clipboard.Close; finally MS.Free; end; end; procedure TForm1.Button2Click(Sender: TObject); var MS:TMemoryStream; JPG:TJpegImage; hBuf:THandle; p:pointer; begin MS := TMemoryStream.Create; JPG := TJpegImage.Create; try if Clipboard.HasFormat(MyDataFormat) then begin ClipBoard.Open; hBuf := Clipboard.GetAsHandle(MyDataFormat); p := GlobalLock(hBuf); MS.WriteBuffer(p^,GlobalSize(hBuf)); GlobalUnlock(hBuf); Clipboard.Close; MS.Position := 0; JPG.LoadFromStream(MS); Canvas.Draw(10,10,JPG); end; finally MS.Free; JPG.Free; end; end; end.
実行して、Button1 を押してクリップボードに Jpeg ファイルの内容をコピーし、Button2 を押すと右図のようになる。一度コピーした内容が有効なうちは、なんどでもペーストできる。同じ実行ファイルを複数エクスプローラから起動して、Button2 を押して確かめることができるだろう。
コードを見てみよう。FormCreate イベントハンドラで独自データを 'HogeHogeData' という文字列で登録している。戻り値のデータフォーマット番号はグローバル変数 MyDataFormat に格納しておく。Button1Click では、MemoryStream のインスタンスをつくり、適当な Jpeg ファイルを読み込んで GlobalAlloc() で確保したメモリに転写している。
p := GlobalLock(hBuf); MS.ReadBuffer(p^,MS.Size);
そして、VCL の ClipBoard オブジェクトをつかって、
Clipboard.Open; // クリップボードをオープン Clipboard.Clear; // 内容をクリア Clipboard.SetAsHandle(MyDataFormat,hBuf); // データフォーマットを指定してハンドルを渡す GlobalUnlock(hBuf); // メモリをアンロックする Clipboard.Close; // クリップボードを閉じる
これでコピー動作は完了である。次に、Button2Click では
if Clipboard.HasFormat(MyDataFormat) then begin
で、独自登録データがあることを確認してから
ClipBoard.Open; // クリップボードをオープン hBuf := Clipboard.GetAsHandle(MyDataFormat); // データフォーマットを指定してハンドルをもらう p := GlobalLock(hBuf); // ロックしてポインタに変換 MS.WriteBuffer(p^,GlobalSize(hBuf)); // メモリストリームに転写 GlobalUnlock(hBuf); // アンロック Clipboard.Close; // クリップボードを閉じる
として、ペースト動作を行う。あとは、JpegImage のオブジェクトを使って描画している。
さて、Jpeg ファイルの内容をメモリにコピーするために TMemoryStream のオブジェクトを使ったのは、クリップボードへの動作と任意のメモリの内容を用意する動作を分離したかったからである。すこし冗長になるが、上のコードを変更して、コピー動作やペースト動作と、受け渡しするデータをメモリに用意するコードを分離してみよう。
List 2
unit Main; interface uses Windows, Messages, SysUtils, Classes, Graphics, Controls, Forms, Dialogs, StdCtrls; type TForm1 = class(TForm) Button1: TButton; Button2: TButton; procedure FormCreate(Sender: TObject); procedure Button1Click(Sender: TObject); procedure Button2Click(Sender: TObject); private { Private 宣言 } public procedure CopyData(MS:TMemoryStream); function PasteData(MS:TMemoryStream):Boolean; end; var Form1: TForm1; implementation {$R *.DFM} uses Clipbrd, Jpeg; const MyDataStr:string = 'HogeHogeData'; var MyDataFormat:UINT; procedure TForm1.FormCreate(Sender: TObject); begin MyDataFormat := RegisterClipboardFormat(PChar(MyDataStr)); end; procedure TForm1.CopyData(MS:TMemoryStream); var hBuf:THandle; p:pointer; begin hBuf := GlobalAlloc(GHND,MS.Size); p := GlobalLock(hBuf); MS.Position := 0; MS.ReadBuffer(p^,MS.Size); Clipboard.Open; Clipboard.Clear; Clipboard.SetAsHandle(MyDataFormat,hBuf); GlobalUnlock(hBuf); Clipboard.Close; end; function TForm1.PasteData(MS:TMemoryStream):Boolean; var hBuf:THandle; p:pointer; begin result := false; if Clipboard.HasFormat(MyDataFormat) then begin ClipBoard.Open; hBuf := Clipboard.GetAsHandle(MyDataFormat); p := GlobalLock(hBuf); MS.Clear; MS.WriteBuffer(p^,GlobalSize(hBuf)); MS.Position := 0; GlobalUnlock(hBuf); Clipboard.Close; result := true; end; end; procedure TForm1.Button1Click(Sender: TObject); var MS:TMemoryStream; begin MS := TMemoryStream.Create; try MS.LoadFromFile('c:\orangecake.jpg'); CopyData(MS); finally MS.Free; end; end; procedure TForm1.Button2Click(Sender: TObject); var MS:TMemoryStream; JPG:TJpegImage; begin MS := TMemoryStream.Create; JPG := TJpegImage.Create; try if PasteData(MS) then begin JPG.LoadFromStream(MS); Canvas.Draw(10,10,JPG); end; finally MS.Free; JPG.Free; end; end; end.
実行結果は右図のようになり、まえのものと同じである。CopyData() 手続きと PasteData() 関数には、TMemoryStream のインスタンスを渡して、その内容に読み書きすることによりコピー・ペーストを行っている。この方がコードの見通しが格段によいだろう。
最後にこの機能を非可視のコンポーネントにしてみよう。CopyPasteUnit.pas の全リストを以下に示す。
List 3
unit CopyPasteUnit; interface uses Windows, Messages, SysUtils, Classes, Clipbrd; type TCopyPaste = class(TComponent) private FDataFormat:UINT; FDataName:string; FCopyData:TMemoryStream; FPasteData:TMemoryStream; procedure SetDataname(value:string); function GetHasData:Boolean; protected public constructor Create(AOwner:TComponent);override; destructor Destroy;override; function CopyToClipboard:Boolean; function PasteFromClipboard:Boolean; property HasData:Boolean read GetHasData; property CopyData:TMemoryStream read FCopyData; property PasteData:TMemoryStream read FPasteData; published property DataName:string read FDataName write SetDataName; end; procedure Register; implementation procedure Register; begin RegisterComponents('Samples',[TCopyPaste]); end; constructor TCopyPaste.Create(AOwner:TComponent); begin inherited Create(AOwner); FCopyData := TMemoryStream.Create; FPasteData := TMemoryStream.Create; end; destructor TCopyPaste.Destroy; begin FCopyData.Free; FPasteData.Free; inherited Destroy; end; procedure TCopyPaste.SetDataName(value:string); begin if (value <> FDataName) and (Length(value)>3) then begin FDataName := value; if not (csDesigning in ComponentState) then FDataFormat := RegisterClipboardFormat(PChar(value)); end; end; function TCopyPaste.GetHasData:Boolean; begin result := Clipboard.HasFormat(FDataFormat); end; function TCopyPaste.CopyToClipboard:Boolean; var hBuf:THandle; p:pointer; begin hBuf := GlobalAlloc(GPTR,FCopyData.Size); p := GlobalLock(hBuf); FCopyData.Position := 0; FCopyData.ReadBuffer(p^,FCopyData.Size); Clipboard.Open; Clipboard.Clear; Clipboard.SetAsHandle(FDataFormat,hBuf); GlobalUnlock(hBuf); Clipboard.Close; result := HasData; end; function TCopyPaste.PasteFromClipboard:Boolean; var hBuf:THandle; p:pointer; begin result := false; if HasData then begin ClipBoard.Open; hBuf := Clipboard.GetAsHandle(FDataFormat); p := GlobalLock(hBuf); FPasteData.Clear; FPasteData.WriteBuffer(p^,GlobalSize(hBuf)); FPasteData.Position := 0; GlobalUnlock(hBuf); Clipboard.Close; result := true; end; end; end.
このコンポーネントをテストするために右図のような配置でプログラムをつくる。以下に全リストを示す。
List 4
unit Main; interface uses Windows, Messages, SysUtils, Classes, Graphics, Controls, Forms, Dialogs, StdCtrls, CopyPasteUnit; type TForm1 = class(TForm) CopyPaste1: TCopyPaste; Button1: TButton; Button2: TButton; Memo1: TMemo; Button3: TButton; Button4: TButton; Button5: TButton; Button6: TButton; procedure Button1Click(Sender: TObject); procedure Button2Click(Sender: TObject); procedure Button3Click(Sender: TObject); procedure Button4Click(Sender: TObject); procedure Button5Click(Sender: TObject); procedure Button6Click(Sender: TObject); private { Private 宣言 } public { Public 宣言 } end; var Form1: TForm1; implementation {$R *.DFM} uses Jpeg; const JpegData:string = 'HogeHogeJpeg'; StringData:string = 'HogeHogeString'; RecordData:string = 'HogeHogeRecord'; procedure TForm1.Button1Click(Sender: TObject); begin CopyPaste1.DataName := JpegData; CopyPaste1.CopyData.LoadFromFile('c:\orangecake.jpg'); CopyPaste1.CopyToClipboard; end; procedure TForm1.Button2Click(Sender: TObject); var JPG:TJpegImage; begin CopyPaste1.DataName := JpegData; if CopyPaste1.PasteFromClipboard then begin JPG := TJpegImage.Create; try JPG.LoadFromStream(CopyPaste1.PasteData); Canvas.Draw(10,10,JPG); finally JPG.Free; end; end; end; procedure TForm1.Button3Click(Sender: TObject); var s:string; begin s := 'DOS アプリからはじめて Windows3.1 に移行してアプリケーションを使'+ 'い始めたとき、クリップボードにデータをコピーしたり、ペーストした'+ 'りして、ことなるアプリ間でデータを交換できることを知り、感激した'+ 'ことを覚えている。'; CopyPaste1.DataName := StringData; CopyPaste1.CopyData.Clear; CopyPaste1.CopyData.WriteBuffer(PChar(s)^,Length(s)); CopyPaste1.CopyToClipboard; end; procedure TForm1.Button4Click(Sender: TObject); var s:string; begin CopyPaste1.DataName := StringData; if CopyPaste1.PasteFromClipboard then begin SetLength(s,CopyPaste1.PasteData.Size); CopyPaste1.PasteData.ReadBuffer(PChar(s)^,CopyPaste1.PasteData.Size); Memo1.Lines.Text := s; end; end; type TMyRecord = record No:integer; Name:string; Handle:HWND; end; TMyRecordArray = array[0..20] of TMyRecord; procedure TForm1.Button5Click(Sender: TObject); var MA:TMyRecordArray; i,count:integer; WR:TWriter; begin CopyPaste1.DataName := RecordData; count := ComponentCount; for i := 0 to count-1 do begin MA[i].No := i; MA[i].Name := Components[i].Name; if Components[i] is TWinControl then MA[i].Handle := TWinControl(Components[i]).Handle else MA[i].Handle := 0; end; CopyPaste1.CopyData.Clear; WR := TWriter.Create(CopyPaste1.CopyData,1000); WR.WriteInteger(count); for i := 0 to count-1 do begin WR.WriteInteger(MA[i].No); WR.WriteString(MA[i].Name); WR.WriteInteger(MA[i].Handle); end; WR.Free; CopyPaste1.CopyToClipboard; end; procedure TForm1.Button6Click(Sender: TObject); var MA:TMyRecordArray; i,count:integer; RD:TReader; begin CopyPaste1.DataName := RecordData; if CopyPaste1.PasteFromClipboard then begin RD := TReader.Create(CopyPaste1.PasteData,1000); count := RD.ReadInteger; for i := 0 to count-1 do begin MA[i].No := RD.ReadInteger; MA[i].Name := RD.ReadString; MA[i].Handle := RD.ReadInteger; end; RD.Free; Memo1.Clear; for i := 0 to count-1 do begin Memo1.Lines.Add(''); Memo1.Lines.Add(IntToStr(MA[i].No)); Memo1.Lines.Add(MA[i].Name); Memo1.Lines.Add('$'+IntToHex(MA[i].Handle,8)); end; end; end; end.
実行して、Button1 を押し、次に Button2 を押すと右図のようになる。これは、これまでと同じである。
つぎに Button3 を押してから Button4 を押すと右図のようになる。文字列の受け渡しはストリームに読み書きするのが簡単なのですぐにできるだろう。
最後に、可変長のレコード型の配列の例として Button5 でコピーし、Button6 でペーストする例を示した。これは、最初に要素の数を書き込み、TWriter で書き込み、TReader で読み込むので少し面倒である。
このテスト例では、一つの TCopyPaste のインスタンスで三つのデータフォーマットを使い回している。複数の独自データフォーマットを使うときには、このように使い回すこともできるし、フォーマットごとに TCopyPaste のインスタンスを一つずつ割り当ててもよい。各インスタンスに一つしか独自フォーマットが無いときは、オブジェクトインスペクタで DataName プロパティーに適当な文字列を設定しておくと便利である。
以上のように、任意のメモリブロックをクリップボード経由で簡単にやりとりすることができた。