基于派生宏的代码实例

Cargo.toml 文件

[package]
name = "demo"
version = "0.1.0"
edition = "2018"

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[dependencies]
native-windows-gui = "*"
native-windows-derive = "*"

依赖两个外部包。我初步分析了实例代码后,猜测:

  • native-windows-gui:应该是 gui 的接口封装代码
  • native-windows-derive:应该封装了宏定义,类似 C++ 的 MFC 框架,以便 Rust 自动生成相关代码。

main.rs 文件

extern crate native_windows_gui as nwg;
extern crate native_windows_derive as nwd;

use nwd::NwgUi;
use nwg::NativeUi;


#[derive(Default, NwgUi)]
pub struct BasicApp {
    #[nwg_control(size: (300, 115), position: (300, 300), title: "Basic example", flags: "WINDOW|VISIBLE")]
    #[nwg_events( OnWindowClose: [BasicApp::say_goodbye] )]
    window: nwg::Window,

    #[nwg_control(text: "Heisenberg", size: (280, 25), position: (10, 10))]
    name_edit: nwg::TextInput,

    #[nwg_control(text: "Say my name", size: (280, 60), position: (10, 40))]
    #[nwg_events( OnButtonClick: [BasicApp::say_hello] )]
    hello_button: nwg::Button
}

impl BasicApp {
    fn say_hello(&self) {
        nwg::simple_message("Hello", &format!("Hello {}", self.name_edit.text()));
    }
    fn say_goodbye(&self) {
        nwg::simple_message("Goodbye", &format!("Goodbye {}", self.name_edit.text()));
        nwg::stop_thread_dispatch();
    }
}

fn main() {
    nwg::init().expect("Failed to init Native Windows GUI");
    let _app = BasicApp::build_ui(Default::default()).expect("Failed to build UI");
    nwg::dispatch_thread_events();
}

NwgUi

观察代码中的 #[derive(Default, NwgUi)],可以断定, NwgUi 是用于自动生成代码的宏。由于 Rust 的宏定义和 C++ 相比具有碾压式的优势,Rust 提供了完备的语法机制进行宏定义的编码,因此,这套库的易用性和性能肯定远远由于 MFC 之类的框架。

控件

看下面的代码:

#[nwg_control(size: (300, 115), position: (300, 300), title: "Basic example", flags: "WINDOW|VISIBLE")]
    #[nwg_events( OnWindowClose: [BasicApp::say_goodbye] )]
    window: nwg::Window,

定义了字段 window: nwg::Windows,这个代码应该在 native_windows_gui 包中。

  • 控件属性:宏定义 #[nwg_control(size: (300, 115), position: (300, 300), title: "Basic example", flags: "WINDOW|VISIBLE")] 定义了属性
  • 事件关联:宏定义 #[nwg_events( OnWindowClose: [BasicApp::say_goodbye] )] 定义了事件与回调函数之间的关联。注意回调函数放在了 [...] 中,这说明事件可以同时关联多个回调函数。

看到这里,初步判断这套 GUI 框架的基本模型与 Delphi 的 PME (Property、Method、Event)模型基本是一致的。我觉得这是好事,因为这意味这套 GUI 架构秉承了 Delphi 简洁明了的风格。我自己觉得,微软在 WinForm 和 WPF 里搞的那些新玩意儿,把简单问题复杂化了。还是把自主权交给程序员,不要越俎代庖,画蛇添足。

事件从何而来?

代码中的事件名称,例如 OnWindowClose 搞得我一头雾水,不知从何而来。查看了开源代码,也没找到定义。这件事情先放一放。我们接下来看看如果不采用宏定义,代码如何编写,或许从中能找到一些线索。

不用派生宏的代码编写方式

main.rs 文件

直接上代码:

extern crate native_windows_gui as nwg;
use nwg::NativeUi;

#[derive(Default)]
pub struct BasicApp {
    window: nwg::Window,
    name_edit: nwg::TextInput,
    hello_button: nwg::Button,
}

impl BasicApp {
    fn say_hello(&self) {
        nwg::simple_message("Hello", &format!("Hello {}", self.name_edit.text()));
    }
    fn say_goodbye(&self) {
        nwg::simple_message("Goodbye", &format!("Goodbye {}", self.name_edit.text()));
        nwg::stop_thread_dispatch();
    }
}

//
// ALL of this stuff is handled by native-windows-derive
//
mod basic_app_ui {
    use super::*;
    use native_windows_gui as nwg;
    use std::cell::RefCell;
    use std::ops::Deref;
    use std::rc::Rc;

    pub struct BasicAppUi {
        inner: Rc<BasicApp>,
        default_handler: RefCell<Option<nwg::EventHandler>>,
    }

    impl nwg::NativeUi<BasicAppUi> for BasicApp {
        fn build_ui(mut data: BasicApp) -> Result<BasicAppUi, nwg::NwgError> {
            use nwg::Event as E;
            // Controls
            nwg::Window::builder()
                .flags(nwg::WindowFlags::WINDOW | nwg::WindowFlags::VISIBLE)
                .size((300, 115))
                .position((300, 300))
                .title("Basic example")
                .build(&mut data.window)?;

            nwg::TextInput::builder()
                .size((280, 25))
                .position((10, 10))
                .text("Heisenberg")
                .parent(&data.window)
                .focus(true)
                .build(&mut data.name_edit)?;

            nwg::Button::builder()
                .size((280, 60))
                .position((10, 40))
                .text("Say my name")
                .parent(&data.window)
                .build(&mut data.hello_button)?;

            // Wrap-up
            let ui = BasicAppUi {
                inner: Rc::new(data),
                default_handler: Default::default(),
            };

            // Events
            let evt_ui = Rc::downgrade(&ui.inner);
            let handle_events = move |evt, _evt_data, handle| {
                if let Some(ui) = evt_ui.upgrade() {
                    match evt {
                        E::OnButtonClick => {
                            if &handle == &ui.hello_button {
                                BasicApp::say_hello(&ui);
                            }
                        }
                        E::OnWindowClose => {
                            if &handle == &ui.window {
                                BasicApp::say_goodbye(&ui);
                            }
                        }
                        _ => {}
                    }
                }
            };

            *ui.default_handler.borrow_mut() = Some(nwg::full_bind_event_handler(
                &ui.window.handle,
                handle_events,
            ));

            return Ok(ui);
        }
    }

    impl Drop for BasicAppUi {
        /// To make sure that everything is freed without issues, the default handler must be unbound.
        fn drop(&mut self) {
            let handler = self.default_handler.borrow();
            if handler.is_some() {
                nwg::unbind_event_handler(handler.as_ref().unwrap());
            }
        }
    }

    impl Deref for BasicAppUi {
        type Target = BasicApp;

        fn deref(&self) -> &BasicApp {
            &self.inner
        }
    }
}

fn main() {
    nwg::init().expect("Failed to init Native Windows GUI");
    nwg::Font::set_global_family("Segoe UI").expect("Failed to set default font");
    let _ui = BasicApp::build_ui(Default::default()).expect("Failed to build UI");
    nwg::dispatch_thread_events();
}

BasicApp 定义

我觉得 BasicApp 的定义相当精炼:

#[derive(Default)]
pub struct BasicApp {
    window: nwg::Window,
    name_edit: nwg::TextInput,
    hello_button: nwg::Button,
}
impl BasicApp {
    fn say_hello(&self) {
        nwg::simple_message("Hello", &format!("Hello {}", self.name_edit.text()));
    }
    fn say_goodbye(&self) {
        nwg::simple_message("Goodbye", &format!("Goodbye {}", self.name_edit.text()));
        nwg::stop_thread_dispatch();
    }
}

控件属性定义

控件属性的构建代码:

fn build_ui(mut data: BasicApp) -> Result<BasicAppUi, nwg::NwgError> {
            use nwg::Event as E;
            // Controls
            nwg::Window::builder()
                .flags(nwg::WindowFlags::WINDOW | nwg::WindowFlags::VISIBLE)
                .size((300, 115))
                .position((300, 300))
                .title("Basic example")
                .build(&mut data.window)?;

            nwg::TextInput::builder()
                .size((280, 25))
                .position((10, 10))
                .text("Heisenberg")
                .parent(&data.window)
                .focus(true)
                .build(&mut data.name_edit)?;

用级联模式为控件的属性逐个赋值,设计模式很有启发性。看看 VSCode 里带语法提示的编辑截屏:

rustDesk开机启动 rust window_gui

Build 的每个 属性set方法的返回结果都是 Build,最后一个 build 方法输出结果。代码看上去很舒服,不知道效率如何。估计这个设计模式,牺牲了运行效率,换取了代码的可读性。查看了一下WindowBuilder源代码:

pub fn flags(mut self, flags: WindowFlags) -> WindowBuilder<'a> {
        self.flags = Some(flags);
        self
    }

如果编译器不能自动优化的话,每次都要把 WindowBuilder 复制一下,这效率堪忧呀!希望 Rust 的惰性求值机制能帮助编译器自动优化代码。

事件关联

代码如下:

// Events
            let evt_ui = Rc::downgrade(&ui.inner);
            let handle_events = move |evt, _evt_data, handle| {
                if let Some(ui) = evt_ui.upgrade() {
                    match evt {
                        E::OnButtonClick => {
                            if &handle == &ui.hello_button {
                                BasicApp::say_hello(&ui);
                            }
                        }
                        E::OnWindowClose => {
                            if &handle == &ui.window {
                                BasicApp::say_goodbye(&ui);
                            }
                        }
                        _ => {}
                    }
                }
            };

基于这套代码,顺藤摸瓜就找到了事件定义。文件 events.rs 定义了事件的枚举类型。至于事件与回调函数具体的关联方式,这里不细究了,感觉搞明白不容易,也没啥用。

实际编程,还是借助派生宏比较省事。上面这个存手工编码,还是有些麻烦。派生宏的作用,估计也是为了生成这些代码。