如何用PyQt6实现兼容i3wm的独占空间置顶任务栏
我想实现一个类似Polybar或i3bar的极简任务栏,需要满足以下要求:
- 拥有专属空间,不会和其他窗口重叠;
- 始终显示在屏幕最顶层;
- 以上特性在i3wm中同样有效。
目前已经实现了任务栏始终置顶,但它现在就像个浮动窗口,其他窗口不会自动避开它的专属空间,具体效果见截图。
以下是生成当前效果的代码:
import sys from PyQt6.QtWidgets import QApplication, QWidget, QHBoxLayout, QLabel from PyQt6.QtCore import Qt, QRect from PyQt6.QtGui import QGuiApplication from Xlib import display, Xatom, X class StatusBar(QWidget): def __init__(self): super().__init__() # 1. 设置高度并获取屏幕宽度 self.setFixedHeight(58) screen_geometry = QGuiApplication.primaryScreen().geometry() self.setGeometry(QRect(0, 0, screen_geometry.width(), 58)) # 2. 样式设置 self.setStyleSheet("background-color: black; color: white;") # 3. 布局设置 layout = QHBoxLayout() layout.addWidget(QLabel("Test")) layout.setContentsMargins(10, 0, 10, 0) self.setLayout(layout) # 4. 窗口标志(无装饰、始终置顶) self.setWindowFlags( Qt.WindowType.FramelessWindowHint | Qt.WindowType.Tool | Qt.WindowType.X11BypassWindowManagerHint # 用于跳过窗口管理器的关键设置 ) ############################################################################# # 核心处理部分 # ############################################################################# def reserve_space(self): # d = display.Display() # wid = int(self.winId()) # win = d.create_resource_object('window', wid) # # screen_width = QGuiApplication.primaryScreen().geometry().width() # bar_height = self.height() # # # 设置窗口类型为DOCK # win.change_property( # d.intern_atom('_NET_WM_WINDOW_TYPE'), # Xatom.ATOM, 32, # [d.intern_atom('_NET_WM_WINDOW_TYPE_DOCK')] # ) # # # 设置STRUT属性 # win.change_property( # d.intern_atom('_NET_WM_STRUT'), # Xatom.CARDINAL, 32, # [0, 0, bar_height, 0] # left, right, top, bottom # ) # # win.change_property( # d.intern_atom('_NET_WM_STRUT_PARTIAL'), # Xatom.CARDINAL, 32, # [ # 0, 0, bar_height, 0, # left, right, top, bottom # 0, 0, # left_start_y, left_end_y # 0, 0, # right_start_y, right_end_y # 0, screen_width - 1, # top_start_x, top_end_x # 0, 0 # bottom_start_x, bottom_end_x # ] # ) # # d.flush() # ############################################################################# if __name__ == "__main__": app = QApplication(sys.argv) bar = StatusBar() bar.show() bar.reserve_space() sys.exit(app.exec())
已有多款同类应用(比如Polybar、i3bar),它们之间可以互相兼容,不会重叠,遵循统一的层级规则,这是一种标准行为。
如何让我的任务栏实现和同类应用一致的行为?
你的代码核心问题在于使用了Qt.WindowType.X11BypassWindowManagerHint这个窗口标志,它会让窗口完全绕过X11窗口管理器(包括i3wm)的管理,导致窗口管理器无法识别你设置的_NET_WM_WINDOW_TYPE_DOCK和STRUT属性,自然不会为任务栏预留空间。
修改步骤如下:
移除
X11BypassWindowManagerHint标志
这个标志会让窗口脱离窗口管理器控制,直接去掉它,保留FramelessWindowHint和Tool,并改用WindowStaysOnTopHint确保置顶:self.setWindowFlags( Qt.WindowType.FramelessWindowHint | Qt.WindowType.WindowStaysOnTopHint | Qt.WindowType.Tool )注:
Tool标志可以让窗口不显示在系统任务栏中,符合任务栏的特性。调整属性设置时机
确保在窗口显示后再设置X11属性,将reserve_space调用移到showEvent中,避免窗口管理器未初始化窗口就接收属性:def showEvent(self, event): super().showEvent(event) self.reserve_space()主函数中无需手动调用
bar.reserve_space(),show()时会自动触发showEvent。适配i3wm的额外设置
i3wm对DOCK窗口的处理需要明确告知不要将其设为浮动,在reserve_space方法中添加:# 告诉i3wm窗口保持固定且置顶 win.change_property( d.intern_atom('_NET_WM_STATE'), Xatom.ATOM, 32, [d.intern_atom('_NET_WM_STATE_STICKY'), d.intern_atom('_NET_WM_STATE_ABOVE')] )同时确保i3配置文件中允许DOCK窗口:
for_window [window_type="dock"] sticky enable for_window [window_type="dock"] border none验证STRUT属性正确性
确保_NET_WM_STRUT_PARTIAL的参数覆盖完整屏幕宽度,当前设置是正确的,多屏幕场景需适配对应屏幕尺寸。
修改后的完整代码:
import sys from PyQt6.QtWidgets import QApplication, QWidget, QHBoxLayout, QLabel from PyQt6.QtCore import Qt, QRect from PyQt6.QtGui import QGuiApplication from Xlib import display, Xatom, X class StatusBar(QWidget): def __init__(self): super().__init__() # 1. 设置高度并获取屏幕宽度 self.setFixedHeight(58) screen_geometry = QGuiApplication.primaryScreen().geometry() self.setGeometry(QRect(0, 0, screen_geometry.width(), 58)) # 2. 样式设置 self.setStyleSheet("background-color: black; color: white;") # 3. 布局设置 layout = QHBoxLayout() layout.addWidget(QLabel("Test")) layout.setContentsMargins(10, 0, 10, 0) self.setLayout(layout) # 4. 窗口标志(无装饰、始终置顶、工具窗口) self.setWindowFlags( Qt.WindowType.FramelessWindowHint | Qt.WindowType.WindowStaysOnTopHint | Qt.WindowType.Tool ) def showEvent(self, event): super().showEvent(event) self.reserve_space() def reserve_space(self): d = display.Display() wid = int(self.winId()) win = d.create_resource_object('window', wid) screen_width = QGuiApplication.primaryScreen().geometry().width() bar_height = self.height() # 设置窗口类型为DOCK win.change_property( d.intern_atom('_NET_WM_WINDOW_TYPE'), Xatom.ATOM, 32, [d.intern_atom('_NET_WM_WINDOW_TYPE_DOCK')] ) # 设置STRUT属性,预留顶部空间 win.change_property( d.intern_atom('_NET_WM_STRUT'), Xatom.CARDINAL, 32, [0, 0, bar_height, 0] # left, right, top, bottom ) # 设置STRUT_PARTIAL,精确指定预留区域 win.change_property( d.intern_atom('_NET_WM_STRUT_PARTIAL'), Xatom.CARDINAL, 32, [ 0, 0, bar_height, 0, # left, right, top, bottom 0, 0, # left_start_y, left_end_y 0, 0, # right_start_y, right_end_y 0, screen_width - 1, # top_start_x, top_end_x 0, 0 # bottom_start_x, bottom_end_x ] ) # 设置窗口状态,确保在i3wm中不浮动且始终置顶 win.change_property( d.intern_atom('_NET_WM_STATE'), Xatom.ATOM, 32, [d.intern_atom('_NET_WM_STATE_STICKY'), d.intern_atom('_NET_WM_STATE_ABOVE')] ) d.flush() if __name__ == "__main__": app = QApplication(sys.argv) bar = StatusBar() bar.show() sys.exit(app.exec())
内容的提问来源于stack exchange,提问作者fauve

