Zoe
Zoe
返回博客

Rust小工具:窗口管理

13 分钟阅读
Rust小工具Windows

大概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),
}

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

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

Zoe

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

https://zoe.im

评论