Introduction
Here in this tip, we will design a simple calculator's graphical interface using pure Win32 API functions in Rust programming language.
In Code Project, I wrote an article on Rust called 'Win32 GUI Programming In Rust Language ' where I showed how we can create a simple window using pure Win32 API. Although there were some mistakes, some Code Project members helped me through comments to correct those mistakes. So I want to say many thanks to them.
Using the Code
Designing graphical user interface using pure Win32 API is a little hard. But at the end, it helps a lot to understand the basic concept. So this tip will help you to understand the use of basic graphical elements such as buttons, labels, etc. to design an application's user interface. And also, we will see how we can handle a button's click event and change label control's background color.
First, we have to extern
our necessary crates (these crates are available on the internet):
extern crate kernel32;
extern crate user32;
extern crate gdi32;
extern crate winapi;
extern crate libc;
use winapi::windef::HWND;
use winapi::windef::HMENU;
use winapi::windef::HBRUSH;
use winapi::minwindef::HINSTANCE;
use winapi::minwindef::UINT;
use winapi::minwindef::DWORD;
use winapi::minwindef::WPARAM;
use winapi::minwindef::LPARAM;
use winapi::minwindef::LRESULT;
use winapi::winnt::LPCWSTR;
use winapi::winuser::WS_VISIBLE;
use winapi::winuser::WS_CHILD;
use winapi::winuser::WS_EX_CLIENTEDGE;
use winapi::winuser::WNDCLASSW;
use std::os::windows::ffi::OsStrExt;
use std::ffi::OsStr;
static mut op_e1 :i32 = 0;
static mut op_e2 :i32 = 0;
static mut op_operator :i32 = 0;
static mut hwnd_display :HWND = 0 as HWND;
We need a Rust's string
to utf16 string
converter function to work with Win32 API functions:
fn to_wstring(str : &str) -> Vec<u16> {
let v : Vec<u16> =
OsStr::new(str).encode_wide().chain(Some(0).into_iter()).collect();
v
}
The following functions are button's click event handlers. These functions are being called from the WM_COMMAND
message in order to handle our calculator's buttons click event. Though, currenly these functions don't do anything important:
unsafe fn on_clear_click() {
op_e1 = 0;
op_e2 = 0;
op_operator = 0;
user32::SetWindowTextW(hwnd_display, to_wstring("0").as_ptr());
}
unsafe fn on_numbers_click(id :u32) {
user32::SetWindowTextW(hwnd_display, to_wstring("Not Implemented!").as_ptr());
}
unsafe fn on_operators_click() {
op_operator = 1;
user32::SetWindowTextW(hwnd_display, to_wstring("0").as_ptr());
}
unsafe fn on_equal_click() {
let result = op_e1 + op_e2;
let res_str: String = result.to_string();
let ws = to_wstring(&res_str[..]).as_ptr();
user32::SetWindowTextW(hwnd_display, ws);
}
This is our window's message handler function. Here, we handle WM_DESTROY
, WM_CTLCOLORSTATIC
and WM_COMMAND
messages:
pub unsafe extern "system" fn window_proc
(h_wnd :HWND, msg :UINT, w_param :WPARAM, l_param :LPARAM) -> LRESULT
{
if msg == winapi::winuser::WM_DESTROY {
user32::PostQuitMessage(0);
}
We handle the WM_CTLCOLORSTATIC
in order to change the display label control background color to white:
else if msg == winapi::winuser::WM_CTLCOLORSTATIC
{
if hwnd_display == (l_param as HWND)
{
let h_brush = gdi32::CreateSolidBrush((255 | (255 << 8)) | (255 | (255 << 16)));
return h_brush as LRESULT;
}
}
And we also handle WM_COMMAND
to detect buttons click event. Though, it doesn't do any important job here but surely helps you to understand the button click event handling.
else if msg == winapi::winuser::WM_COMMAND
{
I know that this isn't the good way, but I found it easier:
if w_param > 100 && w_param < 111 {
on_numbers_click(w_param as u32);
}
else if w_param > 110 && w_param < 117 {
if w_param == 115 {
on_clear_click();
}
else {
on_operators_click();
}
}
else if w_param == 117 {
on_equal_click();
}
}
return user32::DefWindowProcW(h_wnd, msg, w_param, l_param);
}
Since we are going to create a calculator application's interface, we will need a lot of buttons and a label control. To create our calculator's button controls and other graphical elements, we will use the CreateWindowExW
function. As calculator's display control, we will use a static/label control. We will create all the controls inside a function called init_interface
:
unsafe fn init_interface(h_wnd :HWND)
{
hwnd_display = user32::CreateWindowExW(WS_EX_CLIENTEDGE, to_wstring("static").as_ptr(),
to_wstring("0"), WS_CHILD | WS_VISIBLE | 2 ,
46, 20, 256, 60, h_wnd, 340 as HMENU, 0 as HINSTANCE, std::ptr::null_mut());
let mut i = 1;
let mut x = 46;
let mut y = 210;
let mut btn_num_id = 101;
Now we create all the number buttons using a loop statament. It will make thing a lot more easier:
loop
{
if i >= 10 {
break;
}
let txt = format!("{}", i);
user32::CreateWindowExA(0, "button".as_ptr() as *mut _,
txt.as_ptr() as *mut _, WS_CHILD | WS_VISIBLE,
x, y, 40, 32, h_wnd, btn_num_id as HMENU, 0 as HINSTANCE, std::ptr::null_mut());
x = x + 54;
if i % 3 == 0 {
x = 46;
y = y - 54;
}
btn_num_id = btn_num_id + 1;
i = i + 1;
}
Let's ceate all the arithmetical calculation buttons such as ‘+’, ‘-’ etc.
x = 46;
y = 264;
user32::CreateWindowExA(0, "Button".as_ptr() as *mut _,
"0".as_ptr() as *mut _, WS_CHILD | WS_VISIBLE,
x, y, 148, 32, h_wnd, btn_num_id as HMENU, 0 as HINSTANCE, std::ptr::null_mut());
btn_num_id = btn_num_id + 1;
x = 208;
y = 102;
user32::CreateWindowExA(0, "Button".as_ptr() as *mut _,
"+".as_ptr() as *mut _, WS_CHILD | WS_VISIBLE,
x, y, 40, 32, h_wnd, btn_num_id as HMENU, 0 as HINSTANCE, std::ptr::null_mut());
y = y + 54;
btn_num_id = btn_num_id + 1;
user32::CreateWindowExA(0, "Button".as_ptr() as *mut _,
"-".as_ptr() as *mut _, WS_CHILD | WS_VISIBLE,
x, y, 40, 32, h_wnd, btn_num_id as HMENU, 0 as HINSTANCE, std::ptr::null_mut());
y = y + 54;
btn_num_id = btn_num_id + 1;
user32::CreateWindowExA(0, "Button".as_ptr() as *mut _,
"x".as_ptr() as *mut _, WS_CHILD | WS_VISIBLE,
x, y, 40, 32, h_wnd, btn_num_id as HMENU, 0 as HINSTANCE, std::ptr::null_mut());
y = y + 54;
btn_num_id = btn_num_id + 1;
user32::CreateWindowExA(0, "Button".as_ptr() as *mut _,
"/".as_ptr() as *mut _, WS_CHILD | WS_VISIBLE,
x, y, 40, 32, h_wnd, btn_num_id as HMENU, 0 as HINSTANCE, std::ptr::null_mut());
btn_num_id = btn_num_id + 1;
x = 262;
y = 102;
user32::CreateWindowExA(0, "Button".as_ptr() as *mut _,
"C".as_ptr() as *mut _, WS_CHILD | WS_VISIBLE,
x, y, 40, 32, h_wnd, btn_num_id as HMENU, 0 as HINSTANCE, std::ptr::null_mut());
y = y + 54;
btn_num_id = btn_num_id + 1;
user32::CreateWindowExA(0, "Button".as_ptr() as *mut _,
"%".as_ptr() as *mut _, WS_CHILD | WS_VISIBLE,
x, y, 40, 32, h_wnd, btn_num_id as HMENU, 0 as HINSTANCE, std::ptr::null_mut());
y = y + 54;
btn_num_id = btn_num_id + 1;
user32::CreateWindowExA(0, "Button".as_ptr() as *mut _,
"=".as_ptr() as *mut _, WS_CHILD | WS_VISIBLE,
x, y, 40, 86, h_wnd, btn_num_id as HMENU, 0 as HINSTANCE, std::ptr::null_mut());
}
This is the entry point function of our program. Here, we register and create our main window. Then, we call the init_interface function to create our calculator's interface (static and buttons).
fn main()
{
unsafe
{
let class_name = to_wstring("my_window");
let wnd = WNDCLASSW {
style: 0,
lpfnWndProc: Some(window_proc),
cbClsExtra: 0,
cbWndExtra: 0,
hInstance: 0 as HINSTANCE,
hIcon: user32::LoadIconW(0 as HINSTANCE, winapi::winuser::IDI_APPLICATION),
hCursor: user32::LoadCursorW(0 as HINSTANCE, winapi::winuser::IDI_APPLICATION),
hbrBackground: 16 as HBRUSH,
lpszMenuName: 0 as LPCWSTR,
lpszClassName: class_name.as_ptr(),
};
user32::RegisterClassW(&wnd);
let h_wnd_main = user32::CreateWindowExW(0, class_name.as_ptr(),
to_wstring("Simple Calculator Interface In Rust").as_ptr(),
winapi::winuser::WS_OVERLAPPED | winapi::winuser::WS_CAPTION |
winapi::winuser::WS_SYSMENU | winapi::winuser::WS_MINIMIZEBOX | WS_VISIBLE,
0, 0, 366, 400, 0 as HWND, 0 as HMENU, 0 as HINSTANCE, std::ptr::null_mut());
init_interface(h_wnd_main);
user32::ShowWindow(h_wnd_main, winapi::SW_SHOW);
let mut msg = winapi::winuser::MSG {
hwnd : 0 as HWND,
message : 0 as UINT,
wParam : 0 as WPARAM,
lParam : 0 as LPARAM,
time : 0 as DWORD,
pt : winapi::windef::POINT { x: 0, y: 0, },
};
loop {
let pm = user32::GetMessageW(&mut msg, 0 as HWND, 0, 0);
if pm == 0 {
break;
}
if msg.message == winapi::winuser::WM_QUIT {
break;
}
user32::TranslateMessage(&mut msg);
user32::DispatchMessageW(&mut msg);
}
}
}
Our Cargo.toml file's contents will look like this (name, version and authors is your choice):
[package]
name = "simple_calculator_interface"
version = "0.0.1"
authors = [ "Your name <you@example.com>" ]
[dependencies]
libc = "0.1.10"
winapi = "0.2.4"
user32-sys = "0.1.2"
kernel32-sys = "0.1.4"
gdi32-sys = "0.1.1"
Compile the program using the following commands (many thanks to the Code Project member who helped me remove the black console window via command line arguments):
cargo rustc -- -C link-args=-mwindows
Now run your ‘simple_calculator_interface.exe’.
Conclusion
I hope this tip has helped programmers to understand the basic GUI programming in Rust language. Although I know that using the Win API directly isn't the best idea. However, it does help to understand the basic idea.