如何为 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 文档通常写得很好,所以这只是时间问题。
我正在寻找合并离散滑块和 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 文档通常写得很好,所以这只是时间问题。