├── .gitignore ├── Cargo.toml ├── README.md ├── examples ├── anim_one.rs ├── app_one.rs ├── app_three.rs ├── app_two.rs ├── bmp_one │ ├── Cargo.toml │ ├── ball.bmp │ ├── bmp_one.rc │ ├── resource.h │ └── src │ │ ├── build.rs │ │ └── main.rs ├── bmp_two.rs ├── ctl_one │ ├── Cargo.toml │ ├── ctl_one.rc │ ├── resource.h │ └── src │ │ ├── build.rs │ │ └── main.rs ├── dlg_one │ ├── Cargo.toml │ ├── dlg_one.rc │ ├── resource.h │ └── src │ │ ├── build.rs │ │ └── main.rs ├── dlg_three │ ├── Cargo.toml │ ├── dlg_three.rc │ ├── resource.h │ └── src │ │ ├── build.rs │ │ └── main.rs ├── dlg_two │ ├── Cargo.toml │ ├── dlg_two.rc │ ├── resource.h │ └── src │ │ ├── build.rs │ │ └── main.rs ├── intro.rs ├── menu_one │ ├── Cargo.toml │ ├── menu_one.ico │ ├── menu_one.rc │ ├── resource.h │ └── src │ │ ├── build.rs │ │ └── main.rs ├── menu_two.rs ├── simple_window.rs └── window_click.rs └── src ├── class.rs ├── cursor.rs ├── dialog.rs ├── font.rs ├── gdi ├── bitmap.rs ├── brush.rs ├── canvas.rs └── mod.rs ├── icon.rs ├── lib.rs ├── menu.rs ├── message.rs ├── messagebox.rs ├── rect.rs ├── toolbar.rs └── window.rs /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | Cargo.lock 3 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /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/app_three.rs: -------------------------------------------------------------------------------- 1 | //! Tool and Status bars. 2 | //! http://winprog.org/tutorial/app_three.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 | const IDC_MAIN_TOOL: u16 = 102; 12 | const IDC_MAIN_STATUS: u16 = 103; 13 | 14 | const ID_FILE_EXIT: u16 = 40001; 15 | const ID_FILE_OPEN: u16 = 40002; 16 | const ID_FILE_SAVEAS: u16 = 40003; 17 | const ID_FILE_NEW: u16 = 40004; 18 | 19 | fn main() -> win::Result<()> { 20 | win::init_common_controls(); 21 | 22 | let class = &win::class::build() 23 | .load_icon(win::icon::Icon::Application)? 24 | .load_cursor(win::cursor::Cursor::Arrow)? 25 | .background(win::class::Background::Window) 26 | .load_small_icon(win::icon::Icon::Application)? 27 | .register(CLASS_NAME) 28 | .expect("window registration failed"); 29 | 30 | let window = 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(480, 320) 35 | .create(class, "Tutorial Application") 36 | .expect("window creation failed"); 37 | 38 | window.show_default(); 39 | window.update().unwrap(); 40 | 41 | exit(win::message_loop()) 42 | } 43 | 44 | fn main_window_callback( 45 | window: &win::window::Window, 46 | message: win::message::Message, 47 | ) -> Option { 48 | use win::message::Message; 49 | 50 | match message { 51 | Message::Create => { 52 | let edit_ctl = win::window::build() 53 | .add_extended_style(win::window::ExtendedStyle::ClientEdge) 54 | .add_style(win::window::Style::Visible) 55 | .add_style(win::window::Style::VerticalScroll) 56 | .add_style(win::window::Style::HorizontalScroll) 57 | .add_style(win::window::Style::Multiline) 58 | .add_style(win::window::Style::AutoVerticalScroll) 59 | .add_style(win::window::Style::AutoHorizontalScroll) 60 | .pos(0, 0) 61 | .size(100, 100) 62 | .parent(window) 63 | .set_child_id(IDC_MAIN_EDIT) 64 | .create(win::class::edit_control(), "") 65 | .expect("edit box creation failed"); 66 | 67 | let font = win::font::get_default().unwrap(); 68 | edit_ctl.set_font(font); 69 | 70 | let tool_ctl = win::window::build() 71 | .add_style(win::window::Style::Visible) 72 | .pos(0, 0) 73 | .size(0, 0) 74 | .parent(window) 75 | .set_child_id(IDC_MAIN_TOOL) 76 | .create(win::class::toolbar(), "") 77 | .expect("toolbar creation failed"); 78 | 79 | tool_ctl 80 | .add_toolbar_buttons(&[ 81 | win::toolbar::Button::new(ID_FILE_NEW, win::toolbar::Icon::FileNew), 82 | win::toolbar::Button::new(ID_FILE_OPEN, win::toolbar::Icon::FileOpen), 83 | win::toolbar::Button::new(ID_FILE_SAVEAS, win::toolbar::Icon::FileSave), 84 | ]) 85 | .unwrap(); 86 | 87 | // FIXME NoHideSelection == StatusBarSizeGrip 88 | let status_ctl = win::window::build() 89 | .add_style(win::window::Style::Visible) 90 | .add_style(win::window::Style::NoHideSelection) 91 | .pos(0, 0) 92 | .size(0, 0) 93 | .parent(window) 94 | .set_child_id(IDC_MAIN_STATUS) 95 | .create(win::class::status_bar(), "") 96 | .expect("status bar creation failed"); 97 | 98 | status_ctl.set_toolbar_parts(&[100, -1]).unwrap(); 99 | status_ctl.set_toolbar_text(0, "Hi there :)").unwrap(); 100 | 101 | // TODO figure out how/if it's possible to set a child ID to a menu like this. 102 | let menu = win::menu::Menu::new().unwrap(); 103 | let submenu = win::menu::Menu::new_popup().unwrap(); 104 | submenu.append_item("&New", ID_FILE_NEW).unwrap(); 105 | submenu.append_item("&Open...", ID_FILE_OPEN).unwrap(); 106 | submenu.append_item("Save &As...", ID_FILE_SAVEAS).unwrap(); 107 | submenu.append_separator().unwrap(); 108 | submenu.append_item("E&xit", ID_FILE_EXIT).unwrap(); 109 | menu.append_menu("&File", submenu).unwrap(); 110 | window.set_menu(menu).unwrap(); 111 | } 112 | Message::Size(_info) => { 113 | let tool_ctl = window.get_dialog_item(IDC_MAIN_TOOL).unwrap(); 114 | tool_ctl.auto_size_toolbar(); 115 | let tool_height = tool_ctl.get_rect().unwrap().height(); 116 | 117 | let status_ctl = window.get_dialog_item(IDC_MAIN_STATUS).unwrap(); 118 | status_ctl.restore(); 119 | let status_height = status_ctl.get_rect().unwrap().height(); 120 | 121 | let window_rect = window.get_rect().unwrap(); 122 | 123 | let edit_ctl = window.get_dialog_item(IDC_MAIN_EDIT).unwrap(); 124 | edit_ctl 125 | .set_rect( 126 | window_rect 127 | .resized_by(0, -(tool_height + status_height)) 128 | .at(0, tool_height), 129 | ) 130 | .unwrap(); 131 | } 132 | Message::Command(info) => match info 133 | .menu_id() 134 | .unwrap_or_else(|| info.control_data().map(|c| c.id).unwrap_or(0)) 135 | { 136 | ID_FILE_EXIT => { 137 | window.close().unwrap(); 138 | } 139 | ID_FILE_NEW => { 140 | let edit_ctl = window.get_dialog_item(IDC_MAIN_EDIT).unwrap(); 141 | edit_ctl.set_text(""); 142 | } 143 | ID_FILE_OPEN => { 144 | do_open(window); 145 | } 146 | ID_FILE_SAVEAS => { 147 | do_save(window); 148 | } 149 | _ => {} 150 | }, 151 | Message::Close => { 152 | window.destroy().unwrap(); 153 | } 154 | Message::Destroy => { 155 | win::post_quit_message(0); 156 | } 157 | _ => return None, 158 | } 159 | 160 | Some(0) 161 | } 162 | 163 | fn do_open(window: &win::window::Window) { 164 | use win::dialog::OpenFileConfig as Config; 165 | 166 | if let Some(path) = window 167 | .open_file() 168 | .set_filters(&[("Text Files (*.txt)", "*.txt"), ("All Files (*.*)", "*.*")]) 169 | .set_default_ext("txt") 170 | .add_config(Config::Explorer) 171 | .add_config(Config::FileMustExist) 172 | .add_config(Config::HideReadonly) 173 | .ask_open_path() 174 | { 175 | let mut file = File::open(&path).unwrap(); 176 | let mut contents = String::new(); 177 | file.read_to_string(&mut contents).unwrap(); 178 | 179 | let edit_ctl = window.get_dialog_item(IDC_MAIN_EDIT).unwrap(); 180 | edit_ctl.set_text(&contents); 181 | 182 | let status_ctl = window.get_dialog_item(IDC_MAIN_STATUS).unwrap(); 183 | status_ctl.set_toolbar_text(0, "Opened...").unwrap(); 184 | status_ctl.set_toolbar_text(1, &path).unwrap(); 185 | } 186 | } 187 | 188 | fn do_save(window: &win::window::Window) { 189 | use win::dialog::OpenFileConfig as Config; 190 | 191 | if let Some(path) = window 192 | .open_file() 193 | .set_filters(&[("Text Files (*.txt)", "*.txt"), ("All Files (*.*)", "*.*")]) 194 | .set_default_ext("txt") 195 | .add_config(Config::Explorer) 196 | .add_config(Config::PathMustExist) 197 | .add_config(Config::HideReadonly) 198 | .add_config(Config::OverwritePrompt) 199 | .ask_save_path() 200 | { 201 | let edit_ctl = window.get_dialog_item(IDC_MAIN_EDIT).unwrap(); 202 | let contents = edit_ctl.get_text(); 203 | 204 | let mut file = File::create(&path).unwrap(); 205 | file.write_all(contents.as_bytes()).unwrap(); 206 | 207 | let status_ctl = window.get_dialog_item(IDC_MAIN_STATUS).unwrap(); 208 | status_ctl.set_toolbar_text(0, "Saved...").unwrap(); 209 | status_ctl.set_toolbar_text(1, &path).unwrap(); 210 | } 211 | } 212 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/bmp_one/ball.bmp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lonami/rust-windows-gui/28ddc9f17114fd935d698648b226862a4b1d1f06/examples/bmp_one/ball.bmp -------------------------------------------------------------------------------- /examples/bmp_one/bmp_one.rc: -------------------------------------------------------------------------------- 1 | #include "resource.h" 2 | 3 | IDB_BALL BITMAP "ball.bmp" 4 | -------------------------------------------------------------------------------- /examples/bmp_one/resource.h: -------------------------------------------------------------------------------- 1 | #define IDB_BALL 101 2 | -------------------------------------------------------------------------------- /examples/bmp_one/src/build.rs: -------------------------------------------------------------------------------- 1 | use embed_resource; 2 | 3 | fn main() { 4 | embed_resource::compile("bmp_one.rc"); 5 | } 6 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /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/ctl_one/src/build.rs: -------------------------------------------------------------------------------- 1 | use embed_resource; 2 | 3 | fn main() { 4 | embed_resource::compile("ctl_one.rc"); 5 | } 6 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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_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/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/dlg_one/src/build.rs: -------------------------------------------------------------------------------- 1 | use embed_resource; 2 | 3 | fn main() { 4 | embed_resource::compile("dlg_one.rc"); 5 | } 6 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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_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 | -------------------------------------------------------------------------------- /examples/dlg_three/resource.h: -------------------------------------------------------------------------------- 1 | #define IDC_STATIC -1 2 | #define IDD_MAIN 101 3 | -------------------------------------------------------------------------------- /examples/dlg_three/src/build.rs: -------------------------------------------------------------------------------- 1 | use embed_resource; 2 | 3 | fn main() { 4 | embed_resource::compile("dlg_three.rc"); 5 | } 6 | -------------------------------------------------------------------------------- /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/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/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/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/dlg_two/src/build.rs: -------------------------------------------------------------------------------- 1 | use embed_resource; 2 | 3 | fn main() { 4 | embed_resource::compile("dlg_two.rc"); 5 | } 6 | -------------------------------------------------------------------------------- /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/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/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.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lonami/rust-windows-gui/28ddc9f17114fd935d698648b226862a4b1d1f06/examples/menu_one/menu_one.ico -------------------------------------------------------------------------------- /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/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/menu_one/src/build.rs: -------------------------------------------------------------------------------- 1 | use embed_resource; 2 | 3 | fn main() { 4 | embed_resource::compile("menu_one.rc"); 5 | } 6 | -------------------------------------------------------------------------------- /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/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/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/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/class.rs: -------------------------------------------------------------------------------- 1 | //! Window classes https://docs.microsoft.com/en-us/windows/win32/winmsg/about-window-classes. 2 | //! Additionally contains methods to reference system classes. 3 | use crate::{base_instance, cursor, icon, message, window, Error, Result}; 4 | use std::ffi::CString; 5 | use std::num::NonZeroU16; 6 | use std::ptr::{self, NonNull}; 7 | use winapi::shared::minwindef::{LPARAM, LRESULT, UINT, WPARAM}; 8 | use winapi::shared::windef::{HBRUSH, HCURSOR, HICON, HWND}; 9 | use winapi::um::winnt::{LPCSTR, LPSTR}; 10 | use winapi::um::winuser::{ 11 | DefWindowProcA, RegisterClassExA, UnregisterClassA, COLOR_3DDKSHADOW, COLOR_3DFACE, 12 | COLOR_3DHIGHLIGHT, COLOR_3DLIGHT, COLOR_3DSHADOW, COLOR_ACTIVEBORDER, COLOR_ACTIVECAPTION, 13 | COLOR_APPWORKSPACE, COLOR_BTNTEXT, COLOR_CAPTIONTEXT, COLOR_DESKTOP, 14 | COLOR_GRADIENTACTIVECAPTION, COLOR_GRADIENTINACTIVECAPTION, COLOR_GRAYTEXT, COLOR_HIGHLIGHT, 15 | COLOR_HIGHLIGHTTEXT, COLOR_HOTLIGHT, COLOR_INACTIVEBORDER, COLOR_INACTIVECAPTION, 16 | COLOR_INACTIVECAPTIONTEXT, COLOR_INFOBK, COLOR_INFOTEXT, COLOR_MENU, COLOR_MENUBAR, 17 | COLOR_MENUHILIGHT, COLOR_MENUTEXT, COLOR_SCROLLBAR, COLOR_WINDOW, COLOR_WINDOWFRAME, 18 | COLOR_WINDOWTEXT, CS_BYTEALIGNCLIENT, CS_BYTEALIGNWINDOW, CS_CLASSDC, CS_DBLCLKS, 19 | CS_DROPSHADOW, CS_GLOBALCLASS, CS_HREDRAW, CS_NOCLOSE, CS_OWNDC, CS_PARENTDC, CS_SAVEBITS, 20 | CS_VREDRAW, MAKEINTRESOURCEA, WNDCLASSEXA, 21 | }; 22 | 23 | /// Class styles as defined in https://docs.microsoft.com/en-us/windows/win32/winmsg/window-class-styles. 24 | #[repr(u32)] 25 | pub enum Style { 26 | /// Aligns the window's client area on a byte boundary (in the x direction). This style affects the width of the window and its horizontal placement on the display. 27 | ByteAlignClient = CS_BYTEALIGNCLIENT as UINT, 28 | 29 | /// Aligns the window on a byte boundary (in the x direction). This style affects the width of the window and its horizontal placement on the display. 30 | ByteAlignWindow = CS_BYTEALIGNWINDOW as UINT, 31 | 32 | /// Allocates one device context to be shared by all windows in the class. Because window classes are process specific, it is possible for multiple threads of an application to create a window of the same class. It is also possible for the threads to attempt to use the device context simultaneously. When this happens, the system allows only one thread to successfully finish its drawing operation. 33 | ClassDc = CS_CLASSDC as UINT, 34 | 35 | /// Sends a double-click message to the window procedure when the user double-clicks the mouse while the cursor is within a window belonging to the class. 36 | DoubleClicks = CS_DBLCLKS as UINT, 37 | 38 | /// Enables the drop shadow effect on a window. The effect is turned on and off through SPI_SETDROPSHADOW. Typically, this is enabled for small, short-lived windows such as menus to emphasize their Z-order relationship to other windows. Windows created from a class with this style must be top-level windows; they may not be child windows. 39 | DropShadow = CS_DROPSHADOW as UINT, 40 | 41 | /// Indicates that the window class is an application global class. For more information, see the "Application Global Classes" section of About Window Classes. 42 | GlobalClass = CS_GLOBALCLASS as UINT, 43 | 44 | /// Redraws the entire window if a movement or size adjustment changes the width of the client area. 45 | HorizontalRedraw = CS_HREDRAW as UINT, 46 | 47 | /// Disables Close on the window menu. 48 | NoClose = CS_NOCLOSE as UINT, 49 | 50 | /// Allocates a unique device context for each window in the class. 51 | Wndc = CS_OWNDC as UINT, 52 | 53 | /// PARENTDC enhances an application's performance. = Sets the clipping rectangle of the child window to that of the parent window so that the child can draw on the parent. A window with the PARENTDC style bit receives a regular device context from the system's cache of device contexts. It does not give the child the parent's device context or device context settings. Specifying PARENTDC enhances an application's performance., 54 | ParentDc = CS_PARENTDC as UINT, 55 | 56 | /// Saves, as a bitmap, the portion of the screen image obscured by a window of this class. When the window is removed, the system uses the saved bitmap to restore the screen image, including other windows that were obscured. Therefore, the system does not send WM_PAINT messages to windows that were obscured if the memory used by the bitmap has not been discarded and if other screen actions have not invalidated the stored image. 57 | /// 58 | /// This style is useful for small windows (for example, menus or dialog boxes) that are displayed briefly and then removed before other screen activity takes place. This style increases the time required to display the window, because the system must first allocate memory to store the bitmap. 59 | SaveBits = CS_SAVEBITS as UINT, 60 | 61 | /// Redraws the entire window if a movement or size adjustment changes the height of the client area. 62 | VerticalRedraw = CS_VREDRAW as UINT, 63 | } 64 | 65 | // https://docs.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-getsyscolor 66 | #[repr(i32)] 67 | pub enum Background { 68 | /// Dark shadow for three-dimensional display elements. 69 | DarkShadow3D = COLOR_3DDKSHADOW, 70 | 71 | /// Face color for three-dimensional display elements and for dialog box backgrounds. 72 | /// The associated foreground color is `ButtonText`. 73 | /// Equivalent to `ButtonFace`. 74 | Face3D = COLOR_3DFACE, 75 | 76 | /// Highlight color for three-dimensional display elements (for edges facing the light source.) 77 | /// Equivalent to `ButtonHighlight`. 78 | Highlight3D = COLOR_3DHIGHLIGHT, 79 | 80 | /// Light color for three-dimensional display elements (for edges facing the light source.) 81 | Light3D = COLOR_3DLIGHT, 82 | 83 | /// Shadow color for three-dimensional display elements (for edges facing away from the light source). 84 | /// Equivalent to `ButtonShadow`. 85 | Shadow3D = COLOR_3DSHADOW, 86 | 87 | /// Active window border. 88 | ActiveBorder = COLOR_ACTIVEBORDER, 89 | 90 | /// Active window title bar. The associated foreground color is `CaptionText`. 91 | /// Specifies the left side color in the color gradient of an active window's title bar if 92 | /// the gradient effect is enabled. 93 | ActiveCaption = COLOR_ACTIVECAPTION, 94 | 95 | /// Background color of multiple document interface (MDI) applications. 96 | AppWorkspace = COLOR_APPWORKSPACE, 97 | 98 | /// Text on push buttons. The associated background color is `ButtonFace`. 99 | ButtonText = COLOR_BTNTEXT, 100 | 101 | /// Text in caption, size box, and scroll bar arrow box. The associated background color is `ActiveCaption`. 102 | CaptionText = COLOR_CAPTIONTEXT, 103 | 104 | /// Desktop. Equivalent to `Background`. 105 | Desktop = COLOR_DESKTOP, 106 | 107 | /// Right side color in the color gradient of an active window's title bar. `ActiveCaption` 108 | /// specifies the left side color. 109 | GradientActiveCaption = COLOR_GRADIENTACTIVECAPTION, 110 | 111 | /// Right side color in the color gradient of an inactive window's title bar. `InactiveCaption` specifies the left side color. 112 | GradientInactiveCaption = COLOR_GRADIENTINACTIVECAPTION, 113 | 114 | /// Grayed (disabled) text. This color is set to 0 if the current display driver does not support a solid gray color. 115 | GrayText = COLOR_GRAYTEXT, 116 | 117 | /// Item(s) selected in a control. The associated foreground color is `HighlightText`. 118 | Highlight = COLOR_HIGHLIGHT, 119 | 120 | /// Text of item(s) selected in a control. The associated background color is `Highlight`. 121 | HighlightText = COLOR_HIGHLIGHTTEXT, 122 | 123 | /// Color for a hyperlink or hot-tracked item. The associated background color is `Window`. 124 | HotLight = COLOR_HOTLIGHT, 125 | 126 | /// Inactive window border. 127 | InactiveBorder = COLOR_INACTIVEBORDER, 128 | 129 | /// Inactive window caption. The associated foreground color is `InactiveCaptionText`. 130 | /// Specifies the left side color in the color gradient of an inactive window's title bar 131 | /// if the gradient effect is enabled. 132 | InactiveCaption = COLOR_INACTIVECAPTION, 133 | 134 | /// Color of text in an inactive caption. The associated background color is `InactiveCaption`. 135 | InactiveCaptionText = COLOR_INACTIVECAPTIONTEXT, 136 | 137 | /// Background color for tooltip controls. The associated foreground color is `InfoText`. 138 | InfoBackground = COLOR_INFOBK, 139 | 140 | /// Text color for tooltip controls. The associated background color is `InfoBackground`. 141 | InfoText = COLOR_INFOTEXT, 142 | 143 | /// Menu background. The associated foreground color is `MenuText`. 144 | Menu = COLOR_MENU, 145 | 146 | /// The color used to highlight menu items when the menu appears as a flat menu. 147 | /// The highlighted menu item is outlined with `Highlight`. 148 | MenuHighlight = COLOR_MENUHILIGHT, 149 | 150 | /// The background color for the menu bar when menus appear as flat menus. However, `Menu` 151 | /// continues to specify the background color of the menu popup. 152 | MenuBar = COLOR_MENUBAR, 153 | 154 | /// Text in menus. The associated background color is `Menu`. 155 | MenuText = COLOR_MENUTEXT, 156 | 157 | /// Scroll bar gray area. 158 | ScrollBar = COLOR_SCROLLBAR, 159 | 160 | /// Window background. The associated foreground colors are `WindowText` and `HotLite`. 161 | Window = COLOR_WINDOW, 162 | 163 | /// Window frame. 164 | WindowFrame = COLOR_WINDOWFRAME, 165 | 166 | /// Text in windows. The associated background color is `Window`. 167 | WindowText = COLOR_WINDOWTEXT, 168 | } 169 | 170 | pub struct Builder { 171 | style: UINT, 172 | icon: HICON, 173 | cursor: HCURSOR, 174 | background: HBRUSH, 175 | menu: LPSTR, 176 | icon_small: HICON, 177 | } 178 | 179 | pub enum Class { 180 | Owned { 181 | class_name: CString, 182 | atom: NonZeroU16, 183 | }, 184 | Static { 185 | class_name: &'static [u8], 186 | }, 187 | } 188 | 189 | static BUTTON: Class = Class::Static { 190 | class_name: b"Button\0", 191 | }; 192 | static COMBO_BOX: Class = Class::Static { 193 | class_name: b"ComboBox\0", 194 | }; 195 | static EDIT_CONTROL: Class = Class::Static { 196 | class_name: b"Edit\0", 197 | }; 198 | static LIST_BOX: Class = Class::Static { 199 | class_name: b"ListBox\0", 200 | }; 201 | static MDI_CLIENT: Class = Class::Static { 202 | class_name: b"MDIClient\0", 203 | }; 204 | static SCROLL_BAR: Class = Class::Static { 205 | class_name: b"ScrollBar\0", 206 | }; 207 | static STATIC: Class = Class::Static { 208 | class_name: b"Static\0", 209 | }; 210 | 211 | // um/CommCtrl.h 212 | static TOOLBAR: Class = Class::Static { 213 | class_name: b"ToolbarWindow32\0", 214 | }; 215 | static RE_BAR: Class = Class::Static { 216 | class_name: b"ReBarWindow32\0", 217 | }; 218 | static STATUS: Class = Class::Static { 219 | class_name: b"msctls_statusbar32\0", 220 | }; 221 | 222 | pub unsafe extern "system" fn wnd_proc_wrapper( 223 | handle: HWND, 224 | msg: UINT, 225 | wparam: WPARAM, 226 | lparam: LPARAM, 227 | ) -> LRESULT { 228 | if let Some(hwnd) = NonNull::new(handle) { 229 | let lock = crate::HWND_TO_CALLBACK.lock().unwrap(); 230 | 231 | if let Some(&callback) = lock.get(&(handle as usize)).or_else(|| lock.get(&0)) { 232 | let window = window::Window::Borrowed { hwnd }; 233 | let message = message::Message::from_raw(msg, wparam, lparam); 234 | 235 | // Callback may cause additional messages, leading to a deadlock unless dropped. 236 | drop(lock); 237 | if let Some(result) = callback(&window, message) { 238 | return result; 239 | } 240 | } 241 | } 242 | 243 | DefWindowProcA(handle, msg, wparam, lparam) 244 | } 245 | 246 | impl Builder { 247 | /// Adds a new class style. 248 | pub fn add_style(mut self, style: Style) -> Self { 249 | self.style |= style as UINT; 250 | self 251 | } 252 | 253 | /// Replaces the default class icon with a custom one. For this, the icon has to be loaded, 254 | /// which may fail. 255 | pub fn load_icon(mut self, icon: icon::Icon) -> Result { 256 | self.icon = icon.load()?.as_ptr(); 257 | Ok(self) 258 | } 259 | 260 | /// Replaces the default cursor icon with a custom one. For this, the cursor has to be loaded, 261 | /// which may fail. 262 | pub fn load_cursor(mut self, cursor: cursor::Cursor) -> Result { 263 | self.cursor = cursor.load()?.as_ptr(); 264 | Ok(self) 265 | } 266 | 267 | /// Sets the background brush to a standard system color. 268 | pub fn background(mut self, background: Background) -> Self { 269 | // "A color value must be one of the following standard system colors 270 | // (the value 1 must be added to the chosen color)"" 271 | self.background = (background as i32 + 1) as HBRUSH; 272 | self 273 | } 274 | 275 | /// Sets the menu resource constant to use. This should be the same value as the one used 276 | /// in the resource file `.rc`. 277 | pub fn menu(mut self, menu: u16) -> Self { 278 | self.menu = MAKEINTRESOURCEA(menu); 279 | self 280 | } 281 | 282 | /// Replaces the default class icon with a custom one. For this, the icon has to be loaded, 283 | /// which may fail. 284 | pub fn load_small_icon(mut self, icon: icon::Icon) -> Result { 285 | self.icon_small = icon.load_small()?.as_ptr(); 286 | Ok(self) 287 | } 288 | 289 | /// Registers a new window class. 290 | pub fn register(self, name: &str) -> Result { 291 | let class_name = CString::new(name)?; 292 | 293 | let atom = unsafe { 294 | // CreateWindowExA without a class will fail with 0x57f (ERROR_CANNOT_FIND_WND_CLASS). 295 | // https://docs.microsoft.com/en-us/windows/win32/debug/system-error-codes--1300-1699- 296 | // 297 | // For the method itself see: 298 | // https://docs.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-registerclassexa 299 | RegisterClassExA(&WNDCLASSEXA { 300 | cbSize: std::mem::size_of::() as u32, 301 | style: self.style, 302 | lpfnWndProc: Some(wnd_proc_wrapper), 303 | cbClsExtra: 0, 304 | cbWndExtra: 0, 305 | hInstance: base_instance(), 306 | hIcon: self.icon, 307 | hCursor: self.cursor, 308 | hbrBackground: self.background, 309 | lpszMenuName: self.menu, 310 | lpszClassName: class_name.as_ptr() as LPCSTR, 311 | hIconSm: self.icon_small, 312 | }) 313 | }; 314 | 315 | if let Some(atom) = NonZeroU16::new(atom) { 316 | Ok(Class::Owned { class_name, atom }) 317 | } else { 318 | Err(Error::last_os_error()) 319 | } 320 | } 321 | } 322 | 323 | impl Class { 324 | pub(crate) fn class_name_ptr(&self) -> LPCSTR { 325 | match self { 326 | Class::Owned { atom, .. } => { 327 | // The atom must be in the low-order word of lpClassName; the high-order word must be zero. 328 | atom.get() as usize as LPCSTR 329 | } 330 | Class::Static { class_name } => class_name.as_ptr() as LPCSTR, 331 | } 332 | } 333 | } 334 | 335 | impl Drop for Class { 336 | fn drop(&mut self) { 337 | match self { 338 | Class::Owned { .. } => { 339 | let result = 340 | unsafe { UnregisterClassA(self.class_name_ptr(), std::ptr::null_mut()) }; 341 | 342 | if result == 0 { 343 | panic!( 344 | "class deleted by other means or some window still alive: {}", 345 | Error::last_os_error() 346 | ) 347 | } 348 | } 349 | Class::Static { .. } => {} 350 | } 351 | } 352 | } 353 | 354 | /// Creates a builder to define a new application-defined class. 355 | pub fn build() -> Builder { 356 | Builder { 357 | style: 0, 358 | icon: ptr::null_mut(), 359 | cursor: ptr::null_mut(), 360 | background: COLOR_WINDOW as HBRUSH, 361 | menu: ptr::null_mut(), 362 | icon_small: ptr::null_mut(), 363 | } 364 | } 365 | 366 | /// The system class for a button. 367 | pub fn button() -> &'static Class { 368 | &BUTTON 369 | } 370 | 371 | /// The system class for a combo box. 372 | pub fn combo_box() -> &'static Class { 373 | &COMBO_BOX 374 | } 375 | 376 | /// The system class for an edit control. 377 | pub fn edit_control() -> &'static Class { 378 | &EDIT_CONTROL 379 | } 380 | 381 | /// The system class for a list box. 382 | pub fn list_box() -> &'static Class { 383 | &LIST_BOX 384 | } 385 | 386 | /// The system class for an MDI client window. 387 | pub fn mdi_client() -> &'static Class { 388 | &MDI_CLIENT 389 | } 390 | 391 | /// The system class for a scroll bar. 392 | pub fn scroll_bar() -> &'static Class { 393 | &SCROLL_BAR 394 | } 395 | 396 | /// The system class for a static control. 397 | pub fn static_control() -> &'static Class { 398 | &STATIC 399 | } 400 | 401 | /// The common control class for a tool bar. 402 | pub fn toolbar() -> &'static Class { 403 | &TOOLBAR 404 | } 405 | 406 | /// The system class for a "re-bar". 407 | pub fn re_bar() -> &'static Class { 408 | &RE_BAR 409 | } 410 | 411 | /// The system class for a status bar. 412 | pub fn status_bar() -> &'static Class { 413 | &STATUS 414 | } 415 | -------------------------------------------------------------------------------- /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/dialog.rs: -------------------------------------------------------------------------------- 1 | use crate::{base_instance, window, DialogCallback, Error, Result}; 2 | use std::ffi::CString; 3 | use std::ptr; 4 | use winapi::shared::minwindef::MAX_PATH; 5 | use winapi::um::commdlg::{ 6 | GetOpenFileNameA, GetSaveFileNameA, LPOPENFILENAMEA, OFN_ALLOWMULTISELECT, OFN_CREATEPROMPT, 7 | OFN_DONTADDTORECENT, OFN_ENABLEHOOK, OFN_ENABLEINCLUDENOTIFY, OFN_ENABLESIZING, 8 | OFN_ENABLETEMPLATE, OFN_ENABLETEMPLATEHANDLE, OFN_EXPLORER, OFN_EXTENSIONDIFFERENT, 9 | OFN_FILEMUSTEXIST, OFN_FORCESHOWHIDDEN, OFN_HIDEREADONLY, OFN_LONGNAMES, OFN_NOCHANGEDIR, 10 | OFN_NODEREFERENCELINKS, OFN_NOLONGNAMES, OFN_NONETWORKBUTTON, OFN_NOREADONLYRETURN, 11 | OFN_NOTESTFILECREATE, OFN_NOVALIDATE, OFN_OVERWRITEPROMPT, OFN_PATHMUSTEXIST, OFN_READONLY, 12 | OFN_SHAREAWARE, OFN_SHOWHELP, OPENFILENAMEA, 13 | }; 14 | use winapi::um::winnt::{LPCSTR, LPSTR}; 15 | use winapi::um::winuser::{DialogBoxParamA, MAKEINTRESOURCEA}; 16 | 17 | /// Creates a modal dialog box from a dialog box template resource. The function does not 18 | /// return control until the specified callback function terminates the modal dialog box 19 | /// by calling the `Window::end_dialog` function. 20 | pub fn show(resource: u16, callback: DialogCallback) -> Result { 21 | let hinstance = base_instance(); 22 | let resource = MAKEINTRESOURCEA(resource); 23 | 24 | // Can't know what the dialog's handle is beforehand. The special value 0 will be 25 | // replaced with the right value as soon as the init dialog message arrives. 26 | crate::HWND_TO_DLG_CALLBACK 27 | .lock() 28 | .unwrap() 29 | .insert(0, callback); 30 | 31 | let result = unsafe { 32 | DialogBoxParamA( 33 | hinstance, 34 | resource, 35 | ptr::null_mut(), 36 | Some(window::dlg_proc_wrapper), 37 | 0, 38 | ) 39 | }; 40 | 41 | // In the code at http://winprog.org/tutorial/dlgfaq.html, DialogBox returns 0 as well, 42 | // which according to the official documentation "If the function fails because the 43 | // hWndParent parameter is invalid, the return value is zero. The function returns zero 44 | // in this case for compatibility with previous versions of Windows.". 45 | // https://docs.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-dialogboxparama 46 | // 47 | // It seems safe to ignore 0 as there being an error. 48 | match result { 49 | -1 => Err(Error::last_os_error()), 50 | n => Ok(n), 51 | } 52 | } 53 | 54 | #[repr(u32)] 55 | pub enum OpenFileConfig { 56 | /// The File Name list box allows multiple selections. If you also set the `Explorer` flag, 57 | /// the dialog box uses the Explorer-style user interface; otherwise, it uses the old-style 58 | /// user interface. 59 | AllowMultiSelect = OFN_ALLOWMULTISELECT, 60 | 61 | /// If the user specifies a file that does not exist, this flag causes the dialog box to 62 | /// prompt the user for permission to create the file. If the user chooses to create the 63 | /// file, the dialog box closes and the function returns the specified name; otherwise, 64 | /// the dialog box remains open. If you use this flag with the `AllowMultiSelect` flag, the 65 | /// dialog box allows the user to specify only one nonexistent file. 66 | CreatePrompt = OFN_CREATEPROMPT, 67 | 68 | /// Prevents the system from adding a link to the selected file in the file system directory 69 | /// that contains the user's most recently used documents. 70 | DontAddToRecent = OFN_DONTADDTORECENT, 71 | 72 | /// Enables the hook function specified in the hook member. 73 | EnableHook = OFN_ENABLEHOOK, 74 | 75 | /// Causes the dialog box to send `IncludeItem` notification messages to your hook procedure 76 | /// when the user opens a folder. The dialog box sends a notification for each item in the 77 | /// newly opened folder. These messages enable you to control which items the dialog box 78 | /// displays in the folder's item list. 79 | EnableIncludeNotify = OFN_ENABLEINCLUDENOTIFY, 80 | 81 | /// Enables the Explorer-style dialog box to be resized using either the mouse or the 82 | /// keyboard. By default, the Explorer-style Open and Save As dialog boxes allow the dialog 83 | /// box to be resized regardless of whether this flag is set. This flag is necessary only if 84 | /// you provide a hook procedure or custom template. The old-style dialog box does not permit 85 | /// resizing. 86 | EnableSizing = OFN_ENABLESIZING, 87 | 88 | /// The template member is a pointer to the name of a dialog template resource in the module 89 | /// identified by the instance member. If the `Explorer` flag is set, the system uses the 90 | /// specified template to create a dialog box that is a child of the default Explorer-style 91 | /// dialog box. If the `Explorer` flag is not set, the system uses the template to create 92 | /// an old-style dialog box that replaces the default dialog box. 93 | EnableTemplate = OFN_ENABLETEMPLATE, 94 | 95 | /// The instance member identifies a data block that contains a preloaded dialog box template. 96 | /// The system ignores template name if this flag is specified. If the `Explorer` flag is set, 97 | /// the system uses the specified template to create a dialog box that is a child of the 98 | /// default Explorer-style dialog box. If the `Explorer` flag is not set, the system uses the 99 | /// template to create an old-style dialog box that replaces the default dialog box. 100 | EnableTemplateHandle = OFN_ENABLETEMPLATEHANDLE, 101 | 102 | /// Indicates that any customizations made to the Open or Save As dialog box use the 103 | /// Explorer-style customization methods. For more information, see Explorer-Style Hook 104 | /// Procedures and Explorer-Style Custom Templates. By default, the Open and Save As dialog 105 | /// boxes use the Explorer-style user interface regardless of whether this flag is set. This 106 | /// flag is necessary only if you provide a hook procedure or custom template, or set the 107 | /// `AllowMultiSelect` flag. If you want the old-style user interface, omit the `Explorer` 108 | /// flag and provide a replacement old-style template or hook procedure. If you want the old 109 | /// style but do not need a custom template or hook procedure, simply provide a hook procedure 110 | /// that always returns `false`. 111 | Explorer = OFN_EXPLORER, 112 | 113 | /// The user typed a file name extension that differs from the extension specified by 114 | /// `set_default_ext`. The function does not use this flag if no default extension was set. 115 | ExtensionDifferent = OFN_EXTENSIONDIFFERENT, 116 | 117 | /// The user can type only names of existing files in the File Name entry field. If this 118 | /// flag is specified and the user enters an invalid name, the dialog box procedure displays 119 | /// a warning in a message box. If this flag is specified, the `PathMustExist` flag is also 120 | /// used. This flag can be used in an Open dialog box. It cannot be used with a Save As dialog 121 | /// box. 122 | FileMustExist = OFN_FILEMUSTEXIST, 123 | 124 | /// Forces the showing of system and hidden files, thus overriding the user setting to show or 125 | /// not show hidden files. However, a file that is marked both system and hidden is not shown. 126 | ForceShowHidden = OFN_FORCESHOWHIDDEN, 127 | 128 | /// Hides the Read Only check box. 129 | HideReadonly = OFN_HIDEREADONLY, 130 | 131 | /// For old-style dialog boxes, this flag causes the dialog box to use long file names. If 132 | /// this flag is not specified, or if the `AllowMultiSelect` flag is also set, old-style 133 | /// dialog boxes use short file names (8.3 format) for file names with spaces. 134 | /// Explorer-style dialog boxes ignore this flag and always display long file names. 135 | LongNames = OFN_LONGNAMES, 136 | 137 | /// Restores the current directory to its original value if the user changed the directory 138 | /// while searching for files. This flag is ineffective for GetOpenFileName. 139 | NoChangeDir = OFN_NOCHANGEDIR, 140 | 141 | /// Directs the dialog box to return the path and file name of the selected shortcut (.LNK) 142 | /// file. If this value is not specified, the dialog box returns the path and file name of the 143 | /// file referenced by the shortcut. 144 | NoDereferenceLinks = OFN_NODEREFERENCELINKS, 145 | 146 | /// For old-style dialog boxes, this flag causes the dialog box to use short file names (8.3 147 | /// format). Explorer-style dialog boxes ignore this flag and always display long file names. 148 | NoLongNames = OFN_NOLONGNAMES, 149 | 150 | /// Hides and disables the Network button. 151 | NoNetworkButton = OFN_NONETWORKBUTTON, 152 | 153 | /// The returned file does not have the Read Only check box selected and is not in a 154 | /// write-protected directory. 155 | NoReadonlyReturn = OFN_NOREADONLYRETURN, 156 | 157 | /// The file is not created before the dialog box is closed. This flag should be specified if 158 | /// the application saves the file on a create-nonmodify network share. When an application 159 | /// specifies this flag, the library does not check for write protection, a full disk, an 160 | /// open drive door, or network protection. Applications using this flag must perform file 161 | /// operations carefully, because a file cannot be reopened once it is closed. 162 | NoTestFileCreate = OFN_NOTESTFILECREATE, 163 | 164 | /// The common dialog boxes allow invalid characters in the returned file name. Typically, 165 | /// the calling application uses a hook procedure that checks the file name by using the 166 | /// `FileOkString` message. If the text box in the edit control is empty or contains nothing 167 | /// but spaces, the lists of files and directories are updated. If the text box in the edit 168 | /// control contains anything else, nFileOffset and nFileExtension are set to values 169 | /// generated by parsing the text. No default extension is added to the text, nor is text 170 | /// copied to the buffer specified by file title. If the value specified by file offset is 171 | /// less than zero, the file name is invalid. Otherwise, the file name is valid, and 172 | /// file extension and file offset can be used as if the `NoValidate` flag had not been 173 | /// specified. 174 | NoValidate = OFN_NOVALIDATE, 175 | 176 | /// Causes the Save As dialog box to generate a message box if the selected file already 177 | /// exists. The user must confirm whether to overwrite the file. 178 | OverwritePrompt = OFN_OVERWRITEPROMPT, 179 | 180 | /// The user can type only valid paths and file names. If this flag is used and the user types 181 | /// an invalid path and file name in the File Name entry field, the dialog box function 182 | /// displays a warning in a message box. 183 | PathMustExist = OFN_PATHMUSTEXIST, 184 | 185 | /// Causes the Read Only check box to be selected initially when the dialog box is created. 186 | /// This flag indicates the state of the Read Only check box when the dialog box is closed. 187 | Readonly = OFN_READONLY, 188 | 189 | /// Specifies that if a call to the OpenFile function fails because of a network sharing 190 | /// violation, the error is ignored and the dialog box returns the selected file name. If 191 | /// this flag is not set, the dialog box notifies your hook procedure when a network sharing 192 | /// violation occurs for the file name specified by the user. If you set the `Explorer` flag, 193 | /// the dialog box sends the CDN_SHAREVIOLATION message to the hook procedure. If you do not 194 | /// set `Explorer`, the dialog box sends the `ShareViString` registered message to the hook 195 | /// procedure. 196 | Shareware = OFN_SHAREAWARE, 197 | 198 | /// Causes the dialog box to display the Help button. The hwndOwner member must specify the 199 | /// window to receive the `HelpMsgString` registered messages that the dialog box sends when 200 | /// the user clicks the Help button. An Explorer-style dialog box sends a `Help` notification 201 | /// message to your hook procedure when the user clicks the Help button. 202 | ShowHelp = OFN_SHOWHELP, 203 | } 204 | 205 | pub struct OpenFileBuilder<'a> { 206 | owner: &'a window::Window<'a>, 207 | filter: Vec, 208 | file: Vec, 209 | flags: u32, 210 | default_ext: Option, 211 | } 212 | 213 | impl<'a> OpenFileBuilder<'a> { 214 | pub(crate) fn new(owner: &'a window::Window<'a>) -> Self { 215 | Self { 216 | owner, 217 | filter: vec![], 218 | file: vec![0; MAX_PATH], 219 | flags: 0, 220 | default_ext: None, 221 | } 222 | } 223 | 224 | /// The first string in each pair is a display string that describes the filter (for example, 225 | /// "Text Files"), and the second string specifies the filter pattern (for example, ".TXT"). 226 | /// 227 | /// To specify multiple filter patterns for a single display string, use a semicolon to 228 | /// separate the patterns (for example, ".TXT;.DOC;.BAK"). A pattern string can be a 229 | /// combination of valid file name characters and the asterisk (*) wildcard character. 230 | /// 231 | /// Do not include spaces in the pattern string. 232 | pub fn set_filters(mut self, filters: &[(&str, &str)]) -> Self { 233 | self.filter.clear(); 234 | for &(display, pattern) in filters.into_iter() { 235 | self.filter.extend(display.as_bytes().iter().copied()); 236 | self.filter.push(0u8); 237 | self.filter.extend(pattern.as_bytes().iter().copied()); 238 | self.filter.push(0u8); 239 | } 240 | self.filter.push(0u8); 241 | self 242 | } 243 | 244 | /// The default extension. `ask_open_path` and `ask_save_path` append this extension to the 245 | /// file name if the user fails to type an extension. This string can be any length, but only 246 | /// the first three characters are appended. The string should not contain a period (.). If 247 | /// this member is NULL and the user fails to type an extension, no extension is appended. 248 | pub fn set_default_ext(mut self, extension: &str) -> Self { 249 | self.default_ext = Some(CString::new(extension).unwrap()); 250 | self 251 | } 252 | 253 | /// Adds additional configuration options. 254 | pub fn add_config(mut self, config: OpenFileConfig) -> Self { 255 | self.flags |= config as u32; 256 | self 257 | } 258 | 259 | fn structure(&mut self) -> OPENFILENAMEA { 260 | OPENFILENAMEA { 261 | lStructSize: std::mem::size_of::() as u32, 262 | hwndOwner: self.owner.hwnd_ptr(), 263 | hInstance: ptr::null_mut(), 264 | lpstrFilter: self.filter.as_ptr() as LPCSTR, 265 | lpstrCustomFilter: ptr::null_mut(), 266 | nMaxCustFilter: 0, 267 | nFilterIndex: 0, 268 | lpstrFile: self.file.as_mut_ptr() as LPSTR, 269 | nMaxFile: MAX_PATH as u32, 270 | lpstrFileTitle: ptr::null_mut(), 271 | nMaxFileTitle: 0, 272 | lpstrInitialDir: ptr::null(), 273 | lpstrTitle: ptr::null(), 274 | Flags: self.flags, 275 | nFileOffset: 0, 276 | nFileExtension: 0, 277 | lpstrDefExt: self 278 | .default_ext 279 | .as_ref() 280 | .map(|e| e.as_ptr()) 281 | .unwrap_or_else(ptr::null), 282 | lCustData: 0, 283 | lpfnHook: None, 284 | lpTemplateName: ptr::null(), 285 | pvReserved: ptr::null_mut(), 286 | dwReserved: 0, 287 | FlagsEx: 0, 288 | } 289 | } 290 | 291 | /// Creates an Open dialog box that lets the user specify the drive, directory, and the name 292 | /// of a file or set of files to be opened. 293 | pub fn ask_open_path(mut self) -> Option { 294 | // https://docs.microsoft.com/en-us/windows/win32/api/commdlg/nf-commdlg-getopenfilenamea 295 | let mut buffer = self.structure(); 296 | let result = unsafe { GetOpenFileNameA(&mut buffer as LPOPENFILENAMEA) }; 297 | if result != 0 { 298 | self.file 299 | .truncate(self.file.iter().position(|&c| c == 0).unwrap_or(0)); 300 | Some(CString::new(self.file).unwrap().into_string().unwrap()) 301 | } else { 302 | None 303 | } 304 | } 305 | 306 | /// Creates a Save dialog box that lets the user specify the drive, directory, and name of a 307 | /// file to save. 308 | pub fn ask_save_path(mut self) -> Option { 309 | // https://docs.microsoft.com/en-us/windows/win32/api/commdlg/nf-commdlg-getsavefilenamea 310 | let mut buffer = self.structure(); 311 | let result = unsafe { GetSaveFileNameA(&mut buffer as LPOPENFILENAMEA) }; 312 | if result != 0 { 313 | self.file 314 | .truncate(self.file.iter().position(|&c| c == 0).unwrap_or(0)); 315 | Some(CString::new(self.file).unwrap().into_string().unwrap()) 316 | } else { 317 | None 318 | } 319 | } 320 | } 321 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /src/gdi/canvas.rs: -------------------------------------------------------------------------------- 1 | //! A [`Canvas`] is a device-context. A device-context can either represent something on-screen, 2 | //! such as a window, or be an in-memory buffer (something useful to avoid flickering). 3 | //! 4 | //! A device-context always has an object selected onto it (by default, "none", which in reality 5 | //! is a 1x1 compatible bitmap). An object can only be used if it's currently selected. This means 6 | //! that it is necessary to create multiple device-context if one wishes to work with multiple 7 | //! objects at once. To do this, [`Canvas::try_clone`] can be used. 8 | //! 9 | //! The objects that can be used to paint on a canvas all implement the [`Canvas`] trait. 10 | use super::{brush, Bitmap, Paint}; 11 | use crate::{rect, window}; 12 | 13 | use std::fmt; 14 | use std::marker::PhantomData; 15 | use std::mem; 16 | use std::ptr::{self, NonNull}; 17 | use winapi::shared::windef::{HDC__, HGDIOBJ, LPRECT}; 18 | use winapi::um::wingdi::{ 19 | BitBlt, CreateCompatibleBitmap, CreateCompatibleDC, DeleteDC, GetDeviceCaps, SelectObject, 20 | SetBkColor, CLR_INVALID, HGDI_ERROR, HORZRES, RGB, SRCAND, SRCCOPY, SRCINVERT, SRCPAINT, 21 | VERTRES, 22 | }; 23 | use winapi::um::winuser::{BeginPaint, EndPaint, FillRect, GetDC, ReleaseDC, PAINTSTRUCT}; 24 | 25 | #[derive(Debug)] 26 | pub struct Canvas<'w, 'p> 27 | where 28 | 'w: 'p, 29 | { 30 | hdc: NonNull, 31 | mode: Mode<'w>, 32 | selection: Selection<'p>, 33 | } 34 | 35 | enum Mode<'w> { 36 | /// The "default" paint mode, used upon receiving a paint message. 37 | /// 38 | /// Obtained via `BeginPaint`. It must be dropped with `EndPaint`. 39 | Paint { 40 | window: &'w window::Window<'w>, 41 | info: PAINTSTRUCT, 42 | }, 43 | /// Obtained via `GetDC`. It must be dropped with `ReleaseDC`. 44 | BorrowedDc { window: &'w window::Window<'w> }, 45 | /// Obtained via `CreateCompatibleDC`. It must be dropped with `DeleteDC`. 46 | OwnedDc, 47 | /// The data has been moved out somewhere else, so the drop does not need to clean it up. 48 | Moved, 49 | } 50 | 51 | #[derive(Debug)] 52 | enum Selection<'p> { 53 | Default, 54 | Custom { 55 | lifetime: PhantomData<&'p ()>, 56 | dc_object: HGDIOBJ, 57 | }, 58 | } 59 | 60 | pub struct Bitwise<'c, 'w, 'p> { 61 | canvas: &'c Canvas<'w, 'p>, 62 | rect: rect::Rect, 63 | src_x: i32, 64 | src_y: i32, 65 | } 66 | 67 | impl<'w, 'p> Canvas<'w, 'p> 68 | where 69 | 'w: 'p, 70 | { 71 | // Canvas creation (both borrowed and owned). 72 | 73 | /// Attempt to create a new, owned version of a canvas compatible with the current screen. 74 | pub fn from_current_screen() -> Result { 75 | let result = unsafe { CreateCompatibleDC(ptr::null_mut()) }; 76 | NonNull::new(result).ok_or(()).map(|hdc| Self { 77 | hdc, 78 | mode: Mode::OwnedDc, 79 | selection: Selection::Default, 80 | }) 81 | } 82 | 83 | /// Attempt to create a new canvas in order to directly paint on the specified window. 84 | pub fn from_window(window: &'w window::Window) -> Result { 85 | let mut info = unsafe { mem::zeroed() }; 86 | let result = unsafe { BeginPaint(window.hwnd_ptr(), &mut info) }; 87 | NonNull::new(result).ok_or(()).map(|hdc| Canvas { 88 | hdc, 89 | mode: Mode::Paint { window, info }, 90 | selection: Selection::Default, 91 | }) 92 | } 93 | 94 | /// Attempt to create a new canvas to act as a buffer with the same settings as the window. 95 | pub fn from_window_settings(window: &'w window::Window) -> Result { 96 | let result = unsafe { GetDC(window.hwnd_ptr()) }; 97 | NonNull::new(result).ok_or(()).map(|hdc| Canvas { 98 | hdc, 99 | mode: Mode::BorrowedDc { window }, 100 | selection: Selection::Default, 101 | }) 102 | } 103 | 104 | /// Attempt to create a new, owned version of this canvas. 105 | pub fn try_clone(&self) -> Result { 106 | let result = unsafe { CreateCompatibleDC(self.hdc.as_ptr()) }; 107 | NonNull::new(result).ok_or(()).map(|hdc| Self { 108 | hdc, 109 | mode: Mode::OwnedDc, 110 | selection: Selection::Default, 111 | }) 112 | } 113 | 114 | /// Create a new bitmap compatible with this device-context. 115 | pub fn create_bitmap(&self, width: i32, height: i32) -> Result { 116 | let result = unsafe { CreateCompatibleBitmap(self.hdc.as_ptr(), width, height) }; 117 | NonNull::new(result) 118 | .map(|bitmap| Bitmap { bitmap }) 119 | .ok_or(()) 120 | } 121 | 122 | // Device capabilities. 123 | // 124 | // See also https://docs.microsoft.com/en-us/windows/win32/api/wingdi/nf-wingdi-getdevicecaps. 125 | 126 | /// Width, in pixels, of the device-context. 127 | pub fn width(&self) -> i32 { 128 | unsafe { GetDeviceCaps(self.hdc.as_ptr(), HORZRES) } 129 | } 130 | 131 | /// Height, in raster lines, of the device-context. 132 | pub fn height(&self) -> i32 { 133 | unsafe { GetDeviceCaps(self.hdc.as_ptr(), VERTRES) } 134 | } 135 | 136 | /// Miscellaneous painting operations. 137 | 138 | pub fn fill_rect(&self, rect: rect::Rect, brush: brush::Brush) -> Result<(), ()> { 139 | let mut rect = rect.0; 140 | let result = unsafe { FillRect(self.hdc.as_ptr(), &mut rect as LPRECT, brush.as_ptr()) }; 141 | if result != 0 { 142 | Ok(()) 143 | } else { 144 | Err(()) 145 | } 146 | } 147 | 148 | /// Set the background color of the device-context. 149 | pub fn set_background(&self, (r, g, b): (u8, u8, u8)) -> Result<(), ()> { 150 | let result = unsafe { SetBkColor(self.hdc.as_ptr(), RGB(r, g, b)) }; 151 | if result == CLR_INVALID { 152 | Err(()) 153 | } else { 154 | Ok(()) 155 | } 156 | } 157 | 158 | /// Bind a different object to the canvas. 159 | /// 160 | /// Objects must be bound to the canvas before operations can be performed with them. 161 | /// If you need to operate with multiple objects at once, you may need a new buffer, 162 | /// which can be obtained through [`Self::try_clone`]. 163 | /// 164 | /// There can only be one object bound at a time, hence why this method moves the canvas. 165 | /// 166 | /// The default object may be "special", for example, painting to it may render on screen. 167 | pub fn bind<'q, P>(mut self, object: &'q P) -> Result, Canvas<'w, 'p>> 168 | where 169 | P: Paint, 170 | 'w: 'q, 171 | { 172 | let result = unsafe { SelectObject(self.hdc.as_ptr(), object.as_gdi_obj()) }; 173 | if result.is_null() || result == HGDI_ERROR { 174 | Err(self) 175 | } else { 176 | // Make sure to set "moved" data on self so that it's the drop does not affect our new canvas. 177 | let hdc = self.hdc; 178 | let mode = mem::replace(&mut self.mode, Mode::Moved); 179 | let selection = mem::replace(&mut self.selection, Selection::Default); 180 | 181 | // It's a new canvas, as the lifetimes are different (we can't repurpose self to avoid moving). 182 | Ok(Canvas::<'w, 'q> { 183 | hdc: hdc, 184 | mode: mode, 185 | selection: Selection::Custom { 186 | lifetime: PhantomData::<&'q ()>, 187 | dc_object: match selection { 188 | Selection::Default => result, 189 | // When rebinding, we must still keep the original DC object. We can safely 190 | // ignore the one SelectObject returned as it's the previous selection which 191 | // the caller already has access to. 192 | Selection::Custom { dc_object, .. } => dc_object, 193 | }, 194 | }, 195 | }) 196 | } 197 | } 198 | 199 | /// Bind back the default object. 200 | /// 201 | /// Does nothing if the default object was already bound. 202 | pub fn bind_default(mut self) -> Result, Canvas<'w, 'p>> { 203 | match self.selection { 204 | Selection::Default => {} 205 | Selection::Custom { dc_object, .. } => { 206 | let result = unsafe { SelectObject(self.hdc.as_ptr(), dc_object) }; 207 | if result.is_null() || result == HGDI_ERROR { 208 | return Err(self); 209 | } 210 | // self will be dropped and we don't want it to re-select custom back. 211 | self.selection = Selection::Default; 212 | } 213 | } 214 | 215 | Ok(Canvas::<'w, 'static> { 216 | hdc: self.hdc, 217 | // self will be dropped so make sure the DC we now use isn't deleted. 218 | mode: mem::replace(&mut self.mode, Mode::Moved), 219 | selection: Selection::<'static>::Default, 220 | }) 221 | } 222 | 223 | // Bit-block transfer operations. 224 | // 225 | // See also https://docs.microsoft.com/en-us/windows/win32/api/wingdi/nf-wingdi-bitblt. 226 | 227 | /// Begin a bitwise operation, which can be further configured before executing. 228 | pub fn bitwise<'c>(&'c self) -> Bitwise<'c, 'w, 'p> { 229 | Bitwise { 230 | canvas: self, 231 | rect: rect::Rect::new(self.width(), self.height()), 232 | src_x: 0, 233 | src_y: 0, 234 | } 235 | } 236 | } 237 | 238 | impl<'c, 'w, 'p> Bitwise<'c, 'w, 'p> { 239 | /// Set the rectangular region where the bitwise operation will be applied. 240 | pub fn region(mut self, rect: rect::Rect) -> Self { 241 | self.rect = rect; 242 | self 243 | } 244 | 245 | /// Offset the source of the operation by this amount before applying it. 246 | pub fn offset_source(mut self, x: i32, y: i32) -> Self { 247 | self.src_x = x; 248 | self.src_y = y; 249 | self 250 | } 251 | 252 | /// Apply the bitwise AND operation of the given source canvas into self (the destination). 253 | pub fn and(self, source: &Canvas) -> Result<(), ()> { 254 | self.transfer(source, SRCAND) 255 | } 256 | 257 | /// Apply the bitwise OR operation of the given source canvas into self (the destination). 258 | pub fn or(self, source: &Canvas) -> Result<(), ()> { 259 | self.transfer(source, SRCPAINT) 260 | } 261 | 262 | /// Apply the bitwise XOR operation of the given source canvas into self (the destination). 263 | pub fn xor(self, source: &Canvas) -> Result<(), ()> { 264 | self.transfer(source, SRCINVERT) 265 | } 266 | 267 | /// Apply the bitwise SET operation of the given source canvas into self (the destination). 268 | pub fn set(self, source: &Canvas) -> Result<(), ()> { 269 | self.transfer(source, SRCCOPY) 270 | } 271 | 272 | fn transfer(self, source: &Canvas, raster_op: u32) -> Result<(), ()> { 273 | let result = unsafe { 274 | BitBlt( 275 | self.canvas.hdc.as_ptr(), 276 | self.rect.x(), 277 | self.rect.y(), 278 | self.rect.width(), 279 | self.rect.height(), 280 | source.hdc.as_ptr(), 281 | self.src_x, 282 | self.src_y, 283 | raster_op, 284 | ) 285 | }; 286 | 287 | if result != 0 { 288 | Ok(()) 289 | } else { 290 | Err(()) 291 | } 292 | } 293 | } 294 | 295 | impl Drop for Canvas<'_, '_> { 296 | fn drop(&mut self) { 297 | match self.selection { 298 | Selection::Default => {} 299 | Selection::Custom { dc_object, .. } => { 300 | let result = unsafe { SelectObject(self.hdc.as_ptr(), dc_object) }; 301 | if result.is_null() || result == HGDI_ERROR { 302 | panic!("failed to return selected object"); 303 | } 304 | } 305 | } 306 | 307 | let result = match &mut self.mode { 308 | Mode::Paint { window, info } => unsafe { EndPaint(window.hwnd_ptr(), info) }, 309 | Mode::BorrowedDc { window } => unsafe { 310 | ReleaseDC(window.hwnd_ptr(), self.hdc.as_ptr()) 311 | }, 312 | Mode::OwnedDc => unsafe { DeleteDC(self.hdc.as_ptr()) }, 313 | Mode::Moved => 1, 314 | }; 315 | 316 | if result == 0 { 317 | panic!( 318 | "failed to drop {} canvas", 319 | match self.mode { 320 | Mode::Paint { .. } => "painting", 321 | Mode::BorrowedDc { .. } => "borrowed", 322 | Mode::OwnedDc => "owned", 323 | Mode::Moved => "moved", 324 | } 325 | ) 326 | } 327 | } 328 | } 329 | 330 | impl fmt::Debug for Mode<'_> { 331 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 332 | match self { 333 | Self::Paint { window, .. } => f 334 | .debug_struct("Paint") 335 | .field("window", window) 336 | .finish_non_exhaustive(), 337 | Self::BorrowedDc { window } => f 338 | .debug_struct("BorrowedDc") 339 | .field("window", window) 340 | .finish(), 341 | Self::OwnedDc => f.debug_struct("OwnedDc").finish(), 342 | Self::Moved => f.debug_struct("Moved").finish(), 343 | } 344 | } 345 | } 346 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /src/message.rs: -------------------------------------------------------------------------------- 1 | use crate::{messagebox, window}; 2 | use std::ptr::NonNull; 3 | use winapi::shared::minwindef::{HIWORD, LOWORD, LPARAM, UINT, WPARAM}; 4 | use winapi::shared::windef::{HDC, HWND}; 5 | use winapi::um::wingdi::{ 6 | GetBValue, GetGValue, GetRValue, SetBkMode, SetTextColor, CLR_INVALID, OPAQUE, RGB, TRANSPARENT, 7 | }; 8 | use winapi::um::winuser::{ 9 | LBN_SELCHANGE, MK_CONTROL, MK_LBUTTON, MK_MBUTTON, MK_RBUTTON, MK_SHIFT, MK_XBUTTON1, 10 | MK_XBUTTON2, SIZE_MAXHIDE, SIZE_MAXIMIZED, SIZE_MAXSHOW, SIZE_MINIMIZED, SIZE_RESTORED, 11 | WM_CLOSE, WM_COMMAND, WM_CREATE, WM_CTLCOLORDLG, WM_CTLCOLORSTATIC, WM_DESTROY, WM_INITDIALOG, 12 | WM_LBUTTONDOWN, WM_LBUTTONUP, WM_MBUTTONDOWN, WM_MBUTTONUP, WM_PAINT, WM_RBUTTONDOWN, 13 | WM_RBUTTONUP, WM_SIZE, WM_TIMER, 14 | }; 15 | 16 | #[derive(Debug)] 17 | pub struct SizeData { 18 | wparam: WPARAM, 19 | lparam: LPARAM, 20 | } 21 | 22 | #[derive(Debug)] 23 | pub struct TimerData { 24 | wparam: WPARAM, 25 | lparam: LPARAM, 26 | } 27 | 28 | #[derive(Debug)] 29 | pub struct MouseData { 30 | wparam: WPARAM, 31 | lparam: LPARAM, 32 | } 33 | 34 | #[derive(Debug)] 35 | pub struct CommandData { 36 | wparam: WPARAM, 37 | lparam: LPARAM, 38 | } 39 | 40 | pub struct ControlData<'a> { 41 | /// Control-defined notification code 42 | pub code: u16, 43 | /// Control identifier 44 | pub id: u16, 45 | /// Handle to the control window 46 | pub window: window::Window<'a>, 47 | } 48 | 49 | #[derive(Debug)] 50 | pub struct ColorData { 51 | wparam: WPARAM, 52 | lparam: LPARAM, 53 | } 54 | 55 | #[derive(Debug)] 56 | pub enum Message { 57 | Create, 58 | Size(SizeData), 59 | Destroy, 60 | Close, 61 | InitDialog, 62 | Paint, 63 | Timer(TimerData), 64 | LeftMouseButtonDown(MouseData), 65 | RightMouseButtonDown(MouseData), 66 | MiddleMouseButtonDown(MouseData), 67 | LeftMouseButtonUp(MouseData), 68 | RightMouseButtonUp(MouseData), 69 | MiddleMouseButtonUp(MouseData), 70 | Command(CommandData), 71 | ControlColorDialog(ColorData), 72 | ControlColorStatic(ColorData), 73 | Other { 74 | msg: UINT, 75 | wparam: WPARAM, 76 | lparam: LPARAM, 77 | }, 78 | } 79 | 80 | #[derive(Debug)] 81 | pub enum ListBoxMessage { 82 | SelectionChange, 83 | Other { code: u16 }, 84 | } 85 | 86 | // https://docs.microsoft.com/en-us/windows/win32/winmsg/wm-size 87 | impl SizeData { 88 | /// `true` if the message was sent to all pop-up windows when some other window is maximized. 89 | pub fn maximize_hide(&self) -> bool { 90 | self.wparam == SIZE_MAXHIDE 91 | } 92 | 93 | /// `true` if the window has been maximized. 94 | pub fn maximized(&self) -> bool { 95 | self.wparam == SIZE_MAXIMIZED 96 | } 97 | 98 | /// `true` if the message was sent to all pop-up windows when some other window has been 99 | /// restored to its former size. 100 | pub fn maximize_show(&self) -> bool { 101 | self.wparam == SIZE_MAXSHOW 102 | } 103 | 104 | /// `true` if the window has been minimized. 105 | pub fn minimized(&self) -> bool { 106 | self.wparam == SIZE_MINIMIZED 107 | } 108 | 109 | /// `true` if the window has been resized, but neither the `minimized` nor `maximized` value applies. 110 | pub fn restored(&self) -> bool { 111 | self.wparam == SIZE_RESTORED 112 | } 113 | 114 | /// The new width of the client area. 115 | pub fn width(&self) -> u16 { 116 | LOWORD(self.lparam as u32) 117 | } 118 | 119 | /// The new height of the client area. 120 | pub fn height(&self) -> u16 { 121 | HIWORD(self.lparam as u32) 122 | } 123 | } 124 | 125 | // https://docs.microsoft.com/en-us/windows/win32/winmsg/wm-timer 126 | impl TimerData { 127 | pub fn timer_id(&self) -> usize { 128 | self.wparam 129 | } 130 | } 131 | 132 | // https://docs.microsoft.com/en-us/windows/win32/inputdev/wm-lbuttondown 133 | impl MouseData { 134 | /// The x-coordinate of the cursor. The coordinate is relative to the upper-left corner of the client area. 135 | pub fn x(&self) -> u16 { 136 | LOWORD(self.lparam as u32) 137 | } 138 | 139 | /// The y-coordinate of the cursor. The coordinate is relative to the upper-left corner of the client area. 140 | pub fn y(&self) -> u16 { 141 | HIWORD(self.lparam as u32) 142 | } 143 | 144 | /// Whether the CTRL key is down. 145 | pub fn control(&self) -> bool { 146 | (self.wparam & MK_CONTROL) != 0 147 | } 148 | 149 | /// Whether the left mouse button is down. 150 | pub fn lmb(&self) -> bool { 151 | (self.wparam & MK_LBUTTON) != 0 152 | } 153 | 154 | /// Whether the middle mouse button is down. 155 | pub fn mmb(&self) -> bool { 156 | (self.wparam & MK_MBUTTON) != 0 157 | } 158 | 159 | /// Whether the right mouse button is down. 160 | pub fn rmb(&self) -> bool { 161 | (self.wparam & MK_RBUTTON) != 0 162 | } 163 | 164 | /// Whether the SHIFT key is down. 165 | pub fn shift(&self) -> bool { 166 | (self.wparam & MK_SHIFT) != 0 167 | } 168 | 169 | /// Whether the first X button is down. 170 | pub fn xbutton1(&self) -> bool { 171 | (self.wparam & MK_XBUTTON1) != 0 172 | } 173 | 174 | /// Whether the second X button is down. 175 | pub fn xbutton2(&self) -> bool { 176 | (self.wparam & MK_XBUTTON2) != 0 177 | } 178 | } 179 | 180 | // https://docs.microsoft.com/en-us/windows/win32/menurc/wm-command 181 | impl CommandData { 182 | /// The selected menu identifier if the message source is a menu. 183 | pub fn menu_id(&self) -> Option { 184 | if self.lparam == 0 && HIWORD(self.wparam as u32) == 0 { 185 | Some(LOWORD(self.wparam as u32)) 186 | } else { 187 | None 188 | } 189 | } 190 | 191 | /// The selected accelerator identifier if the message source is an accelerator. 192 | pub fn accelerator_id(&self) -> Option { 193 | if self.lparam == 0 && HIWORD(self.wparam as u32) == 1 { 194 | Some(LOWORD(self.wparam as u32)) 195 | } else { 196 | None 197 | } 198 | } 199 | 200 | /// The raw data emitted by a control. 201 | pub fn control_data(&self) -> Option { 202 | if let Some(hwnd) = NonNull::new(self.lparam as HWND) { 203 | Some(ControlData { 204 | code: HIWORD(self.wparam as u32), 205 | id: LOWORD(self.wparam as u32), 206 | window: window::Window::Borrowed { hwnd }, 207 | }) 208 | } else { 209 | None 210 | } 211 | } 212 | } 213 | 214 | impl ControlData<'_> { 215 | /// Which standard button is responsible for this message, or `None` if it was emitted by 216 | /// some other custom control. 217 | pub fn std_button(&self) -> Option { 218 | match messagebox::Button::from_id(self.id as i32) { 219 | Ok(Some(button)) => Some(button), 220 | _ => None, 221 | } 222 | } 223 | 224 | /// Interpret the `code` as if it was a notification emitted by a list box. 225 | pub fn list_box_code(&self) -> ListBoxMessage { 226 | ListBoxMessage::from_raw(self.code) 227 | } 228 | } 229 | 230 | // https://docs.microsoft.com/en-us/windows/win32/dlgbox/wm-ctlcolordlg 231 | // https://docs.microsoft.com/en-us/windows/win32/controls/wm-ctlcolorstatic 232 | impl ColorData { 233 | fn hdc(&self) -> HDC { 234 | self.wparam as HDC 235 | } 236 | 237 | pub fn set_text_color(&self, r: u8, g: u8, b: u8) -> std::result::Result<(u8, u8, u8), ()> { 238 | // https://docs.microsoft.com/en-us/windows/win32/api/wingdi/nf-wingdi-settextcolor 239 | let result = unsafe { SetTextColor(self.hdc(), RGB(r, g, b)) }; 240 | if result != CLR_INVALID { 241 | Ok((GetRValue(result), GetGValue(result), GetBValue(result))) 242 | } else { 243 | Err(()) 244 | } 245 | } 246 | 247 | pub fn set_background_transparency(&self, transparent: bool) -> std::result::Result { 248 | // https://docs.microsoft.com/en-us/windows/win32/api/wingdi/nf-wingdi-setbkmode 249 | let value = if transparent { TRANSPARENT } else { OPAQUE }; 250 | 251 | let result = unsafe { SetBkMode(self.hdc(), value as i32) }; 252 | match result as u32 { 253 | TRANSPARENT => Ok(true), 254 | OPAQUE => Ok(false), 255 | 0 => Err(()), 256 | _ => panic!("invalid return value from SetBkMode"), 257 | } 258 | } 259 | } 260 | 261 | impl Message { 262 | pub(crate) fn from_raw(msg: UINT, wparam: WPARAM, lparam: LPARAM) -> Self { 263 | match msg { 264 | WM_CREATE => Message::Create, 265 | WM_SIZE => Message::Size(SizeData { wparam, lparam }), 266 | WM_DESTROY => Message::Destroy, 267 | WM_CLOSE => Message::Close, 268 | WM_INITDIALOG => Message::InitDialog, 269 | WM_PAINT => Message::Paint, 270 | WM_TIMER => Message::Timer(TimerData { wparam, lparam }), 271 | WM_LBUTTONDOWN => Message::LeftMouseButtonDown(MouseData { wparam, lparam }), 272 | WM_RBUTTONDOWN => Message::RightMouseButtonDown(MouseData { wparam, lparam }), 273 | WM_MBUTTONDOWN => Message::MiddleMouseButtonDown(MouseData { wparam, lparam }), 274 | WM_LBUTTONUP => Message::LeftMouseButtonUp(MouseData { wparam, lparam }), 275 | WM_RBUTTONUP => Message::RightMouseButtonUp(MouseData { wparam, lparam }), 276 | WM_MBUTTONUP => Message::MiddleMouseButtonUp(MouseData { wparam, lparam }), 277 | WM_COMMAND => Message::Command(CommandData { wparam, lparam }), 278 | WM_CTLCOLORDLG => Message::ControlColorDialog(ColorData { wparam, lparam }), 279 | WM_CTLCOLORSTATIC => Message::ControlColorStatic(ColorData { wparam, lparam }), 280 | _ => Message::Other { 281 | msg, 282 | wparam, 283 | lparam, 284 | }, 285 | } 286 | } 287 | } 288 | 289 | impl ListBoxMessage { 290 | pub(crate) fn from_raw(code: u16) -> Self { 291 | match code { 292 | LBN_SELCHANGE => ListBoxMessage::SelectionChange, 293 | _ => ListBoxMessage::Other { code }, 294 | } 295 | } 296 | } 297 | -------------------------------------------------------------------------------- /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