3、事件循环机制:X11事件类型、事件队列、事件分发与处理的源码实现

好,咱们今天来啃一块硬骨头——事件循环。说实话,WMS 里最让我头疼的就是这块代码。你想想看,窗口系统要同时处理鼠标点击、键盘输入、窗口重绘、焦点切换……这么多事情,怎么做到井井有条?

答案就是事件循环。它就像一个永不休息的调度员,不断从 X11 服务器那里取事件,然后分发给对应的窗口去处理。我当年第一次读这部分源码时,差点被那个 while(1) 绕晕。后来才明白,这其实就是个「取件-派单」的过程。

3.1 X11 事件类型:你看到的每个操作,背后都有编号

X11 协议定义了 30 多种事件类型。WMS 不需要处理全部,但核心的那几种必须拿捏住。我个人习惯把事件分成三类:

事件类型 宏定义 触发场景
按键事件 KeyPress / KeyRelease 键盘按下或释放
按钮事件 ButtonPress / ButtonRelease 鼠标按键点击
鼠标移动 MotionNotify 鼠标在窗口内移动
进入/离开 EnterNotify / LeaveNotify 鼠标进入或离开窗口区域
焦点变化 FocusIn / FocusOut 窗口获得或失去键盘焦点
暴露事件 Expose 窗口内容需要重绘
配置事件 ConfigureNotify 窗口大小、位置被改变
映射事件 MapNotify / UnmapNotify 窗口被显示或隐藏
销毁事件 DestroyNotify 窗口被销毁
客户端消息 ClientMessage 应用程序与 WMS 通信

重点:ClientMessage 是 WMS 和普通窗口通信的桥梁。比如窗口要关闭时,会发送 WM_DELETE_WINDOW 协议消息。WMS 收到后,再决定是直接关掉,还是弹个确认框。

3.2 事件队列:X11 的「消息管道」

X11 服务器和 WMS 之间,通过一个 socket 连接。所有事件都通过这个管道传输。WMS 这边,Xlib 库帮我们维护了一个事件队列。

说白了,就是 X11 把事件塞进队列,WMS 从队列头部取出来处理。这个队列是先进先出的,保证了事件处理的顺序性。

我记得有一次调试时,发现窗口总是响应滞后。查了半天,原来是事件队列被某个耗时操作堵住了。从那以后,我特别强调:事件处理函数里绝对不能做耗时操作

3.3 事件分发:谁的事件谁来领

WMS 拿到一个事件后,第一件事就是看这个事件是发给谁的。XEvent 结构体里有个 xany.window 字段,记录了目标窗口的 ID。

分发逻辑其实很简单:

// 伪代码:事件分发核心逻辑
while (1) {
    XNextEvent(display, &event);  // 从队列取事件

    switch (event.type) {
        case ButtonPress:
            handle_button_press(&event);
            break;
        case KeyPress:
            handle_key_press(&event);
            break;
        case Expose:
            handle_expose(&event);
            break;
        case ClientMessage:
            handle_client_message(&event);
            break;
        // ... 其他事件类型
        default:
            // 忽略不关心的事件
            break;
    }
}

小技巧:XNextEvent 是阻塞的。如果队列为空,它会一直等。这样 CPU 不会空转。但如果你需要做动画或定时任务,可以用 XPending 先检查队列是否有事件,没有的话就去干别的事。

3.4 事件处理:从取件到派单的完整流程

咱们以鼠标点击窗口标题栏为例,走一遍完整流程:

  1. X11 服务器检测到鼠标点击,生成 ButtonPress 事件
  2. 事件通过 socket 发送到 WMS 进程
  3. Xlib 把事件放入队列
  4. WMS 调用 XNextEvent 取出事件
  5. 根据 event.type 进入 ButtonPress 分支
  6. 从 event.xbutton 中获取点击坐标、窗口 ID
  7. 判断点击位置是否在标题栏区域
  8. 如果是,启动窗口拖动逻辑

嗯,这里要注意:WMS 只处理自己感兴趣的事件。比如鼠标在客户区点击,WMS 通常不拦截,而是让事件穿透到目标窗口。怎么做到的?靠的是 SubstructureRedirectMaskSubstructureNotifyMask 这两个事件掩码。

避坑指南:我曾经犯过一个错误——在事件处理函数里调用了 XFlush。结果导致事件循环卡死。原因是 XFlush 会强制刷新缓冲区,但此时事件还没处理完,造成了死锁。记住:事件处理期间,不要主动刷新 X11 连接

3.5 源码实现:核心数据结构

WMS 里通常维护一个全局的事件映射表。我个人喜欢用函数指针数组:

// 事件处理函数指针类型
typedef void (*event_handler_t)(XEvent *);

// 事件处理表
static event_handler_t event_handlers[LASTEvent] = {
    [ButtonPress]      = handle_button_press,
    [ButtonRelease]    = handle_button_release,
    [MotionNotify]     = handle_motion_notify,
    [KeyPress]         = handle_key_press,
    [Expose]           = handle_expose,
    [ClientMessage]    = handle_client_message,
    [ConfigureNotify]  = handle_configure_notify,
    [MapNotify]        = handle_map_notify,
    [UnmapNotify]      = handle_unmap_notify,
    [DestroyNotify]    = handle_destroy_notify,
    [EnterNotify]      = handle_enter_notify,
    [LeaveNotify]      = handle_leave_notify,
    [FocusIn]          = handle_focus_in,
    [FocusOut]         = handle_focus_out,
};

// 事件循环主函数
void event_loop(Display *display) {
    XEvent event;

    while (1) {
        XNextEvent(display, &event);

        // 安全检查:防止越界
        if (event.type < 0 || event.type >= LASTEvent) {
            continue;
        }

        // 查找处理函数
        event_handler_t handler = event_handlers[event.type];
        if (handler) {
            handler(&event);
        }
    }
}

这种设计的好处是:添加新事件类型时,只需要注册一个处理函数,主循环完全不用改。你想想看,如果每个事件都写一个 case,那代码得多长?

3.6 实战经验:事件循环的性能优化

事件循环是 WMS 的心脏。它跳得快不快,直接决定了窗口操作的流畅度。我总结了几条优化经验:

  • 批量处理 Expose 事件:多个 Expose 事件可以合并成一次重绘。用 XCheckTypedEvent 把队列里剩余的 Expose 都取出来,只重绘一次。
  • MotionNotify 事件节流:鼠标移动会产生大量事件。如果每帧都处理,CPU 会炸。我一般设置一个最小时间间隔,比如 16ms 内只处理一次。
  • 异步处理 ClientMessage:有些客户端消息需要和应用程序通信,可能会阻塞。用 XSendEvent 异步发送,不要等回复。

核心思想:事件循环的本质是「快进快出」。取事件要快,处理事件要快,然后立刻回到循环头部取下一个事件。任何阻塞操作,都会让整个窗口系统卡住。

好了,事件循环这块就讲到这里。下一章咱们聊聊窗口布局管理——说白了,就是怎么把窗口摆得整整齐齐。到时候我会分享一个我踩过的坑,关于浮动窗口和瓦片窗口的切换逻辑,挺有意思的。