PyQt validity check on QTreeWidgetItem

2019-08-02 17:32发布

问题:

I'm building a QTreeWidget where I'm implementing adding new item and renaming functionality. I'd like to check the validity of the new name given by user, including:

  1. the name can only contain a list of valid characters. This is achieved already by adding a QRegExpValidator to a subclassed QItemDelegate, and assigning the new delegate to the QTreeWidget.
  2. the name can't conflict with its siblings. This I don't know now to achieve.

Here is my current attempt:

import sys
from PyQt5.QtWidgets import QItemDelegate, QTreeWidget, QVBoxLayout, QLineEdit,\
        QMainWindow, QWidget, QTreeWidgetItem, QApplication
from PyQt5.QtCore import QRegExp, Qt
from PyQt5.QtGui import QRegExpValidator


class TreeWidgetDelegate(QItemDelegate):
    def __init__(self, parent=None):
        QItemDelegate.__init__(self, parent=parent)

    def createEditor(self, parent, option, index):
        editor = QLineEdit(parent)
        reg=QRegExp('[A-z0-9\[\]_-]+')
        vd=QRegExpValidator(reg)

        editor.setValidator(vd)
        return editor


class MainWindow(QMainWindow):
    def __init__(self):
        super(self.__class__, self).__init__()
        frame=QWidget()
        self.setCentralWidget(frame)
        hl=QVBoxLayout()
        frame.setLayout(hl)

        self.tree=QTreeWidget(self)
        mydele=TreeWidgetDelegate()
        self.tree.setItemDelegate(mydele)
        hl.addWidget(self.tree)

        # add treewidgetitems
        for ii in range(5):
            item=QTreeWidgetItem([str(ii),])
            self.tree.addTopLevelItem(item)

        self.tree.itemDoubleClicked.connect(self.rename)
        self.tree.itemChanged.connect(self.checkString)

        dele=self.tree.itemDelegate()
        print('dele',dele)

        self.show()

    def rename(self):
        item=self.tree.selectedItems()
        if item:
            item=item[0]
            item.setFlags(item.flags() | Qt.ItemIsEditable)
            self.tree.scrollToItem(item)
            self.tree.editItem(item)

    def checkString(self,item,column):
        text=item.data(0,column)
        print('newname:',text)

        siblings=[self.tree.topLevelItem(ii).data(0,0) for ii in \
                range(self.tree.topLevelItemCount())]
        print('siblings:',siblings)

        if text in siblings:
            print('invalid name')

            # this gives "edit: editing failed"
            self.tree.editItem(item)


if __name__ == "__main__":
     app = QApplication(sys.argv)
     form = MainWindow()
     form.show()
     sys.exit(app.exec_())

In particular, I'm connecting tree.itemChanged.connect(self.checkString), and checkString() checks name conflicts. However, when conflict detected I don't know how to revert to the old name and re-enter into the edit mode and let user rename again. The tree.editItem(item) would throw an error

edit: editing failed

. I guess that would trigger the signal again and end up in an endless loop?

I've found PyQt - Using Multiple Validators for Item Delegates related, but no answer is given, only a suggestion in the comment that one should subclass QValidator to handle name conflict detection in the same regex. No idea how to do this, the validator is created and assigned before those QTreeWidgetItems, isn't it?

Also this question Make QTreeWidgetItem unique among siblings. No body answered.

回答1:

I've found a solution:

import sys
from PyQt5.QtWidgets import QItemDelegate, QTreeWidget, QVBoxLayout, QLineEdit,\
        QMainWindow, QWidget, QTreeWidgetItem, QApplication
from PyQt5.QtCore import QRegExp, Qt
from PyQt5.QtGui import QRegExpValidator


class TreeWidgetDelegate(QItemDelegate):
    def __init__(self, parent=None):
        QItemDelegate.__init__(self, parent=parent)

    def createEditor(self, parent, option, index):
        editor = QLineEdit(parent)
        # allow only these chars
        reg=QRegExp('[A-z0-9\[\]_-]+')
        regvd=QRegExpValidator(reg)
        editor.setValidator(regvd)
        return editor


class MainWindow(QMainWindow):

    def __init__(self):
        super(QMainWindow, self).__init__()
        frame=QWidget()
        self.setCentralWidget(frame)
        hl=QVBoxLayout()
        frame.setLayout(hl)

        self.tree=QTreeWidget(self)
        hl.addWidget(self.tree)

        # assign custom delegate to treewidget
        dele=TreeWidgetDelegate()
        self.tree.setItemDelegate(dele)

        # add treewidgetitems
        for ii in range(5):
            item=QTreeWidgetItem([str(ii)*3,])
            self.tree.addTopLevelItem(item)

        self.tree.itemDoubleClicked.connect(self.rename)

        # QueuedConnection cures the editting failed issue
        self.tree.itemChanged.connect(self.checkName, Qt.QueuedConnection)

        self.show()

    def getSiblings(self,item):
        siblings=[self.tree.topLevelItem(ii).data(0,0) for ii in \
                range(self.tree.topLevelItemCount())]
        item_text=item.data(0,0)
        if item_text in siblings:
            siblings.remove(item_text)
        return siblings

    def rename(self):
        item=self.tree.selectedItems()
        if item:
            item=item[0]
            item.setFlags(item.flags() | Qt.ItemIsEditable)
            self.tree.scrollToItem(item)
            self.tree.editItem(item)

    def checkName(self,item,column):
        text=item.data(0,0)
        siblings=self.getSiblings(item)
        print('checkName: slibings:', siblings)

        if text in siblings:
            print('checkName: ivalid')
            item.setData(0,0,'New_name_needed')
            self.tree.editItem(item)


if __name__ == "__main__":
     app = QApplication(sys.argv)
     form = MainWindow()
     form.show()
     sys.exit(app.exec_())

It's still using a custom delegate to check for invalid characters. I tried adding the sibling conflict check in the delegate's editor, by subclassing QValidator and providing it the current list of siblings. However this would perform on-the-fly validation rather than post-commit validation. For instance when checking for the 'abc' v.s. 'abc' conflict, I won't be able to type 'c' after 'ab', even though I meant to type 'abcd'.

I found this question regarding the edit: editting failed error, and it seems that the Qt.QueuedConnection does the trick. So the tree.itemChanged is connected to a duplication check function, and if it fails the check, it prompts the user to re-enter name again. One can optionally popup a tooltip notifying the conflict.

May not be the ideal solution though.