【问题标题】:How to change row order in a QTableWidget by editing cells?如何通过编辑单元格来更改 QTableWidget 中的行顺序?
【发布时间】:2021-06-20 02:57:25
【问题描述】:

在 QTableWidget 中有不同的方式来改变行顺序:

  1. 通过拖放进行内部移动
  2. 通过单独的按钮将所选行向上或向下移动一个位置

事实证明,这两种方法对于较长的列表和我的特殊目的不是很实用。 因此,我尝试通过更改单元格值来分配新位置来实现以下方法:

  • 第一列保存当前位置编号
  • 通过编辑这些数字,我想将新位置分配给这一行
  • 我希望只允许编辑第一列
  • 如果输入了无效的位置编号(在行数范围内),则不应更改任何内容
  • 如果输入了有效的位置编号,则第一列中的其他位置编号会相应修改。
  • 然后我可以通过单击列标题以新顺序获取重新排列的行以按第一列排序。

示例:职位编号1,2,3,4,5。 如果我将 row3,column1 中的值从 3 更改为 1,则第一列中的位置编号应更改如下:

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

但是,setEditTriggers(QAbstractItemView.NoEditTriggers)setEditTriggers(QAbstractItemView.DoubleClicked) 似乎有问题。

根据我尝试的一些不同的代码变体,看起来我仍然得到一个EditTrigger,尽管我认为我已经通过self.setEditTriggers(QAbstractItemView.NoEditTriggers) 禁用了 EditTriggers。 或者我得到RecursionError: maximum recursion depth exceeded while calling a Python object。 或TypeError: '>' not supported between instances of 'NoneType' and 'int'

我希望我能把问题说清楚。我在这里做错了什么?

代码:(最小化的非工作示例。应该是复制 & 粘贴 & 运行)

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

结果:

【问题讨论】:

  • 如果你通过连接标题的 sortIndicatorChanged 信号来禁用排序,为什么要禁用排序?
  • @musicamante 抱歉,这是早期版本的剩余部分。可以删除。
  • 好的。另一个问题:重新排序是否只能通过单击标题来完成,或者也可以在编辑更改时完成?我的意思是,如果我将值从 3 更改为 1,模型应该自动排序还是应该仅在从标题手动排序时发生?
  • @musicamante 手动排序可能会更好。我之前在填充表格期间使用自动排序遇到了这个问题。这就是我遵循建议关闭排序或进行手动排序的原因:请参阅 stackoverflow.com/q/62284220/7295599stackoverflow.com/q/66506588/7295599

标签: python pyqt pyqt5 qtablewidget


【解决方案1】:

您可以为此使用稍加修改的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_()

【讨论】:

  • 请注意,OP 使用的是 PyQt,而不是 PySide,因此导入应该反映这一点,并且信号被命名为 pyqtSignal
  • @mugiseyebrows 谢谢你的回答。我尝试了您的建议,但是,行标题的数字被“打乱”了。从 1 到最大值(此处为 5),它们应始终保持不变。也许需要一些额外的步骤才能让它们重新整理好?!
  • @theozh 是的,覆盖 headerData 可以解决这个问题,我更新了代码
  • 这种方法似乎马上就奏效了。但是,由于我对 PyQt 的理解有限,它非常抽象,我害怕在尝试将这个最小示例集成到我的较大代码中时遇到新的未知数。但可以肯定的是,从长远来看,我需要学习和理解这些模型。再次感谢您!
【解决方案2】:

主要问题是您尝试以错误的方式禁用编辑:由于视图对事件的反应方式,切换编辑触发器不会为您提供有效的结果。

递归错误是由于您正在更改数据对数据更改做出反应的信号,这显然不是一件好事。

另一个问题与当前项目有关,在某些情况下可能变为None

首先,禁用项目编辑的正确方法是设置项目的flags。这解决了您可能还没有发现的另一个问题:在编辑模式下按 Tab 可以更改其他列中的数据。

然后,为了正确使用第一列来设置顺序,您应该确保所有其他行都正确“重新编号”。由于这样做还需要在其他项目中设置数据,因此您必须暂时断开与更改的信号的连接。

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)

请注意,您还必须更改创建项目的函数并使用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)

【讨论】:

  • 非常感谢您的回答和解释。我不知道disconnect 在这里似乎很重要。我会测试你的代码并尽快给出反馈。
  • 它几乎可以工作。如果我输入一个高于 rowCount 的数字,我可能会在第一列中得到一些重复的数字。它试图找到它的来源。
  • @theozh 抱歉,我在添加范围修复后忘记检查了,这显然需要将断开连接移到上面;查看更新的代码。
  • 嗯,现在,在填充表格时,我在rowNumber = rowItem.data(Qt.DisplayRole) - 1 行中得到AttributeError: 'NoneType' object has no attribute 'data'
  • 我没有收到那个错误。您是否更改了填充表格的函数?您是否按照说明在项目而不是上使用setData
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2014-03-25
  • 2010-10-05
  • 2020-12-25
  • 1970-01-01
  • 2020-06-06
  • 2018-10-30
  • 2018-08-28
相关资源
最近更新 更多