You need to enable JavaScript to run this app.
最新活动
大模型
产品
解决方案
定价
生态与合作
支持与服务
开发者
了解我们

PyQt6桌面壁纸小部件框架适配Wayland(Linux)的可行方案

在KDE Plasma Wayland下实现PyQt6桌面常驻小部件的方案

Wayland与X11的窗口管理模型差异很大,Qt的WindowStaysOnBottomHintTool等窗口标志在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()

三、需要做出的妥协

  1. 跨桌面环境兼容性差:上述方案完全依赖KWin的特性,切换到GNOME、Sway等其他Wayland compositor时会失效,无法做到X11下的跨环境一致性。
  2. 自定义实现的工作量:自己处理拖拽缩放需要考虑边缘判断、最小尺寸限制等细节,比系统提供的方法繁琐。
  3. 窗口行为的限制:Wayland下compositor拥有最终控制权,即使设置了属性,KWin也可能在某些场景下调整窗口层级(比如全屏应用时)。

内容的提问来源于stack exchange,提问作者Incog

火山引擎 最新活动