Zoe

Zoe

beta
实验项目

返回到博客

Rust小工具:窗口管理

Zoe

Zoe

2021-07-14

1 MINS

大概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 Self
2 where
3 Q: EventHandler + Send + Sync + 'static
4 {}

以及一个启动方法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_top
10 │ │ ▼
11 │ │
12 │ │
13 │ │ ▲
14 ▲ │ │ │
15 │ │ │ │
16left_botttom │ │ │ │ right_bottom
17 ───────────┴─┼──────────────────────────┼─┴────────
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 window
2impl Window {
3 // find window by name directlly
4 pub fn from_name(class: Option<&str>, name: &str) -> Option<Self> {
5 // if class is none use default vlaue of PSTR
6 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 filter
17 pub fn from_first_name(class: Option<&str>, name: &str) -> Option<Self> {
18 // use enum to filter the first one
19 let mut my = None ;
20 enum_windows(|w| {
21 if class.is_none() // don't offer a class to match
22 || 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 object
26 my = Self::from_hwnd(w.hwnd);
27
28 // stop enum loop
29 return false;
30 }
31 // don't match continue
32 true
33 });
34 // reutrn the result
35 my
36 }
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 id
4 exited: Arc<AtomicBool>, // exit the thead
5
6 // filter functions: all should be true
7 // 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 message
13}

其中EventHandler是事件的回调处理Trait,只需要实现handle即可。

1pub trait EventHandler {
2 fn handle(&mut self, evt: &WinEvent);
3}

另外,我们需要给函数FnMut(&WinEvent)实现一下这个Trait, 方便将匿名函数注册为处理的Handler。

1impl <F>EventHandler for F
2where
3 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 window
2impl Window {
3 pub fn listen(&self) -> WinEventListener {
4 // return event listener
5 WinEventListener::new(*self)
6 }
7}

注册回调的方法on就是向handlers中根据WinEventType类型添加一个函数,

1impl WinEventListener {
2 // on method to add event listener, evt type -> callback
3 pub fn on<Q>(&mut self, typ: WinEventType, cb: Q) -> &mut Self
4 where
5 Q: EventHandler + Send + Sync + 'static
6 {
7 // TODO: add event listener by config
8 self.handlers.lock().unwrap().entry(typ)
9 .or_insert_with(Vec::new)
10 .push(Box::new(cb));
11
12 self
13 }
14}

到此注册的工作就做完了,剩下就是启动了。 这里稍微有点复杂,我希望start能够接收block参数,来决定是否需要单独开线程来执行。 因为事件监听一定是一个循环,不会退出。

我们使用SetWinEventHook来向Windows系统注册时间Hook函数,需要定义一个FFI函数, 这个函数是给Windows系统使用。

1// handle the window hook event process
2#[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 fields
13 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 channel
26 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().0
32 .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 function
4 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 thread
12 // and set the event hook handle for w32
13 // set to global static send
14 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 type
25 // for f in _filters.lock().unwrap().into_iter() {
26 // if !f(&evt) {
27 // // if with false just ingore
28 // return true;
29 // }
30 // }
31
32 // hard code for window match
33 if target_w.is_valide() && evt.window != target_w {
34 // return true;
35 return;
36 }
37
38 // call functions with type
39 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 all
49 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 loop
63 MessageLoop::start(10, |_msg| {
64 process();
65 true
66 });
67 } else {
68 // store the thread handle
69 self.thread = Some(thread::spawn(move || {
70 MessageLoop::start(10, |_msg| {
71 process();
72 true
73 });
74 }));
75 }
76
77 Ok(())
78 }
79}

这个MessageLoop是一个工具函数,适配Windows的实现机制,调用PeekMessageW进行消息的获取。

别忘记,我们还需要给WinEventListener实现一下Droptrait用来自动删除Hook,并退出事件线程,如果有的话。

1impl Drop for WinEventListener {
2 fn drop(&mut self) {
3 // unhook the window
4 let hid = self.hook.load(Ordering::SeqCst);
5 unsafe {
6 UnhookWinEvent(HWINEVENTHOOK(hid));
7 }
8 println!("remove the hook {}", hid);
9
10 // exit thread
11 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 window
3 w: Window,
4 // target window
5 target: Window,
6
7 // direction
8 dir: AttachDirection,
9 // match the size or not with max and min limit
10 match_size: bool,
11 match_size_max: u32,
12 match_size_min: u32,
13 // fix the position from target
14 fix_pos: (i32, i32),
15}

那么,我们的主要逻辑入口就在WindowAttachstart方法,

1impl WindowAttach {
2 // start the attach
3 pub fn start(&mut self) -> Result<()> {
4 // set the target to be owner
5 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 target
17 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 size
23 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 function
34 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(&current_rect) {
38 // update
39 println!("change rect {}", current_rect);
40 _window.set_rect(&current_rect, false);
41 }
42 println!("same one");
43 };
44
45 // init udpate
46 _window.show();
47 update_rect();
48
49 // start the event hook
50 let mut listener = self.target.listen();
51 let _ = listener
52 .on(WinEventType::LocationChange, move |evt: &WinEvent| {
53 // TODO: too many events
54 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 pos
59 // 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使用涉及到了所有权,堆内存,生命周期,其中一些内容中间遇到了很多问题。 也是慢慢在学习和理解中解决的。


而立之前的驻足
Rust小工具:微信多开

Zoe

Zoe

beta

我将成为我的墓志铭

站点

© 2011 - 2022 Zoe - All rights reserved.

Made with by in Hangzhou.