简体   繁体   English

如何通过编辑单元格来更改 QTableWidget 中的行顺序?

[英]How to change row order in a QTableWidget by editing cells?

There are different ways to change row order in QTableWidget:在 QTableWidget 中有不同的方法来改变行顺序:

  1. by internal move via drag & drop通过拖放进行内部移动
  2. by separate buttons which shift a selected row up or down by one position通过单独的按钮将所选行向上或向下移动一个 position

It turned out that these two approaches are not very practical for longer lists and my special purpose.事实证明,这两种方法对于较长的列表和我的特殊目的不是很实用。 So, I tried to implement the following approach by assigning the new position by changing cell values:因此,我尝试通过更改单元格值来分配新的 position 来实现以下方法:

  • the first column holds current position number第一列包含当前 position 编号
  • by editing these numbers I want to assign the new position to this row通过编辑这些数字,我想将新的 position 分配给这一行
  • I want to allow editing only on the first column我想只允许在第一列进行编辑
  • if an invalid position number is entered (within the range of number of rows) nothing should change如果输入了无效的 position 编号(在行数范围内),则不应更改
  • if a valid position number is entered the other position numbers in the first column are modified accordingly.如果输入了有效的 position 编号,则第一列中的其他 position 编号将相应修改。
  • then I can get the rearranged rows in new order by clicking on the column header for sorting by the first column.然后我可以通过单击列 header 以按第一列排序,以新顺序获得重新排列的行。

Example: position numbers 1,2,3,4,5 .示例: position 编号1,2,3,4,5 If I change the value in row3,column1 from 3 to 1, the position numbers in the first column should change as follows:如果我将 row3,column1 中的值从 3 更改为 1,则第一列中的 position 数字应更改如下:

1 --> 2
2 --> 3
3 --> 1
4 --> 4
5 --> 5

However, it seems I get problems with setEditTriggers(QAbstractItemView.NoEditTriggers) and setEditTriggers(QAbstractItemView.DoubleClicked) .但是,似乎我遇到了setEditTriggers(QAbstractItemView.NoEditTriggers)setEditTriggers(QAbstractItemView.DoubleClicked)的问题。

Depending on some different code variations I tried, it looks like I still get an EditTrigger although I think I have disabled EditTriggers via self.setEditTriggers(QAbstractItemView.NoEditTriggers) .根据我尝试的一些不同的代码变体,看起来我仍然得到一个EditTrigger ,尽管我认为我已经通过self.setEditTriggers(QAbstractItemView.NoEditTriggers)禁用了 EditTriggers。 Or I get RecursionError: maximum recursion depth exceeded while calling a Python object .或者我得到RecursionError: maximum recursion depth exceeded while calling a Python object Or TypeError: '>' not supported between instances of 'NoneType' and 'int' .TypeError: '>' not supported between instances of 'NoneType' and 'int'

I hope I could make the problem clear enough.我希望我能把问题说清楚。 What am I doing wrong here?我在这里做错了什么?

Code: (minimized non-working example. Should be copy & paste & run)代码:(最小化的非工作示例。应该是复制&粘贴&运行)

import sys
from PyQt5.QtWidgets import QMainWindow, QApplication, QWidget, QAction, QTableWidget, QTableWidgetItem, QVBoxLayout, QPushButton, QAbstractItemView
from PyQt5.QtCore import pyqtSlot, Qt
import random

class MyTableWidget(QTableWidget):

    def __init__(self):
        super().__init__()
        
        self.setColumnCount(3)
        self.setRowCount(7)
        self.setSortingEnabled(False)
        header = self.horizontalHeader()
        header.setSortIndicatorShown(True)
        header.sortIndicatorChanged.connect(self.sortItems)
        self.setSelectionBehavior(QAbstractItemView.SelectRows)
        self.setEditTriggers(QAbstractItemView.NoEditTriggers)

        self.col_pos = 0
        self.oldPosValue = None
        self.manualChange = False
        self.cellDoubleClicked.connect(self.cell_doubleClicked)
        self.cellChanged.connect(self.cell_changed)
        
    def cell_doubleClicked(self):
        self.setEditTriggers(QAbstractItemView.NoEditTriggers)
        if self.currentColumn() != self.col_pos:    # editing allowed only for this column
            return
        self.setEditTriggers(QAbstractItemView.DoubleClicked)
        try:
            self.oldPosValue = int(self.currentItem().text())
        except:
            pass
        self.manualChange = True
            
    def cell_changed(self):
        if not self.manualChange:
            return
        self.setEditTriggers(QAbstractItemView.NoEditTriggers)
        try:
            newPosValue = int(self.currentItem().text())
        except:
            newPosValue = None
    
        rowChanged = self.currentRow()
        print("Value: {} --> {}".format(self.oldPosValue, newPosValue))
        if newPosValue>0 and newPosValue<=self.rowCount():
            for row in range(self.rowCount()):
                if row != rowChanged:
                    try:
                        value = int(self.item(row,self.col_pos).text())
                        if value<newPosValue:
                            self.item(row,self.col_pos).setData(Qt.EditRole,value+1)
                    except:
                        print("Error")
                        pass
        else:
            self.item(rowChanged,self.col_pos).setData(Qt.EditRole,self.oldPosValue)
            print("New value outside range")
        self.manualChange = True

class App(QWidget):

    def __init__(self):
        super().__init__()
        self.title = 'PyQt5 table'
        self.initUI()
        
    def initUI(self):
        self.setWindowTitle(self.title)
        self.setGeometry(0,0,400,300)
        self.layout = QVBoxLayout()

        self.tw = MyTableWidget()
        self.layout.addWidget(self.tw)

        self.pb_refill = QPushButton("Refill")
        self.pb_refill.clicked.connect(self.on_click_pb_refill)
        self.layout.addWidget(self.pb_refill)
        
        self.setLayout(self.layout) 
        self.show()

    @pyqtSlot()

    def on_click_pb_refill(self):
        self.tw.setEditTriggers(QAbstractItemView.NoEditTriggers)
        for row in range(self.tw.rowCount()):
            for col in range(self.tw.columnCount()):
                if col==0:
                    number = row+1
                else:
                    number = random.randint(1000,9999)
                twi = QTableWidgetItem()
                self.tw.setItem(row, col, twi)
                self.tw.item(row, col).setData(Qt.EditRole,number)

if __name__ == '__main__':
    app = QApplication(sys.argv)
    ex = App()
    sys.exit(app.exec_())

Result:结果:

在此处输入图像描述

The main problem is that you're trying to disable editing in the wrong way: toggling the edit triggers won't give you a valid result due to the way the view reacts to events.主要问题是您试图以错误的方式禁用编辑:由于视图对事件的反应方式,切换编辑触发器不会给您一个有效的结果。

The recursion error is due to the fact that you are changing data in the signal that reacts to data changes, which clearly is not a good thing to do.递归错误是由于您正在更改对数据更改做出反应信号中的数据,这显然不是一件好事。

The other problem is related to the current item, which could become None in certain situations.另一个问题与当前项目有关,在某些情况下可能变为None

First of all, the correct way to disable editing of items is by setting the item's flags .首先,禁用项目编辑的正确方法是设置项目的标志 This solves another problem you didn't probably found yet: pressing Tab while in editing mode, allows to change data in the other columns.这解决了您可能还没有发现的另一个问题:在编辑模式下按Tab可以更改其他列中的数据。

Then, in order to correctly use the first column to set the order, you should ensure that all other rows get correctly "renumbered".然后,为了正确使用第一列来设置顺序,您应该确保所有其他行都正确“重新编号”。 Since doing that also requires setting data in other items, you must temporarily disconnect from the changed signal.由于这样做还需要在其他项目中设置数据,因此您必须暂时断开与更改的信号的连接。

class MyTableWidget(QTableWidget):

    def __init__(self):
        super().__init__()
        
        self.setColumnCount(3)
        self.setRowCount(7)
        self.setSortingEnabled(False)
        header = self.horizontalHeader()
        header.setSortIndicatorShown(True)
        header.sortIndicatorChanged.connect(self.sortItems)
        self.setSelectionBehavior(QAbstractItemView.SelectRows)
        self.setEditTriggers(QAbstractItemView.DoubleClicked)
        self.itemChanged.connect(self.cell_changed)

    def cell_changed(self, item):
        if item.column():
            return
        newRow = item.data(Qt.DisplayRole)
        self.itemChanged.disconnect(self.cell_changed)
        if not 1 <= newRow <= self.rowCount():
            if newRow < 1:
                newRow = 1
                item.setData(Qt.DisplayRole, 1)
            elif newRow > self.rowCount():
                newRow = self.rowCount()
                item.setData(Qt.DisplayRole, self.rowCount())

        otherItems = []
        for row in range(self.rowCount()):
            otherItem = self.item(row, 0)
            if otherItem == item:
                continue
            otherItems.append(otherItem)

        otherItems.sort(key=lambda i: i.data(Qt.DisplayRole))
        for r, item in enumerate(otherItems, 1):
            if r >= newRow:
                r += 1
            item.setData(Qt.DisplayRole, r)
        self.itemChanged.connect(self.cell_changed)

    def setItem(self, row, column, item):
        # override that automatically disables editing if the item is not on the
        # first column of the table
        self.itemChanged.disconnect(self.cell_changed)
        super().setItem(row, column, item)
        if column:
            item.setFlags(item.flags() & ~Qt.ItemIsEditable)
        self.itemChanged.connect(self.cell_changed)

Note that you must also change the function that creates the items and use item.setData before adding the item to the table:请注意,您还必须更改创建项目的 function 并在将项目添加到表之前使用item.setData

    def on_click_pb_refill(self):
        for row in range(self.tw.rowCount()):
            for col in range(self.tw.columnCount()):
                if col==0:
                    number = row+1
                else:
                    number = random.randint(1000,9999)
                twi = QTableWidgetItem()
                twi.setData(Qt.EditRole, number)
                self.tw.setItem(row, col, twi)

You can use slightly modified QStandardItemModel and QSortFilterProxyModel for that您可以为此使用稍作修改的QStandardItemModelQSortFilterProxyModel

from PyQt5 import QtWidgets, QtCore, QtGui
from PyQt5.QtCore import Qt, pyqtSignal
import random
from contextlib import suppress

def shiftRows(old, new, count):
    items = list(range(1, count + 1))
    item = items.pop(old - 1)
    items.insert(new - 1, item)
    return {item: i + 1 for i, item in enumerate(items)}
    
class Model(QtGui.QStandardItemModel):

    orderChanged = pyqtSignal()

    def __init__(self, rows, columns, parent = None):
        super().__init__(rows, columns, parent)
        self._moving = True
        for row in range(self.rowCount()):
            self.setData(self.index(row, 0), int(row + 1))
            self.setData(self.index(row, 1), random.randint(1000,9999))
            self.setData(self.index(row, 2), random.randint(1000,9999))
        self._moving = False

    def swapRows(self, old, new):
        self._moving = True
        d = shiftRows(old, new, self.rowCount())
        for row in range(self.rowCount()):
            index = self.index(row, 0)
            v = index.data()
            if d[v] != v:
                self.setData(index, d[v])
        self.orderChanged.emit()
        self._moving = False

    def flags(self, index):
        if index.column() == 0:
            return Qt.ItemIsEditable | Qt.ItemIsSelectable | Qt.ItemIsEnabled
        return Qt.ItemIsSelectable | Qt.ItemIsEnabled

    def headerData(self, section, orientation, role):
        if orientation == Qt.Vertical and role == Qt.DisplayRole:
            return self.index(section, 0).data()
        return super().headerData(section, orientation, role)

    def setData(self, index, value, role = Qt.DisplayRole):
        if role == Qt.EditRole and index.column() == 0:
            if self._moving:
                return super().setData(self, index, value, role)
            with suppress(ValueError):
                value = int(value)
                if value < 1 or value > self.rowCount():
                    return False
                prev = index.data()
                self.swapRows(prev, value)
            return True
        return super().setData(index, value, role)
    
if __name__ == "__main__":
    app = QtWidgets.QApplication([])

    model = Model(5, 3)
    sortModel = QtCore.QSortFilterProxyModel()
    sortModel.setSourceModel(model)
    model.orderChanged.connect(lambda: sortModel.sort(0))
    view = QtWidgets.QTableView()
    view.setModel(sortModel)
    view.show()

    app.exec_()

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM