機(jī)制完全指南:EWMH 分層規(guī)則、移動(dòng)命令與配置實(shí)踐)
桌面應(yīng)用操作系統(tǒng)【免費(fèi)下載鏈接】qtile:cookie: A full-featured, hackable tiling window manager written and configured in Python (X11 Wayland)項(xiàng)目地址https://gitcode.com/gh_mirrors/qt/qtile點(diǎn)擊查看免費(fèi)下載導(dǎo)讀Qtile 是一款用 Python 編寫和配置的可平鋪窗口管理器支持 X11 與 Wayland 兩大后端。在多個(gè)窗口同時(shí)存在的桌面上誰顯示在最上面、誰被壓在下面由窗口的**堆疊順序stacking order**決定——這也是move_up()、move_down()、bring_to_front()等一系列窗口命令的底層語義所在。本文以官方文檔 docs/manual/stacking.rst 為核心結(jié)合 X11 后端源碼與測試用例系統(tǒng)講解 Qtile 如何遵循 EWMHExtended Window Manager Hints規(guī)范實(shí)現(xiàn)分層堆疊、各命令的確切行為邊界以及bring_front_click、floats_kept_above等配置項(xiàng)在實(shí)際使用中容易踩到的坑。讀完本文你將能準(zhǔn)確預(yù)判任意窗口在 Qtile 中的堆疊位置并寫出不會(huì)把 Dock 類應(yīng)用頂出屏幕的可靠配置。一、堆疊順序問題的由來窗口管理器中存在兩種順序容易混淆一種是布局層面的幾何順序窗口在屏幕上占據(jù)的位置另一種就是本文討論的z 軸堆疊順序窗口之間的前后遮擋關(guān)系。Qtile 中有若干窗口命令與堆疊順序直接相關(guān)包括move_up()、move_down()、move_to_top()、move_to_bottom()以及bring_to_front()等。在 X11 生態(tài)中桌面環(huán)境與窗口管理器之間通過 EWMH 規(guī)范約定堆疊規(guī)則以保證互操作性。Qtile 的 X11 后端在實(shí)現(xiàn)堆疊時(shí)盡量貼合 EWMH 規(guī)范同時(shí)又在規(guī)范之外增加了一個(gè)自己的分層Scratchpad 層。理解這套規(guī)則是正確使用上述命令的前提。注意官方文檔明確指出堆疊分層目前只在 X11 后端完整實(shí)現(xiàn)Wayland 后端計(jì)劃在未來補(bǔ)充完整支持文檔會(huì)隨之更新。從當(dāng)前倉庫源碼看Wayland 后端已提供move_to_top、move_up、move_down、move_to_bottom、bring_to_front等命令的底層實(shí)現(xiàn)見 libqtile/backend/wayland/window.py但 X11 下按 EWMH 分層優(yōu)先級(jí)重排的完整語義仍是本文討論的主體。二、分層優(yōu)先級(jí)組從底部到頂部的完整規(guī)則Qtile 將窗口劃分為若干分層優(yōu)先級(jí)組layer priority groups。窗口從下往上按照以下優(yōu)先級(jí)依次堆疊規(guī)則本身來自 EWMH 規(guī)范建議的分層堆疊順序優(yōu)先級(jí)自下而上窗口類型 / 狀態(tài)第 1 層最底層類型為_NET_WM_TYPE_DESKTOP的窗口第 2 層具有_NET_WM_STATE_BELOW狀態(tài)的窗口第 3 層默認(rèn)層不屬于其他任何層的窗口第 4 層類型為_NET_WM_TYPE_DOCK的窗口除非它們同時(shí)帶有_NET_WM_STATE_BELOW狀態(tài)以及具有_NET_WM_STATE_ABOVE狀態(tài)的窗口第 5 層最高層具有_NET_WM_STATE_FULLSCREEN狀態(tài)且正處于焦點(diǎn)的窗口Qtile 在此基礎(chǔ)上額外增加了一個(gè)層ScratchPad臨時(shí)彈出窗格中的窗口被放置在所有其他窗口之上且始終如此。源碼實(shí)現(xiàn)印證get_layering_information()上述規(guī)則在 X11 后端中由 libqtile/backend/x11/window.py 的get_layering_information()方法實(shí)現(xiàn)。其核心邏輯是讀取窗口的_NET_WM_STATE屬性與 WM_TYPE并組裝成一個(gè)六元組布爾狀態(tài)state self.window.get_net_wm_state() _type self.window.get_wm_type() or # 檢查該窗口是否當(dāng)前聚焦讀取 _NET_ACTIVE_WINDOW active_window self.qtile.core._root.get_property( _NET_ACTIVE_WINDOW, WINDOW, unpackint ) 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) # 從底到頂排序各標(biāo)記 states [desktop, below, above or (dock and not below), full and focus, is_scratchpad] other not any(states) states.insert(2, other)這段代碼有幾處值得注意的細(xì)節(jié)full and focus全屏窗口只有處于聚焦?fàn)顟B(tài)時(shí)才占據(jù)最高層。換句話說非聚焦的全屏窗口并不會(huì)被強(qiáng)制抬到最頂層——這與 EWMH focused windows having state_NET_WM_STATE_FULLSCREEN 的建議完全一致。above or (dock and not below)Dock面板/停靠欄類型窗口只有在沒有BELOW狀態(tài)時(shí)才進(jìn)入第 4 層若 Dock 同時(shí)聲明了_NET_WM_STATE_BELOW則它會(huì)被壓到第 2 層。is_scratchpad通過判斷窗口所屬組是否為ScratchPad實(shí)例isinstance(self.qtile.groups_map.get(self.group), ScratchPad)來識(shí)別 Scratchpad 窗口并把它放到 Qtile 自定義的最高層。隨后代碼會(huì)找出狀態(tài)為True的最高層級(jí)并把其他層位置設(shè)為False從而得出該窗口當(dāng)前的層位置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則基于這個(gè)層位置把窗口區(qū)分為比自己低的層 / 更高的層 / 同一層并據(jù)此計(jì)算出新的堆疊位置最終通過 X 協(xié)議調(diào)整窗口在服務(wù)器堆棧中的順序。默認(rèn)層與floats_kept_above**平鋪窗口tiled windows**默認(rèn)打開在第 3 層即不屬于其他任何層的窗口這一普通層。而對(duì)于浮動(dòng)窗口Qtile 提供了一個(gè)關(guān)鍵配置項(xiàng)floats_kept_above: bool當(dāng)配置中floats_kept_above True這也是默認(rèn)值見 libqtile/resources/default_config.py時(shí)新打開的浮動(dòng)窗口會(huì)自動(dòng)帶上_NET_WM_STATE_ABOVE狀態(tài)從而確保它們始終顯示在平鋪窗口之上。該狀態(tài)在第 4 層生效因此浮動(dòng)窗口不會(huì)被平鋪窗口遮擋也不會(huì)與 Dock 搶到更高的層級(jí)。從源碼看X11 后端在 libqtile/backend/x11/window.py 處理窗口狀態(tài)時(shí)會(huì)依據(jù)floats_kept_above配置決定是否為浮動(dòng)窗口添加/保留kept_above屬性Wayland 后端也在 libqtile/backend/wayland/window.py 中對(duì)浮動(dòng)窗口的 layer 位置做了相應(yīng)處理。這一配置項(xiàng)在 libqtile/confreader.py 中被聲明為bool類型。三、移動(dòng)窗口move_up/move_down/move_to_top/move_to_bottom官方文檔示例同一層內(nèi)的移動(dòng)假設(shè)有四個(gè)平鋪窗口自頂部向底部堆疊如下One Two Three Four對(duì)窗口 Four 調(diào)用move_up()后結(jié)果變?yōu)镺ne Two Four Three即 Four 與它上方的 Three 交換了 z 軸位置。此時(shí)再對(duì)窗口 Three 調(diào)用move_to_top()結(jié)果變?yōu)門hree One Two FourThree 被提升到當(dāng)前層這里是普通平鋪層的最頂部。注意區(qū)分兩者move_up()/move_down()沿 z 軸逐位移動(dòng)一格與相鄰窗口交換位置move_to_top()/move_to_bottom()移動(dòng)到當(dāng)前所在層的頂部/底部而不是無視分層直接置頂或置底。分層約束普通窗口不能越過釘住的窗口從后端抽象接口的文檔注釋libqtile/backend/base/window.py可以確認(rèn)這些命令的分層約束move_up()不會(huì)把一個(gè)普通窗口即沒有kept_above/kept_below標(biāo)記的窗口抬升到帶有kept_above標(biāo)記的窗口之上move_down()同理不會(huì)把普通窗口降到帶有kept_below標(biāo)記的窗口之下這兩個(gè)命令都帶有一個(gè)force參數(shù)當(dāng)forceTrue時(shí)可以突破上述約束例如把kept_below的窗口強(qiáng)制上移或把kept_above的窗口強(qiáng)制下移move_to_top()只在當(dāng)前層內(nèi)部生效例如三個(gè)都設(shè)置了keep_above的窗口調(diào)用move_to_top()只會(huì)把它提升到這三個(gè)窗口的最上面而不會(huì)抬到其他層之上。X11 后端的實(shí)現(xiàn)libqtile/backend/x11/window.py清晰地體現(xiàn)了這些約束def move_up(self, forceFalse): if self.kept_below and force: self.kept_below False with self.qtile.core.masked(): # 屏蔽掩碼避免沿 z 軸移動(dòng)窗口時(shí)觸發(fā)焦點(diǎn)變化事件例如 follow_mouse_focus self.change_layer() def move_down(self, forceFalse): if self.kept_above and force: self.kept_above False with self.qtile.core.masked(): self.change_layer(upFalse) def move_to_top(self, forceFalse): if self.kept_below and force: self.kept_below False with self.qtile.core.masked(): self.change_layer(top_bottomTrue) def move_to_bottom(self, forceFalse): if self.kept_above and force: self.kept_above False with self.qtile.core.masked(): self.change_layer(upFalse, top_bottomTrue)實(shí)現(xiàn)細(xì)節(jié)值得展開說明force的真正含義它并不是強(qiáng)制越過分層而是先把kept_below/kept_above狀態(tài)移除再執(zhí)行移動(dòng)。這樣窗口就回到了普通層自然可以被移動(dòng)了。masked()上下文change_layer會(huì)通過 X 協(xié)議調(diào)整窗口棧并可能觸發(fā)焦點(diǎn)事件。在with self.qtile.core.masked():中執(zhí)行可以屏蔽這些事件避免因follow_mouse_focus等選項(xiàng)導(dǎo)致移動(dòng) z 軸位置的同時(shí)意外改變焦點(diǎn)。change_layer的父窗口保護(hù)在change_layer內(nèi)部源碼注釋明確列出如果一個(gè)窗口有父窗口transient parent則不應(yīng)被移動(dòng)到父窗口之下否則子窗口被遮擋可能讓應(yīng)用處于不可用狀態(tài)有子窗口時(shí)子窗口應(yīng)跟隨一起移動(dòng)有父窗口時(shí)要么移動(dòng)父窗口、要么什么都不做。此外如果窗口是模態(tài)對(duì)話框_NET_WM_STATE_MODAL它會(huì)優(yōu)先被放置在同組其他窗口之上。測試用例驗(yàn)證分層與命令行為倉庫中的 test/test_window.py 用五個(gè)窗口 one ~ five 構(gòu)造了包含BELOW、普通層、ABOVE三層的場景并逐條斷言了move_up、move_down、keep_above、keep_below、move_to_top、move_to_bottom的組合結(jié)果。幾個(gè)有代表性的斷言# 新窗口依次打開時(shí)后打開的在上面 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 進(jìn)入 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]這些測試從側(cè)面印證了文檔所述的分層規(guī)則與同一層內(nèi)移動(dòng)的語義是理解堆疊行為最直觀的可運(yùn)行文檔。keep_above/keep_below改變窗口所在層與移動(dòng)命令配套的是兩個(gè)改層命令keep_above()與keep_below()。它們本質(zhì)上是讀寫窗口的_NET_WM_STATE_ABOVE/_NET_WM_STATE_BELOW原子屬性。從 X11 后端實(shí)現(xiàn)libqtile/backend/x11/window.py可以看到kept_above屬性的 getter 檢查窗口的_NET_WM_STATE屬性中是否包含_NET_WM_STATE_ABOVE原子setter 在寫入ABOVE的同時(shí)會(huì)移除BELOW原子反之亦然保證兩個(gè)狀態(tài)互斥寫入屬性后立即調(diào)用change_layer()讓窗口進(jìn)入新的層級(jí)。因此在 Qtile 中keep_above()會(huì)把窗口釘?shù)降?4 層keep_below()則會(huì)把窗口壓到第 2 層move_up/move_down默認(rèn)無法跨越這些被釘住的窗口。四、bring_to_front()的特殊行為與bring_front_click的坑無視分層的置頂與失焦后歸位在所有堆疊命令中bring_to_front()行為最為特殊它會(huì)把窗口帶到整個(gè)堆疊的最頂部完全無視上面列出的所有分層優(yōu)先級(jí)規(guī)則。而當(dāng)該窗口失去焦點(diǎn)后Qtile 會(huì)把它重新堆疊到符合規(guī)則的位置。X11 后端實(shí)現(xiàn)libqtile/backend/x11/window.pydef bring_to_front(self): if self.get_wm_type() ! desktop: self.window.configure(stackmodexcffib.xproto.StackMode.Above) self.raise_children() self.qtile.core.update_client_lists()可以看到它直接通過 X 協(xié)議把窗口configure到StackMode.Above即無條件置于堆頂桌面類型窗口除外并會(huì)順帶提升其子窗口、更新客戶端列表。常見陷阱bring_front_click True會(huì)頂?shù)?Dock這種無條件置頂 失焦歸位的行為在特定配置下會(huì)產(chǎn)生不良結(jié)果。Qtile 默認(rèn)在鼠標(biāo)點(diǎn)擊窗口時(shí)聚焦窗口focus_on_window_activation之外的點(diǎn)擊聚焦邏輯而點(diǎn)擊聚焦是否連帶bring_to_front由配置項(xiàng)bring_front_click控制bring_front_click: bool | Literal[floating_only]默認(rèn)值False見 libqtile/resources/default_config.py若設(shè)為True點(diǎn)擊任意窗口都會(huì)把它無條件置頂若設(shè)為floating_only僅對(duì)浮動(dòng)窗口執(zhí)行置頂。官方文檔特別提醒了一個(gè)真實(shí)場景如果配置了bring_front_click True而用戶有一個(gè)靠鼠標(biāo)懸停mousing over來激活的 Dock 類應(yīng)用那么在平鋪窗口上點(diǎn)擊時(shí)這些窗口會(huì)被抬到 Dock 之上并遮擋住 Dock導(dǎo)致 Dock 難以再被激活——因?yàn)?Dock 一旦失去焦點(diǎn)被抬起的平鋪窗口在重新堆疊之前已經(jīng)擋住了它。解決辦法有兩種將bring_front_click設(shè)為False徹底禁用點(diǎn)擊置頂行為設(shè)為floating_only讓該行為只作用于浮動(dòng)窗口平鋪窗口不會(huì)越過 Dock。兩個(gè)后端的處理路徑bring_front_click在點(diǎn)擊聚焦路徑中生效。X11 后端的處理位于 libqtile/backend/x11/core.pyif qtile.config.bring_front_click and ( qtile.config.bring_front_click ! floating_only or getattr(window, floating, False) ): window.bring_to_front()Wayland 后端的對(duì)應(yīng)邏輯位于 libqtile/backend/wayland/core.pyif 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時(shí)對(duì)一切窗口置頂floating_only時(shí)僅對(duì)浮動(dòng)窗口置頂。這也再次說明bring_front_click是跨后端生效的通用配置而EWMH 分層重排則主要在 X11 后端實(shí)現(xiàn)。五、實(shí)踐建議如何配置出一個(gè)可靠的堆疊體驗(yàn)綜合文檔與源碼給出以下可直接落地的配置建議保持默認(rèn)的bring_front_click False除非你明確需要點(diǎn)擊即置頂?shù)男袨?。如果你的工作流依?Dock/面板類應(yīng)用務(wù)必避免True導(dǎo)致的遮擋問題折中選擇floating_only可以兼顧浮動(dòng)窗口點(diǎn)擊置頂與Dock 不被平鋪窗口遮擋。保留floats_kept_above True默認(rèn)值這保證對(duì)話框、浮動(dòng)工具窗始終浮在平鋪窗口之上是 Qtile 默認(rèn)體驗(yàn)的一部分。若你的浮動(dòng)窗口需要與平鋪窗口混排例如希望浮動(dòng)窗口也能被平鋪窗口壓住再改為False。善用keep_above/keep_below而非頻繁bring_to_front需要長期置頂某個(gè)窗口如監(jiān)控面板時(shí)用keep_above()讓它進(jìn)入穩(wěn)定的第 4 層而不是每次用bring_to_front()臨時(shí)置頂后又失焦歸位。在配置中綁定堆疊快捷鍵時(shí)區(qū)分意圖move_up/move_down用于同層微調(diào)如交換兩個(gè)平鋪窗口的前后位置move_to_top/move_to_bottom用于層內(nèi)歸位bring_to_front用于臨時(shí)置頂。它們的效果差異在前文示例與測試斷言中一目了然。記住層約束是單向的普通窗口無法越過kept_above/kept_below窗口若確實(shí)需要強(qiáng)制穿越可對(duì)對(duì)應(yīng)命令使用forceTrue該參數(shù)會(huì)先移除窗口的釘住狀態(tài)再移動(dòng)。六、后端支持現(xiàn)狀與未來當(dāng)前倉庫中X11 后端通過get_layering_information()change_layer()完整實(shí)現(xiàn)了 EWMH 分層堆疊與 Scratchpad 頂層Wayland 后端則已經(jīng)提供了move_to_top/move_up/move_down/move_to_bottom/bring_to_front等命令的底層入口基于lib.qw_view_*原生函數(shù)與LAYER_BRINGTOFRONT層見 libqtile/backend/wayland/window.py??梢酝茢郬ayland 后端正在逐步補(bǔ)齊堆疊能力未來官方文檔的僅 X11 實(shí)現(xiàn)說明會(huì)隨進(jìn)展更新。在使用 Wayland 后端時(shí)建議以上述源碼現(xiàn)狀為準(zhǔn)并留意官方文檔的更新說明??偨Y(jié)Qtile 的窗口堆疊并非簡單的后打開的在上面而是一套完整的分層體系從底部的桌面窗口、BELOW 窗口、普通窗口到上方的 Dock / ABOVE 窗口、聚焦全屏窗口再到 Qtile 自定義的 Scratchpad 頂層。move_up/move_down在同一層內(nèi)逐位交換move_to_top/move_to_bottom在層內(nèi)歸位keep_above/keep_below改變窗口所在層而bring_to_front則是唯一無視分層、臨時(shí)置頂?shù)奶乩?。理解這套規(guī)則配合bring_front_click與floats_kept_above兩個(gè)配置項(xiàng)你就能精確控制任何窗口的前后關(guān)系避免 Dock 被遮擋之類的經(jīng)典問題。贊分享桌面應(yīng)用操作系統(tǒng)【免費(fèi)下載鏈接】qtile:cookie: A full-featured, hackable tiling window manager written and configured in Python (X11 Wayland)項(xiàng)目地址https://gitcode.com/gh_mirrors/qt/qtile點(diǎn)擊查看免費(fèi)下載相關(guān)推薦komorebi 窗口堆疊Stacking完全指南stack / unstack / cycle-stack 等命令的實(shí)戰(zhàn)用法與底層實(shí)現(xiàn)komorebi 窗口堆疊Stacking完全指南stack / unstack / cycle stack 等命令的實(shí)戰(zhàn)用法與底層實(shí)現(xiàn) 堆疊Stack桌面應(yīng)用Qtile 窗口匹配Match完全指南用規(guī)則精準(zhǔn)控制浮動(dòng)、分組與布局Qtile 窗口匹配Match完全指南用規(guī)則精準(zhǔn)控制浮動(dòng)、分組與布局 Qtile 的窗口管理行為并非千篇一律——通過 Match 對(duì)象你可以為哪些窗口桌面應(yīng)用操作系統(tǒng)Qtile 窗口命令 API 全指南在命令圖中控制浮動(dòng)、全屏、透明度與 Z 軸圖層Qtile 窗口命令 API 全指南在命令圖中控制浮動(dòng)、全屏、透明度與 Z 軸圖層 導(dǎo)讀 在 Qtile 中窗口的尺寸與位置通常由當(dāng)前布局layout決桌面應(yīng)用操作系統(tǒng)上一篇rsuite Divider 分割線組件外觀Appearance指南從 Solid、Dashed 到 Dotted 的完整配置與源碼解析下一篇基準(zhǔn)對(duì)決LensVLM-9B相比基座Qwen3.5-9B在長文檔理解上究竟強(qiáng)了多少創(chuàng)作聲明:本文部分內(nèi)容由AI輔助生成(AIGC),僅供參考