【问题标题】:Porting range-slider widget to PyQt5将范围滑块小部件移植到 PyQt5
【发布时间】:2017-11-17 01:59:18
【问题描述】:

我目前需要一个范围滑块(一个可以设置最小值和最大值的滑块)。我发现了两个相关的问题Range slider in Qt (two handles in a QSlider)Why RangeSlider is available in QtQuick and not as standard Widget,但它们都不是用 python3 编写的,而且我对 C++ 不是很熟悉。

我找到了这个完美的 github 工具 https://github.com/rsgalloway/qrangeslider,但不幸的是它是为 PyQt4 编写的,我正在使用 PyQt5。

我打算用 PyQt5 绑定重新格式化这个 github 源,但在这样做之前我想知道是否有人以前做过这样我可以节省时间? 或者,如果有人有不同的解决方案,我愿意接受建议。

【问题讨论】:

    标签: python qt pyqt slider pyqt5


    【解决方案1】:

    下面是QRangeSlider widget 的 PyQt5 端口。为了简洁起见,我删除了所有 cmets、doc-strings、assert 语句等。它似乎在 Python 2 和 Python 3 上都可以正常工作,但我并没有真正对其进行过多测试。

    qrangeslider.py

    import sys, os
    from PyQt5 import QtCore, QtGui, QtWidgets
    
    __all__ = ['QRangeSlider']
    
    DEFAULT_CSS = """
    QRangeSlider * {
        border: 0px;
        padding: 0px;
    }
    QRangeSlider #Head {
        background: #222;
    }
    QRangeSlider #Span {
        background: #393;
    }
    QRangeSlider #Span:active {
        background: #282;
    }
    QRangeSlider #Tail {
        background: #222;
    }
    QRangeSlider > QSplitter::handle {
        background: #393;
    }
    QRangeSlider > QSplitter::handle:vertical {
        height: 4px;
    }
    QRangeSlider > QSplitter::handle:pressed {
        background: #ca5;
    }
    """
    
    def scale(val, src, dst):
        return int(((val - src[0]) / float(src[1]-src[0])) * (dst[1]-dst[0]) + dst[0])
    
    
    class Ui_Form(object):
        def setupUi(self, Form):
            Form.setObjectName("QRangeSlider")
            Form.resize(300, 30)
            Form.setStyleSheet(DEFAULT_CSS)
            self.gridLayout = QtWidgets.QGridLayout(Form)
            self.gridLayout.setContentsMargins(0, 0, 0, 0)
            self.gridLayout.setSpacing(0)
            self.gridLayout.setObjectName("gridLayout")
            self._splitter = QtWidgets.QSplitter(Form)
            self._splitter.setMinimumSize(QtCore.QSize(0, 0))
            self._splitter.setMaximumSize(QtCore.QSize(16777215, 16777215))
            self._splitter.setOrientation(QtCore.Qt.Horizontal)
            self._splitter.setObjectName("splitter")
            self._head = QtWidgets.QGroupBox(self._splitter)
            self._head.setTitle("")
            self._head.setObjectName("Head")
            self._handle = QtWidgets.QGroupBox(self._splitter)
            self._handle.setTitle("")
            self._handle.setObjectName("Span")
            self._tail = QtWidgets.QGroupBox(self._splitter)
            self._tail.setTitle("")
            self._tail.setObjectName("Tail")
            self.gridLayout.addWidget(self._splitter, 0, 0, 1, 1)
            self.retranslateUi(Form)
            QtCore.QMetaObject.connectSlotsByName(Form)
    
        def retranslateUi(self, Form):
            _translate = QtCore.QCoreApplication.translate
            Form.setWindowTitle(_translate("QRangeSlider", "QRangeSlider"))
    
    
    class Element(QtWidgets.QGroupBox):
        def __init__(self, parent, main):
            super(Element, self).__init__(parent)
            self.main = main
    
        def setStyleSheet(self, style):
            self.parent().setStyleSheet(style)
    
        def textColor(self):
            return getattr(self, '__textColor', QtGui.QColor(125, 125, 125))
    
        def setTextColor(self, color):
            if type(color) == tuple and len(color) == 3:
                color = QtGui.QColor(color[0], color[1], color[2])
            elif type(color) == int:
                color = QtGui.QColor(color, color, color)
            setattr(self, '__textColor', color)
    
        def paintEvent(self, event):
            qp = QtGui.QPainter()
            qp.begin(self)
            if self.main.drawValues():
                self.drawText(event, qp)
            qp.end()
    
    
    class Head(Element):
        def __init__(self, parent, main):
            super(Head, self).__init__(parent, main)
    
        def drawText(self, event, qp):
            qp.setPen(self.textColor())
            qp.setFont(QtGui.QFont('Arial', 10))
            qp.drawText(event.rect(), QtCore.Qt.AlignLeft, str(self.main.min()))
    
    
    class Tail(Element):
        def __init__(self, parent, main):
            super(Tail, self).__init__(parent, main)
    
        def drawText(self, event, qp):
            qp.setPen(self.textColor())
            qp.setFont(QtGui.QFont('Arial', 10))
            qp.drawText(event.rect(), QtCore.Qt.AlignRight, str(self.main.max()))
    
    
    class Handle(Element):
        def __init__(self, parent, main):
            super(Handle, self).__init__(parent, main)
    
        def drawText(self, event, qp):
            qp.setPen(self.textColor())
            qp.setFont(QtGui.QFont('Arial', 10))
            qp.drawText(event.rect(), QtCore.Qt.AlignLeft, str(self.main.start()))
            qp.drawText(event.rect(), QtCore.Qt.AlignRight, str(self.main.end()))
    
        def mouseMoveEvent(self, event):
            event.accept()
            mx = event.globalX()
            _mx = getattr(self, '__mx', None)
            if not _mx:
                setattr(self, '__mx', mx)
                dx = 0
            else:
                dx = mx - _mx
            setattr(self, '__mx', mx)
            if dx == 0:
                event.ignore()
                return
            elif dx > 0:
                dx = 1
            elif dx < 0:
                dx = -1
            s = self.main.start() + dx
            e = self.main.end() + dx
            if s >= self.main.min() and e <= self.main.max():
                self.main.setRange(s, e)
    
    
    class QRangeSlider(QtWidgets.QWidget, Ui_Form):
        endValueChanged = QtCore.pyqtSignal(int)
        maxValueChanged = QtCore.pyqtSignal(int)
        minValueChanged = QtCore.pyqtSignal(int)
        startValueChanged = QtCore.pyqtSignal(int)
        minValueChanged = QtCore.pyqtSignal(int)
        maxValueChanged = QtCore.pyqtSignal(int)
        startValueChanged = QtCore.pyqtSignal(int)
        endValueChanged = QtCore.pyqtSignal(int)
    
        _SPLIT_START = 1
        _SPLIT_END = 2
    
        def __init__(self, parent=None):
            super(QRangeSlider, self).__init__(parent)
            self.setupUi(self)
            self.setMouseTracking(False)
            self._splitter.splitterMoved.connect(self._handleMoveSplitter)
            self._head_layout = QtWidgets.QHBoxLayout()
            self._head_layout.setSpacing(0)
            self._head_layout.setContentsMargins(0, 0, 0, 0)
            self._head.setLayout(self._head_layout)
            self.head = Head(self._head, main=self)
            self._head_layout.addWidget(self.head)
            self._handle_layout = QtWidgets.QHBoxLayout()
            self._handle_layout.setSpacing(0)
            self._handle_layout.setContentsMargins(0, 0, 0, 0)
            self._handle.setLayout(self._handle_layout)
            self.handle = Handle(self._handle, main=self)
            self.handle.setTextColor((150, 255, 150))
            self._handle_layout.addWidget(self.handle)
            self._tail_layout = QtWidgets.QHBoxLayout()
            self._tail_layout.setSpacing(0)
            self._tail_layout.setContentsMargins(0, 0, 0, 0)
            self._tail.setLayout(self._tail_layout)
            self.tail = Tail(self._tail, main=self)
            self._tail_layout.addWidget(self.tail)
            self.setMin(0)
            self.setMax(99)
            self.setStart(0)
            self.setEnd(99)
            self.setDrawValues(True)
    
        def min(self):
            return getattr(self, '__min', None)
    
        def max(self):
            return getattr(self, '__max', None)
    
        def setMin(self, value):
            setattr(self, '__min', value)
            self.minValueChanged.emit(value)
    
        def setMax(self, value):
            setattr(self, '__max', value)
            self.maxValueChanged.emit(value)
    
        def start(self):
            return getattr(self, '__start', None)
    
        def end(self):
            return getattr(self, '__end', None)
    
        def _setStart(self, value):
            setattr(self, '__start', value)
            self.startValueChanged.emit(value)
    
        def setStart(self, value):
            v = self._valueToPos(value)
            self._splitter.splitterMoved.disconnect()
            self._splitter.moveSplitter(v, self._SPLIT_START)
            self._splitter.splitterMoved.connect(self._handleMoveSplitter)
            self._setStart(value)
    
        def _setEnd(self, value):
            setattr(self, '__end', value)
            self.endValueChanged.emit(value)
    
        def setEnd(self, value):
            v = self._valueToPos(value)
            self._splitter.splitterMoved.disconnect()
            self._splitter.moveSplitter(v, self._SPLIT_END)
            self._splitter.splitterMoved.connect(self._handleMoveSplitter)
            self._setEnd(value)
    
        def drawValues(self):
            return getattr(self, '__drawValues', None)
    
        def setDrawValues(self, draw):
            setattr(self, '__drawValues', draw)
    
        def getRange(self):
            return (self.start(), self.end())
    
        def setRange(self, start, end):
            self.setStart(start)
            self.setEnd(end)
    
        def keyPressEvent(self, event):
            key = event.key()
            if key == QtCore.Qt.Key_Left:
                s = self.start()-1
                e = self.end()-1
            elif key == QtCore.Qt.Key_Right:
                s = self.start()+1
                e = self.end()+1
            else:
                event.ignore()
                return
            event.accept()
            if s >= self.min() and e <= self.max():
                self.setRange(s, e)
    
        def setBackgroundStyle(self, style):
            self._tail.setStyleSheet(style)
            self._head.setStyleSheet(style)
    
        def setSpanStyle(self, style):
            self._handle.setStyleSheet(style)
    
        def _valueToPos(self, value):
            return scale(value, (self.min(), self.max()), (0, self.width()))
    
        def _posToValue(self, xpos):
            return scale(xpos, (0, self.width()), (self.min(), self.max()))
    
        def _handleMoveSplitter(self, xpos, index):
            hw = self._splitter.handleWidth()
            def _lockWidth(widget):
                width = widget.size().width()
                widget.setMinimumWidth(width)
                widget.setMaximumWidth(width)
            def _unlockWidth(widget):
                widget.setMinimumWidth(0)
                widget.setMaximumWidth(16777215)
            v = self._posToValue(xpos)
            if index == self._SPLIT_START:
                _lockWidth(self._tail)
                if v >= self.end():
                    return
                offset = -20
                w = xpos + offset
                self._setStart(v)
            elif index == self._SPLIT_END:
                _lockWidth(self._head)
                if v <= self.start():
                    return
                offset = -40
                w = self.width() - xpos + offset
                self._setEnd(v)
            _unlockWidth(self._tail)
            _unlockWidth(self._head)
            _unlockWidth(self._handle)
    
    if __name__ == '__main__':
    
        app = QtWidgets.QApplication(sys.argv)
        rs = QRangeSlider()
        rs.show()
        rs.setRange(15, 35)
        rs.setBackgroundStyle('background: qlineargradient(x1:0, y1:0, x2:0, y2:1, stop:0 #222, stop:1 #333);')
        rs.handle.setStyleSheet('background: qlineargradient(x1:0, y1:0, x2:0, y2:1, stop:0 #282, stop:1 #393);')
        app.exec_()
    

    如果要运行examples,只需更改以下代码块(在文件顶部):

    examples.py

    import sys, os
    from PyQt5 import QtCore
    from PyQt5 import QtGui
    from PyQt5 import QtWidgets
    
    from qrangeslider import QRangeSlider
    
    app = QtWidgets.QApplication(sys.argv)
    
    ...
    

    【讨论】:

    • @mangovn 请参阅 PyQt 文档中的示例:Using Qt Designer。一旦你像这样设置了主窗口,你就可以添加你喜欢的任何小部件。或者,您可以使用小部件提升通过 Qt 设计器添加它(参见 herehere)。
    【解决方案2】:

    这是一个具有原生外观的 PySide2 示例:

    from PySide2.QtWidgets import *
    from PySide2.QtCore import *
    from PySide2.QtGui import *
    
    import sys
    
    
    class RangeSlider(QWidget):
        def __init__(self, parent=None):
            super().__init__(parent)
    
            self.first_position = 1
            self.second_position = 8
    
            self.opt = QStyleOptionSlider()
            self.opt.minimum = 0
            self.opt.maximum = 10
    
            self.setTickPosition(QSlider.TicksAbove)
            self.setTickInterval(1)
    
            self.setSizePolicy(
                QSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed, QSizePolicy.Slider)
            )
    
        def setRangeLimit(self, minimum: int, maximum: int):
            self.opt.minimum = minimum
            self.opt.maximum = maximum
    
        def setRange(self, start: int, end: int):
            self.first_position = start
            self.second_position = end
    
        def getRange(self):
            return (self.first_position, self.second_position)
    
        def setTickPosition(self, position: QSlider.TickPosition):
            self.opt.tickPosition = position
    
        def setTickInterval(self, ti: int):
            self.opt.tickInterval = ti
    
        def paintEvent(self, event: QPaintEvent):
    
            painter = QPainter(self)
    
            # Draw rule
            self.opt.initFrom(self)
            self.opt.rect = self.rect()
            self.opt.sliderPosition = 0
            self.opt.subControls = QStyle.SC_SliderGroove | QStyle.SC_SliderTickmarks
    
            #   Draw GROOVE
            self.style().drawComplexControl(QStyle.CC_Slider, self.opt, painter)
    
            #  Draw INTERVAL
    
            color = self.palette().color(QPalette.Highlight)
            color.setAlpha(160)
            painter.setBrush(QBrush(color))
            painter.setPen(Qt.NoPen)
    
            self.opt.sliderPosition = self.first_position
            x_left_handle = (
                self.style()
                .subControlRect(QStyle.CC_Slider, self.opt, QStyle.SC_SliderHandle)
                .right()
            )
    
            self.opt.sliderPosition = self.second_position
            x_right_handle = (
                self.style()
                .subControlRect(QStyle.CC_Slider, self.opt, QStyle.SC_SliderHandle)
                .left()
            )
    
            groove_rect = self.style().subControlRect(
                QStyle.CC_Slider, self.opt, QStyle.SC_SliderGroove
            )
    
            selection = QRect(
                x_left_handle,
                groove_rect.y(),
                x_right_handle - x_left_handle,
                groove_rect.height(),
            ).adjusted(-1, 1, 1, -1)
    
            painter.drawRect(selection)
    
            # Draw first handle
    
            self.opt.subControls = QStyle.SC_SliderHandle
            self.opt.sliderPosition = self.first_position
            self.style().drawComplexControl(QStyle.CC_Slider, self.opt, painter)
    
            # Draw second handle
            self.opt.sliderPosition = self.second_position
            self.style().drawComplexControl(QStyle.CC_Slider, self.opt, painter)
    
        def mousePressEvent(self, event: QMouseEvent):
    
            self.opt.sliderPosition = self.first_position
            self._first_sc = self.style().hitTestComplexControl(
                QStyle.CC_Slider, self.opt, event.pos(), self
            )
    
            self.opt.sliderPosition = self.second_position
            self._second_sc = self.style().hitTestComplexControl(
                QStyle.CC_Slider, self.opt, event.pos(), self
            )
    
        def mouseMoveEvent(self, event: QMouseEvent):
    
            distance = self.opt.maximum - self.opt.minimum
    
            pos = self.style().sliderValueFromPosition(
                0, distance, event.pos().x(), self.rect().width()
            )
    
            if self._first_sc == QStyle.SC_SliderHandle:
                if pos <= self.second_position:
                    self.first_position = pos
                    self.update()
                    return
    
            if self._second_sc == QStyle.SC_SliderHandle:
                if pos >= self.first_position:
                    self.second_position = pos
                    self.update()
    
        def sizeHint(self):
            """ override """
            SliderLength = 84
            TickSpace = 5
    
            w = SliderLength
            h = self.style().pixelMetric(QStyle.PM_SliderThickness, self.opt, self)
    
            if (
                self.opt.tickPosition & QSlider.TicksAbove
                or self.opt.tickPosition & QSlider.TicksBelow
            ):
                h += TickSpace
    
            return (
                self.style()
                .sizeFromContents(QStyle.CT_Slider, self.opt, QSize(w, h), self)
                .expandedTo(QApplication.globalStrut())
            )
    
    
    if __name__ == "__main__":
    
        app = QApplication(sys.argv)
    
        w = RangeSlider()
        w.show()
    
        # q = QSlider()
        # q.show()
    
        app.exec_()
    

    【讨论】:

    • 这个类可以从 QSlider 继承来提供像 valuechanged 这样的信号
    • 我怎样才能让它垂直?
    【解决方案3】:

    据我所知,@ekhumoro 的实现存在一个错误,即当您尝试将滑块拖动到其上限时,由于滑块宽度,比例限制设置错误。

    我通过更改以下内容来修复它;

    v = self._posToValue(xpos)
    

    到;

    if index == 1:
        v = self._posToValue(xpos)
    elif index == 2:
        v = self._posToValue(xpos+hw)
    

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2010-12-24
      • 1970-01-01
      相关资源
      最近更新 更多