エクスプローラでファイルを選んでから右クリックすると右図のようにコンテキストメニューを表示する。例えば、テキストファイルを一つ選んで、コンテキストメニューから「コピー」を選んでも、メモ帳にもテキストエディタにも、「貼り付け」メニュー項目がグレイ表示されていて、貼り付けることができない。では、この場合の「コピー」とは何を意味しているのだろうか。
ワードには右図のように貼り付けることができる。この場合、「パッケージオブジェクト」として貼り付けられるだけで、テキストファイルの内容が表示されるわけではない。ここでは、「エクスプローラからのコピー」とはどんなことなのかを調べる。
最初に、「エクスプローラからのコピー」したとき、クリップボードにどんな形式で格納されているかを調べよう。
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;
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;
これから推測すると 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;
Windows2000 での実行結果は上図のようになる。どうやら、選択したファイル名を取得できたらしい。Windows2000 では、ファイル名を表す文字列が WideString になっていて、したがって、複数のファイル名がある場合は、ファイル名間の区切りは、WideString のヌル文字一つ分、つまり、#0#0 となっているようだ。また、最後は、ヌル文字二つ分、つまり、#0#0#0#0 となっているようだ。このプログラムをフロッピーディスクにコピーして、WindowsMe で実行した結果を以下に示す。
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;
予めメモ帳を一つ起動しておいて、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;
すでに起動しておいた EmEditor に上記のプログラムで WM_DROPFILES を送ると、右図のように複数のファイルを同時に開くことができた。
以上、ここではクリップボードの CF_HDROP 形式の内容を調べ、それを応用して、いくつかのアプリケーションにメッセージを送ってドキュメントファイルを開かせることに成功した。これを応用すると、文字列のリストからドラグドロップで他のコントロールに文字列リストを渡すような独自のコードを書けるかもしれない。