如何正确格式化 QCompleter 弹出列表的列表项? [英] How to format the list items of QCompleter's popup list properly?

查看:69
本文介绍了如何正确格式化 QCompleter 弹出列表的列表项?的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

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

在匹配元素中,键入的字母应使用 HTML 突出显示,如下面的代码示例所示:Austria.最后我合并了一些 SO 答案(参见

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

我对此进行了一些调查,发现 HTMLDelegatesizeHint 方法有时会使用 options 参数调用,该参数包含一个矩形属性 (0, 0, 0, 0).并且在调用doc.setTextWidth(options.rect.width())后显示行为最终发生了变化.但我最终无法找出谁使用此参数调用它以及如何正确解决此问题.

有人可以解释一下这是从哪里来的,我该如何正确地解决这个问题?

解决方案

最后我找到了另一种使用 https 的想法来实现它的方法://stackoverflow.com/a/8036666/1504082.如果没有我还不明白的所有这些自定义绘图的东西,它对我来说更重要:)

from PySide import QtCore, QtGui类 TaskDelegate(QtGui.QItemDelegate):# 基于 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边距_x = 5边距_y = 3def drawDisplay(自我,画家,选项,矩形,文本):label = self.make_label(option, text)# 计算渲染锚点点 = rect.topLeft()point.setX(point.x() + self.margin_x)point.setY(point.y() + self.margin_y)label.render(画家,点,renderFlags=QtGui.QWidget.DrawChildren)def sizeHint(self, option, index):# 使用模型和索引获取文本文本 = index.model().data(index)label = self.make_label(option, text)返回 QtCore.QSize(label.width(), label.height() + self.margin_y)def make_label(self, option, text):标签 = QtGui.QLabel(文本)if option.state &QtGui.QStyle.State_Selected:p = option.palettep.setColor(QtGui.QPalette.WindowText,p.color(QtGui.QPalette.Active,QtGui.QPalette.HighlightedText))label.setPalette(p)label.setStyleSheet("border: 1px dotted black")# 根据widget的目标宽度调整宽度label.setMinimumWidth(self.target_width - (2 * self.margin_x))label.setMaximumWidth(self.target_width - self.margin_x)label.setWordWrap(True)label.adjustSize()退货标签类 CustomQCompleter(QtGui.QCompleter):""" 实施 "contains" 过滤模式,因为过滤模式 "contains" 不是在 Qt 中可用5.2来自:https://stackoverflow.com/a/7767999/1504082"""def __init__(self, parent=None):super(CustomQCompleter, self).__init__(parent)self.local_completion_prefix = ""self.source_model = 无self.delegate = TaskDelegate()# 小部件尚未设置# self.delegate.target_width = self.widget().width()def setModel(自我,模型):self.source_model = 模型超级(CustomQCompleter, self).setModel(self.source_model)def updateModel(self):local_completion_prefix = self.local_completion_prefix# 见:http://doc.qt.io/qt-4.8/model-view-programming.html#proxy-models类 InnerProxyModel(QtGui.QSortFilterProxyModel):def filterAcceptsRow(self, sourceRow, sourceParent):# 按行映射模型索引,1d 模型 =>列始终为 0index = self.sourceModel().index(sourceRow, 0, sourceParent)source_data = self.sourceModel().data(index, QtCore.Qt.DisplayRole)# 执行不区分大小写的匹配# 如果项目应保留在返回的过滤数据中,则返回 True# return False 拒绝一个项目在 source_data.lower() 中返回 local_completion_prefix.lower()proxy_model = InnerProxyModel()proxy_model.setSourceModel(self.source_model)super(CustomQCompleter, self).setModel(proxy_model)# @todo: 为什么又要设置在这里?# ->将弹出列表项重新缩放为小部件宽度self.delegate.target_width = self.widget().width()self.popup().setItemDelegate(self.delegate)def splitPath(self, path):self.local_completion_prefix = 路径self.updateModel()返回 ""类 AutoCompleteEdit(QtGui.QLineEdit):""" 主要来自:http://doc.qt.io/qt-5/qtwidgets-tools-customcompleter-example.html"""def __init__(self, list_data, separator=' ', addSpaceAfterCompleting=True):super(AutoCompleteEdit, self).__init__()# 设置self._separator = 分隔符self._addSpaceAfterCompleting = addSpaceAfterCompleting# 完成者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)# 将完成者连接到行编辑self._completer.setWidget(self)# 在激活时触发所选完成的插入self.connect(self._completer,QtCore.SIGNAL('激活(QString)'),self._insertCompletion)self._ignored_keys = [QtCore.Qt.Key_Enter,QtCore.Qt.Key_Return,QtCore.Qt.Key_Escape,QtCore.Qt.Key_Tab]def _insertCompletion(自我,完成):"""这是 QCompleter.activated(QString) 信号的事件处理程序,当用户在完成器弹出窗口中选择一个项目时调用它.它将使用完成之一删除已经输入的字符串."""stripped_text = self.text()[:-len(self._completer.completionPrefix())]extra_text = 完成 # [-extra:]如果 self._addSpaceAfterCompleting:extra_text += ' 'self.setText(stripped_text + extra_text)def textUnderCursor(self):文本 = self.text()textUnderCursor = ''i = self.cursorPosition() - 1而 i >= 0 和 text[i] != self._separator:textUnderCursor = text[i] + textUnderCursor我 -= 1返回 textUnderCursordef keyPressEvent(self, event):如果 self._completer.popup().isVisible():如果 event.key() 在 self._ignored_keys 中:事件.忽略()返回super(AutoCompleteEdit, self).keyPressEvent(event)completionPrefix = self.textUnderCursor()如果 completionPrefix != self._completer.completionPrefix():self._updateCompleterPopupItems(completionPrefix)如果 len(event.text()) >0 和 len(completionPrefix) >0:self._completer.complete()如果 len(completionPrefix) == 0:self._completer.popup().hide()def _updateCompleterPopupItems(self, completionPrefix):"""过滤完成者的弹出项目以仅显示项目带有给定的前缀."""self._completer.setCompletionPrefix(completionPrefix)# self._completer.popup().setCurrentIndex(# self._completer.completionModel().index(0, 0))如果 __name__ == '__main__':定义演示():导入系统app = QtGui.QApplication(sys.argv)值 = ['德国','Au b st/b ria','瑞士','匈牙利','大不列颠及北爱尔兰联合王国','美国']编辑器 = 自动完成编辑(值)窗口 = QtGui.QWidget()hbox = QtGui.QHBoxLayout()hbox.addWidget(编辑器)window.setLayout(hbox)window.show()sys.exit(app.exec_())演示()

I want to investigate how to make a small user interface in which a user can type some letters and gets some suggestions based on a given data source (list here) which makes searches easier. For this purpose i am using Qt's QCompleter class.

In the matching elements the typed letters shall be highlighted with HTML like the example in the code below: Au<b>st</b>ria. Finally i merged some SO answers (see How to make item view render rich (html) text in Qt) and tutorials to a small standalone module:

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

My problem is the suggestion of user Timo in the answer 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.

So i did this to avoid cropping of long text in the completer's popup. But i get the following output:

Where does this additional vertical margin come from?

I investigated this a bit and i see that the sizeHint method of HTMLDelegate is sometimes called with an options parameter which contains a rectangle with attributes (0, 0, 0, 0). And the display behaviour finally changes after the call of doc.setTextWidth(options.rect.width()). But i couldnt finally find out who calls it with this parameter and how i could properly fix this.

Can somebody explain where this comes from and how i can fix this porperly?

解决方案

Finally i found another way to realize it using the idea of https://stackoverflow.com/a/8036666/1504082. Its much more forward for me without all this custom drawing things which i dont understand yet :)

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

这篇关于如何正确格式化 QCompleter 弹出列表的列表项?的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆