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

Zoe
全栈开发 · AI 工具制造者 · Go / Flutter / Rust · 开源偏执狂
