如何在pyqt中使用 QStyle 重繪 QSlider


前言

使用 qss 可以很方便地改變 QSlider 的樣式,但是有些情況下 qss 無法滿足我們的需求。比如下圖所示樣式:

slider

如果直接使用 qss 將 handle 的內圓設置為透明背景,會看到 handle 下面的 groove ,而且畫出來的圓環還不圓,如下圖所示:

slider with groove

這時候就需要使用 QStyle 來重繪 QSlider,關於 QStyle 的介紹可以參見 《QStyle設置界面的外觀和QCommonStyle繼承關系圖講解和使用》,這里不過多贅述(才不是因為我自己也說不清楚)。

實現過程

對於 QSlider 這種比較復雜小部件,需要重寫 QProxyStyledrawComplexControl()subControlRect(),前者決定了 QSlider 的樣式,后者用來獲取各個子控件所在的矩形區域。為了演示的方便,代碼中只重繪了水平滑動條的樣式。

  1. 首先繪制 groove 子控件,從動圖中可以看到,要想防止看到 handle 下面的 groove,需將 groove 拆分成兩段來繪制:一段是 sub-page 部分,另一段是 add-page 部分;
  2. 接着繪制 handle 子控件,handle 子控件有三個部分:透明內圓、不透明圓環以及透明外邊距。繪制圓環可以實例化 QPainterPathaddEllipse() 添加兩個不同半徑的同心圓之后再 painter.drawPath(path)。為了響應 hoverpressed,需要分別在 opt.activeSubControls == QProxyStyle.SC_SliderHandle 以及 widget.isSliderDown() 時更新滑塊樣式。

下面是具體代碼:

# coding:utf-8
from PyQt5.QtCore import QSize, Qt, pyqtSignal, QPoint, QRectF
from PyQt5.QtGui import QColor, QMouseEvent, QPainter, QPainterPath
from PyQt5.QtWidgets import (QProxyStyle, QSlider, QStyle, QStyleOptionSlider,
                             QWidget)

class HollowHandleStyle(QProxyStyle):
    """ 滑塊中空樣式 """

    def __init__(self, config: dict = None):
        """
        Parameters
        ----------
        config: dict
            樣式配置
        """
        super().__init__()
        self.config = {
            "groove.height": 3,
            "sub-page.color": QColor(255, 255, 255),
            "add-page.color": QColor(255, 255, 255, 64),
            "handle.color": QColor(255, 255, 255),
            "handle.ring-width": 4,
            "handle.hollow-radius": 6,
            "handle.margin": 4
        }
        config = config if config else {}
        self.config.update(config)

        # 計算 handle 的大小
        w = self.config["handle.margin"]+self.config["handle.ring-width"] + \
            self.config["handle.hollow-radius"]
        self.config["handle.size"] = QSize(2*w, 2*w)

    def subControlRect(self, cc: QStyle.ComplexControl, opt: QStyleOptionSlider, sc: QStyle.SubControl, widget: QWidget):
        """ 返回子控件所占的矩形區域 """
        if cc != self.CC_Slider or opt.orientation != Qt.Horizontal or sc == self.SC_SliderTickmarks:
            return super().subControlRect(cc, opt, sc, widget)

        rect = opt.rect

        if sc == self.SC_SliderGroove:
            h = self.config["groove.height"]
            grooveRect = QRectF(0, (rect.height()-h)//2, rect.width(), h)
            return grooveRect.toRect()

        elif sc == self.SC_SliderHandle:
            size = self.config["handle.size"]
            x = self.sliderPositionFromValue(
                opt.minimum, opt.maximum, opt.sliderPosition, rect.width())
            # 解決滑塊跑出滑動條的情況
            x *= (rect.width()-size.width())/rect.width()
            sliderRect = QRectF(x, 0, size.width(), size.height())
            return sliderRect.toRect()

    def drawComplexControl(self, cc: QStyle.ComplexControl, opt: QStyleOptionSlider, painter: QPainter, widget: QWidget):
        """ 繪制子控件 """
        if cc != self.CC_Slider or opt.orientation != Qt.Horizontal:
            return super().drawComplexControl(cc, opt, painter, widget)

        grooveRect = self.subControlRect(cc, opt, self.SC_SliderGroove, widget)
        handleRect = self.subControlRect(cc, opt, self.SC_SliderHandle, widget)
        painter.setRenderHints(QPainter.Antialiasing)
        painter.setPen(Qt.NoPen)

        # 繪制滑槽
        painter.save()
        painter.translate(grooveRect.topLeft())

        # 繪制划過的部分
        w = handleRect.x()-grooveRect.x()
        h = self.config['groove.height']
        painter.setBrush(self.config["sub-page.color"])
        painter.drawRect(0, 0, w, h)

        # 繪制未划過的部分
        x = w+self.config['handle.size'].width()
        painter.setBrush(self.config["add-page.color"])
        painter.drawRect(x, 0, grooveRect.width()-w, h)
        painter.restore()

        # 繪制滑塊
        ringWidth = self.config["handle.ring-width"]
        hollowRadius = self.config["handle.hollow-radius"]
        radius = ringWidth + hollowRadius

        path = QPainterPath()
        path.moveTo(0, 0)
        center = handleRect.center() + QPoint(1, 1)
        path.addEllipse(center, radius, radius)
        path.addEllipse(center, hollowRadius, hollowRadius)

        handleColor = self.config["handle.color"]  # type:QColor
        handleColor.setAlpha(255 if opt.activeSubControls !=
                             self.SC_SliderHandle else 153)
        painter.setBrush(handleColor)
        painter.drawPath(path)

        # 滑塊按下
        if widget.isSliderDown():
            handleColor.setAlpha(255)
            painter.setBrush(handleColor)
            painter.drawEllipse(handleRect)

測試

# coding:utf-8
import sys
from PyQt5.QtCore import Qt
from PyQt5.QtGui import QColor
from PyQt5.QtWidgets import QApplication, QWidget, QSlider


class Demo(QWidget):

    def __init__(self):
        super().__init__()
        self.resize(300, 150)
        self.setStyleSheet("Demo{background: rgb(184, 106, 106)}")
        
        # 改變默認樣式
        style = {
            "sub-page.color": QColor(70, 23, 180)
        }
        self.slider = QSlider(Qt.Horizontal, self)
        self.slider.setStyle(HollowHandleStyle(style))

        # 需要調整高度
        self.slider.resize(200, 28)
        self.slider.move(50, 61)


if __name__ == '__main__':
    app = QApplication(sys.argv)
    w = Demo()
    w.show()
    sys.exit(app.exec_())


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM