如何为每个 QTableView 单元格支持两个单独的可双击值?

Posted

技术标签:

【中文标题】如何为每个 QTableView 单元格支持两个单独的可双击值?【英文标题】:How can I support two separate double-clickable values per QTableView cell? 【发布时间】:2014-06-02 13:20:36 【问题描述】:

使用 PyQt5,我需要在 QTableView 中的每个单元格中显示两个值;基本上,每一列都必须分成两个逻辑子列。将鼠标指针悬停在某个值上方时,应突出显示其文本,而不是同一单元格中的其他值。类似地,应该可以对单元格内的单个值的双击做出反应。我该如何实现?

【问题讨论】:

【参考方案1】:

我通过在QTableView 上实现一个细微的变化解决了这个问题,它利用QStyledItemDelegate 子类来绘制两个不同的值(突出显示或不突出显示)并检测它们中的每一个何时被双击。请注意,每个单元格的两个值在模型中表示为以分号分隔的字符串。

截图

从这个屏幕截图可以看出,左上角的 left 值被突出显示(由于鼠标悬停在其上方)。

代码

代码分为三个主要部分,表视图(QTableView 的子类)、委托(QStyledItemDelegate 的子类)和使用表视图的应用程序代码。

表格视图

import sys
from PyQt5 import QtWidgets, QtGui, QtCore


class TableView(QtWidgets.QTableView):
    def __init__(self, parent):
        super(TableView, self).__init__(parent)
        self.__pressed_index = None
        self.__entered_index = None
        self.setItemDelegate(SplitCellDelegate(self))
        self.setEditTriggers(QtWidgets.QAbstractItemView.NoEditTriggers)
        for header in (self.horizontalHeader(), self.verticalHeader()):
            header.installEventFilter(self)

    def mouseDoubleClickEvent(self, event):
        super(TableView, self).mouseDoubleClickEvent(event)

        index = self.indexAt(event.pos())
        if not index.isValid() or not self.__is_index_enabled(index) or self.__pressed_index != index:
            me = QtGui.QMouseEvent(QtCore.QEvent.MouseButtonPress, event.localPos(), event.windowPos(), event.screenPos(),
                event.button(), event.buttons(), event.modifiers())
            return

        index_rel_pos = self.__get_index_rel_pos(event, index)
        delegate = self.itemDelegate(index)
        delegate.double_clicked(index, index_rel_pos)

    def mousePressEvent(self, event):
        super(TableView, self).mousePressEvent(event)
        self.__pressed_index = self.indexAt(event.pos())

    def mouseMoveEvent(self, event):
        super(TableView, self).mouseMoveEvent(event)
        if self.state() == self.ExpandingState or self.state() == self.CollapsingState or self.state() == self.DraggingState:
            return

        index = self.indexAt(event.pos())

        if self.__entered_index is not None and index != self.__entered_index:
            # We've left the currently entered index
            self.itemDelegate(self.__entered_index).left(self.__entered_index) 
            self.__entered_index = None

        if not index.isValid() or not self.__is_index_enabled(index):
            # No index is currently hovered above
            return

        self.__entered_index = index
        index_rel_pos = self.__get_index_rel_pos(event, index)
        self.itemDelegate(index).mouse_move(index, index_rel_pos)

    def leaveEvent(self, event):
        super(TableView, self).leaveEvent(event)

        self.__handle_mouse_exit()

    def __handle_mouse_exit(self):
        if self.__entered_index is None:
            return

        self.itemDelegate(self.__entered_index).left(self.__entered_index)
        self.__entered_index = None

    def eventFilter(self, obj, event):
        if (obj is not self.horizontalHeader() and obj is not self.verticalHeader()) or \
            event.type() not in (QtCore.QEvent.Enter,):
            return super(TableView, self).eventFilter(obj, event)

        self.__handle_mouse_exit()
        return False

    def __get_index_rel_pos(self, event, index):
        """Get position relative to index."""
        # Get index' y offset
        pos = event.pos()
        x = pos.x()
        y = pos.y()
        while self.indexAt(QtCore.QPoint(x, y-1)) == index:
            y -= 1
        while self.indexAt(QtCore.QPoint(x-1, y)) == index:
            x -= 1

        return QtCore.QPoint(pos.x()-x, pos.y()-y)

    def __is_index_enabled(self, index):
        return index.row() >= 0 and index.column() >= 0 and index.model()

项目委托

class SplitCellDelegate(QtWidgets.QStyledItemDelegate):
    def __init__(self, parent):
        super(SplitCellDelegate, self).__init__(parent)

        self.__view = parent
        parent.setMouseTracking(True)

        self.__hor_padding = 10
        self.__above_value1  = self.__above_value2 = None
        self.__rect = None

    def paint(self, painter, option, index):
        #print('Painting; width: '.format(option.rect.width()))
        painter.setRenderHint(QtGui.QPainter.Antialiasing)
        #print('Painting ,'.format(index.row(), index.column()))

        rect = option.rect
        # Copy the rect in case it changes
        self.__rect = QtCore.QRect(option.rect)

        if option.state & QtWidgets.QStyle.State_Selected:
            painter.fillRect(rect, option.palette.highlight())

        value1, value2 = self.__split_text(index)
        value1_start, separator_start, value2_start = [x + rect.x() for x in self.__compute_offsets(index)]

        if self.__above_value1 == index:
            self.__set_bold_font(painter)
            #print('Drawing value1 highlighted')
        #print('Drawing \'\' from  to '.format(self.__value1, value1_start, separator_start))
        text_rect = QtCore.QRectF(0, rect.y(), rect.width(), rect.height())
        painter.drawText(text_rect.translated(value1_start, 0), value1, QtGui.QTextOption(QtCore.Qt.AlignVCenter))
        if self.__above_value1 == index:
            painter.restore()
        painter.drawText(text_rect.translated(separator_start, 0), '|', QtGui.QTextOption(QtCore.Qt.AlignVCenter))
        if self.__above_value2 == index:
            self.__set_bold_font(painter)
            #print('Drawing value2 highlighted')
        #else:
            #print('Not drawing highlighted')
        painter.drawText(text_rect.translated(value2_start, 0), value2, QtGui.QTextOption(QtCore.Qt.AlignVCenter))
        if self.__above_value2 == index:
            painter.restore()

    def sizeHint(self, option, index):
        value1, value2 = self.__split_text(index)
        font = QtGui.QFont(self.__view.font())
        font.setBold(True)
        fm = QtGui.QFontMetrics(font)
        return QtCore.QSize(self.__hor_padding*2 + fm.width('|'.format(value1, value2)),
            15*2 + fm.height())

    @staticmethod
    def __set_bold_font(painter):
        painter.save()
        font = QtGui.QFont(painter.font())
        font.setBold(True)
        painter.setFont(font)

    @staticmethod
    def __split_text(index):
        text = index.data(QtCore.Qt.DisplayRole).split(';')
        value1 = text[0] + ' '
        value2 = ' ' + text[1]
        return value1, value2

    def mouse_move(self, index, pos):
        if self.__rect is None:
            return

        value1_start, separator_start, value2_start = self.__compute_offsets(index)
        x = pos.x()
        #print('Mouse move in cell:  ( | )'.format(x, separator_start, value2_start))
        if x < separator_start:
            if self.__above_value1 == index:
                return
            self.__above_value1 = index
            self.__above_value2 = None
            #print('Above value1')
            self.__repaint()
        elif x >= value2_start:
            if self.__above_value2 == index:
                return
            self.__above_value2 = index
            self.__above_value1 = None
            #print('Above value2')
            self.__repaint()
        elif self.__above_value1 is not None or self.__above_value2 is not None:
            self.__above_value1 = self.__above_value2 = None
            #print('Above separator')
            self.__repaint()

    def left(self, index):
        #print('Index , left'.format(index.row(), index.column()))
        self.__above_value1 = self.__above_value2 = None
        self.__repaint()

    def double_clicked(self, index, pos):
        x = pos.x()
        value1_start, separator_start, value2_start = self.__compute_offsets(index)
        if x < separator_start:
            print('Index , double-clicked at value 1'.format(index.row(), index.column()))
        elif x >= value2_start:
            print('Index , double-clicked at value 2'.format(index.row(), index.column()))

    def __compute_offsets(self, index):
        rect = self.__rect
        value1, value2 = self.__split_text(index)
        #print('Computing offsets; width: '.format(rect.width()))
        font = QtGui.QFont(self.__view.font())
        font.setBold(True)
        fm = QtGui.QFontMetrics(font)
        value2_start = rect.width() - fm.width(value2) - self.__hor_padding
        separator_start = value2_start - fm.width('|')
        value1_start = separator_start - fm.width(value1)
        #print('Offsets for , are , , '.format(index.row(), index.column(), value1_start, separator_start, value2_start))
        return value1_start, separator_start, value2_start

    def __repaint(self):
        # TODO: Repaint only cell in question
        self.__view.viewport().repaint() 

应用代码

class Window(QtWidgets.QMainWindow):
    def __init__(self):
        super(Window, self).__init__()

        table_view = self.__set_up_table()

        w = QtWidgets.QWidget()
        vbox = QtWidgets.QVBoxLayout(w)
        vbox.addWidget(table_view)
        self.setCentralWidget(w)

    def __set_up_table(self):
        rows = 4
        cols = 4
        table = QtGui.QStandardItemModel()
        for row in range(rows):
            l = [QtGui.QStandardItem('Row ;Column '.format(row, col)) for col in range(cols)]
            table.appendRow(l)
            table.setVerticalHeaderItem(row, QtGui.QStandardItem('Row '.format(row)))
        for col in range(cols):
            table.setHorizontalHeaderItem(col, QtGui.QStandardItem('Column '.format(col)))

        table_view = TableView(self)
        table_view.setModel(table)
        table_view.setSortingEnabled(True)
        table_view.resizeColumnsToContents()
        return table_view


app = QtWidgets.QApplication(sys.argv)
w = Window()
w.show()
app.exec_()

【讨论】:

以上是关于如何为每个 QTableView 单元格支持两个单独的可双击值?的主要内容,如果未能解决你的问题,请参考以下文章

如何为每次用户滚动时更新的每个 tableview 单元格添加一个计时器?

在 FirstVC 中选择单元格后,如何为 SecondVC 中的每个单元格调用按钮操作?

jface tableviewer,如何为每个单元格使用不同的单元格编辑器?

如何为 Objective-C 中的每个单元格设置不同的图像?

如何为 UITableView 中的每个单元格类别指定特定的部分名称

如何为 UICollectionViewFlowLayout 中的每个单元格添加标题/标签补充视图?