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

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

我想研究如何创建一个小用户界面,用户可以在其中键入一些字母,并根据给定的数据源(此处列表)获取一些建议,这使搜索更容易。为此我正在使用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()

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

在行:'doc.setHtml(options.text)'之后,您还需要设置doc.setTextWidth(option.rect.width()),否则委托将不会相对于目标绘图区域正确呈现更长的内容。例如,不在QListView中包装单词。

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

这额外的垂直边距来自哪里?

我调查了一下,我看到sizeHintHTMLDelegate方法有时用options参数调用,该参数包含一个带有属性(0, 0, 0, 0)的矩形。在doc.setTextWidth(options.rect.width())调用之后,显示行为最终会发生变化。但我终于找不到谁用这个参数调用它以及我如何正确解决这个问题。

有人可以解释这来自何处以及如何正确解决这个问题?

python qt pyside qstyleditemdelegate
1个回答
1
投票

最后,我发现了使用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()
© www.soinside.com 2019 - 2024. All rights reserved.