如何在 QTableWidget 中将拖放与排序结合起来?
How to combine drag and drop with sorting in a QTableWidget?
在 QTableWidget 中,我希望能够:
- 按列对 table 进行排序
- 通过将行拖放到不同的位置来更改行顺序
经过大量过时或令人困惑的代码片段,到目前为止,对我来说,我找到了最清晰的移动行解决方案 here。
排序可以简单地是 enabled/disabled by setSortingEnabled(True/False)
.
拖放单独工作,排序单独工作,但不能一起工作。
我想发生的事情是当删除 table 时将再次排序。
所以,我想如果我在拖动过程中关闭排序应该会得到想要的结果,但事实并非如此。显然,我做错了什么。
我确定这一定是一个小细节,但我在这里遗漏了什么?看来我需要去掉 header.
列中的排序箭头
代码:(应该是复制粘贴&运行):
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_())
结果:
您应该关闭自动排序并改为使用 on-demand 排序。这只会在单击 header 时按列排序,并且可以通过 table-widget:
的 header 非常简单地实现
header = self.horizontalHeader()
header.setSortIndicatorShown(True)
header.sortIndicatorChanged.connect(self.sortItems)
唯一需要的其他更改是删除所有 setSortingEnabled
调用,并在填充 table 时设置初始排序。所以你的例子看起来像这样:
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_())
在 QTableWidget 中,我希望能够:
- 按列对 table 进行排序
- 通过将行拖放到不同的位置来更改行顺序
经过大量过时或令人困惑的代码片段,到目前为止,对我来说,我找到了最清晰的移动行解决方案 here。
排序可以简单地是 enabled/disabled by setSortingEnabled(True/False)
.
拖放单独工作,排序单独工作,但不能一起工作。
我想发生的事情是当删除 table 时将再次排序。 所以,我想如果我在拖动过程中关闭排序应该会得到想要的结果,但事实并非如此。显然,我做错了什么。
我确定这一定是一个小细节,但我在这里遗漏了什么?看来我需要去掉 header.
列中的排序箭头代码:(应该是复制粘贴&运行):
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_())
结果:
您应该关闭自动排序并改为使用 on-demand 排序。这只会在单击 header 时按列排序,并且可以通过 table-widget:
的 header 非常简单地实现header = self.horizontalHeader()
header.setSortIndicatorShown(True)
header.sortIndicatorChanged.connect(self.sortItems)
唯一需要的其他更改是删除所有 setSortingEnabled
调用,并在填充 table 时设置初始排序。所以你的例子看起来像这样:
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_())