代码之家  ›  专栏  ›  技术社区  ›  theozh

如何在QTableWidget中将拖放与排序相结合?

  •  0
  • theozh  · 技术社区  · 4 年前

    在QTableWidget中,我希望能够:

    • 按列对表格进行排序
    • 通过拖动更改行顺序&将行放到不同的位置

    在经历了大量过时或令人困惑的代码片段之后,到目前为止,对我来说,这是移动行的最清晰的解决方案 here . 排序可以简单地通过以下方式启用/禁用 setSortingEnabled(True/False) .

    拖放单独工作,排序单独工作,但不能一起工作。

    我想现在的情况是,当桌子掉落时,它会再次被排序。 所以,我想如果我在拖动过程中关闭排序,它应该会得到理想的结果,但事实并非如此。显然,我做错了什么。

    我确信这一定是一个小细节,但我在这里错过了什么?看起来我需要去掉列标题中的排序箭头。

    代码: (应该是复制、粘贴和运行):

    import sys
    from PyQt5.QtCore import Qt
    from PyQt5.QtWidgets import QApplication, QWidget, QTableWidget, QVBoxLayout, QTableWidgetItem, QAbstractItemView
    from PyQt5.QtGui import QDropEvent, QDragMoveEvent
    from datetime import datetime
    import random
    
    class TableWidgetDragRows(QTableWidget):
        def __init__(self, *args, **kwargs):
            super().__init__(*args, **kwargs)
    
            self.setSortingEnabled(True)    # enable sorting by default
            self.setDragEnabled(True)
            self.setAcceptDrops(True)
            self.viewport().setAcceptDrops(True)
            self.setDragDropOverwriteMode(False)
            self.setDropIndicatorShown(True)
            self.setSelectionMode(QAbstractItemView.ExtendedSelection)
            self.setSelectionBehavior(QAbstractItemView.SelectRows)
            self.setDragDropMode(QAbstractItemView.InternalMove)
    
        def dragEvent(self, event: QDragMoveEvent):
            self.setSortingEnabled(False)   # disable sorting during dragging
    
        def dropEvent(self, event: QDropEvent):
            self.setSortingEnabled(False)   # disable sorting during dropping
    
            if not event.isAccepted() and event.source() == self:
                drop_row = self.drop_on(event)
    
                rows = sorted(set(item.row() for item in self.selectedItems()))
                rows_to_move = [[QTableWidgetItem(self.item(row_index, column_index)) for column_index in range(self.columnCount())]
                                for row_index in rows]
                for row_index in reversed(rows):
                    self.removeRow(row_index)
                    if row_index < drop_row:
                        drop_row -= 1
    
                for row_index, data in enumerate(rows_to_move):
                    row_index += drop_row
                    self.insertRow(row_index)
                    for column_index, column_data in enumerate(data):
                        self.setItem(row_index, column_index, column_data)
                event.accept()
    
                for row_index in range(len(rows_to_move)):   # maybe can be done smarter
                    for col in range(self.columnCount()):
                        self.item(drop_row + row_index, col).setSelected(True)
    
            super().dropEvent(event)
            self.setSortingEnabled(True)
    
        def drop_on(self, event):
            index = self.indexAt(event.pos())
            if not index.isValid():
                return self.rowCount()
            return index.row() + 1 if self.is_below(event.pos(), index) else index.row()
    
        def is_below(self, pos, index):
            rect = self.visualRect(index)
            margin = 2
            if pos.y() - rect.top() < margin:
                return False
            elif rect.bottom() - pos.y() < margin:
                return True
            # noinspection PyTypeChecker
            return rect.contains(pos, True) and not (int(self.model().flags(index)) & Qt.ItemIsDropEnabled) and pos.y() >= rect.center().y()
    
    class MyWindow(QWidget):
        def __init__(self):
            super(MyWindow,self).__init__()
            self.setGeometry(100,100,600,300)
            self.layout = QVBoxLayout()
            self.setLayout(self.layout)
            
            self.tw = TableWidgetDragRows(self)
            self.layout.addWidget(self.tw)
            self.tw.setRowCount(5)
            self.tw.setColumnCount(3)
            #
            for row in range(self.tw.rowCount()):
                for col in range(self.tw.columnCount()):
                    if col==0:
                        myValue = "".join([chr(random.randint(65,90)) for i in range(0,4)])
                    else:
                        myValue = random.randint(0,100)
                    twi = QTableWidgetItem()
                    twi.setData(Qt.DisplayRole,myValue)
                    self.tw.setItem(row, col, twi)
            self.show()
                
    if __name__ == '__main__':
        app = QApplication(sys.argv)
        app.setStyle("Fusion")
        window = MyWindow()
        sys.exit(app.exec_())
    

    结果:

    enter image description here

    0 回复  |  直到 4 年前
        1
  •  1
  •   ekhumoro    4 年前

    您应该关闭自动排序,改用按需排序。当单击标题时,这只会按列排序,并且可以通过表小部件的标题非常简单地实现:

    header = self.horizontalHeader()
    header.setSortIndicatorShown(True)
    header.sortIndicatorChanged.connect(self.sortItems)
    

    唯一需要的其他更改是删除所有 setSortingEnabled 调用,并在填充表时设置初始排序。所以你的例子看起来像这样:

    import sys
    from PyQt5.QtCore import Qt
    from PyQt5.QtWidgets import QApplication, QWidget, QTableWidget, QVBoxLayout, QTableWidgetItem, QAbstractItemView
    from PyQt5.QtGui import QDropEvent, QDragMoveEvent
    from datetime import datetime
    import random
    
    class TableWidgetDragRows(QTableWidget):
        def __init__(self, *args, **kwargs):
            super().__init__(*args, **kwargs)
    
            # set up on demand sorting
            header = self.horizontalHeader()
            header.setSortIndicatorShown(True)
            header.sortIndicatorChanged.connect(self.sortItems)
    
            self.setDragEnabled(True)
            self.setAcceptDrops(True)
            self.viewport().setAcceptDrops(True)
            self.setDragDropOverwriteMode(False)
            self.setDropIndicatorShown(True)
            self.setSelectionMode(QAbstractItemView.ExtendedSelection)
            self.setSelectionBehavior(QAbstractItemView.SelectRows)
            self.setDragDropMode(QAbstractItemView.InternalMove)
    
        def dropEvent(self, event: QDropEvent):
            if not event.isAccepted() and event.source() == self:
                drop_row = self.drop_on(event)
    
                rows = sorted(set(item.row() for item in self.selectedItems()))
                rows_to_move = [[QTableWidgetItem(self.item(row_index, column_index)) for column_index in range(self.columnCount())]
                                for row_index in rows]
                for row_index in reversed(rows):
                    self.removeRow(row_index)
                    if row_index < drop_row:
                        drop_row -= 1
    
                for row_index, data in enumerate(rows_to_move):
                    row_index += drop_row
                    self.insertRow(row_index)
                    for column_index, column_data in enumerate(data):
                        self.setItem(row_index, column_index, column_data)
                event.accept()
    
                for row_index in range(len(rows_to_move)):   # maybe can be done smarter
                    for col in range(self.columnCount()):
                        self.item(drop_row + row_index, col).setSelected(True)
    
            super().dropEvent(event)
    
        def drop_on(self, event):
            index = self.indexAt(event.pos())
            if not index.isValid():
                return self.rowCount()
            return index.row() + 1 if self.is_below(event.pos(), index) else index.row()
    
        def is_below(self, pos, index):
            rect = self.visualRect(index)
            margin = 2
            if pos.y() - rect.top() < margin:
                return False
            elif rect.bottom() - pos.y() < margin:
                return True
            # noinspection PyTypeChecker
            return rect.contains(pos, True) and not (int(self.model().flags(index)) & Qt.ItemIsDropEnabled) and pos.y() >= rect.center().y()
    
    class MyWindow(QWidget):
        def __init__(self):
            super(MyWindow,self).__init__()
            self.setGeometry(100,100,600,300)
            self.layout = QVBoxLayout()
            self.setLayout(self.layout)
    
            self.tw = TableWidgetDragRows(self)
            self.layout.addWidget(self.tw)
            self.tw.setRowCount(5)
            self.tw.setColumnCount(3)
            for row in range(self.tw.rowCount()):
                for col in range(self.tw.columnCount()):
                    if col==0:
                        myValue = "".join([chr(random.randint(65,90)) for i in range(0,4)])
                    else:
                        myValue = random.randint(0,100)
                    twi = QTableWidgetItem()
                    twi.setData(Qt.DisplayRole,myValue)
                    self.tw.setItem(row, col, twi)
            # do initial sort
            self.tw.sortItems(0)
            self.show()
    
    if __name__ == '__main__':
        app = QApplication(sys.argv)
        app.setStyle("Fusion")
        window = MyWindow()
        sys.exit(app.exec_())