ウィンドウの作成 - Rustで作るWindowsアプリ
目次
ウィンドウを作る
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), } } }
上記のプログラムを実行すると以下のようなウィンドウが表示されます。
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_STYLES
はu32
の整数です。この構造体にはビット演算が実装されており、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>,
定義済みのカーソルのいずれかを使用するにはhInstance
をNone
にして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);
これらはusize
とisize
のラッパーのタプル構造体になります。この整数値は通常、数値かまたは構造体へのポインタになります。
LRESULT
Struct windows::Win32::Foundation::LRESULT #[repr(transparent)] pub struct LRESULT(pub isize);
LRESULT
もisize
のラッパーでタプル構造体です。LRESULT
の意味的な定義は「プログラムがWindowsに返す整数値」とされています。ウィンドウプロシージャではこのLRESULT
を処理ごとに返します。DispatchMessageW
関数の返り値がこのLRESULT
型になっていることに注意してください。