返回到博客
Rust小工具:窗口管理
大概4、5年前就在关注和断断续续的学习Rust,大多数从写小工具开始。最近闲暇便整理了一些代码并撰写成文发布。
几年前在数据抓取组时,做过一个微信抓取系统。其中一部分是Windows上窗口的管理,当时是用Go完成的。后来自己一直在用Rust做相关重构。这里将「窗口吸附」这个很小的功能单独领出来,虽然功能就一个,代码很简单,不过也能比较好地展示如何用Rust开展一个小项目。
窗口管理#
我们主要想是实现的窗口的吸附,即将窗口B吸附到窗口A上,那么窗口B将随着窗口A的变化而变化, 包含,
- 移动
- 隐藏
- 显示
这在实现一些窗口工具时很有用。
所以我们需要捕获窗口A的一些事件,然后按需调整窗口B的属性。
在Windows上,就是通过SetWinEventHook
这个来实现事件的监听。
方案设计#
首先是需要实现一个窗口的事件监听器,我们定义操作接口如下,
1Window::from_name(None, "窗口 A").unwrap()2 .listen()3 .on(WinEventType::Destroy, move |evt| {4
5 })6 .on(WinEventType::Create, move |evt| {7
8 })9 .start(true);
Window
提供窗口的构造函数,
1pub fn from_name(class: Option<&str>, name: &str) -> Option<Self> {}
给Window
扩展listen
方法返回Listener
实例,
1pub fn listen(&self) -> WinEventListener {}
Listener
提供注册事件处理函数的方法on
1pub fn on<Q>(&mut self, typ: WinEventType, cb: Q) -> &mut Self2 where3 Q: EventHandler + Send + Sync + 'static4 {}
以及一个启动方法start
1pub fn start(&mut self, block: bool) -> Result<()> {}
接下来就是给Window
扩展附着函数attach_to
1fn attach_to(&self, target: Window) -> WindowAttach {}
返回一个WindowAttach
对象,这个对象就是一些对窗口操作的配置,包括,
1pub fn dir(&mut self, direction: AttachDirection) -> &mut Self {}2pub fn match_size(&mut self, enable: bool) -> &mut Self {}3pub fn match_size_limit(&mut self, max: u32, min: u32) -> &mut Self {}4pub fn match_size_max(&mut self, max: u32) -> &mut Self {}5pub fn match_size_min(&mut self, min: u32) -> &mut Self {}6pub fn fix_pos(&mut self, fixed: (i32, i32)) -> &mut Self {}
然后就是一个启动函数start
1pub fn start(&mut self) -> Result<()> {}
基本的框架应该就是这样,最后使用示例如下,
1#[test]2fn test_demo() {3 let child = Window::from_name(None, "MINGW64:/d/Zoe").unwrap();4 let target = Window::from_name(None, "MINGW64:/c/Users/Zoe").unwrap();5 let target = Window::from_name(Some("WeChatMainWndForPC"), "微信").unwrap(); 6
7 let _ = child.attach_to(target)8 .match_size(true)9 .dir(AttachDirection::RightTop)10 .match_size_min(200)11 .match_size_max(800)12 .fix_pos((-10, 0))13 .start();14}
其中AttachDirection
附着方向暂时定义8个,如下所示
1| │2 │ │3 │ │4 │ top_left top_right│5 │ │6 ├───► ◄───┤7 ────────────┬─┼──────────────────────────┼─┬───────────8 left_top │ │ │ │9 ▼ │ │ │ right_top10 │ │ ▼11 │ │12 │ │13 │ │ ▲14 ▲ │ │ │15 │ │ │ │16left_botttom │ │ │ │ right_bottom17 ───────────┴─┼──────────────────────────┼─┴────────18 ├───► ◄─────┤19 │ │20 │ botom_left bottom_right│21 │ │22 │ │
代码实现#
由于之前在Rust小工具:微信多开中比较完整了演示了 如何创建一个Rust项目,这里就不再赘述,直接记录一些核心的内容。
这里我们使用windows-rs这个crate来支持Windows上的一些操作。
我们先从Window
的构造方法开始实现,主要实现一个from_name
方法,
查找窗口的函数是使用FindWindowW
,实现比较简单。
另外我们也实现了窗口的枚举用于遍历窗口enum_windows
,
应用在窗口名匹配查找上的方法from_first_name
。
1// implement factory methods for window2impl Window {3 // find window by name directlly4 pub fn from_name(class: Option<&str>, name: &str) -> Option<Self> {5 // if class is none use default vlaue of PSTR6 let hwnd = unsafe { 7 if let Some(class) = class {8 FindWindowW(class, name)9 } else {10 FindWindowW(PWSTR::default(), name)11 }12 };13 Self::from_hwnd(hwnd)14 }15 16 // find first one window by name: enums and filter17 pub fn from_first_name(class: Option<&str>, name: &str) -> Option<Self> {18 // use enum to filter the first one19 let mut my = None ;20 enum_windows(|w| {21 if class.is_none() // don't offer a class to match22 || class.unwrap().eq(w.class().unwrap().as_str())23 || name.eq(w.title().unwrap_or_default().as_str())24 {25 // matched create or copy the a new window object26 my = Self::from_hwnd(w.hwnd);27
28 // stop enum loop29 return false;30 }31 // don't match continue32 true33 });34 // reutrn the result35 my36 }37}
接下来就是我们重点的Listener
的实现了。
首先需要保存主窗口w: Window
,然后还需要保存事件处理的回调函数handlers: Arc::new(Mutex::new(HashMap::new()))
,
再一个就是用于传递事件消息ch: Arc::new(Mutex::new(unbounded()))
最后WinEventListener
结构体如下,
1pub struct WinEventListener {2 w: Window,3 hook: AtomicIsize, // sotre the handle id4 exited: Arc<AtomicBool>, // exit the thead5
6 // filter functions: all should be true7 // filters: Arc<Mutex<Vec<Box<dyn FnMut(&WinEvent) -> bool + Send>>>>,8 // handle functions,9 handlers: Arc<Mutex<HashMap<WinEventType, Vec<Box<dyn EventHandler + Send + Sync + 'static>>>>>,10 // handlers: Arc<Mutex<HashMap<WinEventType, Box<dyn EventHandler + Send + Sync + 'static>>>>,11
12 thread: Option<JoinHandle<()>>, // thread for handle message13}
其中EventHandler
是事件的回调处理Trait,只需要实现handle
即可。
1pub trait EventHandler {2 fn handle(&mut self, evt: &WinEvent);3}
另外,我们需要给函数FnMut(&WinEvent)
实现一下这个Trait,
方便将匿名函数注册为处理的Handler。
1impl <F>EventHandler for F2where3 F: FnMut(&WinEvent)4{5 fn handle(&mut self, evt: &WinEvent) {6 self(evt)7 }8}
我们还需要一个静态的事件队列,因为我们时间的捕获函数,不是在当前线程内的,所以需要通过队列来传递。使用HashMap来放不同Hook函数的队列,用HookID作为Key。
1lazy_static! {2 static ref EVENTS_CHANNELS: Arc<Mutex<HashMap<isize, Arc<Mutex<(Sender<WinEvent>, Receiver<WinEvent>)>>>>> = 3 Arc::new(Mutex::new(HashMap::new()));4}
给Window
扩展一个listen
方法用于返回一个WinEventListener
实例,
1// add ext for window2impl Window {3 pub fn listen(&self) -> WinEventListener {4 // return event listener5 WinEventListener::new(*self)6 }7}
注册回调的方法on
就是向handlers
中根据WinEventType
类型添加一个函数,
1impl WinEventListener {2 // on method to add event listener, evt type -> callback3 pub fn on<Q>(&mut self, typ: WinEventType, cb: Q) -> &mut Self4 where5 Q: EventHandler + Send + Sync + 'static6 {7 // TODO: add event listener by config8 self.handlers.lock().unwrap().entry(typ)9 .or_insert_with(Vec::new)10 .push(Box::new(cb));11
12 self13 }14}
到此注册的工作就做完了,剩下就是启动了。
这里稍微有点复杂,我希望start
能够接收block
参数,来决定是否需要单独开线程来执行。
因为事件监听一定是一个循环,不会退出。
我们使用SetWinEventHook
来向Windows系统注册时间Hook函数,需要定义一个FFI函数,
这个函数是给Windows
系统使用。
1// handle the window hook event process2#[allow(non_snake_case)]3unsafe extern "system" fn thunk(4 hook_handle: HWINEVENTHOOK,5 event: u32,6 hwnd: HWND,7 _id_object: i32,8 _id_child: i32,9 _id_event_thread: u32,10 _dwms_event_time: u32,11) {12 // create the event, add more id fields13 let mut evt = WinEvent::new(hook_handle, event, hwnd);14 evt.raw_id_child = _id_child;15 evt.raw_id_object = _id_object;16 evt.raw_id_thread = _id_event_thread;17
18 // TODO: add filter at here ingore windows not match???19
20 if evt.etype == WinEventType::Unknown {21 // println!("unknown event type");22 return;23 }24
25 // geet the handle channel26 match EVENTS_CHANNELS.lock().unwrap().get(&hook_handle.0) {27 None => {28 println!("can't get event channel {:?}", evt.etype);29 },30 Some(v) => {31 v.lock().unwrap().032 .send(evt)33 .expect("could not send message event channel");34 }35 }36}
start
函数的主要逻辑,
1impl WinEventListener {2 pub fn start(&mut self, block: bool) -> Result<()> {3 // install the win event hook function4 let hook_handle = unsafe {SetWinEventHook(EVENT_MIN, EVENT_MAX, None, Some(thunk), 0, 0, 0,)};5
6 // take the ch with hook_id?7 println!("the event hook id {:?}", hook_handle);8
9 self.hook.store(hook_handle.0, Ordering::SeqCst);10
11 // start the message loop thread12 // and set the event hook handle for w3213 // set to global static send14 EVENTS_CHANNELS.lock().unwrap().insert(hook_handle.0, self.ch.clone());15
16 let ch = self.ch.clone();17 let _handlers = self.handlers.clone();18 let _exited = self.exited.clone();19 // let _filters = self.filters.clone();20 let target_w = self.w;21
22 let process = move || {23 if let Ok(evt) = ch.lock().unwrap().1.try_recv() {24 // filter and call with event type25 // for f in _filters.lock().unwrap().into_iter() {26 // if !f(&evt) {27 // // if with false just ingore28 // return true;29 // }30 // }31
32 // hard code for window match33 if target_w.is_valide() && evt.window != target_w {34 // return true;35 return;36 }37
38 // call functions with type39 match _handlers.lock().unwrap().get_mut(&evt.etype) {40 Some(v) => {41 for cb in v.into_iter() {42 cb.handle(&evt);43 }44 },45 None => {},46 }47
48 // call functions all49 match _handlers.lock().unwrap().get_mut(&WinEventType::All) {50 Some(v) => {51 for cb in v.into_iter() {52 cb.handle(&evt);53 }54 },55 None => {},56 }57 }58
59 };60
61 if block {62 // start the message loop63 MessageLoop::start(10, |_msg| {64 process();65 true66 });67 } else {68 // store the thread handle69 self.thread = Some(thread::spawn(move || {70 MessageLoop::start(10, |_msg| {71 process();72 true73 });74 }));75 }76
77 Ok(())78 }79}
这个MessageLoop
是一个工具函数,适配Windows的实现机制,调用PeekMessageW
进行消息的获取。
别忘记,我们还需要给WinEventListener
实现一下Drop
trait用来自动删除Hook,并退出事件线程,如果有的话。
1impl Drop for WinEventListener {2 fn drop(&mut self) {3 // unhook the window4 let hid = self.hook.load(Ordering::SeqCst);5 unsafe {6 UnhookWinEvent(HWINEVENTHOOK(hid));7 }8 println!("remove the hook {}", hid);9
10 // exit thread11 self.exited.store(true, Ordering::SeqCst);12 }13}
至于AttachDirection
就比较简单了,
定义好枚举类型并增加一个apply
方法即可,
1#[derive(Debug, Clone, Copy, PartialEq)]2pub enum AttachDirection {3 LeftTop, TopLeft,4 RightTop, TopRight,5 RightBottom, BottomRight,6 LeftBottom, BottomLeft,7}8
9impl AttachDirection {10 pub fn apply(self, current: Rect, target: Rect, fixed: (i32, i32)) -> (i32, i32) {11 match self {12 Self::LeftTop => {13 let p = target.left_top();14 (p.0 + fixed.0 - current.width, p.1 + fixed.1)15 },16 Self::TopLeft => {17 let p = target.left_top();18 (p.0 + fixed.0, p.1 + fixed.1 - current.height)19 },20 // ...21 }22 }23}
最后一步就是WindowAttach
的实现了,主要包含窗口A(leader)和窗口B(slave),
然后再加上一些其他配置信息,
1pub struct WindowAttach {2 // self window3 w: Window,4 // target window5 target: Window,6
7 // direction8 dir: AttachDirection,9 // match the size or not with max and min limit10 match_size: bool,11 match_size_max: u32,12 match_size_min: u32,13 // fix the position from target14 fix_pos: (i32, i32),15}
那么,我们的主要逻辑入口就在WindowAttach
的start
方法,
1impl WindowAttach {2 // start the attach3 pub fn start(&mut self) -> Result<()> {4 // set the target to be owner5 self.w.set_owner(self.target);6
7 let _dir = self.dir;8 let _match_size = self.match_size;9 let _max = self.match_size_max;10 let _min = self.match_size_min;11 let _fix_pos = self.fix_pos;12 let _target = self.target;13 let _window = self.w;14
15 let update_rect = move || {16 // get the rect of target17 let target_rect = _target.rect().unwrap();18
19 let mut current_rect = _window.rect().unwrap();20 let old = current_rect;21 // resize self, this must be first!22 // postion needs size23 if _match_size {24 let size = _dir.match_size(25 (current_rect.width, current_rect.height), 26 (target_rect.width, target_rect.height),27 _min as _,28 _max as _,29 );30 current_rect.width = size.0;31 current_rect.height = size.1;32 }33 // this shoudl be in a function34 let p = _dir.apply(current_rect, target_rect, _fix_pos);35 current_rect.x = p.0;36 current_rect.y = p.1;37 if !old.eq(¤t_rect) {38 // update 39 println!("change rect {}", current_rect);40 _window.set_rect(¤t_rect, false);41 }42 println!("same one");43 };44
45 // init udpate46 _window.show();47 update_rect();48
49 // start the event hook50 let mut listener = self.target.listen();51 let _ = listener52 .on(WinEventType::LocationChange, move |evt: &WinEvent| {53 // TODO: too many events54 println!("evt.obejct {}, evt.child {}", evt.raw_id_object, evt.raw_id_child);55 if 0 == evt.raw_id_object { update_rect(); }56 })57 .on(WinEventType::MoveResizeEnd, move |evt: &WinEvent| {58 // reset size and pos59 // get the old place???60 update_rect();61 })62 .on(WinEventType::Show, move |evt: &WinEvent| {63 if 0 == evt.raw_id_object {64 println!("window show");65 _window.show();66 }67 })68 .on(WinEventType::Hide, move |evt: &WinEvent| {69 if 0 == evt.raw_id_object {70 _window.hidden();71 }72 })73 .start(true);74
75 Ok(())76 }77}
主要过程是,
- 先将slave窗口的owner设置为master
- 初始化一些状态,准备更新slave窗口的闭包函数
update_rect
- 然后注册一系列的事件处理函数
结论#
最后,这个库实现的效果基本能够满足使用, 具体的实现代码在winapp-rs仓库中。
对于Rust使用涉及到了所有权
,堆内存
,生命周期
,其中一些内容中间遇到了很多问题。
也是慢慢在学习和理解中解决的。