eri*_*ric 11 qt pyqt pyqt4 pyside
目标
我的目标是让QTableWidget用户可以在内部拖放行.也就是说,用户可以拖放整行,将其在表中向上或向下移动到另外两行之间的不同位置.目标如下图所示:

我尝试了什么,发生了什么
一旦我填充了QTableWidget数据,我将其属性设置如下:
table.setDragDropMode(QtGui.QAbstractItemView.InternalMove)
#select one row at a time
table.setSelectionMode(QtGui.QAbstractItemView.SingleSelection)
table.setSelectionBehavior(QtGui.QAbstractItemView.SelectRows)
Run Code Online (Sandbox Code Playgroud)
类似的代码使得QListWidget行为很好:当你在内部移动一个项目时,它被删除在列表的两个元素之间,其余的项目以合理的方式排序,没有数据被覆盖(换句话说,视图就像上图,但它是一个列表).
相反,在使用上述代码修改的表中,事情无法按计划进行.下图显示了实际发生的情况:

在单词中:当行i被删除时,该行在表中变为空白.此外,如果我意外地将行i放到行j上(而不是两行之间的空间),则来自行i 的数据将替换行j中的数据.也就是说,在那个不幸的情况下,除了第i行变成空白之外,第j行被覆盖.
注意我也尝试添加,table.setDragDropOverwriteMode(False)但它没有改变行为.
前进的方向?
这个bug报告可能包括在C++中一个可能的解决方案:看来他们重新实现dropEvent了QTableWidget,但我不知道如何干净地移植到了Python.
相关内容:
thr*_*les 11
这似乎是非常奇怪的默认行为.无论如何,按照你链接到的bug报告中的代码,我已成功将一些东西移植到PyQt.它可能会或可能不会像该代码一样强大,但它至少似乎适用于您在屏幕截图中提供的简单测试用例!
以下实施的潜在问题是:
当前选定的行不遵循拖放(因此,如果移动第三行,则第三行在移动后保持选中状态).这可能不太难修复!
它可能不适用于具有子行的行.我甚至不确定是否QTableWidgetItem可以生孩子,所以也许没事.
我没有测试选择多行,但我认为它应该工作
出于某种原因,尽管在表中插入了新行,但我不必删除正在移动的行.这对我来说似乎很奇怪.它几乎看起来像在任何地方插入一行,但结尾不会增加rowCount()表格.
我的实施GetSelectedRowsFast与他们的实施有点不同.它可能不会很快,并且可能会有一些错误(我不会检查项目是否已启用或可选择),就像它们一样.我认为这也很容易修复,但如果在选择行时禁用某行,然后有人执行拖放操作,则只会出现问题.在这种情况下,我认为更好的解决方案可能是在禁用行时取消选择行,但这取决于你在做什么,我猜!
如果您在生产环境中使用此代码,您可能希望用细齿梳检查它并确保一切都有意义.我的PyQt端口很可能存在问题,并且可能与我的端口基于的原始c ++算法有关.然而,它确实可以证明你想要的东西可以用a来实现QTableWidget.
更新:请注意下面的PyQt5 还有一个额外的答案,它也修复了我上面提到的一些问题.你可能想看一下!
码:
import sys, os
from PyQt4.QtCore import *
from PyQt4.QtGui import *
class TableWidgetDragRows(QTableWidget):
def __init__(self, *args, **kwargs):
QTableWidget.__init__(self, *args, **kwargs)
self.setDragEnabled(True)
self.setAcceptDrops(True)
self.viewport().setAcceptDrops(True)
self.setDragDropOverwriteMode(False)
self.setDropIndicatorShown(True)
self.setSelectionMode(QAbstractItemView.SingleSelection)
self.setSelectionBehavior(QAbstractItemView.SelectRows)
self.setDragDropMode(QAbstractItemView.InternalMove)
def dropEvent(self, event):
if event.source() == self and (event.dropAction() == Qt.MoveAction or self.dragDropMode() == QAbstractItemView.InternalMove):
success, row, col, topIndex = self.dropOn(event)
if success:
selRows = self.getSelectedRowsFast()
top = selRows[0]
# print 'top is %d'%top
dropRow = row
if dropRow == -1:
dropRow = self.rowCount()
# print 'dropRow is %d'%dropRow
offset = dropRow - top
# print 'offset is %d'%offset
for i, row in enumerate(selRows):
r = row + offset
if r > self.rowCount() or r < 0:
r = 0
self.insertRow(r)
# print 'inserting row at %d'%r
selRows = self.getSelectedRowsFast()
# print 'selected rows: %s'%selRows
top = selRows[0]
# print 'top is %d'%top
offset = dropRow - top
# print 'offset is %d'%offset
for i, row in enumerate(selRows):
r = row + offset
if r > self.rowCount() or r < 0:
r = 0
for j in range(self.columnCount()):
# print 'source is (%d, %d)'%(row, j)
# print 'item text: %s'%self.item(row,j).text()
source = QTableWidgetItem(self.item(row, j))
# print 'dest is (%d, %d)'%(r,j)
self.setItem(r, j, source)
# Why does this NOT need to be here?
# for row in reversed(selRows):
# self.removeRow(row)
event.accept()
else:
QTableView.dropEvent(event)
def getSelectedRowsFast(self):
selRows = []
for item in self.selectedItems():
if item.row() not in selRows:
selRows.append(item.row())
return selRows
def droppingOnItself(self, event, index):
dropAction = event.dropAction()
if self.dragDropMode() == QAbstractItemView.InternalMove:
dropAction = Qt.MoveAction
if event.source() == self and event.possibleActions() & Qt.MoveAction and dropAction == Qt.MoveAction:
selectedIndexes = self.selectedIndexes()
child = index
while child.isValid() and child != self.rootIndex():
if child in selectedIndexes:
return True
child = child.parent()
return False
def dropOn(self, event):
if event.isAccepted():
return False, None, None, None
index = QModelIndex()
row = -1
col = -1
if self.viewport().rect().contains(event.pos()):
index = self.indexAt(event.pos())
if not index.isValid() or not self.visualRect(index).contains(event.pos()):
index = self.rootIndex()
if self.model().supportedDropActions() & event.dropAction():
if index != self.rootIndex():
dropIndicatorPosition = self.position(event.pos(), self.visualRect(index), index)
if dropIndicatorPosition == QAbstractItemView.AboveItem:
row = index.row()
col = index.column()
# index = index.parent()
elif dropIndicatorPosition == QAbstractItemView.BelowItem:
row = index.row() + 1
col = index.column()
# index = index.parent()
else:
row = index.row()
col = index.column()
if not self.droppingOnItself(event, index):
# print 'row is %d'%row
# print 'col is %d'%col
return True, row, col, index
return False, None, None, None
def position(self, pos, rect, index):
r = QAbstractItemView.OnViewport
margin = 2
if pos.y() - rect.top() < margin:
r = QAbstractItemView.AboveItem
elif rect.bottom() - pos.y() < margin:
r = QAbstractItemView.BelowItem
elif rect.contains(pos, True):
r = QAbstractItemView.OnItem
if r == QAbstractItemView.OnItem and not (self.model().flags(index) & Qt.ItemIsDropEnabled):
r = QAbstractItemView.AboveItem if pos.y() < rect.center().y() else QAbstractItemView.BelowItem
return r
class Window(QWidget):
def __init__(self):
super(Window, self).__init__()
layout = QHBoxLayout()
self.setLayout(layout)
self.table_widget = TableWidgetDragRows()
layout.addWidget(self.table_widget)
# setup table widget
self.table_widget.setColumnCount(2)
self.table_widget.setHorizontalHeaderLabels(['Colour', 'Model'])
items = [('Red', 'Toyota'), ('Blue', 'RV'), ('Green', 'Beetle')]
for i, (colour, model) in enumerate(items):
c = QTableWidgetItem(colour)
m = QTableWidgetItem(model)
self.table_widget.insertRow(self.table_widget.rowCount())
self.table_widget.setItem(i, 0, c)
self.table_widget.setItem(i, 1, m)
self.show()
app = QApplication(sys.argv)
window = Window()
sys.exit(app.exec_())
Run Code Online (Sandbox Code Playgroud)
小智 10
这是三菠萝答案的修订版,专为 PyQt5 和 Python 3 设计。它还修复了多选拖放和移动后重新选择行的问题。
import sys
from PyQt5.QtCore import Qt
from PyQt5.QtGui import QDropEvent
from PyQt5.QtWidgets import QTableWidget, QAbstractItemView, QTableWidgetItem, QWidget, QHBoxLayout, \
QApplication
class TableWidgetDragRows(QTableWidget):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
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)):
self.item(drop_row + row_index, 0).setSelected(True)
self.item(drop_row + row_index, 1).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 Window(QWidget):
def __init__(self):
super(Window, self).__init__()
layout = QHBoxLayout()
self.setLayout(layout)
self.table_widget = TableWidgetDragRows()
layout.addWidget(self.table_widget)
# setup table widget
self.table_widget.setColumnCount(2)
self.table_widget.setHorizontalHeaderLabels(['Type', 'Name'])
items = [('Red', 'Toyota'), ('Blue', 'RV'), ('Green', 'Beetle'), ('Silver', 'Chevy'), ('Black', 'BMW')]
self.table_widget.setRowCount(len(items))
for i, (color, model) in enumerate(items):
self.table_widget.setItem(i, 0, QTableWidgetItem(color))
self.table_widget.setItem(i, 1, QTableWidgetItem(model))
self.resize(400, 400)
self.show()
if __name__ == '__main__':
app = QApplication(sys.argv)
window = Window()
sys.exit(app.exec_())
Run Code Online (Sandbox Code Playgroud)
所以我最近遇到了同样的问题,我将上面的代码块提炼成我认为具有相同行为但更简洁的东西。
def dropEvent(self, event):
if event.source() == self:
rows = set([mi.row() for mi in self.selectedIndexes()])
targetRow = self.indexAt(event.pos()).row()
rows.discard(targetRow)
rows = sorted(rows)
if not rows:
return
if targetRow == -1:
targetRow = self.rowCount()
for _ in range(len(rows)):
self.insertRow(targetRow)
rowMapping = dict() # Src row to target row.
for idx, row in enumerate(rows):
if row < targetRow:
rowMapping[row] = targetRow + idx
else:
rowMapping[row + len(rows)] = targetRow + idx
colCount = self.columnCount()
for srcRow, tgtRow in sorted(rowMapping.iteritems()):
for col in range(0, colCount):
self.setItem(tgtRow, col, self.takeItem(srcRow, col))
for row in reversed(sorted(rowMapping.iterkeys())):
self.removeRow(row)
event.accept()
return
Run Code Online (Sandbox Code Playgroud)
| 归档时间: |
|
| 查看次数: |
14097 次 |
| 最近记录: |