如何为 QTableWidget 设置选择滑块?

How to set selecting slider for a QTableWidget?

我正在寻找合并离散滑块和 QTableWidget 的解决方案(参见所附屏幕截图)。滑块用作选择指针(而不是默认选择荧光笔)。如何使用Qt(PyQt)实现?

小前提。从技术上讲,根据 Whosebug 标准,您的问题不是很好。我会在这个答案的最后解释。

得到你想要的东西并不容易,最重要的是因为滑块不是为此目的而构建的(而且有很多 UX reasons for which you should not do that, go to User Experience 可以询问它们)。

诀窍是创建一个以 table 小部件作为父级的 QSlider。创建具有父级的小部件可确保子小部件将始终包含在父级边界内(这仅对 QMainWindow 和 QDialog 后代为 false),只要该小部件未添加到父级布局即可。这允许您自由设置其几何形状(位置和大小)。

在下面的示例中,我添加了一个内部 QSlider,但关于此小部件的主要问题是以其值位置与 table 内容对齐的方式对齐它。

class GhostHeader(QtWidgets.QHeaderView):
    '''
    A "fake" vertical header that does not paint its sections
    '''
    def __init__(self, parent):
        super().__init__(QtCore.Qt.Vertical, parent)
        self.setSectionResizeMode(self.Fixed)

    def paintEvent(self, event):
        pass


class SliderTable(QtWidgets.QTableWidget):
    def __init__(self, rows=0, columns=0, parent=None):
        super().__init__(rows, columns, parent)
        self.horizontalHeader().setStretchLastSection(True)
        self.setHorizontalHeaderLabels(['Item table'])
        self.setVerticalHeader(GhostHeader(self))

        # create a slider that is a child of the table; there is no layout, but
        # setting the table as its parent will cause it to be shown "within" it.
        self.slider = QtWidgets.QSlider(QtCore.Qt.Vertical, self)
        # by default, a slider has its maximum on the top, let's invert this
        self.slider.setInvertedAppearance(True)
        self.slider.setInvertedControls(True)
        # show tick marks at each slider value, on both sides
        self.slider.setTickInterval(1)
        self.slider.setTickPosition(self.slider.TicksBothSides)
        self.slider.setRange(0, max(0, self.rowCount() - 1))
        # not necessary, but useful for wheel and click interaction
        self.slider.setPageStep(1)
        # disable focus on the slider
        self.slider.setFocusPolicy(QtCore.Qt.NoFocus)

        self.slider.valueChanged.connect(self.selectRowFromSlider)
        self.slider.valueChanged.connect(self.updateSlider)
        self.verticalScrollBar().valueChanged.connect(self.updateSlider)

        self.model().rowsInserted.connect(self.modelChanged)
        self.model().rowsRemoved.connect(self.modelChanged)

    def selectRowFromSlider(self, row):
        if self.currentIndex().isValid():
            column = self.currentIndex().column()
        else:
            column = 0
        self.setCurrentIndex(self.model().index(row, column))

    def modelChanged(self):
        self.slider.setMaximum(max(0, self.rowCount() - 1))
        self.updateSlider()

    def updateSlider(self):
        slider = self.slider
        option = QtWidgets.QStyleOptionSlider()
        slider.initStyleOption(option)
        style = slider.style()

        # get the available extent of the slider
        available = style.pixelMetric(style.PM_SliderSpaceAvailable, option, slider)
        # compute the space between the top of the slider and the position of
        # the minimum value (0)
        deltaTop = (slider.height() - available) // 2
        # do the same for the maximum
        deltaBottom = slider.height() - available - deltaTop

        # the vertical center of the first item
        top = self.visualRect(self.model().index(0, 0)).center().y()
        # the vertical center of the last
        bottom = self.visualRect(self.model().index(self.model().rowCount() - 1, 0)).y()

        # get the slider width and adjust the size of the "ghost" vertical header
        width = self.slider.sizeHint().width()
        left = self.frameWidth() + 1
        self.verticalHeader().setFixedWidth(width // 2 + left)

        viewGeo = self.viewport().geometry()
        headerHeight = viewGeo.top()
        # create the rectangle for the slider geometry
        rect = QtCore.QRect(0, headerHeight + top, width, headerHeight + bottom - top // 2)
        # adjust to the values computed above
        rect.adjust(0, -deltaTop + 1, 0, -deltaBottom)
        # translate it so that its center will be between the vertical header and
        # the table contents
        rect.translate(left, 0)
        self.slider.setGeometry(rect)

        # set the mask, in case the item view is scrolled, so that the top of the
        # slider won't be shown in the horizontal header
        visible = self.rect().adjusted(0, viewGeo.top(), 0, 0)
        mask = QtGui.QPainterPath()
        topLeft = slider.mapFromParent(visible.topLeft())
        bottomRight = slider.mapFromParent(visible.bottomRight() + QtCore.QPoint(1, 1))
        mask.addRect(QtCore.QRectF(topLeft, bottomRight))
        self.slider.setMask(QtGui.QRegion(mask.toFillPolygon(QtGui.QTransform()).toPolygon()))

    def currentChanged(self, current, previous):
        super().currentChanged(current, previous)
        if current.isValid():
            self.slider.setValue(current.row())

    def resizeEvent(self, event):
        # whenever the table is resized (even when first shown) call the base
        # implementation (which is required for correct drawing of items and
        # selections), then update the slider
        super().resizeEvent(event)
        self.updateSlider()


class Test(QtWidgets.QWidget):
    def __init__(self):
        super().__init__()
        layout = QtWidgets.QVBoxLayout(self)
        self.table = SliderTable()
        self.table.setRowCount(4)
        self.table.setColumnCount(1)
        self.table.setHorizontalHeaderLabels(['Item table'])
        layout.addWidget(self.table)
        for row in range(self.table.rowCount()):
            item = QtWidgets.QTableWidgetItem('item {}'.format(row + 1))
            item.setTextAlignment(QtCore.Qt.AlignCenter)
            self.table.setItem(row, 0, item)

为什么这个问题不是好?

好吧,它非常接近 "I don't know how to do this, can you do it for me?" 限制。你应该提供任何minimal, reproducible example(如果它不起作用没关系,你应该做一些研究并展示你的努力),问题有点模糊,即使在评论部分进行了一些澄清之后。
长话短说:如果它太难而你不能让它工作,你可能仍然需要一些学习和锻炼才能实现它。耐心点,研究文档:幸运的是,Qt 文档通常写得很好,所以这只是时间问题。