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 プロパティーに適当な文字列を設定しておくと便利である。
以上のように、任意のメモリブロックをクリップボード経由で簡単にやりとりすることができた。