スクリーンキャプチャ

さて、今回はWindows APIを用いてデスクトップのスクリーン画面を取得してみたいと思います。デスクトップ画面の画像は、スクリーンセーバーなどのソフトウェアで使われることがあると思います。また、画像を保存するツールなどにも使えそうです。

画像の取得には画像をコピーするAPI関数で実現することができますが、そのためにはいろいろと前知識が必要になりますので、まずは、それらから説明していきましょう。

GDI

Windowsは、グラフィックスの描画のためにGDI(グラフィックス・デバイス・インターフェース)というグラフィックス描画機能を提供しています。このGDIは、gdi32.dllによって提供されるさまざまなAPI関数と、描画のための情報を管理しているデバイスコンテキスト(DC)によって構成されています。

ウィンドウにAPIを使って文字や絵を描画したり、画像コピーを行ったりするには、それぞれの機能を果たすAPI関数を使うことになります。このとき、GDIの提供するAPI関数を使うためには、引数として操作先のデバイスコンテキストのハンドルが必要になります。デバイスコンテキストとはWindowsが描画のための情報を内部的に管理しているメモリ上のデータベースのことです。これはWindowsの持つオブジェクトの1つで、アプリケーションはハンドルと呼ばれる値によりデバイスコンテキストを識別します。

デバイスコンテキストは一般的に“DC”と略記されます。また、デバイスコンテキストのハンドルを表すデータ型がHDC型と定義されているため、デバイスコンテキストのハンドルのことを“HDC”と略記することがあります。

HSPウィンドウのデバイスコンテキスト

HSP画面の描画の内部動作

ここで、HSPの画面への描画がどのように行われているのかを、少し詳しく見ていくことにしましょう。

本来、Windowsアプリケーションのウィンドウには、ウィンドウを再描画する必要が生じると(例えば、他のウィンドウに隠れていたウィンドウが前面に現れると)、Windowsから「ウィンドウのこの部分を再描画してほしい」というメッセージ(WM_PAINTメッセージ)が送られます。このとき、アプリケーションはウィンドウを描き直さなくてはいけないのです。そこで、HSPでは、実際にデスクトップ上に表示されているウィンドウとは別に、メモリ上に仮想的な画面(ビットマップ)を用意しておいて、再描画の必要性が生じたら、メモリ上の画面を実際のウィンドウ画面にコピーするという手続きをとっています。

HSPでmesboxfgcopyなどといった描画関連命令を実行したとき、これらの描画操作はまずメモリ上のビットマップ画面に対して行われ、デフォルトでは、変更部分がただちにウィンドウ画面にコピーされるようになっています。一方、redraw 0を実行することにより、描画命令実行時にメモリ画面からウィンドウ画面への自動的なコピーが行われないようにすることもできます。これにより、まずredraw 0でウィンドウ画面へのコピーを無効にしている状態でメモリ画面に対してさまざまな描画を行い、すべての描画が終了した時点でredrawを実行してウィンドウ画面にコピーするという手続きをとることができます。この手続きにより、描画時の画面のちらつきを防いだり、描画を高速化させたりすることができるのです。

実際、redraw 0を実行して描画を行っても実際のウィンドウ画面には反映されませんが、この状態でHSPのウィンドウをいったん他のウィンドウに隠してからもう一度前面に表示させると、隠れていた部分だけは自動的に再描画されますね。これは、WindowsからHSPのウィンドウにWM_PAINTメッセージが送られ、その部分の画像がメモリ画面からウィンドウ画面にコピーされたことを表しています。

おそらく、これまではscreen命令やbgscr命令で作成される画面は「実際に表示されるウィンドウ」で、buffer命令で作成される画面は「メモリ上の仮想画面」であるという印象を持っていたと思います。しかし、より正確には、screen命令やbgscr命令で作成される画面は「メモリ上の仮想画面」プラス「実際に表示されるウィンドウ」で構成されているということです。そして、実際に表示されているウィンドウにはメモリ上の画面がコピーされるようになっているのです。

システム変数 hdc

HSPでGDIの機能を使うからには、HSPの画面に対して操作できなければ意味がありません。そのため、HSPの画面に割り当てられているデバイスコンテキストのハンドルを取得する必要があります。HSPでは、現在描画を行っている画面に割り当てられているデバイスコンテキストのハンドルを、システム変数hdcという形で参照できるようになっています。描画中の画面のデバイスコンテキストが必要な場合は、このシステム変数を使います。

ここで、注意しなければならないことがあります。システム変数hdcから取得したデバイスコンテキストは、実際のウィンドウに割り当てられているデバイスコンテキストではなく、メモリ上の仮想画面を管理しているデバイスコンテキストであるということです。そのため、APIを使って描画を行ったとき、実際に表示されているウィンドウに反映させるためにはredraw命令を実行する必要があるのです。

GDIに関するAPI関数にHSP画面のデバイスコンテキスト(システム変数hdc)を渡したとき、その操作がメモリ上の画面に対して行われるということは、すなわちredraw 0を実行した状態で描画命令を実行することと同じなのです。したがって、その操作を実際に表示されているウィンドウに反映させるためには、redraw命令を実行して強制的に再描画を行わなければいけません。もちろん、ウィンドウが表示されているときだけ実行すれば十分ですが。

スクリーン画面のキャプチャ

スクリーン画面のデバイスコンテキストの作成

今回は、デスクトップスクリーンの画面とHSP画面の間でコピーを行うので、HSP画面のデバイスコンテキストだけでなく、スクリーン画面のデバイスコンテキストのハンドルもまた取得しておく必要があります。

スクリーン画面のデバイスコンテキストは、CreateDC関数の第1引数に文字列"DISPLAY"を指定することによって“作成”するという形をとります。CreateDC関数はgdi32.dllによって提供されている関数で、以下のように定義されています。

HDC CreateDCA(
    PCTSTR pszDriver,
    PCTSTR pszDevice,
    PCTSTR pszOutput,
    CONST DEVMODE* pInitData
);

最初のpszDriverパラメータにのみ文字列"DISPLAY"へのポインタを指定し、残りのパラメータにすべて0 (NULL) を指定することにより、デスクトップのスクリーン画面のデバイスコンテキストを取得することができます。

これでデスクトップウィンドウのデバイスコンテキストを取得することができたので、画像のコピーなど、デバイスコンテキストに対する処理を行います。

デバイスコンテキスト間での画像のコピー

さて、実際にコピーを行ってみましょう。コピーを行うには、gdi32.dllが提供するBitBlt関数を呼び出します。この関数は、指定された長方形領域に対応する色データのビットブロック転送を行うための関数で、次のように定義されています。

BOOL BitBlt(
    HDC   hdcDest,   // destination DC
    int   nXDest,    // destination X-coord
    int   nYDest,    // destination Y-coord
    int   nWidth,    // width
    int   nHeight,   // height
    HDC   hdcSource, // source DC
    int   nXSource,  // source X-coord
    int   nYSource,  // source Y-coord
    DWORD dwRaster   // raster operation code
);

はじめの3つのパラメータhdcDest, nXDest, nYDestは、転送先のデバイスコンテキストのハンドルおよび座標を指定します。

次の2つのパラメータnWidth, nHeightには、それぞれビットブロック転送を行う画像の幅と高さを指定します。

次の3つのパラメータhdcSource, nXSource, nYSourceは、転送先のデバイスコンテキストのハンドルおよび座標を指定します。

9番目のdwRasterパラメータは、ラスターオペレーションコード(転送元および転送先の色データをどのように結合するかを定義する値)です。今回は転送元から転送先へそのままコピーするので、0x00CC0020 (SRCCOPY) を指定します。

レイヤードウィンドウ (layered window)』と呼ばれる半透明のウィンドウが画面に表示されている場合、BitBlt関数のラスターオペレーションコードにSRCCOPYフラグを指定するだけでは、そのウィンドウのイメージがコピーされません。レイヤードウィンドウを含めたイメージをコピーするには、SRCCOPYとともに0x40000000 (CAPTUREBLT) を組み合わせて指定する必要があります。

デバイスコンテキストの破棄

CreateDC関数で作成されたデバイスコンテキストは、必要な操作が完了したらすぐにDeleteDC関数で破棄しなければいけません。これを怠ると、システム全体に致命的な影響を及ぼす可能性があります。DeleteDC関数もまたgdi32.dllによって提供されている関数で、以下のように定義されています。

BOOL DeleteDC(
    HDC hDC     // handle to DC
);

hDCパラメータは、破棄するデバイスコンテキストのハンドルを指定します。このハンドルは、CreateDC関数の戻り値として返されたものです。

実際にコピーしてみる

サンプルスクリプト

現在のスクリーン画面の画像をビットマップファイルとして保存するプログラムです。

ginfo関数(正確にはginfo_dispxマクロとginfo_dispyマクロ)でデスクトップ画面のサイズを取得し、上記の手順にしたがってHSP画面へのコピーを行っています。レイヤードウィンドウ(半透明ウィンドウ)が存在する場合にも対応できるようにCAPTUREBLTフラグも合わせて指定しています。

#uselib "gdi32.dll"
#cfunc CreateDC "CreateDCA" sptr,sptr,sptr,int
#func DeleteDC "DeleteDC"  int
#func BitBlt "BitBlt" int,int,int,int,int,int,int,int,int

#define NULL        0
#define SRCCOPY     0x00CC0020
#define CAPTUREBLT  0x40000000

; ウィンドウ非表示
screen 0, 150, 50 : gsel 0, -1
wait 50

; デスクトップ画面のサイズでバッファ画面を作成
sx = ginfo_dispx : sy = ginfo_dispy
buffer 2, sx, sy

; デスクトップ画面のデバイスコンテキスト取得
hdcScreen = CreateDC("DISPLAY", NULL, NULL, NULL)

; デスクトップ画面の画像をコピー
BitBlt hdc, 0, 0, sx, sy, hdcScreen, 0, 0, SRCCOPY | CAPTUREBLT

; デバイスコンテキスト削除
DeleteDC hdcScreen

; ビットマップファイルとして保存
gsel 0, 1 : dialog "bmp", 17
if stat {
    gsel 2 : bmpsave refstr
}
end

上のスクリプトを実行させてみましょう。デスクトップ画面をビットマップファイルとして保存することができます。


ちなみに BitBlt関数の引数で転送元と転送先とを入れ替えると、HSPウィンドウに描画されている画像をデスクトップ上に直接コピーできてしまいます。この場合はプログラムが終了しても描画したものが自動的に消えないので、終了時に

bgscr 3, sx, sy, 0, 0, 0

のようにして、全画面表示のウィンドウを表示させることで、画面をいったん消したほうがいいでしょう。