ダブルバッファで文字列を描画する - Rustで作るWindowsアプリ

657, 2023-05-07

目次

ダブルバッファで文字列を描画する

取得したデバイス コンテキストに直に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)
    }

この編集を行いプログラムを実行すると以下のようなウィンドウが表示されます。ウィンドウに表示される文字列はちらつきもありません。

windows-rs-double-buf

コードはいささか長いですが、やってることはオフスクリーン(表示しない)バッファを作り、そのバッファに文字列を書き込んで最後にデバイス コンテキストにそのバッファをコピーしてるだけです。各コードについて解説をしていきます。

RECT構造体

Struct windows::Win32::Foundation::RECT

#[repr(C)]
pub struct RECT {
    pub left: i32,  // 左
    pub top: i32,  // 上
    pub right: i32,  // 右
    pub bottom: i32,  // 下
}

RECT構造体は矩形の領域を表現する構造体です。lefttopで左上の位置を指定しrightbottomで右下の位置を指定します。この左上と右下の範囲が矩形になります。

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構造体に保存します。
関数が成功したら引数のウィンドウのディスプレイ デバイス コンテキストへのハンドルを返します。失敗した場合はHDCis_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)へのハンドルです。失敗した場合はHBITMAPis_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>,

この関数は引数のデバイス コンテキストにオブジェクトを設定します。新しく設定されるオブジェクトは前のオブジェクトを置き換えます。
返り値は関数が成功した場合は置き換えたオブジェクトへのハンドルです。失敗した場合はHGDIOBJis_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>,

この関数は指定された色の論理ブラシを作って返します。
関数が成功した場合は返り値は論理ブラシとして機能します。失敗した場合はHBRUSHis_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以外です。



この記事のアンケートを送信する