Delphi Programming / Object Pascal
サイトのトップページへ リンクのページへ ロゴマーク
[掲載 2011年01月16日] [更新 2022年10月23日] Delphi サンプルプログラム集
   
823_貼り付け操作可否のチェック ( クリップボードの監視 )
動作確認等 Windows 7 U64(SP1) + Delphi XE(UP1) Pro
一部,Windows 10 ビルド 19043 Pro 64 bit + Delphi XE5(UP2) Pro VCL-32
823_PasteCheck.zip [4.5 MB] 2022年10月23日版 (EXE 同梱)







貼り付け操作可否のチェック - クリップボードの監視


アプリケーションで [貼り付け] の操作を実装したいことがあります.Clipbrd.pas,ActiveX.pas 内の関数類を使用すると,[貼り付け] の機能を実装することができます.また,関連するインターフェイスを実装すれば,[821_アプリ間のドラッグ・ドロップ] のような機能も実現できます.
[貼り付け] ボタンをアプリケーションに配置したとします.クリップボードに [貼り付け] 可能なデータがある時だけ [貼り付け] ボタンが使用可能な状態となっている状態が望ましいでしょう.そのためにはクリップボードの内容を監視する必要があります.クリップボードの監視は,以下のような手順で行うことになります.
  • (1) SetClipboardViewer 関数で,自アプリ等をクリップボードビューワとして登録
  • (2) WM_DRAWCLIPBOARD メッセージでクリップボードの変更を受信
  • (3) WM_CHANGECBCHAIN メッセージでビューワからの削除等を監視
  • (4) 終了時は ChangeClipboardChain 関数でビューワとしての登録を解除
下の表は,クリップボードの監視で検出可能なクリップボードのフォーマットで,2011年01月16日現在, MSDN の記事に書いてある全ての値です.実際にクリップボードからデータを取り出す方法は,データの種類によって異なります.それらの方法,手順は,ネット上に情報があります.

このページのプログラムは,Windows Vista 以降の場合,IDE 上 (管理者権限) で実行すると正常に動作しないことがあります.そのような場合は,作成した EXE を直接実行してみてください.


[備考]
Windows Vista 以降では,一般ユーザ (標準ユーザ) がコピーしたクリップボードの内容は,管理者権限で起動しているアプリケーションには貼り付けできません.これは UIPI による制限です.


表1
標準クリップボードフォーマット
Delphi 2010 の Windows.pas で定義されているもの
クリップボードのデータの種類は Standard Clipboard Formats (MSDN) の記事による

CF_BITMAP $0002 デバイス依存のビットマップハンドル (HBITMAP) を持つデータ
CF_DIB $0008 デバイス非依存のビットマップデータ
CF_DIBV5 $0011 BITMAPV5HEADER 構造体を持つデバイス非依存のビットマップデータ
CF_DIF $0015 ソフトウェアアート社のデータ交換フォーマット
CF_DSPBITMAP $0082 プライベート (独自) 形式のビットマップデータ
CF_DSPENHMETAFILE $008E プライベート (独自) 形式の拡張メタファイルデータ
CF_DSPMETAFILEPICT $0083 プライベート (独自) 形式のメタファイルデータ
CF_DSPTEXT $0081 プライベート (独自) 形式のテキストデータ
CF_ENHMETAFILE $000E 拡張メタファイル
CF_GDIOBJFIRST $0300 アプリケーション定義 GDI オブジェクトの整数値の開始
範囲の終わりは CF_GDIOBJLAST
CF_GDIOBJLAST $03FF CF_GDIOBJFIRST 参照
CF_HDROP $000F ファイルのリスト
DragQueryFile 関数でファイルに関する情報を取得する
CF_LOCALE $0010 データはクリップボード内のテキストに関連付けられているロケール識別子へのハンドル
CF_METAFILEPICT $0003 メタファイル画像形式
METAFILEPICT 構造体にデータが格納される
CF_OEMTEXT $0007 データは OEM 文字の文字を含むテキスト形式
CF_OWNERDISPLAY $0080 クリップボードの所有者が表示するオーナー表示形式
CF_PALETTE $0009 データはカラーパレットのハンドル
CF_PENDATA $000A ペンコンピューティング用
ペン拡張機能のためのデータ
CF_PRIVATEFIRST $0200 プライベートクリップボード形式の整数値の範囲の開始
範囲は CF_PRIVATELAST で終了する
CF_PRIVATELAST $02FF CF_PRIVATEFIRST参照
CF_RIFF $000B RIFF 形式のオーディオデータ
CF_SYLK $0044 マイクロソフトシンボリックリンク (SYLK) 形式のデータ
CF_TEXT $0001 テキスト形式のデータ
CF_TIFF $0006 TIFF 形式の画像データ
CF_UNICODETEXT $000D Unicode のテキスト形式
CF_WAVE $000C 11 kHz または 22 kHz の PCM のような標準的な WAVE 形式オーディオデータ




01_各種コピー操作によるクリップボードのフォーマット


[貼り付け] は,作成するアプリケーションによって,画像あるいはテキストの場合もあるでしょう.そのため,上の表のどのフォーマットが現在使用可能であるかを検出することになります.
このテストプログラムでは,[コピー] 操作で,どんなフォーマットのデータがクリップボードに格納されるかを確認します.対象のフォーマットが格納された場合に [貼り付け] ボタンを使用可能にすればいいことになります.


図1
設計時画面
図2
エクスプローラでファイルを [コピー]
図3
ビットマップ画像を [コピー]
図4
メタファイル画像を [コピー]
図5
ワードアートを [コピー]

リスト1
直前の [コピー] 操作で,クリップボードに格納されるフォーマットを調べるテスト
クリックボードのデータ取得のサンプルではないので注意
unit Unit1;

interface

uses
  Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms,
  Dialogs, StdCtrls, ComCtrls, ExtCtrls;

type
  TForm1 = class(TForm)
    Panel1: TPanel;
    Button1: TButton;
    ListBox1: TListBox;
    procedure Button1Click(Sender: TObject);
    procedure FormCreate(Sender: TObject);
    procedure FormDestroy(Sender: TObject);
  private
    { Private 宣言 }
    NextWindow : HWND;
    procedure WMChangeCBChain(var Message: TWMChangeCBChain); message
      WM_CHANGECBCHAIN;
    procedure WMDrawClipboard(var Message: TWMDrawClipboard); message
      WM_DRAWCLIPBOARD;
  public
    { Public 宣言 }
  end;

var
  Form1: TForm1;

implementation

uses Clipbrd;

{$R *.dfm}

//=============================================================================
//  このフォームをクリップボードビューワとして登録
//  NextWindowは次のクリップボードビューワウィンドウのハンドル
//=============================================================================
procedure TForm1.FormCreate(Sender: TObject);
begin
  NextWindow := SetClipboardViewer(Handle);
end;

//=============================================================================
//  クリップボードビューワとしての登録を解除
//  クリップボード監視のチェインから外す
//=============================================================================
procedure TForm1.FormDestroy(Sender: TObject);
begin
  ChangeClipboardChain(Handle, NextWindow);
end;

//=============================================================================
//  クリップボードを空にする
//  単なる動作確認用
//=============================================================================
procedure TForm1.Button1Click(Sender: TObject);
begin
  Clipboard.Clear;
end;

//=============================================================================
//  ウィンドウがクリップボードチェインから削除されようとしている時に,チェイン
//  のウィンドウに送られてくるメッセージ
//
//  Remove : チェインから削除されるウィンドウのハンドル
//  Nesxt  : チェインから削除される次のウィンドウのハンドル
//
//  チェイン内の次のウィンドウが削除されようとしている場合は,チェイン内の次の
//  ウィンドウとして,Nextの値を保存する必要がある
//  チェイン内の次のウィンドウが削除されるウィンドウでない場合,チェインの次の
//  ウィンドウにメッセージをSendMessageへ渡さなければならない
//=============================================================================
procedure TForm1.WMChangeCBChain(var Message: TWMChangeCBChain);
begin
  if (Message.Remove = NextWindow) then begin
    NextWindow := Message.Next;
    Message.Result := 0;
  end else begin
    SendMessage(NextWindow, WM_CHANGECBCHAIN, Message.Remove, Message.Next);
  end;
end;

//=============================================================================
//  クリップボード更新フック
//  クリップボードの内容に更新がある時に,クリップボードチェインのウィンドウに
//  送られてくるメッセージ
//  SetClipboradViewer関数で,クリップボードチェインに登録したウィンドウだけが
//  このメッセージを受信できる
//  このメッセージを受信したウィンドウは,クリップボードチェインの次のウィンド
//  ウにこのメッセージを送信し,全てのビューワウィンドウにメッセージが届くよう
//  にしなければならない
//
//  本サンプルでは,アプリ起動時にもこのメッセージを受信
//=============================================================================
procedure TForm1.WMDrawClipboard(var Message: TWMDrawClipboard);
const
  CF_DIBV5 = 17; //Delphi 7以降では不要
begin
  if not OpenClipboard(Handle) then Exit;

  try
    ListBox1.Items.BeginUpdate;
    ListBox1.Items.Clear;

    //メッセージが発生した日時を表示
    ListBox1.Items.Add(FormatDateTime('yyyy/mm/dd hh:nn:ss', Now));
    ListBox1.Items.Add('');

    //クリップボードが空になった
    if Clipboard.FormatCount = 0 then begin
      ListBox1.Items.Add('Data Empty');
    end else begin
      with ListBox1.Items do begin
        if Clipboard.HasFormat(CF_BITMAP)          then Add('CF_BITMAP');
        if Clipboard.HasFormat(CF_DIB)             then Add('CF_DIB');
        if Clipboard.HasFormat(CF_DIBV5)           then Add('CF_DIBV5');
        if Clipboard.HasFormat(CF_DIF)             then Add('CF_DIF');
        if Clipboard.HasFormat(CF_DSPBITMAP)       then Add('CF_DSPBITMAP');
        if Clipboard.HasFormat(CF_DSPENHMETAFILE)  then Add('CF_DSPENHMETAFILE');
        if Clipboard.HasFormat(CF_DSPMETAFILEPICT) then Add('CF_DSPMETAFILEPICT');
        if Clipboard.HasFormat(CF_DSPTEXT)         then Add('CF_DSPTEXT');
        if Clipboard.HasFormat(CF_ENHMETAFILE)     then Add('CF_ENHMETAFILE');
        if Clipboard.HasFormat(CF_GDIOBJFIRST)     then Add('CF_GDIOBJFIRST');
        if Clipboard.HasFormat(CF_GDIOBJLAST)      then Add('CF_GDIOBJLAST');
        if Clipboard.HasFormat(CF_HDROP)           then Add('CF_HDROP');
        if Clipboard.HasFormat(CF_LOCALE)          then Add('CF_LOCALE');
        if Clipboard.HasFormat(CF_METAFILEPICT)    then Add('CF_METAFILEPICT');
        if Clipboard.HasFormat(CF_OEMTEXT)         then Add('CF_OEMTEXT');
        if Clipboard.HasFormat(CF_OWNERDISPLAY)    then Add('CF_OWNERDISPLAY');
        if Clipboard.HasFormat(CF_PALETTE)         then Add('CF_PALETTE');
        if Clipboard.HasFormat(CF_PENDATA)         then Add('CF_PENDATA');
        if Clipboard.HasFormat(CF_PRIVATEFIRST)    then Add('CF_PRIVATEFIRST');
        if Clipboard.HasFormat(CF_PRIVATELAST)     then Add('CF_PRIVATELAST');
        if Clipboard.HasFormat(CF_RIFF)            then Add('CF_RIFF');
        if Clipboard.HasFormat(CF_SYLK)            then Add('CF_SYLK');
        if Clipboard.HasFormat(CF_TEXT)            then Add('CF_TEXT');
        if Clipboard.HasFormat(CF_TIFF)            then Add('CF_TIFF');
        if Clipboard.HasFormat(CF_UNICODETEXT)     then Add('CF_UNICODETEXT');
        if Clipboard.HasFormat(CF_WAVE)            then Add('CF_WAVE');
      end;
    end;
    ListBox1.Items.EndUpdate;
  finally
    CloseClipboard;
  end;

  //次のクリップボードビューワウィンドウにメッセージを送る
  SendMessage(NextWindow, WM_DRAWCLIPBOARD, 0, 0);
end;

end.


上のリストの CF_TEXT に相当するテキストをクリップボードから取得するために,この部分を以下のようにすると,
      「クリップボード アクセスが拒否されました。 を開けません.」
というエラーが発生して,取得できません.


        if Clipboard.HasFormat(CF_TEXT) then S := Clipboard.AsText;



図6
アクセス拒否 ?

既に述べたように,実際にクリップボードからデータを取り出す手順,方法はデータの種類によって異なります.実際にクリップボードからデータを取得するコードは,アプリケーションの [貼り付け] ボタン等を使用することになるでしょう
Delphi の TClipboard を使用して WM_DRAWCLIPBOARD メッセージ内でクリップボードのテキストを取得するには,例えば次のようなコードで取得できます.データを取得するコードは,プログラムのどの位置で取得するかによっても異なることがあります.


リスト2
クリップボードのテキストを,TClipboard を使用して取得する
procedure TForm1.WMDrawClipboard(var Message: TWMDrawClipboard);
var
  LhClipboard : THandle;
begin
  ClipBoard.Open;
  if Clipboard.HasFormat(CF_TEXT) then begin
    LhClipboard := Clipboard.GetAsHandle(CF_TEXT);
    try
      Memo1.Text := Clipboard.AsText;
    finally
      GlobalUnlock(LhClipboard);
    end;
  end;
  Clipboard.Close;

  //次のクリップボードビューワウィンドウにメッセージを送る
  SendMessage(NextWindow, WM_DRAWCLIPBOARD, 0, 0);
end;




02_コンポーネントを利用したテスト


クリップボードの監視は,上のようなコードをアプリケーションのコードに追加してもいいのですが,Delphi の利点であるコンポーネントを作成して利用すると使い勝手がよくなります.ネット上には既にいくつかコンポーネント提供されています.これを利用したテストです.
このテストでは,中村の里にある,中村拓男さん作の TNkClipDetector を使用してみました.このコンポーネントは最終更新が 1997年5月8日となっていますが,十分使用できます.これもまた Delphi の特徴の1つです.


図7
設計時画面
  • 以下を配置
    TPanel (この上にボタン類を配置)
    TPanel (この上に TImage を配置)
    TSplitter
    TRichEdit
    TNkClipDetector

図8
テストプログラム起動
  • クリップボードに何もない場合
図9
実行例
  • 当サイトのロゴ画像を,ブラウザ上で [画像のコピー] を実行し,[画像貼り付け] ボタンをクリックした結果

リスト3
クリップボードの監視テスト
unit Unit1;

interface

uses
  Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms,
  Dialogs, StdCtrls, ComCtrls, ExtCtrls, NkClipDetector;

type
  TForm1 = class(TForm)
    Panel1: TPanel;
    Panel2: TPanel;
    Splitter1: TSplitter;
    RichEdit1: TRichEdit;
    Image1: TImage;
    Button1: TButton;
    Button2: TButton;
    Button3: TButton;
    NkClipDetector1: TNkClipDetector;
    procedure Button1Click(Sender: TObject);
    procedure Button2Click(Sender: TObject);
    procedure Button3Click(Sender: TObject);
    procedure NkClipDetector1ClipChange(Sender: TObject);
    procedure FormCreate(Sender: TObject);
  private
    { Private 宣言 }
  public
    { Public 宣言 }
  end;

var
  Form1: TForm1;

implementation

uses Clipbrd;

{$R *.dfm}

//=============================================================================
//  画像は左上隅から表示するものとする
//  TNkClipDetectorのIgnoreFirstをオブジェクト・インスペクタでFalseにしておく
//=============================================================================
procedure TForm1.FormCreate(Sender: TObject);
begin
  Image1.Left := 0;
  Image1.Top  := 0;

  Image1.Transparent := True;
  Image1.AutoSize    := True;

  RichEdit1.ScrollBars := ssBoth;
end;

//=============================================================================
//  [画像の貼り付け]ボタン
//=============================================================================
procedure TForm1.Button1Click(Sender: TObject);
begin
  Image1.Picture.Assign(Clipboard);
end;

//=============================================================================
//  [テキストの貼り付け]ボタン
//=============================================================================
procedure TForm1.Button2Click(Sender: TObject);
begin
  //現在のキャレット(カーソル)位置から文字列を挿入
  RichEdit1.SelText := Clipboard.AsText;
end;

//=============================================================================
//  [クリア]ボタン
//  クリップボードのクリア.ついでに表示中の画像とテキストもクリア
//=============================================================================
procedure TForm1.Button3Click(Sender: TObject);
begin
  Clipboard.Clear;
  Image1.Picture.Assign(nil);
  RichEdit1.Lines.Clear;
end;

//=============================================================================
//  TNkClipDetectorのOnClipChangeイベント
//  クリップボードの監視イベント
//
//  Button1は[画像貼り付け]
//  Button2は[テキスト貼り付け]
//  Button3はクリップボードのクリア
//=============================================================================
procedure TForm1.NkClipDetector1ClipChange(Sender: TObject);
begin
  Button1.Enabled := Clipboard.HasFormat(CF_BITMAP) or
                     Clipboard.HasFormat(CF_ENHMETAFILE);
  Button2.Enabled := Clipboard.HasFormat(CF_TEXT);

  Button3.Enabled := Clipboard.FormatCount > 0;
end;

end.




03_エクスプローラでコピーしたファイルのリスト


エクスプローラで選択したフォルダ,ファイルに対して [コピー] を実行すると,クリップボードのフォーマットは CF_HDROP となりますが,この時クリップボードにはコピーしたファイルのリストが格納されます.このファイルリストは,DragQueryFile 関数を使用して取得できます.
このサンプルは,エクスプローラで [コピー] を実行すると,直ちにファイルリストを表示します.

このサンプルコードは,自分のファイルリスト表示用プログラムに,エクスプローラで [コピー] したファイルを追加表示する用途に利用できます.


図10
コピーしたファイルのリスト

リスト4
クリップボードにコピーされたファイルリストを表示
unit Unit1;

interface

uses
  Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms,
  Dialogs, StdCtrls, ComCtrls, ExtCtrls, NkClipDetector;

type
  TForm1 = class(TForm)
    Panel1: TPanel;
    RichEdit1: TRichEdit;
    Button1: TButton;
    NkClipDetector1: TNkClipDetector;
    procedure Button1Click(Sender: TObject);
    procedure NkClipDetector1ClipChange(Sender: TObject);
    procedure FormCreate(Sender: TObject);
  private
    { Private 宣言 }
  public
    { Public 宣言 }
  end;

var
  Form1: TForm1;

implementation

uses
  Clipbrd,
  ShellAPI,
  ShlObj,
  ActiveX;

{$R *.dfm}

//=============================================================================
//  ファイルリストを表示するRichEditの設定
//=============================================================================
procedure TForm1.FormCreate(Sender: TObject);
begin
  RichEdit1.ScrollBars := ssBoth;
end;

//=============================================================================
//  [クリア]ボタン
//=============================================================================
procedure TForm1.Button1Click(Sender: TObject);
begin
  Clipboard.Clear;
  RichEdit1.Lines.Clear;
end;

//=============================================================================
//  TNkClipDetectorのOnClipChangeイベント
//  クリップボードの監視イベント
//  エクスプローラでファイルを[コピー]したら直ちにそのリストを表示
//=============================================================================
procedure TForm1.NkClipDetector1ClipChange(Sender: TObject);
var
  CF_DROPEFFECT : UINT;
  hMem          : Pointer;
  LdwEffect     : Word;
  LhDrop        : THandle;
  LCount        : Integer;
  LBuff         : array [0..MAX_PATH - 1] of Char;
  i             : Integer;
begin
  RichEdit1.Lines.Clear;
  RichEdit1.Lines.BeginUpdate;


  //---------------------------------------------------------------------------
  //  エクスプローラでの操作種類を検出
  //  コピー,切り取り,移動等の検出が不要ならこの部分は不要
  //---------------------------------------------------------------------------
  LdwEffect     := 0;
  CF_DROPEFFECT := RegisterClipboardFormat(CFSTR_PREFERREDDROPEFFECT);
  if Clipboard.HasFormat(CF_DROPEFFECT) then begin
    LhDrop := Clipboard.GetAsHandle(CF_DROPEFFECT);
    if LhDrop <> 0 then begin
      hMem := Windows.GlobalLock(LhDrop);
      try
        LdwEffect := PWord(hMem)^;
      finally
        GlobalUnlock(LhDrop);
      end;
    end;
    if (LdwEffect and DROPEFFECT_NONE)   > 0 then RichEdit1.Lines.Add('NONE');
    if (LdwEffect and DROPEFFECT_COPY)   > 0 then RichEdit1.Lines.Add('COPY');
    if (LdwEffect and DROPEFFECT_MOVE)   > 0 then RichEdit1.Lines.Add('MOVE');
    if (LdwEffect and DROPEFFECT_LINK)   > 0 then RichEdit1.Lines.Add('LINK');
    if (LdwEffect and DROPEFFECT_SCROLL) > 0 then RichEdit1.Lines.Add('SCROLL');
  end;
  //---------------------------------------------------------------------------


  if Clipboard.HasFormat(CF_HDROP) then begin
    //CF_HDROP内部構造体のハンドルを取得
    LhDrop  := Clipboard.GetAsHandle(CF_HDROP);
    LCount := DragQueryFile(LhDrop, UINT(-1), nil, 0);

    RichEdit1.Lines.Add('コピーファイル数 : ' + IntToStr(LCount));

    for i := 0 to LCount - 1 do begin
      DragQueryFile(LhDrop, i, LBuff, Length(LBuff));
      //ファイル名をリストに追加(フォルダも含まれる)
      RichEdit1.Lines.Add(LBuff);
    end;
  end;

  RichEdit1.Lines.EndUpdate;
end;

end.




04_クリップボードのフォーマット形式の列挙


[01_各種コピー操作によるクリップボードのフォーマット] では標準フォーマットだけ取得して列挙しましたが,クリップボードのフォーマットには,アプリケーション独自のもの,システムが使用するフォーマット形式もあります.これらも列挙して表示するサンプルです.

このサンプルは,クリップボードの監視は行いません.アプリでコピーしたら,[Button1] をクリックして確認するようになっています.
いくつかのアプリケーションで,実際にどのようなフォーマット形式があるを見てみました.
これらのフォーマット形式のデータは,その形式のデータの構造が分からなければ,その内容を取得することはできません.自アプリに,独自のフォーマット形式を実装することもできます.

下のコードで使用しているフォーマット形式は,Delphi XE の Windows.pas で定義されている定数の全てです.


図11
設計時画面
図12
ビットマップ画像のコピー
  • この次のサンプルで画像をコピーした結果
図13
文字列 (テキスト) のコピー
  • この次のサンプルで文字列をコピーした結果
図14
ブラウザ Firefox でテキストをコピー
  • 上と同じテキストであるが,ブラウザの HTML 関係の情報もクリップボードに格納される
図15
Delphi のフォームのコピー
  • 開発環境の状態 (IDE上) でフォームをコピー
  • Delphi Components (コンポーネント関係の情報) もクリップボードに格納される
図16
ワード 2010 の図形をコピー
  • Art::GVML Clipformat がワードの画像や図形をクリップボードに格納するフォーマット形式
図17
エクセル 2010 のシートをコピー
  • エクセルのシートにはいろいろな情報があるので,フォーマットの形式も多い
  • Biff12 は Excel 2007 以降のファイル形式
  • Biff8 は Excel97 から Excel 2003 のファイル形式
  • Biff5 は Excel95 のファイル形式
  • つまり Excel 2010 はどのファイル形式も扱える
図18
エクセル 2010 の画像をコピー
  • ワードと同じ Art::GVML Clipformat がある

次の図は,Halbow 資料館の [クリップボードで任意のデータを受け渡す] のコンポーネント化したサンプル,List 4 を実行した結果です.
クリップボードのフォーマット形式の名前が HogeHogeRecord となっています.
このテストは,レコード型に格納した複数の可変長のテキストをコピーしていますが,CF_TEXT がありません.これは,全て HogeHogeRecord というフォーマットに格納しているためです.


図19
Halbow 資料館のサンプルを実行

リスト5
クリップボードのフォーマット形式の列挙
unit Unit1;

interface

uses
  Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms,
  Dialogs, StdCtrls;

type
  TForm1 = class(TForm)
    Button1: TButton;
    Memo1: TMemo;
    procedure Button1Click(Sender: TObject);
  private
    { Private 宣言 }
  public
    { Public 宣言 }
  end;

var
  Form1: TForm1;

implementation

uses Clipbrd;

{$R *.dfm}

{ TForm1 }

//=============================================================================
//  クリップボードのフォーマットの形式名を取得して表示
//  GetClipBoardFormatName関数を使用
//=============================================================================
procedure TForm1.Button1Click(Sender: TObject);
var
  i       : Integer;
  FmtID   : Integer;
  FmtName : array [0..MAX_PATH-1] of Char;
begin
  Memo1.Lines.Clear;
  Memo1.Lines.BeginUpdate;

  for i := 0 to ClipBoard.FormatCount - 1 do begin
    FmtID:= ClipBoard.Formats[i];

    FillChar(FmtName, SizeOf(FmtName), #0);
    if GetClipBoardFormatName(FmtID, FmtName,SizeOf(FmtName)) > 0 then begin
      Memo1.Lines.Add(IntToHex(ClipBoard.Formats[i], 4) + '  ' + FmtName);
    end else begin
      case ClipBoard.Formats[i] of
      CF_TEXT:
        Memo1.Lines.Add(IntToHex(ClipBoard.Formats[i], 4) + '  CF_TEXT');
      CF_BITMAP:
        Memo1.Lines.Add(IntToHex(ClipBoard.Formats[i], 4) + '  CF_BITMAP');
      CF_METAFILEPICT:
        Memo1.Lines.Add(IntToHex(ClipBoard.Formats[i], 4) + '  CF_METAFILEPICT');
      CF_SYLK:
        Memo1.Lines.Add(IntToHex(ClipBoard.Formats[i], 4) + '  CF_SYLK');
      CF_DIF:
        Memo1.Lines.Add(IntToHex(ClipBoard.Formats[i], 4) + '  CF_DIF');
      CF_TIFF:
        Memo1.Lines.Add(IntToHex(ClipBoard.Formats[i], 4) + '  CF_TIFF');
      CF_OEMTEXT:
        Memo1.Lines.Add(IntToHex(ClipBoard.Formats[i], 4) + '  CF_OEMTEXT');
      CF_DIB:
        Memo1.Lines.Add(IntToHex(ClipBoard.Formats[i], 4) + '  CF_DIB');
      CF_PALETTE:
        Memo1.Lines.Add(IntToHex(ClipBoard.Formats[i], 4) + '  CF_PALETTE');
      CF_PENDATA:
        Memo1.Lines.Add(IntToHex(ClipBoard.Formats[i], 4) + '  CF_PENDATA');
      CF_RIFF:
        Memo1.Lines.Add(IntToHex(ClipBoard.Formats[i], 4) + '  CF_RIFF');
      CF_WAVE:
        Memo1.Lines.Add(IntToHex(ClipBoard.Formats[i], 4) + '  CF_WAVE');
      CF_UNICODETEXT:
        Memo1.Lines.Add(IntToHex(ClipBoard.Formats[i], 4) + '  CF_UNICODETEXT');
       CF_ENHMETAFILE:
        Memo1.Lines.Add(IntToHex(ClipBoard.Formats[i], 4) + '  CF_ENHMETAFILE');
      CF_HDROP:
        Memo1.Lines.Add(IntToHex(ClipBoard.Formats[i], 4) + '  CF_HDROP');
      CF_LOCALE:
        Memo1.Lines.Add(IntToHex(ClipBoard.Formats[i], 4) + '  CF_LOCALE');
      CF_DIBV5:
        Memo1.Lines.Add(IntToHex(ClipBoard.Formats[i], 4) + '  CF_DIBV5');
      CF_MAX_XP:
        Memo1.Lines.Add(IntToHex(ClipBoard.Formats[i], 4) + '  CF_MAX_XP');
      CF_OWNERDISPLAY:
        Memo1.Lines.Add(IntToHex(ClipBoard.Formats[i], 4) + '  CF_OWNERDISPLAY');
      CF_DSPTEXT:
        Memo1.Lines.Add(IntToHex(ClipBoard.Formats[i], 4) + '  CF_DSPTEXT');
      CF_DSPBITMAP:
        Memo1.Lines.Add(IntToHex(ClipBoard.Formats[i], 4) + '  CF_DSPBITMAP');
      CF_DSPMETAFILEPICT:
        Memo1.Lines.Add(IntToHex(ClipBoard.Formats[i], 4) + '  CF_DSPMETAFILEPIC');
      CF_DSPENHMETAFILE:
        Memo1.Lines.Add(IntToHex(ClipBoard.Formats[i], 4) + '  CF_DSPENHMETAFILE');
      CF_GDIOBJFIRST:
        Memo1.Lines.Add(IntToHex(ClipBoard.Formats[i], 4) + '  CF_GDIOBJFIRST');
      CF_GDIOBJLAST:
        Memo1.Lines.Add(IntToHex(ClipBoard.Formats[i], 4) + '  CF_GDIOBJLAST');
      CF_PRIVATEFIRST:
        Memo1.Lines.Add(IntToHex(ClipBoard.Formats[i], 4) + '  CF_PRIVATEFIRST');
      CF_PRIVATELAST:
        Memo1.Lines.Add(IntToHex(ClipBoard.Formats[i], 4) + '  CF_PRIVATELAST');
      else
        Memo1.Lines.Add(IntToHex(ClipBoard.Formats[i], 4) + '  UnKown');
      end;
    end;
  end;
  Memo1.Lines.EndUpdate;
end;

end.




05_コピーとクリップボードからのデータ取得の参考コード


クリップボードの画像またはテキストデータは,次のように,クリップボードのハンドルを取得してメモリストリームに格納することができます.


リスト6
クリップボードのデータをメモリストリームに格納
procedure TForm1.Button1Click(Sender: TObject);
var
  DataFormat : UINT;
  dataStream : TMemoryStream;
  hBuffer    : THandle;
  pBuffer    : Pointer;
  dwSize     : Cardinal;
begin
  //検出するクリップボードのデータ形式
  DataFormat := CF_BITMAP;

  //その形式だったらクリップボードからdataStreamにデータを格納
  if Clipboard.HasFormat(DataFormat) then begin
    dataStream := TMemoryStream.Create;
    try
      ClipBoard.Open;

      hBuffer := Clipboard.GetAsHandle(DataFormat);
      pBuffer := GlobalLock(hBuffer);
      try
        dwSize := GlobalSize(hBuffer);
        if dwSize > 0 then begin
          dataStream.WriteBuffer(pBuffer^, dwSize);
        end;  
      finally
        GlobalUnlock(hBuffer);
        Clipboard.Close;
      end;
    finally
      FreeAndNil(dataStream);
    end;
  end;
end;


このメモリストリームに格納されたデータは,指定フォーマット形式の情報です.ビットマップ画像の場合,上のコードで取得したデータだけではビットマップ画像としては表示できません.ビットマップヘッダ部の処理が必要です.
Delphi には,クリップボードから画像,テキストを取得する便利な関数やプロパティが実装されています.上の方のサンプルにも使用例がありますが,再掲しておきます.


図20
設計時画面
  • TImage を配置
図21
ワード 2010 の図形のコピーを取得
  • サンプルコードでは,これをビットマップ画像として保存しているが,画像の形式を判定すれば,元の画像形式での保存も可能
  • ワード 2010 の図形はメタファイルとして保存できる
図22
ビットマップ画像のコピー
  • [Button2] をクリックして,ディスク上のビットマップ画像をコピーして,[Button1] クリックで取得して表示
図23
テキストのコピーと取得
  • テキストをコピーして,それをクリップボードから取得して表示した結果
図24
エクセル 2010 のシートのコピーを取得
  • 上のサンプルで確認できるように,シートの画像もクリップボードに格納されている
  • テキストを取得すれば,セルの文字列のみの取得となる

リスト7
コピーと貼り付けの参考コード.クリップボードの監視機能は省略
扱う画像の種類によっては,uses に Jpeg, Gifimg, PngImage 等が必要
unit Unit1;

interface

uses
  Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms,
  Dialogs, StdCtrls, ExtCtrls;

type
  TForm1 = class(TForm)
    Button1: TButton;
    Image1: TImage;
    Button2: TButton;
    Button3: TButton;
    procedure Button1Click(Sender: TObject);
    procedure Button2Click(Sender: TObject);
    procedure Button3Click(Sender: TObject);
  private
    { Private 宣言 }
  public
    { Public 宣言 }
  end;

var
  Form1: TForm1;

implementation

uses Clipbrd, Jpeg, GifImg, PngImage;

{$R *.dfm}

{ TForm1 }

//=============================================================================
//  貼り付け
//  クリップボードから画像を取得
//  このコードは画像の種類を区別しない場合
//=============================================================================
procedure TForm1.Button1Click(Sender: TObject);
var
  Pict         : TPicture;
  DestFilePath : String;
begin
  if (Clipboard.HasFormat(CF_METAFILEPICT)) or
     (Clipboard.HasFormat(CF_ENHMETAFILE))  or
     (Clipboard.HasFormat(CF_METAFILEPICT)) or
     (Clipboard.HasFormat(CF_BITMAP))       or
     (Clipboard.HasFormat(CF_DIB))          then begin
    Pict := TPicture.Create;
    try
      //クリップボードから画像データのみ取得
      Pict.Assign(Clipboard);

      //TImageに表示してビットマップ画像として保存
      Image1.Picture.Assign(Pict);
      DestFilePath := ExtractFilePath(Application.ExeName) + 'image1.bmp';
      Pict.SaveToFile(DestFilePath);
    finally
      FreeAndNil(Pict);
    end;
  end;
end;

//=============================================================================
//  コピー
//  ディスク上の画像ファイルをクリップボードに格納
//  画像のオブジェクトのコピーは,TPictureの直接操作となる
//=============================================================================
procedure TForm1.Button2Click(Sender: TObject);
var
  Pict        : TPicture;
  SrcFilePath : String;
begin
  SrcFilePath := ExtractFilePath(Application.ExeName) + '501.bmp';

  Pict := TPicture.Create;
  try
    Pict.LoadFromFile(SrcFilePath);
    Clipboard.Assign(Pict);
  finally
    FreeAndNil(Pict);
  end;
end;

//=============================================================================
//  文字列(テキスト)のコピーと貼り付け
//=============================================================================
procedure TForm1.Button3Click(Sender: TObject);
var
  strText : String;
begin
  strText := 'クリップボード';

  //コピー
  Clipboard.AsText := strText + sLineBreak + 'テスト';
  strText := '';
  Sleep(300);

  //貼り付け
  if (Clipboard.HasFormat(CF_TEXT)) then begin
    strText := Clipboard.AsText;
    MessageBox(Handle, PChar(strText), '情報', MB_ICONINFORMATION);
  end;
end;

end.


画像等のデータは,クリップボードのハンドルから次のようにして取得することもできます.このコードはビットマップ画像の場合です.


[備考]
このコードでフォーマットに CF_DIB を指定しても,DIB のデータは取得できません.
Delphi の GetAsHandle は単に Windows API の GetClipboardData 関数を実行しているだけです.したがつて,この現象は GetClipboardData 関数の仕様と思われます.


リスト8
GetAsHandle でデータのハンドルからデータを取得する例.ビットマップ画像の場合
procedure TForm1.Button4Click(Sender: TObject);
var
  Bmp : TBitmap;
begin
  if (Clipboard.HasFormat(CF_BITMAP)) then begin
    Bmp := TBitmap.Create;
    try
      Bmp.Handle := Clipboard.GetAsHandle(CF_BITMAP);
      Image1.Picture.Assign(Bmp);
    finally
      FreeAndNil(Bmp);
    end;
  end;
end;




06_TRichEdit のテキストの書式付きコピーと貼り付け


TRichEdit には専用のコピー貼り付けのメソッドがあります.その確認です.
CopyToClipboard, PasteFromClipboard メソッドは,TDBImage,TCustomEdit にもあります.
TRichEdit の書式付のテキストは,TMemo にも貼り付けできますが,書式は失われます.


図25
テキストを選択してコピー
図26
貼り付け

リスト9
TRichEdit のテキストの書式付きコピーと貼り付け
CF_OEMTEXT を検出する.クリップボードの監視機能は省略
unit Unit1;

interface

uses
  Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms,
  Dialogs, StdCtrls, ComCtrls, ExtCtrls, Buttons;

type
  TForm1 = class(TForm)
    Panel1: TPanel;
    RichEdit1: TRichEdit;
    Splitter1: TSplitter;
    RichEdit2: TRichEdit;
    SpeedButton1: TSpeedButton;
    SpeedButton2: TSpeedButton;
    SpeedButton3: TSpeedButton;
    procedure FormCreate(Sender: TObject);
    procedure SpeedButton1Click(Sender: TObject);
    procedure SpeedButton2Click(Sender: TObject);
    procedure SpeedButton3Click(Sender: TObject);
  private
    { Private 宣言 }
  public
    { Public 宣言 }
  end;

var
  Form1: TForm1;

implementation

uses RichEdit, Clipbrd;

{$R *.dfm}

//=============================================================================
//  フォーム生成時の処理
//  リッチテキストファイルの読み込み
//=============================================================================
procedure TForm1.FormCreate(Sender: TObject);
begin
  RichEdit1.Lines.LoadFromFile(ExtractFilePath(Application.ExeName) + 'RichText.RTF');
end;

//=============================================================================
//  コピー
//=============================================================================
procedure TForm1.SpeedButton1Click(Sender: TObject);
begin
  RichEdit1.CopyToClipboard;
end;

//=============================================================================
//  貼り付け
//=============================================================================
procedure TForm1.SpeedButton2Click(Sender: TObject);
begin
  if Clipboard.HasFormat(CF_OEMTEXT) then begin
    RichEdit2.PasteFromClipboard;
  end;
end;

//=============================================================================
//  切り取り
//=============================================================================
procedure TForm1.SpeedButton3Click(Sender: TObject);
begin
  RichEdit1.CutToClipboard;
end;

end.




07_TRichEdit のテキストの書式付きコピーと貼り付け  -  TActionList


上のサンプルでは,クリップボードの監視機能を省略しています.実際のアプリでは,本ページでテストした監視機能を追加することになります.
テキスト (プレインテキストまたは書式付テキスト) だけであれば,TActionList 等を使用することにより,クリップボードの監視を自動で行うことができます.その例です.
設計時にフォームに配置した TActionList で下図のような設定をします.


図27
アクションの追加
  • TActionList コンポをダブルクリック
  • [編集] ダイアログでポップアップメニューを表示
  • [標準アクションの新規作成] を選択
図28
アクションクラスの選択
  • [編集] から以下の 3 つを選択
    TEditCut
    TEditCopy
    TEditPaste
  • [指定可能なアクションクラス] はインストールしているコンポーネントによって異なる
図29
カテゴリ [標準] が追加される

上記の設定をしたら,各々のボタンの Action プロパティの値を,オブジェクトインスペクタで以下のように設定します.
  • SpeedButton1.Action を EditCopy1 に
  • SpeedButton2.Action を EditPaste1 に
  • SpeedButton3.Action を EditCut1 に
すると,ボタンの Caption が下図のように,自動で設定されます.必要に応じて変更します.


[備考]
ボタンのアクセラレーターキーを無効にするには,例えば,Caption の値を [コピー(&C)] から [コピー] に変更します.該当アクションクラス全体で無効にするには,該当クラスの Caption プロパティの値を同じように変更します.


図30
Action プロパティを設定した結果

下図は,作成した EXE の実行例です.
TActionList の各クラスのオブジェクトには,メソッドやイベントも実装されています.各アクションが実行された時の処理を実装することができます.

TActionList の各アクションは,TPopupMenu の Action プロパティに指定できます.アプリケーションの開発では,TActionList を実装して利用すると便利です.




図31
EXE 起動時の画面

図32
テキストを選択
  • [コピー] ボタンが使用可能になる
  • [コピー] ボタンをクリックすると [貼り付け] ボタンが使用可能になる

図33
[貼り付け] の操作結果

リスト10
TRichEdit のテキストの書式付きコピーと貼り付け
TActionList を使用したクリップボードの監視機能付き
unit Unit1;

interface

uses
  Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms,
  Dialogs, StdCtrls, ComCtrls, ExtCtrls, Buttons, StdActns, ActnList;

type
  TForm1 = class(TForm)
    Panel1: TPanel;
    RichEdit1: TRichEdit;
    SpeedButton1: TSpeedButton;
    SpeedButton2: TSpeedButton;
    SpeedButton3: TSpeedButton;
    ActionList1: TActionList;
    EditCut1: TEditCut;
    EditCopy1: TEditCopy;
    EditPaste1: TEditPaste;
    procedure FormCreate(Sender: TObject);
  private
    { Private 宣言 }
  public
    { Public 宣言 }
  end;

var
  Form1: TForm1;

implementation

uses
  Clipbrd;

{$R *.dfm}

//=============================================================================
//  フォーム生成時の処理
//  リッチテキストファイルの読み込み
//=============================================================================
procedure TForm1.FormCreate(Sender: TObject);
begin
  //リッチテキストを読み込んで表示
  RichEdit1.Lines.LoadFromFile(ExtractFilePath(Application.ExeName) + 'RichText.RTF');

  //クリップボードをクリア
  Clipboard.Clear;
end;

end.




08_クリップボードにコピーしたテキストを自動取得


本ページの最初のサンプルコードの下に,テキストをコピーすると自動的にクリップボードからテキストを取得して TMemo に表示するサンプルがあります.
そのサンプルで,Microsoft Edge ブラウザと Google Chrome ブラウザのアドレスバーの URL をコピーすると
      「クリップボード アクセスが拒否されました。 を開けません.」
というエラーが発生します.


図34
クリップボードのアクセス拒否
  • Microsoft Edge または Google Chrome で URL をコピーした場合
  • Firefox では発生しない

そこで,本サンプルでは,ClipBoard.Open の処理を次のようにしています.また,クリップボードからデータを取得したら,本サンプルのフォームを最前面に表示するようにしています.
ClipBoard.Open の処理がある WMDrawClipboard ( WM_DRAWCLIPBOARD メッセージ ) と,最前面の処理以外は最初のサンプルのコードと同じです.

  try
    ClipBoard.Open;
  except
    Exit;
  end;


下図は,Firefox と Google Chrome における実行結果です.


図35
ブラウザの URL を自動取得
  • ウェブブラウザのアドレスパーのテキストをコピーした結果
  • Firefox と Google Chrome の場合
  • 本サンプルの EXE を予め起動しておく
  • テキストをコピーする前にリストをクリア

図36
一本的なテキストの場合

リスト11
クリップボードにコピーしたテキストを自動的に取得して TMemo に表示
unit Unit1;

interface

uses
  Winapi.Windows, Winapi.Messages, System.SysUtils, System.Variants, System.Classes,
  Vcl.Graphics, Vcl.Controls, Vcl.Forms, Vcl.Dialogs, Vcl.StdCtrls, Vcl.ExtCtrls;

type
  TForm1 = class(TForm)
    Panel1: TPanel;
    Button1: TButton;
    Button2: TButton;
    Memo1: TMemo;
    procedure Button1Click(Sender: TObject);
    procedure Button2Click(Sender: TObject);
    procedure FormCreate(Sender: TObject);
    procedure FormDestroy(Sender: TObject);
  private
    { Private 宣言 }
    FNextWindow : HWND;
    procedure WMChangeCBChain(var Message: TWMChangeCBChain); message
      WM_CHANGECBCHAIN;
    procedure WMDrawClipboard(var Message: TWMDrawClipboard); message
      WM_DRAWCLIPBOARD;
  public
    { Public 宣言 }
    procedure BringToFrontForm(AHandle: HWND);
  end;

var
  Form1: TForm1;

implementation

uses
  Vcl.Clipbrd;

{$R *.dfm}

//=============================================================================
//  このフォームをクリップボードビューワとして登録
//  FNextWindow は次のクリップボードビューワウィンドウのハンドル
//=============================================================================
procedure TForm1.FormCreate(Sender: TObject);
begin
  FNextWindow := SetClipboardViewer(Handle);
end;

//=============================================================================
//  クリップボードビューワとしての登録を解除
//  クリップボード監視のチェインから外す
//=============================================================================
procedure TForm1.FormDestroy(Sender: TObject);
begin
  ChangeClipboardChain(Handle, FNextWindow);
end;

//=============================================================================
//  クリップボードを空にする
//=============================================================================
procedure TForm1.Button1Click(Sender: TObject);
begin
  Clipboard.Clear;
end;

//=============================================================================
//  取得した内容をクリアする
//=============================================================================
procedure TForm1.Button2Click(Sender: TObject);
begin
  Memo1.Lines.Clear;
end;

//=============================================================================
//  ウィンドウがクリップボードチェインから削除される時に送られてくるメッセージ
//=============================================================================
procedure TForm1.WMChangeCBChain(var Message: TWMChangeCBChain);
begin
  if (Message.Remove = FNextWindow) then begin
    FNextWindow := Message.Next;
    Message.Result := 0;
  end else begin
    SendMessage(FNextWindow, WM_CHANGECBCHAIN, Message.Remove, Message.Next);
  end;
end;

//=============================================================================
//  クリップボード更新フック
//  クリップボードの内容に更新がある時に送られてくるメッセージ
//  CF_TEXT の時にテキストをクリップボードから自動的に取得
//
//  本サンプルでは,アプリ起動時にもこのメッセージを受信
//=============================================================================
procedure TForm1.WMDrawClipboard(var Message: TWMDrawClipboard);
var
  LhClipbrd : THandle;
begin
  // Microsoft Edge, Google Chrome の URL をコピーした時の対応策
  // クリップボードが開けないエラーが発生する
  try
    ClipBoard.Open;
  except
    Exit;
  end;

  if Clipboard.HasFormat(CF_TEXT) then begin
    LhClipbrd := Clipboard.GetAsHandle(CF_TEXT);
    try
      Memo1.Lines.Add(Clipboard.AsText);
    finally
      GlobalUnlock(LhClipbrd);
    end;
  end;
  Clipboard.Close;

  //次のクリップボードビューワウィンドウにメッセージを送る
  SendMessage(FNextWindow, WM_DRAWCLIPBOARD, 0, 0);

  BringToFrontForm(Handle);
end;

//-----------------------------------------------------------------------------
//  指定フォームを強制的に最前面に表示する関数
//  この手続きを実行するウィンドウがアクティブでない場合用
//
//  AHandle : 前面に表示するフォームのハンドル (メインフォーム以外も可)
//  SPI_GETFOREGROUNDLOCKTIMEOUT, SPI_SETFOREGROUNDLOCKTIMEOUT はデバック時無効
//-----------------------------------------------------------------------------
procedure TForm1.BringToFrontForm(AHandle: HWND);
var
  LhForeWnd     : HWND;
  LTimeOut      : DWORD;
  LThreadForeID : Cardinal;
  LThreadSelfID : Cardinal;
begin
  // 現在の最前面のウィンドウハンドルを取得
  // 少しタイムラグがないとウィンドウハンドルが取得できないことがある
  LhForeWnd := 0;
  while True do begin
    LhForeWnd := GetForegroundWindow;
    if LhForeWnd <> 0 then Break;
    Application.ProcessMessages;
    Sleep(10);
  end;
  // 現在の最前面ウィンドウのプロセスIDの値を取得
  LThreadForeID := GetWindowThreadProcessId(LhForeWnd);
  // 前面表示するウィンドウのプロセスのスレッドIDの値を取得
  // LThreadSelfID := GetWindowThreadProcessId(AHandle); でもOK (値は違う)
  LThreadSelfID := GetCurrentThreadId;
  try
    // スレッドのアタッチ
    AttachThreadInput(LThreadSelfID, LThreadForeID, True);
    // タスクバーの点滅時間の設定値を取得 (フォアグランドにするまでの時間)
    // デフォルト値は非常に大きな値.通常は前面に表示しないで点滅するだけ
    SystemParametersInfo(SPI_GETFOREGROUNDLOCKTIMEOUT, 0, @LTimeOut, 0);

    // タスクバーの点滅時間の設定
    // 点滅時間を 0 にして直ちに最前面に表示するようにする
    SystemParametersInfo(SPI_SETFOREGROUNDLOCKTIMEOUT, 0, nil, 0);

    // 最小化されているときは元に戻す
    if IsIconic(AHandle) then begin
      SendMessage(AHandle, WM_SYSCOMMAND, SC_RESTORE, 0);
    end;
    // 最前面に表示する
    SetForegroundWindow(AHandle);
  finally
    // タスクバーの点滅時間の設定を元に戻す
    SystemParametersInfo(SPI_SETFOREGROUNDLOCKTIMEOUT, 0, Pointer(LTimeOut), 0);
    // 終了したらアタッチを切り離す
    AttachThreadInput(LThreadSelfID, LThreadForeID, False);
  end;
end;

end.


下図は,Microsof Edge における URL のコピーの結果です.URL をコピーする前に,リストをクリアしてテストしています.コピーとしての操作は 1 回だけですが,ほとんど場合,複数回の受信となっています.これにはいくつものパターンがあります.まれにですが,1 回だけの時もあります.
この現象は,Firefox, Google Chrome では発生しません.

本ページの他のサンプルのように,ボタンクリックでクリップボードのデータを取得するのであれば,最後のテキストだけを URL として取得することになります.



図37
Microsoft Edge で URL をコピーした結果
  • プロトコルなしの後に
    プロトコル付き 2 回
  • プロトコル付きが 3 回の時もある
図38
Microsoft Edge で URL をコピーした結果
  • プロトコルなしの後に
    プロトコル付き 1 回
図39
Microsoft Edge で URL をコピーした結果
  • プロトコル付き 2 回

Microsoft Edge の URL のコピーが,何故,上記のような仕様になっているのか,筆者は知りません.この場合,次のように,一定時間内に受信したメッセージを処理しないようにすれば,最初のメッセージだけを処理するようにできます.


//=============================================================================
//  クリップボード更新フック
//  クリップボードの内容に更新がある時に送られてくるメッセージ
//  CF_TEXT の時にテキストをクリップボードから自動的に取得
//
//  一定時間内に受信したメッセージを無視する例
//=============================================================================
procedure TForm1.WMDrawClipboard(var Message: TWMDrawClipboard);
{$WRITEABLECONST ON}
const
  LGetTime : Cardinal = 0;
{$WRITEABLECONST OFF}
var
  LhClipbrd   : THandle;
  LTimeSpane  : Cardinal;
begin
  LTimeSpane := timeGetTime - LGetTime;
  if LTimeSpane < 100 then Exit;

  // Microsoft Edge, Google Chrome の URL をコピーした時の対応策
  // クリップボードが開けないエラーが発生する
  try
    ClipBoard.Open;
  except
    Exit;
  end;
  LGetTime := timeGetTime;


  if Clipboard.HasFormat(CF_TEXT) then begin
    LhClipbrd := Clipboard.GetAsHandle(CF_TEXT);
    try
      Memo1.Lines.Add(Clipboard.AsText);
    finally
      GlobalUnlock(LhClipbrd);
    end;
  end;
  Clipboard.Close;

  //次のクリップボードビューワウィンドウにメッセージを送る
  SendMessage(FNextWindow, WM_DRAWCLIPBOARD, 0, 0);

  BringToFrontForm(Handle);
end;


下図は,今回動作確認したウェブブラウザとそのバージョンです.


図40
動作確認したブラウザとそのバージョン