PyQt6桌面壁纸小部件框架适配Wayland(Linux)的可行方案
在KDE Plasma Wayland下实现PyQt6桌面常驻小部件的方案
Wayland与X11的窗口管理模型差异很大,Qt的WindowStaysOnBottomHint、Tool等窗口标志在Wayland下的行为完全由 compositor(这里是KWin)决定,失效是正常现象。针对KDE Plasma环境,你可以尝试以下方案:
一、利用KWin的专属窗口属性/DBus接口
KWin提供了扩展的窗口控制能力,你可以通过Qt的属性设置或DBus调用让窗口符合需求:
1. 设置KWin桌面窗口类型
通过Qt的setProperty给窗口标记KWin专属的桌面类型属性,让它被识别为桌面部件(显示在壁纸上方、不进入任务栏):
from PyQt6.QtWidgets import QWidget, QApplication from PyQt6.QtCore import Qt, QByteArray app = QApplication([]) widget = QWidget() # 基础窗口标志(Frameless去掉标题栏,配合后续属性) widget.setWindowFlags(Qt.WindowType.FramelessWindowHint | Qt.WindowType.Tool) # 设置KWin专属属性,标记为桌面窗口 widget.setProperty("_kde_net_wm_window_type", QByteArray(b"desktop")) widget.show() app.exec()
2. 通过DBus强制设置窗口层级与任务栏隐藏
如果上述属性不生效,可以直接调用KWin的DBus接口精确控制:
from PyQt6.QtWidgets import QWidget, QApplication from PyQt6.QtCore import Qt import dbus app = QApplication([]) widget = QWidget() widget.setWindowFlags(Qt.WindowType.FramelessWindowHint) widget.show() # 获取窗口WId,调用KWin接口 wid = widget.winId() bus = dbus.SessionBus() kwin_obj = bus.get_object("org.kde.KWin", "/org/kde/KWin") kwin_iface = dbus.Interface(kwin_obj, "org.kde.KWin") # 让窗口保持在所有窗口底层(壁纸上方) kwin_iface.setKeepBelow(wid, True) # 跳过任务栏显示 kwin_iface.setSkipTaskbar(wid, True) # 跳过窗口切换器 kwin_iface.setSkipSwitcher(wid, True) app.exec()
二、替代系统拖拽/缩放,自定义实现
Wayland下startSystemResize()和startSystemMove()依赖compositor的支持,KWin可能对桌面类型窗口禁用了这些功能,这时可以自己实现鼠标事件处理:
from PyQt6.QtWidgets import QWidget, QApplication from PyQt6.QtCore import Qt, QPoint class DesktopWidget(QWidget): def __init__(self): super().__init__() self.setWindowFlags(Qt.WindowType.FramelessWindowHint) self.setProperty("_kde_net_wm_window_type", QByteArray(b"desktop")) self.resize(300, 200) # 鼠标状态记录 self._drag_pos = None self._resize_pos = None self._resize_edge = None self._resize_margin = 10 # 边缘触发缩放的像素范围 def mousePressEvent(self, event): pos = event.position().toPoint() # 判断是否触发缩放 if pos.x() < self._resize_margin and pos.y() < self._resize_margin: self._resize_edge = "top-left" self._resize_pos = pos elif pos.x() > self.width() - self._resize_margin and pos.y() < self._resize_margin: self._resize_edge = "top-right" self._resize_pos = pos elif pos.x() < self._resize_margin and pos.y() > self.height() - self._resize_margin: self._resize_edge = "bottom-left" self._resize_pos = pos elif pos.x() > self.width() - self._resize_margin and pos.y() > self.height() - self._resize_margin: self._resize_edge = "bottom-right" self._resize_pos = pos elif pos.y() < self._resize_margin: self._resize_edge = "top" self._resize_pos = pos elif pos.y() > self.height() - self._resize_margin: self._resize_edge = "bottom" self._resize_pos = pos elif pos.x() < self._resize_margin: self._resize_edge = "left" self._resize_pos = pos elif pos.x() > self.width() - self._resize_margin: self._resize_edge = "right" self._resize_pos = pos else: # 触发拖拽 self._drag_pos = pos super().mousePressEvent(event) def mouseMoveEvent(self, event): current_pos = event.position().toPoint() if self._drag_pos: # 拖拽窗口 delta = current_pos - self._drag_pos self.move(self.pos() + delta) elif self._resize_pos: # 调整窗口大小 delta = current_pos - self._resize_pos geo = self.geometry() min_w, min_h = self.minimumSize().width(), self.minimumSize().height() if self._resize_edge == "top-left": geo.setTopLeft(geo.topLeft() + delta) geo.setWidth(max(geo.width() - delta.x(), min_w)) geo.setHeight(max(geo.height() - delta.y(), min_h)) elif self._resize_edge == "top-right": geo.setTop(geo.top() + delta.y()) geo.setWidth(max(geo.width() + delta.x(), min_w)) geo.setHeight(max(geo.height() - delta.y(), min_h)) elif self._resize_edge == "bottom-left": geo.setLeft(geo.left() + delta.x()) geo.setWidth(max(geo.width() - delta.x(), min_w)) geo.setHeight(max(geo.height() + delta.y(), min_h)) elif self._resize_edge == "bottom-right": geo.setWidth(max(geo.width() + delta.x(), min_w)) geo.setHeight(max(geo.height() + delta.y(), min_h)) elif self._resize_edge == "top": geo.setTop(geo.top() + delta.y()) geo.setHeight(max(geo.height() - delta.y(), min_h)) elif self._resize_edge == "bottom": geo.setHeight(max(geo.height() + delta.y(), min_h)) elif self._resize_edge == "left": geo.setLeft(geo.left() + delta.x()) geo.setWidth(max(geo.width() - delta.x(), min_w)) elif self._resize_edge == "right": geo.setWidth(max(geo.width() + delta.x(), min_w)) self.setGeometry(geo) self._resize_pos = current_pos super().mouseMoveEvent(event) def mouseReleaseEvent(self, event): self._drag_pos = None self._resize_pos = None self._resize_edge = None super().mouseReleaseEvent(event) if __name__ == "__main__": app = QApplication([]) w = DesktopWidget() w.show() app.exec()
三、需要做出的妥协
- 跨桌面环境兼容性差:上述方案完全依赖KWin的特性,切换到GNOME、Sway等其他Wayland compositor时会失效,无法做到X11下的跨环境一致性。
- 自定义实现的工作量:自己处理拖拽缩放需要考虑边缘判断、最小尺寸限制等细节,比系统提供的方法繁琐。
- 窗口行为的限制:Wayland下compositor拥有最终控制权,即使设置了属性,KWin也可能在某些场景下调整窗口层级(比如全屏应用时)。
内容的提问来源于stack exchange,提问作者Incog




