RustのwindowsクレートのDialogBoxParamのメモ。
Win32 APIのDialogBoxParamは、ダイアログを表示する関数。
具体的にはDialogBoxParamAとDialogBoxParamW関数。
ダイアログのレイアウトはリソースで定義する。
→RustでWindowsリソースを扱う方法
windowsクレートのDialogBoxParamを使うには、Win32_UI_WindowsAndMessagingフィーチャーを指定する。
〜 [dependencies] windows = { version = "0.61.3", features = ["Win32_UI_WindowsAndMessaging"] }
ダイアログの定義を作り、DialogBoxParamW関数で表示する例。
まず、ダイアログのレイアウトを拡張子rcのテキストファイルで定義する。
テキストファイルなので手で書くことも可能だろうが、ダイアログのレイアウトをテキストベースで修正したくないので、GUIのエディターを使いたい。
Visutal Studio Community 2022にはGUIのダイアログエディターがあるので、それを使うのが良さそう。
Visutal Studio Community 2022を起動し、新しいプロジェクトを作成する。
テンプレートは「空のプロジェクト(C++)」。
(このテンプレートを使うには、Visutal Studio Community 2022のインストール時に「C++によるデスクトップ開発」を選択しておく必要がある)
プロジェクトを作成したら、ダイアログを新設する。
リソースの初期状態は以下のような感じ(抜粋)。
〜 #include "resource.h" 〜 ///////////////////////////////////////////////////////////////////////////// // // Dialog // IDD_DIALOG1 DIALOGEX 0, 0, 308, 176 STYLE DS_SETFONT | DS_MODALFRAME | DS_FIXEDSYS | WS_POPUP | WS_CAPTION | WS_SYSMENU CAPTION "Dialog" FONT 8, "MS Shell Dlg", 400, 0, 0x1 BEGIN DEFPUSHBUTTON "OK",IDOK,199,156,50,14 PUSHBUTTON "キャンセル",IDCANCEL,253,156,50,14 END 〜
〜 #define IDD_DIALOG1 101 〜
このダイアログ(IDD_DIALOG1)のIDは101として定義されている。
(DialogBoxParamW関数のダイアログIDにこの値を指定する)
ダイアログが定義されたrcファイルをコンパイルして拡張子resのリソースファイルを作成し、Rustアプリケーションのビルド時にリンクする。
手動でresファイルを作ってリンクする方法や、rcファイルをコンパイルしてリンクしてくれるクレートを使う方法がある。
ここではembed-resourceクレートを使ってみる。
〜 [build-dependencies] embed-resource = "3.0.5"
まず、rcファイルとresource.hをRustプロジェクトのresourceディレクトリーの下にコピーしておく。
それから、build.rsを定義する。
extern crate embed_resource; fn main() { embed_resource::compile("resource/Project1.rc", embed_resource::NONE) .manifest_optional() .unwrap(); }
ダイアログを表示するためのDialogBoxParamW関数を呼び出す。
use windows::{ Win32::{ Foundation::{GetLastError, HWND, LPARAM, WPARAM}, UI::WindowsAndMessaging::*, }, core::PCWSTR, };
fn main() { let dialog_id = PCWSTR(101 as *const u16); // IDD_DIALOG1 unsafe { let rc = DialogBoxParamW(None, dialog_id, None, Some(dialog_proc), LPARAM(0)); if rc < 0 { println!("DialogBoxParamW failed: {:?}", GetLastError()); } else { println!("DialogBoxParamW rc={}", rc); } } }
ダイアログIDには、rcファイル(resource.h)で定義されているID(IDD_DIALOG1の値)を指定する。
dialog_procは、ダイアログのウィンドウメッセージを処理する(自分の)関数。
DialogBoxParamWの戻り値は、-1ならエラー。
それ以外は、dialog_procのEndDialogによってセットされた値。
OKボタンが押されたら1(IDOKの値)、キャンセルされたときは2(IDCANCELの値)が返るようにしてある。
unsafe extern "system" fn dialog_proc( hwnd: HWND, message: u32, wparam: WPARAM, _lparam: LPARAM, ) -> isize { const TRUE: isize = 1; const FALSE: isize = 0; match message { // WM_INITDIALOG => { // println!("Dialog initialized"); // TRUE // } WM_COMMAND => { let id = wparam.0 as i32; if id == IDOK.0 || id == IDCANCEL.0 { unsafe { let _ = EndDialog(hwnd, id as isize); } TRUE } else { FALSE } } _ => FALSE, } }
上記の例では、ダイアログは画面の左上(座標(0, 0)
)に表示される。
これは、rcファイルで「DIALOGEX
0, 0
」と定義されているため。
画面の中央に表示するには、WM_INITDIALOGメッセージ内でダイアログの位置を設定する。
unsafe extern "system" fn dialog_proc(〜) -> isize {
〜
match message {
WM_INITDIALOG => match init_dialog(hwnd) {
Ok(_) => TRUE,
Err(e) => {
println!("init_dialog failed. {:?}", e);
FALSE
}
},
〜
}
〜
}
fn init_dialog(hwnd: HWND) -> Result<(), windows::core::Error> { use windows::Win32::Foundation::RECT; unsafe { let parent_hwnd = match GetParent(hwnd) { Ok(value) => value, Err(_) => GetDesktopWindow(), }; let mut rect = RECT::default(); GetWindowRect(hwnd, &mut rect)?; let mut parent_rect = RECT::default(); GetWindowRect(parent_hwnd, &mut parent_rect)?; let width = rect.right - rect.left; let height = rect.bottom - rect.top; let parent_width = parent_rect.right - parent_rect.left; let parent_height = parent_rect.bottom - parent_rect.top; let x = parent_rect.left + (parent_width - width) / 2; let y = parent_rect.top + (parent_height - height) / 2; SetWindowPos(hwnd, None, x, y, 0, 0, SWP_NOZORDER | SWP_NOSIZE)?; } Ok(()) }
親ウィンドウの位置とダイアログのサイズを元に、座標を地道に計算する^^;
ダイアログにテキストの入力フィールドを追加し、Rustアプリケーションでその値を取得する例。
ちなみに、rcファイルは以下のような感じになった。
〜
IDD_DIALOG1 DIALOGEX 0, 0, 307, 176
STYLE DS_SETFONT | DS_MODALFRAME | DS_FIXEDSYS | WS_POPUP | WS_CAPTION | WS_SYSMENU
CAPTION "Example"
FONT 8, "MS Shell Dlg", 400, 0, 0x1
BEGIN
DEFPUSHBUTTON "OK",IDOK,199,156,50,14
PUSHBUTTON "キャンセル",IDCANCEL,253,156,50,14
LTEXT "ユーザー",IDC_STATIC,27,23,28,8
EDITTEXT IDC_USER,71,21,117,14,ES_AUTOHSCROLL
LTEXT "パスワード",IDC_STATIC,27,44,34,8
EDITTEXT IDC_PASSWORD,71,42,117,14,ES_PASSWORD | ES_AUTOHSCROLL
END
〜
〜
#define IDD_DIALOG1 101
#define IDC_USER 1001
#define IDC_PASSWORD 1002
〜
DialogBoxParamW関数の引数LPARAMで、入力フィールドの値を受け取る構造体を渡す。
で、dialog_procのWM_COMMANDで、OKボタンが押されたときにその構造体に値をセットする。
#[derive(Debug)] struct MyDialogValue { user: Option<String>, password: Option<String>, } impl Default for MyDialogValue { fn default() -> Self { Self { user: None, password: None, } } }
fn main() { let dialog_id = PCWSTR(101 as *const u16); // IDD_DIALOG1 let dialog_value = Box::new(MyDialogValue::default()); let dialog_value_ptr = Box::into_raw(dialog_value); unsafe { let rc = DialogBoxParamW( None, dialog_id, None, Some(dialog_proc), LPARAM(dialog_value_ptr as isize), ); let dialog_value = Box::from_raw(dialog_value_ptr); if rc < 0 { println!("DialogBoxParamW failed: {:?}", GetLastError()); } else { println!("DialogBoxParamW rc={}, dialog_value={:?}", rc, dialog_value); } } }
ダイアログの値を受け取る構造体MyDialogValue(のポインター)を、DialogBoxParamW関数のLPARAMで渡す。
DialogBoxParamW関数で渡したLPARAMは、WM_INITDIALOGイベントで受け取り、SetWindowLongPtrW関数でGWLP_USERDATAに保存しておく。
(WM_COMMANDイベントで渡ってくるLPARAMは、最初に渡したLPARAMとは異なる値なので)
unsafe extern "system" fn dialog_proc(〜) -> isize { 〜 match message { WM_INITDIALOG => { unsafe { SetWindowLongPtrW(hwnd, GWLP_USERDATA, lparam.0) }; 〜 } WM_COMMAND => { let id = wparam.0 as i32; if id == IDOK.0 || id == IDCANCEL.0 { if id == IDOK.0 { dialog_result(hwnd); } unsafe { let _ = EndDialog(hwnd, id as isize); } TRUE } else { FALSE } } 〜 } 〜 }
OKボタンが押されたらdialog_resultを呼ぶようにする。
dialog_resultでは、ダイアログの入力フィールドの値を取得してMyDialogValueにセットする。
fn dialog_result(hwnd: HWND) { let dialog_value = unsafe { let ptr = GetWindowLongPtrW(hwnd, GWLP_USERDATA) as *mut MyDialogValue; &mut *ptr }; const IDC_USER: i32 = 1001; let user = text_value(hwnd, IDC_USER); dialog_value.user = Some(user); const IDC_PASSWORD: i32 = 1002; let password = text_value(hwnd, IDC_PASSWORD); dialog_value.password = Some(password); }
GetWindowLongPtrW関数で、GWLP_USERDATAからMyDialogValue(のポインター)を取得する。
fn text_value(hwnd: HWND, id: i32) -> String { use std::{ffi::OsString, os::windows::ffi::OsStringExt}; let mut buf = [0u16; 1024]; let len = unsafe { GetDlgItemTextW(hwnd, id, &mut buf) }; let widestring = OsString::from_wide(&buf[..len as usize]); let s = widestring.to_string_lossy(); s.into() }
GetDlgItemTextW関数は、ダイアログからテキストを取得する。
GetDlgItemTextW関数の返り値は取得した文字数。
フィールドのテキストの長さがバッファーサイズ以上だった場合は「バッファーサイズ -
1(NUL終端の分)」が返る。つまり、バッファーに入りきらない分はカットされる。
ダイアログのテキスト入力フィールドに、Rustアプリケーションから初期値をセットする例。
WM_INITDIALOGイベントで、SetDlgItemTextW関数を使って初期値をセットする。
fn main() {
〜
let dialog_value = Box::new(MyDialogValue { // ダイアログの初期値
user: Some("hishidama".into()),
password: Some("password".into()),
});
〜
}
fn init_dialog(hwnd: HWND, lparam: LPARAM) -> Result<(), windows::core::Error> { let dialog_value = unsafe { let ptr = lparam.0 as *mut MyDialogValue; &*ptr }; if let Some(s) = &dialog_value.user { const IDC_USER: i32 = 1001; set_text_value(hwnd, IDC_USER, s)?; } if let Some(s) = &dialog_value.password { const IDC_PASSWORD: i32 = 1002; set_text_value(hwnd, IDC_PASSWORD, s)?; } 〜 }
fn set_text_value(hwnd: HWND, id: i32, value: &str) -> Result<(), windows::core::Error> { let mut v: Vec<u16> = value.encode_utf16().collect(); v.push(0); // NUL終端 unsafe { SetDlgItemTextW(hwnd, id, PCWSTR(v.as_ptr()))? }; Ok(()) }