ラベルの表示 - Rustで作るWindowsアプリ

658, 2023-05-08

目次

ラベルを表示する

Windows APIではウィンドウ内に表示されるボタンやテキストなどの部品を「コントロール」と呼びます。ウィンドウ内にコントロールを配置し、ユーザーがそのコントロールに対して入力を行い、プログラムの振る舞いを変えるというのがWindows APIに限らず一般的なGUIアプリの動作です。

ここではウィンドウ内にラベルを配置してテキストを表示するということをやってみたいと思います。ラベルとはテキストを表示するコントロールで、ラベル内のテキストは変更したり取得したりできます。
ラベルは汎用性のある部品でたとえばテキストボックスの上部にラベルを配置してテキストボックスが何のためにあるのか表示するなどの使われ方をします。
ラベルの作成と配置方法を通じてWindows APIの基本的なコントロールの配置方法を知っておきましょう。

ウィンドウプロシージャの改造

ウィンドウプロシージャ関数を以下のように改造します。

extern "system" fn window_proc(
    window: HWND, message: u32, wparam: WPARAM, lparam: LPARAM
) -> LRESULT {
    unsafe {
        static mut HWND_LABEL: HWND = HWND(0);
        static mut COUNT: i32 = 0;

        // メッセージ内容によって処理を分岐する
        match message {
            // ウィンドウの作成時に呼ばれる
            WM_CREATE => {
                // ラベルの作成
                HWND_LABEL = CreateWindowExW(
                    WINDOW_EX_STYLE::default(),
                    w!("STATIC"), // ラベルのウィンドウ クラス名
                    w!("0回描画されました。"), // ラベルに表示するテキスト
                    WS_VISIBLE | WS_CHILD, // スタイル
                    50, 50, // XおよびY座標
                    200, 20, // 幅および高さ
                    window, // 親ウィンドウのハンドル
                    None, // メニューハンドル(使用しない)
                    None, // インスタンスハンドル
                    None // 追加のアプリケーションデータ
                );
                LRESULT(0)
            }
            // ウィンドウの背景を削除するときに呼ばれる
            WM_ERASEBKGND => {
                let hdc = HDC(wparam.0 as isize);
                let mut rect = RECT::default();

                // ウィンドウのサイズを取得
                GetClientRect(window, &mut rect);

                // 白色のブラシで四角形を描画
                let h_white_brush: HBRUSH = CreateSolidBrush(COLORREF(0x00ffffff));
                FillRect(hdc, &mut rect, h_white_brush);

                LRESULT(1) // 背景を消去したことを示すために1を返す
            }
            // ウィンドウの変更によりクライアント領域の内容が変わった
            WM_PAINT => {
                let mut ps: PAINTSTRUCT = PAINTSTRUCT::default();
                BeginPaint(window, &mut ps);

                // ラベル内のテキストを更新
                let formatted: String = format!("{}回描画されました。", COUNT);
                COUNT += 1;
                let mut u16_ary: Vec<u16> = formatted.encode_utf16().collect();
                u16_ary.push(0);  // 重要: ナル文字をプッシュ
                let text = PCWSTR::from_raw(&u16_ary[0]);
                SetWindowTextW(HWND_LABEL, text);

                // ラベル内のテキストを取得
                let mut u16_ary: [u16; 100] = [0; 100];
                GetWindowTextW(HWND_LABEL, &mut u16_ary);
                let text = String::from_utf16(&u16_ary).unwrap();
                println!("{}", text);

                // ラベルを再描画
                RedrawWindow(HWND_LABEL, None, None, RDW_INVALIDATE | RDW_UPDATENOW | RDW_ERASE);

                EndPaint(window, &ps);
                LRESULT(0)
            }
            // ウィンドウが破棄される
            WM_DESTROY => {
                println!("ウィンドウを破棄します。");
                // メッセージキューにWM_QUITメッセージをポストする
                PostQuitMessage(0);
                LRESULT(0)
            }
            _ => DefWindowProcW(window, message, wparam, lparam),
        }
    }
}

他のコードはウィンドウを表示した時と同じです。このコードでは以下の修正を加えています。

  • WM_CREATEでラベルの作成
  • WM_ERASEBKGNDで背景色のクリア
  • WM_PAINTでラベルの再描画

修正を加えたコードを実行すると以下のようなウィンドウが表示されます。

windows-rs-label

WM_CREATEでラベルを作成する

    // ウィンドウの作成時に呼ばれる
    WM_CREATE => {
        // ラベルの作成
        HWND_LABEL = CreateWindowExW(
            WINDOW_EX_STYLE::default(),
            w!("STATIC"), // ラベルのウィンドウ クラス名
            w!("0回描画されました。"), // ラベルに表示するテキスト
            WS_VISIBLE | WS_CHILD, // スタイル
            50, 50, // XおよびY座標
            200, 20, // 幅および高さ
            window, // 親ウィンドウのハンドル
            None, // メニューハンドル(使用しない)
            None, // インスタンスハンドル
            None // 追加のアプリケーションデータ
        );
        LRESULT(0)
    }

WM_CREATEメッセージはウィンドウの作成時に発行されます。ここでHWND_LABELを初期化します。HWND_LABELはウィンドウプロシージャ内で定義されているstatic変数です。

    static mut HWND_LABEL: HWND = HWND(0);
    static mut COUNT: i32 = 0;

これはラベルのウィンドウ ハンドルです。ラベルと言ってもその実体はウィンドウになっています。このラベルの設定で大事なのは以下の点です。

  • ウィンドウ クラス名にSTATICを設定する
  • スタイルにWS_CHILDを設定する
  • 親ウィンドウのハンドルを設定する

ウィンドウ クラス名にSTATICを設定するとそのウィンドウは静的コントロールになります。静的コントロールは選択不可、キーボードによるフォーカス不可なコントロールです。ラベルの特性から言ってラベルにはこの静的コントロールが適していると言えます。
スタイルにWS_CHILDを設定するとそのウィンドウは子ウィンドウになります。子ウィンドウにはメニューバーを含めることができません。WS_CHILDWS_POPUPと一緒に使うことはできません。
子ウィンドウの場合は親ウィンドウを設定します。ここではwindowを指定しています。

WM_ERASEBKGNDで背景色をクリアする

    // ウィンドウの背景を削除するときに呼ばれる
    WM_ERASEBKGND => {
        let hdc = HDC(wparam.0 as isize);
        let mut rect = RECT::default();

        // ウィンドウのサイズを取得
        GetClientRect(window, &mut rect);

        // 白色のブラシで四角形を描画
        let h_white_brush: HBRUSH = CreateSolidBrush(COLORREF(0x00ffffff));
        FillRect(hdc, &mut rect, h_white_brush);

        LRESULT(1) // 背景を消去したことを示すために1を返す
    }

WM_ERASEBKGNDはウィンドウの背景を削除するときに発行されます。たとえばウィンドウのサイズを変更すると背景色を削除する必要があるためこのメッセージが発行されます。
ここでは背景色を白色のブラシでクリアしています。こうすると背景色が真っ白になります。

WM_PAINTでラベルを再描画する

    // ウィンドウの変更によりクライアント領域の内容が変わった
    WM_PAINT => {
        let mut ps: PAINTSTRUCT = PAINTSTRUCT::default();
        BeginPaint(window, &mut ps);

        // ラベル内のテキストを更新
        let formatted: String = format!("{}回描画されました。", COUNT);
        COUNT += 1;
        let mut u16_ary: Vec<u16> = formatted.encode_utf16().collect();
        u16_ary.push(0);  // 重要: ナル文字をプッシュ
        let text = PCWSTR::from_raw(&u16_ary[0]);
        SetWindowTextW(HWND_LABEL, text);

        // ラベル内のテキストを取得
        let mut u16_ary: [u16; 100] = [0; 100];
        GetWindowTextW(HWND_LABEL, &mut u16_ary);
        let text = String::from_utf16(&u16_ary).unwrap();
        println!("{}", text);

        // ラベルを再描画
        RedrawWindow(HWND_LABEL, None, None, RDW_INVALIDATE | RDW_UPDATENOW | RDW_ERASE);

        EndPaint(window, &ps);
        LRESULT(0)
    }

WM_PAINT内ではラベルを再描画します。ラベルの再描画はRedrawWindow関数で行います。この関数にHWND_LABELを指定して再描画します。

RedrawWindow関数

Function windows::Win32::Graphics::Gdi::RedrawWindow

pub unsafe fn RedrawWindow<P0, P1>(
    hwnd: P0,  // 再描画するウィンドウのハンドル
    lprcupdate: Option<*const RECT>,  // 更新する四角形の範囲
    hrgnupdate: P1,  // 更新リージョンのハンドル。Noneの場合はクライアント領域全体が更新
    flags: REDRAW_WINDOW_FLAGS  // 再描画フラグ
) -> BOOL
where
    P0: IntoParam<HWND>,
    P1: IntoParam<HRGN>,


RedrawWindow関数はウィンドウのクライアント領域を更新します。四角形でも指定可能です。再描画フラグがありこのフラグを設定しておく必要があります。再描画フラグの一部は以下の通りです。

  • windows::Win32::Graphics::Gdi
  • RDW_ERASE ... ウィンドウが再描画されるときにWM_ERASEBKGNDを受信
  • RDW_INVALIDATE ... lprcupdateまたはhrgnupdateの無効化
  • RDW_ERASENOW ... 影響を受けるウィンドウが必要に応じてWM_ERASEBKGNDなどのメッセージを受信
  • RDW_UPDATENOW ... 影響を受けるウィンドウが必要に応じてWM_PAINTなどのメッセージを受信

RDW_ERASEは無効化フラグでこれを指定する場合はRDW_INVALIDATEも指定する必要があります。
RDW_INVALIDATEはウィンドウへの無効化フラグで、lprcupdatehrgnupdateが両方Noneだった場合はウィンドウ全体が無効になります。
この関数で再描画を行う場合はRDW_UPDATENOWRDW_ERASENOWのいずれかを指定する必要があります。

REDRAW_WINDOW_FLAGS構造体

Struct windows::Win32::Graphics::Gdi::REDRAW_WINDOW_FLAGS

#[repr(transparent)]
pub struct REDRAW_WINDOW_FLAGS(pub u32);

REDRAW_WINDOW_FLAGSは再描画フラグ用のu32のラッパーでタプル構造体です。トレイトでビット演算子を実装しておりOR演算子などで複数の定数を合成可能です。

ラベルのテキストの設定と取得

    // ラベル内のテキストを更新
    let formatted: String = format!("{}回描画されました。", COUNT);
    COUNT += 1;
    let mut u16_ary: Vec<u16> = formatted.encode_utf16().collect();
    u16_ary.push(0);  // 重要: ナル文字をプッシュ
    let text = PCWSTR::from_raw(&u16_ary[0]);
    SetWindowTextW(HWND_LABEL, text);

    // ラベル内のテキストを取得
    let mut u16_ary: [u16; 100] = [0; 100];
    GetWindowTextW(HWND_LABEL, &mut u16_ary);
    let text = String::from_utf16(&u16_ary).unwrap();
    println!("{}", text);

ラベルにテキストを設定するにはSetWindowTextW関数を使います。この関数に渡すテキストはPCWSTRにする必要がありますが、色々コードを書いてformat!関数で整形した文字列をPCWSTRに変換しています。format!関数はRustの組み込みマクロで引数などを指定して文字列を作成できます。ここではCOUNTというstatic変数を文字列に格納しています。COUNTWM_PAINTが実行されるたびにインクリメントされます。Stringencode_utf16メソッドでUTF-16なベクタに変換できます。
このベクタにナル文字をプッシュしている点に注意してください。以前にも書きましたがwindows-rsの使っているWindows APIはC言語由来のものなので文字列にナル文字を入れておく必要があります。ナル文字が入ってないとC言語の文字列関数は文字列の終着点を検知することができずそのままメモリ空間の彼方へ突っ走ってしまう場合があります。このナル文字を入れる処理をコメントアウトしてプログラムを実行するとアプリがクラッシュする場合があります。
PCWSTRなどのfrom_rawメソッド等を使う場合は引数のポインタにナル文字が入ってるかどうか注意してください。

ラベルからテキストを取得するにはGetWindowTextW関数を使います。この関数にはミュータブルなu16の配列を渡します。DLLの関数を呼び出すときにwindows-rs側で配列の長さも関数に渡してくれてます。取得したu16の配列はStringfrom_utf16メソッドでRustの文字列にすることができます。取得した文字列をprintln!関数で出力しています。

SetWindowTextW関数

Function windows::Win32::UI::WindowsAndMessaging::SetWindowTextW

pub unsafe fn SetWindowTextW<P0, P1>(
    hwnd: P0,  // 変更対象のウィンドウのハンドル
    lpstring: P1  // 新しいテキスト
) -> BOOL
where
    P0: IntoParam<HWND>,
    P1: IntoParam<PCWSTR>,

この関数は指定したウィンドウにタイトルバーが存在する場合、タイトルバーのテキストを変更します。ウィンドウがコントロールならコントロールのテキストを変更します。別のアプリケーションのコントロールのテキストは変更できません。
返り値は成功の場合は0以外、失敗した場合は0です。

GetWindowTextW関数

Function windows::Win32::UI::WindowsAndMessaging::GetWindowTextW

pub unsafe fn GetWindowTextW<P0>(
    hwnd: P0,  // 取得対象のウィンドウのハンドル
    lpstring: &mut [u16]  // 保存先のu16の配列
) -> i32
where
    P0: IntoParam<HWND>,

この関数は指定したウィンドウにタイトルバーが存在する場合、タイトルバーのテキストをlpstringにコピーします。ウィンドウがコントロールの場合はコントロールのテキストをコピーします。この関数は別のアプリケーションのテキストは取得できません。
この関数は同名のWin32APIの関数が存在しますがwindows-rsではこの関数をラップして配列の要素数の引数を省略しています。
返り値は関数が成功した場合はコピーされた文字列の長さ(ナル文字を除く)です。ウィンドウにタイトルやテキストが無かったり空だった場合、ハンドルが無効な場合は戻り値は0です。



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