トップWindows入門 > キーボード・フック

キーボード・フックによるスクリーン・キャプチャ

自作ランチャーにスクリーンキャプチャ機能を組み込んで、1か月近く経つ。 この間、数十回は使用したが、問題はなく、重宝にしている[2013.5.5]。

1.キーボード・フックによるスクリーン・キャプチャ

キー入力やマウス操作、SendMessage()等によるメッセージは すべてシステムから送られてくる。 フックには様々な種類のものがあるが、 これらのうちの一つであるキーボードフック(Keyboard Hook)は システムから送られてくるキー入力情報を横取りするものである。

フックには、自スレッドあてのメッセージだけを対象としたローカルフックと すべてのスレッドを対象としたグローバルフック(システムフック)がある。

ローカルフックはあまり意味を持たない。 通常、ユーティリティで使われるのはグローバルフックである。 グローバルフックはバグがあるとシステム全体に影響を及ぼすので、大変危険である。 したがって、特別な事情がない限り、グローバルフックのプログラムを作成すべきではない。

今回は、マウスカーソルを含めたスクリーンキャプチャ(画面のハードコピー) を実現するため、危険を承知で、プログラム作成に踏み切った。

次のプログラムでは、F6キーとF7キーを横取りしている。 キー入力メッセージはキーを押したときと離したときに送られてくるが、 下のプログラムは押したときのメッセージだけを横取りしている。

capture関数は F6キーを押した時は、 画面全体(デスクトップウィンドウ)のビットマップをクリップボードにコピーする。 F7キーを押した時は、アクティブウィンドウ(フォアグラウンドウィンドウ) のビットマップをクリップボードにコピーする。

ウィンドウの標準機能である PrtScrキーおよびAltキー+PrtScrキーの場合は マウスカーソルのイメージが含まれないが、下のプログラムではマウスカーソルも含まれる。 そのために、作成したものである。

F6,F7キーはかな漢字変換でも使われるので、 マウスカーソルを含めたスクリーンキャプチャを実行するときだけ、 このキーボードフックを有効とし、使い終わったら、リセットする。

#include <windows.h>
// コンパイル方法: tcc -shared keyhook.c
#define DLL_EXPORT __declspec(dllexport)

HHOOK hHook = NULL;
HINSTANCE hInst;

void drawCursor(HDC hdc, int left, int top) {
    CURSORINFO ci;		// ここからマウスのハンドルと位置を得る。
    ICONINFO ii;		// ここからマウスのホットスポットを得る。
    ci.cbSize = sizeof(ci);
    GetCursorInfo(&ci);
    GetIconInfo(ci.hCursor, &ii);
    DrawIcon(hdc, ci.ptScreenPos.x - ii.xHotspot - left,
                  ci.ptScreenPos.y - ii.yHotspot - top, ci.hCursor);
}

void capture(BOOL fFgnd) {
    HWND hwndCapture;
    HDC hdc, hdcMem;
    HBITMAP hBitmap;
    RECT rc;

    hwndCapture = fFgnd ? GetForegroundWindow() : GetDesktopWindow();
    GetWindowRect(hwndCapture, &rc);
    hdc = GetWindowDC(hwndCapture);         	// ウインドウのDCを取得
    hdcMem = CreateCompatibleDC(hdc);	
    hBitmap = CreateCompatibleBitmap(hdc, rc.right-rc.left, rc.bottom-rc.top);
    SelectObject(hdcMem, hBitmap);		// MDCにビットマップを割り付け
    BitBlt(hdcMem, 0, 0, rc.right-rc.left, rc.bottom-rc.top, hdc, 0, 0, SRCCOPY|CAPTUREBLT);
    drawCursor(hdcMem, rc.left, rc.top);
    OpenClipboard(NULL);
    EmptyClipboard();
    SetClipboardData(CF_BITMAP, hBitmap);
    CloseClipboard();
    DeleteDC(hdcMem);
    ReleaseDC(hwndCapture, hdc);
}

// nCode: フックコード, wParam: 仮想キーコード, lParam: キーストロークメッセージ
DLL_EXPORT LRESULT CALLBACK KeyboardProc(int nCode, WPARAM wParam, LPARAM lParam) {
    int fUp  = 0x40000000 & lParam;
    if (nCode == HC_ACTION && !fUp && (wParam==VK_F6 || wParam==VK_F7)) {
	capture(wParam==VK_F7);
        return 1;
    }
    return CallNextHookEx(hHook, nCode, wParam, lParam);
}

DLL_EXPORT BOOL SetHook() {
    if (hHook != NULL) return TRUE;
    hHook = SetWindowsHookEx(WH_KEYBOARD, (HOOKPROC)KeyboardProc, hInst, 0);
    return hHook != NULL;
}

DLL_EXPORT BOOL FreeHook() {
    BOOL rtn = UnhookWindowsHookEx(hHook);
    hHook = NULL;
    return rtn;
}

BOOL APIENTRY DllMain(HMODULE hModule, DWORD dwReason, LPVOID lpReserved) {
    if (dwReason == DLL_PROCESS_ATTACH)
        hInst = (HINSTANCE)hModule;
    return TRUE;
}

F7キーでPC画面全体(デスクトップウィンドウ)、 F7キー+Altキーでアクティブウィンドウ(フォアグラウンドウィンドウ)とするには 次のようにする。

DLL_EXPORT LRESULT CALLBACK KeyboardProc(int nCode, WPARAM wParam, LPARAM lParam) {
    int fAlt = 0x20000000 & lParam;
    int fUp  = 0x40000000 & lParam;
    if (nCode == HC_ACTION && !fUp && wParam == VK_F7) {
        capture(fAlt);
        return 1;
    }
    return CallNextHookEx(hHook, nCode, wParam, lParam);
}

2.ビジュアル・スタイルにおけるツールチップのスクリーン・キャプチャ[2013.5.28]

ビジュアル・スタイルにおけるツールチップをPrtScrキーではキャプチャできるが、 上のプログラムではキャプチャできない。

ネット検索すると 以下のプログラムが見つかった。 VB2005でのCopyFromScreenでのCopyPixelOperation.CaptureBltはバグがあるとのことで、 WindowAPIでのサンプル通りやったら、キャプチャー出来たとのこと。

わがプログラムと比較すると、BitBlt関数の最後の引数に問題があるのだろうか?

Private Sub キャプチャ()
Dim sz As Size = Screen.PrimaryScreen.Bounds.Size
 Dim hDesk As IntPtr = GetDesktopWindow()
 Dim hSrce As IntPtr = GetWindowDC(hDesk)
 Dim hDest As IntPtr = CreateCompatibleDC(hSrce)
 Dim hBmp As IntPtr = CreateCompatibleBitmap(hSrce, sz.Width, sz.Height)
 Dim hOldBmp As IntPtr = SelectObject(hDest, hBmp)
 Dim b As Integer = BitBlt(hDest, 0, 0, sz.Width, sz.Height, hSrce, 0, 0, _
 CopyPixelOperation.SourceCopy Or CopyPixelOperation.CaptureBlt)
 Dim bmp As Bitmap = Bitmap.FromHbitmap(hBmp)
 SelectObject(hDest, hOldBmp)
 DeleteObject(hBmp)
 DeleteDC(hDest)
 ReleaseDC(hDesk, hSrce)
 bmp.Save("e:\キャプチャー.png")
 bmp.Dispose()
 MsgBox("キャプチャが終了しました")
End Sub 

【解決】ネットは有難い。20、30分の調査で原因と対策が分かった。 BitBlt関数に問題があり、

    BitBlt(hdcMem, 0, 0, rc.right-rc.left, rc.bottom-rc.top, hdc, 0, 0, SRCCOPY);
のところを
#define CAPTUREBLT  0x40000000
    BitBlt(hdcMem, 0, 0, rc.right-rc.left, rc.bottom-rc.top, hdc, 0, 0, SRCCOPY|CAPTUREBLT);
とすればよいことが分かった。

リンク

1. スクリーン・キャプチャ
2. Item 3: Capturing screenshots
3. 画面をキャプチャする