选择 QTreeView 的最顶层项目
Selecting the top-most item of a QTreeView
我有一个 Treeview
(继承自 QTreeView
),型号为 QFileSystemModel
。这个 Treeview
对象被添加到另一个带有 QPushButton
的小部件。该按钮将通过 QFileDialog
打开一个目录,并调用 Treeview
的 set_directory
方法。该方法会将模型的根路径设置为所选目录,并将修改树视图的根索引。然后选择树视图中最顶部的项目。
但是,第一次选择目录时,没有选择最上面的项目。注释掉第 11 行:self.model.directoryLoaded.connect(self.set_directory)
,问题就解决了。但这意味着 set_directory
方法被调用了两次:
# default directory opened
<class 'NoneType'>
# open new directory, set_directory called twice
<class 'NoneType'>
<class 'PyQt5.QtWidgets.QFileSystemModel'>
如命令行输出所示,首次选择目录时,QModelIndex.model()
方法 returns 为 NoneType。如何在不调用 set_directory
方法两次的情况下将树视图的当前索引设置为最上面的项目?为什么 QModelIndex
模型在目录未被访问时为 NoneType?
主要脚本如下:
from PyQt5.QtWidgets import QPushButton, QTreeView, QFileSystemModel, QWidget, QFileDialog, QApplication, QHBoxLayout
import sys
class Treeview(QTreeView):
def __init__(self, parent=None):
super(QTreeView, self).__init__(parent)
self.model = QFileSystemModel()
self.setModel(self.model)
self.set_directory(".")
# self.model.directoryLoaded.connect(self.set_directory)
def set_directory(self, path):
self.setRootIndex(self.model.setRootPath(path))
self.setCurrentIndex(self.model.index(0, 0, self.rootIndex()))
print(type(self.model.index(0, 0, self.rootIndex()).model()))
class MainWidget(QWidget):
def __init__(self, parent=None):
super(QWidget, self).__init__(parent)
self.tview = Treeview(self)
vlayout = QHBoxLayout(self)
vlayout.addWidget(self.tview)
open_button = QPushButton(self)
open_button.clicked.connect(self.open_dir)
vlayout.addWidget(open_button)
def open_dir(self):
filepath = QFileDialog.getExistingDirectory(self)
if filepath:
self.tview.set_directory(filepath)
if __name__ == '__main__':
app = QApplication(sys.argv)
widget = MainWidget()
widget.show()
sys.exit(app.exec_())
存在三个问题:
directoryLoaded
每当目录的内容第一次被加载时被调用 ,这是在一个单独的线程上异步发生的;如果“root”目录的内容还没有被加载,它还没有 children,所以任何访问根索引的 children 的尝试都会 return 无效索引(这就是为什么你得到None
);
- 每当目录在树中第一次 扩展 时也会发送信号,如果您将它连接到
set_directory
会产生问题:如果您打开“home”文件夹,然后展开“documents”目录,模型将加载其所有内容并为“documents”文件夹发出 directoryLoaded
,然后再次依次调用 set_directory
;
- 文件系统模型还对项目进行排序(默认情况下按字母顺序排列,目录在前),这意味着它需要更多时间才能获得您认为排在第一位的内容item:出于性能原因,OS returns 目录的内容取决于文件系统,这取决于它的实现:有时按 creation/modification 时间排序,其他按物理时间排序position/index 块等;
考虑到以上情况,你不能依赖 directoryLoaded
(当然你不应该将它连接到 set_directory
),但你可以使用 layoutChanged
信号,因为它总是被发射每当排序完成时(并且 QFileSystemModel 总是在根更改时对模型进行排序);唯一的问题是你必须只在需要时才这样做。
解决方案是创建一个尝试设置顶部项目的函数,如果它无效则它将自身连接到layoutChanged
信号;那时,当模型完成其工作并且顶部索引可用时,将发出信号。使用标志可以帮助我们知道信号是否已连接,然后断开连接,这在您需要支持排序的情况下很重要。
class Treeview(QTreeView):
layoutCheck = False
def __init__(self, parent=None):
super(QTreeView, self).__init__(parent)
self.model = QFileSystemModel()
self.setModel(self.model)
self.set_directory(QDir.currentPath())
self.setSortingEnabled(True)
def setTopIndex(self):
topIndex = self.model.index(0, 0, self.rootIndex())
print(topIndex.isValid(), topIndex.model())
if topIndex.isValid():
self.setCurrentIndex(topIndex)
if self.layoutCheck:
self.model.layoutChanged.disconnect(self.setTopIndex)
self.layoutCheck = False
else:
if not self.layoutCheck:
self.model.layoutChanged.connect(self.setTopIndex)
self.layoutCheck = True
def set_directory(self, path):
self.setRootIndex(self.model.setRootPath(path))
self.setTopIndex()
请考虑 layoutCheck
标志 非常 重要,原因有很多:
- 如前所述,
layoutChanged
总是 在模型排序时发出;这不仅在尝试使用 headers 进行排序时发生,而且在添加文件或目录时也会发生;
- 信号连接不是排他性的,您甚至可以将同一个信号多次连接到同一个 slot/function,结果是该函数将被调用多次;如果你不小心,你可能会面临递归的风险;
- 该标志也适用于空目录,避免了上述递归风险;如果打开一个新的根目录并且它是空的,它显然会 return 一个无效的顶级项目索引(因为有 none),但无论如何信号都会断开:如果另一个目录( with contents) 已打开但尚未加载,则信号不会再次连接,如果内容已加载,则无论如何都会断开连接;
一种可能性是使用 Qt.UniqueConnection
连接类型和 try
块来断开连接,但是,虽然这种方法有效且一致,但有点麻烦:只要连接总是与设置配对,使用基本的布尔标志更简单,更容易阅读和理解。
我有一个 Treeview
(继承自 QTreeView
),型号为 QFileSystemModel
。这个 Treeview
对象被添加到另一个带有 QPushButton
的小部件。该按钮将通过 QFileDialog
打开一个目录,并调用 Treeview
的 set_directory
方法。该方法会将模型的根路径设置为所选目录,并将修改树视图的根索引。然后选择树视图中最顶部的项目。
但是,第一次选择目录时,没有选择最上面的项目。注释掉第 11 行:self.model.directoryLoaded.connect(self.set_directory)
,问题就解决了。但这意味着 set_directory
方法被调用了两次:
# default directory opened
<class 'NoneType'>
# open new directory, set_directory called twice
<class 'NoneType'>
<class 'PyQt5.QtWidgets.QFileSystemModel'>
如命令行输出所示,首次选择目录时,QModelIndex.model()
方法 returns 为 NoneType。如何在不调用 set_directory
方法两次的情况下将树视图的当前索引设置为最上面的项目?为什么 QModelIndex
模型在目录未被访问时为 NoneType?
主要脚本如下:
from PyQt5.QtWidgets import QPushButton, QTreeView, QFileSystemModel, QWidget, QFileDialog, QApplication, QHBoxLayout
import sys
class Treeview(QTreeView):
def __init__(self, parent=None):
super(QTreeView, self).__init__(parent)
self.model = QFileSystemModel()
self.setModel(self.model)
self.set_directory(".")
# self.model.directoryLoaded.connect(self.set_directory)
def set_directory(self, path):
self.setRootIndex(self.model.setRootPath(path))
self.setCurrentIndex(self.model.index(0, 0, self.rootIndex()))
print(type(self.model.index(0, 0, self.rootIndex()).model()))
class MainWidget(QWidget):
def __init__(self, parent=None):
super(QWidget, self).__init__(parent)
self.tview = Treeview(self)
vlayout = QHBoxLayout(self)
vlayout.addWidget(self.tview)
open_button = QPushButton(self)
open_button.clicked.connect(self.open_dir)
vlayout.addWidget(open_button)
def open_dir(self):
filepath = QFileDialog.getExistingDirectory(self)
if filepath:
self.tview.set_directory(filepath)
if __name__ == '__main__':
app = QApplication(sys.argv)
widget = MainWidget()
widget.show()
sys.exit(app.exec_())
存在三个问题:
directoryLoaded
每当目录的内容第一次被加载时被调用 ,这是在一个单独的线程上异步发生的;如果“root”目录的内容还没有被加载,它还没有 children,所以任何访问根索引的 children 的尝试都会 return 无效索引(这就是为什么你得到None
);- 每当目录在树中第一次 扩展 时也会发送信号,如果您将它连接到
set_directory
会产生问题:如果您打开“home”文件夹,然后展开“documents”目录,模型将加载其所有内容并为“documents”文件夹发出directoryLoaded
,然后再次依次调用set_directory
; - 文件系统模型还对项目进行排序(默认情况下按字母顺序排列,目录在前),这意味着它需要更多时间才能获得您认为排在第一位的内容item:出于性能原因,OS returns 目录的内容取决于文件系统,这取决于它的实现:有时按 creation/modification 时间排序,其他按物理时间排序position/index 块等;
考虑到以上情况,你不能依赖 directoryLoaded
(当然你不应该将它连接到 set_directory
),但你可以使用 layoutChanged
信号,因为它总是被发射每当排序完成时(并且 QFileSystemModel 总是在根更改时对模型进行排序);唯一的问题是你必须只在需要时才这样做。
解决方案是创建一个尝试设置顶部项目的函数,如果它无效则它将自身连接到layoutChanged
信号;那时,当模型完成其工作并且顶部索引可用时,将发出信号。使用标志可以帮助我们知道信号是否已连接,然后断开连接,这在您需要支持排序的情况下很重要。
class Treeview(QTreeView):
layoutCheck = False
def __init__(self, parent=None):
super(QTreeView, self).__init__(parent)
self.model = QFileSystemModel()
self.setModel(self.model)
self.set_directory(QDir.currentPath())
self.setSortingEnabled(True)
def setTopIndex(self):
topIndex = self.model.index(0, 0, self.rootIndex())
print(topIndex.isValid(), topIndex.model())
if topIndex.isValid():
self.setCurrentIndex(topIndex)
if self.layoutCheck:
self.model.layoutChanged.disconnect(self.setTopIndex)
self.layoutCheck = False
else:
if not self.layoutCheck:
self.model.layoutChanged.connect(self.setTopIndex)
self.layoutCheck = True
def set_directory(self, path):
self.setRootIndex(self.model.setRootPath(path))
self.setTopIndex()
请考虑 layoutCheck
标志 非常 重要,原因有很多:
- 如前所述,
layoutChanged
总是 在模型排序时发出;这不仅在尝试使用 headers 进行排序时发生,而且在添加文件或目录时也会发生; - 信号连接不是排他性的,您甚至可以将同一个信号多次连接到同一个 slot/function,结果是该函数将被调用多次;如果你不小心,你可能会面临递归的风险;
- 该标志也适用于空目录,避免了上述递归风险;如果打开一个新的根目录并且它是空的,它显然会 return 一个无效的顶级项目索引(因为有 none),但无论如何信号都会断开:如果另一个目录( with contents) 已打开但尚未加载,则信号不会再次连接,如果内容已加载,则无论如何都会断开连接;
一种可能性是使用 Qt.UniqueConnection
连接类型和 try
块来断开连接,但是,虽然这种方法有效且一致,但有点麻烦:只要连接总是与设置配对,使用基本的布尔标志更简单,更容易阅读和理解。