第 12 章

交互组件

本章共 6 个小节 · PySide6 Basic Tutorial
本章要点:
  • 进度条(QProgressBar)
  • 滑动条(QSlider)和仪表盘(QDial)
  • 系统托盘图标
  • 工具提示
12.1

进度条

QProgressBar组件的功能是显示当前正在运行的任务进度,用户可以根据已处理的进度来决定继续等待还是取消任务。进度条常用的方案有大文件下载、数据压缩等。

进度条默认是水平呈现的,可通过setOrientation方法修改为垂直呈现。为了让进度条能按照应用需求呈现进度,QProgressBar组件一般要设置3个整数值。

  1. 最大值:通过setMaximum方法设置,例如100。
  2. 最小值:通过setMinimum方法设置,例如0。
  3. 当前值:当前进度,通过 setValue方法设置,如50。

如果当前进度超出最大值或最小值,QProgressBar组件就会重置,变为无进度显示状态。调用setRange方法可以同时设置最大值和最小值,例如:

progressBar.setRange(0, 800)

上述代码设置 QProgressBar的最大值为 800,最小值为 0。

12.1.1 示例:水平和垂直进度条

本示例将演示 QProgressBar 组件的两个呈现方向。默认水平呈现,可通过编程方式改为垂直呈现。

核心代码如下:

#窗口 window = QWidget() #设置窗口大小 window.resize(420, 380) #设置布局 layout = QFormLayout() window.setLayout(layout) #第一个进度条,水平方向 pb1 = QProgressBar(window) #最大值100,最小值0 pb1.setRange(0, 100) #设置当前值 pb1.setValue(67) #第二个进度条,垂直方向 pb2 = QProgressBar(window) #最大值40,最小值1 pb2.setMaximum(40), pb2.setMinimum(1) #设置当前值 pb2.setValue(13) #修改方向 pb2.setOrientation(Qt.Orientation.Vertical) #将进度条组件添加到布局中 layout.addRow("水平进度条:", pb1) layout.addRow("垂直进度条:", pb2) #显示窗口 window.show()

示例窗口使用 QFormLayout类进行布局,第一行显示水平方向的进度条,第二行将显示垂直方向的进度条。最终效果如图12-1所示。

图 12-1 不同方向的进度条
图 12-1 不同方向的进度条

12.1.2 示例:模拟耗时任务

本示例将模拟一个需要长时间运行的任务,并使用QProgressBar组件实时显示任务的进度。通常,耗时任务应放在新线程上执行(非主线程),并通过信号来报告进度。

示例的实现步骤如下。

  1. 模拟任务将运行在独立的线程上,需要从 QThread 派生一个自定义类。本示例将命名为 MyThread。
class MyThread(QThread): ......
  1. 在MyThread 类中定义 reportProgress 信号,用于向主线程实时报告进度。该信号包含一个整型值,表示当前处理进度。
reportProgress = Signal(int)
  1. 定义 setData 方法,用于接收来自主线程的数据(进度的最小值和最大值)。
def setData(self, min: int, max: int): self._max = max self._min = min
  1. 重写 run 方法,实现模拟的耗时任务。
def run(self): if not getattr(self, '_max'): self._max = 100 if not getattr(self, '_min'): self._min = 0 #模拟长时间运行的任务 current = self._min while current <= self._max: QThread.msleep(100) #报告进度 self.reportProgress.emit(current) current += 1
  1. 定义窗口类 MyWindow,派生自 QWidget 类。
class MyWindow(QWidget): ......
  1. 定义 sendData 信号,与 MyThread 类的 setData 方法连接后,可以实现向新线程传递数据。
sendData = Signal(int, int)

该信号带有两个参数,分别是进度条的最小值与最大值。

  1. 在 __init__ 方法中初始化窗口。
def __init__(self): super().__init__() self.resize(280, 160) #布局 layout = QVBoxLayout() self.setLayout(layout) #进度条 self.pb = QProgressBar(self) #设置最大值、最小值 self.pb.setRange(0, 80) layout.addWidget(self.pb) #按钮 self.btn = QPushButton("启动任务", self) layout.addWidget(self.btn)
  1. 实例化 MyThread 类。
self.theThread = MyThread(self)
  1. 建立当前窗口的 sendData 信号与 MyThread 对象的 setData 方法的连接。
self.sendData.connect(self.theThread.setData)
  1. 将 MyThread 对象的 reportProgress 信号连接到当前窗口的 setProgress 方法。
self.theThread.reportProgress.connect(self.setProgress)
  1. 实现 setProgress 方法,更新 QProgressBar 组件的当前进度值。
def setProgress(self, p: int): self.pb.setValue(p)
  1. 与 MyThread 对象的 started、finished 信号(从 QThread 类继承)信号连接。在新线程启动或完成时改变 QPushButton 组件的可用状态(任务执行过程中禁用按钮,任务完成后恢复)。
self.theThread.started.connect(self.onStarted) self.theThread.finished.connect(self.onFinished) ...... def onStarted(self): self.btn.setEnabled(False) def onFinished(self): self.btn.setEnabled(True) self.pb.reset() # 重配进度条
  1. 连接 QPushButton 组件的 clicked 信号,在按钮被单击后启动耗时任务。
self.btn.clicked.connect(self.onBtnClicked) ...... def onBtnClicked(self): # 发出 sendData 信号,告知新线程进度的最小值和最大值 self.sendData.emit(self.pb.minimum(),self.pb.maximum()) # 启动新线程 self.theThread.start()
  1. 初始化和显示窗口。
wind = MyWindow() wind.show()

运行示例程序,然后单击“启动任务”按钮,模拟的耗时任务开始执行。此时QProgressBar组件的进度会实时更新,如图12-2所示。

图12-2 实时更新进度条
图12-2 实时更新进度条

12.1.3 示例:设置进度文本的格式

调用 QProgressBar 组件的 setFormat 方法可以修改进度文本的显示格式。格式文本是通过替换占位符的方式实现的。三种占位符及其含义如下。

  1. %p:表示当前进度的百分比,字符串末尾不包含百分号(%)。
  2. %v:表示当前进度值,即 QProgressBar的 value方法返回的值。
  3. %m:表示总进度值,即maximum-minimum。

假设进度条的最大值为 80,最小值为 0,当前值为 20,那么

%p: 25 %v: 20 %m: 80

若setFormat方法设置的格式字符串为“已完成%v步,共%m步”,最后呈现出来的结果是“已完成20步,共80步”。

本示例将在窗口中放置3个按钮,单击它们可以切换QProgressBar组件的显示格式。关键代码如下:

#窗口 window = QWidget() window.resize(360, 100) window.setWindowTitle("Demo") #布局 layout = QGridLayout(window) #进度条 pb = QProgressBar(window) layout.addWidget(pb, 0, 0, 1, 3) #让文本显示在进度条中间 pb.setAlignment(Qt.AlignmentFlag.AlignCenter) #设置最小值 pb.setMinimum(0) #设置最大值 pb.setMaximum(120) #设置当前值 pb.setValue(48) #3个按钮 btn1 = QPushButton("格式 1", window) btn2 = QPushButton("格式 2", window) btn3 = QPushButton("格式 3", window) layout.addWidget(btn1, 1, 0) layout.addWidget(btn2, 1, 1) layout.addWidget(btn3, 1, 2) #连接信号 btn1.clicked.connect(lambda: pb.setFormat("当前百分比: %p%")) btn2.clicked.connect(lambda: pb.setFormat("真实进度值:%v")) btn3.clicked.connect(lambda: pb.setFormat("%v / %m")) #显示窗口 window.show()

运行示例程序,Qt默认显示当前进度的百分比。单击“格式2”按钮,将显示实际进度值,如图12-3所示。

单击“格式3”按钮,将呈现当前进度和总进度,如图12-4所示。

图 12-3 实际进度值 图 12-4当前进度和总进度
图 12-3 实际进度值
图 12-4 当前进度和总进度
12.2

滑动条

QSlider组件允许用户拖动滑块来输入整数值。该组件适用于有范围限制的数值,如表示颜色的R、G、B值,就可以使用QSlider组件进行输入,其范围为[0,255]。

QSlider组件默认是垂直显示的,若需要水平呈现,可以调用setOrientation方法进行修改。与QProgressBar 组件相似,QSlider 组件也通过 setMaximum、setMinimum 和 setRange 方法设置最大值与最小值(这几个方法继承自QAbstractSlider类)。

setValue方法可设置滑块的当前值。当用户设置了新的值,QSlider组件会发出 valueChanged信号。

该信号带有一个整型参数,表示最新设置的值。

QSlider组件默认不显示刻度,可以通过 setTickPosition方法进行设置。该方法的参数为TickPosition枚举,它定义的值如下。

  1. NoTicks:不显示刻度。
  2. TicksAbove:刻度仅显示在滑动条的上方。
  3. TicksLeft:刻度仅显示在滑动条的左侧。
  4. TicksBelow:刻度仅显示在滑动条的下方。
  5. TicksRight:刻度仅显示在滑动条的右侧。
  6. TicksBothSides:刻度同时显示在滑动条的两边。

当 QSlider 组件水平呈现时,应使用 TicksAbove、TicksBelow;当 QSlider 组件垂直呈现时,应使用 TicksLeft 和 TicksRight。

12.2.1 示例:处理 valueChanged 信号

修改QSlider组件的值有多种方法,例如鼠标拖动滑块、鼠标滚轮、键盘上的方向键。不管用户以何种方式与QSlider组件交互,QSlider 组件都会发出 valueChanged 信号,以指示数值已更新。将代码与该信号连接,可以获取QSlider组件最新的值。

本示例将通过 QLabel 组件来实时显示 QSlider组件的值。示例窗口使用QVBoxLayout 布局,布局内包含QSlider组件和QLabel组件。关键代码如下:

window = QWidget() #布局 layout = QVBoxLayout() window.setLayout(layout) #滑动条 slider = QSlider(window) layout.addWidget(slider) #改为水平方向 slider.setOrientation(Qt.Orientation.Horizontal) #设置最大值和最小值 slider.setRange(0, 40) #标签 label = QLabel(window) layout.addWidget(label)

QSlider组件默认是垂直显示的,因此需要调用setOrientation(Qt.Orientation.Horizontal)改为水平显示。

建立 valueChanged 信号与 onValChanged 函数的连接,更新 QLabel 组件的文本内容。

def onValChanged(val: int): label.setText(f"当前值:{val}") slider.valueChanged.connect(onValChanged)

运行示例程序,拖动滑块来调整 QSlider 组件的值,QLabel 组件实时显示最新的数值,如图 12-5 所示。

图 12-5 显示 QSlider 组件的值
图 12-5 显示 QSlider 组件的值

12.2.2 示例:设置刻度的显示位置

本示例主要演示 setTickPosition方法的使用。示例窗口使用QGridLayout布局,第一行有4个QLabel组件,用于显示说明文本;第二行有4个QSlider组件,代表4种刻度显示方式。

示例的具体实现步骤如下。

  1. 实例化QWidget对象,作为程序主窗口。
win = QWidget()
  1. 为窗口设置布局对象。
layout = QGridLayout() win.setLayout(layout)
  1. 布局的第一行是4个QLabel组件。
lb1 = QLabel(win) lb1.setText("无刻度") layout.addWidget(lb1, 0, 0, Qt.AlignmentFlag.AlignHCenter) lb2 = QLabel(win) lb2.setText("刻度在左侧") layout.addWidget(lb2, 0, 1, Qt.AlignmentFlag.AlignHCenter) lb3 = QLabel(win) lb3.setText("刻度在右侧") layout.addWidget(lb3, 0, 2, Qt.AlignmentFlag.AlignHCenter) lb4 = QLabel(win) lb4.setText("两侧都有刻度") layout.addWidget(lb4, 0, 3, Qt.AlignmentFlag.AlignHCenter)
  1. 布局的第二行是 4 个 QSlider 组件。
sld1 = QSlider(win) sld1.setRange(0, 20) sld1.setTickPosition(QSlider.TickPosition.NoTicks) layout.addWidget(sld1, 1, 0, Qt.AlignmentFlag.AlignHCenter) sld2 = QSlider(win) sld2.setRange(0, 20) sld2.setTickPosition(QSlider.TickPosition.TicksLeft) layout.addWidget(sld2, 1, 1, Qt.AlignmentFlag.AlignHCenter) sld3 = QSlider(win) sld3.setRange(0, 20) sld3.setTickPosition(QSlider.TickPosition.TicksRight) layout.addWidget(sld3, 1, 2, Qt.AlignmentFlag.AlignHCenter) sld4 = QSlider(win) sld4.setRange(0, 20) sld4.setTickPosition(QSlider.TickPosition.TicksBothSides) layout.addWidget(sld4, 1, 3, Qt.AlignmentFlag.AlignHCenter)

4个QSlider组件的区别是调用 setTickPosition方法所设置的刻度位置。

  1. 显示窗口。
win.show()

运行示例程序,结果如图12-6所示。

图 12-6 刻度的 4 种显示方式
图 12-6 刻度的 4 种显示方式

12.2.3 步长

在按下键盘上的方向键时,QSlider组件数值的变化量称为步长。setSingleStep和 setPageStep方法都可以设置步长值。

setSingleStep方法是设置每次按下方向键(上、下、左、右箭头)后数值的变化量。假设QSlider组件是水平呈现的,当前值是7,调用 setSingleStep(2),那么按下“→”键后,QSlider的值会增加2,变成9。再按一下“←”键,QSlider组件的值减去2,变回7。

setPageStep 方法用于设置 Page Up 和 Page Down 按键所产生的步长值。假设 QSlider 组件是垂直显示的,当前值是10,若调用 setPageStep(5),那么按下 Page Up 键,QSlider 组件的值就变为 15。

12.2.4 示例:设置 QSlider 组件的步长

本示例定义的主窗口类为 MyWindow,派生自QMainWindow。窗口左侧是 QDockWidget 组件,其中包含4个QSpinBox组件,分别用于修改QSlider组件的最大值、最小值、单步步长以及页面步长。

单步步长调用 setSingleStep 方法设置,页面步长使用 setPageStep 方法设置。

示例的具体实现步骤如下。

  1. 在 MyWindow类中实现 initCentral 方法。为主窗口创建内容区域。内容区域包括 QSlider 和QLabel 组件。QLabel 组件用于显示 QSlider 组件的值。
def initCentral(self): frame = QFrame() frame.setFrameShape(QFrame.Shape.Box) layout = QBoxLayout(QBoxLayout.Direction.TopToBottom) frame.setLayout(layout) #滑动条 self.slider = QSlider(frame) layout.addWidget(self.slider, 1, Qt.AlignmentFlag.AlignHCenter) #标签 lb = QLabel(frame) layout.addWidget(lb, 0) self.slider.valueChanged.connect(lambda v: lb.setText(f'当前值: {v}')) self.setCentralWidget(frame)
  1. 实现 initDockWindows 方法,初始化停靠在主窗口左侧的 DockWidget 组件。该 QDockWidget的内容区域包括4个QSpinBox组件。
  1. 下面两个 QSpinBox组件用于设置QSlider组件的最大值和最小值。
spboxMax = QSpinBox(content) spboxMax.setRange(20, 200) #valueChanged 信号连接到 QSlider 组件的 setMaximum 方法 spboxMax.valueChanged.connect(self.slider.setMaximum) layout.addRow("最大值:",spboxMax) spboxMin = QSpinBox(content) spboxMin.setRange(0, 100) spboxMin.valueChanged.connect(self.slider.setMinimum) layout.addRow("最小值:",spboxMin)
  1. 下面两个 QSpinBox 组件用于设置 QSlider 组件的单步步长和页面步长。
spboxSingleStep = QSpinBox(content) spboxSingleStep.setRange(1, 10) #连接信号 spboxSingleStep.valueChanged.connect(self.slider.setSingleStep) layout.addRow("单步步长:", spboxSingleStep) spboxPageStep = QSpinBox(content) spboxPageStep.setRange(3, 20) #连接信号 spboxPageStep.valueChanged.connect(self.slider.setPageStep) layout.addRow("页面步长:", spboxPageStep)
  1. initDockWindows 方法的完整代码如下:
def initDockWindows(self): self.dock = QDockWidget("基本参数", self) self.dock.setFeatures(QDockWidget.DockWidgetFeature.NoDockWidgetFeatures) #Dock窗口的内容组件 content = QFrame() content.setFrameShape(QFrame.Shape.Panel) layout = QFormLayout() content.setLayout(layout) #下面两个组件用于设置滑动条的最大值和最小值 spboxMax = QSpinBox(content) spboxMax.setRange(20, 200) #valueChanged 信号连接到QSlider 组件的 setMaximum 方法 spboxMax.valueChanged.connect(self.slider.setMaximum) layout.addRow("最大值:", spboxMax) spboxMin = QSpinBox(content) spboxMin.setRange(0, 100) spboxMin.valueChanged.connect(self.slider.setMinimum) layout.addRow("最小值:",spboxMin) layout.addItem(QSpacerItem(0, 20)) #以下QSpinBox用于设置步长 spboxSingleStep = QSpinBox(content) spboxSingleStep.setRange(1, 10) #连接信号 spboxSingleStep.valueChanged.connect(self.slider.setSingleStep) layout.addRow("单步步长:",spboxSingleStep) spboxPageStep = QSpinBox(content) spboxPageStep.setRange(3, 20) #连接信号 spboxPageStep.valueChanged.connect(self.slider.setPageStep) layout.addRow("页面步长:",spboxPageStep) #为QSpinBox设置默认值 spboxMax.setValue(60) spboxMin.setValue(0) spboxSingleStep.setValue(2) spboxPageStep.setValue(5) self.dock.setWidget(content) #将 Dock窗口添加到主窗口中 self.addDockWidget(Qt.DockWidgetArea.LeftDockWidgetArea, self.dock)

4 个 QSpinBox 的 valueChanged 信号依次连接到 QSlider 组件的 setMaximum、setMinimum、setSingleStep 和 setPageStep 方法。上述方法在 QSpinBox 组件被修改后会自动调用。

  1. 初始化并显示主窗口。
win = MyWindow() win.show()

运行示例程序,在窗口左侧的Dock窗口中,设置QSlider组件的最大值为80,最小值为10,单步步长值为2,页面步长值为5,如图12-7所示。

此时,滑动条(QSlider组件)的最小值是 10,按下 Page Up 键,QSlider组件的值变为 15;再按一次 Page Up键,QSlider 组件的值就变为20,如图12-8所示。

图 12-7 设置 QSlider 的步长 图 12-8 QSlider 组件的最新值
图 12-7 设置 QSlider 的步长
图 12-8 QSlider 组件的最新值
12.3

仪表盘

仪表盘组件(QDial)的外观为正方形,用户通过旋转表针来设置当前数值。QDial的基类是QAbstractSlider,因此它的使用方法和 QSlider 相似,也支持使用键盘上的方向键或 Page Up 键、PageDown 键来旋转表针。

12.3.1 示例:使用 QDial 组件

本示例将在窗口上创建一个 QDial 组件和一个 QLabel 组件,通过 QDial 组件的 valueChanged 信号让 QLabel 组件实时更新。

关键代码如下:

#应用程序窗口 window = QWidget() #布局 layout = QVBoxLayout() window.setLayout(layout) # QDial 组件 dial = QDial(window) #设置最大值和最小值 dial.setRange(0, 100) layout.addWidget(dial, 1) # QLabel 组件 lb = QLabel(window) layout.addWidget(lb, 0) #连接valueChanged 信号 dial.valueChanged.connect(lambda val: lb.setText(f"当前数值: {val}")) #显示窗口 window.show()

和QSlider组件一样,设置数值范围时既可以用 setRange方法,也可以使用 setMaximum、setMinimum方法。

运行示例程序后,通过单击和拖动鼠标,就能调整QDial组件的值,如图12-9所示。

图 12-9 QDial 组件的当前数值
图 12-9 QDial 组件的当前数值

12.3.2 示例:显示刻度线

QDial 组件的刻度线默认是隐藏的,若要显示,则需要调用 setNotchesVisible方法,并将 visible参数设置为True。还可以调用 setNotchTarget方法来调节刻度线的密集程度,参数值表示刻度线之间的像素距离。默认为3.7像素。

本示例将演示带刻度线的QDial组件。关键代码如下:

#程序窗口 window = QWidget() window.resize(330, 350) #布局 layout = QGridLayout() window.setLayout(layout) # QDial 组件 dial = QDial(window) layout.addWidget(dial, 0, 0) layout.setRowStretch(0, 1) #设置范围 dial.setMaximum(200) dial.setMinimum(1) #显示刻度 dial.setNotchesVisible(True) #设置刻度线之间的距离 dial.setNotchTarget(2.5) # QLabel 组件 lbVal = QLabel(window) layout.addWidget(lbVal, 1, 0) layout.setRowStretch(1, 0) #连接信号 def onValChanged(value: int): #设置QLabel组件的文本 lbVal.setText("当前数值:{val}".format(val = value)) dial.valueChanged.connect(onValChanged) #显示窗口 window.showNormal()

setNotchesVisible(True)表示开启显示刻度线功能,刻度线之间的距离设置为2.5像素。

运行效果如图12-10所示。

图12-10 显示刻度线
图12-10 显示刻度线

注意调整窗口尺寸。如果窗口过小,QDial组件的刻度线会显示不全。

12.3.3 wrapping属性

使用 setWrapping 方法可以修改 wrapping 属性,当设置为 True 时,QDial 组件刻度线的起点与终点相连,表针可以朝任意方向旋转。通过下面的代码可以直观地对比出开启与未开启wrapping属性的区别:

# QDial 组件 dial = QDial(window) #最大值 dial.setMaximum(100) #最小值 dial.setMinimum(0) #显示刻度 dial.setNotchesVisible(True) ...... # QCheckBox 组件 ckbWrapping = QCheckBox(window) ckbWrapping.setText("Set wrapping enabled") ...... #连接信号 ckbWrapping.toggled.connect(dial.setWrapping)

上述代码将 QCheckBox 组件的 toggled 信号连接到 QDial 组件的 setWrapping 方法。当 QCheckBox的状态改变后会自动修改 wrapping 属性。

如图 12-11 所示,当 wrapping 属性为 False 时,QDial 组件的表盘底部会留出一段空白,以区分刻度的起点与终点。

选中“Set wrapping enabled”后,QDial 组件刻度的起点与终点连在一起,表针可以任意旋转,如图 12-12 所示。

图 12-11 QDial 的 wrapping 属性为 False 图 12-12 QDial 的 wrapping 属性为 True
图 12-11 QDial 的 wrapping 属性为 False
图 12-12 QDial 的 wrapping 属性为 True
12.4

QLCDNumber

QLCDNumber组件模拟LCD屏幕,可以显示数字以及少量的字母和符号(如A、C、H、L、Y、g、-等)。如果传递给QLCDNumber组件的内容中包含不支持的字符,将以空格替代。

要设置 QLCDNumber 组件的显示内容,应调用 display 方法,该方法有以下 3个重载:

def display(num: float) def display(num: int) def display(str: str)

前两个重载分别向参数传递整型和浮点数值,第3个重载向参数传递的是字符串内容。字符串内容中如果存在不支持的字符,QLCDNumber组件将用空格替代。

12.4.1 示例:显示整数和浮点数

本示例将创建两个QLCDNumber实例,分别用于显示整数值和浮点数值。核心代码如下:

win = QWidget() layout = QFormLayout() win.setLayout(layout) lcd1 = QLCDNumber(win) #显示整数 lcd1.display(1234) layout.addRow("显示整数:", lcd1) lcd2 = QLCDNumber(win) #显示浮点数 lcd2.display(0.25) layout.addRow("显示浮点数:", lcd2)

上述代码的运行结果如图12-13所示。

图12-13 显示整数和浮点数
图12-13 显示整数和浮点数

12.4.2 示例:切换进制

QLCDNumber组件默认显示十进制数值,调用setMode方法可以设置为二进制、八进制或十六进制,也可以使用以下便捷方法来切换进制。

setBinMode #二进制 setHexMode #十六进制 setOctMode #八进制 setDecMode #十进制

本示例将使用QLCDNumber组件来显示整数值36,并且支持切换不同进制的数值。DemoWindow类的实现代码如下:

class DemoWindow(QWidget): def __init__(self): super().__init__() self.initUI() def initUI(self): #布局 layout = QGridLayout() self.setLayout(layout) # QLCDNumber 组件 self.lcdNum = QLCDNumber(self) #设置显示位数 self.lcdNum.setDigitCount(10) #设置数值 self.lcdNum.display(36) layout.addWidget(self.lcdNum, 0, 0, 1, 4) #4个按钮,用于切换4种进制 self.btnDec = QPushButton("十进制", self) self.btnBin = QPushButton("二进制", self) self.btnOct = QPushButton("八进制", self) self.btnHex = QPushButton("十六进制",self) layout.addWidget(self.btnDec, 1, 0) layout.addWidget(self.btnBin, 1, 1) layout.addWidget(self.btnOct, 1, 2) layout.addWidget(self.btnHex, 1, 3) #连接clicked信号 self.btnDec.clicked.connect(self.lcdNum.setDecMode) self.btnBin.clicked.connect(self.lcdNum.setBinMode) self.btnOct.clicked.connect(self.lcdNum.setOctMode) self.btnHex.clicked.connect(self.lcdNum.setHexMode)

在实例化 QLCDNumber组件后,调用 setDigitCount方法设置该组件能显示的最大数位。此处设置为10,能显示10个字符。由于默认的位数是5,无法容纳36的二进制值(100100)。4个QPushButton组件分别用于切换 4 种进制,它们的 clicked 信号依次连接到 setDecMode、setBinMode、setOctMode 和setHexMode方法。单击某个按钮后,与 clicked信号连接的方法就会被调用。

运行示例程序,默认显示十进制数值36,如图12-14所示。

单击“二进制”按钮,就会显示为二进制数值,如图12-15所示。

图12-14 显示十进制数值 图12-15 显示二进制数值
图12-14 显示十进制数值
图12-15 显示二进制数值
12.5

托盘图标

许多桌面环境都有一个特殊的区域叫“系统托盘”(或“通知区域”),应用程序可以向系统托盘区域添加自己的图标。应用程序可以隐藏主窗口长期运行,并通过系统托盘中的图标与用户交互。例如,托盘图标可以在需要时向用户发出提示信息,也可以在图标上添加上下文菜单,用户通过菜单命令执行常用任务。

应用程序可以通过QSystemTrayIcon类显示、隐藏系统托盘图标,或设置上下文菜单。由于不需要呈现窗口部件,QSystemTrayIcon 并不从 QWidget 类派生,而是从 QObject 派生。但 QSystemTrayIcon实例可以与 QWidget 对象建立对象树关系——QWidget 对象是 QSystemTrayIcon 对象的父级。

12.5.1 示例:显示和隐藏托盘图标

要在系统托盘中显示图标,需要调用 QSystemTrayIcon 类的 show 方法;相反地,调用 hide 方法可以隐藏图标。

本示例在程序窗口上垂直放置两个按钮。第一个按钮的功能是显示托盘图标,第二个按钮的功能是隐藏图标。程序代码如下:

win = QWidget() #窗口标题 win.setWindowTitle("显示或隐藏系统托盘图标") #窗口大小 win.resize(200, 160) #垂直布局 layout = QVBoxLayout() win.setLayout(layout) #两个按钮 btnShow = QPushButton("显示托盘图标", win) btnHide = QPushButton("隐藏托盘图标",win) layout.addWidget(btnShow) layout.addWidget(btnHide) #初始化图标 sysTray = QSystemTrayIcon(QIcon("clock.png"), win) #连接按钮的clicked信号 def showIcon(): #显示图标 sysTray.show() def hideIcon(): #隐藏图标 sysTray.hide() btnShow.clicked.connect(showIcon) btnHide.clicked.connect(hideIcon)

运行示例程序,单击窗口上的“显示托盘图标”按钮,桌面的系统托盘区域就会多出一个图标(当前应用程序);单击“隐藏托盘图标”按钮后,系统托盘区域将移除刚刚设置的图标。

以下静态方法可用于检测当前环境是否支持系统托盘图标。

  1. isSystemTrayAvailable:系统托盘当前是否可用。若可用则返回True,否则返回False。
  2. supportsMessages:系统托盘是否支持显示消息通知。返回True表示支持,False表示不支持。

12.5.2 示例:添加上下文件菜单

本示例将演示在托盘图标上添加上下文菜单,用户右击系统托盘上的图标即可调出上下文菜单。

在初始化 QSystemTrayIcon 实例后,创建 QMenu 实例,再通过 addAction 方法添加菜单项。本示例会添加三个菜单:“显示/隐藏主窗口”“命令1”“命令2”。代码如下:

trayIco = QSystemTrayIcon(QIcon("icon.png"), window) #添加上下文菜单 menu = QMenu(window) action1 = menu.addAction("显示/隐藏主窗口") action1.setCheckable(True) #连接信号 action1.toggled.connect(window.setVisible) #另外两个菜单项 action2 = menu.addAction("命令1") action2.triggered.connect(lambda:QMessageBox.information(window,"提示","执行【命令1】")) action3 = menu.addAction("命令2") action3.triggered.connect(lambda:QMessageBox.information(window,"提示","执行【命令2】")) trayIco.setContextMenu(menu) #显示图标 trayIco.show() #默认显示窗口 action1.setChecked(True)

第一个菜单项(“显示/隐藏主窗口”)要使用 setCheckable 方法打开 check 功能,然后 QAction 对象的 toggled 信号连接到窗口对象的 setVisible 方法。这样就可以实现用菜单项的 check 状态来控制窗口的显示或隐藏。另外两个菜单项均处理 triggered 信号,并用消息对话框显示文本信息。

在初始化上下文菜单后,必须调用 QSystemTrayIcon 对象的 setContextMenu 方法进行关联,否则托盘图标无法显示菜单。

当示例程序运行后,在桌面的系统托盘中会看到应用程序设置的图标,右击图标,会弹出上下文菜单,如图12-16所示。

图12-16 托盘图标的上下文菜单
图12-16 托盘图标的上下文菜单

12.5.3 示例:发送“气球”消息

系统托盘图标可以向用户展示“气球”消息,消息的出现位置一般在托盘图标上方或系统通知栏附近(如 Windows 11)。调用以下方法即可发送消息:

def showMessage( title: str, msg: str, icon: QSystemTrayIcon.MessageIcon = ..., msecs: int = ... ) def showMessage( title: str, msg: str, icon: Union[QIcon, QPixmap], msecs: int = ... )

title参数指定消息的标题,msg参数指定消息的内容。icon参数指定显示的图标,该参数有两种使用方案。

使用系统图标。由QSystemTrayIcon.MessageIcon 枚举的值来指定。Information 表示普通信息通知,Warning为警告消息,Critical的严重程度较高,表示错误信息。

使用自定义的图标。类型为 QIcon对象或 QPixmap 对象。

msecs参数指定消息的持续时间,单位是毫秒,默认为10000。

本示例的主界面允许输入消息标题和内容,以及选择消息持续时间。4个按钮代表4种系统图标—NoIcon、Information、Warning 和 Critical。

MyWindow类的实现代码如下:

class MyWindow(QWidget): def __init__(self): super().__init__() layout = QFormLayout() self.setLayout(layout) #消息标题 self.titleEdit = QLineEdit(self) layout.addRow("标题:", self.titleEdit) #消息内容 self.bodyEdit = QLineEdit(self) layout.addRow("内容:", self.bodyEdit) #消息持续时间 self.slidOn = QSlider(Qt.Orientation.Horizontal, self) #范围:2~10秒 self.slidOn.setRange(2, 10) layout.addRow("持续时间:", self.slidOn) #按钮组 self.btnGroup = QButtonGroup(self) self.btnGroup.addButton(QPushButton("无图标", self), 0) self.btnGroup.addButton(QPushButton("信息", self), 1) self.btnGroup.addButton(QPushButton("警告", self), 2) self.btnGroup.addButton(QPushButton("错误", self), 3) #连接信号 self.btnGroup.idClicked.connect(self.onButtonClicked) #布局按钮 btnlayout = QHBoxLayout() for b in self.btnGroup.buttons(): btnlayout.addWidget(b) layout.addRow(btnlayout) #托盘图标 self.trayIco = QSystemTrayIcon(QIcon("wel.png"), self) #图标可见 self.trayIco.setVisible(True) #QButtonGroup.idClicked信号连接到此方法 def onButtonClicked(self, id: int): title = self.titleEdit.text() content = self.bodyEdit.text() #确定所使用的图标 icon = QSystemTrayIcon.MessageIcon.NoIcon if id == 0 else QSystemTrayIcon.MessageIcon.Information if id == 1 else QSystemTrayIcon.MessageIcon.Warning if id == 2 else QSystemTrayIcon.MessageIcon.Critical if id == 3 else QSystemTrayIcon.MessageIcon.NoIcon #显示提示消息 self.trayIco.showMessage(title, content, icon, self.slidOn.value())

4个按钮(QPushButton)组件由QButtonGroup 对象管理,idClicked 信号连接 onButtonClicked方法,然后根据id的值来确定显示消息时所使用的系统图标。

示例运行效果如图12-17所示。

图12-17 设置“气球”消息的参数
图12-17 设置“气球”消息的参数
12.6

工具提示

工具提示将呈现一个短暂的小窗口,用于说明某个组件的功能。工具提示一般使用简单的文本,如有特殊需求,也可以使用HTML标记。

12.6.1 示例:使用 setToolTip 方法

QWidget类公开了setToolTip方法,设置工具提示非常方便。调用该方法时直接传递提示文本即可。

本示例将在窗口上创建两个按钮组件,然后调用setToolTip方法为按钮组件设置工具提示。详细的代码如下:

#程序窗口 window = QWidget() #窗口标题 window.setWindowTitle("工具提示") #布局 layout = QVBoxLayout() window.setLayout(layout) #两个按钮 btn1 = QPushButton("开 始", window) btn2 = QPushButton("停 止", window) layout.addWidget(btn1) layout.addWidget(btn2) #为按钮设置工具提示 btn1.setToolTip("单击此按钮开始游戏") btn2.setToolTip("单击此按钮结束游戏") #显示窗口 window.show()

运行应用程序后,将鼠标指针移动到按钮上并停留片刻,就会看到提示信息了,如图12-18所示。

图12-18 按钮上的工具提示
图12-18 按钮上的工具提示

12.6.2 示例:拦截 ToolTip 事件

本示例将演示通过事件过滤器(Event Filter)拦截 ToolTip事件,然后调用 QToolTip类的 showText方法显示提示文本。showText是静态方法,可直接调用。

应用程序窗口中有3个QRadioButton组件。代码如下:

class CustWindow(QWidget): def __init__(self): super().__init__() #布局 layout = QVBoxLayout() self.setLayout(layout) lbDisplay = QLabel("请选择一种模式:", self) layout.addWidget(lbDisplay) #3个单选按钮 self.rd1 = QRadioButton("模式-1", self) self.rd2 = QRadioButton("模式-2", self) self.rd3 = QRadioButton("模式-3", self) layout.addWidget(self.rd1, 0, Qt.AlignmentFlag.AlignHCenter) layout.addWidget(self.rd2, 0, Qt.AlignmentFlag.AlignHCenter) layout.addWidget(self.rd3, 0, Qt.AlignmentFlag.AlignHCenter) layout.addStretch(1) ......

3 个 QRadioButton 组件都安装事件过滤器,代码如下:

self.rd1.installEventFilter(self) self.rd2.installEventFilter(self) self.rd3.installEventFilter(self)

在窗口类中重写 eventFilter 方法。如果遇到 ToolTip 事件,就设置工具提示。

def eventFilter(self, watched: QObject, event: QEvent) -> bool: #判断事件类型,只处理ToolTip事件 if event.type() == QEvent.Type.ToolTip: #事件参数是 QHelpEvent 类型 helpev: QHelpEvent = event #被监听对象是 QRadioButton 类型 rdButton: QRadioButton = watched #提示文本 tipText = "Nonthing" #看看被拦截的是哪个 QRadioButton 实例 if rdButton is self.rd1: tipText = "模式1:仅睡眠,不关机" if rdButton is self.rd2: tipText ="模式2:进入关机状态,但电源未切断" if rdButton is self.rd3: tipText = "模式3:关机,并且切断电源" #显示提示信息 QToolTip.showText(helpev.globalPos(), tipText) #返回值交给基类处理 return super().eventFilter(watched, event)

由于 eventFilter 方法拦截的是 3 个 QRadioButton 组件的事件,因此 watched 参数可能是 rd1,也可能是 rd2或 rd3。需要用 if语句判断当前被拦截的是哪个QRadioButton 组件,以便设置不同的提示文本。

if rdButton is self.rd1: ...... if rdButton is self.rd2: ...... if rdButton is self.rd3: ......

手动显示工具提示需要调用QToolTip.showText静态方法。该方法的声明如下:

def showText(pos: QPoint, text: str, w: Optional[QWidget] = ..., rect: QRect = ..., msecShowTime: int = ...)

pos 参数是提示信息显示的位置,需要指定全局坐标(屏幕坐标)。text 参数是要显示的文本。

w和 rect参数需要一起使用。w指的是要显示提示信息的对象,在本示例中是QRadioButton。rect是w内的某个矩形区域。这两个参数的含义是:当鼠标指针移出 rect所指定的区域后,工具提示就会隐藏。w和 rect参数都是可选的,调用 showText方法时可以忽略。

msecShowTime参数指定提示信息显示的时间,单位是毫秒。默认为-1,表示显示时间由提示文本的长度决定。文本内容越多,显示的时间越长。计算方法可以参考下面的C++源代码:

qsizetype time = 10000 + 40 * qMax(0, textLength - 100);

时长以10000毫秒(即10秒)为基础,当文本长度超过100字符时,每个字符延长40毫秒。

示例程序的运行结果如图12-19所示。

图 12-19 QRadioButton 的提示信息
图 12-19 QRadioButton 的提示信息

12.6.3 示例:在工具提示中使用 HTML

Qt的工具提示文本支持使用HTML。本示例将创建一个包含3个字段的表单窗口,其中“货号”和“数量”字段的输入框设置了工具提示。

3 个 QLineEdit 组件将通过 QFormLayout 对象布局,代码如下:

layout = QFormLayout() window.setLayout(layout) #第一行 txtNo = QLineEdit(window) layout.addRow("货号:", txtNo) #第二行 txtQty = QLineEdit(window) layout.addRow("数量:", txtQty) #第三行 txtRem = QLineEdit(window) layout.addRow("备注:",txtRem)

为前两个QLineEdit组件设置工具提示文本,采用HTML格式。代码如下:

html = """ <p>格式:<i>[品类]-[日期]-[序号]</i></p> <table> <tr> <td>[品类]: </td> <td>用两个字母表示货物类型,如服装类货物就使用"FZ",工艺品类就用"GY"</td> </tr> <tr> <td>[日期]:</td> <td>进货日期,格式为yyyyMMdd,如20190524</td> </tr> <tr> <td>[序号]:</td> <td>入库序号,用四位数字表示.如0001、0057</td> </tr> </table> <p> 例如: <span style="color:blue">FZ-20221028-0073</span> </p> """ txtNo.setToolTip(html) html = """ <p>填写货物数量,包含单位</p> <p> 例如: <span style="color: navy;">15 条</span>、<span style="color: orangered;"> 100 张</span>、<span style="color: green;">97 套</span>等 </p> """ txtQty.setToolTip(html)

运行示例程序,然后将鼠标指针移动到“货号”字段的文本输入框内,稍等片刻,就会出现如图12-20所示的提示信息。

将鼠标指针移到“数量”字段的输入框内,就会看到如图12-21所示的工具提示。

图12-20“货号”字段的提示文本 图12-21 “数量”字段的工具提示
图 12-20 “货号”字段的提示文本
图 12-21 “数量”字段的工具提示

12.6.4 示例:修改调色板

在Qt应用程序内部,工具提示是由QLabel组件呈现的,因此通过修改调色板相关参数,可以自定义工具提示的文本以及背景颜色。QToolTip类提供了获取和设置调色板的静态方法:palette方法返回正在使用的调色板,setPalette方法则用于设置新的调色板对象(QPalette类的实例)。

本示例通过调色板对象,将工具提示的文本设置为白色,背景为黑色。自定义窗口类的代码如下:

class DemoWindow(QWidget): def __init__(self): super().__init__() #布局 rootLayout = QVBoxLayout() self.setLayout(rootLayout) #选项1 self.chBox1 = QCheckBox(self) self.chBox1.setText("启动时加载历史记录") rootLayout.addWidget(self.chBox1) #选项2 self.chBox2 = QCheckBox(self) self.chBox2.setText("下载元数据") rootLayout.addWidget(self.chBox2) #选项3 self.chBox3 = QCheckBox(self) self.chBox3.setText("关闭时备份") rootLayout.addWidget(self.chBox3) #设置工具提示 self.chBox1.setToolTip("程序启动时读取上一次的阅读记录") self.chBox2.setToolTip("自动下载正在阅读的资料的最新消息") self.chBox3.setToolTip("程序退出时保存资料的阅读记录") ......

修改工具提示的调色板需要在初始化窗口之前完成。由于某些系统主题会导致工具提示的调色板失效(例如“WindowsVista”主题),所以需要先修改应用程序的默认主题。代码如下:

app = QApplication() app.setStyle("Fusion")

如果想知道当前系统环境支持哪些主题名称,可以使用下面的代码将它们打印到屏幕上:

print(QStyleFactory.keys())

下面代码先从QToolTip类中获取现有的调色板,然后修改文本和背景颜色,最后调用 setPalette方法设置新的调色板。

palette = QToolTip.palette() #修改颜色 palette.setColor(QPalette.ColorGroup.Inactive, QPalette.ColorRole.ToolTipBase, QColor("black")) palette.setColor(QPalette.ColorGroup.Inactive, QPalette.ColorRole.ToolTipText, QColor("white")) #设置调色板 QToolTip.setPalette(palette)

工具提示所在的颜色分组是Inactive(非活动窗口),颜色角色分别是ToolTipBase(工具提示窗口的背景颜色)和ToolTipText(工具提示窗口的文本颜色)。

示例程序运行后的效果如图12-22所示。

图12-22 自定义工具提示的外观
图12-22 自定义工具提示的外观