MFCプログラミング(2):コピー機能の実装

2021年4月10日

どもです。
前回に引き続き、今回もMFCネタです。
内容は、MFCでの「クリップボードへのデータの書き込み」についてです。

1.背景(簡単に)

入力内容を「Ctrl + c」でクリップボードに設定する際の処理でハマりました。
ネットで調べてみると、まぁ~たくさん情報が出てきます。
それらの情報を十分に吟味/検討せずに、そのまま使用してしまったのがハマった原因です。

2.内容

コピー処理を、1つのコマンドとして実装するために、対応する処理を持つ1つのクラスを定義しました。
そのクラスの中でクリップボードにデータを設定する、ということを行いました。
すると、クリップボードに設定したデータを別のアプリケーション(テキストエディタ、など)に貼り付けることができない、という現象が発生しました。

クリップボードにデータを設定するためには、以下の手順を取ります。

  1. クリップボードに貼り付けるデータを、グローバルブロックに設定する。
  2. クリップボードを開く
  3. クリップボードにデータを渡す
  4. クリップボードを閉じる

各Stepで使用する関数については、ネットで調べてみてください。
「mfc クリップボード」などで検索すると、たくさん情報が得られます。
このとき、各ステップにおいて戻り値を確認、エラーがない場合でも問題の現象が発生します。
(…問題の現象が発生している際のコードが残っていないので、具体的な実装を提示できません。ごめんなさい。)

検討段階において、CDialogクラスにボタンを貼り付け、このボタンのクリックイベントハンドラの中で同じ処理を行った場合には、問題なく動作していました。

3.原因

調べてみたところ、原因はクリップボードを開く際に使用する関数のようです。
クリップボードを開く際には、「OpenClipboard()」を使用します。
この関数は、MSDNでは下記のように定義されています。

BOOL OpenClipboard(
    HWND hWndNewOwner
);

MSDN
しかし、この関数ですが、CWndクラスでも定義されており、その定義は下記の通りです。

BOOL OpenClipboard();

MSDN
多くのサンプルは、多くの場合は下側の関数を使用していました。
また、検討段階では、こちらの関数を使用していました。
あるいは、上側の関数を使用していたとしても、引数にはNULLを指定していました。
この「NULLを指定」していたことが、原因の様です。

OpenClipboard()の引数にNULLを指定した場合、処理の上では特にエラーは発生しません。
また、引数にNULLを指定してクリップボードを開いた場合、クリップボードは現在のタスクと関連付けられます。
そうした状態で開いたクリップボードに書き込んだデータは、どうやら他のアプリケーションからは参照できない様です。
(ソースコードの内容まで追えていないので、本当に正確かはわかりません。)

4.対策

対策は、実に簡単です。
OpenClipboard()関数の引数に、適切な値を設定することです。
では、「適切な値」とは何か、ですが、これも実は簡単です。
CWndクラスのOpenClipboard()関数は、下記のように定義されています。

_AFXWIN_INLINE BOOL CWnd::OpenClipboard()
{ ASSERT(::IsWindow(m_hWnd)); return ::OpenClipboard(m_hWnd); }

つまり、OpenClipboard()関数に、現在のウィンドウのハンドルを指定すればよい、ということになります。
この対策を施した、具体的な実装は下記のようになるかと思います。

BOOL CCopyCommand::Execute(CString& SrcString)
{
    BOOL ExecResult = FALSE;
    if (::OpenClipboard(this->m_WndHnd)) {
        ::EmptyClipboard();
        SIZE_T SrcStringSize = (SrcString.GetLength() + 1) * sizeof(TCHAR);
        HGLOBAL GlobalMemHnd = ::GlobalAlloc(GHND | GMEM_SHARE, SrcStringSize);
        if (0 != GlobalMemHnd) {
            LPTSTR SrcStringMem = (LPTSTR)::GlobalLock(GlobalMemHnd);
            if (0 != SrcStringMem) {
                int Len = SrcString.GetLength();
                //lstrcpy(SrcStringMem, SrcString.GetBuffer());
                _tcscpy_s(SrcStringMem, SrcString.GetLength() + 1, (LPCTSTR)SrcString.GetBuffer());

                ::GlobalUnlock(GlobalMemHnd);
                ::SetClipboardData(CF_UNICODETEXT, SrcStringMem);
            }
        }
        ::CloseClipboard();

        ExecResult = TRUE;
    }
    else {
        ExecResult = FALSE;
    }
    return ExecResult;
}

5.貼り付けの場合は?

本エントリのタイトルからは少し離れた内容になりますが、「貼り付け」処理の話になります。
貼り付け、即ちクリップボードからデータを取得する際にも、OpenClipboard()メソッドを使用してクリップボードを開く必要があります。
その時にも、コピー処理時と同様でOpenClipboard()の引数に現在のウィンドウのハンドルを指定します。
これで、ほかのアプリケーションでクリップボードに貼り付けたデータを取得することができます。

6.まとめ

今回は、MFCでクリップボードにデータを設定する際の処理でハマった点を書きました。
クリップボードにアクセスする処理をCWndを継承していないクラスで行う場合、処理を行うクラスを保持するダイアログのウィンドウのハンドラを指定してクリップボードを開く必要があります。

コピー機能を1つのコマンド/クラスで実装する場合には、このウィンドウのハンドルを(面倒くさがらず)必ず指定するようにしましょう、という話でした。

ではっ!