2、核心数据结构:窗口对象(Window)、屏幕对象(Screen)、显示对象(Display)的源码结构
好,咱们直接进入正题。WMS 里最核心的三个数据结构,说白了就是 Window、Screen、Display。这三兄弟撑起了整个窗口管理系统的骨架。我个人习惯把这三者比作「演员、舞台、剧场」——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 三者如何协作?
好,现在咱们把这三者串起来。假设用户打开了一个新终端窗口,整个流程是这样的:
- Display 从 X11/Wayland 收到
WINDOW_CREATED事件。 - Display 创建 Window 对象,并把它分配给当前焦点所在的 Screen。
- Screen 把 Window 加入自己的
windows列表,并调用布局管理器重新排列所有窗口。 - Screen 把新窗口设为焦点,调用
window.focus()。 - Display 通知后端更新屏幕显示。
你看,这个链条非常清晰。每个对象只做自己分内的事,不越界。我在项目中遇到过有人把「窗口移动」的逻辑写在 Window 类里,结果导致 Screen 和 Display 都得去调用 Window 的方法,耦合度极高。后来我重构时,把所有「跨窗口操作」都放到了 Display 里,世界清净了。
我的建议:在写 WMS 源码时,可以遵循一个原则——「谁拥有数据,谁负责操作」。Window 拥有自己的位置数据,所以 move 和 resize 放在 Window 里。Screen 拥有窗口列表,所以 add_window 和 remove_window 放在 Screen 里。Display 拥有全局状态,所以事件处理和屏幕切换放在 Display 里。
2.5 总结
嗯,这一章的内容就到这里。核心就三句话:
- Window 是「个体」,描述一个窗口的状态。
- Screen 是「容器」,管理一组窗口的排列和焦点。
- Display 是「总管」,协调所有 Screen 和与窗口系统的通信。
下一章,我会带你深入布局算法的实现。到时候你会看到,Screen 里的 layout 属性是怎么把窗口排得整整齐齐的。咱们下章见。