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(())
}