ウィンドウの作成 - Rustで作るWindowsアプリ

655, 2023-05-06

目次

ウィンドウを作る

Windows APIでウィンドウを作る場合、けっこうコードを書く必要があります。現代的なGUIライブラリに慣れている人からするとギョっとするかもしれません。またコードに詳細なコメントを入れているのでなおさらボリュームがありますが、ご容赦ください。
ここでは拡張スタイルのウィンドウを作成する方法を取り上げます。

Windowsアプリの駆動方式

Windowsアプリはイベントドリブン(イベント駆動)です。具体的にはメッセージキューというメッセージを登録するキューがあります。キューというのは一定の長さで循環する配列みたいなものです(実装方式によって配列じゃない場合もあります)。そしてユーザーから入力などがあったり、ウィンドウの状態が変わったりするとそのメッセージキューにメッセージが送られます。開発者はこのメッセージキューのメッセージに対応して処理を書きます。
メッセージはウィンドウプロシージャという関数に送られてくるので、この関数をウィンドウと紐づける必要があります。ウィンドウプロシージャ内ではメッセージの値に応じて処理を書き、再描画やウィンドウの終了などを行います。

ウィンドウを作る流れ

Windowsアプリのウィンドウを作る流れですが以下になります。

  • ウィンドウプロシージャ関数を定義する
  • モジュールのハンドルを得る
  • ウィンドウクラスの構造体の作成
  • 作成したウィンドウクラスの構造体をシステムに登録する
  • ウィンドウクラスを登録してからウィンドウを作成する
  • ウィンドウプロシージャ関数内でメッセージを処理する

やってることは結構多いですが、コードを読み解けばそれほど複雑なことはやっていません。CreateWindowExW()関数でウィンドウを作成しますが、その引数として登録されたウィンドウクラス構造体の名前が必要になります。またウィンドウクラス構造体の作成にはウィンドウプロシージャ関数が必要になる、といった具合に依存関係があります。

処理を簡略的に並べると以下のような流れです。

  • 自作window_proc関数の作成
  • GetModuleHandleW関数でモジュールのハンドルを取得
  • WNDCLASSW構造体で変数を作成
  • RegisterClassW関数で作成したWNDCLASSW構造体の変数を登録
  • CreateWindowExW関数でウィンドウを作成
  • GetMessageW関数でメッセージ取得のループ処理
  • DispatchMessageW関数でウィンドウプロシージャにメッセージを送る

これらの処理は定型文なので、暗記して一から書くというものでも無いと思います。一度書いたらあとはコピペで済ませましょう。

パッケージの作成

cargoで新規パッケージを作成します。

> cargo new create_window
     Created binary (application) `create_window` package

Cargo.tomlの設定

作成したパッケージのCargo.tomlを以下のように編集します。

[package]
name = "create_window"
version = "0.1.0"
edition = "2021"

[dependencies.windows]
version = "0.48.0"
features = [
    "Win32_Foundation",
    "Win32_Graphics_Gdi",
    "Win32_System_LibraryLoader",
    "Win32_UI_WindowsAndMessaging",
]

main.rsの編集

src\main.rsを以下の内容で編集します。

use windows::{
    core::*,
    Win32::Foundation::*, 
    Win32::Graphics::Gdi::*,
    Win32::System::LibraryLoader::*,
    Win32::UI::WindowsAndMessaging::*,
};

/// # ウィンドウを作成する
///
/// Windows APIを使いウィンドウを作成する。
///
fn main() -> Result<()> {
    unsafe {
        // モジュールのハンドルの取得
        // 引数には読み込まれたモジュールの名前を渡す
        // 引数がNoneの場合、GetModuleHandleW()は呼び出し元のハンドルを返す
        // (.exeファイルのハンドル)
        let mod_instance = GetModuleHandleW(None)?;
        if mod_instance.0 == 0 {
            panic!("モジュールのハンドルの取得に失敗しました。")
        }

        // ウィンドウクラスの名前
        // この名前はWNDCLASSWやCreateWindowExW()などで使われる
        let window_class_name = w!("my_window_class");

        // ウィンドウクラスの作成
        let wc = WNDCLASSW {
            lpszClassName: window_class_name,  // ウィンドウクラスの名前
            hInstance: mod_instance,  // モジュールのハンドルの指定
            hCursor: LoadCursorW(None, IDC_ARROW)?,  // ウィンドウ上のマウスカーソルの指定
            style: CS_HREDRAW | CS_VREDRAW,  /* CS_HREDRAWは水平、CS_VREDRAWは垂直に
                                                ウィンドウサイズに合わせて再描画する */
            lpfnWndProc: Some(window_proc),  /* ウィンドウプロシージャ(関数)の設定。このプロシージャに
                                                メッセージが発行される */
            ..Default::default()  // 他の属性にはデフォルト値を設定
        };

        // ウィンドウクラスの登録
        // 返り値は登録されているクラスを識別するアトム(整数)
        let atom = RegisterClassW(&wc);
        if atom == 0 {
            panic!("ウィンドウクラスの登録に失敗しました。");
        }

        // 拡張ウィンドウスタイルを使ってウィンドウを作成する
        CreateWindowExW(
            WINDOW_EX_STYLE::default(),  // ウィンドウの拡張スタイル
            window_class_name,  // 作成したウィンドウクラス名
            w!("これは私のウィンドウです"),  // ウィンドウの名前
            WS_OVERLAPPEDWINDOW | WS_VISIBLE,  // ウィンドウのスタイル
            CW_USEDEFAULT,  // ウィンドウのx座標
            CW_USEDEFAULT,  // ウィンドウのy座標
            CW_USEDEFAULT,  // ウィンドウの横幅(指定する値はx座標 + 横幅)
            CW_USEDEFAULT,  // ウィンドウの高さ(指定する値はy座標 + 高さ)
            None,  // 親ウィンドウのハンドル
            None,  // メニューへのハンドル
            mod_instance,  // ウィンドウに関連付けるモジュールのハンドル
            None,
        );

        // メッセージキューからのメッセージを保持する変数
        let mut message = MSG::default();

        // メッセージキューからループでメッセージを取得
        while GetMessageW(&mut message, None, 0, 0).into() {
            // 登録したウィンドウプロシージャ(window_proc)にメッセージを発行する
            DispatchMessageW(&message);
        }

        Ok(())
    }
}

/// # window_proc
///
/// メッセージを処理するウィンドウプロシージャ。
/// Windowsアプリケーションはイベント駆動になっている。ユーザーからの入力はシステムがウィンドウプロシージャに
/// メッセージで渡す。開発者はそのメッセージに対応して処理を書く。
/// 
/// ## 引数
/// 
/// * `window` - ウィンドウハンドル
/// * `message` - メッセージ
/// * `wparam` - 追加のメッセージ情報
/// * `lparam` - 追加のメッセージ情報
/// 
/// ## 返り値
///
/// メッセージ処理の結果。値はメッセージごとに異なる。
///
extern "system" fn window_proc(
    window: HWND, message: u32, wparam: WPARAM, lparam: LPARAM
) -> LRESULT {
    unsafe {
        // メッセージ内容によって処理を分岐する
        match message {
            // ウィンドウの変更によりクライアント領域の内容が変わった
            // 再描画が必要
            WM_PAINT => {
                println!("再描画します。");
                // 指定したウィンドウの更新領域から四角形を削除する
                // 第2引数がNoneの場合、クライアント領域全体が削除される
                ValidateRect(window, None);
                LRESULT(0)
            }
            // ウィンドウが破棄される
            WM_DESTROY => {
                println!("ウィンドウを破棄します。");
                // メッセージキューにWM_QUITメッセージをポストする
                // これによってシステムに終了を要求する
                PostQuitMessage(0);
                LRESULT(0)
            }
            _ => DefWindowProcW(window, message, wparam, lparam),
        }
    }
}

上記のプログラムを実行すると以下のようなウィンドウが表示されます。

windows-rs-create-window_window

GetModuleHandleW関数

Function windows::Win32::System::LibraryLoader::GetModuleHandleW

pub unsafe fn GetModuleHandleW<P0>(lpmodulename: P0) -> Result<HMODULE>
where
    P0: IntoParam<PCWSTR>,

ウィンドウを作成するにはウィンドウクラスの作成が必要で、ウィンドウクラスの作成にはモジュールハンドルが必要です。この関数は引数で指定されたモジュールハンドルを取得します。引数はPCWSTR型の文字列で、モジュールの名前を指定します。この引数がNoneの時はこの関数は呼び出し元のプロセスの作成に使用されるファイルへのハンドルを返します。返り値はResult<HMODULE>型になっています。

#[repr(transparent)]
pub struct HMODULE(pub isize);

HMODULEはタプル構造体でisizeのラッパーです。isizeはRustのプリミティブ型で、符号付き整数です。ビルドターゲットが32ビットの場合は4バイト、64ビットの場合は8バイトになります。関数が失敗したとき、ラップされている値は0になります。実装されているメソッドは必要があれば解説します。

WNDCLASSW関数

Struct windows::Win32::UI::WindowsAndMessaging::WNDCLASSW

#[repr(C)]
pub struct WNDCLASSW {
    pub style: WNDCLASS_STYLES,  // ウィンドウクラスのスタイル
    pub lpfnWndProc: WNDPROC,  // ウィンドウプロシージャ
    pub cbClsExtra: i32,  // ウィンドウクラス構造体の後に付加する余分なバイト数
    pub cbWndExtra: i32,  // ウィンドウインスタンスの後に付加する余分なバイト数
    pub hInstance: HMODULE,  // モジュールのハンドルの指定
    pub hIcon: HICON,  // クラスアイコンへのハンドル
    pub hCursor: HCURSOR,  // ウィンドウ上のマウスカーソルの指定
    pub hbrBackground: HBRUSH,  // クラスの背景ブラシへのハンドル
    pub lpszMenuName: PCWSTR,  // リソースファイルに名前が存在するクラスメニューのリソース名
    pub lpszClassName: PCWSTR,  // ウィンドウクラスの名前
}

WNDCLASSWはウィンドウクラスを作るための構造体です。この構造体にウィンドウプロシージャやウィンドウのスタイルを設定します。

style属性

style属性にはWNDCLASS_STYLESを指定します。

Struct windows::Win32::UI::WindowsAndMessaging::WNDCLASS_STYLES

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

WNDCLASS_STYLESu32の整数です。この構造体にはビット演算が実装されており、OR演算で複数のスタイルを指定できます。スタイルの種類は

  • windows::Win32::UI::WindowsAndMessaging
  • CS_HREDRAW ... ウィンドウのクライアント領域の幅が変更された場合にウィンドウ全体を再描画する
  • CS_VREDRAW ... ウィンドウのクライアント領域の高さが変更された場合にウィンドウ全体を再描画する
  • CS_DROPSHADOW ... ウィンドウにドロップシャドウを落とす
  • CS_NOCLOSE ... ウィンドウメニューの「閉じる」ボタンを無効にする
  • CS_DBLCLKS ... ウィンドウ内でダブルクリックされたらプロシージャにダブルクリックメッセージを送信する

などがあり、他にも存在します。
クライアント領域とはタイトルバーやメニューバーなどをの枠を除いた内側のコンテンツ表示エリアのことです。子ウィンドウを配置する場合はこのクライアント領域の左上隅が原点になります。
スタイルの指定は例えば以下の様に可能です。

let wc = WNDCLASSW {
    ...
    style: CS_HREDRAW | CS_VREDRAW | CS_DROPSHADOW | CS_NOCLOSE | CS_DBLCLKS,
    ...
};

hCursor属性

hCursor属性には

   hCursor: LoadCursorW(None, IDC_ARROW)?,

のようにカーソルを指定しています。LoadCursorW()はインスタンスに関連付けされている実行ファイルから指定されたカーソルを読み込みます。

Function windows::Win32::UI::WindowsAndMessaging::LoadCursorW

pub unsafe fn LoadCursorW<P0, P1>(
    hinstance: P0,
    lpcursorname: P1
) -> Result<HCURSOR>
where
    P0: IntoParam<HMODULE>,
    P1: IntoParam<PCWSTR>,

定義済みのカーソルのいずれかを使用するにはhInstanceNoneにしてlpcursornameを以下から指定します。

  • windows::Win32::UI::WindowsAndMessaging
  • IDC_APPSTARTING ... 標準的な矢印と小さな砂時計のカーソル
  • IDC_ARROW ... 標準的な矢印のカーソル
  • IDC_CROSS ... 十字線カーソル
  • IDC_HAND ... 手のカーソル
  • IDC_HELP ... 矢印と疑問符のカーソル
  • IDC_IBEAM ... Iビームのカーソル
  • IDC_UPARROW ... 垂直矢印のカーソル
  • IDC_WAIT ... 砂時計のカーソル

ここで紹介したカーソルは一部です。

RegisterClassW関数

pub unsafe fn RegisterClassW(lpwndclass: *const WNDCLASSW) -> u16

この関数はCreateWindowExW関数で使用するウィンドウクラスを登録します。引数はWNDCLASSWへのポインタになっています。この関数は成功するとアトムを返します。アトムとは整数のことで、これは登録されているウィンドウクラスを一意に識別するものです。関数が失敗した場合は0を返します。
アプリケーションが登録するウィンドウクラスは終了と同時に登録が解除されますがDLLがアンロードされる場合は異なり、登録解除されません。DLLの場合は明示的な解除が必要です。

CreateWindowExW関数

Function windows::Win32::UI::WindowsAndMessaging::CreateWindowExW

pub unsafe fn CreateWindowExW<P0, P1, P2, P3, P4>(
    dwexstyle: WINDOW_EX_STYLE,  // ウィンドウの拡張スタイル
    lpclassname: P0,  // 作成したウィンドウクラス名
    lpwindowname: P1,  // ウィンドウの名前
    dwstyle: WINDOW_STYLE,  // ウィンドウのスタイル
    x: i32,  // ウィンドウのx座標
    y: i32,  // ウィンドウのy座標
    nwidth: i32,  // ウィンドウの横幅(指定する値はx座標 + 横幅)
    nheight: i32,  // ウィンドウの高さ(指定する値はy座標 + 高さ)
    hwndparent: P2,  // 親ウィンドウのハンドル
    hmenu: P3,  // メニューへのハンドル
    hinstance: P4,  // ウィンドウに関連付けるモジュールのハンドル
    lpparam: Option<*const c_void>  // CREATESTRUCT構造体 (lpCreateParams メンバー) を介してウィンドウに渡される値へのポインター
) -> HWND
where
    P0: IntoParam<PCWSTR>,
    P1: IntoParam<PCWSTR>,
    P2: IntoParam<HWND>,
    P3: IntoParam<HMENU>,
    P4: IntoParam<HMODULE>,

この関数は拡張されたウィンドウスタイルを使いウィンドウを作成します。また拡張されたスタイルであるdwexstyleの他にも普通のスタイル指定であるdwstyleも引数に存在します。

WINDOW_EX_STYLE

Struct windows::Win32::UI::WindowsAndMessaging::WINDOW_EX_STYLE

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

WINDOW_EX_STYLEはタプル構造体でu32のラッパーです。u32は符号なしの32ビット(4バイト)整数です。この構造体もビット演算子を実装しているのでOR演算で複数のスタイルを指定可能です。スタイルの定数は

  • windows::Win32::UI::WindowsAndMessaging
  • WS_EX_ACCEPTFILES ... ウィンドウがドラッグ&ドロップでファイルを受け入れるようになる
  • WS_EX_APPWINDOW ... ウィンドウが表示されるとタスクバーに最上位のウィンドウを表示する
  • WS_EX_COMPOSITED ... ダブルバッファリングを使用する
  • WS_EX_LEFT ... 左揃えプロパティ(規定値)
  • WS_EX_RIGHT ... 右揃えプロパティ

などがありますが、これらは一部でまだ存在します。

WINDOW_STYLE

Struct windows::Win32::UI::WindowsAndMessaging::WINDOW_STYLE

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

WINDOW_STYLEもタプル構造体でu32のラッパーです。これもビット演算子を実装しています。スタイルの定数は

  • windows::Win32::UI::WindowsAndMessaging
  • WS_OVERLAPPEDWINDOW ... 重なり合ったウィンドウ
  • WS_VISIBLE ... ウィンドウが最初に表示される
  • WS_MINIMIZE ... ウィンドウが最初に最小化される
  • WS_MAXIMIZE ... ウィンドウが最初に最大化される
  • WS_HSCROLL ... 水平スクロールバー
  • WS_VSCROLL ... 垂直スクロールバー

などがありこれらは一部です。

GetMessageW関数

Function windows::Win32::UI::WindowsAndMessaging::GetMessageW

pub unsafe fn GetMessageW<P0>(
    lpmsg: *mut MSG,  // MSG構造体へのポインタ
    hwnd: P0,  // メッセージを取得するウィンドウのハンドル
    wmsgfiltermin: u32,  // 取得するメッセージの最小値
    wmsgfiltermax: u32  // 取得するメッセージの最大値
) -> BOOL
where
    P0: IntoParam<HWND>,

この関数はメッセージキューからメッセージを取得します。第1引数のlpmsgにメッセージが保存されます。取得したメッセージはDispatchMessageW関数でウィンドウプロシージャに送ることができます。この関数はWM_QUITメッセージを取得した場合は返り値は0になります。そのほかは0以外です。エラーが発生した場合は-1を返します。hwndが無効だったりlpmsgが不正なポインターだったらエラーになります。返り値はBOOL型ですが、これはi32をラップしたタプル構造体です。

MSG構造体

Struct windows::Win32::UI::WindowsAndMessaging::MSG

#[repr(C)]
pub struct MSG {
    pub hwnd: HWND,  // ウィンドウプロシージャがメッセージを受信するウィンドウのハンドル
    pub message: u32,  // メッセージID
    pub wParam: WPARAM,  // メッセージの追加情報(メッセージIDにより変わる)
    pub lParam: LPARAM,  // メッセージの追加情報(メッセージIDにより変わる
    pub time: u32,  // メッセージが投稿された時刻
    pub pt: POINT,  // メッセージが投稿されたときのカーソル位置
}

MSG構造体はメッセージキューのメッセージの情報を保存する構造体です。この構造体のメッセージID(message)がウィンドウプロシージャに送られます。

POINT構造体

Struct windows::Win32::Foundation::POINT

#[repr(C)]
pub struct POINT {
    pub x: i32,  // x座標
    pub y: i32,  // y座標
}

POINT構造体はこのような構造体になっています。

DispatchMessageW関数

Function windows::Win32::UI::WindowsAndMessaging::DispatchMessageW

pub unsafe fn DispatchMessageW(lpmsg: *const MSG) -> LRESULT

この関数は引数のポインタのMSGのメッセージをウィンドウプロシージャに送ります。返り値はウィンドウプロシージャによって返される値によって異なります。普通はこの戻り値は無視されます。

ウィンドウプロシージャ関数

この関数はWNDPROC型に依存しています。Rustでは以下の様に定義されています。

pub type WNDPROC = Option<
    unsafe extern "system" fn(
        param0: HWND,  // ウィンドウのハンドル
        param1: u32,  // メッセージ
        param2: WPARAM,  // 追記のメッセージ情報(メッセージによって変わる)
        param3: LPARAM  // 追加のメッセージ情報(メッセージによって変わる)
    ) -> LRESULT
>;

これは関数の型エイリアスです。C言語で言うところの関数ポインタ型になります。この関数の返り値はメッセージの処理の結果になるのでウィンドウプロシージャ内の処理によって変わります。

WPARAM, LPARAM

Struct windows::Win32::Foundation::WPARAM

#[repr(transparent)]
pub struct WPARAM(pub usize);

Struct windows::Win32::Foundation::LPARAM

#[repr(transparent)]
pub struct LPARAM(pub isize);

これらはusizeisizeのラッパーのタプル構造体になります。この整数値は通常、数値かまたは構造体へのポインタになります。

LRESULT

Struct windows::Win32::Foundation::LRESULT

#[repr(transparent)]
pub struct LRESULT(pub isize);

LRESULTisizeのラッパーでタプル構造体です。LRESULTの意味的な定義は「プログラムがWindowsに返す整数値」とされています。ウィンドウプロシージャではこのLRESULTを処理ごとに返します。DispatchMessageW関数の返り値がこのLRESULT型になっていることに注意してください。



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