エクスプローラからのコピーについて


redball.gif目次へ戻る

n02206.gif

エクスプローラでファイルを選んでから右クリックすると右図のようにコンテキストメニューを表示する。例えば、テキストファイルを一つ選んで、コンテキストメニューから「コピー」を選んでも、メモ帳にもテキストエディタにも、「貼り付け」メニュー項目がグレイ表示されていて、貼り付けることができない。では、この場合の「コピー」とは何を意味しているのだろうか。



n02205.gif

ワードには右図のように貼り付けることができる。この場合、「パッケージオブジェクト」として貼り付けられるだけで、テキストファイルの内容が表示されるわけではない。ここでは、「エクスプローラからのコピー」とはどんなことなのかを調べる。



最初に、「エクスプローラからのコピー」したとき、クリップボードにどんな形式で格納されているかを調べよう。

List 1

uses
  Clipbrd;

procedure TForm1.Button1Click(Sender: TObject);
var
  i,len:integer;
  PC:PChar;
  s:string;
begin
  ListBox1.Clear;
  GetMem(PC,100);
  try
    for i := 0 to Clipboard.FormatCount-1 do begin
      len := GetClipboardFormatName(Clipboard.Formats[i],PC,100);
      if len = 0 then
        s := IntToStr(Clipboard.Formats[i])
      else
        SetString(s,PC,len);

      ListBox1.Items.Add(s);
    end;
  finally
    FreeMem(PC);
  end;
end;
n02204.gif

Windows2000 で実行すると右図のようになる。



GetClipboardFormatName() は、ウィンドウズが定めている標準形式の場合は文字列を返さないので、この場合は 15 が唯一の標準形式であることが分かる。Windows.pas には以下の定義がある。

const
  CF_HDROP = 15;

この CF_HDROP が何を意味しているのかを調べるために、Delphi5 のソースをグレップしてみると、ShlObj.pas に以下の宣言が見つかった。

{ format of CF_HDROP and CF_PRINTERS, in the HDROP case the data that follows
  is a double null terinated list of file names, for printers they are printer
  friendly names }

  PDropFiles = ^TDropFiles;
  _DROPFILES = record
    pFiles: DWORD;                       { offset of file list }
    pt: TPoint;                          { drop point (client coords) }
    fNC: BOOL;                           { is it on NonClient area }
                                         { and pt is in screen coords }
    fWide: BOOL;                         { WIDE character switch }
  end;
  TDropFiles = _DROPFILES;
  DROPFILES = _DROPFILES;
n02201.gif

これから推測すると CF_HDROP のクリップボード形式の内容は、先頭に TDropFiles 型のレコードがあり、その後にファイル名の文字列が続くらしい。そして、最後にヌル文字が二つ続いて終わるらしい。右図のように選択した場合、これを確かめてみよう。


List 2

uses
  Clipbrd, ShlObj;

function WideStrLen(WideStr:PWideChar):Cardinal;
begin
  result := lstrlenW(WideStr);
end;

procedure TForm1.GetFilesW(MS:TMemoryStream; offset:Cardinal; Files:TStrings);
var
  ws:WideString;
  p:pointer;
  pEnd:Cardinal;
begin
  pEnd := Cardinal(MS.Memory)+Cardinal(MS.Size);
  p := pointer(Cardinal(MS.Memory)+offset);
  repeat
    SetString(ws,PWideChar(p),WideStrLen(p));
    Files.Add(ws);
    p := pointer(integer(p)+Length(ws)*2+2);
  until (Cardinal(p) >= pEnd) or (PWord(p)^ = 0);
end;

procedure TForm1.GetFilesA(MS:TMemoryStream; offset:Cardinal; Files:TStrings);
var
  s:string;
  p:pointer;
  pEnd:Cardinal;
begin
  pEnd := Cardinal(MS.Memory)+Cardinal(MS.Size);
  p := pointer(Cardinal(MS.Memory)+offset);
  repeat
    SetString(s,PChar(p),StrLen(PChar(p)));
    Files.Add(s);
    p := pointer(integer(p)+Length(s)+1);
  until (Cardinal(p) >= pEnd) or (PByte(p)^ = 0);
end;

procedure TForm1.Button1Click(Sender: TObject);
var
  MS:TMemoryStream;
  hBuf:THandle;
  DF:TDropFiles;
  p:pointer;
begin
  MS := TMemoryStream.Create;
  try
    if Clipboard.HasFormat(CF_HDROP) then begin
      ClipBoard.Open;
      hBuf := Clipboard.GetAsHandle(CF_HDROP);
      p := GlobalLock(hBuf);
      MS.WriteBuffer(p^,GlobalSize(hBuf));
      GlobalUnlock(hBuf);
      Clipboard.Close;

      MS.Position := 0;
      MS.Read(DF,SizeOf(TDropFiles));
      ListBox1.Items.Add(Format('Offset = %d',[DF.pFiles]));
      if DF.fNC then
        ListBox1.Items.Add('NC')
      else
        ListBox1.Items.Add('non NC');

      if DF.fWide then begin
        ListBox1.Items.Add('Wide');
        GetFilesW(MS,DF.pFiles,ListBox1.Items);
      end else begin
        ListBox1.Items.Add('non Wide');
        GetFilesA(MS,DF.pFiles,ListBox1.Items);
      end;
    end;
  finally
    MS.Free;
  end;
end;
n02202.gif

Windows2000 での実行結果は上図のようになる。どうやら、選択したファイル名を取得できたらしい。Windows2000 では、ファイル名を表す文字列が WideString になっていて、したがって、複数のファイル名がある場合は、ファイル名間の区切りは、WideString のヌル文字一つ分、つまり、#0#0 となっているようだ。また、最後は、ヌル文字二つ分、つまり、#0#0#0#0 となっているようだ。このプログラムをフロッピーディスクにコピーして、WindowsMe で実行した結果を以下に示す。

n02203.gif

WindowsMe の場合は、普通の AnsiString になっていて GetFilesA() の方が実行される。以上のように、CF_HDROP のクリップボード形式の内容が判明した。

それでは、なぜ、例えば CF_FILESCOPY ではなく、CF_HDROP なのであろうか。ここの、ドラグドロップでファイル名を取得 でやったように、ひょっとして、エクスプローラなどのファイラからドラグドロップでファイル名を受け取る場合も、OSレベルで同じ内容が渡されているのではないだろうか。これを確かめるために、以下のプログラムを作った。

List 3

uses
  Clipbrd, ShlObj;

procedure TForm1.Button1Click(Sender: TObject);
var
  hMemo:HWND;
  DF:TDropFiles;
  s:string;
  hDrop:THandle;
  p:pointer;
begin
  hMemo := FindWindow('Notepad',nil);
  if hMemo = 0 then exit;

  DF.pFiles := SizeOf(TDropFiles);
  DF.fNC := false;
  DF.fWide := false;
  DF.pt := Point(0,0);

  s := Edit1.Text+#0;
  hDrop := GlobalAlloc(GHND,SizeOf(TDropFiles)+Length(s)+1);
  p := GlobalLock(hDrop);
  Move(DF,p^,SizeOf(TDropFiles));
  Inc(PByte(p),SizeOf(TDropFiles));
  Move(PChar(s)^,p^,Length(s)+1);
  GlobalUnlock(hDrop);

  PostMessage(hMemo,WM_DROPFILES,WPARAM(hDrop),0);

  GlobalFree(hDrop);
end;

procedure TForm1.Button2Click(Sender: TObject);
var
  hMemo:HWND;
  DF:TDropFiles;
  ws:WideString;
  hDrop:THandle;
  p:pointer;
begin
  hMemo := FindWindow('Notepad',nil);
  if hMemo = 0 then exit;

  DF.pFiles := SizeOf(TDropFiles);
  DF.fNC := false;
  DF.fWide := true;
  DF.pt := Point(0,0);

  ws := Edit1.Text+#0#0;
  hDrop := GlobalAlloc(GHND,SizeOf(TDropFiles)+Length(ws)*2+2);
  p := GlobalLock(hDrop);
  Move(DF,p^,SizeOf(TDropFiles));
  Inc(PByte(p),SizeOf(TDropFiles));
  Move(PWideChar(ws)^,p^,Length(ws)*2+2);
  GlobalUnlock(hDrop);

  PostMessage(hMemo,WM_DROPFILES,WPARAM(hDrop),0);

  GlobalFree(hDrop);
end;
n02207.gif

予めメモ帳を一つ起動しておいて、Button1 を押すと、右図のように、メモ帳に WM_DROPFILES メッセージを通して送ったファイルの内容が表示される。これから分かったことは、WM_DROPFILES メッセージで渡されるドロップハンドルは、上記の内容を含むメモリのハンドルだったことである。ここでは、PostMessage()で成功しているが、SendMessage() では失敗する。また、メモリの内容はOSレベルでコピーされるようなので、GlobalFree() で破棄しても大丈夫なようだ。Windows2000 では、ファイル名が AnsiString で渡す Button1Click() でも、WideString で渡す Button2Click() でも同じようにうまくいく。しかし、WindowsMe では、AnsiString で渡す Button1Click() のときだけうまくいくことが分かった。すべてのバージョンのウィンドウズで成功するためには、現状では AnsiString 版をつかったほうがよいだろう。同様にして、ワードパッド(クラス名は 'WordPadClass' )でもうまくいく。


メモ帳は、複数のファイルをエクスプローラからドラグドロップしても、そのうち一つしか開かない。わたしがつかっている EmEditor (フリー版)や秀丸エディタなどのメジャーなエディタは、ファイルの数だけインスタンスを作ってすべてのファイルを開く。タブエディタでは同様に、ファイルの数と同じタブページを作って開く。ここでは WM_DROPFILES メッセージを送って、これを試してみよう。

List 4

uses
  Clipbrd, ShlObj;

procedure TForm1.Button1Click(Sender: TObject);
var
  hMemo:HWND;
  DF:TDropFiles;
  s:string;
  hDrop:THandle;
  p:pointer;
  i:integer;
begin
  hMemo := FindWindow('EmEditorMainFrame',nil);
  if hMemo = 0 then exit;

  DF.pFiles := SizeOf(TDropFiles);
  DF.fNC := false;
  DF.fWide := false;
  DF.pt := Point(0,0);

  if Memo1.Lines.Count = 0 then exit;
  s := '';
  for i := 0 to Memo1.Lines.Count-1 do
    s := s+Memo1.Lines[i]+#0;
  hDrop := GlobalAlloc(GHND,SizeOf(TDropFiles)+Length(s)+1);
  p := GlobalLock(hDrop);
  Move(DF,p^,SizeOf(TDropFiles));
  Inc(PByte(p),SizeOf(TDropFiles));
  Move(PChar(s)^,p^,Length(s)+1);
  GlobalUnlock(hDrop);

  PostMessage(hMemo,WM_DROPFILES,WPARAM(hDrop),0);

  GlobalFree(hDrop);
end;
n02208.gif

すでに起動しておいた EmEditor に上記のプログラムで WM_DROPFILES を送ると、右図のように複数のファイルを同時に開くことができた。




以上、ここではクリップボードの CF_HDROP 形式の内容を調べ、それを応用して、いくつかのアプリケーションにメッセージを送ってドキュメントファイルを開かせることに成功した。これを応用すると、文字列のリストからドラグドロップで他のコントロールに文字列リストを渡すような独自のコードを書けるかもしれない。

redball.gif目次へ戻る