如何正确格式化QCompleter弹出列表的列表项?

2024-09-22 16:43:29 发布

您现在位置:Python中文网/ 问答频道 /正文

我想研究如何制作一个小的用户界面,在这个界面中,用户可以根据给定的数据源(在这里列出)键入一些字母并获得一些建议,从而使搜索更容易。为此,我使用Qt的QCompleter类。在

在匹配元素中,键入的字母应该用HTML突出显示,就像下面代码中的示例:Au<b>st</b>ria。 最后,我将一些SO答案(参见How to make item view render rich (html) text in Qt)和教程合并到一个独立的小模块中:

from PySide import QtCore, QtGui

class HTMLDelegate(QtGui.QStyledItemDelegate):
    """ From: https://stackoverflow.com/a/5443112/1504082 """

    def paint(self, painter, option, index):
        options = QtGui.QStyleOptionViewItemV4(option)
        self.initStyleOption(options, index)
        if options.widget is None:
            style = QtGui.QApplication.style()
        else:
            style = options.widget.style()

        doc = QtGui.QTextDocument()
        doc.setHtml(options.text)
        doc.setTextWidth(option.rect.width())

        options.text = ""
        style.drawControl(QtGui.QStyle.CE_ItemViewItem, options, painter)

        ctx = QtGui.QAbstractTextDocumentLayout.PaintContext()

        # Highlighting text if item is selected
        # if options.state & QtGui.QStyle.State_Selected:
        #     ctx.palette.setColor(QtGui.QPalette.Text,
        #                          options.palette.color(QtGui.QPalette.Active,
        #                                                QtGui.QPalette.HighlightedText))

        textRect = style.subElementRect(QtGui.QStyle.SE_ItemViewItemText,
                                        options)
        painter.save()
        painter.translate(textRect.topLeft())
        painter.setClipRect(textRect.translated(-textRect.topLeft()))
        doc.documentLayout().draw(painter, ctx)
        painter.restore()

    def sizeHint(self, option, index):
        options = QtGui.QStyleOptionViewItemV4(option)
        self.initStyleOption(options, index)
        doc = QtGui.QTextDocument()
        doc.setHtml(options.text)
        doc.setTextWidth(options.rect.width())
        return QtCore.QSize(doc.size().width(), doc.size().height())


class CustomQCompleter(QtGui.QCompleter):
    """ Implement "contains" filter mode as the filter mode "contains" is not
    available in Qt < 5.2
    From: https://stackoverflow.com/a/7767999/1504082 """

    def __init__(self, parent=None):
        super(CustomQCompleter, self).__init__(parent)
        self.local_completion_prefix = ""
        self.source_model = None
        self.delegate = HTMLDelegate()

    def setModel(self, model):
        self.source_model = model
        super(CustomQCompleter, self).setModel(self.source_model)

    def updateModel(self):
        local_completion_prefix = self.local_completion_prefix

        # see: http://doc.qt.io/qt-4.8/model-view-programming.html#proxy-models
        class InnerProxyModel(QtGui.QSortFilterProxyModel):
            def filterAcceptsRow(self, sourceRow, sourceParent):
                # model index mapping by row, 1d model => column is always 0
                index = self.sourceModel().index(sourceRow, 0, sourceParent)
                source_data = self.sourceModel().data(index, QtCore.Qt.DisplayRole)
                # performs case insensitive matching
                # return True if item shall stay in th returned filtered data
                # return False to reject an item
                return local_completion_prefix.lower() in source_data.lower()

        proxy_model = InnerProxyModel()
        proxy_model.setSourceModel(self.source_model)
        super(CustomQCompleter, self).setModel(proxy_model)
        # @todo: Why to be set here again?
        self.popup().setItemDelegate(self.delegate)

    def splitPath(self, path):
        self.local_completion_prefix = path
        self.updateModel()
        return ""


class AutoCompleteEdit(QtGui.QLineEdit):
    """ Basically from:
    http://doc.qt.io/qt-5/qtwidgets-tools-customcompleter-example.html
    """

    def __init__(self, list_data, separator=' ', addSpaceAfterCompleting=True):
        super(AutoCompleteEdit, self).__init__()
        # settings
        self._separator = separator
        self._addSpaceAfterCompleting = addSpaceAfterCompleting
        # completer
        self._completer = CustomQCompleter(self)
        self._completer.setCaseSensitivity(QtCore.Qt.CaseInsensitive)
        self._completer.setCompletionMode(QtGui.QCompleter.PopupCompletion)

        self.model = QtGui.QStringListModel(list_data)
        self._completer.setModel(self.model)

        # connect the completer to the line edit
        self._completer.setWidget(self)
        # trigger insertion of the selected completion when its activated
        self.connect(self._completer,
                     QtCore.SIGNAL('activated(QString)'),
                     self._insertCompletion)

        self._ignored_keys = [QtCore.Qt.Key_Enter,
                              QtCore.Qt.Key_Return,
                              QtCore.Qt.Key_Escape,
                              QtCore.Qt.Key_Tab]

    def _insertCompletion(self, completion):
        """
        This is the event handler for the QCompleter.activated(QString) signal,
        it is called when the user selects an item in the completer popup.
        It will remove the already typed string with the one of the completion.
        """
        stripped_text = self.text()[:-len(self._completer.completionPrefix())]

        extra_text = completion  # [-extra:]
        if self._addSpaceAfterCompleting:
            extra_text += ' '
        self.setText(stripped_text + extra_text)

    def textUnderCursor(self):
        text = self.text()
        textUnderCursor = ''
        i = self.cursorPosition() - 1
        while i >= 0 and text[i] != self._separator:
            textUnderCursor = text[i] + textUnderCursor
            i -= 1
        return textUnderCursor

    def keyPressEvent(self, event):
        if self._completer.popup().isVisible():
            if event.key() in self._ignored_keys:
                event.ignore()
                return
        super(AutoCompleteEdit, self).keyPressEvent(event)
        completionPrefix = self.textUnderCursor()
        if completionPrefix != self._completer.completionPrefix():
            self._updateCompleterPopupItems(completionPrefix)
        if len(event.text()) > 0 and len(completionPrefix) > 0:
            self._completer.complete()
        if len(completionPrefix) == 0:
            self._completer.popup().hide()

    def _updateCompleterPopupItems(self, completionPrefix):
        """
        Filters the completer's popup items to only show items
        with the given prefix.
        """
        self._completer.setCompletionPrefix(completionPrefix)
        # self._completer.popup().setCurrentIndex(
        #     self._completer.completionModel().index(0, 0))


if __name__ == '__main__':
    def demo():
        import sys
        app = QtGui.QApplication(sys.argv)
        values = ['Germany',
                  'Au<b>st</b>ria',
                  'Switzerland',
                  'Hungary',
                  'The United Kingdom of Great Britain and Northern Ireland']
        editor = AutoCompleteEdit(values)
        window = QtGui.QWidget()
        hbox = QtGui.QHBoxLayout()
        hbox.addWidget(editor)
        window.setLayout(hbox)
        window.show()

        sys.exit(app.exec_())

    demo()

我的问题是用户Timo在答案https://stackoverflow.com/a/5443112/1504082中的建议:

After line: 'doc.setHtml(options.text)', you need to set also doc.setTextWidth(option.rect.width()), otherwise the delegate wont render longer content correctly in respect to target drawing area. For example does not wrap words in QListView.

所以我这样做是为了避免在完成者弹出窗口中裁剪长文本。但我得到以下输出: Where does this vertical margin come from?

这个额外的垂直边距从何而来?在

我对此进行了一点研究,发现HTMLDelegatesizeHint方法有时会用一个options参数来调用,该参数包含一个具有(0, 0, 0, 0)属性的矩形。在调用doc.setTextWidth(options.rect.width())之后,显示行为最终改变。但我最终无法找出谁用这个参数来调用它,以及如何正确地修复这个问题。在

有人能解释一下这是从哪里来的吗?我怎么才能解决这个问题?在


Tags: thetextselfindexdocmodelifdef
1条回答
网友
1楼 · 发布于 2024-09-22 16:43:29

最后,我找到了另一种实现它的方法,使用了https://stackoverflow.com/a/8036666/1504082。对于我来说,如果没有这些我还不懂的定制画作,那就更令人期待了:)

from PySide import QtCore, QtGui


class TaskDelegate(QtGui.QItemDelegate):
    # based on https://stackoverflow.com/a/8036666/1504082
    # https://doc.qt.io/archives/qt-4.7/qitemdelegate.html#drawDisplay
    # https://doc.qt.io/archives/qt-4.7/qwidget.html#render
    margin_x = 5
    margin_y = 3

    def drawDisplay(self, painter, option, rect, text):
        label = self.make_label(option, text)
        # calculate render anchor point
        point = rect.topLeft()
        point.setX(point.x() + self.margin_x)
        point.setY(point.y() + self.margin_y)

        label.render(painter, point, renderFlags=QtGui.QWidget.DrawChildren)

    def sizeHint(self, option, index):
        # get text using model and index
        text = index.model().data(index)
        label = self.make_label(option, text)
        return QtCore.QSize(label.width(), label.height() + self.margin_y)

    def make_label(self, option, text):
        label = QtGui.QLabel(text)

        if option.state & QtGui.QStyle.State_Selected:
            p = option.palette
            p.setColor(QtGui.QPalette.WindowText,
                       p.color(QtGui.QPalette.Active,
                               QtGui.QPalette.HighlightedText)
                       )

            label.setPalette(p)

        label.setStyleSheet("border: 1px dotted black")

        # adjust width according to widget's target width
        label.setMinimumWidth(self.target_width - (2 * self.margin_x))
        label.setMaximumWidth(self.target_width - self.margin_x)
        label.setWordWrap(True)
        label.adjustSize()
        return label


class CustomQCompleter(QtGui.QCompleter):
    """ Implement "contains" filter mode as the filter mode "contains" is not
    available in Qt < 5.2
    From: https://stackoverflow.com/a/7767999/1504082 """

    def __init__(self, parent=None):
        super(CustomQCompleter, self).__init__(parent)
        self.local_completion_prefix = ""
        self.source_model = None
        self.delegate = TaskDelegate()
        # widget not set yet
        # self.delegate.target_width = self.widget().width()

    def setModel(self, model):
        self.source_model = model
        super(CustomQCompleter, self).setModel(self.source_model)

    def updateModel(self):
        local_completion_prefix = self.local_completion_prefix

        # see: http://doc.qt.io/qt-4.8/model-view-programming.html#proxy-models
        class InnerProxyModel(QtGui.QSortFilterProxyModel):
            def filterAcceptsRow(self, sourceRow, sourceParent):
                # model index mapping by row, 1d model => column is always 0
                index = self.sourceModel().index(sourceRow, 0, sourceParent)
                source_data = self.sourceModel().data(index, QtCore.Qt.DisplayRole)
                # performs case insensitive matching
                # return True if item shall stay in th returned filtered data
                # return False to reject an item
                return local_completion_prefix.lower() in source_data.lower()

        proxy_model = InnerProxyModel()
        proxy_model.setSourceModel(self.source_model)
        super(CustomQCompleter, self).setModel(proxy_model)
        # @todo: Why to be set here again?
        # -> rescale popup list items to widget width
        self.delegate.target_width = self.widget().width()
        self.popup().setItemDelegate(self.delegate)

    def splitPath(self, path):
        self.local_completion_prefix = path
        self.updateModel()
        return ""


class AutoCompleteEdit(QtGui.QLineEdit):
    """ Basically from:
    http://doc.qt.io/qt-5/qtwidgets-tools-customcompleter-example.html
    """

    def __init__(self, list_data, separator=' ', addSpaceAfterCompleting=True):
        super(AutoCompleteEdit, self).__init__()
        # settings
        self._separator = separator
        self._addSpaceAfterCompleting = addSpaceAfterCompleting
        # completer
        self._completer = CustomQCompleter(self)
        self._completer.setCaseSensitivity(QtCore.Qt.CaseInsensitive)
        self._completer.setCompletionMode(QtGui.QCompleter.PopupCompletion)

        self.model = QtGui.QStringListModel(list_data)
        self._completer.setModel(self.model)

        # connect the completer to the line edit
        self._completer.setWidget(self)
        # trigger insertion of the selected completion when its activated
        self.connect(self._completer,
                     QtCore.SIGNAL('activated(QString)'),
                     self._insertCompletion)

        self._ignored_keys = [QtCore.Qt.Key_Enter,
                              QtCore.Qt.Key_Return,
                              QtCore.Qt.Key_Escape,
                              QtCore.Qt.Key_Tab]

    def _insertCompletion(self, completion):
        """
        This is the event handler for the QCompleter.activated(QString) signal,
        it is called when the user selects an item in the completer popup.
        It will remove the already typed string with the one of the completion.
        """
        stripped_text = self.text()[:-len(self._completer.completionPrefix())]

        extra_text = completion  # [-extra:]
        if self._addSpaceAfterCompleting:
            extra_text += ' '
        self.setText(stripped_text + extra_text)

    def textUnderCursor(self):
        text = self.text()
        textUnderCursor = ''
        i = self.cursorPosition() - 1
        while i >= 0 and text[i] != self._separator:
            textUnderCursor = text[i] + textUnderCursor
            i -= 1
        return textUnderCursor

    def keyPressEvent(self, event):
        if self._completer.popup().isVisible():
            if event.key() in self._ignored_keys:
                event.ignore()
                return
        super(AutoCompleteEdit, self).keyPressEvent(event)
        completionPrefix = self.textUnderCursor()
        if completionPrefix != self._completer.completionPrefix():
            self._updateCompleterPopupItems(completionPrefix)
        if len(event.text()) > 0 and len(completionPrefix) > 0:
            self._completer.complete()
        if len(completionPrefix) == 0:
            self._completer.popup().hide()

    def _updateCompleterPopupItems(self, completionPrefix):
        """
        Filters the completer's popup items to only show items
        with the given prefix.
        """
        self._completer.setCompletionPrefix(completionPrefix)
        # self._completer.popup().setCurrentIndex(
        #     self._completer.completionModel().index(0, 0))


if __name__ == '__main__':
    def demo():
        import sys
        app = QtGui.QApplication(sys.argv)
        values = ['Germany',
                  'Au<b>st</b>ria',
                  'Switzerland',
                  'Hungary',
                  'The United Kingdom of Great Britain and Northern Ireland',
                  'USA']
        editor = AutoCompleteEdit(values)
        window = QtGui.QWidget()
        hbox = QtGui.QHBoxLayout()
        hbox.addWidget(editor)
        window.setLayout(hbox)
        window.show()

        sys.exit(app.exec_())

    demo()

相关问题 更多 >