我需要一個QTabWidget每個選項卡都包含一個QTreeView. 每個都QTreeView顯示一個更大模型的“分支”。用戶QLineEdit可以根據他們輸入的內容實時過濾視圖。我復制了下面的 GUI 問題。
問題是我使用QTreeView::setRootIndex它來顯示主模型的單個內部分支。該QLineEdit“filterer”與實作
QSortFilterProxyModel。每當用戶鍵入時,我都會呼叫
QSortFilterProxyModel::invalidateFilter從頭開始重新過濾。這兩種方法QTreeView::setRootIndex和QSortFilterProxyModel::invalidateFilter不能很好地混合在一起。
當QSortFilterProxyModel::invalidateFilter被呼叫時,我之前設定的索引QTreeView::setRootIndex現在無效。在QTreeView隨后顯示了整個樹,而不是“分支”,我一直在使用之前選擇的setRootIndex。將QTreeView有效地“忘記”根指數我已成立。
我復制了下面的問題
import functools
from Qt import QtCore, QtWidgets
_NO_ROW = -1
_NUMBER_OF_BRANCHES = 3
class _MatchProxy(QtCore.QSortFilterProxyModel):
"""Use a callable function to determine if an index should be hidden from view or not."""
def __init__(self, matcher, parent=None):
super(_MatchProxy, self).__init__(parent=parent)
self._matcher = matcher
def filterAcceptsRow(self, source_row, source_index):
return self._matcher(source_index)
class _MultiTree(QtWidgets.QWidget):
"""A widget which makes smaller, individual QTreeViews for each section of an given index."""
def __init__(self, source_index, parent=None):
super(_MultiTree, self).__init__(parent=parent)
self.setLayout(QtWidgets.QVBoxLayout())
model = source_index.model()
self._views = []
for index in range(model.rowCount(source_index)):
section = model.index(index, 0, parent=source_index)
label = model.data(section, QtCore.Qt.DisplayRole)
view = QtWidgets.QTreeView()
view.setModel(model)
view.setRootIndex(section)
self.layout().addWidget(QtWidgets.QLabel(label))
self.layout().addWidget(view)
self._views.append(view)
def iter_models(self):
for view in self._views:
yield view.model()
def iter_views(self):
for view in self._views:
yield view
class Node(object):
"""A generic name children parent graph node class."""
def __init__(self, name, parent=None):
super(Node, self).__init__()
self._name = name
self._children = []
self._parent = parent
if self._parent:
self._parent.add_child(self)
def add_child(self, node):
node._parent = self
self._children.append(node)
def get_child(self, row):
return self._children[row]
def get_children(self):
return list(self._children)
def get_label(self):
return self._name
def get_parent(self):
return self._parent
def get_row(self):
parent = self.get_parent()
if not parent:
return _NO_ROW
return parent.get_children().index(self)
def __repr__(self):
return "{self.__class__.__name__}({self._name!r}, parent={self._parent!r})".format(
self=self
)
class Branch(Node):
"""Syntax sugar for debugging. This class isn't "necessary" for the reproduction."""
pass
class Model(QtCore.QAbstractItemModel):
"""The basic Qt model which contains the entire tree, including each branch."""
def __init__(self, roots, parent=None):
super(Model, self).__init__(parent=parent)
self._roots = roots
def _get_node(self, index):
return index.internalPointer()
def columnCount(self, _):
return 1
def data(self, index, role=QtCore.Qt.DisplayRole):
if role != QtCore.Qt.DisplayRole:
return None
node = self._get_node(index)
return node.get_label()
def index(self, row, column, parent=QtCore.QModelIndex()):
if not parent.isValid():
return self.createIndex(row, 0, self._roots[row])
parent_node = self._get_node(parent)
child_node = parent_node.get_child(row)
return self.createIndex(row, column, child_node)
def parent(self, index):
if not index.isValid():
return QtCore.QModelIndex()
node = self._get_node(index)
parent = node.get_parent()
if not parent:
return QtCore.QModelIndex()
return self.createIndex(node.get_row(), 0, parent)
def rowCount(self, index):
if not index.isValid():
return len(self._roots)
node = self._get_node(index)
return len(node.get_children())
class Widget(QtWidgets.QWidget):
"""The main widget / window which has the filterer QTreeViews."""
def __init__(self, model, parent=None):
super(Widget, self).__init__(parent=parent)
self.setLayout(QtWidgets.QVBoxLayout())
self._filterer = QtWidgets.QLineEdit()
self._tabs = QtWidgets.QTabWidget()
self._set_model(model)
self.layout().addWidget(self._filterer)
self.layout().addWidget(self._tabs)
self._filterer.textChanged.connect(self._update_current_view)
def _replace_model_with_filterer_proxy(self, view):
def _match(line_edit, index):
if not index.isValid():
return True # Show everything, don't filter anything
current = line_edit.text().strip()
if not current:
return True # Show everything, don't filter anything.
return current in index.data(QtCore.Qt.DisplayRole)
model = view.model()
current_root = view.rootIndex()
proxy = _MatchProxy(functools.partial(_match, self._filterer))
proxy.setSourceModel(model)
proxy.setRecursiveFilteringEnabled(True)
view.setModel(proxy)
view.setRootIndex(proxy.mapFromSource(current_root))
def _set_model(self, model):
tabs_count = model.rowCount(QtCore.QModelIndex())
for row in range(tabs_count):
branch_index = model.index(row, 0)
tab_name = model.data(branch_index, QtCore.Qt.DisplayRole)
widget = _MultiTree(branch_index)
for view in widget.iter_views():
self._replace_model_with_filterer_proxy(view)
self._tabs.addTab(widget, tab_name)
def _update_current_view(self):
widget = self._tabs.currentWidget()
for proxy in widget.iter_models():
proxy.invalidateFilter()
def _make_branch_graph():
default = Node("default")
optional = Node("optional")
Node("camera", parent=default)
Node("set", parent=default)
light = Node("light", parent=default)
Node("directional light", parent=light)
spot = Node("spot light", parent=light)
Node("light center", parent=spot)
Node("volume light", parent=light)
Node("model", parent=optional)
surfacing = Node("surfacing", parent=optional)
Node("look", parent=surfacing)
Node("hair", parent=surfacing)
Node("fur", parent=surfacing)
Node("rig", parent=optional)
return (default, optional)
def _make_full_graph():
roots = []
for index in range(_NUMBER_OF_BRANCHES):
branch = Branch("branch_{index}".format(index=index))
for node in _make_branch_graph():
branch.add_child(node)
roots.append(branch)
return roots
def main():
application = QtWidgets.QApplication([])
roots = _make_full_graph()
model = Model(roots)
window = Widget(model)
window.show()
application.exec_()
if __name__ == "__main__":
main()
- 運行上面的代碼以打開 GUI。選項卡 內部 QTreeViews 應如下所示:
QTabWidget tab - branch_0
QTreeView (default)
- camera
- set
- light
- directional light
- spot light
- light center
- volume light
QTreeView (optional)
- model
- surfacing
- look
- hair
- fur
- rig
QTabWidget tab - branch_1
- Same as branch_0
QTabWidget tab - branch_2
- Same as branch_0
這是正確的、預期的節點顯示。現在在過濾器 QLineEdit 中,輸入“light”。您現在將獲得:
QTabWidget tab - branch_0
QTreeView (default)
- light
- directional light
- spot light
- light center
- volume light
QTreeView (optional)
- branch_0
- default
- light
- directional light
- spot light
- light center
- volume light
- branch_1
- default
- light
- directional light
- spot light
- light center
- volume light
- branch_2
- default
- light
- directional light
- spot light
- light center
- volume light
QTabWidget tab - branch_1
- Same as branch_0
QTabWidget tab - branch_2
- Same as branch_0
在這里您可以看到標記為“可選”的 QTreeView 現在顯示每個分支的內容,而不是像它應該的那樣只顯示一個分支。
這不是預期的行為。作為參考,這是我希望得到的觀點:
QTabWidget tab - branch_0
QTreeView (default)
- default
- light
- directional light
- spot light
- light center
- volume light
QTreeView (optional) [EMPTY, no children]
QTabWidget tab - branch_1
- Same as branch_0
QTabWidget tab - branch_2
- Same as branch_0
Also notice that if you clear the filterer QLineEdit's text, "", you don't go back to the first graph. The "optional" QTreeView is stuck being shown everything, in every QTreeView.
Now in the filterer QLineEdit text, type "asdf". Now the graph is
QTabWidget tab - branch_0
QTreeView (default)
- branch_0
- branch_1
- branch_2
QTreeView (optional)
- branch_0
- branch_1
- branch_2
QTabWidget tab - branch_1
- Same as branch_0
QTabWidget tab - branch_2
- Same as branch_0
When my intended view was
QTabWidget tab - branch_0
QTreeView (default) [EMPTY]
QTreeView (optional) [EMPTY]
QTabWidget tab - branch_1
- Same as branch_0
QTabWidget tab - branch_2
- Same as branch_0
And if you clear the filterer text with "", now both QTreeViews show everything across all branches.
Is there a simple way to get the intended graph that I'm describing? Maybe there's a way to re-run QSortFilterProxyModel without invalidating indices, or some other mechanism I can use to get the same effect? At the moment I'm getting around the problem by "saving and restoring" the rootIndex for each view, pre and post invalidateFilter. But my approach for that doesn't work in all cases and feels like a hack.
對此的任何建議將不勝感激。
uj5u.com熱心網友回復:
問題來自這樣一個事實,即在應用過濾時,未被接受的索引成為無效索引,而對于 Qt 而言,無效索引與根索引相同。
由于您將視圖的根索引設定為過濾器使無效的索引,因此結果與 do 相同setRootIndex(QModelIndex()),它顯示了整個模型。
事實上,如果您嘗試使用不匹配任何“默認”分支項的字串過濾模型,也會遇到您在“可選”視圖中看到的相同問題:它將顯示整個根樹模型,這是因為如果索引無效,您的_match函式將回傳True,因此無論如何都會顯示根(在正常情況下,它會顯示一個空模型)。
請注意這方面:我不確定所需的行為,但如果過濾器不匹配任何內容,則不應回傳True,因為這將使所有根索引都有效,在您的情況下,即使它不應該是顯示的索引。
問題的根源在于模型不知道視圖的根索引(也不應該知道!)并且視圖無法知道無效的根索引何時再次變為有效(再次,它也不應該知道) )。請參閱有關類似問題的討論。
一個可能的解決方案(它沒有解決接受無效根的問題)是跟蹤根索引,然后通過連接到模型的信號來更新視圖rowsRemoved,最重要的是,rowsInserted這樣我們就可以恢復原始的每當根索引再次“可用”時:
class TreePersistentRootIndex(QtWidgets.QTreeView):
_sourceRootIndex = QtCore.QModelIndex()
def setModel(self, model):
if self.model() and isinstance(self.model(), QtCore.QSortFilterProxyModel):
self.model().layoutChanged.disconnect(self.checkRootIndex)
self.model().rowsRemoved.disconnect(self.checkRootIndex)
self.model().rowsInserted.disconnect(self.checkRootIndex)
super().setModel(model)
self._model = model
if isinstance(model, QtCore.QSortFilterProxyModel):
model.layoutChanged.connect(self.checkRootIndex)
model.rowsRemoved.connect(self.checkRootIndex)
model.rowsInserted.connect(self.checkRootIndex)
def checkRootIndex(self):
if (not self._sourceRootIndex.isValid() or
not isinstance(self.model(), QtCore.QSortFilterProxyModel)):
return
rootIndex = self.model().mapFromSource(self._sourceRootIndex)
if rootIndex != self.rootIndex():
super().setRootIndex(rootIndex)
def setRootIndex(self, rootIndex):
super().setRootIndex(rootIndex)
if isinstance(self.model(), QtCore.QSortFilterProxyModel):
rootIndex = self.model().mapToSource(rootIndex)
self._sourceRootIndex = rootIndex
class _MultiTree(QtWidgets.QWidget):
def __init__(self, source_index, parent=None):
# ...
for index in range(model.rowCount(source_index)):
section = model.index(index, 0, parent=source_index)
label = model.data(section, QtCore.Qt.DisplayRole)
view = TreePersistentRootIndex()
# ...
為了克服接受的根索引的問題,您可能會考慮更改過濾行為,或者最終(如果您完全確定如果過濾器不匹配任何子項,則不應在子根索引中顯示任何內容)中,使用setRowHidden()利用True(如在隱藏)如果所得的索引無效或False(如在顯示),否則。
可能的實作(未完全測驗)可能如下:
def checkRootIndex(self):
# ...as above, then:
if rootIndex.isValid() != self._sourceRootIndex.isValid():
hide = not rootIndex.isValid()
for row in range(self.model().rowCount(rootIndex)):
self.setRowHidden(row, rootIndex.parent(), hide)
轉載請註明出處,本文鏈接:https://www.uj5u.com/qukuanlian/368570.html
標籤:Python qt 模型视图控制器 pyqt pyside
