我想以动画形式显示QProgressBar的值变化. 例如,我还没有找到比仅将所有值乘以QProgressBar0,然后设置过渡动画更好的解决方案. 例如,如果要将值从5更改为4,则进度条动画会将值从5000更改为4000,但显示的是数字5和4,而不是5000和4000.以下是我是如何实现它的:

class CustomProgressBar(QWidget):
    def __init__(self,
                 parent,
                 curr_value: int,
                 max_value: int):
        super().__init__(parent=parent)
        self.MULTIPLIER = 1000

        layout = QVBoxLayout(self)

        self.progress_bar = self.pg = QProgressBar(self)
        self.progress_bar.setTextVisible(True)
        self.progress_bar.setAlignment(Qt.AlignmentFlag.AlignCenter)
        self.progress_bar.setFixedSize(QSize(1000, 100))
        self.progress_bar.setMaximum(max_value * self.MULTIPLIER)
        self.progress_bar.setValue(curr_value * self.MULTIPLIER)

        displayed_value = int(self.pg.value() / self.MULTIPLIER)
        displayed_max_value = int(self.pg.maximum() / self.MULTIPLIER)
        self.progress_bar.setFormat(f"{displayed_value}/{displayed_max_value}")

        layout.addWidget(self.progress_bar)

        # Animation
        self.animation = QPropertyAnimation(self.progress_bar, b"value", self.progress_bar)
        self.animation.setDuration(500)
        self.animation.setEasingCurve(QEasingCurve.Type.OutCubic)

    def set_value(self, new_value):
        new_value = new_value * self.MULTIPLIER if new_value > 0 else 0
        self.animate_value_change(new_value)

        displayed_new_value = int(new_value / self.MULTIPLIER)
        displayed_max_value = int(self.pg.maximum() / self.MULTIPLIER)
        self.progress_bar.setFormat(f"{displayed_new_value}/{displayed_max_value}")

    def animate_value_change(self, new_value):
        """Animation of value change"""

        # changing text color
        if new_value <= self.progress_bar.maximum() / 2:
            self.progress_bar.setStyleSheet("QProgressBar {color: white;}")
        else:
            self.progress_bar.setStyleSheet("QProgressBar {color: black;}")

        # animating bar
        self.animation.stop()
        self.animation.setStartValue(self.progress_bar.value())
        self.animation.setEndValue(new_value)
        self.animation.start()

下面是它的工作原理: https://youtu.be/tUyuoVAD-KY

这部动画片的效果有点不稳定和不稳定. 我找到了一个非常奇怪的解决方案:在格式上加%v:

self.progress_bar.setFormat(f"{displayed_value}/{displayed_total_value} %v")

是的,动画要流畅得多: https://youtu.be/8eOC9xC8K7w

为什么会是这样呢?有没有可能在不显示真实值QProgressBar(%v)的情况下获得同样的流畅度?

Full Python example:

from PySide6.QtWidgets import QWidget, QProgressBar, QVBoxLayout, QApplication
from PySide6.QtCore import Qt, QPropertyAnimation, QEasingCurve, QObject, Signal, QSize
from threading import Thread
import time


STYLE = """
QProgressBar {
    border-radius: 3px;
    background-color: rgb(26, 39, 58);
}

QProgressBar::chunk {
    border-radius: 3px;
    background-color: qlineargradient(spread:pad, x1:0, y1:0.54, x2:1, y2:0.5625, stop:0 rgba(0, 85, 255, 255), stop:1 rgba(8, 255, 227, 255));
}
"""


class CustomProgressBar(QWidget):
    def __init__(self,
                 parent,
                 curr_value: int,
                 max_value: int):
        super().__init__(parent=parent)
        self.MULTIPLIER = 1000
        self.setStyleSheet(STYLE)
        layout = QVBoxLayout(self)

        self.progress_bar = self.pg = QProgressBar(self)
        self.progress_bar.setTextVisible(True)
        self.progress_bar.setAlignment(Qt.AlignmentFlag.AlignCenter)
        self.progress_bar.setFixedSize(QSize(1000, 100))
        self.progress_bar.setMaximum(max_value * self.MULTIPLIER)
        self.progress_bar.setValue(curr_value * self.MULTIPLIER)

        displayed_value = int(self.pg.value() / self.MULTIPLIER)
        displayed_max_value = int(self.pg.maximum() / self.MULTIPLIER)
        self.progress_bar.setFormat(f"{displayed_value}/{displayed_max_value}")

        layout.addWidget(self.progress_bar)

        # Animation
        self.animation = QPropertyAnimation(self.progress_bar, b"value", self.progress_bar)
        self.animation.setDuration(500)
        self.animation.setEasingCurve(QEasingCurve.Type.OutCubic)

    def set_value(self, new_value):
        new_value = new_value * self.MULTIPLIER if new_value > 0 else 0
        self.animate_value_change(new_value)

        displayed_new_value = int(new_value / self.MULTIPLIER)
        displayed_max_value = int(self.pg.maximum() / self.MULTIPLIER)
        self.progress_bar.setFormat(f"{displayed_new_value}/{displayed_max_value}")

    def animate_value_change(self, new_value):
        """Animation of value change"""

        # changing text color
        if new_value <= self.progress_bar.maximum() / 2:
            self.progress_bar.setStyleSheet("QProgressBar {color: white;}")
        else:
            self.progress_bar.setStyleSheet("QProgressBar {color: black;}")

        # animating bar
        self.animation.stop()
        self.animation.setStartValue(self.progress_bar.value())
        self.animation.setEndValue(new_value)
        self.animation.start()


class MyCounter(QObject):
    value_changed = Signal(int)

    def __init__(self, value: int, step: int = 1):
        super().__init__()

        self._value = value
        self.step = step

    @property
    def value(self) -> int:
        return self._value

    @value.setter
    def value(self, value: int):
        self._value = value if value >= 0 else 0
        self.value_changed.emit(self.value)

    def make_step(self):
        self.value -= self.step

    def process(self, delay: int | float):
        time.sleep(3)
        default_value = self.value
        while True:
            while self.value:
                self.make_step()
                time.sleep(delay)
            self.value = default_value
            time.sleep(delay)

    def run(self, delay: int | float = 1):
        Thread(target=self.process, args=(delay, ), daemon=True).start()


if __name__ == "__main__":
    app = QApplication()

    widget = QWidget()
    layout = QVBoxLayout(widget)

    counter = MyCounter(30, 4)
    progress_bar = CustomProgressBar(widget, 30, 30)

    counter.value_changed.connect(progress_bar.set_value)

    layout.addWidget(progress_bar)

    widget.show()
    counter.run(1)
    app.exec()

推荐答案

TL;DR

QProgressBar有一个内部函数,可以判断它是否真的需要重新绘制自己,但在某些情况下(特别是在使用QSS和较大的值时),这是无效的.

要强制更新进度条,请将其valueChanged信号连接到update()函数:

    self.progress_bar.valueChanged.connect(self.progress_bar.update)

Why the %v format text causes a proper update

我们必须考虑到,QProgressBar是最古老的Qt小部件之一,在那个时候,使用显示进度近似值的任意"块"来显示这样的条是很常见的,这也允许一定程度的优化:只有当"显示的块"(进度)的数量实际发生变化时,小部件才被更新.记住,我们谈论的是20年前为计算机编写的东西,基于20年前首次出现的UI概念,其中"块"是屏幕上显示的简单字符:这种优化在当时非常重要.

现在,所有这些都是由QProgressBar repaintRequired()(参见Qt6 sources,但它从Qt4开始就存在了)的一个私有函数引起的,该函数在value属性更改时判断它是否真的需要重新绘制.

更具体地说,当调用setValue()时(这是动画更新属性值时发生的情况),它会通过调用repaintRequired()函数来判断新值是否确实需要绘制,并最终根据其返回值进行重新绘制.

这是一个优化,当进度条的外观不需要完全重新绘制时,如果细微的值更改(理论上)不会影响其外观,则可以避免不必要的重新绘制.

仅当该函数返回True时,QProgressBar才实际调用repaint(),这可能会发生,具体取决于以下判断:

  • 计算"绘制的值差",这是新值和last painted的值(内部值更新为paintEvent())之间的绝对差;如果相同,则返回False(不重画);
  • 如果该值等于最小值or最大值,则返回True
  • 如果文本可见,则仅当显示的值发生更改时才返回True(因为它需要重新绘制),这在至少满足以下条件之一时发生:
    • 格式包含%v(这是您的"工作" case );
    • 格式包含%p,上面的"绘制差异"大于或等于总步长(最大-最小)除以True;这意味着在这种情况下,"进度矩形"的visual精度主要基于百分比,以整数为基础;
  • 查询当前样式,返回groove size(整条显示的空间)乘以绘制的差值是否等于块大小乘以总步数的greater

最后一点是什么导致了您的问题,因为在某些条件下,由于槽大小和值范围之间的巨大差异,它"认为"块大小没有引起外观变化.

现在,针对您的特定情况的一个简单解决方案是添加以下行:

    self.animation.valueChanged.connect(self.progress_bar.update)

这将确保,无论发生什么情况,当值发生更改时,进度条都会请求重新绘制,即使上面的私有函数会显示不同的情况.

不过,更合适的方法是执行以下操作,该方法在任何情况下都有效(即使没有动画):

    self.progress_bar.valueChanged.connect(self.progress_bar.update)

请注意,由于您实际上是在使用QSS覆盖进度条的整体外观,因此最好对其使用默认的fusion样式,并避免在QSS中设置Chunk Width属性;这是因为该样式具有更合适的样式表实现,并且可以在所有平台上一致地工作:

    self.progress_bar.setStyle(QFactoryStyle.create('fusion'))

以上部分与未解析的QTBUG-50919有关,因为设置块宽度时,使用半径大于宽度的圆角边框会出现问题.这是因为,默认情况下,块大小要大得多,并且用于显示"块",而不是整个进度条"值区域".

一种可能的 Select

虽然上述建议在技术上可以解决问题,但您的实现有几个重要的问题:

  • 文本 colored颜色 仅在"实际"值改变并开始动画时更新,但不考虑当前动画位置;
  • 由于上述原因,以及您只根据"目标"值来决定 colored颜色 的事实,在某些情况下,文本可能部分或几乎完全不可读;
  • 最重要的是,它使用另一个小部件作为进度条的容器,而不是actual个进度条,限制了它的正确访问和使用,包括函数和信号;

我建议你采取一种可能更合适的不同方法.它涉及覆盖小部件的绘制,但由于它使用现有的QStyle功能,因此不会更改整体外观,因为这是QProgressBar在paintEvent()中所做的基本工作.

其概念是QProgressBar绘制使用QStyleOptionProgressBar来绘制其内容,并且该选项是使用当前进度条状态设置的,最重要的是minimummaximumprogress值.

相反,在覆盖中,我们用动画的值替换这些值,动画does使用倍增.这样做的主要好处是进度条的实际value属性是always一致的,而不是返回值multiplied.

因为我们已经覆盖了绘制,所以我还改进了文本外观: colored颜色 同时绘制为白色和黑色,但将clipped绘制为基本的进度矩形,以便始终可读,即使进度位于文本中间.这样做的好处是,您还可以通过使用具有colorselection-color属性的QSS来设置相关 colored颜色 .

Screenshot of the result

STYLE = '''
    QProgressBar {
        color: navy;
        selection-color: lavender;
        border-radius: 3px;
        background-color: rgb(26, 39, 58);
    }

    QProgressBar::chunk:horizontal {
        border-radius: 3px;
        background-clip: padding;
        background-color: qlineargradient(spread:pad, 
            x1:0, y1:0.54, x2:1, y2:0.5625, 
            stop:0 rgba(0, 85, 255, 255), stop:1 rgba(8, 255, 227, 255)
        );
    }
'''

class CustomProgressBar(QProgressBar):
    MULTIPLIER = 1000
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Preferred)
        self.setStyleSheet(STYLE)
        self.setFormat('%v/%m')
        self.setAlignment(Qt.AlignmentFlag.AlignCenter)

        self.animation = QVariantAnimation(self)
        self.animation.setDuration(500)
        self.animation.setEasingCurve(QEasingCurve.Type.OutCubic)

        # set the animation state based on the current progress bar state; this
        # is required for proper painting until the value is actually changed
        aniValue = (self.value() - self.minimum()) * self.MULTIPLIER
        self.animation.setStartValue(aniValue)
        self.animation.setEndValue(aniValue)

        self.animation.valueChanged.connect(self.update)

    def setValue(self, value):
        current = self.value()
        if current == value:
            return
        super().setValue(value)
        if self.animation.state():
            self.animation.stop()
            self.animation.setStartValue(self.animation.currentValue())
        else:
            self.animation.setStartValue(current * self.MULTIPLIER)
        self.animation.setEndValue(value * self.MULTIPLIER)
        self.animation.start()

        # setValue() uses repaint(), we need to do it too to avoid flickering
        self.repaint()

    def minimumSizeHint(self):
        return super().sizeHint()

    def sizeHint(self):
        return QSize(1000, 100)

    def paintEvent(self, event):
        qp = QStylePainter(self)
        opt = QStyleOptionProgressBar()
        self.initStyleOption(opt)

        # set the option values based on the animation values
        opt.minimum = self.minimum() * self.MULTIPLIER
        opt.maximum = self.maximum() * self.MULTIPLIER
        opt.progress = self.animation.currentValue()
        opt.textVisible = False

        style = self.style()
        qp.drawControl(style.ControlElement.CE_ProgressBar, opt)

        progRect = style.subElementRect(
            style.SubElement.SE_ProgressBarContents, opt, self)
        progressPos = (
            (opt.progress - opt.minimum) 
            * progRect.width() 
            / (opt.maximum - opt.minimum)
        )
        left = QRect(
            progRect.left(), progRect.top(), 
            int(progressPos), progRect.height()
        )
        textRect = style.subElementRect(
            style.SubElement.SE_ProgressBarLabel, opt, self)

        if (left & textRect).isValid():
            qp.setClipRect(left)
            qp.setPen(self.palette().color(QPalette.ColorRole.Text))
            qp.drawText(textRect, Qt.AlignmentFlag.AlignCenter, self.text())

        if left.right() < textRect.right():
            qp.setPen(self.palette().color(QPalette.ColorRole.HighlightedText))
            right = progRect.adjusted(left.right() + 1, 0, 0, 0)
            qp.setClipRect(right)
            qp.drawText(textRect, Qt.AlignmentFlag.AlignCenter, self.text())


...

if __name__ == "__main__":
    app = QApplication([])

    widget = QWidget()
    layout = QVBoxLayout(widget)

    counter = MyCounter(30, 3)
    progress_bar = CustomProgressBar(maximum=30, value=30)

    counter.value_changed.connect(progress_bar.setValue)

    layout.addWidget(progress_bar)

    widget.show()
    counter.run(1)
    app.exec()

结束语

Directly using a subclass of the "target" widget is always the preferred choice, since it allows to properly and directly access Qt functions and features and also improves object management and memory usage. Consider, for instance, the benefit of accessing the value property of the actual progress bar, and the fact that it returns its real value.
Also, if you are not careful enough, adding a further widget can cause layout discrepancies, due to default layout margins and spacings.

请注意,上面的setValue()覆盖将not替换existing slot,这意味着对property的任何外部更改不是通过显式调用"new"setValue()方法来实现的,则104更改将影响动画.例如,使用progress_bar.setProperty('value', 5).

While your MyCounter implementation is acceptable in principle, the fact that it's based on a not persistent Thread object may create some issues, since you have no direct nor easy access to it once run is called, for instance, if you need to stop that thread or properly interact with it.
The more appropriate solutions are QObject/QThread pairs (using moveToThread()) or even a QThread subclass that overrides run(). Unless you really know what you're doing and why, always try to use existing Qt classes; in situations like these, using Python's Thread would pose no benefit, especially considering that the low level implementation of the thread is fundamentally identical.

Python相关问答推荐

Pandas或pyspark跨越列创建

Django关于UniqueBindition的更新

情节生成的饼图文本超出页面边界

Python中的锁定类和线程以实现dict移动

如何对行使用分段/部分.diff()或.pct_change()?

如何在Python中使用ijson解析SON期间检索文件位置?

无法使用python.h文件; Python嵌入错误

按照行主要蛇扫描顺序对点列表进行排序

实现的差异取决于计算出的表达是直接返回还是首先存储在变量中然后返回

如何使用上下文管理器创建类的实例?

如何调整spaCy token 化器,以便在德国模型中将数字拆分为行末端的点

标题:如何在Python中使用嵌套饼图可视化分层数据?

在Python中管理打开对话框

如何让这个星型模式在Python中只使用一个for循环?

递归访问嵌套字典中的元素值

索引到 torch 张量,沿轴具有可变长度索引

如何在Pyplot表中舍入值

手动设置seborn/matplotlib散点图连续变量图例中显示的值

为什么常规操作不以其就地对应操作为基础?

在Admin中显示从ManyToMany通过模型的筛选结果