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

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

【问题讨论】:

    标签: qt pyqt qt5 pyqt5


    【解决方案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_()
    

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2014-02-12
      • 1970-01-01
      • 2016-09-15
      • 2018-08-30
      • 2014-01-22
      • 1970-01-01
      相关资源
      最近更新 更多