2、核心数据结构:窗口对象(Window)、屏幕对象(Screen)、显示对象(Display)的源码结构

好,咱们直接进入正题。WMS 里最核心的三个数据结构,说白了就是 WindowScreenDisplay。这三兄弟撑起了整个窗口管理系统的骨架。我个人习惯把这三者比作「演员、舞台、剧场」——Window 是演员,Screen 是舞台,Display 就是整个剧场。你想想看,没有剧场,舞台和演员往哪儿搁?

今天我就带你把它们的源码结构扒个底朝天。嗯,这里要注意,不同 WMS 实现(比如 Qtile、dwm、Hyprland)的命名和细节略有差异,但核心逻辑是相通的。我当年在做一个嵌入式窗口系统时,就踩过「把 Screen 和 Display 混为一谈」的坑,后面重构时真是欲哭无泪。

2.1 窗口对象(Window)—— 一切交互的起点

Window 对象,说白了就是屏幕上那个「框」。它承载了用户能看到、能操作的一切。在源码里,它通常是一个类,属性多到能写满一页纸。但我建议你只关注这几个核心字段:

属性名 类型 说明
x, y, width, height int 窗口的位置和尺寸,单位像素
border_width int 边框宽度,我习惯设为 2 或 4
border_color str 边框颜色,比如 "#ff0000"
state enum 窗口状态:NORMAL、MAXIMIZED、MINIMIZED、FULLSCREEN
wm_class str 窗口类名,比如 "firefox"、"terminal"
pid int 进程 ID,用于追踪窗口归属

来看一段简化后的 Python 代码,感受一下 Window 对象的骨架:

class Window:
    def __init__(self, x, y, width, height, wm_class):
        self.x = x
        self.y = y
        self.width = width
        self.height = height
        self.border_width = 2
        self.border_color = "#333333"
        self.state = WindowState.NORMAL
        self.wm_class = wm_class
        self.pid = None
        self._mapped = False  # 是否已映射到屏幕

    def move(self, new_x, new_y):
        """移动窗口到新位置"""
        self.x = new_x
        self.y = new_y
        # 这里会触发重绘,具体实现看后端

    def resize(self, new_w, new_h):
        """调整窗口大小"""
        self.width = max(new_w, 100)  # 最小宽度 100px
        self.height = max(new_h, 50)  # 最小高度 50px

    def focus(self):
        """让窗口获得焦点"""
        # 我曾经在这里忘记更新 Z-order,导致窗口被挡住
        self._raise_window()
        self._update_focus_border()

重点:Window 对象的核心职责是「描述自身状态」,它不负责管理其他窗口。每个 Window 都是独立的个体。

我的经验:在写 Window 类时,记得把 __repr__ 方法实现一下。调试时直接打印窗口对象,能省下大量时间。比如 return f"Window({self.wm_class}, {self.x},{self.y})"

2.2 屏幕对象(Screen)—— 窗口的容器

Screen 对象,你可以把它理解成「物理显示器上的一个工作区」。在多显示器环境下,每个显示器对应一个 Screen。但注意,一个显示器也可以有多个 Screen(比如虚拟桌面)。

Screen 的核心职责是管理它上面的所有 Window。它维护了一个窗口列表,以及当前焦点窗口、布局策略等信息。来看它的核心结构:

class Screen:
    def __init__(self, width, height, x_offset=0, y_offset=0):
        self.width = width
        self.height = height
        self.x_offset = x_offset  # 在多显示器中的偏移
        self.y_offset = y_offset
        self.windows = []         # 当前屏幕上的窗口列表
        self.focused_window = None
        self.layout = Layout()    # 布局管理器,比如 Tile、MonadTall

    def add_window(self, window):
        """添加窗口到屏幕"""
        self.windows.append(window)
        # 嗯,这里要注意:新窗口默认获得焦点
        self.focus_window(window)

    def remove_window(self, window):
        """从屏幕移除窗口"""
        self.windows.remove(window)
        if self.focused_window == window:
            # 如果移除的是焦点窗口,需要重新分配焦点
            self.focused_window = self.windows[0] if self.windows else None

    def focus_window(self, window):
        """设置焦点窗口"""
        if window in self.windows:
            self.focused_window = window
            window.focus()

Screen 对象还有一个关键属性——布局(Layout)。布局决定了窗口在屏幕上的排列方式。比如 Tile 布局会把屏幕分成主区域和堆叠区域。我建议你把 Layout 单独抽成一个类,这样 Screen 的职责就清晰了。

避坑指南:我曾经在 Screen 的 remove_window 方法里直接调用了 window.destroy(),结果导致其他引用该窗口的地方崩溃。记住:Screen 只负责管理窗口的「存在」,不负责窗口的「生命周期」。销毁窗口是 Display 的事。

2.3 显示对象(Display)—— 一切的总管

Display 对象,是整个 WMS 的「大脑」。它管理着所有 Screen,以及全局的配置、事件循环、与 X11/Wayland 服务器的通信。说白了,没有 Display,Window 和 Screen 就是一堆死数据。

来看 Display 的核心结构:

class Display:
    def __init__(self, config):
        self.config = config
        self.screens = []          # 所有屏幕对象
        self._event_loop = None
        self._backend = None       # 与窗口系统的接口(X11/Wayland)

    def add_screen(self, screen):
        """添加一个屏幕"""
        self.screens.append(screen)
        # 我习惯在添加屏幕时自动分配一个 ID
        screen.id = len(self.screens) - 1

    def run(self):
        """启动事件循环"""
        self._event_loop = EventLoop(self)
        self._event_loop.start()

    def handle_event(self, event):
        """处理来自窗口系统的事件"""
        # 事件类型:按键、鼠标移动、窗口创建/销毁等
        if event.type == EventType.WINDOW_CREATED:
            # 创建 Window 对象,并分配到当前焦点屏幕
            window = Window(
                x=event.x, y=event.y,
                width=event.width, height=event.height,
                wm_class=event.wm_class
            )
            current_screen = self.get_focused_screen()
            current_screen.add_window(window)
        elif event.type == EventType.WINDOW_DESTROYED:
            # 找到对应的 Window 并移除
            self._destroy_window(event.window_id)

Display 还负责管理全局的快捷键绑定、鼠标行为、以及多显示器之间的窗口迁移。你想想看,如果用户按了 Super+Shift+Right,想把窗口移到右边显示器,这个逻辑就得在 Display 里实现。

方法 说明
get_focused_screen() 返回当前获得焦点的 Screen
move_window_to_screen(window, screen) 将窗口从一个屏幕移到另一个屏幕
toggle_fullscreen(window) 切换窗口的全屏状态
quit() 优雅退出 WMS,清理所有资源

核心思想:Display 是「上帝视角」,它知道一切。Screen 是「区域视角」,它只知道自己的窗口。Window 是「个体视角」,它只知道自己。

2.4 三者如何协作?

好,现在咱们把这三者串起来。假设用户打开了一个新终端窗口,整个流程是这样的:

  1. Display 从 X11/Wayland 收到 WINDOW_CREATED 事件。
  2. Display 创建 Window 对象,并把它分配给当前焦点所在的 Screen
  3. ScreenWindow 加入自己的 windows 列表,并调用布局管理器重新排列所有窗口。
  4. Screen 把新窗口设为焦点,调用 window.focus()
  5. Display 通知后端更新屏幕显示。

你看,这个链条非常清晰。每个对象只做自己分内的事,不越界。我在项目中遇到过有人把「窗口移动」的逻辑写在 Window 类里,结果导致 Screen 和 Display 都得去调用 Window 的方法,耦合度极高。后来我重构时,把所有「跨窗口操作」都放到了 Display 里,世界清净了。

我的建议:在写 WMS 源码时,可以遵循一个原则——「谁拥有数据,谁负责操作」。Window 拥有自己的位置数据,所以 moveresize 放在 Window 里。Screen 拥有窗口列表,所以 add_windowremove_window 放在 Screen 里。Display 拥有全局状态,所以事件处理和屏幕切换放在 Display 里。

2.5 总结

嗯,这一章的内容就到这里。核心就三句话:

  • Window 是「个体」,描述一个窗口的状态。
  • Screen 是「容器」,管理一组窗口的排列和焦点。
  • Display 是「总管」,协调所有 Screen 和与窗口系统的通信。

下一章,我会带你深入布局算法的实现。到时候你会看到,Screen 里的 layout 属性是怎么把窗口排得整整齐齐的。咱们下章见。