ダブルバッファで文字列を描画する - Rustで作るWindowsアプリ
- 作成日: 2023-05-07
- 更新日: 2023-12-24
- カテゴリ: windows-rs
ダブルバッファで文字列を描画する
取得したデバイス コンテキストに直にTextOutW
関数名で文字列を描画すると文字列がウィンドウ上でちらつくという現象が出ました。 今回はこれに対応するためダブルバッファを使った実装をやっていきたいと思います。
ソースコードは以前に作成したウィンドウを表示するコードを改造して書いていきます。ウィンドウ表示のコードをコピーして新規パッケージで作成するのをおすすめします。
ダブルバッファとは?
ダブルバッファとはバッファをダブルで持つ描画方法です。ダブルで持つというのは2つ持つということです。描画用バッファと表示用バッファを2つ持ち、描画用バッファに描画を行い描画が完了したらそのバッファを表示用バッファにコピーします。こういう感じでバッファをお手玉の様に入れ替えて描画と表示を行います。ダブルバッファによって画面のちらつきを抑制することが可能です。
useするパッケージ
今回の実装のuse
内容は以下になります。これはウィンドウの表示用のuse
も含まれています。
use windows::{
core::*,
Win32::Foundation::*,
Win32::Graphics::Gdi::*,
Win32::System::LibraryLoader::*,
Win32::UI::WindowsAndMessaging::*,
};
WM_PAINT
内で使う関数とオブジェクトは以下の通りです。
- PAINTSTRUCT
- RECT
- HBRUSH
- COLORREF
- BeginPaint
- EndPaint
- GetClientRect
- FillRect
- CreateCompatibleDC
- CreateCompatibleBitmap
- SelectObject
- CreateSolidBrush
- TextOutW
- BitBlt
- DeleteObject
- DeleteDC
WM_PAINTの編集
ウィンドウプロシージャ内のWM_PAINT
の内容を以下のように編集します。
// ウィンドウの変更によりクライアント領域の内容が変わった
WM_PAINT => {
// ダブルバッファリングによるちらつきの防止
// ウィンドウのサイズを取得
let mut rect: RECT = RECT::default();
GetClientRect(window, &mut rect);
// 描画を始める
let mut ps: PAINTSTRUCT = PAINTSTRUCT::default();
let hdc: HDC = BeginPaint(window, &mut ps);
// オフスクリーンバッファを作成
let hdc_mem: CreatedHDC = CreateCompatibleDC(hdc);
let hbm_mem: HBITMAP = CreateCompatibleBitmap(hdc, rect.right, rect.bottom);
SelectObject(hdc_mem, hbm_mem);
// オフスクリーンバッファを白色で塗りつぶす
let h_white_brush: HBRUSH = CreateSolidBrush(COLORREF(0x00ffffff));
FillRect(hdc_mem, &rect, h_white_brush);
// テキストを描画する
TextOutW(hdc_mem, 10, 10, w!("こんにちは。さようなら").as_wide());
TextOutW(hdc_mem, 10, 40, w!("こんにちは。さようなら").as_wide());
// オフスクリーンの描画内容をスクリーンにコピーする
BitBlt(hdc, 0, 0, rect.right, rect.bottom, hdc_mem, 0, 0, SRCCOPY);
// 掃除
DeleteObject(hbm_mem);
DeleteObject(h_white_brush);
DeleteDC(hdc_mem);
EndPaint(window, &ps);
LRESULT(0)
}
この編集を行いプログラムを実行すると以下のようなウィンドウが表示されます。ウィンドウに表示される文字列はちらつきもありません。
コードはいささか長いですが、やってることはオフスクリーン(表示しない)バッファを作り、そのバッファに文字列を書き込んで最後にデバイス コンテキストにそのバッファをコピーしてるだけです。各コードについて解説をしていきます。
RECT構造体
Struct windows::Win32::Foundation::RECT
#[repr(C)]
pub struct RECT {
pub left: i32, // 左
pub top: i32, // 上
pub right: i32, // 右
pub bottom: i32, // 下
}
RECT
構造体は矩形の領域を表現する構造体です。left
とtop
で左上の位置を指定しright
とbottom
で右下の位置を指定します。この左上と右下の範囲が矩形になります。
GetClientRect関数
Function windows::Win32::UI::WindowsAndMessaging::GetClientRect
pub unsafe fn GetClientRect<P0>(
hwnd: P0, // ウィンドウのハンドル
lprect: *mut RECT // RECT構造体へのポインター
) -> BOOL
where
P0: IntoParam<HWND>,
この関数はウィンドウのクライアント領域の座標を取得します。座標はRECT
構造体で左上と右下が指定されます。クライアント領域の左上を基準にするので左上の座標は(0, 0)
になります。
関数が成功した場合は返り値は0
以外、失敗した場合は0
になります。
PAINTSTRUCT構造体
Struct windows::Win32::Graphics::Gdi::PAINTSTRUCT
#[repr(C)]
pub struct PAINTSTRUCT {
pub hdc: HDC, // 描画に使用するデバイス コンテキストへのハンドル
pub fErase: BOOL, // 背景を削除するなら真、削除しないなら偽
pub rcPaint: RECT, // 描画が要求される矩形領域
pub fRestore: BOOL, // 内部的に使用される
pub fIncUpdate: BOOL, // 内部的に使用される
pub rgbReserved: [u8; 32], // 内部的に使用される
}
PAINTSTRUCT
構造体はアプリケーションの所有するウィンドウのクライアント領域を描画するための構造体です。BeginPaint
関数でこの構造体が必要になります。
BeginPaint関数
Function windows::Win32::Graphics::Gdi::BeginPaint
pub unsafe fn BeginPaint<P0>(
hwnd: P0, // ウィンドウのハンドル
lppaint: *mut PAINTSTRUCT // PAINTSTRUCT構造体へのポインタ
) -> HDC
where
P0: IntoParam<HWND>,
この関数は引数で指定されたウィンドウを描画用に準備します。また描画に関する情報をPAINTSTRUCT
構造体に保存します。
関数が成功したら引数のウィンドウのディスプレイ デバイス コンテキストへのハンドルを返します。失敗した場合はHDC
はis_invalid()
の返り値がtrue
になります。
GetDC
関数でもデバイス コンテキストは取得できますが、TextOutW
関数で文字列を描画する場合はこのBeginPaint
関数でも描画用のデバイス コンテキストを取得できます。描画が終わったらEndPaint
関数で描画を終了します。GetDC
関数との違いはBeginPaint
関数はPAINTSTRUCT
構造体を利用できる点です。
CreateCompatibleDC関数
Function windows::Win32::Graphics::Gdi::CreateCompatibleDC
pub unsafe fn CreateCompatibleDC<P0>(
hdc: P0 // デバイス コンテキスト
) -> CreatedHDC
where
P0: IntoParam<HDC>,
この関数は引数のデバイス コンテキストと互換性のあるメモリ デバイス コンテキストを返します。ダブルバッファではデバイス コンテキストを複数作りお手玉します。そのためこの関数で既存のデバイス コンテキストをもとに新しいデバイス コンテキストを作ります。
引数がNone
の場合、関数はアプリケーションの現在の画面と互換性のあるメモリ デバイス コンテキストを返します。
CreatedHDC構造体
Struct windows::Win32::Graphics::Gdi::CreatedHDC
#[repr(transparent)]
pub struct CreatedHDC(pub isize);
CreatedHDC
構造体はHDC
構造体と互換性のある構造体です。2つとも同じトレイトを実装していて例えばFillRect
関数にはこの2つのどちらも渡すことができます。ジェネリック プログラミングですね。windows-rs
ではジェネリック プログラミングが多用されています。
CreateCompatibleBitmap関数
Function windows::Win32::Graphics::Gdi::CreateCompatibleBitmap
pub unsafe fn CreateCompatibleBitmap<P0>(
hdc: P0, // デバイス コンテキスト
cx: i32, // 作成するビットマップの幅(ピクセル)
cy: i32 // 作成するビットマップの高さ(ピクセル)
) -> HBITMAP
where
P0: IntoParam<HDC>,
この関数は引数のデバイス コンテキストからビットマップを作り返します。
関数が成功した場合はビットマップ(DDB)へのハンドルです。失敗した場合はHBITMAP
はis_invalid()
の返り値がtrue
になります。
HBITMAP構造体
Struct windows::Win32::Graphics::Gdi::HBITMAP
#[repr(transparent)]
pub struct HBITMAP(pub isize);
HBITMAP
構造体はisize
のラッパーのタプル構造体です。
状態が不正のときis_invalid()
メソッドの返り値がtrue
になります。整数が-1
または0
のとき不正となります。
SelectObject関数
Function windows::Win32::Graphics::Gdi::SelectObject
pub unsafe fn SelectObject<P0, P1>(
hdc: P0, // デバイス コンテキスト
h: P1 // オブジェクトのハンドル
) -> HGDIOBJ
where
P0: IntoParam<HDC>,
P1: IntoParam<HGDIOBJ>,
この関数は引数のデバイス コンテキストにオブジェクトを設定します。新しく設定されるオブジェクトは前のオブジェクトを置き換えます。
返り値は関数が成功した場合は置き換えたオブジェクトへのハンドルです。失敗した場合はHGDIOBJ
のis_invalid()
がtrue
になります。
HGDIOBJ構造体
Struct windows::Win32::Graphics::Gdi::HGDIOBJ
#[repr(transparent)]
pub struct HGDIOBJ(pub isize);
この構造体はGDI
オブジェクトへのハンドルを表す構造体です。is_invalid()
メソッドが実装されており状態が不正の時にtrue
を返します。整数の値が-1
または0
のとき不正になります。
CreateSolidBrush関数
Function windows::Win32::Graphics::Gdi::CreateSolidBrush
pub unsafe fn CreateSolidBrush<P0>(
color: P0
) -> HBRUSH
where
P0: IntoParam<COLORREF>,
この関数は指定された色の論理ブラシを作って返します。
関数が成功した場合は返り値は論理ブラシとして機能します。失敗した場合はHBRUSH
のis_invalid()
がtrue
になります。
HBRUSH構造体
Struct windows::Win32::Graphics::Gdi::HBRUSH
#[repr(transparent)]
pub struct HBRUSH(pub isize);
HBRUSH
構造体はブラシのハンドルを表すisize
のラッパーのタプル構造体です。
状態が不正の時にis_invalid()
メソッドがtrue
を返します。整数が-1
または0
のとき不正になります。
COLORREF構造体
Struct windows::Win32::Foundation::COLORREF
#[repr(transparent)]
pub struct COLORREF(pub u32);
この構造体は色を表現する構造体です。u32
のラッパーのタプル構造体です。
色を指定する場合は16進数で指定します。色の並びはrgb
で言うと以下になります。
0x00bbggrr
0
が暗くf
で明るくなります。
たとえば色の指定は以下のような色が指定できます。
- 白色 ...
0x00ffffff
- 黒色 ...
0x00000000
- 赤色 ...
0x000000ff
- 緑色 ...
0x0000ff00
- 青色 ...
0x00ff0000
- 黄色 ...
0x0000ffff
- 水色 ...
0x00ffff00
- 紫色 ...
0x00ff00ff
FillRect関数
Function windows::Win32::Graphics::Gdi::FillRect
pub unsafe fn FillRect<P0, P1>(
hdc: P0, // デバイス コンテキスト
lprc: *const RECT, // 塗りつぶす範囲を表す`RECT`構造体へのポインタ
hbr: P1 // 塗りつぶしに使うブラシ
) -> i32
where
P0: IntoParam<HDC>,
P1: IntoParam<HBRUSH>,
この関数は引数のブラシで引数のデバイス コンテキストを四角形で塗りつぶします。
返り値は関数が成功すると0
以外で失敗すると0
になります。
BitBlt関数
Function windows::Win32::Graphics::Gdi::BitBlt
pub unsafe fn BitBlt<P0, P1>(
hdc: P0, // コピー先のデバイス コンテキスト
x: i32, // コピー先の四角形の左上のx座標
y: i32, // コピー先の四角形の左上のy座標
cx: i32, // コピー先とコピー元の四角形の幅
cy: i32, // コピー先とコピー元の四角形の高さ
hdcsrc: P1, // コピー元のデバイス コンテキスト
x1: i32, // コピー元の四角形の左上のx座標
y1: i32, // コピー元の四角形の左上のy座標
rop: ROP_CODE // ラスター演算コード
) -> BOOL
where
P0: IntoParam<HDC>,
P1: IntoParam<HDC>,
この関数は第1引数のhdc
のデバイス コンテキストに第6引数のhdcsrc
のデバイス コンテキストを指定の四角形の範囲で色をコピーします。
// オフスクリーンの描画内容をスクリーンにコピーする
BitBlt(hdc, 0, 0, rect.right, rect.bottom, hdc_mem, 0, 0, SRCCOPY);
最後の引数のrop
はラスター演算コードというなんだか難しい名前ですが、これは簡単に言うとコピー方法の指定です。SRCCOPY
を指定するとコピー元の四角形をコピー先の四角形に直接コピーします。ラスター演算コードについては以下に一部を掲載します。
- SRCCOPY ... 四角形の直接コピー
- SRCAND ... 色のAND結合
- SRCERASE ... 色のAND結合。変換先の四角形の反転色とコピー元の四角形の色を組み合わせる
- SRCINVERT ... 色のXOR結合
- SRCPAINT ... 色のOR結合
DeleteObject関数
Function windows::Win32::Graphics::Gdi::DeleteObject
pub unsafe fn DeleteObject<P0>(
ho: P0 // ハンドル(論理ペンやブラシなど)
) -> BOOL
where
P0: IntoParam<HGDIOBJ>,
この関数は論理ペンやブラシ、フォントやビットマップ、領域やパレットなどを削除します。削除されたオブジェクトはリソースが解放されます。Windows APIではリソースの解放を手動で行う必要があります。これはC言語由来の仕様だと思われます。C言語ではmalloc
関数などでメモリを確保し、free
関数などでメモリを手動開放するのが一般的な実装です。
関数が成功すると返り値は0以外になります。引数のハンドルが無効だったり、ハンドルがデバイス コンテキストに選択されていたりした場合、返り値は0
になります。
注意点として、描画オブジェクトはデバイス コンテキストに選択されている間は削除しないでください。またブラシを削除してもブラシに紐づけられたビットマップは削除されません。ビットマップはビットマップごとに削除が必要です。
DeleteDC関数
Function windows::Win32::Graphics::Gdi::DeleteDC
pub unsafe fn DeleteDC<P0>(
hdc: P0 // デバイス コンテキスト
) -> BOOL
where
P0: IntoParam<CreatedHDC>,
この関数は引数のデバイス コンテキストを削除します。
関数が成功すると返り値は0
以外になり、失敗した場合は0
を返します。
EndPaint関数
Function windows::Win32::Graphics::Gdi::EndPaint
pub unsafe fn EndPaint<P0>(
hwnd: P0, // ウィンドウのハンドル
lppaint: *const PAINTSTRUCT // PAINTSTRUCT構造体へのポインタ(BeginPaintで渡したもの)
) -> BOOL
where
P0: IntoParam<HWND>,
この関数は指定されたウィンドウでの描画の終了をマークします。BeginPaint
関数を呼び出した後は、描画が終了したらこの関数を呼び出す必要があります。
返り値は常に0
以外です。