在 QTableWidget 中拖放行 [英] Drag and drop rows within QTableWidget

查看:142
本文介绍了在 QTableWidget 中拖放行的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

目标

我的目标是拥有一个 QTableWidget,用户可以在其中在内部拖放行.也就是说,用户可以拖放一整行,将其在表格中向上或向下移动到其他两行之间的不同位置.目标如下图所示:

My goal is to have a QTableWidget in which the user can drag/drop rows internally. That is, the user can drag and drop one entire row, moving it up or down in the table to a different location in between two other rows. The goal is illustrated in this figure:

我尝试了什么,然后发生了什么

用数据填充 QTableWidget 后,我将其属性设置如下:

Once I have populated a QTableWidget with data, I set its properties as follows:

table.setDragDropMode(QtGui.QAbstractItemView.InternalMove)   
#select one row at a time
table.setSelectionMode(QtGui.QAbstractItemView.SingleSelection) 
table.setSelectionBehavior(QtGui.QAbstractItemView.SelectRows)

类似的代码使 QListWidget 表现得很好:当你在内部移动一个项目时,它被放置在列表的两个元素之间,其余的项目以合理的方式自行排序,没有数据被覆盖(换句话说,视图的作用类似于上图,但它是一个列表).

Similar code makes QListWidget behave nicely: when you move an item internally, it is dropped between two elements of the list, and the rest of the items sort themselves out in a reasonable way, with no data overwritten (in other words, the view acts like the figure above, but it is a list).

相比之下,在使用上述代码修改的表格中,事情并没有按计划进行.下图显示了实际发生的情况:

In contrast, in a table modified with the code above, things don't work out as planned. The following figure shows what actually happens:

换句话说:当行 i 被删除时,该行在表格中变为空白.此外,如果我不小心将行 i 放到行 j(而不是两行之间的空间)上,则行 i 中的数据替换j中的数据.也就是说,在这种不幸的情况下,除了行 i 变为空白之外,行 j 也被覆盖.

In words: when row i is dropped, that row becomes blank in the table. Further, if I accidentally drop row i onto row j (instead of the space between two rows), the data from row i replaces the data in row j. That is, in that unfortunate case, in addition to row i becoming blank, row j is overwritten.

注意我也尝试添加 table.setDragDropOverwriteMode(False) 但它没有改变行为.

Note I also tried adding table.setDragDropOverwriteMode(False) but it didn't change the behavior.

前进的道路?

此错误报告可能包含 C++ 中的可能解决方案:似乎他们为 QTableWidget 重新实现了 dropEvent,但我不确定如何干净地移植到 Python.

This bug report might include a possible solution in C++: it seems they reimplemented dropEvent for QTableWidget, but I am not sure how to cleanly port to Python.

相关内容:

推荐答案

这似乎是非常奇怪的默认行为.无论如何,按照 您链接到的错误报告,我已成功将某些内容移植到 PyQt.它可能会,也可能不会像该代码那样健壮,但它至少似乎适用于您在屏幕截图中提供的简单测试用例!

This seems very bizarre default behaviour. Anyway, following the code in the bug report you linked to, I have successfully ported something to PyQt. It may, or may not be as robust as that code, but it at least seems to work for the simple test case you provide in your screenshots!

以下实施的潜在问题是:

The potential issues with the below implementation are:

  • 当前选中的行不跟随拖放(所以如果你移动第三行,移动后第三行保持选中状态).这可能不太难解决!

  • The currently selected row doesn't follow the drag and drop (so if you move the third row, the third row stays selected after the move). This probably isn't too hard to fix!

它可能不适用于具有子行的行.我什至不确定 QTableWidgetItem 是否可以有孩子,所以也许没问题.

It might not work for rows with child rows. I'm not even sure if a QTableWidgetItem can have children, so maybe it is fine.

我还没有测试过选择多行,但我认为它应该可以工作

I haven't tested with selecting multiple rows, but I think it should work

出于某种原因,尽管在表中插入了一个新行,但我不必删除正在移动的行.这对我来说似乎很奇怪.它几乎看起来像是在任何地方插入一行,但最后不会增加表的 rowCount().

For some reason I didn't have to remove the row that was being moved, despite inserting a new row into the table. This seems very odd to me. It almost appears like inserting a row anywhere but the end does not increase the rowCount() of the table.

我对 GetSelectedRowsFast 的实现与他们的略有不同.它可能不快,并且可能像他们那样存在一些错误(我不检查项目是否已启用或可选择).我认为这也很容易解决,但如果您在选中一行时禁用它,然后有人执行拖放操作,这只是一个问题.在这种情况下,我认为更好的解决方案可能是在行被禁用时取消选择行,但我猜这取决于您用它做什么!

My implementation of GetSelectedRowsFast is a bit different to theirs. It may not be fast, and could potentially have some bugs in it (I don't check if the items are enabled or selectable) like they did. This would also be easy to fix I think, but is only a problem if you disable a row while it is selected and someone then performs a drag/drop. In this situation, I think the better solution might be to unselect rows as they were disabled, but it depends on what you are doing with it I guess!

如果您在生产环境中使用此代码,您可能希望仔细检查它并确保一切都有意义.我的 PyQt 端口很可能存在问题,并且我的端口所基于的原始 C++ 算法也可能存在问题.然而,它确实证明了使用 QTableWidget 可以实现您想要的.

If you were using this code in a production environment, you would probably want to go over it with a fine-tooth-comb and make sure everything made sense. There are quite probably issues with my PyQt port, and possibly issues with the original c++ algorithm my port was based on. It does however serve as a proof that what you want can be achieved using a QTableWidget.

更新:请注意,PyQt5 有一个下面的附加答案,也解决了我上面的一些问题.你可能想看看!

Update: note there is an additional answer below for PyQt5 that also fixes some of the concerns I had above. You might want to check it out!

代码:

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_())

这篇关于在 QTableWidget 中拖放行的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆