Skip to content

RoundProgressBar

Qt Base Class: QWidget

Signature: QWidget(self, parent: Optional[PySide6.QtWidgets.QWidget] = None, f: PySide6.QtCore.Qt.WindowType = Default(Qt.WindowFlags)) -> None

Base classes

Name Children Inherits
Widget
prettyqt.widgets.widget

⋔ Inheritance diagram

graph TD
  1473367146768["custom_widgets.RoundProgressBar"]
  1473293661888["widgets.Widget"]
  1473293688240["widgets.WidgetMixin"]
  1473299815024["core.ObjectMixin"]
  140713234304496["builtins.object"]
  1473245548480["gui.PaintDeviceMixin"]
  1473290849680["QtWidgets.QWidget"]
  1473288842240["QtCore.QObject"]
  1473291690208["Shiboken.Object"]
  1473300082368["QtGui.QPaintDevice"]
  1473293661888 --> 1473367146768
  1473293688240 --> 1473293661888
  1473299815024 --> 1473293688240
  140713234304496 --> 1473299815024
  1473245548480 --> 1473293688240
  140713234304496 --> 1473245548480
  1473290849680 --> 1473293661888
  1473288842240 --> 1473290849680
  1473291690208 --> 1473288842240
  140713234304496 --> 1473291690208
  1473300082368 --> 1473290849680
  1473291690208 --> 1473300082368

🛈 DocStrings

Bases: Widget

Source code in prettyqt\custom_widgets\roundprogressbar.py
class RoundProgressBar(widgets.Widget):
    class Position(float, enum.Enum):
        """Start position of progress bar in degrees."""

        Left = 180.0
        Top = 90.0
        Right = 0.0
        Bottom = -90.0

    class BarStyle(enum.IntEnum):
        """Progress bar style."""

        Donut = 1
        Pie = 2
        Line = 3
        Expand = 4

    class ValueType(enum.IntEnum):
        """Value type."""

        value = 1
        percent = 2
        maximum = 3

    def __init__(self, parent: widgets.QWidget | None = None):
        super().__init__(parent)
        self._min_value = 0.0
        self._max_value = 100.0
        self.current_value = 0.0
        self.null_pos = self.Position.Top
        self.bar_style: RoundProgressBar.BarStyle = self.BarStyle.Donut
        self.outline_pen_width = 1.0
        self.data_pen_width = 1.0
        self._rebuild_brush = False
        self.number_format = "%p%"
        self.decimals = 1
        self._update_flags: ValueTypeStr = "percent"
        self.gradient_data: Sequence[gui.QColor] = []

    def minimum(self):
        return self._min_value

    def maximum(self):
        return self._max_value

    # SETTERS -------------------------------------------------------

    def set_null_position(self, position: RoundProgressBar.Position):
        if position != self.null_pos:
            self.null_pos = position
            self._rebuild_brush = True
            self.update()

    def get_null_position(self):
        return self.null_pos

    def set_bar_style(self, style: BarStyleStr | RoundProgressBar.BarStyle):
        if isinstance(style, str):
            BAR_STYLE = dict(
                donut=RoundProgressBar.BarStyle.Donut,
                pie=RoundProgressBar.BarStyle.Pie,
                line=RoundProgressBar.BarStyle.Line,
                expand=RoundProgressBar.BarStyle.Expand,
            )
            style = BAR_STYLE[style]
        if style != self.bar_style:
            self.bar_style = style
            self._rebuild_brush = True
            self.update()

    def set_outline_pen_width(self, width: float):
        if width != self.outline_pen_width:
            self.outline_pen_width = width
            self.update()

    def set_data_pen_width(self, width: float):
        if width != self.data_pen_width:
            self.data_pen_width = width
            self.update()

    def set_data_colors(self, stop_points: list[gui.QColor]):
        if stop_points != self.gradient_data:
            self.gradient_data = stop_points
            self._rebuild_brush = True
            self.update()

    def set_format(self, val: str):
        if val != self.number_format:
            self.number_format = val
            self._value_format_changed()

    def set_decimals(self, count: int):
        if count >= 0 and count != self.decimals:
            self.decimals = count
            self._value_format_changed()

    # SLOTS ---------------------------------------------------------

    @core.Slot(float, float)
    def set_range(self, minval: float, maxval: float):
        self._min_value = min(minval, maxval)
        self._max_value = max(minval, maxval)
        self.current_value = min(
            self._max_value, max(self._min_value, self.current_value)
        )
        self._rebuild_brush = True
        self.update()

    @core.Slot(float)
    def setMinimum(self, val: float):
        self.set_range(val, self._max_value)

    @core.Slot(float)
    def setMaximum(self, val: float):
        self.set_range(self._min_value, val)

    @core.Slot(float)
    def set_value(self, val: float):
        if self.current_value != val:
            self.current_value = min(self._max_value, max(self._min_value, val))
            self.update()

    def get_value(self) -> float:
        return self.current_value

    # PAINTING ------------------------------------------------------

    def paintEvent(self, event: gui.PaintEvent):
        outer_radius = min(self.width(), self.height())
        rect = core.RectF(1, 1, outer_radius - 2, outer_radius - 2)
        with gui.Painter(self) as painter:
            painter.use_antialiasing()
            if self.bar_style != self.BarStyle.Line:
                self._rebuild_data_brush_if_needed()
            painter.fillRect(0, 0, outer_radius, outer_radius, self.palette().window())
            self._draw_base(painter, rect)
            self._draw_value(painter, rect, self.current_value)
            inner_rect, inner_radius = self._calculate_inner_rect(outer_radius)
            self._draw_inner_background(painter, inner_rect)
            self._draw_text(painter, inner_rect, inner_radius, self.current_value)

    def _draw_base(self, painter: gui.Painter, rect: core.RectF):
        match self.bar_style:
            case self.BarStyle.Donut:
                color = self.palette().shadow().color()
                painter.set_pen(color=color, width=self.outline_pen_width)
                painter.setBrush(self.palette().base())
                painter.drawEllipse(rect)
            case self.BarStyle.Line:
                base_color = self.palette().base().color()
                painter.set_pen(color=base_color, width=self.outline_pen_width)
                painter.setBrush(constants.BrushStyle.NoBrush)
                width = self.outline_pen_width / 2
                adjusted = rect.adjusted(width, width, -width, -width)
                painter.drawEllipse(adjusted)
            case self.BarStyle.Pie | self.BarStyle.Expand:
                base_color = self.palette().base().color()
                painter.set_pen(color=base_color, width=self.outline_pen_width)
                painter.setBrush(self.palette().base())
                painter.drawEllipse(rect)

    def _draw_value(self, painter: gui.Painter, rect: core.RectF, value: float):
        if value == self._min_value:
            return
        diff = self.current_value - self._min_value
        value_range = self._max_value - self._min_value
        delta = max(value_range / diff, 0)
        match self.bar_style:
            case self.BarStyle.Expand:
                painter.setBrush(self.palette().highlight())
                color = self.palette().shadow().color()
                painter.set_pen(color=color, width=self.data_pen_width)
                radius = (rect.height() / 2) / delta
                painter.drawEllipse(rect.center(), radius, radius)
            case self.BarStyle.Line:
                color = self.palette().highlight().color()
                painter.set_pen(color=color, width=self.data_pen_width)
                painter.setBrush(constants.BrushStyle.NoBrush)
                pen_width = self.outline_pen_width / 2
                adjusted = rect.adjusted(pen_width, pen_width, -pen_width, -pen_width)
                if value == self._max_value:
                    painter.drawEllipse(adjusted)
                else:
                    arc_length = 360 / delta
                    arc_length = int(-arc_length * 16)
                    painter.drawArc(adjusted, int(self.null_pos * 16), arc_length)
            case self.BarStyle.Donut | self.BarStyle.Pie:
                data_path = gui.PainterPath()
                data_path.set_fill_rule("winding")
                if value == self._max_value:
                    data_path.addEllipse(rect)
                else:
                    arc_length = 360 / delta
                    center_point = rect.center()
                    data_path.moveTo(center_point)
                    data_path.arcTo(rect, self.null_pos, -arc_length)
                    data_path.lineTo(center_point)
                painter.setBrush(self.palette().highlight())
                shadow_color = self.palette().shadow().color()
                painter.set_pen(color=shadow_color, width=self.data_pen_width)
                painter.drawPath(data_path)

    def _calculate_inner_rect(self, outer_radius: float) -> tuple[core.RectF, float]:
        if self.bar_style in (self.BarStyle.Line, self.BarStyle.Expand):
            inner_radius = outer_radius - self.outline_pen_width
        else:
            inner_radius = outer_radius * 0.75
        delta = (outer_radius - inner_radius) / 2
        inner_rect = core.RectF(delta, delta, inner_radius, inner_radius)
        return inner_rect, inner_radius

    def _draw_inner_background(self, painter: gui.Painter, inner_rect: core.RectF):
        if self.bar_style == self.BarStyle.Donut:
            painter.setBrush(self.palette().base())
            painter.drawEllipse(inner_rect)

    def _draw_text(
        self,
        painter: gui.Painter,
        inner_rect: core.RectF,
        inner_radius: float,
        value: float,
    ):
        if not self.number_format:
            return
        font = self.get_font()
        font.setPixelSize(10)
        metrics = gui.FontMetricsF(font)
        max_width = metrics.horizontalAdvance(self._value_to_text(self._max_value))
        delta = inner_radius / max_width
        font_size = int(font.pixelSize() * delta * 0.75)
        font.setPixelSize(max(font_size, 1))
        painter.setFont(font)
        painter.setPen(self.palette().text().color())
        text = self._value_to_text(value)
        painter.drawText(inner_rect, constants.ALIGN_CENTER, text)  # type: ignore

    def _value_to_text(self, value: float) -> str:
        text_to_draw = self.number_format
        match self._update_flags:
            case "value":
                val = round(value, self.decimals)
                return text_to_draw.replace(r"%v", str(val))
            case "percent":
                diff = self._max_value - self._min_value
                pct = (value - self._min_value) / diff * 100
                val = round(pct, self.decimals)
                return text_to_draw.replace(r"%p", str(val))
            case "max":
                val = round(self._max_value - self._min_value + 1, self.decimals)
                return text_to_draw.replace(r"%m", str(val))
            case _:
                raise ValueError(self._update_flags)

    def _value_format_changed(self):
        for k, v in VALUE_MAP.items():
            if k in self.number_format:
                self._update_flags = v
        self.update()

    def _rebuild_data_brush_if_needed(self):
        if not self._rebuild_brush or not self.gradient_data:
            return
        self._rebuild_brush = False
        if self.bar_style == "expand":
            data_brush = gui.RadialGradient(0.5, 0.5, 0.5, 0.5, 0.5)
            for i in self.gradient_data:
                data_brush[i[0]] = i[1]
        else:
            data_brush = gui.ConicalGradient(0.5, 0.5, self.null_pos)
            for i in self.gradient_data:
                data_brush[1 - i[0]] = i[1]
        data_brush.set_coordinate_mode("stretch_to_device")
        with self.edit_palette() as palette:
            palette.set_brush("highlight", data_brush)

    nullPosition = core.Property(
        enum.Enum,
        get_null_position,
        set_null_position,
        doc="Position for value = 0",
    )

BarStyle

Bases: IntEnum

Progress bar style.

Source code in prettyqt\custom_widgets\roundprogressbar.py
class BarStyle(enum.IntEnum):
    """Progress bar style."""

    Donut = 1
    Pie = 2
    Line = 3
    Expand = 4

Position

Bases: float, Enum

Start position of progress bar in degrees.

Source code in prettyqt\custom_widgets\roundprogressbar.py
class Position(float, enum.Enum):
    """Start position of progress bar in degrees."""

    Left = 180.0
    Top = 90.0
    Right = 0.0
    Bottom = -90.0

ValueType

Bases: IntEnum

Value type.

Source code in prettyqt\custom_widgets\roundprogressbar.py
class ValueType(enum.IntEnum):
    """Value type."""

    value = 1
    percent = 2
    maximum = 3

⌗ Property table

Qt Property Type Doc
objectName QString
modal bool
windowModality Qt::WindowModality
enabled bool
geometry QRect
frameGeometry QRect
normalGeometry QRect
x int
y int
pos QPoint
frameSize QSize
size QSize
width int
height int
rect QRect
childrenRect QRect
childrenRegion QRegion
sizePolicy QSizePolicy
minimumSize QSize
maximumSize QSize
minimumWidth int
minimumHeight int
maximumWidth int
maximumHeight int
sizeIncrement QSize
baseSize QSize
palette QPalette
font QFont
cursor QCursor
mouseTracking bool
tabletTracking bool
isActiveWindow bool
focusPolicy Qt::FocusPolicy
focus bool
contextMenuPolicy Qt::ContextMenuPolicy
updatesEnabled bool
visible bool
minimized bool
maximized bool
fullScreen bool
sizeHint QSize
minimumSizeHint QSize
acceptDrops bool
windowTitle QString
windowIcon QIcon
windowIconText QString
windowOpacity double
windowModified bool
toolTip QString
toolTipDuration int
statusTip QString
whatsThis QString
accessibleName QString
accessibleDescription QString
layoutDirection Qt::LayoutDirection
autoFillBackground bool
styleSheet QString
locale QLocale
windowFilePath QString
inputMethodHints QFlags
nullPosition PySide::PyObjectWrapper Position for value = 0