单列 QTreeview 搜索过滤器

Single column QTreeview search filter

我有两个问题:

  1. 我想知道这是否是在单列树视图上执行 search/filter 的正确方法。我觉得我的很多 copying/pasting 可能包含不必要的东西。 QSortFilterProxyModel的子类中的代码和search_text_changed方法中的代码都需要吗?我觉得不需要正则表达式,因为我将过滤器代理设置为忽略区分大小写。

  2. 我怎样才能做到这一点,以便当用户双击树视图项目时,信号会发出一个字符串列表,其中包含所单击项目的字符串及其递归的所有祖先?例如,如果我双击 "Birds",它会 return ['Birds','Animals'];如果我双击 "Animals",它只会 return ['Animals'].

import os, sys
from PySide import QtCore, QtGui

tags = {
    "Animals": [
        "Birds",
        "Various"
    ],
    "Brick": [
        "Blocks",
        "Special"
    ],
    "Manmade": [
        "Air Conditioners",
        "Audio Equipment"
    ],
    "Food": [
        "Fruit",
        "Grains and Seeds"
    ]
}

class SearchProxyModel(QtGui.QSortFilterProxyModel):
    def __init__(self, parent=None):
        super(SearchProxyModel, self).__init__(parent)
        self.text = ''

    # Recursive search
    def _accept_index(self, idx):
        if idx.isValid():
            text = idx.data(role=QtCore.Qt.DisplayRole).lower()
            condition = text.find(self.text) >= 0

            if condition:
                return True
            for childnum in range(idx.model().rowCount(parent=idx)):
                if self._accept_index(idx.model().index(childnum, 0, parent=idx)):
                    return True
        return False

    def filterAcceptsRow(self, sourceRow, sourceParent):
        # Only first column in model for search
        idx = self.sourceModel().index(sourceRow, 0, sourceParent)
        return self._accept_index(idx)

    def lessThan(self, left, right):
        leftData = self.sourceModel().data(left)
        rightData = self.sourceModel().data(right)
        return leftData < rightData


class TagsBrowserWidget(QtGui.QWidget):

    clickedTag = QtCore.Signal(list)

    def __init__(self, parent=None):
        super(TagsBrowserWidget, self).__init__(parent)
        self.resize(300,500)

        # controls
        self.ui_search = QtGui.QLineEdit()
        self.ui_search.setPlaceholderText('Search...')

        self.tags_model = SearchProxyModel()
        self.tags_model.setSourceModel(QtGui.QStandardItemModel())
        self.tags_model.setDynamicSortFilter(True)
        self.tags_model.setFilterCaseSensitivity(QtCore.Qt.CaseInsensitive)

        self.ui_tags = QtGui.QTreeView()
        self.ui_tags.setSortingEnabled(True)
        self.ui_tags.sortByColumn(0, QtCore.Qt.AscendingOrder)
        self.ui_tags.setEditTriggers(QtGui.QAbstractItemView.NoEditTriggers)
        self.ui_tags.setHeaderHidden(True)
        self.ui_tags.setRootIsDecorated(True)
        self.ui_tags.setUniformRowHeights(True)
        self.ui_tags.setModel(self.tags_model)

        # layout
        main_layout = QtGui.QVBoxLayout()
        main_layout.addWidget(self.ui_search)
        main_layout.addWidget(self.ui_tags)
        self.setLayout(main_layout)

        # signals
        self.ui_tags.doubleClicked.connect(self.tag_double_clicked)
        self.ui_search.textChanged.connect(self.search_text_changed)

        # init
        self.create_model()

    def create_model(self):
        model = self.ui_tags.model().sourceModel()
        self.populate_tree(tags, model.invisibleRootItem())
        self.ui_tags.sortByColumn(0, QtCore.Qt.AscendingOrder)


    def populate_tree(self, children, parent):
        for child in sorted(children):
            node = QtGui.QStandardItem(child)
            parent.appendRow(node)

            if isinstance(children, dict):
                self.populate_tree(children[child], node)


    def tag_double_clicked(self, item):
        text = item.data(role=QtCore.Qt.DisplayRole)
        print [text]
        self.clickedTag.emit([text])


    def search_text_changed(self, text=None):
        regExp = QtCore.QRegExp(self.ui_search.text(), QtCore.Qt.CaseInsensitive, QtCore.QRegExp.FixedString)

        self.tags_model.text = self.ui_search.text().lower()
        self.tags_model.setFilterRegExp(regExp)

        if len(self.ui_search.text()) >= 1 and self.tags_model.rowCount() > 0:
            self.ui_tags.expandAll()
        else:
            self.ui_tags.collapseAll()


def main():
    app = QtGui.QApplication(sys.argv)
    ex = TagsBrowserWidget()
    ex.show()
    sys.exit(app.exec_())


if __name__ == '__main__':
    main()

完全没有必要设置过滤器代理的大小写敏感度,因为您正在通过覆盖 filterAcceptsRow 来绕过内置过滤.即使您没有这样做,setFilterRegExp 也会忽略当前的区分大小写设置。

我会将过滤器代理简化为:

class SearchProxyModel(QtGui.QSortFilterProxyModel):

    def setFilterRegExp(self, pattern):
        if isinstance(pattern, str):
            pattern = QtCore.QRegExp(
                pattern, QtCore.Qt.CaseInsensitive,
                QtCore.QRegExp.FixedString)
        super(SearchProxyModel, self).setFilterRegExp(pattern)

    def _accept_index(self, idx):
        if idx.isValid():
            text = idx.data(QtCore.Qt.DisplayRole)
            if self.filterRegExp().indexIn(text) >= 0:
                return True
            for row in range(idx.model().rowCount(idx)):
                if self._accept_index(idx.model().index(row, 0, idx)):
                    return True
        return False

    def filterAcceptsRow(self, sourceRow, sourceParent):
        idx = self.sourceModel().index(sourceRow, 0, sourceParent)
        return self._accept_index(idx)

并将搜索方法更改为:

def search_text_changed(self, text=None):
    self.tags_model.setFilterRegExp(self.ui_search.text())

    if len(self.ui_search.text()) >= 1 and self.tags_model.rowCount() > 0:
        self.ui_tags.expandAll()
    else:
        self.ui_tags.collapseAll()

现在 SearchProxyModel 独自负责决定如何通过其 setFilterRegExp 方法执行搜索。透明处理区分大小写,因此无需对输入进行预处理。

获取后代列表的方法,可以这样写:

def tag_double_clicked(self, idx):
    text = []
    while idx.isValid():
        text.append(idx.data(QtCore.Qt.DisplayRole))
        idx = idx.parent()
    text.reverse()
    self.clickedTag.emit(text)