クリップボードで任意のデータを受け渡す


redball.gif目次へ戻る

DOS アプリからはじめて Windows3.1 に移行してアプリケーションを使い始めたとき、クリップボードにデータをコピーしたり、ペーストしたりして、ことなるアプリ間でデータを交換できることを知り、感激したことを覚えている。Windows は、クリップボードを経由して交換できる標準的なデータフォーマットをいくつか予め決めている。しかし、多くのアプリケーションでは、そのアプリ独自のフォーマットを使ってクリップボードを利用している。ここでは、メッセージによるプロセス間通信 と同様なインタフェースを使って、任意のメモリブロックをクリップボードを経由して受け渡しすることを試してみよう。

クリップボードでやりとりする独自のデータフォーマットを登録するには

function RegisterClipboardFormat(lpszFormat: PChar): UINT; stdcall;

を使う。lpszFormat には、任意の文字列を設定する。この API の戻り値が、登録されたデータフォーマットの番号であり、$C000 から $FFFF までの値になる。一回のウィンドウセッションでは、同じ文字列を設定すると、どの場合にも同じデータフォーマット番号が返ってくる。したがって、二重に登録されることはない。早い者勝ちである。上記の API を同じ文字列を使って何度実行しても、登録数が増えるわけではないので安心である。さて、任意のメモリブロックをクリップボードとやり取りしなくてはならないが、それは「メモリハンドル」を通して行う。したがって、DelphiGetMem( )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 ファイルの内容を受け渡ししてみよう。Form1Button を二つ置いて以下のようにする。

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.
n01401.jpg

実行して、Button1 を押してクリップボードに Jpeg ファイルの内容をコピーし、Button2 を押すと右図のようになる。一度コピーした内容が有効なうちは、なんどでもペーストできる。同じ実行ファイルを複数エクスプローラから起動して、Button2 を押して確かめることができるだろう。


コードを見てみよう。FormCreate イベントハンドラで独自データを 'HogeHogeData' という文字列で登録している。戻り値のデータフォーマット番号はグローバル変数 MyDataFormat に格納しておく。Button1Click では、MemoryStream のインスタンスをつくり、適当な Jpeg ファイルを読み込んで GlobalAlloc() で確保したメモリに転写している。

    p := GlobalLock(hBuf);
    MS.ReadBuffer(p^,MS.Size);

そして、VCLClipBoard オブジェクトをつかって、

    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.
n01401.jpg

実行結果は右図のようになり、まえのものと同じである。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.
n01402.jpg

このコンポーネントをテストするために右図のような配置でプログラムをつくる。以下に全リストを示す。


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.
n01403.jpg

実行して、Button1 を押し、次に Button2 を押すと右図のようになる。これは、これまでと同じである。


n01404.jpg

つぎに Button3 を押してから Button4 を押すと右図のようになる。文字列の受け渡しはストリームに読み書きするのが簡単なのですぐにできるだろう。


n01405.jpg

最後に、可変長のレコード型の配列の例として Button5 でコピーし、Button6 でペーストする例を示した。これは、最初に要素の数を書き込み、TWriter で書き込み、TReader で読み込むので少し面倒である。


このテスト例では、一つの TCopyPaste のインスタンスで三つのデータフォーマットを使い回している。複数の独自データフォーマットを使うときには、このように使い回すこともできるし、フォーマットごとに TCopyPaste のインスタンスを一つずつ割り当ててもよい。各インスタンスに一つしか独自フォーマットが無いときは、オブジェクトインスペクタで DataName プロパティーに適当な文字列を設定しておくと便利である。


以上のように、任意のメモリブロックをクリップボード経由で簡単にやりとりすることができた。

redball.gif目次へ戻る