PyQt5“类似代码”语法的代码完成

问题描述 投票:0回答:1

我正在尝试创建一个 GUI(使用 PyQt5)文本完成功能,它支持类似于编程 IDE 的功能,其中我有对象,对象有字段和子字段,可以通过“.”访问。例如,如果用户输入“obj.fi”,我希望它建议“field1”,如果用户输入“obj.field1.s”,它应该建议“subfield1,subfield2等...”

我已经能够使用以下脚本在 QLineEdit 小部件上运行它:

import sys, re

from PyQt5 import QtCore
from PyQt5.QtCore import Qt
from PyQt5.QtGui import QStandardItemModel, QStandardItem
from PyQt5.QtWidgets import QApplication, QWidget, QVBoxLayout, QLineEdit, QCompleter

test_model_data = [
    ('obj1',[ ('field1',[('subfield1',[])]),
              ('field2',[])]),                        
    ('obj2',[ ('field1',[('subfiedl1',[])]),
              ('field2',[])]),                        
    ('obj3',[]),
    ('music',[('melody',[]), 
              ('harmony',[('chords',[]),
                          ('rythm',[])])])
]

class CodeCompleter(QCompleter):
    ConcatenationRole = Qt.UserRole + 1
    def __init__(self, data, parent=None):
        super().__init__(parent)
        self.createModel(data)
        
        # Pattern to match the "last word" of an expression
        # Must start with a letter, and be followed by any number of letter,numbers, undrscores or periods(.)
        self.rePtrn = re.compile(r'\b[a-zA-Z][a-zA-Z0-9_.]*$')
        
    def splitPath(self, path):
        ''' This function gets called when the text changes, 
        and we need to select how the new text is to be used to filter options
        '''
        lastWord = (self.rePtrn.findall(path) + [''])[0] #Append empty string, to deal with empty list exception
        return lastWord.split('.')

    def pathFromIndex(self, ix):
        ''' This function is responsible for generating the "autocompletedText"
        Whatever this function returns is the final text on the widget
        '''
        currentText = self.widget().text()
        completionText = ix.data(CodeCompleter.ConcatenationRole)
        
        newText, wasFound = self.rePtrn.subn(completionText, currentText)
        if not wasFound:
            newText = currentText + completionText
            
        return newText

    def createModel(self, data):
        
        def addItems(parent, elements, t=""):
            for text, children in elements:
                item = QStandardItem(text)
                data = t + "." + text if t else text
                item.setData(data)#, CodeCompleter.ConcatenationRole)
                parent.appendRow(item)
                if children:
                    addItems(item, children, data)
                    
        model = QStandardItemModel(self)
        addItems(model, data)
        self.setModel(model)
    
class mainApp(QWidget):
    def __init__(self):
        super().__init__()
        
        self.completer = CodeCompleter(test_model_data, self)
        layout = QVBoxLayout()
        self.setLayout(layout)
        
        for i in range(5):
            entry = QLineEdit(self)
            entry.setCompleter(self.completer)
            layout.addWidget(entry)
        

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

但是,这仍然存在一些问题。特别是,我可能并不总是希望获得有关“最后一个单词”的建议,而是实际上想要获得有关“当前单词”的建议,如光标当前所在的位置。其次,我也可能更喜欢 QTextEdit 小部件,因为此后墙可能需要多行功能。

当我试图解决这两个问题时,我彻底碰壁了。看起来 QCompleter 对于 QTextEdit 的设置必须与 QLineEdit 完全不同,并且不太清楚为什么会出现这种情况。

我能做的最好的就是这样:

import sys
from PyQt5.QtWidgets import QApplication, QWidget, QTextEdit, QVBoxLayout, QCompleter, QLabel
from PyQt5 import QtGui, QtCore
from PyQt5.QtCore import Qt, QRect
from PyQt5.QtGui import QTextCursor, QStandardItemModel, QStandardItem, QPalette

test_model_data = [
    ('obj1',[ ('field1',[('subfield1',[])]),
              ('field2',[])]),                        
    ('obj2',[ ('field1',[('subfiedl1',[])]),
              ('field2',[])]),                        
    ('obj3',[]),
    ('music',[('melody',[]), 
              ('harmony',[('chords',[]),
                          ('rythm',[])])])
]



class CustomCompleter(QCompleter):
    def __init__(self, *args, **kwargs):
        QCompleter.__init__(self, *args, **kwargs)
        
        self.createModel(test_model_data)
        
    def createModel(self, data):
        
        def addItems(parent, elements, t=""):
            for text, children in elements:
                item = QStandardItem(text)
                data = t + "." + text if t else text
                item.setData(data)#, CodeCompleter.ConcatenationRole)
                parent.appendRow(item)
                if children:
                    addItems(item, children, data)
                    
        model = QStandardItemModel(self)
        model.splitPath = lambda t: print(t)
        addItems(model, data)
        self.setModel(model)
    
    def splitPaths(self, path):
        print(path)
    
    def complete(self, rect, prefix):
        self.setCompletionPrefix(prefix)
        super().complete(rect)

class CustomTextEdit(QTextEdit):
    def __init__(self, idx, completer, initial_text, *args, **kwargs):
        super().__init__(initial_text, *args, **kwargs)
        self.completer = completer
        self.idx = idx 
        
        completer.activated.connect(self.insertCompletion)
        
    def keyPressEvent(self, event):
        if event.key() == Qt.Key_Tab:  # Show suggestions when Tab is pressed
            self.showSuggestions()
        elif event.key() == Qt.Key_Return and self.completer.popup().isVisible():  # Replace suggestion when Enter is pressed
            
            self.insertCompletion()
            self.completer.popup().hide()
        else:
            super().keyPressEvent(event)

    def insertCompletion(self):
        if not self.completer.widget() is self: return
        
        popup_index = self.completer.popup().currentIndex()
        completion = self.completer.popup().model().data(popup_index)
        
        cursor = self.textCursor()
        cursor.movePosition(cursor.StartOfWord)
        cursor.movePosition(cursor.EndOfWord, cursor.KeepAnchor)
        cursor.insertText(completion)

    def currentWord(self):
        cursor = self.textCursor()
        cursor.movePosition(cursor.WordLeft)
        cursor.select(cursor.WordUnderCursor)
        word = cursor.selectedText()
        #print('Cur word: "%s"' %word)
        return word
    
    def showSuggestions(self):
        cursor_rect = self.cursorRect()
        popup_height = self.completer.popup().sizeHintForColumn(0)  # Get the height of the popup
        popup_size = self.completer.popup().sizeHint()
        popup_rect = QRect(cursor_rect.bottomRight(), popup_size)
        popup_rect.setHeight(popup_height)  # Set the height of the popup
        popup_rect.moveTop(popup_rect.top() - popup_height)  # Move the top of the popup
        
        #print( popup_rect )
        #self.completer.setWidget(None)
        self.completer.setWidget(self)
        self.completer.complete(popup_rect, self.currentWord())  # Show popup with suggestions
        
class MainWindow(QWidget):
    def __init__(self):
        super().__init__()
        self.initUI()

    def initUI(self):
        layout = QVBoxLayout()

        completer = CustomCompleter()
        
        # Create multiple text widgets and connect them to the same completer instance
        text_edit1 = CustomTextEdit(1, completer, "Hello my friend")
        text_edit2 = CustomTextEdit(2, completer, "For lunch we have fruit")
        text_edit3 = CustomTextEdit(3, completer, "And then maybe a movie")


        layout.addWidget(QLabel("Type below (press Tab for suggestions, Enter to replace):"))
        layout.addWidget(text_edit1)
        layout.addWidget(text_edit2)
        layout.addWidget(text_edit3)

        self.setLayout(layout)
        self.setWindowTitle("Multiline Textbox with Dropdown")
        self.setGeometry(100, 100, 400, 300)
        self.show()

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

它至少具有作用于“当前单词”以及多行 QEditText 小部件的功能。但是,我完全失去了“嵌套”字段的功能。在这种方法中,“splitPath”函数似乎被完全忽略,我找不到任何合适的方法来基于“分割路径”方法“过滤”我的建议(除了从头开始重写整个模型/搜索/过滤器)。

有没有什么好的方法可以将“当前对象单词”(例如“obj1.field1.su”)发送给完成者,并使用“splitPath”在“.”处将其打破,通过QStandardItemModel进行匹配字符,就像前面的例子一样?

python user-interface pyqt autocomplete
1个回答
0
投票

我对这个答案不是很满意。基本上我必须编写自己的嵌套模型,它与 QStringModel 上的包装器一起使用。我还必须修改 QCompleter 才能使用它。我觉得由于我自己的无知,我没有完全/正确地利用 PyQt5 库,而且我在 Python 上做了很多工作,我担心这会影响性能。

但它目前正在做最初问题所要求的事情,所以我将其留在这里,以防它可以帮助其他遇到同样问题的人:

import sys
import re

from PyQt5.QtWidgets import QApplication, QWidget, QTextEdit, QVBoxLayout, QCompleter, QLabel, QPushButton
from PyQt5.QtCore import Qt, QRect, QStringListModel, QPropertyAnimation

test_model_data = [
    ('obj1',[ ('field1',[('subfield1',[])]),
              ('field2',[])]),                        
    ('obj2',[ ('field1',[('subfiedl1',[])]),
              ('field2',[])]),                        
    ('obj3',[]),
    ('music',[('melody',[]), 
              ('harmony',[('chords',[]),
                          ('rythm',[])])])
]


class NestedDictCompleteModel:
    ''' This wraps a QStringModel so that it allows for recursive models, 
    supporting a syntax of "obj.field.subfield.subSubfield"
    It creates a recursive Dict structure, to search through the fields quickly
    '''
    def __init__(self):
        self.itemDict = {}
        self.stringModel = QStringListModel()
        
    def addItem(self, itemData):
        ''' Creates a new Item at this level with the text=itemData
        Returns the "empty" model of the new item, so that it can be used to fill in
        '''
        subModel = NestedDictCompleteModel()
        self.itemDict[ itemData ] = subModel
        
        n = self.stringModel.rowCount()
        self.stringModel.insertRows(n, 1)
        self.stringModel.setData( self.stringModel.index(n), itemData)
    
        return subModel
    
    def filterByWords(self, words):
        if len(words)<2: 
            return self.stringModel
        if not words[0] in self.itemDict:
            return QStringListModel() # Empty model
        
        return self.itemDict[ words[0] ].filterByWords( words[1:] )
        
class CustomCompleter(QCompleter):
    def __init__(self, *args, **kwargs):
        QCompleter.__init__(self, *args, **kwargs)
        
        self.mainModel = self.createModel(test_model_data)
        self.setModel( self.mainModel.stringModel )
    def createModel(self, data):
        
        def addItems(parent, elements, t=""):
            for text, children in elements:
                item = parent.addItem( text )
                if children:
                    addItems(item, children, data)
                    
        model = NestedDictCompleteModel()
        addItems(model, data)
        
        return model 
    
    def complete(self, rect, prefix):
        #New Model, based on recursive model searching on ['objName', 'fieldName', 'subFieldName' etc...]
        words = prefix.split('.')
        model = self.mainModel.filterByWords( words )
        
        self.setModel( model )
        self.setCompletionPrefix( words[-1] ) # Filter the current model dynamically based on last word
        
        super().complete(rect)
        
class CustomTextEdit(QTextEdit):
    def __init__(self, idx, completer, initial_text, *args, **kwargs):
        super().__init__(initial_text, *args, **kwargs)
        self.completer = completer
        self.idx = idx 
        
        self.heightInFocus = kwargs.get('heightInFocus', 5)
        self.heightOutFocus = kwargs.get('heightInFocus', 1)
        self.setHeightLines(self.heightOutFocus, anim=False)
        
        self.animDur = 200 #100miliseconds
        self.textChanged.connect(self.showSuggestions)
        
        completer.activated.connect(self.insertCompletion)
        
        self.blockEventFocus = False # Prevent
        
        # Regex to match all "words.subWords" to the left/right of the string
        self.reLeftAll = re.compile( r'.*?(?P<lw>[a-zA-Z][a-zA-Z0-9\.]*)$' )
        self.reRightAll = re.compile( r'^(?P<rw>[a-zA-Z0-9\.]*)' )
        
        # Regex to match single "subWord" to the left/right of the string
        self.reLeft = re.compile( r'.*?(?P<lw>[a-zA-Z][a-zA-Z0-9]*)$' )
        self.reRight = re.compile( r'^(?P<rw>[a-zA-Z0-9]*)' )
        
    def isolateFocusWord(self, allTerms=True):
        ''' In the current text, finds the "words.subWord" around the current cursor.
        Returns the word.subword string, as well the the indexes for start and end,
        in the whole text
        '''
        text = self.toPlainText()
        ti = self.textCursor().positionInBlock() # index of the cursor in text
        
        textLeft, textRight = text[:ti], text[ti:]
        
        if allTerms:
            ml = self.reLeftAll.match(textLeft)
            mr = self.reRightAll.match(textRight)
        else:
            ml = self.reLeft.match(textLeft)
            mr = self.reRight.match(textRight)
        
        wl = ml.group(1) if ml else ''
        wr = mr.group(1) if mr else ''
        
        return wl+wr, ti-len(wl), ti+len(wr)
        
    def keyPressEvent(self, event):
        if event.key() == Qt.Key_Return and self.completer.popup().isVisible():  # Replace suggestion when Enter is pressed
            self.insertCompletion()
            self.completer.popup().hide()
        else:
            super().keyPressEvent(event)

    def insertCompletion(self):
        ''' Finds the current selected suggestion and replaces it in the text,
        by replacing the current "subword"
        '''
        if not self.completer.widget() is self: return
        
        popup_index = self.completer.popup().currentIndex()
        completion = self.completer.popup().model().data(popup_index)
        
        w, si, ei = self.isolateFocusWord(allTerms=False)
        
        cursor = self.textCursor()
        cursor.setPosition(si, cursor.MoveAnchor)#cursor.StartOfWord)
        cursor.setPosition(ei, cursor.KeepAnchor) #cursor.EndOfWord, cursor.KeepAnchor)
        cursor.insertText(completion)
        
    def showSuggestions(self):
        # Find the "word.subword" at the cursor
        focusWord, li,ri = self.isolateFocusWord()
        
        self.blockEventFocus = True
        
        cursor_rect = self.cursorRect()
        
        popup_height = self.completer.popup().sizeHintForColumn(0)  # Get the height of the popup
        popup_size = self.completer.popup().sizeHint()
        popup_rect = QRect(cursor_rect.bottomRight(), popup_size)
        popup_rect.setHeight(popup_height)  # Set the height of the popup
        newTop = popup_rect.top() - popup_height
        popup_rect.moveTop(newTop)  # Move the top of the popup
        
        self.completer.setWidget(self)
        self.completer.complete(popup_rect, focusWord )  # Show popup with suggestions
        self.blockEventFocus = False
    
    def setHeightLines(self, nlines, anim=True):
        line_height = self.fontMetrics().lineSpacing()
        height = (nlines+1) * line_height
        
        if anim:
            animation = QPropertyAnimation(self, b"maximumHeight")
            animation.setDuration(self.animDur)  # Duration of the animation in milliseconds
            animation.setStartValue(self.height())
            animation.setEndValue(height)
    
            # Start the animation
            animation.start()
            
            animation2 = QPropertyAnimation(self, b"minimumHeight")
            animation2.setDuration(self.animDur)  # Duration of the animation in milliseconds
            animation2.setStartValue(self.height())
            animation2.setEndValue(height)
    
            # Start the animation
            animation2.start()
            
            self.animations = animation, animation2
        else:
            self.setFixedHeight(height)
            
        
    def focusInEvent(self, event):
        if self.blockEventFocus: return
        self.setHeightLines(self.heightInFocus)
        super().focusInEvent(event)
    def focusOutEvent(self, event):
        if self.blockEventFocus: return
        self.setHeightLines(self.heightOutFocus)
        super().focusOutEvent(event)
        
class MainWindow(QWidget):
    def __init__(self):
        super().__init__()

        layout = QVBoxLayout()
        completer = CustomCompleter()
        
        btn = QPushButton('Delete obj1')
        
        # Create multiple text widgets and connect them to the same completer instance
        text_edit1 = CustomTextEdit(1, completer, "Here we have obj1.field1  and also obj2")
        text_edit2 = CustomTextEdit(2, completer, "For lunch we have fruit")
        text_edit3 = CustomTextEdit(3, completer, "music is wonderful and obj")


        layout.addWidget(QLabel("Start Typing to see object and field suggestions"))
        layout.addWidget(btn)
        layout.addWidget(text_edit1)
        layout.addWidget(text_edit2)
        layout.addWidget(text_edit3)

        self.setLayout(layout)
        self.setWindowTitle("Multiline Textbox with Dropdown")
        #self.setGeometry(100, 100, 400, 300)
        self.show()

if __name__ == "__main__":
    app = QApplication(sys.argv)
    window = MainWindow()
    sys.exit(app.exec_())
© www.soinside.com 2019 - 2024. All rights reserved.