基于PySide6实现系统托盘、图标资源与全局热键

注意 :全局热键功能只支持windows系统。

PySide6和PyQT5的系统托盘项目的实现方法基本一致,细节上有些小变化:

  1. QAction QtWidgets 调整到 QtGui
  2. 生成rc文件的命令由 pyrcc5 变为 pyside6-rcc
  3. pyqtkeybind 只支持PyQT5

1. 初始化

初始化时顺带实现一个点右上角不会退出的MainWindow。

import sys
from PySide6.QtWidgets import QApplication, QMainWindow, QWidget
import ui_untitled
class MyMainWindow(QMainWindow):
    # 继承一个QMainWindow,点右上角不会退出
    def __init__(self):
        QMainWindow.__init__(self)
    def closeEvent(self, event):
        # 忽略退出事件,而是隐藏到托盘
        event.ignore()
        self.hide()
if __name__=='__main__':
    # 初始化应用和窗口
    app = QApplication(sys.argv)
    win = MyMainWindow()
    # 载入界面
    ui = ui_untitled.Ui_MainWindow()
    #ui.setupUi(win)
    # 创建系统托盘项目
    tray = MySysTrayWidget(app=app, window=win, ui=ui)
    # 显示窗口
    #win.show()
    # 运行应用
    sys.exit(app.exec())

2. 系统托盘

导入依赖:

from PySide6.QtWidgets import QSystemTrayIcon, QMenu, QWidget
from PySide6.QtGui import QIcon, QAction
import app_rc # 由pyside6-rcc生成的资源文件

注意 :如果没有资源文件 app_rc.py 的话,程序正常运行不报错,但是看不到托盘图标。

定义QWidget类:

class MySysTrayWidget(QWidget):
    def __init__(self, ui=None, app=None, window=None):
        QWidget.__init__(self)  # 必须调用,否则信号系统无法启用
        # 私有变量
        self.__ui = ui
        self.__app = app
        self.__window = window
        self.__ui.setupUi(self.__window)
        # 配置系统托盘
        self.__trayicon = QSystemTrayIcon(self)
        self.__trayicon.setIcon(QIcon(':/app_icon.png'))
        self.__trayicon.setToolTip('D22Maid\n热键Ctrl+Alt+M')
        # 创建托盘的右键菜单
        self.__traymenu = QMenu()
        self.__trayaction = []
        self.addTrayMenuAction




    
('显示主界面', self.show_userinterface)
        self.addTrayMenuAction('退出', self.quit)
        # 配置菜单并显示托盘
        self.__trayicon.setContextMenu(self.__traymenu) #把tpMenu设定为托盘的右键菜单
        self.__trayicon.show()  #显示托盘   
        # 连接信号
        self.__ui.pushButton.clicked.connect(self.hide_userinterface)
        # 默认隐藏界面
        self.hide_userinterface()
    def __del__(self):
    def addTrayMenuAction(self, text='empty', callback=None):
        a = QAction(text, self)
        a.triggered.connect(callback)
        self.__traymenu.addAction(a)
        self.__trayaction.append(a)
    def quit(self):
        # 真正的退出
        self.__app.exit()
    def show_userinterface(self):
        self.__window.show()
    def hide_userinterface(self):
        self.__window.hide()
最终效果


3. 资源文件

按:为什么icon放在资源文件里?因为pyinstaller在windows下构建出来的应用不显示托盘图标,用qrc可以解决这个问题。

随便画一个图片。

随手画个托盘图标

随便写一个 app_icon.qrc 文件:

<!DOCTYPE RCC><RCC version="1.0">
<qresource prefix="/">
   <file>app_icon.png</file>
</qresource>
</RCC>

提示 :qrc文件中可以包含多个图片,也可以分层级存放,更多的信息查看 qrcfiles

根据 qrc 文件生成 py 文件:

pyside6-rcc app-icon.qrc -o app_rc.py

有了 app_rc.py 后,就可以在py中 import app_rc ,然后 QIcon(':/app_icon.png') 就能找到图片资源了。

pyside6-rcc 实际上就是 rcc -g python

4. 全局热键

给程序添加一个全局热键的功能,实现显示/隐藏主窗口。

pyqtkeybind 只支持PyQT5, global-hotkeys 不依赖Qt,但是基于threading实现,所以不能直接在其回调中显示/隐藏QWidget(但是可以访问QWidget的isVisible()方法)。

导入依赖:

# 定义全局快捷键
import global_hotkeys as hotkey

修改 MySysTrayWidget(QWidget) ,思路是这样的:

  1. 增加一个信号 hotkeyed ,对应的slot是 onHotkey()
  2. MySysTrayWidget 中正常注册热键,并将自身的方法 wakeHotkey() 送给 global-hotkeys 做回调
  3. global-hotkeys 监测到热键时,会调用 wakeHotkey() ,这时只发射 hotkeyed 信号,并不操作QWidget
  4. 回到 MySysTrayWidget 处理 hotkeyed 信号,正常显示/隐藏主窗口

定义信号,并且注册热键的相关代码如下:

class MySysTrayWidget(QWidget):
    # 自定义个信号
    hotkeyed = Signal(bool)
    def __init__(self, ui=None, app=None, window=None):
        QWidget.__init__(self)  # 必须调用,否则信号系统无法启用
        # 私有变量
        # 配置系统托盘
        # 创建托盘的右键菜单
        # 配置菜单并显示托盘
        # These take the format of [<key list>, <keydown handler callback>, <keyup handler callback>]
        self.hotkey_bindings = [
            [["control", "alt", "m"], None, self.wakeHotkey],
        hotkey.register_hotkeys(self.hotkey_bindings)
        hotkey.start_checking_hotkeys()
        # 连接信号
        self.hotkeyed.connect(self.onHotkey)
        # 默认隐藏界面

发射信号、相应信号的槽函数如下:

    def wakeHotkey(self):
        self.hotkeyed.emit(self.__window.isVisible())
    @Slot(bool)
    def onHotkey(self, visible):
        print('here', visible)
        if visible:
            self.hideUserInterface()
        else:
            self.showUserInterface()

5. 全部代码

d22maid.py

################################################################################
# d22maid           
# Author : Wang Jun (MWO)
################################################################################
# 定义全局快捷键
import global_hotkeys as hotkey
# 实现系统托盘程序
from PySide6.QtWidgets import QSystemTrayIcon, QMenu, QWidget
from PySide6.QtGui import QIcon, QAction
from PySide6.QtCore import Signal, Slot
import app_rc # 由pyside6-rcc生成的资源文件
class MySysTrayWidget(QWidget):
    # 自定义个信号
    hotkeyed = Signal(bool)
    def __init__(self, ui=None, app=None, window=None):
        QWidget.__init__(self)  # 必须调用,否则信号系统无法启用
        # 私有变量
        self.__ui = ui
        self.__app = app
        self.__window = window
        self.__ui.setupUi(self.__window)
        # 配置系统托盘
        self.__trayicon = QSystemTrayIcon(self)
        self.__trayicon.setIcon(QIcon(':/app_icon.png'))
        self.__trayicon.setToolTip('D22Maid\n热键Ctrl+Alt+M')
        # 创建托盘的右键菜单
        self.__traymenu = QMenu()
        self.__trayaction = []
        self.addTrayMenuAction('显示主界面', self.showUserInterface)
        self.addTrayMenuAction('退出', self.quit)
        # 配置菜单并显示托盘
        self.__trayicon.setContextMenu(self.__traymenu) #把tpMenu设定为托盘的右键菜单
        self.__trayicon.show()  #显示托盘   
        # These take the format of [<key list>, <keydown handler callback>, <keyup handler callback>]
        self.hotkey_bindings = [
            [["control", "alt", "m"], None, self.wakeHotkey],
        hotkey.register_hotkeys(self.hotkey_bindings)
        hotkey.start_checking_hotkeys()
        # 连接信号
        self.__ui.pushButton.clicked.connect(self.hideUserInterface)
        self.hotkeyed.connect(self.onHotkey)
        # 默认隐藏界面
        self.hideUserInterface()
    def __del__(self):
    def addTrayMenuAction(self, text='empty', callback=None):
        a = QAction(text, self)
        a.triggered.connect(callback)
        self.__traymenu.addAction(a)
        self.__trayaction.append(a)
    def quit(self):
        # 真正的退出
        self.__app.exit()
    def showUserInterface(self):
        self.__window.show()
    def hideUserInterface(self):
        self.__window.hide()
    def wakeHotkey(self):
        self.hotkeyed.emit(self.__window.isVisible())
    @Slot(bool)
    def onHotkey(self, visible):
        print('here', visible)
        if visible:
            self.hideUserInterface()
        else:
            self.showUserInterface()
import sys
from PySide6.QtWidgets import QApplication, QMainWindow, QWidget
import ui_untitled
class MyMainWindow(QMainWindow):
    # 继承一个QMainWindow,点右上角不会退出
    def __init__(self):
        QMainWindow.__init__(self)
    def closeEvent(self, event):
        # 忽略退出事件,而是隐藏到托盘
        event.ignore()
        self.hide()
if __name__=='__main__':
    # 初始化应用和窗口
    app = QApplication(sys.argv)
    win = MyMainWindow()
    # 载入界面
    ui = ui_untitled.Ui_MainWindow()
    #ui.setupUi(win)
    # 创建系统托盘项目
    tray = MySysTrayWidget(app=app, window=win, ui=ui)
    # 显示窗口
    #win.show()
    # 运行应用
    sys.exit(app.exec())

ui_untitled.py ,用 pyside6-designer 随便画了个界面。

# -*- coding: utf-8 -*-
################################################################################
## Form generated from reading UI file 'untitled.ui'
## Created by: Qt User Interface Compiler version 6.4.2
## WARNING! All changes made in this file will be lost when recompiling UI file!
################################################################################
from PySide6.QtCore import (QCoreApplication, QDate, QDateTime, QLocale,
    QMetaObject, QObject, QPoint, QRect,
    QSize, QTime, QUrl, Qt)
from PySide6.QtGui import (QBrush, QColor, QConicalGradient, QCursor,
    QFont, QFontDatabase, QGradient, QIcon,
    QImage, QKeySequence, QLinearGradient, QPainter,
    QPalette, QPixmap, QRadialGradient, QTransform)
from PySide6.QtWidgets import (QApplication, QMainWindow, QMenuBar, QPushButton,
    QSizePolicy, QStatusBar, QWidget)
class Ui_MainWindow(object):
    def setupUi(self, MainWindow):
        if not MainWindow.objectName():
            MainWindow.setObjectName(u"MainWindow")
        MainWindow.resize(300, 300)
        self.centralwidget = QWidget(MainWindow)
        self.centralwidget.setObjectName(u"centralwidget")
        self.pushButton = QPushButton(self.centralwidget)
        self.pushButton.setObjectName(u"pushButton")
        self.pushButton.setGeometry(QRect(110, 170, 75, 24))
        MainWindow.setCentralWidget(self.centralwidget)
        self.menubar = QMenuBar(MainWindow)
        self.menubar.setObjectName(u"menubar")
        self.menubar.setGeometry(QRect(0, 0, 300, 22))
        MainWindow.setMenuBar(self.menubar)
        self.statusbar = QStatusBar(MainWindow)
        self.statusbar.setObjectName(u"statusbar")
        MainWindow.setStatusBar(self.statusbar)
        self.retranslateUi(MainWindow)
        QMetaObject.connectSlotsByName(MainWindow)