使用 QPainter 从零开始构建一个功能齐全的自定义小部件

上一篇教程中,我们介绍QPainter并了解了一些基本的位图绘图操作,您可以使用这些操作在诸如曲面之类的QPainter 表面QPixmap上绘制点、线、矩形和圆。

这种在表面上绘制图形的过程QPainter实际上是 Qt 中所有控件绘制的基础。现在你知道如何使用它了,QPainter你也知道如何绘制自己的自定义控件了!

在本文中,我们将运用目前所学的知识来构建一个全新的自定义GUI 组件。为了演示,我们将构建以下组件:一个带有旋钮控制的可自定义 PowerBar 电量计。

我们的自定义小部件 PowerBar 正在运行

这个组件实际上是复合组件自定义组件的结合体:我们使用了 Qt 内置QDial组件来显示刻度盘,而电源条则由我们自己绘制。然后,我们将这两个部分组合成一个父组件,它可以无缝地添加到任何应用程序中,无需了解其具体构造。最终的组件提供了一个通用的QAbstractSlider界面,并添加了一些用于配置电源条显示的选项。

按照这个例子,你就能制作出属于你自己的自定义 GUI 小部件——无论是内置组件的组合,还是完全原创的、自己绘制的奇妙作品。

入门

正如我们之前看到的,复合组件其实就是应用了布局的组件,而该布局本身又包含多个其他组件。最终生成的“组件”可以像其他组件一样使用,您可以根据需要隐藏或显示其内部结构。

使用 Python 和 Qt6 创建 GUI 应用程序 (作者:Martin Fitzpatrick )——(PyQt6 版)使用 Python 制作应用程序的实用指南——销量超过 10,000 册!

 

下面给出了PowerBar小部件的概要——我们将从这个概要框架开始逐步构建自定义小部件。将其保存到名为 的文件中。power_bar.py

Python
from PySide6 import QtCore, QtGui, QtWidgets
from PySide6.QtCore import Qt

class _Bar(QtWidgets.QWidget):
    pass

class PowerBar(QtWidgets.QWidget):
    """
    Custom Qt Widget to show a power bar and dial.
    Demonstrating compound and custom-drawn widget.
    """

    def __init__(self, steps=5, *args, **kwargs):
        super().__init__(*args, **kwargs)

        layout = QtWidgets.QVBoxLayout()
        self._bar = _Bar()
        layout.addWidget(self._bar)

        self._dial = QtWidgets.QDial()
        layout.addWidget(self._dial)

        self.setLayout(layout)

这简单地定义了我们自定义的电源条,它定义在_Bar对象中——这里只是未修改的子类QWidget。该PowerBar组件(即完整的组件)将其QVBoxLayout与内置组件结合使用,QDial以便将它们一起显示。

我们还需要一个简单的演示应用程序来展示这个小部件。

Python
import sys
from PySide6 import QtWidgets
from power_bar import PowerBar

app = QtWidgets.QApplication(sys.argv)
bar = PowerBar()
bar.show()
app.exec()

我们不需要创建父窗口,QMainWindow因为任何没有父窗口的小部件本身就是一个窗口。我们的自定义PowerBar小部件会像普通窗口一样显示。

这就是你需要做的全部,只需将其保存在与之前文件相同的文件夹中,文件名类似 `.js` 即可demo.py。你可以随时运行此文件来查看你的小部件的效果。现在运行它,你应该会看到类似这样的内容:

我们的组件是一个 QDial,它上方有一个不可见的空白组件(相信我)。

我们的组件是一个 QDial,它上方有一个不可见的空白组件(相信我)。

如果将窗口向下拉伸,你会发现表盘上方的空间比下方的空间要多——这部分空间被我们(目前不可见的)小部件占据了_Bar

paintEvent

处理paintEvent程序是 PySide 中所有控件绘制的核心。

小部件的每一次完整或部分重绘都是通过paintEvent小部件自身处理的绘制事件触发的。绘制事件paintEvent可以通过以下方式触发:

——但这种情况也可能由其他多种原因引起。重要的是,当paintEvent触发某个事件时,你的组件能够重新绘制它。

如果控件足够简单(比如我们这个),通常每次发生任何变化时,只需重新绘制整个控件即可。但对于更复杂的控件,这种方法效率会非常低下。在这种情况下,paintEvent需要指定需要更新的特定区域。我们将在后续更复杂的示例中用到这一点。

现在我们先做一些非常简单的事情,将整个控件填充为单一颜色。这样我们就能看到要绘制的区域,从而开始绘制条形图。将以下代码添加到类_Bar中。

Python
    def paintEvent(self, e):
        painter = QtGui.QPainter(self)
        brush = QtGui.QBrush()
        brush.setColor(QtGui.QColor("black"))
        brush.setStyle(Qt.BrushStyle.SolidPattern)
        rect = QtCore.QRect(
            0,
            0,
            painter.device().width(),
            painter.device().height(),
        )
        painter.fillRect(rect, brush)

使用我们在上一部分中介绍的绘制说明,您可以完全自定义自定义小部件的样式、外观和感觉。

定位

现在我们可以看到这个_Bar小部件,可以调整它的位置和大小。拖动窗口形状,你会看到两个小部件会改变形状以适应可用空间。这正是我们想要的效果,但是小部件的QDial垂直方向也扩展得过大,留下了原本可以用来放置工具栏的空白区域。

将自定义小部件部分填充为黑色。

将自定义小部件部分填充为黑色。

我们可以setSizePolicy_Bar组件上使用此方法来确保它尽可能地展开。通过使用QSizePolicy.Policy.MinimumExpanding提供的参数sizeHint,组件将以最小值展开,并尽可能地展开。

Python
class _Bar(QtWidgets.QWidget):

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)

        self.setSizePolicy(
            QtWidgets.QSizePolicy.Policy.MinimumExpanding,
            QtWidgets.QSizePolicy.Policy.MinimumExpanding,
        )

    def sizeHint(self):
        return QtCore.QSize(40,120)

虽然小部件调整大小的方式有点别扭,但它仍然不够完美QDial,不过我们的工具栏现在正在扩展以填充所有可用空间。

已设置 QSizePolicy.Policy.MinimumExpanding 的小部件。

已设置 QSizePolicy.Policy.MinimumExpanding 的小部件。

定位问题解决后,我们现在可以开始定义绘制方法,以便在小部件的顶部(目前为黑色)绘制 PowerBar 计量条。

正在更新显示

现在我们的画布已经完全被黑色填充,接下来我们将使用QPainter绘制命令在小部件上实际绘制一些内容。

在开始制作吧台之前,我们需要进行一些测试,以确保能够根据表盘上的数值更新显示屏。请将代码替换_Bar.paintEvent为以下代码。

Python
    def paintEvent(self, e):
        painter = QtGui.QPainter(self)

        brush = QtGui.QBrush()
        brush.setColor(QtGui.QColor("black"))
        brush.setStyle(Qt.BrushStyle.SolidPattern)
        rect = QtCore.QRect(
            0,
            0,
            painter.device().width(),
            painter.device().height(),
        )
        painter.fillRect(rect, brush)

        # Get current state.
        dial = self.parent()._dial
        vmin, vmax = dial.minimum(), dial.maximum()
        value = dial.value()

        pen = painter.pen()
        pen.setColor(QtGui.QColor("red"))
        painter.setPen(pen)

        font = painter.font()
        font.setFamily("Times")
        font.setPointSize(18)
        painter.setFont(font)

        painter.drawText(
            25,
            25,
            "{}-->{}<--{}".format(vmin, value, vmax),
        )
        painter.end()

这段代码像之前一样绘制黑色背景,然后.parent()访问父 PowerBar控件,并通过父QDial控件获取_dial值。由此我们可以获取当前值以及允许的最小值和最大值范围。最后,我们使用绘图器绘制这些值,就像上一部分一样。

T> 我们把当前值、最小值和最大值的处理留给这里QDial,但我们也可以自己存储该值,并使用拨盘的信号来保持同步。

运行这段代码,晃动旋钮……结果什么也没发生。虽然我们已经定义了paintEvent处理程序,但旋钮改变时并没有触发重绘。

你可以通过调整窗口大小来强制刷新,刷新后应该就能看到文本了。这设计挺巧妙,但用户体验糟糕透顶——“只需调整应用窗口大小就能看到设置!”

为了解决这个问题,我们需要让_Bar控件能够根据表盘上数值的变化进行重绘。我们可以使用QDial.valueChanged信号来实现这一点,将其连接到一个自定义的槽方法,该方法.refresh()会触发一次完整的重绘。

将以下方法添加到_Bar小部件中。

Python
    def _trigger_refresh(self):
        self.update()

…并将以下内容添加到__init__父组件的代码块中PowerBar

Python
self._dial.valueChanged.connect(self._bar._trigger_refresh)

现在重新运行代码,你会看到显示屏在你转动旋钮(用鼠标点击并拖动)时自动更新。当前值以文本形式显示。

以文本形式显示当前 QDial 值。

以文本形式显示当前 QDial 值。

抽签

现在显示屏已经更新并显示了表盘的当前值,我们可以开始绘制实际的柱状图了。这部分稍微复杂一些,需要用到一些数学运算来计算柱状图的位置,但我们会一步一步地讲解,让大家清楚地了解其中的原理。

下图显示了我们的目标——一系列N 个盒子,从控件边缘向内凹陷,盒子之间有间隙。

我们开发这个小部件的目标是什么?

我们开发这个小部件的目标是什么?

计算要画什么

要绘制的方框数量取决于当前值,以及该值与配置的最小值和最大值之间的距离QDial。我们在上面的示例中已经获得了这些信息。

Python
dial = self.parent()._dial
vmin, vmax = dial.minimum(), dial.maximum()
value = dial.value()

如果x 位于 x和yvalue的中间,那么我们要画一半的方框(如果总共有 4 个方框,则画 2 个)。如果x 位于 y 处,那么我们要画出所有的方框。vminvmaxvaluevmax

为此,我们首先将值转换value为 0 到 1 之间的数字,其中0 = vmin1 = vmax。我们首先vmin从 中减去value,以调整可能值的范围,使其从零开始——即从vmin...vmax0…(vmax-vmin)。然后将此值除以vmax-vmin(新的最大值),即可得到 0 到 1 之间的数字。

诀窍在于将此值(pc下面称为)乘以步数,这样就能得到 0 到 5 之间的数字——即要绘制的方格数。

Python
pc = (value - vmin) / (vmax - vmin)
n_steps_to_draw = int(pc * 5)

我们将结果进行包装int,将其转换为整数(向下取整),以消除任何不完整的方框。

更新drawText绘制事件中的方法,改为输出这个数字。

Python
pc = (value - vmin) / (vmax - vmin)
n_steps_to_draw = int(pc * 5)
painter.drawText(25, 25, "{}".format(n_steps_to_draw))

转动旋钮后,您将看到一个介于 0 和 5 之间的数字。

绘图盒

接下来,我们要将 0 到 5 这个数字转换成画布上绘制的条形数量。首先,移除drawText字体和笔设置,因为我们不再需要它们了。

为了精确绘制,我们需要知道画布的大小——也就是控件的大小。我们还会在边缘添加一些内边距,以便在黑色背景上留出一些空间,使方块边缘与背景之间更加协调。

所有尺寸均以QPainter像素为单位。

Python
        padding = 5

        # Define our canvas.
        d_height = painter.device().height() - (padding * 2)
        d_width = painter.device().width() - (padding * 2)

我们取高度和宽度,然后分别减去2 * padding原来的值——结果是 2 倍,因为我们在左右(以及上下)边缘都添加了内边距。这样我们就得到了最终的活动画布区域,大小为d_heightd_width

小部件画布、内边距和内部绘制区域。

小部件画布、内边距和内部绘制区域。

我们需要将平面分成d_height5 个相等的部分,每个部分对应一个方块——我们可以简单地计算出方块的高度d_height / 5。此外,由于我们希望方块之间留有空隙,因此我们需要计算出这个步长中有多少被空隙(顶部和底部,因此减半)占据,又有多少被方块本身占据。

已有超过10,000 名开发者购买了《使用 Python 和 Qt 创建 GUI 应用程序》!

使用 Python 和 Qt6 创建 GUI 应用程序

可下载电子书(PDF、ePub)及完整源代码

购买力平价
中国开发者使用优惠码DQ3JY5购买所有书籍和课程可享35% 折扣。

Python
step_size = d_height / 5
bar_height = step_size * 0.6
bar_spacer = step_size * 0.4 / 2

这些数值就是我们在画布上绘制方块所需的全部信息。为此,我们从 0 开始计数到步数减 1 range,然后fillRect为每个方块绘制一个区域。

Python
brush.setColor(QtGui.QColor('red'))

for n in range(5):
    rect = QtCore.QRect(
        padding,
        int(padding + d_height - ((n + 1) * step_size) + bar_spacer),
        d_width,
        int(bar_height),
    )
    painter.fillRect(rect, brush)

填充颜​​色初始设置为红色画笔,但稍后我们会进行自定义。

要绘制的方框fillRect被定义为一个QRect对象,我们依次向其传递左侧 x 坐标、顶部 y 坐标、宽度和高度。

宽度是画布的完整宽度减去内边距,内边距我们之前已经计算并存储好了d_width左侧x值同样是padding控件左侧边缘的距离(5px)。

我们计算出的柱子高度bar_height是 0.6 倍step_size

这样就只剩下参数 2 d_height - ((1 + n) * step_size) + bar_spacer ,它指定要绘制的矩形的顶部 y坐标。这是绘制方块时唯一会改变的计算。

这里需要记住的关键一点是,y坐标从QPainter画布顶部开始向下递增。这意味着在 x = 0 处绘制图形,d_height实际上是在画布的最底部绘制。当我们从一个点绘制矩形时,矩形会起始位置向右下方绘制。

要在最底部画一个方块,我们必须从向上画d_height-step_size一个方块的位置开始,以便留出向下画的空间。

在我们的条形图中,我们依次绘制方块,从底部开始向上绘制。因此,第一个方块必须放置在 处d_height-step_size,第二个方块放置在 处d_height-(step_size*2)。我们的循环从 0 向上迭代,所以我们可以使用以下公式来实现这一点——

Python
d_height - ((1 + n) * step_size

最后需要调整的是,由于每个方块只占据了方框的一部分step_size(目前是 0.6),所以我们需要添加一些边距,将方块从方框边缘移到中间,最后再添加底部边缘的边距。这样就得到了最终的公式——

Python
padding + d_height - ((n+1) * step_size) + bar_spacer,

这将生成以下布局。

下图n显示了当前值,并在方框周围画了一个蓝色方框,step_size以便可以看到填充和间隔的实际效果。

布局中条形之间的间距和块绘制顺序。

布局中条形之间的间距和块绘制顺序。

将以上内容整合到我们的文件中,即可得到以下代码。这段代码power_bar.py会生成一个带有红色方块的电量条控件。您可以来回拖动滚轮,电量条也会随之上下移动。

Python
from PySide6 import QtCore, QtGui, QtWidgets
from PySide6.QtCore import Qt

class _Bar(QtWidgets.QWidget):

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)

        self.setSizePolicy(
            QtWidgets.QSizePolicy.Policy.MinimumExpanding,
            QtWidgets.QSizePolicy.Policy.MinimumExpanding,
        )

    def sizeHint(self):
        return QtCore.QSize(40, 120)

    def paintEvent(self, e):
        painter = QtGui.QPainter(self)

        brush = QtGui.QBrush()
        brush.setColor(QtGui.QColor("black"))
        brush.setStyle(Qt.BrushStyle.SolidPattern)
        rect = QtCore.QRect(
            0,
            0,
            painter.device().width(),
            painter.device().height(),
        )
        painter.fillRect(rect, brush)

        # Get current state.
        dial = self.parent()._dial
        vmin, vmax = dial.minimum(), dial.maximum()
        value = dial.value()

        padding = 5

        # Define our canvas.
        d_height = painter.device().height() - (padding * 2)
        d_width = painter.device().width() - (padding * 2)

        # Draw the bars.
        step_size = d_height / 5
        bar_height = step_size * 0.6
        bar_spacer = step_size * 0.4 / 2

        pc = (value - vmin) / (vmax - vmin)
        n_steps_to_draw = int(pc * 5)
        brush.setColor(QtGui.QColor("red"))
        for n in range(n_steps_to_draw):
            rect = QtCore.QRect(
                padding,
                int(padding + d_height - ((n + 1) * step_size) + bar_spacer),
                d_width,
                int(bar_height),
            )
            painter.fillRect(rect, brush)

        painter.end()

    def _trigger_refresh(self):
        self.update()

class PowerBar(QtWidgets.QWidget):
    """
    Custom Qt Widget to show a power bar and dial.
    Demonstrating compound and custom-drawn widget.
    """

    def __init__(self, steps=5, *args, **kwargs):
        super().__init__(*args, **kwargs)

        layout = QtWidgets.QVBoxLayout()
        self._bar = _Bar()
        layout.addWidget(self._bar)

        self._dial = QtWidgets.QDial()
        self._dial.valueChanged.connect(self._bar._trigger_refresh)

        layout.addWidget(self._dial)
        self.setLayout(layout)

一个能正常工作的单色电源条。

一个能正常工作的单色电源条。

这已经可以满足需求了,但我们还可以更进一步,提供更多自定义选项,增加一些用户体验改进,并改进与我们的小部件一起使用的 API。

酒吧定制

我们现在有了一个可以通过旋钮控制的电源条。但是,在创建控件时,最好能提供一些选项来配置控件的行为和样式,使其更加灵活。在本部分,我们将添加一些方法来设置可自定义的分段数、颜色、内边距和间距。

我们将提供定制服务的元素如下——

选项 描述
酒吧数量 小部件上显示多少个柱状图?
颜色 每个条形图使用不同的颜色
背景颜色 绘图画布的颜色(默认为黑色)
填充 小部件边缘、栏与画布边缘之间的空白区域。
条形高度/条形百分比 实心条的比例(0…1)(其余部分为相邻条之间的间距)

我们可以将这些属性分别存储在_bar对象上,并在paintEvent方法中使用它们来改变对象的行为。

该函数_Bar.__init__已更新,可接受一个初始参数,该参数可以是柱状图的数量(整数)或柱状图的颜色(列表QColor,可以是十六进制值或名称)。如果提供的是数字,则所有柱状图都将显示为红色。如果提供的是颜色列表,则柱状图的数量将根据颜色列表的长度确定。此外,还设置了 `n` self._bar_solid_percent、 `color` 和 `colors` 的默认值。self._background_colorself._padding

Python
class _Bar(QtWidgets.QWidget):
    clickedValue = QtCore.Signal(int)

    def __init__(self, steps, *args, **kwargs):
        super().__init__(*args, **kwargs)

        self.setSizePolicy(
            QtWidgets.QSizePolicy.Policy.MinimumExpanding,
            QtWidgets.QSizePolicy.Policy.MinimumExpanding
        )

        if isinstance(steps, list):
            # list of colors.
            self.n_steps = len(steps)
            self.steps = steps

        elif isinstance(steps, int):
            # int number of bars, defaults to red.
            self.n_steps = steps
            self.steps = ['red'] * steps

        else:
            raise TypeError('steps must be a list or int')

        self._bar_solid_percent = 0.8
        self._background_color = QtGui.QColor('black')
        self._padding = 4  # n-pixel gap around edge.

同样地,我们更新了它PowerBar.__init__,使其接受 steps 参数,并将其传递出去。

Python
class PowerBar(QtWidgets.QWidget):
    def __init__(self, steps=5, *args, **kwargs):
        super().__init__(*args, **kwargs)

        layout = QtWidgets.QVBoxLayout()
        self._bar = _Bar(steps)

        #...continued as before.

现在我们已经准备好更新方法所需的参数paintEvent。修改后的代码如下所示。

Python
    def paintEvent(self, e):
        painter = QtGui.QPainter(self)

        brush = QtGui.QBrush()
        brush.setColor(self._background_color)
        brush.setStyle(Qt.BrushStyle.SolidPattern)
        rect = QtCore.QRect(
            0,
            0,
            painter.device().width(),
            painter.device().height(),
        )
        painter.fillRect(rect, brush)

        # Get current state.
        dial = self.parent()._dial
        vmin, vmax = dial.minimum(), dial.maximum()
        value = dial.value()

        # Define our canvas.
        d_height = painter.device().height() - (self._padding * 2)
        d_width = painter.device().width() - (self._padding * 2)

        # Draw the bars.
        step_size = d_height / self.n_steps
        bar_height = step_size * self._bar_solid_percent
        bar_spacer = step_size * (1 - self._bar_solid_percent) / 2

        # Calculate the y-stop position, from the value in range.
        pc = (value - vmin) / (vmax - vmin)
        n_steps_to_draw = int(pc * self.n_steps)

        for n in range(n_steps_to_draw):
            brush.setColor(QtGui.QColor(self.steps[n]))
            rect = QtCore.QRect(
                self._padding,
                int(self._padding + d_height - ((1 + n) * step_size) + bar_spacer),
                d_width,
                int(bar_height),
            )
            painter.fillRect(rect, brush)

        painter.end()

现在您可以尝试为 `init` 参数传入不同的值PowerBar,例如增加条形数量或提供颜色列表。下面是一些示例——Bokeh 源代码提供了一个很好的十六进制调色板资源

Python
PowerBar(10)
PowerBar(3)
PowerBar(["#5e4fa2", "#3288bd", "#66c2a5", "#abdda4", "#e6f598", "#ffffbf", "#fee08b", "#fdae61", "#f46d43", "#d53e4f", "#9e0142"])
PowerBar(["#a63603", "#e6550d", "#fd8d3c", "#fdae6b", "#fdd0a2", "#feedde"])

定制酒吧示例。

定制酒吧示例。

你可以通过变量来调整填充设置,self._bar_solid_percent但最好能提供合适的方法来设置这些设置。

为了与从其他继承的方法保持一致,我们对这些外部方法遵循 Qt 标准的驼峰式方法名QDial

Python
    def setColor(self, color):
        self._bar.steps = [color] * self._bar.n_steps
        self._bar.update()

    def setColors(self, colors):
        self._bar.n_steps = len(colors)
        self._bar.steps = colors
        self._bar.update()

    def setBarPadding(self, i):
        self._bar._padding = int(i)
        self._bar.update()

    def setBarSolidPercent(self, f):
        self._bar._bar_solid_percent = float(f)
        self._bar.update()

    def setBackgroundColor(self, color):
        self._bar._background_color = QtGui.QColor(color)
        self._bar.update()

在每种情况下,我们都会在对象上设置私有变量_bar,然后调用_bar.update()该变量来触发控件的重绘。该方法支持将颜色更改为单一颜色,或更新颜色列表——设置颜色列表也可用于更改条形的数量。

由于需要扩展颜色列表,因此没有方法可以设置条形图数量,但您可以尝试自己添加此功能!

以下是一个使用 25px 内边距、全实心条形和灰色背景的示例。

Python
bar = PowerBar(["#49006a", "#7a0177", "#ae017e", "#dd3497", "#f768a1", "#fa9fb5", "#fcc5c0", "#fde0dd", "#fff7f3"])
bar.setBarPadding(2)
bar.setBarSolidPercent(0.9)
bar.setBackgroundColor('gray')

使用这些设置,您将得到以下结果。

条形内边距 2,实线百分比 0.9,灰色背景。

条形内边距 2,实线百分比 0.9,灰色背景。

添加 QAbstractSlider 接口

我们添加了用于配置功率条行为的方法。但目前我们还没有提供QDial从组件中配置标准方法(例如,设置最小值、最大值或步长)的方法。我们可以逐一添加封装方法来实现所有这些功能,但这很快就会变得非常繁琐。

Python
# Example of a single wrapper, we'd need 30+ of these.
def setNotchesVisible(self, b):
    return self._dial.setNotchesVisible(b)

QDial我们可以在外部组件上添加一个简单的处理程序,以便在类中没有直接存在的方法(或属性)时,自动在实例中查找它们。这样,我们既可以实现自己的方法,又能QAbstractSlider免费获得所有优点。

下面显示的是封装器,它是作为自定义__getattr__方法实现的。

Python
def __getattr__(self, name):
    if name in self.__dict__:
        return self[name]

    try:
        return getattr(self._dial, name)
    except AttributeError:
        raise AttributeError(
          "'{}' object has no attribute '{}'".format(self.__class__.__name__, name)
        )

当访问属性(或方法)时——例如,当调用PowerBar.setNotchesVisible(true)Python 内部方法时——Python 会使用__getattr__对象字典来获取当前对象的属性。此处理程序通过对象字典来实现这一点self.__dict__。我们重写了此方法以提供自定义的处理逻辑。

现在,当我们调用 `get_methods()` 时PowerBar.setNotchesVisible(true),这个处理程序首先会检查当前对象(一个PowerBar实例)是否.setNotchesVisible存在 `get_methods()`,如果存在则使用它。如果不存在,则改为getattr()调用self._dial`get_methods()` 并返回找到的内容。这样,我们就可以QDial从自定义小部件访问PowerBar`get_methods()` 的所有方法。

如果QDial也没有该属性,并且引发了异常,AttributeError我们会捕获它,并从我们自定义的小部件中再次引发它,因为它应该在那里。

这适用于任何属性或方法,包括信号。因此,QDial诸如此类的标准信号.valueChanged也可用。

从仪表显示屏更新

目前,您可以通过旋转旋钮来更新能量条的当前值。但如果还可以通过点击能量条上的特定位置或上下拖动鼠标来更新数值,用户体验会更好。为此,我们可以更新组件_Bar以处理鼠标事件。

Python
class _Bar(QtWidgets.QWidget):

    clickedValue = QtCore.Signal(int)

    # ... existing code ...

    def _calculate_clicked_value(self, e):
        parent = self.parent()
        vmin, vmax = parent.minimum(), parent.maximum()
        d_height = self.size().height() + (self._padding * 2)
        step_size = d_height / self.n_steps
        click_y = e.y() - self._padding - step_size / 2

        pc = (d_height - click_y) / d_height
        value = vmin + pc * (vmax - vmin)
        self.clickedValue.emit(value)

    def mouseMoveEvent(self, e):
        self._calculate_clicked_value(e)

    def mousePressEvent(self, e):
        self._calculate_clicked_value(e)

__init__在小部件的模块中,PowerBar我们可以连接到_Bar.clickedValue信号并将值发送出去,self._dial.setValue以设置拨盘上的当前值。

Python
# Take feedback from click events on the meter.
self._bar.clickedValue.connect(self._dial.setValue)

如果现在运行该小部件,您就可以在条形区域中点击,数值将会更新,并且拨盘也会同步旋转。

拖动以更新值

最终代码

以下是我们的 PowerBar 计量器小部件的完整最终代码,名为PowerBar。您可以将其保存到之前的文件(例如名为power_bar.py)上,然后在您自己的任何项目中使用它,或根据您的需求对其进行进一步自定义。

Python
from PySide6 import QtCore, QtGui, QtWidgets
from PySide6.QtCore import Qt

class _Bar(QtWidgets.QWidget):

    clickedValue = QtCore.Signal(int)

    def __init__(self, steps, *args, **kwargs):
        super().__init__(*args, **kwargs)

        self.setSizePolicy(
            QtWidgets.QSizePolicy.Policy.MinimumExpanding,
            QtWidgets.QSizePolicy.Policy.MinimumExpanding,
        )

        if isinstance(steps, list):
            # list of colors.
            self.n_steps = len(steps)
            self.steps = steps

        elif isinstance(steps, int):
            # int number of bars, defaults to red.
            self.n_steps = steps
            self.steps = ['red'] * steps

        else:
            raise TypeError('steps must be a list or int')

        self._bar_solid_percent = 0.8
        self._background_color = QtGui.QColor('black')
        self._padding = 4.0  # n-pixel gap around edge.

    def paintEvent(self, e):
        painter = QtGui.QPainter(self)

        brush = QtGui.QBrush()
        brush.setColor(self._background_color)
        brush.setStyle(Qt.SolidPattern)
        rect = QtCore.QRect(
            0,
            0,
            painter.device().width(),
            painter.device().height(),
        )
        painter.fillRect(rect, brush)

        # Get current state.
        parent = self.parent()
        vmin, vmax = parent.minimum(), parent.maximum()
        value = parent.value()

        # Define our canvas.
        d_height = painter.device().height() - (self._padding * 2)
        d_width = painter.device().width() - (self._padding * 2)

        # Draw the bars.
        step_size = d_height / self.n_steps
        bar_height = step_size * self._bar_solid_percent
        bar_spacer = step_size * (1 - self._bar_solid_percent) / 2

        # Calculate the y-stop position, from the value in range.
        pc = (value - vmin) / (vmax - vmin)
        n_steps_to_draw = int(pc * self.n_steps)

        for n in range(n_steps_to_draw):
            brush.setColor(QtGui.QColor(self.steps[n]))
            rect = QtCore.QRect(
                self._padding,
                self._padding + d_height - ((1 + n) * step_size) + bar_spacer,
                d_width,
                bar_height
            )
            painter.fillRect(rect, brush)

        painter.end()

    def sizeHint(self):
        return QtCore.QSize(40, 120)

    def _trigger_refresh(self):
        self.update()

    def _calculate_clicked_value(self, e):
        parent = self.parent()
        vmin, vmax = parent.minimum(), parent.maximum()
        d_height = self.size().height() + (self._padding * 2)
        step_size = d_height / self.n_steps
        click_y = e.y() - self._padding - step_size / 2

        pc = (d_height - click_y) / d_height
        value = vmin + pc * (vmax - vmin)
        self.clickedValue.emit(value)

    def mouseMoveEvent(self, e):
        self._calculate_clicked_value(e)

    def mousePressEvent(self, e):
        self._calculate_clicked_value(e)


class PowerBar(QtWidgets.QWidget):
    """
    Custom Qt Widget to show a power bar and dial.
    Demonstrating compound and custom-drawn widget.

    Left-clicking the button shows the color-chooser, while
    right-clicking resets the color to None (no-color).
    """

    colorChanged = QtCore.Signal()

    def __init__(self, steps=5, *args, **kwargs):
        super().__init__(*args, **kwargs)

        layout = QtWidgets.QVBoxLayout()
        self._bar = _Bar(steps)
        layout.addWidget(self._bar)

        # Create the QDial widget and set up defaults.
        # - we provide accessors on this class to override.
        self._dial = QtWidgets.QDial()
        self._dial.setNotchesVisible(True)
        self._dial.setWrapping(False)
        self._dial.valueChanged.connect(self._bar._trigger_refresh)

        # Take feedback from click events on the meter.
        self._bar.clickedValue.connect(self._dial.setValue)

        layout.addWidget(self._dial)
        self.setLayout(layout)

    def __getattr__(self, name):
        if name in self.__dict__:
            return self[name]

        return getattr(self._dial, name)

    def setColor(self, color):
        self._bar.steps = [color] * self._bar.n_steps
        self._bar.update()

    def setColors(self, colors):
        self._bar.n_steps = len(colors)
        self._bar.steps = colors
        self._bar.update()

    def setBarPadding(self, i):
        self._bar._padding = int(i)
        self._bar.update()

    def setBarSolidPercent(self, f):
        self._bar._bar_solid_percent = float(f)
        self._bar.update()

    def setBackgroundColor(self, color):
        self._bar._background_color = QtGui.QColor(color)
        self._bar.update()

你应该能够运用这些技巧来创建你自己的自定义小部件。

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐