├── .gitignore ├── examples ├── bmp_one │ ├── resource.h │ ├── bmp_one.rc │ ├── ball.bmp │ ├── src │ │ ├── build.rs │ │ └── main.rs │ └── Cargo.toml ├── dlg_three │ ├── resource.h │ ├── src │ │ ├── build.rs │ │ └── main.rs │ ├── Cargo.toml │ └── dlg_three.rc ├── menu_one │ ├── menu_one.ico │ ├── src │ │ ├── build.rs │ │ └── main.rs │ ├── resource.h │ ├── Cargo.toml │ └── menu_one.rc ├── ctl_one │ ├── src │ │ ├── build.rs │ │ └── main.rs │ ├── resource.h │ ├── Cargo.toml │ └── ctl_one.rc ├── dlg_one │ ├── src │ │ ├── build.rs │ │ └── main.rs │ ├── resource.h │ ├── Cargo.toml │ └── dlg_one.rc ├── dlg_two │ ├── src │ │ ├── build.rs │ │ └── main.rs │ ├── resource.h │ ├── Cargo.toml │ └── dlg_two.rc ├── intro.rs ├── window_click.rs ├── simple_window.rs ├── app_one.rs ├── menu_two.rs ├── bmp_two.rs ├── anim_one.rs ├── app_two.rs └── app_three.rs ├── src ├── font.rs ├── gdi │ ├── mod.rs │ ├── brush.rs │ ├── bitmap.rs │ └── canvas.rs ├── toolbar.rs ├── rect.rs ├── cursor.rs ├── menu.rs ├── icon.rs ├── lib.rs ├── messagebox.rs ├── message.rs ├── dialog.rs ├── class.rs └── window.rs ├── Cargo.toml └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | Cargo.lock 3 | -------------------------------------------------------------------------------- /examples/bmp_one/resource.h: -------------------------------------------------------------------------------- 1 | #define IDB_BALL 101 2 | -------------------------------------------------------------------------------- /examples/dlg_three/resource.h: -------------------------------------------------------------------------------- 1 | #define IDC_STATIC -1 2 | #define IDD_MAIN 101 3 | -------------------------------------------------------------------------------- /examples/bmp_one/bmp_one.rc: -------------------------------------------------------------------------------- 1 | #include "resource.h" 2 | 3 | IDB_BALL BITMAP "ball.bmp" 4 | -------------------------------------------------------------------------------- /examples/bmp_one/ball.bmp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lonami/rust-windows-gui/HEAD/examples/bmp_one/ball.bmp -------------------------------------------------------------------------------- /examples/menu_one/menu_one.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lonami/rust-windows-gui/HEAD/examples/menu_one/menu_one.ico -------------------------------------------------------------------------------- /examples/bmp_one/src/build.rs: -------------------------------------------------------------------------------- 1 | use embed_resource; 2 | 3 | fn main() { 4 | embed_resource::compile("bmp_one.rc"); 5 | } 6 | -------------------------------------------------------------------------------- /examples/ctl_one/src/build.rs: -------------------------------------------------------------------------------- 1 | use embed_resource; 2 | 3 | fn main() { 4 | embed_resource::compile("ctl_one.rc"); 5 | } 6 | -------------------------------------------------------------------------------- /examples/dlg_one/src/build.rs: -------------------------------------------------------------------------------- 1 | use embed_resource; 2 | 3 | fn main() { 4 | embed_resource::compile("dlg_one.rc"); 5 | } 6 | -------------------------------------------------------------------------------- /examples/dlg_two/src/build.rs: -------------------------------------------------------------------------------- 1 | use embed_resource; 2 | 3 | fn main() { 4 | embed_resource::compile("dlg_two.rc"); 5 | } 6 | -------------------------------------------------------------------------------- /examples/dlg_three/src/build.rs: -------------------------------------------------------------------------------- 1 | use embed_resource; 2 | 3 | fn main() { 4 | embed_resource::compile("dlg_three.rc"); 5 | } 6 | -------------------------------------------------------------------------------- /examples/menu_one/src/build.rs: -------------------------------------------------------------------------------- 1 | use embed_resource; 2 | 3 | fn main() { 4 | embed_resource::compile("menu_one.rc"); 5 | } 6 | -------------------------------------------------------------------------------- /examples/dlg_one/resource.h: -------------------------------------------------------------------------------- 1 | #define IDC_STATIC -1 2 | #define IDR_MYMENU 101 3 | #define IDD_ABOUT 102 4 | 5 | #define ID_FILE_EXIT 40001 6 | #define ID_HELP_ABOUT 40002 7 | -------------------------------------------------------------------------------- /examples/menu_one/resource.h: -------------------------------------------------------------------------------- 1 | #define IDR_MYMENU 101 2 | #define IDI_MYICON 102 3 | 4 | #define ID_FILE_EXIT 9001 5 | #define ID_STUFF_GO 9002 6 | -------------------------------------------------------------------------------- /examples/dlg_two/resource.h: -------------------------------------------------------------------------------- 1 | #define IDR_MYMENU 101 2 | #define IDD_TOOLBAR 101 3 | 4 | #define IDC_PRESS 1000 5 | #define IDC_OTHER 1001 6 | 7 | #define ID_FILE_EXIT 40001 8 | #define ID_DIALOG_SHOW 40002 9 | #define ID_DIALOG_HIDE 40003 10 | -------------------------------------------------------------------------------- /examples/ctl_one/resource.h: -------------------------------------------------------------------------------- 1 | #define IDC_STATIC -1 2 | #define IDD_MAIN 101 3 | 4 | #define IDC_TEXT 1000 5 | #define IDC_NUMBER 1001 6 | #define IDC_LIST 1002 7 | #define IDC_ADD 1003 8 | #define IDC_CLEAR 1004 9 | #define IDC_REMOVE 1005 10 | #define IDC_SHOWCOUNT 1006 11 | -------------------------------------------------------------------------------- /examples/bmp_one/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "bmp_one" 3 | version = "0.1.0" 4 | authors = ["Lonami Exo "] 5 | edition = "2018" 6 | build = "src/build.rs" 7 | 8 | [target.'cfg(windows)'.dependencies] 9 | minimal-windows-gui = "*" 10 | 11 | [target.'cfg(windows)'.build-dependencies] 12 | embed-resource = "1.3.3" 13 | -------------------------------------------------------------------------------- /examples/ctl_one/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "ctl_one" 3 | version = "0.1.0" 4 | authors = ["Lonami Exo "] 5 | edition = "2018" 6 | build = "src/build.rs" 7 | 8 | [target.'cfg(windows)'.dependencies] 9 | minimal-windows-gui = "*" 10 | 11 | [target.'cfg(windows)'.build-dependencies] 12 | embed-resource = "1.3.3" 13 | -------------------------------------------------------------------------------- /examples/dlg_one/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "dlg_one" 3 | version = "0.1.0" 4 | authors = ["Lonami Exo "] 5 | edition = "2018" 6 | build = "src/build.rs" 7 | 8 | [target.'cfg(windows)'.dependencies] 9 | minimal-windows-gui = "*" 10 | 11 | [target.'cfg(windows)'.build-dependencies] 12 | embed-resource = "1.3.3" 13 | -------------------------------------------------------------------------------- /examples/dlg_three/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "dlg_three" 3 | version = "0.1.0" 4 | authors = ["Lonami Exo "] 5 | edition = "2018" 6 | build = "src/build.rs" 7 | 8 | [target.'cfg(windows)'.dependencies] 9 | minimal-windows-gui = "*" 10 | 11 | [target.'cfg(windows)'.build-dependencies] 12 | embed-resource = "1.3.3" 13 | -------------------------------------------------------------------------------- /examples/dlg_two/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "dlg_two" 3 | version = "0.1.0" 4 | authors = ["Lonami Exo "] 5 | edition = "2018" 6 | build = "src/build.rs" 7 | 8 | [target.'cfg(windows)'.dependencies] 9 | minimal-windows-gui = "*" 10 | 11 | [target.'cfg(windows)'.build-dependencies] 12 | embed-resource = "1.3.3" 13 | -------------------------------------------------------------------------------- /examples/menu_one/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "menu_one" 3 | version = "0.1.0" 4 | authors = ["Lonami Exo "] 5 | edition = "2018" 6 | build = "src/build.rs" 7 | 8 | [target.'cfg(windows)'.dependencies] 9 | minimal-windows-gui = "*" 10 | 11 | [target.'cfg(windows)'.build-dependencies] 12 | embed-resource = "1.3.3" 13 | -------------------------------------------------------------------------------- /examples/menu_one/menu_one.rc: -------------------------------------------------------------------------------- 1 | #include "resource.h" 2 | 3 | IDR_MYMENU MENU 4 | BEGIN 5 | POPUP "&File" 6 | BEGIN 7 | MENUITEM "E&xit", ID_FILE_EXIT 8 | END 9 | 10 | POPUP "&Stuff" 11 | BEGIN 12 | MENUITEM "&Go", ID_STUFF_GO 13 | MENUITEM "G&o somewhere else", 0, GRAYED 14 | END 15 | END 16 | 17 | IDI_MYICON ICON "menu_one.ico" 18 | -------------------------------------------------------------------------------- /examples/dlg_three/dlg_three.rc: -------------------------------------------------------------------------------- 1 | #include "windows.h" 2 | #include "resource.h" 3 | 4 | IDD_MAIN DIALOG DISCARDABLE 0, 0, 186, 46 5 | STYLE DS_MODALFRAME | WS_POPUP | WS_CAPTION | WS_SYSMENU 6 | CAPTION "Dialog tricks" 7 | FONT 8, "MS Sans Serif" 8 | BEGIN 9 | DEFPUSHBUTTON "OK", IDOK, 129, 7, 50, 14 10 | LTEXT "Hi there.", IDC_STATIC, 43, 16, 28, 8 11 | END 12 | -------------------------------------------------------------------------------- /src/font.rs: -------------------------------------------------------------------------------- 1 | use winapi::shared::windef::HFONT; 2 | use winapi::um::wingdi::{GetStockObject, DEFAULT_GUI_FONT}; 3 | 4 | pub struct Font { 5 | font: HFONT, 6 | } 7 | 8 | pub fn get_default() -> Result { 9 | let result = unsafe { GetStockObject(DEFAULT_GUI_FONT as i32) }; 10 | if result.is_null() { 11 | Err(()) 12 | } else { 13 | Ok(Font { 14 | font: result as HFONT, 15 | }) 16 | } 17 | } 18 | 19 | impl Font { 20 | pub(crate) fn as_ptr(&self) -> HFONT { 21 | self.font 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/gdi/mod.rs: -------------------------------------------------------------------------------- 1 | //! Wrappers around the Graphics Device Interface. 2 | //! 3 | //! See also https://docs.microsoft.com/en-us/windows/win32/gdi/windows-gdi. 4 | pub mod bitmap; 5 | pub mod brush; 6 | pub mod canvas; 7 | 8 | use winapi::shared::windef::HGDIOBJ; 9 | 10 | /// The capability of objects that can be used to paint on a canvas. 11 | pub trait Paint { 12 | /// Interpret self as a GDI object and return a pointer to self. 13 | fn as_gdi_obj(&self) -> HGDIOBJ; 14 | } 15 | 16 | pub use bitmap::Bitmap; 17 | pub use brush::Brush; 18 | pub use canvas::Canvas; 19 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "minimal-windows-gui" 3 | version = "0.1.0" 4 | authors = ["Lonami Exo "] 5 | edition = "2018" 6 | 7 | [target.'cfg(windows)'.dependencies] 8 | once_cell = "1.4.1" 9 | widestring = "0.4.2" 10 | winapi = { version = "0.3", features = ["commctrl", "commdlg", "libloaderapi", "winuser"] } 11 | 12 | [workspace] 13 | members = [ 14 | "examples/menu_one", 15 | "examples/dlg_one", 16 | "examples/dlg_two", 17 | "examples/dlg_three", 18 | "examples/ctl_one", 19 | "examples/bmp_one", 20 | ] 21 | 22 | [patch.crates-io] 23 | mui = { path = ".", package = "minimal-windows-gui" } 24 | -------------------------------------------------------------------------------- /examples/dlg_two/dlg_two.rc: -------------------------------------------------------------------------------- 1 | #include "windows.h" 2 | #include "resource.h" 3 | 4 | IDR_MYMENU MENU 5 | BEGIN 6 | POPUP "&File" 7 | BEGIN 8 | MENUITEM "E&xit", ID_FILE_EXIT 9 | END 10 | POPUP "&Dialog" 11 | BEGIN 12 | MENUITEM "&Show", ID_DIALOG_SHOW 13 | MENUITEM "&Hide", ID_DIALOG_HIDE 14 | END 15 | END 16 | 17 | IDD_TOOLBAR DIALOGEX 0, 0, 98, 52 18 | STYLE DS_MODALFRAME | WS_POPUP | WS_CAPTION 19 | EXSTYLE WS_EX_TOOLWINDOW 20 | CAPTION "My Dialog Toolbar" 21 | FONT 8, "MS Sans Serif" 22 | BEGIN 23 | PUSHBUTTON "&Press This Button", IDC_PRESS, 7, 7, 84, 14 24 | PUSHBUTTON "&Or This One", IDC_OTHER, 7, 31, 84, 14 25 | END 26 | -------------------------------------------------------------------------------- /examples/intro.rs: -------------------------------------------------------------------------------- 1 | //! The simplest Win32 program. 2 | //! http://winprog.org/tutorial/start.html 3 | use minimal_windows_gui as win; 4 | 5 | // For a "Win32 GUI" project the entry point is the following: 6 | // 7 | // int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nCmdShow) 8 | // 9 | // In Rust we start with `main()` instead, and don't have access to those parameters. 10 | // 11 | // However, the base address of the memory image of the executable `hInstance` can be obtained 12 | // with `GetModuleHandle`, `hPrevInstance` is always 0, `lpCmdLine` can be obtained with either 13 | // `GetCommandLine` or `std::env::args`, and `nCmdShow` be obtained from `GetStartupInfo`, so 14 | // if those are needed they can be obtained. 15 | fn main() { 16 | win::messagebox::message_box("Note", "Goodbye, cruel world!", &[]).unwrap(); 17 | } 18 | -------------------------------------------------------------------------------- /examples/dlg_one/dlg_one.rc: -------------------------------------------------------------------------------- 1 | #include "windows.h" 2 | #include "resource.h" 3 | 4 | IDR_MYMENU MENU 5 | BEGIN 6 | POPUP "&File" 7 | BEGIN 8 | MENUITEM "E&xit", ID_FILE_EXIT 9 | END 10 | POPUP "&Help" 11 | BEGIN 12 | MENUITEM "&About...", ID_HELP_ABOUT 13 | END 14 | END 15 | 16 | IDD_ABOUT DIALOG DISCARDABLE 0, 0, 239, 66 17 | STYLE DS_MODALFRAME | WS_POPUP | WS_CAPTION | WS_SYSMENU 18 | CAPTION "My About Box" 19 | FONT 8, "MS Sans Serif" 20 | BEGIN 21 | DEFPUSHBUTTON "&OK", IDOK, 174, 18, 50, 14 22 | PUSHBUTTON "&Cancel", IDCANCEL, 174, 35, 50, 14 23 | GROUPBOX "About this program...", IDC_STATIC, 7, 7, 225, 52 24 | CTEXT "An example program showing how to use Dialog Boxes\r\n\r\nby Lonami (original by theForger)", 25 | IDC_STATIC, 16, 18, 144, 33 26 | END 27 | -------------------------------------------------------------------------------- /examples/ctl_one/ctl_one.rc: -------------------------------------------------------------------------------- 1 | #include "windows.h" 2 | #include "resource.h" 3 | 4 | IDD_MAIN DIALOG DISCARDABLE 0, 0, 207, 156 5 | STYLE DS_MODALFRAME | DS_CENTER | WS_POPUP | WS_CAPTION | WS_SYSMENU 6 | CAPTION "Controls One" 7 | FONT 8, "MS Sans Serif" 8 | BEGIN 9 | LTEXT "Add", IDC_STATIC, 7, 10, 14, 8 10 | EDITTEXT IDC_TEXT, 25, 7, 120, 14, ES_AUTOHSCROLL 11 | EDITTEXT IDC_NUMBER, 150, 7, 21, 14, ES_NUMBER 12 | LTEXT "times.", IDC_STATIC, 177, 10, 23, 8 13 | 14 | LISTBOX IDC_LIST, 7, 25, 138, 106, LBS_NOINTEGRALHEIGHT | 15 | LBS_EXTENDEDSEL | WS_VSCROLL | WS_TABSTOP 16 | 17 | PUSHBUTTON "&Add", IDC_ADD, 150, 30, 50, 14 18 | PUSHBUTTON "&Remove", IDC_REMOVE, 150, 47, 50, 14 19 | PUSHBUTTON "&Clear", IDC_CLEAR, 150, 63, 50, 14 20 | 21 | LTEXT "This item was added", IDC_STATIC, 7, 141, 66, 8 22 | CTEXT "-", IDC_SHOWCOUNT, 77, 141, 32, 8 23 | LTEXT "times", IDC_STATIC, 114, 141, 17, 8 24 | END 25 | -------------------------------------------------------------------------------- /src/gdi/brush.rs: -------------------------------------------------------------------------------- 1 | use std::ptr::NonNull; 2 | use winapi::shared::windef::{HBRUSH, HBRUSH__, HGDIOBJ}; 3 | use winapi::um::wingdi::{ 4 | CreateSolidBrush, DeleteObject, GetStockObject, LTGRAY_BRUSH, RGB, WHITE_BRUSH, 5 | }; 6 | 7 | #[derive(Debug)] 8 | pub struct Brush { 9 | brush: NonNull, 10 | stock: bool, 11 | } 12 | 13 | impl Brush { 14 | pub fn new_solid_rgb(r: u8, g: u8, b: u8) -> Option { 15 | let result = unsafe { CreateSolidBrush(RGB(r, g, b)) }; 16 | NonNull::new(result).map(|brush| Brush { 17 | brush, 18 | stock: false, 19 | }) 20 | } 21 | 22 | /// Value to be returned from a dialog procedure to use this brush. 23 | pub fn dlg_proc_value(&self) -> isize { 24 | self.as_ptr() as isize 25 | } 26 | 27 | pub(crate) fn as_ptr(&self) -> HBRUSH { 28 | self.brush.as_ptr() 29 | } 30 | } 31 | 32 | pub fn white() -> Result { 33 | let result = unsafe { GetStockObject(WHITE_BRUSH as i32) }; 34 | NonNull::new(result as HBRUSH) 35 | .ok_or(()) 36 | .map(|brush| Brush { brush, stock: true }) 37 | } 38 | 39 | pub fn light_gray() -> Result { 40 | let result = unsafe { GetStockObject(LTGRAY_BRUSH as i32) }; 41 | NonNull::new(result as HBRUSH) 42 | .ok_or(()) 43 | .map(|brush| Brush { brush, stock: true }) 44 | } 45 | 46 | impl Drop for Brush { 47 | fn drop(&mut self) { 48 | if self.stock { 49 | return; 50 | } 51 | 52 | let result = unsafe { DeleteObject(self.brush.as_ptr() as HGDIOBJ) }; 53 | if result == 0 { 54 | panic!("invalid handle or still selected into a DC"); 55 | } 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /examples/window_click.rs: -------------------------------------------------------------------------------- 1 | //! Add the capability to show the user what the name of our program is when they click on our window. 2 | //! http://winprog.org/tutorial/window_click.html 3 | use minimal_windows_gui as win; 4 | use std::process::exit; 5 | 6 | const CLASS_NAME: &str = "myWindowClass"; 7 | 8 | fn main() -> win::Result<()> { 9 | let class = &win::class::build() 10 | .load_icon(win::icon::Icon::Application)? 11 | .load_cursor(win::cursor::Cursor::Arrow)? 12 | .background(win::class::Background::Window) 13 | .load_small_icon(win::icon::Icon::Application)? 14 | .register(CLASS_NAME) 15 | .expect("window registration failed"); 16 | 17 | let window = win::window::build() 18 | .set_message_callback(main_window_callback) 19 | .add_extended_style(win::window::ExtendedStyle::ClientEdge) 20 | .add_style(win::window::Style::OverlappedWindow) 21 | .size(240, 120) 22 | .create(class, "The title of my window") 23 | .expect("window creation failed"); 24 | 25 | window.show_default(); 26 | window.update().unwrap(); 27 | 28 | exit(win::message_loop()) 29 | } 30 | 31 | fn main_window_callback( 32 | window: &win::window::Window, 33 | message: win::message::Message, 34 | ) -> Option { 35 | use win::message::Message; 36 | 37 | match message { 38 | Message::LeftMouseButtonDown(_info) => { 39 | let file_name = win::module_file_name().unwrap(); 40 | win::messagebox::message_box( 41 | "This program is:", 42 | &file_name.into_string().unwrap(), 43 | &[win::messagebox::Config::IconInformation], 44 | ) 45 | .unwrap(); 46 | } 47 | Message::Close => { 48 | window.destroy().unwrap(); 49 | } 50 | Message::Destroy => { 51 | win::post_quit_message(0); 52 | } 53 | _ => return None, 54 | } 55 | 56 | Some(0) 57 | } 58 | -------------------------------------------------------------------------------- /src/toolbar.rs: -------------------------------------------------------------------------------- 1 | use winapi::um::commctrl::{ 2 | STD_COPY, STD_CUT, STD_DELETE, STD_FILENEW, STD_FILEOPEN, STD_FILESAVE, STD_FIND, STD_HELP, 3 | STD_PASTE, STD_PRINT, STD_PRINTPRE, STD_PROPERTIES, STD_REDOW, STD_REPLACE, STD_UNDO, TBBUTTON, 4 | TBSTATE_ENABLED, TBSTYLE_BUTTON, 5 | }; 6 | 7 | // For IDB_STD_SMALL_COLOR. 8 | // https://docs.microsoft.com/en-us/windows/win32/controls/toolbar-standard-button-image-index-values 9 | #[repr(i32)] 10 | pub enum Icon { 11 | /// Copy operation. 12 | Copy = STD_COPY, 13 | 14 | /// Cut operation. 15 | Cut = STD_CUT, 16 | 17 | /// Delete operation. 18 | Delete = STD_DELETE, 19 | 20 | /// New file operation. 21 | FileNew = STD_FILENEW, 22 | 23 | /// Open file operation. 24 | FileOpen = STD_FILEOPEN, 25 | 26 | /// Save file operation. 27 | FileSave = STD_FILESAVE, 28 | 29 | /// Find operation. 30 | Find = STD_FIND, 31 | 32 | /// Help operation. 33 | Help = STD_HELP, 34 | 35 | /// Paste operation. 36 | Paste = STD_PASTE, 37 | 38 | /// Print operation. 39 | Print = STD_PRINT, 40 | 41 | /// Print preview operation. 42 | PrintPre = STD_PRINTPRE, 43 | 44 | /// Properties operation. 45 | Properties = STD_PROPERTIES, 46 | 47 | /// Redo operation. 48 | Redo = STD_REDOW, 49 | 50 | /// Replace operation. 51 | Replace = STD_REPLACE, 52 | 53 | /// Undo operation. 54 | Undo = STD_UNDO, 55 | } 56 | 57 | #[repr(transparent)] 58 | pub struct Button { 59 | _data: TBBUTTON, 60 | } 61 | 62 | impl Button { 63 | pub fn new(id: u16, icon: Icon) -> Self { 64 | Button { 65 | _data: TBBUTTON { 66 | iBitmap: icon as i32, 67 | idCommand: id as i32, 68 | fsState: TBSTATE_ENABLED, 69 | fsStyle: TBSTYLE_BUTTON as u8, 70 | bReserved: [0; 6], 71 | dwData: 0, 72 | iString: 0, 73 | }, 74 | } 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /examples/dlg_three/src/main.rs: -------------------------------------------------------------------------------- 1 | //! Additional dialog messages without using a main window. 2 | //! http://winprog.org/tutorial/dlgfaq.html 3 | use std::cell::Cell; 4 | use std::process::exit; 5 | use minimal_windows_gui as win; 6 | 7 | const IDD_MAIN: u16 = 101; 8 | 9 | thread_local! { 10 | static BACKGROUND_BRUSH: Cell> = Cell::new(None); 11 | } 12 | 13 | fn main() -> win::Result<()> { 14 | exit(win::dialog::show(IDD_MAIN, dialog_callback).unwrap() as i32) 15 | } 16 | 17 | fn dialog_callback(dialog: &win::window::Window, message: win::message::Message) -> isize { 18 | use win::message::Message; 19 | use win::messagebox::Button; 20 | 21 | match message { 22 | Message::InitDialog => { 23 | let brush = win::gdi::brush::Brush::new_solid_rgb(0, 0, 0).unwrap(); 24 | BACKGROUND_BRUSH.with(|cell| cell.set(Some(brush))); 25 | 26 | dialog.set_icon(win::icon::Icon::Application).unwrap(); 27 | dialog.set_small_icon(win::icon::Icon::Application).unwrap(); 28 | } 29 | Message::Close => { 30 | dialog.end_dialog(0).unwrap(); 31 | } 32 | Message::ControlColorDialog(_info) => { 33 | let mut result = 0; 34 | BACKGROUND_BRUSH.with(|cell| { 35 | let brush = cell.take(); 36 | result = brush.as_ref().unwrap().dlg_proc_value(); 37 | cell.set(brush); 38 | }); 39 | return result; 40 | } 41 | Message::ControlColorStatic(info) => { 42 | info.set_text_color(255, 255, 255).unwrap(); 43 | info.set_background_transparency(true).unwrap(); 44 | 45 | let mut result = 0; 46 | BACKGROUND_BRUSH.with(|cell| { 47 | let brush = cell.take(); 48 | result = brush.as_ref().unwrap().dlg_proc_value(); 49 | cell.set(brush); 50 | }); 51 | return result; 52 | } 53 | Message::Command(info) => match info.control_data().map(|c| c.std_button()).flatten() { 54 | Some(Button::Ok) => dialog.end_dialog(0).unwrap(), 55 | _ => {} 56 | }, 57 | Message::Destroy => { 58 | BACKGROUND_BRUSH.with(|cell| drop(cell.take().take())); 59 | } 60 | _ => return 0, 61 | } 62 | 63 | 1 64 | } 65 | -------------------------------------------------------------------------------- /examples/menu_one/src/main.rs: -------------------------------------------------------------------------------- 1 | //! Shows how to add basic menus to your window. Usually pre-made menu resources are used. 2 | //! These will be in an `.rc` file, which will be compiled and linked into the `.exe`. 3 | //! http://winprog.org/tutorial/menus.html 4 | use std::process::exit; 5 | use minimal_windows_gui as win; 6 | 7 | const CLASS_NAME: &str = "myWindowClass"; 8 | 9 | const IDR_MYMENU: u16 = 101; 10 | const IDI_MYICON: u16 = 102; 11 | 12 | const ID_FILE_EXIT: u16 = 9001; 13 | const ID_STUFF_GO: u16 = 9002; 14 | 15 | fn main() -> win::Result<()> { 16 | let class = &win::class::build() 17 | .load_icon(win::icon::Icon::FromResource(IDI_MYICON))? 18 | .load_cursor(win::cursor::Cursor::Arrow)? 19 | .background(win::class::Background::Window) 20 | .menu(IDR_MYMENU) 21 | .load_small_icon(win::icon::Icon::FromResource(IDI_MYICON))? 22 | .register(CLASS_NAME) 23 | .expect("window registration failed"); 24 | 25 | let window = win::window::build() 26 | .set_message_callback(main_window_callback) 27 | .add_extended_style(win::window::ExtendedStyle::ClientEdge) 28 | .add_style(win::window::Style::OverlappedWindow) 29 | .size(240, 120) 30 | .create(class, "A Menu") 31 | .expect("window creation failed"); 32 | 33 | window.show_default(); 34 | window.update().unwrap(); 35 | 36 | exit(win::message_loop()) 37 | } 38 | 39 | fn main_window_callback( 40 | window: &win::window::Window, 41 | message: win::message::Message, 42 | ) -> Option { 43 | use win::message::Message; 44 | 45 | match message { 46 | Message::Command(info) => { 47 | if let Some(menu_id) = info.menu_id() { 48 | match menu_id { 49 | ID_FILE_EXIT => { 50 | window.close().unwrap(); 51 | } 52 | ID_STUFF_GO => { 53 | win::messagebox::message_box("Woo!", "You clicked Go!", &[]).unwrap(); 54 | } 55 | _ => {} 56 | } 57 | } 58 | } 59 | Message::Close => { 60 | window.destroy().unwrap(); 61 | } 62 | Message::Destroy => { 63 | win::post_quit_message(0); 64 | } 65 | _ => return None, 66 | } 67 | 68 | Some(0) 69 | } 70 | -------------------------------------------------------------------------------- /examples/simple_window.rs: -------------------------------------------------------------------------------- 1 | //! (Mostly) the simplest windows program you can write that actually creates a functional window. 2 | //! http://winprog.org/tutorial/simple_window.html 3 | use std::process::exit; 4 | use minimal_windows_gui as win; 5 | 6 | const CLASS_NAME: &str = "myWindowClass"; 7 | 8 | fn main() -> win::Result<()> { 9 | // Step 1: Registering the Window Class 10 | let class = &match win::class::build() 11 | .load_icon(win::icon::Icon::Application)? 12 | .load_cursor(win::cursor::Cursor::Arrow)? 13 | .background(win::class::Background::Window) 14 | .load_small_icon(win::icon::Icon::Application)? 15 | .register(CLASS_NAME) 16 | { 17 | Ok(cls) => cls, 18 | Err(e) => { 19 | win::messagebox::message_box( 20 | "Error!", 21 | &format!("Window Registration Failed! {}", e), 22 | &[win::messagebox::Config::IconWarning], 23 | ) 24 | .unwrap(); 25 | exit(0); 26 | } 27 | }; 28 | 29 | // Step 2: Creating the Window 30 | let window = match win::window::build() 31 | .set_message_callback(main_window_callback) 32 | .add_extended_style(win::window::ExtendedStyle::ClientEdge) 33 | .add_style(win::window::Style::OverlappedWindow) 34 | .size(240, 120) 35 | .create(class, "The title of my window") 36 | { 37 | Ok(win) => win, 38 | Err(e) => { 39 | win::messagebox::message_box( 40 | "Error!", 41 | &format!("Window Creation Failed! {}", e), 42 | &[win::messagebox::Config::IconWarning], 43 | ) 44 | .unwrap(); 45 | exit(0); 46 | } 47 | }; 48 | 49 | window.show_default(); 50 | window.update().unwrap(); 51 | 52 | // Step 3: The Message Loop 53 | exit(win::message_loop()) 54 | } 55 | 56 | // Step 4: the Window Procedure 57 | fn main_window_callback( 58 | window: &win::window::Window, 59 | message: win::message::Message, 60 | ) -> Option { 61 | use win::message::Message; 62 | 63 | match message { 64 | Message::Close => { 65 | window.destroy().unwrap(); 66 | } 67 | Message::Destroy => { 68 | win::post_quit_message(0); 69 | } 70 | _ => return None, 71 | } 72 | 73 | Some(0) 74 | } 75 | -------------------------------------------------------------------------------- /examples/bmp_one/src/main.rs: -------------------------------------------------------------------------------- 1 | //! Bitmaps, Device Contexts and BitBlt. 2 | //! http://winprog.org/tutorial/bitmaps.html 3 | use std::cell::Cell; 4 | use std::process::exit; 5 | use minimal_windows_gui as win; 6 | 7 | const CLASS_NAME: &str = "myWindowClass"; 8 | 9 | const IDB_BALL: u16 = 101; 10 | 11 | thread_local! { 12 | static BALL: Cell> = Cell::new(None); 13 | } 14 | 15 | fn main() -> win::Result<()> { 16 | let class = &win::class::build() 17 | .load_icon(win::icon::Icon::Application)? 18 | .load_cursor(win::cursor::Cursor::Arrow)? 19 | .background(win::class::Background::Window) 20 | .load_small_icon(win::icon::Icon::Application)? 21 | .register(CLASS_NAME) 22 | .expect("window registration failed"); 23 | 24 | let window = win::window::build() 25 | .set_message_callback(main_window_callback) 26 | .add_extended_style(win::window::ExtendedStyle::ClientEdge) 27 | .add_style(win::window::Style::OverlappedWindow) 28 | .size(240, 120) 29 | .create(class, "A Bitmap Program") 30 | .expect("window creation failed"); 31 | 32 | window.show_default(); 33 | window.update().unwrap(); 34 | 35 | exit(win::message_loop()) 36 | } 37 | 38 | fn main_window_callback( 39 | window: &win::window::Window, 40 | message: win::message::Message, 41 | ) -> Option { 42 | use win::message::Message; 43 | 44 | match message { 45 | Message::Create => { 46 | let ball = win::bitmap::load(IDB_BALL).expect("could not load IDB_BALL"); 47 | BALL.with(|cell| cell.set(Some(ball))); 48 | } 49 | Message::Paint => { 50 | let paint = window.paint().unwrap(); 51 | 52 | BALL.with(|cell| { 53 | let ball = cell.take().unwrap(); 54 | let info = ball.info().unwrap(); 55 | paint 56 | .copy_bitmap_to_rect(0, 0, info.width(), info.height(), &ball, 0, 0) 57 | .unwrap(); 58 | 59 | cell.set(Some(ball)); 60 | }); 61 | } 62 | Message::Close => { 63 | window.destroy().unwrap(); 64 | } 65 | Message::Destroy => { 66 | BALL.with(|cell| drop(cell.take().take())); 67 | win::post_quit_message(0); 68 | } 69 | _ => return None, 70 | } 71 | 72 | Some(0) 73 | } 74 | -------------------------------------------------------------------------------- /src/rect.rs: -------------------------------------------------------------------------------- 1 | use winapi::shared::windef::RECT; 2 | 3 | #[derive(Clone)] 4 | pub struct Rect(pub(crate) RECT); 5 | 6 | impl Rect { 7 | /// Create a new rect with the given dimensions. 8 | pub fn new(width: i32, height: i32) -> Self { 9 | Self(RECT { 10 | left: 0, 11 | top: 0, 12 | right: width, 13 | bottom: height, 14 | }) 15 | } 16 | 17 | /// Construct a new rect with the same size but at the new location. 18 | pub fn at(&self, x: i32, y: i32) -> Self { 19 | Self(RECT { 20 | left: x, 21 | top: y, 22 | right: x + self.width(), 23 | bottom: y + self.height(), 24 | }) 25 | } 26 | 27 | /// Construct a new rect at the same position but with a new size. 28 | /// 29 | /// This will cause the sides to switch positions when negative values are used. 30 | pub fn sized(&self, width: i32, height: i32) -> Self { 31 | Self(RECT { 32 | left: self.0.left + width.min(0), 33 | top: self.0.top + height.min(0), 34 | right: self.0.left + width.max(0), 35 | bottom: self.0.top + height.max(0), 36 | }) 37 | } 38 | 39 | /// Construct a new rect at the same position but extending or stretching its size. 40 | /// 41 | /// This will cause the sides to switch positions when the deltas' absolute values are larger 42 | /// than the current dimension and their sign is negative. 43 | pub fn resized_by(&self, delta_width: i32, delta_height: i32) -> Self { 44 | self.sized(self.width() + delta_width, self.height() + delta_height) 45 | } 46 | 47 | pub fn left(&self) -> i32 { 48 | self.0.left 49 | } 50 | 51 | pub fn top(&self) -> i32 { 52 | self.0.top 53 | } 54 | 55 | pub fn right(&self) -> i32 { 56 | self.0.right 57 | } 58 | 59 | pub fn bottom(&self) -> i32 { 60 | self.0.bottom 61 | } 62 | 63 | /// Alias for [`Self::left`]. 64 | pub fn x(&self) -> i32 { 65 | self.0.left 66 | } 67 | 68 | /// Alias for [`Self::top`]. 69 | pub fn y(&self) -> i32 { 70 | self.0.top 71 | } 72 | 73 | /// Calculates the difference between the right and the left margins. 74 | pub fn width(&self) -> i32 { 75 | self.0.right - self.0.left 76 | } 77 | 78 | /// Calculates the difference between the bottom and the top margins. 79 | pub fn height(&self) -> i32 { 80 | self.0.bottom - self.0.top 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /src/cursor.rs: -------------------------------------------------------------------------------- 1 | use crate::{non_null_or_err, Result}; 2 | use std::ptr::{self, NonNull}; 3 | use winapi::shared::windef::HICON__; 4 | use winapi::um::winnt::LPCWSTR; 5 | use winapi::um::winuser::{ 6 | LoadCursorW, IDC_APPSTARTING, IDC_ARROW, IDC_CROSS, IDC_HAND, 7 | IDC_HELP, IDC_IBEAM, IDC_ICON, IDC_NO, IDC_SIZE, IDC_SIZEALL, IDC_SIZENESW, IDC_SIZENS, 8 | IDC_SIZENWSE, IDC_SIZEWE, IDC_UPARROW, IDC_WAIT, 9 | }; 10 | 11 | /// Built-in cursors as defined in https://docs.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-loadcursorw. 12 | pub enum Cursor { 13 | /// Standard arrow and small hourglass. 14 | AppStarting, 15 | /// Standard arrow. 16 | Arrow, 17 | /// Crosshair. 18 | Cross, 19 | /// Hand. 20 | Hand, 21 | /// Arrow and question mark. 22 | Help, 23 | /// I-beam. 24 | Ibeam, 25 | /// Obsolete for applications marked version 4.0 or later. 26 | Icon, 27 | /// Slashed circle. 28 | No, 29 | /// Obsolete for applications marked version 4.0 or later. Use `SizeAll`. 30 | Size, 31 | /// Four-pointed arrow pointing north, south, east, and west. 32 | SizeAll, 33 | /// Double-pointed arrow pointing northeast and southwest. 34 | SizeNesw, 35 | /// Double-pointed arrow pointing north and south. 36 | SizeNs, 37 | /// Double-pointed arrow pointing northwest and southeast. 38 | SizeNwse, 39 | /// Double-pointed arrow pointing west and east. 40 | SizeWe, 41 | /// Vertical arrow. 42 | UpArrow, 43 | /// Hourglass . 44 | Wait, 45 | } 46 | 47 | impl Cursor { 48 | // IDC* are defined as pointers which we can't use as values in the enum. 49 | fn value(&self) -> LPCWSTR { 50 | match self { 51 | Cursor::AppStarting => IDC_APPSTARTING, 52 | Cursor::Arrow => IDC_ARROW, 53 | Cursor::Cross => IDC_CROSS, 54 | Cursor::Hand => IDC_HAND, 55 | Cursor::Help => IDC_HELP, 56 | Cursor::Ibeam => IDC_IBEAM, 57 | Cursor::Icon => IDC_ICON, 58 | Cursor::No => IDC_NO, 59 | Cursor::Size => IDC_SIZE, 60 | Cursor::SizeAll => IDC_SIZEALL, 61 | Cursor::SizeNesw => IDC_SIZENESW, 62 | Cursor::SizeNs => IDC_SIZENS, 63 | Cursor::SizeNwse => IDC_SIZENWSE, 64 | Cursor::SizeWe => IDC_SIZEWE, 65 | Cursor::UpArrow => IDC_UPARROW, 66 | Cursor::Wait => IDC_WAIT, 67 | } 68 | } 69 | 70 | pub(crate) fn load(&self) -> Result> { 71 | let result = unsafe { 72 | // https://docs.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-loadcursorw 73 | LoadCursorW(ptr::null_mut(), self.value()) 74 | }; 75 | 76 | non_null_or_err(result) 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /src/menu.rs: -------------------------------------------------------------------------------- 1 | use crate::{non_null_or_err, ok_or_last_err, Result}; 2 | use std::ffi::CString; 3 | use std::ptr::{self, NonNull}; 4 | use winapi::shared::windef::{HMENU, HMENU__}; 5 | use winapi::um::winuser::{ 6 | AppendMenuA, CreateMenu, CreatePopupMenu, MF_POPUP, MF_SEPARATOR, MF_STRING, 7 | }; 8 | 9 | pub struct Menu { 10 | menu: NonNull, 11 | } 12 | 13 | impl Menu { 14 | pub(crate) fn as_ptr(&self) -> HMENU { 15 | self.menu.as_ptr() 16 | } 17 | 18 | /// Creates a menu. The menu is initially empty, but it can be filled with menu items by using 19 | /// the InsertMenuItem, AppendMenu, and InsertMenu functions. 20 | pub fn new() -> Result { 21 | let result = unsafe { CreateMenu() }; 22 | non_null_or_err(result).map(|menu| Menu { menu }) 23 | } 24 | 25 | /// Creates a drop-down menu, submenu, or shortcut menu. The menu is initially empty. You can 26 | /// insert or append menu items by using the InsertMenuItem function. You can also use the 27 | /// InsertMenu function to insert menu items and the AppendMenu function to append menu items. 28 | pub fn new_popup() -> Result { 29 | let result = unsafe { CreatePopupMenu() }; 30 | non_null_or_err(result).map(|menu| Menu { menu }) 31 | } 32 | 33 | /// Appends a new item to the end of the specified menu bar, drop-down menu, submenu, or 34 | /// shortcut menu. You can use this function to specify the content, appearance, and behavior 35 | /// of the menu item. 36 | pub fn append_item(&self, name: &str, value: u16) -> Result<()> { 37 | let name = CString::new(name)?; 38 | let result = 39 | unsafe { AppendMenuA(self.menu.as_ptr(), MF_STRING, value as usize, name.as_ptr()) }; 40 | 41 | ok_or_last_err(result) 42 | } 43 | 44 | /// Creates an inactive menu item that serves as a dividing bar between two active menu items 45 | /// on a menu. 46 | pub fn append_separator(&self) -> Result<()> { 47 | let result = unsafe { AppendMenuA(self.menu.as_ptr(), MF_SEPARATOR, 0, ptr::null_mut()) }; 48 | ok_or_last_err(result) 49 | } 50 | 51 | /// Appends a new menu to the end of the specified menu bar, drop-down menu, submenu, or 52 | /// shortcut menu. You can use this function to specify the content, appearance, and behavior 53 | /// of the menu item. 54 | pub fn append_menu(&self, name: &str, value: Menu) -> Result<()> { 55 | let name = CString::new(name)?; 56 | let result = unsafe { 57 | AppendMenuA( 58 | self.menu.as_ptr(), 59 | MF_STRING | MF_POPUP, 60 | value.as_ptr() as usize, 61 | name.as_ptr(), 62 | ) 63 | }; 64 | 65 | ok_or_last_err(result) 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /examples/app_one.rs: -------------------------------------------------------------------------------- 1 | //! Starting the workings of a text editor. 2 | //! http://winprog.org/tutorial/app_one.html 3 | use std::process::exit; 4 | use minimal_windows_gui as win; 5 | 6 | const CLASS_NAME: &str = "myWindowClass"; 7 | 8 | const IDC_MAIN_EDIT: u16 = 101; 9 | 10 | fn main() -> win::Result<()> { 11 | let class = &win::class::build() 12 | .load_icon(win::icon::Icon::Application)? 13 | .load_cursor(win::cursor::Cursor::Arrow)? 14 | .background(win::class::Background::Window) 15 | .load_small_icon(win::icon::Icon::Application)? 16 | .register(CLASS_NAME) 17 | .expect("window registration failed"); 18 | 19 | let window = win::window::build() 20 | .set_message_callback(main_window_callback) 21 | .add_extended_style(win::window::ExtendedStyle::ClientEdge) 22 | .add_style(win::window::Style::OverlappedWindow) 23 | .size(480, 320) 24 | .create(class, "Tutorial Application") 25 | .expect("window creation failed"); 26 | 27 | window.show_default(); 28 | window.update().unwrap(); 29 | 30 | exit(win::message_loop()) 31 | } 32 | 33 | fn main_window_callback( 34 | window: &win::window::Window, 35 | message: win::message::Message, 36 | ) -> Option { 37 | use win::message::Message; 38 | 39 | match message { 40 | Message::Create => { 41 | let edit_ctl = match win::window::build() 42 | .add_extended_style(win::window::ExtendedStyle::ClientEdge) 43 | .add_style(win::window::Style::Visible) 44 | .add_style(win::window::Style::VerticalScroll) 45 | .add_style(win::window::Style::HorizontalScroll) 46 | .add_style(win::window::Style::Multiline) 47 | .add_style(win::window::Style::AutoVerticalScroll) 48 | .add_style(win::window::Style::AutoHorizontalScroll) 49 | .pos(0, 0) 50 | .size(100, 100) 51 | .parent(window) 52 | .set_child_id(IDC_MAIN_EDIT) 53 | .create(win::class::edit_control(), "") 54 | { 55 | Ok(c) => c, 56 | Err(_) => { 57 | win::messagebox::message_box( 58 | "Error", 59 | "Could not create edit box.", 60 | &[win::messagebox::Config::IconError], 61 | ) 62 | .unwrap(); 63 | exit(1); 64 | } 65 | }; 66 | 67 | let font = win::font::get_default().unwrap(); 68 | edit_ctl.set_font(font); 69 | } 70 | Message::Size(info) => { 71 | let edit_ctl = window.get_dialog_item(IDC_MAIN_EDIT).unwrap(); 72 | edit_ctl 73 | .set_rect(win::rect::Rect::new( 74 | info.width() as i32, 75 | info.height() as i32, 76 | )) 77 | .unwrap(); 78 | } 79 | Message::Close => { 80 | window.destroy().unwrap(); 81 | } 82 | Message::Destroy => { 83 | win::post_quit_message(0); 84 | } 85 | _ => return None, 86 | } 87 | 88 | Some(0) 89 | } 90 | -------------------------------------------------------------------------------- /examples/dlg_one/src/main.rs: -------------------------------------------------------------------------------- 1 | //! Dialogs are just windows, which can be defined as dialog resources. 2 | //! http://winprog.org/tutorial/dialogs.html 3 | use std::process::exit; 4 | use minimal_windows_gui as win; 5 | 6 | const CLASS_NAME: &str = "myWindowClass"; 7 | 8 | const IDR_MYMENU: u16 = 101; 9 | const IDD_ABOUT: u16 = 102; 10 | 11 | const ID_FILE_EXIT: u16 = 40001; 12 | const ID_HELP_ABOUT: u16 = 40002; 13 | 14 | const DLG_OK: isize = 1; 15 | const DLG_CANCEL: isize = 2; 16 | 17 | fn main() -> win::Result<()> { 18 | let class = &win::class::build() 19 | .load_icon(win::icon::Icon::Application)? 20 | .load_cursor(win::cursor::Cursor::Arrow)? 21 | .background(win::class::Background::Window) 22 | .menu(IDR_MYMENU) 23 | .load_small_icon(win::icon::Icon::Application)? 24 | .register(CLASS_NAME) 25 | .expect("window registration failed"); 26 | 27 | let window = win::window::build() 28 | .set_message_callback(main_window_callback) 29 | .add_extended_style(win::window::ExtendedStyle::ClientEdge) 30 | .add_style(win::window::Style::OverlappedWindow) 31 | .size(240, 120) 32 | .create(class, "The title of my window") 33 | .expect("window creation failed"); 34 | 35 | window.show_default(); 36 | window.update().unwrap(); 37 | 38 | exit(win::message_loop()) 39 | } 40 | 41 | fn main_window_callback( 42 | window: &win::window::Window, 43 | message: win::message::Message, 44 | ) -> Option { 45 | use win::message::Message; 46 | 47 | match message { 48 | Message::Command(info) => { 49 | if let Some(menu_id) = info.menu_id() { 50 | match menu_id { 51 | ID_FILE_EXIT => { 52 | window.close().unwrap(); 53 | } 54 | ID_HELP_ABOUT => { 55 | let msg = match window.show_dialog(IDD_ABOUT, about_dialog_callback) { 56 | Ok(DLG_OK) => "Dialog exited with IDOK.", 57 | Ok(DLG_CANCEL) => "Dialog exited with IDCANCEL.", 58 | Ok(_) | Err(_) => "Dialog failed!", 59 | }; 60 | 61 | win::messagebox::message_box( 62 | "Notice", 63 | msg, 64 | &[win::messagebox::Config::IconInformation], 65 | ) 66 | .unwrap(); 67 | } 68 | _ => {} 69 | } 70 | } 71 | } 72 | Message::Close => { 73 | window.destroy().unwrap(); 74 | } 75 | Message::Destroy => { 76 | win::post_quit_message(0); 77 | } 78 | _ => return None, 79 | } 80 | 81 | Some(0) 82 | } 83 | 84 | fn about_dialog_callback(dialog: &win::window::Window, message: win::message::Message) -> isize { 85 | use win::message::Message; 86 | use win::messagebox::Button; 87 | 88 | match message { 89 | Message::InitDialog => {} 90 | Message::Command(info) => match info.control_data().map(|c| c.std_button()).flatten() { 91 | Some(Button::Ok) => dialog.end_dialog(DLG_OK).unwrap(), 92 | Some(Button::Cancel) => dialog.end_dialog(DLG_CANCEL).unwrap(), 93 | _ => {} 94 | }, 95 | _ => return 0, 96 | } 97 | 98 | 1 99 | } 100 | -------------------------------------------------------------------------------- /src/icon.rs: -------------------------------------------------------------------------------- 1 | use crate::{base_instance, non_null_or_err, Result}; 2 | use std::path::Path; 3 | use std::ptr::{self, NonNull}; 4 | use widestring::U16CString; 5 | use winapi::shared::windef::{HICON, HICON__}; 6 | use winapi::um::winnt::LPCWSTR; 7 | use winapi::um::winuser::{ 8 | LoadIconW, LoadImageW, IDI_APPLICATION, IDI_ERROR, IDI_INFORMATION, IDI_QUESTION, IDI_SHIELD, 9 | IDI_WARNING, IDI_WINLOGO, IMAGE_ICON, LR_LOADFROMFILE, MAKEINTRESOURCEW, 10 | }; 11 | 12 | pub struct FileData { 13 | path: U16CString, 14 | } 15 | 16 | /// Built-in icons as defined in https://docs.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-loadicona. 17 | pub enum Icon { 18 | /// Default application icon. 19 | Application, 20 | /// Hand-shaped icon. 21 | Error, 22 | /// Asterisk icon. 23 | Information, 24 | /// Question mark icon. 25 | Question, 26 | /// Security Shield icon. 27 | Shield, 28 | /// Exclamation point icon. 29 | Warning, 30 | /// Default application icon. 31 | WinLogo, 32 | /// Custom icons defined in the resource file `.rc`. 33 | FromResource(u16), 34 | /// Custom icons stored somewhere in the disk. 35 | FromFile(FileData), 36 | } 37 | 38 | impl Icon { 39 | /// Creates a new icon from in-disk file. 40 | pub fn from_file>(path: P) -> Self { 41 | Icon::FromFile(FileData { 42 | path: U16CString::from_os_str(path.as_ref().as_os_str()).unwrap(), 43 | }) 44 | } 45 | 46 | // IDI* are defined as pointers which we can't use as values in the enum. 47 | fn value(&self) -> LPCWSTR { 48 | match self { 49 | Icon::Application => IDI_APPLICATION, 50 | Icon::Error => IDI_ERROR, 51 | Icon::Information => IDI_INFORMATION, 52 | Icon::Question => IDI_QUESTION, 53 | Icon::Shield => IDI_SHIELD, 54 | Icon::Warning => IDI_WARNING, 55 | Icon::WinLogo => IDI_WINLOGO, 56 | Icon::FromResource(value) => MAKEINTRESOURCEW(*value), 57 | Icon::FromFile(data) => data.path.as_ptr(), 58 | } 59 | } 60 | 61 | pub(crate) fn load(&self) -> Result> { 62 | let handle = if matches!(self, Icon::FromResource(_)) { 63 | base_instance() 64 | } else { 65 | ptr::null_mut() 66 | }; 67 | 68 | let result = unsafe { LoadIconW(handle, self.value()) }; 69 | non_null_or_err(result) 70 | } 71 | 72 | pub(crate) fn load_small(&self) -> Result> { 73 | self.load_size(16) 74 | } 75 | 76 | pub(crate) fn load_large(&self) -> Result> { 77 | self.load_size(32) 78 | } 79 | 80 | fn load_size(&self, size: i32) -> Result> { 81 | match self { 82 | Icon::FromResource(_) => { 83 | let result = 84 | unsafe { LoadImageW(base_instance(), self.value(), IMAGE_ICON, size, size, 0) }; 85 | 86 | let result = result as HICON; 87 | non_null_or_err(result) 88 | } 89 | Icon::FromFile(_) => { 90 | let result = unsafe { 91 | LoadImageW( 92 | ptr::null_mut(), 93 | self.value(), 94 | IMAGE_ICON, 95 | size, 96 | size, 97 | LR_LOADFROMFILE, 98 | ) 99 | }; 100 | 101 | let result = result as HICON; 102 | non_null_or_err(result) 103 | } 104 | _ => self.load(), 105 | } 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /examples/menu_two.rs: -------------------------------------------------------------------------------- 1 | //! Shows how to add basic menus and icons to your window during runtime. 2 | //! http://winprog.org/tutorial/menus.html 3 | use std::process::exit; 4 | use minimal_windows_gui as win; 5 | 6 | const CLASS_NAME: &str = "myWindowClass"; 7 | 8 | const ID_FILE_EXIT: u16 = 9001; 9 | const ID_STUFF_GO: u16 = 9002; 10 | 11 | fn main() -> win::Result<()> { 12 | let class = &win::class::build() 13 | .load_cursor(win::cursor::Cursor::Arrow)? 14 | .background(win::class::Background::Window) 15 | .register(CLASS_NAME) 16 | .expect("window registration failed"); 17 | 18 | let window = win::window::build() 19 | .set_message_callback(main_window_callback) 20 | .add_extended_style(win::window::ExtendedStyle::ClientEdge) 21 | .add_style(win::window::Style::OverlappedWindow) 22 | .size(240, 120) 23 | .create(class, "A Menu #2") 24 | .expect("window creation failed"); 25 | 26 | window.show_default(); 27 | window.update().unwrap(); 28 | 29 | exit(win::message_loop()) 30 | } 31 | 32 | fn main_window_callback( 33 | window: &win::window::Window, 34 | message: win::message::Message, 35 | ) -> Option { 36 | use win::message::Message; 37 | 38 | match message { 39 | Message::Create => { 40 | let menu = win::menu::Menu::new().unwrap(); 41 | 42 | let submenu = win::menu::Menu::new_popup().unwrap(); 43 | submenu.append_item("E&xit", ID_FILE_EXIT).unwrap(); 44 | menu.append_menu("&File", submenu).unwrap(); 45 | 46 | let submenu = win::menu::Menu::new_popup().unwrap(); 47 | submenu.append_item("&Go", ID_STUFF_GO).unwrap(); 48 | menu.append_menu("&Stuff", submenu).unwrap(); 49 | 50 | window.set_menu(menu).unwrap(); 51 | 52 | let icon = win::icon::Icon::from_file(r"examples\menu_one\menu_one.ico"); 53 | match window.set_icon(icon) { 54 | Ok(_) => {} 55 | Err(_) => { 56 | win::messagebox::message_box( 57 | "Error", 58 | "Could not load large icon! Is the program running from the project root?", 59 | &[win::messagebox::Config::IconError], 60 | ) 61 | .unwrap(); 62 | } 63 | } 64 | 65 | let icon = win::icon::Icon::from_file(r"examples\menu_one\menu_one.ico"); 66 | match window.set_small_icon(icon) { 67 | Ok(_) => {} 68 | Err(_) => { 69 | win::messagebox::message_box( 70 | "Error", 71 | "Could not load small icon! Is the program running from the project root?", 72 | &[win::messagebox::Config::IconError], 73 | ) 74 | .unwrap(); 75 | } 76 | } 77 | } 78 | Message::Command(info) => { 79 | if let Some(menu_id) = info.menu_id() { 80 | match menu_id { 81 | ID_FILE_EXIT => { 82 | window.close().unwrap(); 83 | } 84 | ID_STUFF_GO => { 85 | win::messagebox::message_box("Woo!", "You clicked Go!", &[]).unwrap(); 86 | } 87 | _ => {} 88 | } 89 | } 90 | } 91 | Message::Close => { 92 | window.destroy().unwrap(); 93 | } 94 | Message::Destroy => { 95 | win::post_quit_message(0); 96 | } 97 | _ => return None, 98 | } 99 | 100 | Some(0) 101 | } 102 | -------------------------------------------------------------------------------- /examples/bmp_two.rs: -------------------------------------------------------------------------------- 1 | //! Transparent Bitmaps. 2 | //! http://winprog.org/tutorial/transparency.html 3 | use std::cell::Cell; 4 | use std::process::exit; 5 | use minimal_windows_gui as win; 6 | 7 | const CLASS_NAME: &str = "myWindowClass"; 8 | 9 | thread_local! { 10 | static BALL_MASK: Cell> = Cell::new(None); 11 | } 12 | 13 | fn main() -> win::Result<()> { 14 | let class = &win::class::build() 15 | .load_icon(win::icon::Icon::Application)? 16 | .load_cursor(win::cursor::Cursor::Arrow)? 17 | .background(win::class::Background::Window) 18 | .load_small_icon(win::icon::Icon::Application)? 19 | .register(CLASS_NAME) 20 | .expect("window registration failed"); 21 | 22 | let window = win::window::build() 23 | .set_message_callback(main_window_callback) 24 | .add_extended_style(win::window::ExtendedStyle::ClientEdge) 25 | .add_style(win::window::Style::OverlappedWindow) 26 | .size(240, 160) 27 | .create(class, "Another Bitmap Program") 28 | .expect("window creation failed"); 29 | 30 | window.show_default(); 31 | window.update().unwrap(); 32 | 33 | exit(win::message_loop()) 34 | } 35 | 36 | fn main_window_callback( 37 | window: &win::window::Window, 38 | message: win::message::Message, 39 | ) -> Option { 40 | use win::message::Message; 41 | 42 | match message { 43 | Message::Create => { 44 | // In contrast with the original tutorial, load from file to avoid needing `.rc` files. 45 | let ball_bmp = win::gdi::bitmap::from_file(r"examples\bmp_one\ball.bmp") 46 | .expect("could not load ball"); 47 | let mask_bmp = ball_bmp.set_color_transparent((0, 0, 0)).unwrap(); 48 | BALL_MASK.with(|cell| cell.set(Some((ball_bmp, mask_bmp)))); 49 | } 50 | Message::Paint => { 51 | let paint = window.paint().unwrap(); 52 | 53 | BALL_MASK.with(|cell| { 54 | let (ball, mask) = cell.take().unwrap(); 55 | 56 | let rect = window.get_rect().unwrap(); 57 | paint 58 | .fill_rect(rect, win::gdi::brush::light_gray().unwrap()) 59 | .unwrap(); 60 | 61 | let info = ball.info().unwrap(); 62 | let (w, h) = (info.width(), info.height()); 63 | let rect = win::rect::Rect::new(w, h); 64 | 65 | { 66 | let canvas = paint.try_clone().unwrap().bind(&mask).unwrap(); 67 | paint.bitwise().region(rect.at(0, 0)).set(&canvas).unwrap(); 68 | paint.bitwise().region(rect.at(w, 0)).and(&canvas).unwrap(); 69 | paint 70 | .bitwise() 71 | .region(rect.at(w * 2, h * 2)) 72 | .and(&canvas) 73 | .unwrap(); 74 | 75 | let canvas = canvas.bind(&ball).unwrap(); 76 | paint.bitwise().region(rect.at(0, h)).set(&canvas).unwrap(); 77 | paint.bitwise().region(rect.at(w, h)).or(&canvas).unwrap(); 78 | paint 79 | .bitwise() 80 | .region(rect.at(w * 2, h * 2)) 81 | .or(&canvas) 82 | .unwrap(); 83 | } 84 | 85 | cell.set(Some((ball, mask))); 86 | }); 87 | } 88 | Message::Close => { 89 | window.destroy().unwrap(); 90 | } 91 | Message::Destroy => { 92 | BALL_MASK.with(|cell| drop(cell.take().take())); 93 | win::post_quit_message(0); 94 | } 95 | _ => return None, 96 | } 97 | 98 | Some(0) 99 | } 100 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | #![cfg(windows)] 2 | pub mod class; 3 | pub mod cursor; 4 | pub mod dialog; 5 | pub mod font; 6 | pub mod gdi; 7 | pub mod icon; 8 | pub mod menu; 9 | pub mod message; 10 | pub mod messagebox; 11 | pub mod rect; 12 | pub mod toolbar; 13 | pub mod window; 14 | 15 | use once_cell::sync::Lazy; 16 | use std::ffi::CString; 17 | use std::ptr::{self, NonNull}; 18 | use std::{collections::HashMap, sync::Mutex}; 19 | use winapi::shared::minwindef::{BOOL, HINSTANCE, MAX_PATH}; 20 | use winapi::shared::ntdef::LPSTR; 21 | use winapi::um::commctrl::InitCommonControls; 22 | use winapi::um::libloaderapi::{GetModuleFileNameA, GetModuleHandleA}; 23 | use winapi::um::winuser::{ 24 | DispatchMessageA, GetMessageA, PostQuitMessage, TranslateMessage, LPMSG, MSG, 25 | }; 26 | 27 | pub use std::io::{Error, Result}; 28 | pub type MessageCallback = fn(&window::Window, message::Message) -> Option; 29 | pub type DialogCallback = fn(&window::Window, message::Message) -> isize; 30 | 31 | // We want to wrap user functions to provide them with a safer interface. 32 | // 33 | // The wrappers can't be lambdas because they need to be "extern system" and kept alive for as 34 | // long as the class lives. 35 | // 36 | // We don't know how many functions they will need ahead of time, so we can't define that many 37 | // static functions either. 38 | // 39 | // The only solution is to have a single static wrapper function that queries a global map (this 40 | // map) to determine what to call based on the window. 41 | // 42 | // Because messages may be emitted before the pointer is obtained, a special value of 0 is used 43 | // to indicate "newly created", and is used as a fallback. 44 | static HWND_TO_CALLBACK: Lazy>> = 45 | Lazy::new(|| Mutex::new(HashMap::new())); 46 | 47 | static HWND_TO_DLG_CALLBACK: Lazy>> = 48 | Lazy::new(|| Mutex::new(HashMap::new())); 49 | 50 | /// Obtains the `hInstance` parameter from `WinMain`. 51 | pub(crate) fn base_instance() -> HINSTANCE { 52 | unsafe { GetModuleHandleA(std::ptr::null()) } 53 | } 54 | 55 | /// Registers and initializes certain common control window classes. 56 | /// This method must be called early in the program if common controls are used. 57 | pub fn init_common_controls() { 58 | unsafe { 59 | InitCommonControls(); 60 | } 61 | } 62 | 63 | /// Retrieves the fully qualified path for the file that contains the specified module. 64 | /// The module must have been loaded by the current process. 65 | pub fn module_file_name() -> Result { 66 | let module = base_instance(); 67 | let mut buffer = vec![0u8; MAX_PATH]; 68 | 69 | let result = 70 | unsafe { GetModuleFileNameA(module, buffer.as_mut_ptr() as LPSTR, buffer.len() as u32) }; 71 | 72 | if result == 0 { 73 | Err(Error::last_os_error()) 74 | } else { 75 | buffer.truncate(result as usize); 76 | Ok(CString::new(buffer)?) 77 | } 78 | } 79 | 80 | /// Indicates to the system that a thread has made a request to terminate (quit). 81 | /// It is typically used in response to a `Destroy` message. 82 | /// 83 | /// The application exit code is used as the wParam parameter of the `Quit` message. 84 | pub fn post_quit_message(exit_code: i32) { 85 | unsafe { PostQuitMessage(exit_code) } 86 | } 87 | 88 | pub fn message_loop() -> i32 { 89 | unsafe { 90 | let mut msg: MSG = std::mem::zeroed(); 91 | while GetMessageA(&mut msg as LPMSG, ptr::null_mut(), 0, 0) > 0 { 92 | TranslateMessage(&mut msg as LPMSG); 93 | // This effectively looks up the window corresponding to the message's window handle 94 | // and calls its window procedure. Alternatively `GetWindowLong` can be used to do 95 | // the same, but manually (http://winprog.org/tutorial/message_loop.html). 96 | DispatchMessageA(&mut msg as LPMSG); 97 | } 98 | msg.wParam as i32 99 | } 100 | } 101 | 102 | /// Checks the resulting return value of a function. If it's `true`, `Ok` is returned. Otherwise, 103 | /// the last OS error is returned in the `Err` variant. 104 | pub(crate) fn ok_or_last_err(result: BOOL) -> Result<()> { 105 | if result != 0 { 106 | Ok(()) 107 | } else { 108 | Err(Error::last_os_error()) 109 | } 110 | } 111 | 112 | pub(crate) fn non_null_or_err(value: *mut T) -> Result> { 113 | NonNull::new(value).ok_or_else(|| Error::last_os_error()) 114 | } 115 | -------------------------------------------------------------------------------- /examples/ctl_one/src/main.rs: -------------------------------------------------------------------------------- 1 | //! Dialogs are just windows, which can be defined as dialog resources. 2 | //! http://winprog.org/tutorial/dialogs.html 3 | use std::process::exit; 4 | use minimal_windows_gui as win; 5 | 6 | const IDD_MAIN: u16 = 101; 7 | 8 | const IDC_TEXT: u16 = 1000; 9 | const IDC_NUMBER: u16 = 1001; 10 | const IDC_LIST: u16 = 1002; 11 | const IDC_ADD: u16 = 1003; 12 | const IDC_CLEAR: u16 = 1004; 13 | const IDC_REMOVE: u16 = 1005; 14 | const IDC_SHOWCOUNT: u16 = 1006; 15 | 16 | fn main() -> win::Result<()> { 17 | exit(win::dialog::show(IDD_MAIN, dialog_callback).unwrap() as i32) 18 | } 19 | 20 | fn dialog_callback(dialog: &win::window::Window, message: win::message::Message) -> isize { 21 | use win::message::Message; 22 | 23 | match message { 24 | Message::InitDialog => { 25 | let text_ctl = dialog.get_dialog_item(IDC_TEXT).unwrap(); 26 | let number_ctl = dialog.get_dialog_item(IDC_NUMBER).unwrap(); 27 | 28 | text_ctl.set_text("This is a string"); 29 | number_ctl.set_text("5"); 30 | } 31 | Message::Command(info) => { 32 | if let Some(control) = info.control_data() { 33 | handle_command(dialog, control).unwrap(); 34 | } 35 | } 36 | Message::Close => { 37 | dialog.end_dialog(0).unwrap(); 38 | } 39 | _ => return 0, 40 | } 41 | 42 | 1 43 | } 44 | 45 | // Separate function to avoid a bit of rightwards drift. 46 | fn handle_command( 47 | dialog: &win::window::Window, 48 | control: win::message::ControlData, 49 | ) -> win::Result<()> { 50 | let number_ctl = dialog.get_dialog_item(IDC_NUMBER)?; 51 | let text_ctl = dialog.get_dialog_item(IDC_TEXT)?; 52 | let list_ctl = dialog.get_dialog_item(IDC_LIST)?; 53 | let show_count_ctl = dialog.get_dialog_item(IDC_SHOWCOUNT)?; 54 | 55 | match control.id { 56 | IDC_ADD => match number_ctl.get_text().parse::() { 57 | Ok(n_times) => { 58 | let len = text_ctl.get_text_len(); 59 | if len > 0 { 60 | let text = text_ctl.get_text(); 61 | for _ in 0..n_times { 62 | let index = list_ctl.add_string_item(&text).unwrap(); 63 | list_ctl.set_item_data(index, n_times as isize).unwrap(); 64 | } 65 | } else { 66 | win::messagebox::message_box("Warning", "You didn't enter anything!", &[])?; 67 | } 68 | } 69 | Err(_) => { 70 | win::messagebox::message_box("Warning", "Couldn't translate that number :(", &[])?; 71 | } 72 | }, 73 | IDC_REMOVE => match list_ctl.get_selection_count() { 74 | Ok(0) => { 75 | win::messagebox::message_box("Warning", "No items selected.", &[])?; 76 | } 77 | Ok(_) => { 78 | for index in list_ctl.get_selected_items().unwrap().into_iter().rev() { 79 | list_ctl.delete_string_item(index).unwrap(); 80 | } 81 | } 82 | Err(_) => { 83 | win::messagebox::message_box("Warning", "Error counting items :(", &[])?; 84 | } 85 | }, 86 | IDC_LIST 87 | if matches!( 88 | control.list_box_code(), 89 | win::message::ListBoxMessage::SelectionChange 90 | ) => 91 | { 92 | match list_ctl.get_selection_count() { 93 | Ok(1) => match list_ctl.get_selected_items() { 94 | Ok(items) => { 95 | let data = list_ctl.get_item_data(items[0] as usize).unwrap(); 96 | show_count_ctl.set_text(&data.to_string()); 97 | } 98 | Err(_) => { 99 | win::messagebox::message_box( 100 | "Warning", 101 | "Error getting selected item :(", 102 | &[], 103 | )?; 104 | } 105 | }, 106 | Ok(_) => { 107 | show_count_ctl.set_text("-"); 108 | } 109 | Err(_) => { 110 | win::messagebox::message_box("Warning", "Error counting items :(", &[])?; 111 | } 112 | } 113 | } 114 | IDC_CLEAR => { 115 | list_ctl.clear_content(); 116 | } 117 | _ => {} 118 | } 119 | Ok(()) 120 | } 121 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # minimal-windows-gui 2 | 3 | *miwig: MInimal, WIndows-focused Graphical user-interface toolkit.* 4 | 5 | A crate to build Graphical User Interfaces for Windows applications, using just WinAPI. 6 | 7 | The primary goal is to build a better abstraction for Rust to deal with the [Windows API][winapi], 8 | and the development is tailored by [theForger's Win32 API Programming Tutorial][winapi-tut] which 9 | is a great source to get into the API. The examples from that resource are ported to Rust using 10 | this crate under the `examples/` folder, which can be ran with `cargo`: 11 | 12 | ```sh 13 | # for small single-file examples 14 | cargo run --example test 15 | 16 | # for larger examples using resources 17 | cargo run --package menu_one 18 | ``` 19 | 20 | For [Using Resources][using-res], a separate crate such as [embed-resource] or [winres] may be 21 | used for commodity (essentially emitting the correct `cargo:rustc-link-lib` value in `build.rs`). 22 | You may also [read about `.rc` files][about-rc] to understand what they are and how they work. 23 | 24 | ## Why another toolkit? 25 | 26 | I like to avoid heavy dependencies on my projects when possible, and if those dependencies are 27 | my own, it's even more justifiable to use them! 28 | 29 | I know there are other good options out there ([A 2021 Survey of Rust GUI Libraries][other-ui] 30 | is a great summary), even Windows-focused alternatives such as [Native Windows GUI][nwg], but 31 | that's like, not my own! And I can't prevent NWG from growing more than I'd want my own to. 32 | 33 | So, this is my own personal take on this thing. It's very minimal. You can expect a certain 34 | level of abstraction, but don't expect a full-blown DSL or fancy derive macros. If you're doing 35 | anything half-serious, you may want to consider a different toolkit. 36 | 37 | ## Ported theForger's examples 38 | 39 | To run a `file.rs`-based example, use: 40 | 41 | ```sh 42 | cargo run --example file 43 | ``` 44 | 45 | To run a `folder/`-based example, use: 46 | 47 | ```sh 48 | cargo run --manifest-path examples/folder/Cargo.toml 49 | ``` 50 | 51 | To run all of them: 52 | 53 | ```sh 54 | for f in examples/*; do test -f "$f" && { cargo run --example $(basename "$f" .rs); } || { cargo run --manifest-path $f/Cargo.toml; }; done 55 | ``` 56 | 57 | * Basics 58 | * Getting Started: `intro.rs`. Displays a message box and exits. 59 | * A Simple Window: `simple_window.rs`. A blank window with default behaviour. 60 | * Handling Messages: `window_click.rs`. A window that reacts to click events. 61 | * The Message Loop: No full program examples in this section. 62 | * Using Resources: No full program examples in this section. 63 | * Menus and Icons: `menu_one/`, `menu_two.rs`. A window with toolbar actions, using resources and created programatically, respectively. 64 | * Dialog Boxes: `dlg_one/`. A window with a toolbar action to open a custom About dialog. 65 | * Modeless Dialogs: `dlg_two/`. A window with a dialog pre-opened that cannot be closed. 66 | * Standard Controls: `ctl_one/`. A complex window layout with text inputs, scrollable lists, and buttons. 67 | * Dialog FAQ: `dlg_three/`. A window with a custom background color. 68 | * Creating a simple application 69 | * Creating controls at runtime. `app_one.rs`. A window with a text area with scrollbars in both directions and a resize grip. 70 | * Files and the common dialogs. `app_two.rs`. Builds on top of `app_one` and adds a window with a toolbar to open the standard *Open File* and *Save As* dialogs. 71 | * Tool and Status bars. `app_three.rs`. Builds on top of `app_two` and adds a status bar and icon bar as an alternative to the toolbar. 72 | * Multiple Document Interface. Not ported yet. Builds on top of `app_three` and adds nested windows that are contained within the main window. 73 | * Graphics Device Interface 74 | * Bitmaps and Device Contexts. `bmp_one/`. A window that loads and displays a bitmap. 75 | * Transparency. `bmp_two.rs`. A window that loads and displays a bitmap with different masks applied. 76 | * Timers and Animation. `anim_one.rs`. A window that loads a bitmap and uses a timer to animate it. 77 | * Text, Fonts and Colours. Not ported yet. A window that uses a custom font in its text area. 78 | 79 | [winapi]: https://docs.microsoft.com/en-us/windows/win32/apiindex/windows-api-list 80 | [winapi-tut]: http://winprog.org/tutorial/ 81 | [using-res]: http://winprog.org/tutorial/resources.html 82 | [embed-resource]: https://crates.io/crates/embed_resource 83 | [winres]: https://crates.io/crates/winres 84 | [about-rc]: https://docs.microsoft.com/en-us/windows/win32/menurc/resource-compiler 85 | [other-ui]: https://www.boringcactus.com/2021/10/24/2021-survey-of-rust-gui-libraries.html#native-windows-gui 86 | [nwg]: https://gabdube.github.io/native-windows-gui/ 87 | -------------------------------------------------------------------------------- /examples/dlg_two/src/main.rs: -------------------------------------------------------------------------------- 1 | //! Modeless dialogs depend on the main message loop to pump the messages as it does for the 2 | //! main window. 3 | //! http://winprog.org/tutorial/modeless_dialogs.html 4 | use std::cell::Cell; 5 | use std::process::exit; 6 | use minimal_windows_gui as win; 7 | 8 | const CLASS_NAME: &str = "myWindowClass"; 9 | 10 | const IDR_MYMENU: u16 = 101; 11 | const IDD_TOOLBAR: u16 = 101; 12 | 13 | const IDC_PRESS: u16 = 1000; 14 | const IDC_OTHER: u16 = 1001; 15 | 16 | const ID_FILE_EXIT: u16 = 40001; 17 | const ID_DIALOG_SHOW: u16 = 40002; 18 | const ID_DIALOG_HIDE: u16 = 40003; 19 | 20 | thread_local! { 21 | static TOOLBAR_HANDLE: Cell>> = Cell::new(None); 22 | } 23 | 24 | fn main() -> win::Result<()> { 25 | let class = &win::class::build() 26 | .load_icon(win::icon::Icon::Application)? 27 | .load_cursor(win::cursor::Cursor::Arrow)? 28 | .background(win::class::Background::Window) 29 | .menu(IDR_MYMENU) 30 | .load_small_icon(win::icon::Icon::Application)? 31 | .register(CLASS_NAME) 32 | .expect("window registration failed"); 33 | 34 | let window = win::window::build() 35 | .set_message_callback(main_window_callback) 36 | .add_extended_style(win::window::ExtendedStyle::ClientEdge) 37 | .add_style(win::window::Style::OverlappedWindow) 38 | .size(240, 120) 39 | .create(class, "The title of my window") 40 | .expect("window creation failed"); 41 | 42 | window.show_default(); 43 | window.update().unwrap(); 44 | 45 | exit(win::message_loop()) 46 | } 47 | 48 | fn main_window_callback( 49 | window: &win::window::Window, 50 | message: win::message::Message, 51 | ) -> Option { 52 | use win::message::Message; 53 | 54 | match message { 55 | Message::Create => { 56 | match window.create_dialog(IDD_TOOLBAR, tool_dialog_callback) { 57 | Ok(dialog) => { 58 | dialog.show(); 59 | TOOLBAR_HANDLE.with(|cell| cell.set(Some(dialog))); 60 | } 61 | Err(_) => { 62 | win::messagebox::message_box( 63 | "Warning!", 64 | "CreateDialog returned NULL", 65 | &[win::messagebox::Config::IconInformation], 66 | ) 67 | .unwrap(); 68 | } 69 | }; 70 | } 71 | Message::Command(info) => { 72 | if let Some(menu_id) = info.menu_id() { 73 | match menu_id { 74 | ID_FILE_EXIT => { 75 | window.close().unwrap(); 76 | } 77 | ID_DIALOG_SHOW => { 78 | TOOLBAR_HANDLE.with(|cell| { 79 | let handle = cell.take(); 80 | handle.as_ref().unwrap().show(); 81 | cell.set(handle); 82 | }); 83 | } 84 | ID_DIALOG_HIDE => { 85 | TOOLBAR_HANDLE.with(|cell| { 86 | let handle = cell.take(); 87 | handle.as_ref().unwrap().hide(); 88 | cell.set(handle); 89 | }); 90 | } 91 | _ => {} 92 | } 93 | } 94 | } 95 | Message::Close => { 96 | TOOLBAR_HANDLE.with(|cell| { 97 | let handle = cell.take(); 98 | handle.unwrap().destroy().unwrap(); 99 | }); 100 | window.destroy().unwrap(); 101 | } 102 | Message::Destroy => { 103 | win::post_quit_message(0); 104 | } 105 | _ => return None, 106 | } 107 | 108 | Some(0) 109 | } 110 | 111 | fn tool_dialog_callback(_dialog: &win::window::Window, message: win::message::Message) -> isize { 112 | use win::message::Message; 113 | 114 | match message { 115 | Message::InitDialog => {} 116 | Message::Command(info) => match info.control_data().map(|c| c.id) { 117 | Some(IDC_PRESS) => { 118 | win::messagebox::message_box( 119 | "Hi!", 120 | "This is a message", 121 | &[win::messagebox::Config::IconWarning], 122 | ) 123 | .unwrap(); 124 | } 125 | Some(IDC_OTHER) => { 126 | win::messagebox::message_box( 127 | "Bye!", 128 | "This is also a message", 129 | &[win::messagebox::Config::IconWarning], 130 | ) 131 | .unwrap(); 132 | } 133 | _ => {} 134 | }, 135 | _ => return 0, 136 | } 137 | 138 | 1 139 | } 140 | -------------------------------------------------------------------------------- /examples/anim_one.rs: -------------------------------------------------------------------------------- 1 | //! Timers and Animation. 2 | //! http://winprog.org/tutorial/animation.html 3 | use std::cell::Cell; 4 | use std::num::NonZeroUsize; 5 | use std::process::exit; 6 | use std::time::Duration; 7 | use minimal_windows_gui as win; 8 | 9 | const CLASS_NAME: &str = "myWindowClass"; 10 | const ID_TIMER: usize = 1; 11 | 12 | const BALL_MOVE_DELTA: i32 = 2; 13 | 14 | struct Global { 15 | width: i32, 16 | height: i32, 17 | x: i32, 18 | y: i32, 19 | 20 | dx: i32, 21 | dy: i32, 22 | 23 | ball: win::gdi::Bitmap, 24 | mask: win::gdi::Bitmap, 25 | } 26 | 27 | thread_local! { 28 | static GLOBAL: Cell> = Cell::new(None); 29 | } 30 | 31 | fn main() -> win::Result<()> { 32 | let class = &win::class::build() 33 | .load_icon(win::icon::Icon::Application)? 34 | .load_cursor(win::cursor::Cursor::Arrow)? 35 | .background(win::class::Background::Window) 36 | .load_small_icon(win::icon::Icon::Application)? 37 | .register(CLASS_NAME) 38 | .expect("window registration failed"); 39 | 40 | let window = win::window::build() 41 | .set_message_callback(main_window_callback) 42 | .add_extended_style(win::window::ExtendedStyle::ClientEdge) 43 | .add_style(win::window::Style::OverlappedWindow) 44 | .size(320, 240) 45 | .create(class, "An Animation Program") 46 | .expect("window creation failed"); 47 | 48 | window.show_default(); 49 | window.update().unwrap(); 50 | 51 | exit(win::message_loop()) 52 | } 53 | 54 | fn main_window_callback( 55 | window: &win::window::Window, 56 | message: win::message::Message, 57 | ) -> Option { 58 | use win::message::Message; 59 | 60 | match message { 61 | Message::Create => { 62 | let ball = win::gdi::bitmap::from_file(r"examples\bmp_one\ball.bmp") 63 | .expect("could not load ball"); 64 | let info = ball.info().unwrap(); 65 | let mask = ball.set_color_transparent((0, 0, 0)).unwrap(); 66 | 67 | window 68 | .set_timer( 69 | NonZeroUsize::new(ID_TIMER).unwrap(), 70 | Duration::from_millis(50), 71 | ) 72 | .unwrap(); 73 | 74 | GLOBAL.with(|cell| { 75 | cell.set(Some(Global { 76 | ball, 77 | width: info.width(), 78 | height: info.height(), 79 | x: 0, 80 | y: 0, 81 | dx: BALL_MOVE_DELTA, 82 | dy: BALL_MOVE_DELTA, 83 | mask, 84 | })) 85 | }); 86 | } 87 | Message::Paint => { 88 | let paint = window.paint().unwrap(); 89 | 90 | GLOBAL.with(|cell| { 91 | let mut global = cell.take().unwrap(); 92 | let rect = window.get_rect().unwrap(); 93 | draw_ball(&mut global, &paint, rect); 94 | cell.set(Some(global)); 95 | }); 96 | } 97 | Message::Timer(_timer) => { 98 | let paint = window.repaint().unwrap(); 99 | 100 | GLOBAL.with(|cell| { 101 | let mut global = cell.take().unwrap(); 102 | let rect = window.get_rect().unwrap(); 103 | update_ball(&mut global, rect.width(), rect.height()); 104 | draw_ball(&mut global, &paint, rect); 105 | cell.set(Some(global)); 106 | }); 107 | } 108 | Message::Close => { 109 | window.destroy().unwrap(); 110 | } 111 | Message::Destroy => { 112 | window 113 | .kill_timer(NonZeroUsize::new(ID_TIMER).unwrap()) 114 | .unwrap(); 115 | GLOBAL.with(|cell| drop(cell.take().take())); 116 | win::post_quit_message(0); 117 | } 118 | _ => return None, 119 | } 120 | 121 | Some(0) 122 | } 123 | 124 | fn draw_ball(global: &mut Global, paint: &win::gdi::Canvas, rect: win::rect::Rect) { 125 | paint 126 | .fill_rect(rect.clone(), win::gdi::brush::white().unwrap()) 127 | .unwrap(); 128 | 129 | let ball_rect = win::rect::Rect::new(global.width, global.height).at(global.x, global.y); 130 | let canvas = paint.try_clone().unwrap(); 131 | 132 | let canvas = canvas.bind(&global.mask).unwrap(); 133 | paint 134 | .bitwise() 135 | .region(ball_rect.clone()) 136 | .and(&canvas) 137 | .unwrap(); 138 | let canvas = canvas.bind(&global.ball).unwrap(); 139 | paint 140 | .bitwise() 141 | .region(ball_rect.clone()) 142 | .or(&canvas) 143 | .unwrap(); 144 | } 145 | 146 | fn update_ball(global: &mut Global, width: i32, height: i32) { 147 | global.x += global.dx; 148 | global.y += global.dy; 149 | 150 | if global.x < 0 { 151 | global.x = 0; 152 | global.dx = BALL_MOVE_DELTA; 153 | } else if global.x + global.width > width { 154 | global.x = width - global.width; 155 | global.dx = -BALL_MOVE_DELTA; 156 | } 157 | 158 | if global.y < 0 { 159 | global.y = 0; 160 | global.dy = BALL_MOVE_DELTA; 161 | } else if global.y + global.height > height { 162 | global.y = height - global.height; 163 | global.dy = -BALL_MOVE_DELTA; 164 | } 165 | } 166 | -------------------------------------------------------------------------------- /src/gdi/bitmap.rs: -------------------------------------------------------------------------------- 1 | use super::{Canvas, Paint}; 2 | use crate::{base_instance, non_null_or_err}; 3 | 4 | use std::ffi::CString; 5 | use std::mem; 6 | use std::ptr::{self, NonNull}; 7 | use winapi::shared::minwindef::LPVOID; 8 | use winapi::shared::windef::{HBITMAP, HBITMAP__, HGDIOBJ}; 9 | use winapi::um::wingdi::{CreateBitmap, DeleteObject, GetObjectA, BITMAP}; 10 | use winapi::um::winnt::{HANDLE, LPCSTR}; 11 | use winapi::um::winuser::{ 12 | LoadBitmapA, LoadImageA, IMAGE_BITMAP, LR_LOADFROMFILE, MAKEINTRESOURCEA, 13 | }; 14 | 15 | #[derive(Debug)] 16 | pub struct Bitmap { 17 | pub(crate) bitmap: NonNull, 18 | } 19 | 20 | pub struct Info { 21 | info: BITMAP, 22 | _size: usize, 23 | } 24 | 25 | pub fn new(width: i32, height: i32, planes: u32, bit_count: u32) -> Result { 26 | // Documentation claims: 27 | // > This function can return the following value: 28 | // > ERROR_INVALID_BITMAP | The calculated size of the bitmap is less than zero. 29 | // 30 | // However the code seems nowhere to be found. Instead assert here. 31 | assert!(width >= 0); 32 | assert!(height >= 0); 33 | let result = unsafe { CreateBitmap(width, height, planes, bit_count, ptr::null()) }; 34 | NonNull::new(result) 35 | .map(|bitmap| Bitmap { bitmap }) 36 | .ok_or(()) 37 | } 38 | 39 | pub fn load(resource: u16) -> Result { 40 | let result = unsafe { LoadBitmapA(base_instance(), MAKEINTRESOURCEA(resource)) }; 41 | NonNull::new(result) 42 | .map(|bitmap| Bitmap { bitmap }) 43 | .ok_or(()) 44 | } 45 | 46 | pub fn from_file(path: &str) -> crate::Result { 47 | let path = CString::new(path)?; 48 | let result = unsafe { 49 | LoadImageA( 50 | ptr::null_mut(), 51 | path.as_ptr() as LPCSTR, 52 | IMAGE_BITMAP, 53 | 0, 54 | 0, 55 | LR_LOADFROMFILE, 56 | ) 57 | }; 58 | non_null_or_err(result as HBITMAP).map(|bitmap| Bitmap { bitmap }) 59 | } 60 | 61 | impl Bitmap { 62 | /// Retrieves information for the specified graphics object. 63 | pub fn info(&self) -> Result { 64 | let mut info: BITMAP = unsafe { mem::zeroed() }; 65 | let result = unsafe { 66 | GetObjectA( 67 | self.bitmap.as_ptr() as HANDLE, 68 | mem::size_of::() as i32, 69 | &mut info as *mut BITMAP as LPVOID, 70 | ) 71 | }; 72 | if result != 0 { 73 | Ok(Info { 74 | info, 75 | _size: result as usize, 76 | }) 77 | } else { 78 | Err(()) 79 | } 80 | } 81 | 82 | /// Replace all pixels with the given color with a transparent pixel. 83 | /// 84 | /// This essentially tells the bitmap which color to treat as transparent. 85 | /// 86 | /// The mask used to update self is returned. 87 | pub fn set_color_transparent(&self, (r, g, b): (u8, u8, u8)) -> Result { 88 | let info = self.info().unwrap(); 89 | 90 | // Create the bitmap that will hold the object mask (single plane, single bit depth). 91 | let mask_bmp = new(info.width(), info.height(), 1, 1).unwrap(); 92 | 93 | // Create the canvas we can operate on (and drop it or the bmp will remain held). 94 | { 95 | let masked = Canvas::from_current_screen() 96 | .unwrap() 97 | .bind(self) 98 | .map_err(drop) // TODO remove this once it impls debug 99 | .unwrap(); 100 | 101 | let mask = Canvas::from_current_screen() 102 | .unwrap() 103 | .bind(&mask_bmp) 104 | .map_err(drop) // TODO remove this once it impls debug 105 | .unwrap(); 106 | 107 | // Here's where the magic happens. 108 | masked.set_background((r, g, b)).unwrap(); 109 | mask.bitwise().set(&masked).unwrap(); 110 | masked.bitwise().xor(&mask).unwrap(); 111 | } 112 | 113 | Ok(mask_bmp) 114 | } 115 | } 116 | 117 | impl Paint for Bitmap { 118 | fn as_gdi_obj(&self) -> HGDIOBJ { 119 | self.bitmap.as_ptr() as HGDIOBJ 120 | } 121 | } 122 | 123 | impl Info { 124 | /// The width, in pixels, of the bitmap. The width is greater than zero. 125 | pub fn width(&self) -> i32 { 126 | self.info.bmWidth 127 | } 128 | 129 | /// The height, in pixels, of the bitmap. The height is greater than zero. 130 | pub fn height(&self) -> i32 { 131 | self.info.bmHeight 132 | } 133 | 134 | /// The number of bytes in each scan line. This value is divisible by 2, because the system 135 | /// assumes that the bit values of a bitmap form an array that is word aligned. 136 | pub fn stride(&self) -> i32 { 137 | self.info.bmWidthBytes 138 | } 139 | 140 | /// The count of color planes. 141 | pub fn planes(&self) -> u16 { 142 | self.info.bmPlanes 143 | } 144 | 145 | /// The number of bits required to indicate the color of a pixel. 146 | pub fn bits_per_pixel(&self) -> u16 { 147 | self.info.bmBitsPixel 148 | } 149 | } 150 | 151 | impl Drop for Bitmap { 152 | fn drop(&mut self) { 153 | let result = unsafe { DeleteObject(self.bitmap.as_ptr() as HGDIOBJ) }; 154 | if result == 0 { 155 | panic!("failed to delete bitmap, it might still be in use"); 156 | } 157 | } 158 | } 159 | -------------------------------------------------------------------------------- /examples/app_two.rs: -------------------------------------------------------------------------------- 1 | //! Using files and the common dialogs. 2 | //! http://winprog.org/tutorial/app_two.html 3 | use std::fs::File; 4 | use std::io::{Read, Write}; 5 | use std::process::exit; 6 | use minimal_windows_gui as win; 7 | 8 | const CLASS_NAME: &str = "myWindowClass"; 9 | 10 | const IDC_MAIN_EDIT: u16 = 101; 11 | 12 | const ID_FILE_EXIT: u16 = 40001; 13 | const ID_FILE_OPEN: u16 = 40002; 14 | const ID_FILE_SAVEAS: u16 = 40003; 15 | const ID_FILE_NEW: u16 = 40004; 16 | 17 | fn main() -> win::Result<()> { 18 | let class = &win::class::build() 19 | .load_icon(win::icon::Icon::Application)? 20 | .load_cursor(win::cursor::Cursor::Arrow)? 21 | .background(win::class::Background::Window) 22 | .load_small_icon(win::icon::Icon::Application)? 23 | .register(CLASS_NAME) 24 | .expect("window registration failed"); 25 | 26 | let window = win::window::build() 27 | .set_message_callback(main_window_callback) 28 | .add_extended_style(win::window::ExtendedStyle::ClientEdge) 29 | .add_style(win::window::Style::OverlappedWindow) 30 | .size(480, 320) 31 | .create(class, "Tutorial Application") 32 | .expect("window creation failed"); 33 | 34 | window.show_default(); 35 | window.update().unwrap(); 36 | 37 | exit(win::message_loop()) 38 | } 39 | 40 | fn main_window_callback( 41 | window: &win::window::Window, 42 | message: win::message::Message, 43 | ) -> Option { 44 | use win::message::Message; 45 | 46 | match message { 47 | Message::Create => { 48 | let edit_ctl = win::window::build() 49 | .add_extended_style(win::window::ExtendedStyle::ClientEdge) 50 | .add_style(win::window::Style::Visible) 51 | .add_style(win::window::Style::VerticalScroll) 52 | .add_style(win::window::Style::HorizontalScroll) 53 | .add_style(win::window::Style::Multiline) 54 | .add_style(win::window::Style::AutoVerticalScroll) 55 | .add_style(win::window::Style::AutoHorizontalScroll) 56 | .pos(0, 0) 57 | .size(100, 100) 58 | .parent(window) 59 | .set_child_id(IDC_MAIN_EDIT) 60 | .create(win::class::edit_control(), "") 61 | .expect("edit box creation failed"); 62 | 63 | let font = win::font::get_default().unwrap(); 64 | edit_ctl.set_font(font); 65 | 66 | // Created after so that its automatic ID does not collide with other controls. 67 | // The original tutorial uses a resource file but for simplicity we want to avoid 68 | // the additional complexity. 69 | // TODO figure out how/if it's possible to set a child ID to a menu like this. 70 | let menu = win::menu::Menu::new().unwrap(); 71 | let submenu = win::menu::Menu::new_popup().unwrap(); 72 | submenu.append_item("&New", ID_FILE_NEW).unwrap(); 73 | submenu.append_item("&Open...", ID_FILE_OPEN).unwrap(); 74 | submenu.append_item("Save &As...", ID_FILE_SAVEAS).unwrap(); 75 | submenu.append_separator().unwrap(); 76 | submenu.append_item("E&xit", ID_FILE_EXIT).unwrap(); 77 | menu.append_menu("&File", submenu).unwrap(); 78 | window.set_menu(menu).unwrap(); 79 | } 80 | Message::Size(info) => { 81 | let edit_ctl = window.get_dialog_item(IDC_MAIN_EDIT).unwrap(); 82 | edit_ctl 83 | .set_rect(win::rect::Rect::new( 84 | info.width() as i32, 85 | info.height() as i32, 86 | )) 87 | .unwrap(); 88 | } 89 | Message::Command(info) => match info.menu_id().unwrap_or(0) { 90 | ID_FILE_EXIT => { 91 | window.close().unwrap(); 92 | } 93 | ID_FILE_NEW => { 94 | let edit_ctl = window.get_dialog_item(IDC_MAIN_EDIT).unwrap(); 95 | edit_ctl.set_text(""); 96 | } 97 | ID_FILE_OPEN => { 98 | do_open(window); 99 | } 100 | ID_FILE_SAVEAS => { 101 | do_save(window); 102 | } 103 | _ => {} 104 | }, 105 | Message::Close => { 106 | window.destroy().unwrap(); 107 | } 108 | Message::Destroy => { 109 | win::post_quit_message(0); 110 | } 111 | _ => return None, 112 | } 113 | 114 | Some(0) 115 | } 116 | 117 | // The whole file creation, obtaining the file size, allocating a large enough buffer, reading 118 | // and writing, could of course be abstracted away by this library. However, Rust's standard 119 | // library already provides wonderful abstractions to do this, which means this library does 120 | // not need to provide abstractions for those (and so it doesn't). 121 | 122 | fn do_open(window: &win::window::Window) { 123 | use win::dialog::OpenFileConfig as Config; 124 | 125 | if let Some(file) = window 126 | .open_file() 127 | .set_filters(&[("Text Files (*.txt)", "*.txt"), ("All Files (*.*)", "*.*")]) 128 | .set_default_ext("txt") 129 | .add_config(Config::Explorer) 130 | .add_config(Config::FileMustExist) 131 | .add_config(Config::HideReadonly) 132 | .ask_open_path() 133 | { 134 | let mut file = File::open(file).unwrap(); 135 | let mut contents = String::new(); 136 | file.read_to_string(&mut contents).unwrap(); 137 | 138 | let edit_ctl = window.get_dialog_item(IDC_MAIN_EDIT).unwrap(); 139 | edit_ctl.set_text(&contents); 140 | } 141 | } 142 | 143 | fn do_save(window: &win::window::Window) { 144 | use win::dialog::OpenFileConfig as Config; 145 | 146 | if let Some(file) = window 147 | .open_file() 148 | .set_filters(&[("Text Files (*.txt)", "*.txt"), ("All Files (*.*)", "*.*")]) 149 | .set_default_ext("txt") 150 | .add_config(Config::Explorer) 151 | .add_config(Config::PathMustExist) 152 | .add_config(Config::HideReadonly) 153 | .add_config(Config::OverwritePrompt) 154 | .ask_save_path() 155 | { 156 | let edit_ctl = window.get_dialog_item(IDC_MAIN_EDIT).unwrap(); 157 | let contents = edit_ctl.get_text(); 158 | 159 | let mut file = File::create(file).unwrap(); 160 | file.write_all(contents.as_bytes()).unwrap(); 161 | } 162 | } 163 | -------------------------------------------------------------------------------- /src/messagebox.rs: -------------------------------------------------------------------------------- 1 | use crate::{Error, Result}; 2 | use std::ffi::CString; 3 | use std::ptr; 4 | use winapi::shared::minwindef::UINT; 5 | use winapi::um::winuser::{ 6 | MessageBoxA, IDABORT, IDCANCEL, IDCONTINUE, IDIGNORE, IDNO, IDOK, IDRETRY, IDTRYAGAIN, IDYES, 7 | MB_ABORTRETRYIGNORE, MB_APPLMODAL, MB_CANCELTRYCONTINUE, MB_DEFAULT_DESKTOP_ONLY, 8 | MB_DEFBUTTON2, MB_DEFBUTTON3, MB_DEFBUTTON4, MB_HELP, MB_ICONERROR, MB_ICONINFORMATION, 9 | MB_ICONQUESTION, MB_ICONWARNING, MB_OKCANCEL, MB_RETRYCANCEL, MB_RIGHT, MB_RTLREADING, 10 | MB_SERVICE_NOTIFICATION, MB_SETFOREGROUND, MB_SYSTEMMODAL, MB_TASKMODAL, MB_TOPMOST, MB_YESNO, 11 | MB_YESNOCANCEL, 12 | }; 13 | 14 | /// Message box type configuration as defined in https://docs.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-messagebox. 15 | #[derive(Clone, Copy)] 16 | #[repr(u32)] 17 | pub enum Config { 18 | /// The message box contains three push buttons: Abort, Retry, and Ignore. 19 | AbortRetryIgnore = MB_ABORTRETRYIGNORE, 20 | 21 | /// The message box contains three push buttons: Cancel, Try Again, Continue. Use this message box type instead of `AbortRetryIgnore`. 22 | CancelTryContinue = MB_CANCELTRYCONTINUE, 23 | 24 | /// Adds a Help button to the message box. When the user clicks the Help button or presses F1, the system sends a `Help` message to the owner. 25 | Help = MB_HELP, 26 | 27 | /// The message box contains two push buttons: OK and Cancel. 28 | OkCancel = MB_OKCANCEL, 29 | 30 | /// The message box contains two push buttons: Retry and Cancel. 31 | RetryCancel = MB_RETRYCANCEL, 32 | 33 | /// The message box contains two push buttons: Yes and No. 34 | YesNo = MB_YESNO, 35 | 36 | /// The message box contains three push buttons: Yes, No, and Cancel. 37 | YesNoCancel = MB_YESNOCANCEL, 38 | 39 | /// An exclamation-point icon appears in the message box. 40 | IconWarning = MB_ICONWARNING, 41 | 42 | /// An icon consisting of a lowercase letter i in a circle appears in the message box. 43 | IconInformation = MB_ICONINFORMATION, 44 | 45 | /// A question-mark icon appears in the message box. The question-mark message icon is no longer recommended because it does not clearly represent a specific type of message and because the phrasing of a message as a question could apply to any message type. In addition, users can confuse the message symbol question mark with Help information. Therefore, do not use this question mark message symbol in your message boxes. The system continues to support its inclusion only for backward compatibility. 46 | IconQuestion = MB_ICONQUESTION, 47 | 48 | /// A stop-sign icon appears in the message box. 49 | IconError = MB_ICONERROR, 50 | 51 | /// The second button is the default button. 52 | DefaultButton2 = MB_DEFBUTTON2, 53 | 54 | /// The third button is the default button. 55 | DefaultButton3 = MB_DEFBUTTON3, 56 | 57 | /// The fourth button is the default button. 58 | DefaultButton4 = MB_DEFBUTTON4, 59 | 60 | /// The user must respond to the message box before continuing work in the window identified by the hWnd parameter. However, the user can move to the windows of other threads and work in those windows. Depending on the hierarchy of windows in the application, the user may be able to move to other windows within the thread. All child windows of the parent of the message box are automatically disabled, but pop-up windows are not. `ApplicationModal` is the default if neither `SystemModal` nor `TaskModal` is specified. 61 | ApplicationModal = MB_APPLMODAL, 62 | 63 | /// Same as `ApplicationModal` except that the message box has the `TopMost` style. Use system-modal message boxes to notify the user of serious, potentially damaging errors that require immediate attention (for example, running out of memory). This flag has no effect on the user's ability to interact with windows other than those associated with hWnd. 64 | SystemModal = MB_SYSTEMMODAL, 65 | 66 | /// Same as `ApplicationModal` except that all the top-level windows belonging to the current thread are disabled if the hWnd parameter is NULL. Use this flag when the calling application or library does not have a window handle available but still needs to prevent input to other windows in the calling thread without suspending other threads. 67 | TaskModal = MB_TASKMODAL, 68 | 69 | /// Same as desktop of the interactive window station. For more information, see Window Stations. If the current input desktop is not the default desktop, MessageBox does not return until the user switches to the default desktop. 70 | DefaultDesktopOnly = MB_DEFAULT_DESKTOP_ONLY, 71 | 72 | /// The text is right-justified. 73 | Right = MB_RIGHT, 74 | 75 | /// Displays message and caption text using right-to-left reading order on Hebrew and Arabic systems. 76 | RtlReading = MB_RTLREADING, 77 | 78 | /// The message box becomes the foreground window. Internally, the system calls the SetForegroundWindow function for the message box. 79 | SetForeground = MB_SETFOREGROUND, 80 | 81 | /// The message box is created with the `TopMost` window style. 82 | TopMost = MB_TOPMOST, 83 | 84 | /// The caller is a service notifying the user of an event. The function displays a message box on the current active desktop, even if there is no user logged on to the computer. Terminal Services: If the calling thread has an impersonation token, the function directs the message box to the session specified in the impersonation token. If this flag is set, the hWnd parameter must be NULL. This is so that the message box can appear on a desktop other than the desktop corresponding to the hWnd. For information on security considerations in regard to using this flag, see Interactive Services. In particular, be aware that this flag can produce interactive content on a locked desktop and should therefore be used for only a very limited set of scenarios, such as resource exhaustion. 85 | ServiceNotification = MB_SERVICE_NOTIFICATION, 86 | } 87 | 88 | /// Indicates which button was selected. 89 | /// 90 | /// If a message box has a Cancel button, the function returns the `Cancel` value if either the 91 | /// ESC key is pressed or the Cancel button is selected. If the message box has no Cancel button, 92 | /// pressing ESC will no effect - unless an `Ok` button is present. If an `Ok` button is 93 | /// displayed and the user presses ESC, the return value will be `Ok`. 94 | #[derive(Debug)] 95 | #[repr(i32)] 96 | pub enum Button { 97 | Abort = IDABORT, 98 | Cancel = IDCANCEL, 99 | Continue = IDCONTINUE, 100 | Ignore = IDIGNORE, 101 | No = IDNO, 102 | Ok = IDOK, 103 | Retry = IDRETRY, 104 | TryAgain = IDTRYAGAIN, 105 | Yes = IDYES, 106 | } 107 | 108 | impl Button { 109 | pub(crate) fn from_id(id: i32) -> Result> { 110 | Ok(Some(match id { 111 | 0 => return Err(Error::last_os_error()), 112 | IDABORT => Button::Abort, 113 | IDCANCEL => Button::Cancel, 114 | IDCONTINUE => Button::Continue, 115 | IDIGNORE => Button::Ignore, 116 | IDNO => Button::No, 117 | IDOK => Button::Ok, 118 | IDRETRY => Button::Retry, 119 | IDTRYAGAIN => Button::TryAgain, 120 | IDYES => Button::Yes, 121 | _ => return Ok(None), 122 | })) 123 | } 124 | } 125 | 126 | /// If no config is provided, the message box defaults to containing one push button: OK. 127 | /// The first button is the default button 128 | pub fn message_box(caption: &str, text: &str, config: &[Config]) -> Result