☰
Qtile 窗口堆叠(Window Stacking)机制完全指南:EWMH 分层规则、移动命令与配置实践
2026/10/13 9:31:45 网站建设 项目流程
  • 桌面应用
  • 操作系统

【免费下载链接】qtile

:cookie: A full-featured, hackable tiling window manager written and configured in Python (X11 + Wayland)

项目地址:https://gitcode.com/gh_mirrors/qt/qtile
点击查看免费下载

导读

Qtile 是一款用 Python 编写和配置的可平铺窗口管理器,支持 X11 与 Wayland 两大后端。在多个窗口同时存在的桌面上,"谁显示在最上面、谁被压在下面"由窗口的**堆叠顺序(stacking order)**决定——这也是move_up()、move_down()、bring_to_front()等一系列窗口命令的底层语义所在。本文以官方文档 docs/manual/stacking.rst 为核心,结合 X11 后端源码与测试用例,系统讲解 Qtile 如何遵循 EWMH(Extended Window Manager Hints)规范实现分层堆叠、各命令的确切行为边界,以及bring_front_click、floats_kept_above等配置项在实际使用中容易踩到的坑。读完本文,你将能准确预判任意窗口在 Qtile 中的堆叠位置,并写出不会把 Dock 类应用"顶出屏幕"的可靠配置。

一、堆叠顺序问题的由来

窗口管理器中存在两种"顺序"容易混淆:一种是布局层面的几何顺序(窗口在屏幕上占据的位置),另一种就是本文讨论的z 轴堆叠顺序(窗口之间的前后遮挡关系)。Qtile 中有若干窗口命令与堆叠顺序直接相关,包括move_up()、move_down()、move_to_top()、move_to_bottom()以及bring_to_front()等。

在 X11 生态中,桌面环境与窗口管理器之间通过 EWMH 规范约定堆叠规则,以保证互操作性。Qtile 的 X11 后端在实现堆叠时"尽量贴合 EWMH 规范",同时又在规范之外增加了一个自己的分层(Scratchpad 层)。理解这套规则,是正确使用上述命令的前提。

注意:官方文档明确指出,堆叠分层目前只在 X11 后端完整实现;Wayland 后端计划在未来补充完整支持,文档会随之更新。从当前仓库源码看,Wayland 后端已提供move_to_top、move_up、move_down、move_to_bottom、bring_to_front等命令的底层实现(见 libqtile/backend/wayland/window.py),但 X11 下"按 EWMH 分层优先级重排"的完整语义仍是本文讨论的主体。

二、分层优先级组:从底部到顶部的完整规则

Qtile 将窗口划分为若干分层优先级组(layer priority groups)。窗口从下往上,按照以下优先级依次堆叠(规则本身来自 EWMH 规范建议的分层堆叠顺序):

优先级(自下而上)窗口类型 / 状态
第 1 层(最底层)类型为_NET_WM_TYPE_DESKTOP的窗口
第 2 层具有_NET_WM_STATE_BELOW状态的窗口
第 3 层(默认层)不属于其他任何层的窗口
第 4 层类型为_NET_WM_TYPE_DOCK的窗口(除非它们同时带有_NET_WM_STATE_BELOW状态),以及具有_NET_WM_STATE_ABOVE状态的窗口
第 5 层(最高层)具有_NET_WM_STATE_FULLSCREEN状态且正处于焦点的窗口

Qtile 在此基础上额外增加了一个层:ScratchPad(临时弹出窗格)中的窗口被放置在所有其他窗口之上,且始终如此。

源码实现印证:get_layering_information()

上述规则在 X11 后端中由 libqtile/backend/x11/window.py 的get_layering_information()方法实现。其核心逻辑是读取窗口的_NET_WM_STATE属性与 WM_TYPE,并组装成一个六元组布尔状态:

state = self.window.get_net_wm_state() _type = self.window.get_wm_type() or "" # 检查该窗口是否当前聚焦(读取 _NET_ACTIVE_WINDOW) active_window = self.qtile.core._root.get_property( "_NET_ACTIVE_WINDOW", "WINDOW", unpack=int ) if active_window and active_window[0] == self.window.wid: focus = True desktop = _type == "desktop" below = "_NET_WM_STATE_BELOW" in state dock = _type == "dock" above = "_NET_WM_STATE_ABOVE" in state full = "fullscreen" in state is_scratchpad = isinstance(self.qtile.groups_map.get(self.group), ScratchPad) # 从底到顶排序各标记 states = [desktop, below, above or (dock and not below), full and focus, is_scratchpad] other = not any(states) states.insert(2, other)

这段代码有几处值得注意的细节:

  • full and focus:全屏窗口只有处于聚焦状态时才占据最高层。换句话说,非聚焦的全屏窗口并不会被强制抬到最顶层——这与 EWMH "focused windows having state_NET_WM_STATE_FULLSCREEN" 的建议完全一致。
  • above or (dock and not below):Dock(面板/停靠栏)类型窗口只有在没有BELOW状态时才进入第 4 层;若 Dock 同时声明了_NET_WM_STATE_BELOW,则它会被压到第 2 层。
  • is_scratchpad:通过判断窗口所属组是否为ScratchPad实例(isinstance(self.qtile.groups_map.get(self.group), ScratchPad))来识别 Scratchpad 窗口,并把它放到 Qtile 自定义的最高层。

随后代码会找出状态为True的最高层级并把其他层位置设为False,从而得出该窗口当前的"层位置":

highest = max(i for i, state in enumerate(states) if state) return tuple(i == highest for i in range(6))

change_layer()(libqtile/backend/x11/window.py)则基于这个层位置,把窗口区分为"比自己低的层 / 更高的层 / 同一层",并据此计算出新的堆叠位置,最终通过 X 协议调整窗口在服务器堆栈中的顺序。

默认层与floats_kept_above

**平铺窗口(tiled windows)**默认打开在第 3 层,即"不属于其他任何层的窗口"这一普通层。

而对于浮动窗口,Qtile 提供了一个关键配置项:

floats_kept_above: bool

当配置中floats_kept_above = True(这也是默认值,见 libqtile/resources/default_config.py)时,新打开的浮动窗口会自动带上_NET_WM_STATE_ABOVE状态,从而确保它们始终显示在平铺窗口之上。该状态在第 4 层生效,因此浮动窗口不会被平铺窗口遮挡,也不会与 Dock 抢到更高的层级。

从源码看,X11 后端在 libqtile/backend/x11/window.py 处理窗口状态时,会依据floats_kept_above配置决定是否为浮动窗口添加/保留kept_above属性;Wayland 后端也在 libqtile/backend/wayland/window.py 中对浮动窗口的 layer 位置做了相应处理。这一配置项在 libqtile/confreader.py 中被声明为bool类型。

三、移动窗口:move_up/move_down/move_to_top/move_to_bottom

官方文档示例:同一层内的移动

假设有四个平铺窗口,自顶部向底部堆叠如下:

"One" "Two" "Three" "Four"

对窗口 "Four" 调用move_up()后,结果变为:

"One" "Two" "Four" "Three"

即 "Four" 与它上方的 "Three" 交换了 z 轴位置。此时再对窗口 "Three" 调用move_to_top(),结果变为:

"Three" "One" "Two" "Four"

"Three" 被提升到当前层(这里是普通平铺层)的最顶部。

注意区分两者:

  • move_up()/move_down():沿 z 轴逐位移动一格,与相邻窗口交换位置;
  • move_to_top()/move_to_bottom():移动到当前所在层的顶部/底部,而不是无视分层直接置顶或置底。

分层约束:普通窗口不能越过"钉住"的窗口

从后端抽象接口的文档注释(libqtile/backend/base/window.py)可以确认这些命令的分层约束:

  • move_up()不会把一个普通窗口(即没有kept_above/kept_below标记的窗口)抬升到带有kept_above标记的窗口之上;
  • move_down()同理,不会把普通窗口降到带有kept_below标记的窗口之下;
  • 这两个命令都带有一个force参数:当force=True时,可以突破上述约束(例如把kept_below的窗口强制上移,或把kept_above的窗口强制下移);
  • move_to_top()只在当前层内部生效:例如三个都设置了keep_above的窗口,调用move_to_top()只会把它提升到这三个窗口的最上面,而不会抬到其他层之上。

X11 后端的实现(libqtile/backend/x11/window.py)清晰地体现了这些约束:

def move_up(self, force=False): if self.kept_below and force: self.kept_below = False with self.qtile.core.masked(): # 屏蔽掩码,避免沿 z 轴移动窗口时触发焦点变化事件(例如 follow_mouse_focus) self.change_layer() def move_down(self, force=False): if self.kept_above and force: self.kept_above = False with self.qtile.core.masked(): self.change_layer(up=False) def move_to_top(self, force=False): if self.kept_below and force: self.kept_below = False with self.qtile.core.masked(): self.change_layer(top_bottom=True) def move_to_bottom(self, force=False): if self.kept_above and force: self.kept_above = False with self.qtile.core.masked(): self.change_layer(up=False, top_bottom=True)

实现细节值得展开说明:

  1. force的真正含义:它并不是"强制越过分层",而是"先把kept_below/kept_above状态移除,再执行移动"。这样窗口就回到了普通层,自然可以被移动了。
  2. masked()上下文:change_layer会通过 X 协议调整窗口栈并可能触发焦点事件。在with self.qtile.core.masked():中执行可以屏蔽这些事件,避免因follow_mouse_focus等选项导致移动 z 轴位置的同时意外改变焦点。
  3. change_layer的父窗口保护:在change_layer内部(源码注释明确列出),如果一个窗口有父窗口(transient parent),则不应被移动到父窗口之下,否则子窗口被遮挡可能让应用处于不可用状态;有子窗口时,子窗口应跟随一起移动;有父窗口时要么移动父窗口、要么什么都不做。此外,如果窗口是模态对话框(_NET_WM_STATE_MODAL),它会优先被放置在同组其他窗口之上。

测试用例验证:分层与命令行为

仓库中的 test/test_window.py 用五个窗口 "one" ~ "five" 构造了包含BELOW、普通层、ABOVE三层的场景,并逐条断言了move_up、move_down、keep_above、keep_below、move_to_top、move_to_bottom的组合结果。几个有代表性的断言:

# 新窗口依次打开时,后打开的在上面 assert _clients() == ["one", "two", "three", "four", "five"] # move_up 与相邻窗口交换 window_by_name(manager.c, "four").move_up() assert _clients() == ["two", "one", "three", "five", "four"] # keep_above 进入 ABOVE 层,位于该层顶部 window_by_name(manager.c, "two").keep_above() assert _clients() == ["one", "three", "five", "four", "two"] # kept_below 窗口的 move_to_top 无效(它仍受 BELOW 层约束) window_by_name(manager.c, "three").move_to_top() assert _clients() == ["three", "five", "one", "four", "two"] # 唯一 kept_above 窗口的 move_to_bottom 同样无效 window_by_name(manager.c, "three").move_to_bottom() assert _clients() == ["five", "one", "four", "two", "three"]

这些测试从侧面印证了文档所述的分层规则与"同一层内移动"的语义,是理解堆叠行为最直观的"可运行文档"。

keep_above/keep_below:改变窗口所在层

与移动命令配套的是两个"改层"命令:keep_above()与keep_below()。它们本质上是读写窗口的_NET_WM_STATE_ABOVE/_NET_WM_STATE_BELOW原子属性。从 X11 后端实现(libqtile/backend/x11/window.py)可以看到:

  • kept_above属性的 getter 检查窗口的_NET_WM_STATE属性中是否包含_NET_WM_STATE_ABOVE原子;
  • setter 在写入ABOVE的同时会移除BELOW原子(反之亦然),保证两个状态互斥;
  • 写入属性后立即调用change_layer()让窗口进入新的层级。

因此在 Qtile 中,keep_above()会把窗口"钉"到第 4 层,keep_below()则会把窗口压到第 2 层;move_up/move_down默认无法跨越这些被钉住的窗口。

四、bring_to_front()的特殊行为与bring_front_click的坑

无视分层的置顶与"失焦后归位"

在所有堆叠命令中,bring_to_front()行为最为特殊:它会把窗口带到整个堆叠的最顶部,完全无视上面列出的所有分层优先级规则。而当该窗口失去焦点后,Qtile 会把它重新堆叠到符合规则的位置。

X11 后端实现(libqtile/backend/x11/window.py):

def bring_to_front(self): if self.get_wm_type() != "desktop": self.window.configure(stackmode=xcffib.xproto.StackMode.Above) self.raise_children() self.qtile.core.update_client_lists()

可以看到它直接通过 X 协议把窗口configure到StackMode.Above,即无条件置于堆顶(桌面类型窗口除外),并会顺带提升其子窗口、更新客户端列表。

常见陷阱:bring_front_click = True会顶掉 Dock

这种"无条件置顶 + 失焦归位"的行为在特定配置下会产生不良结果。Qtile 默认在鼠标点击窗口时聚焦窗口(focus_on_window_activation之外的点击聚焦逻辑),而点击聚焦是否连带bring_to_front由配置项bring_front_click控制:

bring_front_click: bool | Literal["floating_only"]
  • 默认值:False(见 libqtile/resources/default_config.py);
  • 若设为True:点击任意窗口都会把它无条件置顶;
  • 若设为"floating_only":仅对浮动窗口执行置顶。

官方文档特别提醒了一个真实场景:如果配置了bring_front_click = True,而用户有一个靠鼠标悬停(mousing over)来激活的 Dock 类应用,那么在平铺窗口上点击时,这些窗口会被抬到 Dock 之上并遮挡住 Dock,导致 Dock 难以再被激活——因为 Dock 一旦失去焦点,被抬起的平铺窗口在"重新堆叠"之前已经挡住了它。

解决办法有两种:

  1. 将bring_front_click设为False,彻底禁用点击置顶行为;
  2. 设为"floating_only",让该行为只作用于浮动窗口,平铺窗口不会越过 Dock。

两个后端的处理路径

bring_front_click在点击聚焦路径中生效。X11 后端的处理位于 libqtile/backend/x11/core.py:

if qtile.config.bring_front_click and ( qtile.config.bring_front_click != "floating_only" or getattr(window, "floating", False) ): window.bring_to_front()

Wayland 后端的对应逻辑位于 libqtile/backend/wayland/core.py:

if self.qtile.config.bring_front_click is True: win.bring_to_front() elif self.qtile.config.bring_front_click == "floating_only": if isinstance(win, base.Window) and win.floating: win.bring_to_front()

两段代码语义一致:True时对一切窗口置顶;"floating_only"时仅对浮动窗口置顶。这也再次说明bring_front_click是跨后端生效的通用配置,而"EWMH 分层重排"则主要在 X11 后端实现。

五、实践建议:如何配置出一个可靠的堆叠体验

综合文档与源码,给出以下可直接落地的配置建议:

  1. 保持默认的bring_front_click = False,除非你明确需要"点击即置顶"的行为。如果你的工作流依赖 Dock/面板类应用,务必避免True导致的遮挡问题;折中选择"floating_only"可以兼顾"浮动窗口点击置顶"与"Dock 不被平铺窗口遮挡"。
  2. 保留floats_kept_above = True(默认值):这保证对话框、浮动工具窗始终浮在平铺窗口之上,是 Qtile 默认体验的一部分。若你的浮动窗口需要与平铺窗口混排(例如希望浮动窗口也能被平铺窗口压住),再改为False。
  3. 善用keep_above/keep_below而非频繁bring_to_front:需要长期置顶某个窗口(如监控面板)时,用keep_above()让它进入稳定的第 4 层,而不是每次用bring_to_front()临时置顶后又"失焦归位"。
  4. 在配置中绑定堆叠快捷键时区分意图:move_up/move_down用于同层微调(如交换两个平铺窗口的前后位置),move_to_top/move_to_bottom用于层内归位,bring_to_front用于临时置顶。它们的效果差异在前文示例与测试断言中一目了然。
  5. 记住层约束是"单向"的:普通窗口无法越过kept_above/kept_below窗口;若确实需要强制穿越,可对对应命令使用force=True(该参数会先移除窗口的钉住状态再移动)。

六、后端支持现状与未来

当前仓库中,X11 后端通过get_layering_information()+change_layer()完整实现了 EWMH 分层堆叠与 Scratchpad 顶层;Wayland 后端则已经提供了move_to_top/move_up/move_down/move_to_bottom/bring_to_front等命令的底层入口(基于lib.qw_view_*原生函数与LAYER_BRINGTOFRONT层,见 libqtile/backend/wayland/window.py)。可以推断,Wayland 后端正在逐步补齐堆叠能力,未来官方文档的"仅 X11 实现"说明会随进展更新。在使用 Wayland 后端时,建议以上述源码现状为准,并留意官方文档的更新说明。

总结

Qtile 的窗口堆叠并非简单的"后打开的在上面",而是一套完整的分层体系:从底部的桌面窗口、BELOW 窗口、普通窗口,到上方的 Dock / ABOVE 窗口、聚焦全屏窗口,再到 Qtile 自定义的 Scratchpad 顶层。move_up/move_down在同一层内逐位交换,move_to_top/move_to_bottom在层内归位,keep_above/keep_below改变窗口所在层,而bring_to_front则是唯一无视分层、临时置顶的"特例"。理解这套规则,配合bring_front_click与floats_kept_above两个配置项,你就能精确控制任何窗口的前后关系,避免 Dock 被遮挡之类的经典问题。

  • 桌面应用
  • 操作系统

【免费下载链接】qtile

:cookie: A full-featured, hackable tiling window manager written and configured in Python (X11 + Wayland)

项目地址:https://gitcode.com/gh_mirrors/qt/qtile
点击查看免费下载

相关推荐

上一篇:rsuite Divider 分割线组件外观(Appearance)指南:从 Solid、Dashed 到 Dotted 的完整配置与源码解析
下一篇:基准对决:LensVLM-9B相比基座Qwen3.5-9B在长文档理解上究竟强了多少?

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询