第5章 有趣的GUI技术 有很多特殊的应用,在初学者看来,简直就和魔法一样。例如,在屏幕上显示一只青蛙或者一只怪兽,用鼠标还可以来回拖动。在屏幕的任何位置绘制曲线,随时可以清除这些曲线,或者在系统托盘添加图标,弹出对话气泡等。这些看似很复杂,其实用Python实现起来相当简单,这是因为Python有大量第三方模块,通过这些模块,只需要几行代码就可以实现这些操作。本章将通过大量完整的例子演示如何实现这些看似复杂,其实相当简单的功能。 5.1特殊窗口 在很多场景中,往往需要实现各种特殊的窗口形态,例如,非矩形的窗口(称为异形窗口)、半透明窗口等。本节将介绍如何实现这些特殊窗口。 5.1.1使用Canvas实现五角星窗口 窗口通常都是矩形的,但在很多应用中,尤其是游戏,窗口是不规则的,例如,圆形、椭圆形、三角形、五角形,甚至是一个怪兽的形态(如有些游戏程序),其实这些仍然是窗口,只不过通过掩模(mask)技术将某些部分变得透明,因此,用户看到的窗口就变成了不规则的形状。 在计算机科学中,掩模(mask)或位掩码(bitmask)是用于位运算的数据,特别是在位域中。使用掩模可以在一个字节的多个位上设置开或关,或者在一次位运算中将开和关反转。 在计算机图形学中,掩模是用于隐藏或显示另一图像的部分的数字图像。掩模可以用于创建特殊效果或选择图像的区域进行编辑。掩模可以从零开始在图像编辑器中创建,也可以基于现有图像生成。例如,你可以使用一张照片作为掩模,来显示或隐藏另一张照片的部分,如果对窗口使用掩模,就会让窗口变成异形窗口。 本节会使用Canvas实现五角星形状的异形窗口,也就是说,运行程序后,窗口会变成一个红色的五角星,其他部分是透明的,效果如图51所示。使用鼠标拖曳即可移动该窗口。 从图51所示的五角星可以看出,运行程序,只会显示一个五角星,其他部分是透明的,可以看到后面的Python文件目录。 这个五角星是在Canvas中绘制的,所以需要先了解如何绘制五角星。绘制五角星需要使用如下几组数据。 (1) 五角星的中心点坐标。 (2) 五角星内切圆半径和外切圆半径。 (3) 五角星10个顶点的坐标。 (4) 五角星的旋转角度。 在这些数据中,(1)、(2)和(4)是直接指定的,而(3)可以通过计算获得。本节绘制的五角星是正五角星 正五角星是一个由五条对角线构成的正五边形的内接星形。它由5个相等的等腰三角形组成,每个三角形的顶点是正五边形的一个顶点。正五角星的每个内角是36°,每个外角是72°。正五角星有很多数学和几何性质,例如,它的对角线之比是黄金比。,所以这里只讨论正五角星。五角星以及内切圆和外切圆如图52所示。其中,A到J一共10个字母,分别表示五角星的10个顶点。现在计算顶点B的坐标,其他顶点的计算方法类似。 图51五角星形状的异形窗口 图52五角星顶点坐标的计算 假设五角星中心点的坐标是(centerX,centerY),顶点B与中心点的连线与水平线的夹角是θ,外切圆半径是r,那么B点的坐标如下: (centerX + r * cos(θ),centerY - r * sin(θ)) 按类似的方法计算完10个顶点,就可以绘制10个点的多边形,最终形成一个五角星。 要想让窗口完全变成五角星,需要使用下面的代码将窗口的背景设置为透明,而且需要将窗口设置为无边框。 # 设置窗口的背景为透明 self.setAttribute(Qt.WidgetAttribute.WA_TranslucentBackground) # 设置窗口的边框为无边框 self.setWindowFlag(Qt.WindowType.FramelessWindowHint) 五角星会将窗口的标题栏隐藏,这样将无法拖动窗口,所以需要使用下面的步骤,用鼠标可以通过拖动五角星来移动窗口。 (1) 在QWidget的mousePressEvent方法中,检测鼠标左键是否按下,并记录下鼠标的位置。 (2) 在QWidget的mouseMoveEvent方法中,检测鼠标是否移动,并计算移动的距离,然后用move方法来移动窗口。 (3) 在QWidget的mouseReleaseEvent方法中,检测鼠标左键是否松开,并重置鼠标位置为None。 下面的例子完整地演示了如何使用PyQt6实现一个五角星窗口,并通过鼠标拖动五角星移动窗口。 代码位置: src/interesting_gui/pyqt6/star_window_canvas.py from PyQt6.QtWidgets import QApplication, QWidget from PyQt6.QtGui import QPainter, QPen, QBrush, QColor from PyQt6.QtCore import Qt, QPoint import sys import math # 定义一个自定义的窗口类,继承自QWidget class StarWindow(QWidget): # 初始化方法 def __init__(self): # 调用父类的初始化方法 super().__init__() # 设置窗口的标题和尺寸 self.setWindowTitle("红色正五角星") self.resize(300, 300) # 设置窗口的背景为透明 self.setAttribute(Qt.WidgetAttribute.WA_TranslucentBackground) # 设置窗口的边框为无边框 self.setWindowFlag(Qt.WindowType.FramelessWindowHint) # 初始化鼠标位置和偏移量为None self.mouse_pos = None self.offset = None # 重写绘图事件方法 def paintEvent(self, event): # 创建一个画笔对象,设置颜色为红色,宽度为2像素,样式为实线 pen = QPen(QColor(255, 0, 0), 2, Qt.PenStyle.SolidLine) # 创建一个画刷对象,设置颜色为红色,样式为实心填充 brush = QBrush(QColor(255, 0, 0), Qt.BrushStyle.SolidPattern) # 创建一个绘图对象,传入self作为参数 painter = QPainter(self) # 设置画笔和画刷 painter.setPen(pen) painter.setBrush(brush) # 获取窗口的宽度和高度 width = self.width() height = self.height() # 计算五角星的外接圆半径,取宽度和高度中较小的一半减去10像素作为边距 radius = min(width, height) / 2 - 10 # 计算五角星的内接圆半径,根据正五角星的性质,内接圆半径是外接圆半径的0.382倍 inner_radius = radius * 0.382 # 计算五角星的中心点,即窗口的中心点 center = QPoint(int(width / 2), int(height / 2)) # 创建一个空列表,用于存储五角星的顶点坐标 points = [] # 循环5次,每次计算一个顶点坐标,并添加到列表中 for i in range(5): # 计算当前顶点对应的角度,单位是弧度,注意要减去90度,使得第一个顶点在正上方 angle = (i * 72 - 90) / 180 * math.pi # 计算当前顶点的横坐标和纵坐标,根据三角函数公式,横坐标等于中心点横坐标加 # 上外接圆半径乘以角度的余弦值,纵坐标等于中心点纵坐标加上外接圆半径乘以角度的正弦值 x = center.x() + radius * math.cos(angle) y = center.y() + radius * math.sin(angle) # 创建一个QPoint对象,表示当前顶点坐标,并添加到列表中 points.append(QPoint(int(x), int(y))) # 计算下一个顶点对应的角度,单位是弧度,注意要加上36度,使得每两个相邻的顶 # 点之间有一个内角为36度的凹角 next_angle = (i * 72 + 36 - 90) / 180 * math.pi # 计算下一个顶点的横坐标和纵坐标,根据三角函数公式,横坐标等于中心点横坐标 # 加上内接圆半径乘以角度的余弦值,纵坐标等于中心点纵坐标加上内接圆半径乘以角度的正弦值 next_x = center.x() + inner_radius * math.cos(next_angle) next_y = center.y() + inner_radius * math.sin(next_angle) # 创建一个QPoint对象,表示下一个顶点坐标,并添加到列表中 points.append(QPoint(int(next_x), int(next_y))) # 使用绘图对象的drawPolygon方法,传入列表作为参数,绘制一个多边形,即五角星 print(len(points)) painter.drawPolygon(*points) # 重写鼠标按下事件方法 def mousePressEvent(self, event): # 如果鼠标左键被按下 if event.button() == Qt.MouseButton.LeftButton: # 获取鼠标当前的位置,并赋值给mouse_pos属性 self.mouse_pos = event.globalPosition() # 获取窗口当前的位置,并赋值给offset属性 self.offset = self.pos() # 重写鼠标移动事件方法 def mouseMoveEvent(self, event): # 如果鼠标左键被按下,并且mouse_pos和offset属性不为None if event.buttons() == Qt.MouseButton.LeftButton and self.mouse_pos is not None and self.offset is not None: # 计算鼠标移动的距离,等于鼠标当前的位置减去mouse_pos属性 delta = event.globalPosition() - self.mouse_pos # 将delta转换为QPoint类型,使用QPoint的构造函数,传入delta的x()和y()作为参数 delta = QPoint(int(delta.x()), int(delta.y())) # 计算窗口移动后的位置,等于offset属性加上delta new_pos = self.offset + delta # 设置窗口的位置为new_pos self.move(new_pos) # 重写鼠标松开事件方法 def mouseReleaseEvent(self, event): # 如果鼠标左键被松开 if event.button() == Qt.MouseButton.LeftButton: # 将mouse_pos和offset属性重置为None self.mouse_pos = None self.offset = None # 创建一个应用对象,传入sys.argv作为参数 app = QApplication(sys.argv) # 创建一个窗口对象 window = StarWindow() # 显示窗口 window.show() # 进入应用的主循环 sys.exit(app.exec()) 运行程序,就会看到如图51所示的五角星窗口。 5.1.2使用透明png图像实现美女机器人窗口 尽管使用Canvas可以实现异形窗口,但对于更复杂的异形窗口,使用Canvas就很麻烦, 图53美女机器人窗口 尤其是人物、机械装置这些几乎不可能通过Canvas来绘制的效果。因此,需要直接使用透明png图像实现异形窗口,如图53所示为美女机器人窗口。使用鼠标拖动美女机器人,就可以移动该窗口。 本节的例子除了要显示png图像外,其他效果的实现方式与5.1.1节的例子完全相同。下面讲一下如何处理png图像。 可以使用QLabel显示png图像,所以创建一个从QLabel派生的类作为窗口类。然后使用QPixmap装载png图像,并使用QLabel.setPixmap方法在QLabel组件中显示图像。最后,使用QLabel.setMask设置窗口形状跟随图像不透明部分,从而让窗口的形状呈现出与png图像中不透明的部分完全一样的效果。代码如下: # 加载png图像 self.pixmap = QPixmap("images/robot.png") # 设置标签大小和图像大小一致 self.resize(self.pixmap.size()) # 设置标签显示图像 self.setPixmap(self.pixmap) # 设置窗口形状跟随图像不透明部分 self.setMask(self.pixmap.mask()) 下面的例子演示了如何装载png图像,如何设置掩模,如何通过鼠标拖动窗口的完整实现过程。 代码位置: src/interesting_gui/pyqt6/image_transparent_window.py from PyQt6.QtWidgets import QApplication, QLabel from PyQt6.QtGui import QPixmap from PyQt6.QtCore import Qt class ImageTransparentWindow(QLabel): def __init__(self, parent=None): super().__init__(parent) # 设置窗口无边框和半透明 self.setWindowFlags(Qt.WindowType.FramelessWindowHint) self.setWindowOpacity(0.99) # 加载png图像 self.pixmap = QPixmap("images/robot.png") # 设置标签大小和图像大小一致 self.resize(self.pixmap.size()) # 设置标签显示图像 self.setPixmap(self.pixmap) # 设置窗口形状跟随图像不透明部分 self.setMask(self.pixmap.mask()) # 初始化鼠标位置 self.mouse_pos = None # 重写鼠标按下事件 def mousePressEvent(self, event): # 如果是左键按下,记录当前鼠标位置 if event.button() == Qt.MouseButton.LeftButton: self.mouse_pos = event.globalPosition() # 重写鼠标移动事件 def mouseMoveEvent(self, event): # 如果鼠标位置已记录,计算鼠标移动的偏移量,并更新窗口位置 if self.mouse_pos is not None: delta = event.globalPosition() - self.mouse_pos self.move(int(self.x() + delta.x()), int(self.y() + delta.y())) self.mouse_pos = event.globalPosition() # 重写鼠标释放事件 def mouseReleaseEvent(self, event): # 如果是左键释放,清空鼠标位置记录 if event.button() == Qt.MouseButton.LeftButton: self.mouse_pos = None # 创建一个应用程序对象 app = QApplication([]) # 创建一个图像标签对象 win = ImageTransparentWindow() # 显示透明图像 win.show() # 运行应用程序循环 app.exec() 在运行程序之前,先要准备一个png图像文件,并将其命名为robot.png。将这个图像文件放到images目录下,然后运行程序,界面上会立刻显示一个美女机器人。如果读者使用其他透明png图像,则会显示其他形态的异形窗口。 5.1.3半透明窗口 通过QWidget.SetWindowOpacity方法可以设置窗口的透明度,通过QWidget.setWindowFlag方法可以隐藏窗口的标题栏和边框。将这两个方法组合起来使用,可以实现将组件放到半透明窗口上的效果。例如,图54在一个半透明窗口上放置了一个标签组件和一个按钮组件,而且隐藏了窗口的边框和标题栏。单击close按钮就能关闭窗口。 图54半透明窗口 下面的例子完整地演示了图54所示效果的实现方法。 代码位置: src/interesting_gui/pyqt6/translucent _window.py from PyQt6.QtWidgets import QApplication, QWidget, QLabel, QPushButton from PyQt6.QtCore import Qt # 定义一个自定义的窗口类,继承自QWidget class MyWindow(QWidget): # 初始化方法 def __init__(self): # 调用父类的初始化方法 super().__init__() # 设置窗口大小为400*300 self.resize(400, 300) # 设置窗口透明度为0.5 self.setWindowOpacity(0.7) # 设置窗口无边框和标题栏 self.setWindowFlag(Qt.WindowType.FramelessWindowHint) # 初始化鼠标位置为None self.mousePos = None # 调用创建组件的方法 self.createWidgets() # 创建组件的方法 def createWidgets(self): # 创建一个标签,显示"Hello, world!" label = QLabel("Hello, world!", self) # 设置标签的字体大小为20 label.setStyleSheet("font-size: 20px;") # 设置标签的位置和大小 label.setGeometry(150, 100, 200, 50) # 创建一个按钮,显示"Close" button = QPushButton("Close", self) # 设置按钮的位置和大小 button.setGeometry(150, 200, 100, 50) # 绑定按钮的点击信号和关闭窗口的槽函数 button.clicked.connect(self.close) # 重写鼠标按下事件的方法 def mousePressEvent(self, event): # 如果鼠标左键被按下 if event.button() == Qt.MouseButton.LeftButton: # 获取鼠标相对于窗口的位置 self.mousePos = event.pos() # 重写鼠标移动事件的方法 def mouseMoveEvent(self, event): # 如果鼠标左键被按下并且鼠标位置不为空 if event.buttons() == Qt.MouseButton.LeftButton and self.mousePos: # 获取鼠标相对于屏幕的位置 globalPos = event.globalPosition().toPoint() # 计算窗口应该移动到的位置 windowPos = globalPos - self.mousePos # 移动窗口到新的位置 self.move(windowPos) # 创建一个应用对象 app = QApplication([]) # 创建一个窗口对象 window = MyWindow() # 显示窗口 window.show() # 进入应用的事件循环 app.exec() 5.2在屏幕上绘制曲线 有很多画笔应用,可以在整个电脑屏幕上绘制曲线和各种图形,这种应用很适合在线教学或演示。例如,要讲解代码的编写过程,可以一边展示代码,一边在代码上绘制曲线、手写一些文字等,如图55所示。 图55在屏幕上自由绘制曲线 实现这个功能的基本做法是让窗口充满整个屏幕,并且隐藏边框和标题栏,以及让窗口完全透明,这样就可以看到窗口下方的内容了。我们还可以直接在窗口上绘制曲线,放置组件,绘制各种简单和复杂的图形,放置图像等。相关内容已在前文有所涉及,下面看一个完整的例子,即如何实现在屏幕上绘制曲线。 代码位置: src/interesting_gui/pyqt6/ screen_drawing.py from PyQt6.QtWidgets import QApplication, QWidget from PyQt6.QtGui import QPainter, QPen, QColor from PyQt6.QtCore import Qt # 定义一个自定义的窗口类,继承自QWidget class MyWindow(QWidget): # 初始化方法 def __init__(self): # 调用父类的初始化方法 super().__init__() # 设置窗口标题 self.resize(QApplication.primaryScreen().size()) # 设置窗口的背景颜色为透明 self.setStyleSheet("background-color:transparent;") # 设置窗口的边框和标题栏为无 self.setWindowFlag(Qt.WindowType.FramelessWindowHint) # 设置窗口的透明度为1,即完全不透明 self.setWindowOpacity(1) # 设置窗口无边框和标题栏 self.setWindowFlag(Qt.WindowType.FramelessWindowHint) # 设置窗口始终在最前 self.setWindowFlag(Qt.WindowType.WindowStaysOnTopHint) # 创建一个画笔对象,设置颜色为红色,线宽为3像素,样式为实线 self.pen = QPen(QColor("black"), 3, Qt.PenStyle.SolidLine) # 创建一个空列表,用于存储鼠标移动的点坐标 self.points = [] # 创建一个布尔变量,用于标记鼠标是否按下 self.pressed = False # 重写绘图事件方法 def paintEvent(self, event): # 调用父类的绘图事件方法 super().paintEvent(event) # 创建一个画家对象,传入窗口作为参数 painter = QPainter(self) # 设置画笔 painter.setPen(self.pen) # 如果点列表不为空,绘制点之间的曲线 if self.points: painter.drawPolyline(*self.points) # 重写鼠标按下事件方法 def mousePressEvent(self, event): # 调用父类的鼠标按下事件方法 super().mousePressEvent(event) # 如果鼠标左键被按下 if event.button() == Qt.MouseButton.LeftButton: # 将布尔变量设为True,表示鼠标按下状态 self.pressed = True # 清空点列表,开始新的绘制 self.points.clear() # 将鼠标当前位置添加到点列表中 self.points.append(event.pos()) # 更新窗口,触发绘图事件 self.update() # 重写鼠标移动事件方法 def mouseMoveEvent(self, event): # 调用父类的鼠标移动事件方法 super().mouseMoveEvent(event) # 如果鼠标处于按下状态 if self.pressed: # 将鼠标当前位置添加到点列表中 self.points.append(event.pos()) # 更新窗口,触发绘图事件 self.update() # 重写鼠标释放事件方法 def mouseReleaseEvent(self, event): # 调用父类的鼠标释放事件方法 super().mouseReleaseEvent(event) # 如果鼠标左键被释放 if event.button() == Qt.MouseButton.LeftButton: # 将布尔变量设为False,表示鼠标释放状态 self.pressed = False # 重写键盘按下事件方法 def keyPressEvent(self, event): # 调用父类的键盘按下事件方法 super().keyPressEvent(event) # 如果按下Esc键 if event.key() == Qt.Key.Key_Escape: # 关闭窗口,退出程序 self.close() elif event.key() == Qt.Key.Key_2: self.pen = QPen(QColor("red"), 3, Qt.PenStyle.SolidLine) elif event.key() == Qt.Key.Key_3: self.pen = QPen(QColor("blue"), 3, Qt.PenStyle.SolidLine) elif event.key() == Qt.Key.Key_4: self.pen = QPen(QColor("green"), 3, Qt.PenStyle.SolidLine) # 创建一个应用对象 app = QApplication([]) # 创建一个窗口对象 window = MyWindow() # 显示窗口 window.show() # 进入应用的主循环 app.exec() 运行程序,默认可以在屏幕上绘制黑色曲线; 按2键,可以绘制红色曲线; 按3键,可以绘制蓝色曲线; 按4键,可以绘制绿色曲线; 按Esc键,关闭程序。 注意: 本例只能在macOS上运行。 5.3控制状态栏 状态栏一直是各类程序争夺的主阵地之一,有很多主流程序都会在状态栏添加1个或多个图标,以及添加图标弹出菜单、对话气泡等功能。本节将详细讲解如何Python“攻占”这块主阵地。 5.3.1在状态栏上添加图标 Windows、macOS和Linux都有状态栏,而且都允许用户添加图标,只是添加图标的区域不同。Windows称为通知区域,也就是任务栏右侧的部分,它包含了一些常用的图标和通知,如电量、WiFi、音量、时钟和日历等。macOS中称为菜单栏,在菜单栏左侧显示macOS菜单,右侧显示各种图标,单击这些图标后会触发不同的操作。Linux右上角显示图标的区域的称呼可能因不同的桌面环境而有所不同,但一般可以称为状态栏或者通知区域。为了统一,后文统称为状态栏。 通常一个应用程序会对应一个图标,但也可以对应多个图标。使用pystray模块可以将图标添加到状态栏上。如果没有安装pystray模块,可以使用下面的命令安装pystray模块。 pip3 install pystray pystray模块是跨平台的,可以用其将图标添加到Windows的通知区域、macOS的菜单栏右侧以及Linux的通知区域。 使用pystray模块将图标和菜单添加到状态栏的流程大致如下。 (1) 导入pystray模块和PIL模块,用于创建图标和加载图像。 (2) 创建一个pystray.Icon对象,指定图标的名称、图像、标题和单击回调函数(这些不一定都指定,也可以指定一部分)。 (3) 创建一个pystray.Menu对象,指定菜单的各个项,每个项可以是一个pystray.MenuItem对象或者一个分隔符。 (4) 将菜单对象赋值给pystray.Icon对象的menu命名参数。 (5) 调用图标对象的run方法,启动图标的主循环。 在创建图标和菜单的过程中,涉及pystray.Icon类、pystray.Menu类和pystray.MenuItem类,下面分别给出这3个类的构造方法原型以及参数函数。 1. pystray.Icon类 该类用于创建图标对象,其构造方法原型如下: pystray.Icon(name, icon=None, title=None, menu=None, action=None) 参数含义如下。 (1) name: 图标的名称,必须是唯一的字符串。 (2) icon: 图标的图像,可以是一个PIL.Image对象或者一个返回PIL.Image对象的函数。 (3) title: 图标的标题,可以是一个字符串或者一个返回字符串的函数。 (4) menu: 图标的菜单,可以是一个pystray.Menu对象或者一个返回pystray.Menu对象的函数。 (5) action: 图标被单击时执行的回调函数,可以是一个无参数的函数或者一个返回无参数函数的函数。 2. pystray.Menu类 该类用于创建菜单对象,其构造方法原型如下: pystray.Menu(*items) Menu类的构造方法只有一个items参数,用于表示菜单中的菜单项,可以是pystray.MenuItem对象或者pystray.SEPARATOR常量。 3. pystray.MenuItem类 该类用于创建菜单项对象,其构造方法原型如下: pystray.MenuItem(text, action, checked=None, enabled=None, visible=None) 参数含义如下。 (1) text: 菜单项的文本,可以是一个字符串或者一个返回字符串的函数。 (2) action: 菜单项被单击时执行的回调函数,可以是一个无参数的函数或者一个返回无参数函数的函数。 (3) checked: 菜单项是否被选中,可以是一个布尔值或者一个返回布尔值的函数。 (4) enabled: 菜单项是否可用,可以是一个布尔值或者一个返回布尔值的函数。 (5) visible: 菜单项是否可见,可以是一个布尔值或者一个返回布尔值的函数。 下面的例子完整地演示了如何使用pystray模块在状态栏添加一个菜单,以及相应菜单项的点击动作。本例的菜单中有两个菜单项——Hello和“退出”。单击Hello菜单项会在终端输出Hello字符串,单击“退出”菜单项,会退出应用程序。 代码位置: src/interesting_gui/pystray_demo.py from pystray import Icon, MenuItem, Menu from PIL import Image class MyTray: # 定义一个无参数的函数,用于弹出消息框 def __init__(self, image): image = Image.open(image) menu = Menu( MenuItem("Hello", lambda: print("Hello")), MenuItem("退出", lambda: self.icon.stop()) ) self.icon = Icon(name='nameTray', title='titleTray', icon=image, menu=menu) def stopProgram(self, icon): self.icon.stop() def runProgram(self): self.icon.run() if __name__ == '__main__': myTray = MyTray(image="images/tray.png") myTray.runProgram() 运行程序,会看到在状态栏上显示一个绿色的图标,在图标上右击鼠标,会弹出一个菜单,图56是Windows下的效果,图57是macOS下的效果,图58是Ubuntu Linux下的效果。 图56Windows中通知区域 图标和菜单 图57macOS中菜单栏 图标和菜单 图58Ubuntu Linux中 菜单栏图标和菜单 5.3.2添加Windows 10风格的Toast消息框 使用win10toast模块可以添加Windows 10风格的Toast消息框。该模块只能在Windows 10上运行。读者可以使用下面的命令安装win10toast模块: pip3 install win10toast 与win10toast对应的还有一个win10toast_click模块,该模块的功能与win10toast的功能类似,只是能响应Toast消息框的单击事件。win10toast_click模块可以完全取代win10toast模块,所以推荐使用win10toast_click模块。读者可以使用下面的命令安装win10toast_click模块: pip install win10toast-click 注意: 在使用win10toast_click模块时,win10toast与click之间使用下画线(_)连接,而安装win10toastclick模块时,win10toast与click之间使用连字符()连接。 win10toast_click模块中的核心函数是show_toast,该函数用于显示Toast消息框,其函数原型如下: def show_toast(self, title, msg, icon_path=None, duration=None, threaded=False, callback_on_click=None) 函数参数的含义如下。 (1) title: 通知的标题,必须是一个字符串。 (2) msg: 通知的内容,必须是一个字符串。 (3) icon_path: 通知的图标路径,必须是一个.ico文件,如果为None,则使用默认图标。 (4) duration: 通知的持续时间,单位为s,如果为None,则使用默认值(5s)。 (5) threaded: 是否使用多线程来显示通知。如果为True,则不会阻塞程序的运行; 如果为False,则会等待通知消失后再继续程序的运行。 (6) callback_on_click: 在用户点击通知时执行的函数,必须是一个无参数的函数。如果为None,则不执行任何函数。 由于icon_path参数只支持ico文件,所以如果图标文件是其他图像格式(如png文件),就需要使用PIL模块的相关API将其他图像格式的文件转换为ico图像格式的文件。 下面的例子使用win10toast_click模块在Windows通知区域添加一个图标,以及在图标上方显示一个Toast消息框,点击消息框,会打开浏览器,并在浏览器中显示webbrowser.open函数打开的页面。 代码位置: src/interesting_gui/toast_demo.py from win10toast_click import ToastNotifier from PIL import Image # 导入Image模块 import webbrowser # 导入webbrowser模块,用于打开网页 filename = "images\\tray.png" # 指定要转换的.png文件名 img = Image.open(filename) # 打开图片 img.save('images\\tray.ico') # 保存为.ico文件 def open_url(): # 定义一个函数,用于打开一个网址 webbrowser.open("https://www.unitymarvel.com") toaster = ToastNotifier() toaster.show_toast("软件更新", # 通知的标题 "UnityMarvel已经更新到2.01版本,点击下载", # 通知的内容 icon_path="images\\tray.ico",# 通知的图标路径,如果为None则使用 # 默认图标 duration=20, # 通知的持续时间,单位为s threaded=True,# 是否使用多线程来显示通知,如果为 # True则不会阻塞程序的运行 callback_on_click=open_url) 运行程序后,Windows右下角会显示如图59所示的Toast消息框。 图59Windows 10风格的Toast消息框 win10toast_click模块在Windows 11或以上版本运行可能会有问题,如果读者使用的是Windows 11,可以尝试使用win11toast模块。读者可以使用下面的命令安装win11toast模块: pip3 install win11toast win11toast模块可以在Windows 10和Windows 11上运行,例子代码如下: from win11toast import toast toast('Hello', '这是一个通知') 运行程序,会看到Windows右下角出现如图510所示的Toast消息框。 图510win11toast模块显示的Toast消息框 5.3.3使用PyQt6管理系统托盘 QSystemTrayIcon类是PyQt6中的一个类,它提供了一种在系统托盘系统托盘就是Windows中的通知区域;macOS中的菜单栏右上角的位置;Linux中右上角的通知区域; 只是另一种称呼而已。中显示图标的方法。以下是QSystemTrayIcon类的主要功能: (1) 设置图标; (2) 设置提示信息; (3) 添加菜单; (4) 响应菜单单击事件; (5) 响应单击图标事件; (6) 显示消息(对话气泡); (7) 响应消息单击事件。 要设置图标,可以使用setIcon方法。要设置提示信息,可以使用setToolTip方法。要添加菜单,可以使用setContextMenu方法。要响应菜单单击事件,可以使用connect方法连接QAction对象的triggered信号到槽函数。要响应单击图标事件,可以使用activated信号连接到槽函数。要显示消息,可以使用showMessage方法。要响应消息单击事件,可以使用messageClicked信号连接到槽函数。相关方法的描述如下。 1. setIcon方法 方法原型如下: def setIcon(self, icon: Union[QIcon, QPixmap]) -> None: 该方法用于设置QSystemTrayIcon的图标,接收一个QIcon或QPixmap对象作为参数。如果传递一个QPixmap对象,它将自动转换为QIcon对象。 2. setToolTip方法 方法原型如下: def setToolTip(self, tip: str) -> None: 该方法用于设置QSystemTrayIcon的提示信息,接收一个字符串作为参数,该字符串将显示在鼠标悬停在图标上时。 3. setContextMenu方法 方法原型如下: def setContextMenu(self, menu: QMenu) -> None: 该方法用于设置QSystemTrayIcon的上下文菜单,接收一个QMenu对象作为参数,该对象包含要显示的菜单项。 4. showMessage方法 方法原型如下: def showMessage(self, title: str, message: str, icon: Union[QSystemTrayIcon.MessageIcon, int] = QSystemTrayIcon.Information, msecs: int = 10000) -> bool: 该方法用于在系统托盘中显示一条消息,接收4个参数——标题、消息、图标和显示时间(以ms为单位)。默认情况下,消息将显示10s。 下面的例子使用QSystemTrayIcon类的相关API在系统托盘添加一个图标,以及一个菜单,并显示一个窗口。单击Show菜单项,会显示这个窗口; 单击Hide菜单项,会隐藏窗口。单击托盘图标,会触发单击图标事件,在终端会输出如下信息: You clicked the icon 单击图标的同时,在图标附近会显示一条消息,单击消息窗口,会在终端输出如下信息: You clicked the message 代码位置: src/interesting_gui/pyqt6/bubble.py import sys from PyQt6.QtWidgets import QApplication, QWidget, QSystemTrayIcon, QMenu from PyQt6.QtGui import QIcon,QAction from PyQt6.QtCore import QCoreApplication class MainWindow(QWidget): def __init__(self): super().__init__() self.initUI() def initUI(self): self.setWindowTitle("Tray Icon Example") self.resize(300, 200) self.tray_icon = QSystemTrayIcon() # 创建一个QSystemTrayIcon对象 self.tray_icon.setIcon(QIcon("images/tray.png")) # 设置图标 self.tray_icon.setToolTip("This is a tray icon") # 设置提示信息 self.tray_icon.activated.connect(self.on_activated) # 连接activated信号 self.tray_icon.messageClicked.connect(self.on_message_clicked) # 连接messageClicked信号 self.tray_menu = QMenu() # 创建一个QMenu对象 self.show_action = QAction("Show", self.tray_menu) # 创建一个QAction对象,用于显 # 示窗口 self.hide_action = QAction("Hide", self.tray_menu) # 创建一个QAction对象,用于隐 # 藏窗口 self.quit_action = QAction("Quit", self.tray_menu) # 创建一个QAction对象,用于退 # 出程序 self.show_action.triggered.connect(self.show) # 连接triggered信号,显示窗口 self.hide_action.triggered.connect(self.hide) # 连接triggered信号,隐藏窗口 self.quit_action.triggered.connect(QCoreApplication.instance().quit) # 连接triggered信号,退出程序 self.tray_menu.addAction(self.show_action) # 将QAction对象添加到QMenu对象中 self.tray_menu.addAction(self.hide_action) self.tray_menu.addSeparator() self.tray_menu.addAction(self.quit_action) self.tray_icon.setContextMenu(self.tray_menu) # 将QMenu对象设置为QSystemTrayIcon # 对象的上下文菜单 self.tray_icon.show() # 显示QSystemTrayIcon对象 def on_activated(self, reason): # 定义一个槽函数,用于处理用户单击 # 图标的事件 if reason == QSystemTrayIcon.ActivationReason.Trigger: # 如果用户单击了图标 print("You clicked the icon") self.tray_icon.showMessage("Hello World", "This is a message from PyQt", QIcon("images/tray.png")) # 显示对话气泡 def on_message_clicked(self): # 定义一个槽函数,用于处理用户单击 # 对话气泡的事件 print("You clicked the message") QCoreApplication.instance().quit() # 退出程序 if __name__ == '__main__': app = QApplication(sys.argv) window = MainWindow() window.show() sys.exit(app.exec()) 运行程序,单击图标,会显示一个消息窗口(对话气泡),macOS中的对话气泡如图511所示。Windows中的对话气泡如图512所示。Ubuntu Linux并不会出现对话气泡,但其他功能正常。 图511macOS中的对话气泡 图512Windows中的对话气泡 5.4小结 本章介绍的内容相当有意思,尽管这些功能对于大多数应用程序不是必需的,但如果自己的应用程序有这些功能,会显得更酷、更专业。尤其是在状态栏中添加图标,读者可以将一些常用的功能添加到图标菜单中,这样用户就可以很方便使用这些功能了。本章的很多内容使用了第4章讲的PyQt6,所以如果读者对PyQt6不了解,请先阅读相关内容。