为什么 QTreeView.scrollTo() 一开始不起作用
Why doesn't QTreeView.scrollTo() work initially
下面的代码只是显示计算机驱动器的树视图。每次选择新的 file/folder 时,视图都会滚动以使新选择可见。
问题1:虽然这可行,但应用程序启动后的初始选择不会触发滚动。为什么?
问题2:如说明:
self.my_view.scrollTo(index, QAbstractItemView.EnsureVisible)
self.my_view.resizeColumnToContents(0)
反转:
self.my_view.resizeColumnToContents(0)
self.my_view.scrollTo(index, QAbstractItemView.EnsureVisible)
第一列的大小在初始显示时也没有调整,只有在之后。为什么?
import sys
from PyQt5.QtCore import Qt, QModelIndex, QDir
from PyQt5.QtWidgets import QApplication, QTreeView, QMainWindow, QFileSystemModel, QAbstractItemView
class MyWindow(QMainWindow):
def __init__(self):
super().__init__()
# Instance variables
self.my_view = QTreeView()
self.my_model = QFileSystemModel()
# Init FS model to show all computer drives
model_root_path = str(self.my_model.myComputer())
self.my_model.setRootPath(model_root_path)
# Init tree view
self.my_view.setModel(self.my_model)
self.my_view.setRootIndex(self.my_model.index(model_root_path))
self.my_view.setSelectionMode(QAbstractItemView.SingleSelection)
self.my_view.setSelectionBehavior(QAbstractItemView.SelectRows)
# Connect selection change events to custom slot
select_model = self.my_view.selectionModel()
select_model.currentRowChanged.connect(self.current_row_changed)
# Main window
self.setCentralWidget(self.my_view)
self.setGeometry(200, 200, 800, 600)
# Select initial row on view
focus_path = QDir.currentPath()
focus_index = self.my_model.index(focus_path)
self.my_view.setCurrentIndex(focus_index)
def current_row_changed(self):
"""Current row of the model has changed"""
# Scroll view to new row
index = self.my_view.selectionModel().currentIndex()
self.my_view.scrollTo(index, QAbstractItemView.EnsureVisible)
self.my_view.resizeColumnToContents(0)
# Show path of current row in window title
absolute_path = self.my_model.filePath(index)
self.setWindowTitle(absolute_path)
def main():
a = QApplication(sys.argv)
mw = MyWindow()
mw.show()
sys.exit(a.exec_())
if __name__ == '__main__':
main()
`
编辑:使用@ekhumoro 提供的好的解决方案后,我上面的示例代码有效。然而,另一段代码仍然没有:
import os
import sys
from PyQt5.QtCore import pyqtSignal, QTimer, QDir, Qt
from PyQt5.QtWidgets import QMainWindow, QGridLayout, QWidget, QTreeView, QAbstractItemView, QFileSystemModel, \
QApplication
class AppWindow(QMainWindow):
default_folder_path = "."
def __init__(self):
super().__init__()
self.folder_view = FolderTreeView()
self.folder_view.folder_has_changed.connect(self.folder_changed)
self.build_ui()
self.show()
# Select initial folder
self.select_initial_folder()
def build_ui(self):
main_widget = QWidget()
layout = QGridLayout(main_widget)
layout.addWidget(self.folder_view)
self.setCentralWidget(main_widget)
self.setGeometry(200, 100, 800, 600)
def select_initial_folder(self):
folder_index = self.folder_view.get_index(AppWindow.default_folder_path)
if folder_index.isValid():
self.folder_view.select_folder(folder_index)
def folder_changed(self, folder_path):
if not os.path.isdir(folder_path):
print("Non existing folder:", folder_path)
return
class FolderTreeView(QTreeView):
folder_has_changed = pyqtSignal(str)
def __init__(self):
super().__init__()
self.folder_tree_model = FolderTreeModel()
self.setModel(self.folder_tree_model)
self.setSelectionMode(QAbstractItemView.SingleSelection)
self.setSelectionBehavior(QAbstractItemView.SelectRows)
def select_folder(self, folder_index):
self.setCurrentIndex(folder_index)
def currentChanged(self, current, previous):
super(FolderTreeView, self).currentChanged(current, previous)
# Scroll the view to current item and resize folder name column
QTimer.singleShot(50, lambda: self.delayed_scroll(current))
# Emit signal for other uses
self.folder_has_changed.emit(self.folder_tree_model.filePath(current))
def delayed_scroll(self, index):
self.scrollTo(index, QAbstractItemView.EnsureVisible)
self.resizeColumnToContents(0)
def get_index(self, folder_path):
return self.folder_tree_model.index(folder_path)
class FolderTreeModel(QFileSystemModel):
def __init__(self):
super().__init__()
self.setFilter(QDir.AllDirs | QDir.NoDotAndDotDot)
self.setRootPath("")
def main():
app = QApplication(sys.argv)
window = AppWindow()
sys.exit(app.exec_())
if __name__ == '__main__':
main()
如果默认情况下模型将其当前索引初始化为当前目录,则可能会导致第一个问题。这意味着如果您将它 再次 设置为相同的索引,则不会发出行更改信号(因为没有任何更改)。这可以通过直接调用行更改处理程序来解决:
class MyWindow(QMainWindow):
def __init__(self):
super().__init__()
...
focus_path = QDir.currentPath()
focus_index = self.my_model.index(focus_path)
self.my_view.setCurrentIndex(focus_index)
self.current_row_changed()
def current_row_changed(self):
index = self.my_view.currentIndex()
self.my_view.scrollTo(index, QAbstractItemView.EnsureVisible)
self.my_view.resizeColumnToContents(0)
...
关于第二个问题:当你调用scrollTo
时,可能要扩展几个目录才能select需要的索引。这显然会改变第一列的宽度,因此您应该始终在 resizeColumnToContents
之后调用 以获得正确的宽度。
更新:
我认为还有一个问题是时序问题引起的。 QFileSystemModel
在某种程度上必须异步工作,因为它必须向操作系统请求资源,然后等待响应。此外,在收到响应之前,它无法提前准确知道将要接收多少数据,因为文件系统可能在等待期间已更新。潜在地,响应可能包括来自包含数千个文件的巨大目录的数据。因此,为了保持 GUI 响应,数据被分批处理,其大小足以填充当前视图。如果在 window 显示之前 设置了当前索引并且其所有小部件都已完全布局 ,则无法保证视图能够正确调整其列的大小。
这可以通过使用具有小延迟的一次性计时器显式重新调用行更改处理程序来解决。这应该允许视图正确地重新计算其列宽:
...
focus_path = QDir.currentPath()
focus_index = self.my_model.index(focus_path)
self.my_view.setCurrentIndex(focus_index)
QTimer.singleShot(50, self.current_row_changed)
def current_row_changed(self):
index = self.my_view.currentIndex()
self.my_view.scrollTo(index, QAbstractItemView.EnsureVisible)
self.my_view.resizeColumnToContents(0)
下面的代码只是显示计算机驱动器的树视图。每次选择新的 file/folder 时,视图都会滚动以使新选择可见。
问题1:虽然这可行,但应用程序启动后的初始选择不会触发滚动。为什么?
问题2:如说明:
self.my_view.scrollTo(index, QAbstractItemView.EnsureVisible)
self.my_view.resizeColumnToContents(0)
反转:
self.my_view.resizeColumnToContents(0)
self.my_view.scrollTo(index, QAbstractItemView.EnsureVisible)
第一列的大小在初始显示时也没有调整,只有在之后。为什么?
import sys
from PyQt5.QtCore import Qt, QModelIndex, QDir
from PyQt5.QtWidgets import QApplication, QTreeView, QMainWindow, QFileSystemModel, QAbstractItemView
class MyWindow(QMainWindow):
def __init__(self):
super().__init__()
# Instance variables
self.my_view = QTreeView()
self.my_model = QFileSystemModel()
# Init FS model to show all computer drives
model_root_path = str(self.my_model.myComputer())
self.my_model.setRootPath(model_root_path)
# Init tree view
self.my_view.setModel(self.my_model)
self.my_view.setRootIndex(self.my_model.index(model_root_path))
self.my_view.setSelectionMode(QAbstractItemView.SingleSelection)
self.my_view.setSelectionBehavior(QAbstractItemView.SelectRows)
# Connect selection change events to custom slot
select_model = self.my_view.selectionModel()
select_model.currentRowChanged.connect(self.current_row_changed)
# Main window
self.setCentralWidget(self.my_view)
self.setGeometry(200, 200, 800, 600)
# Select initial row on view
focus_path = QDir.currentPath()
focus_index = self.my_model.index(focus_path)
self.my_view.setCurrentIndex(focus_index)
def current_row_changed(self):
"""Current row of the model has changed"""
# Scroll view to new row
index = self.my_view.selectionModel().currentIndex()
self.my_view.scrollTo(index, QAbstractItemView.EnsureVisible)
self.my_view.resizeColumnToContents(0)
# Show path of current row in window title
absolute_path = self.my_model.filePath(index)
self.setWindowTitle(absolute_path)
def main():
a = QApplication(sys.argv)
mw = MyWindow()
mw.show()
sys.exit(a.exec_())
if __name__ == '__main__':
main()
`
编辑:使用@ekhumoro 提供的好的解决方案后,我上面的示例代码有效。然而,另一段代码仍然没有:
import os
import sys
from PyQt5.QtCore import pyqtSignal, QTimer, QDir, Qt
from PyQt5.QtWidgets import QMainWindow, QGridLayout, QWidget, QTreeView, QAbstractItemView, QFileSystemModel, \
QApplication
class AppWindow(QMainWindow):
default_folder_path = "."
def __init__(self):
super().__init__()
self.folder_view = FolderTreeView()
self.folder_view.folder_has_changed.connect(self.folder_changed)
self.build_ui()
self.show()
# Select initial folder
self.select_initial_folder()
def build_ui(self):
main_widget = QWidget()
layout = QGridLayout(main_widget)
layout.addWidget(self.folder_view)
self.setCentralWidget(main_widget)
self.setGeometry(200, 100, 800, 600)
def select_initial_folder(self):
folder_index = self.folder_view.get_index(AppWindow.default_folder_path)
if folder_index.isValid():
self.folder_view.select_folder(folder_index)
def folder_changed(self, folder_path):
if not os.path.isdir(folder_path):
print("Non existing folder:", folder_path)
return
class FolderTreeView(QTreeView):
folder_has_changed = pyqtSignal(str)
def __init__(self):
super().__init__()
self.folder_tree_model = FolderTreeModel()
self.setModel(self.folder_tree_model)
self.setSelectionMode(QAbstractItemView.SingleSelection)
self.setSelectionBehavior(QAbstractItemView.SelectRows)
def select_folder(self, folder_index):
self.setCurrentIndex(folder_index)
def currentChanged(self, current, previous):
super(FolderTreeView, self).currentChanged(current, previous)
# Scroll the view to current item and resize folder name column
QTimer.singleShot(50, lambda: self.delayed_scroll(current))
# Emit signal for other uses
self.folder_has_changed.emit(self.folder_tree_model.filePath(current))
def delayed_scroll(self, index):
self.scrollTo(index, QAbstractItemView.EnsureVisible)
self.resizeColumnToContents(0)
def get_index(self, folder_path):
return self.folder_tree_model.index(folder_path)
class FolderTreeModel(QFileSystemModel):
def __init__(self):
super().__init__()
self.setFilter(QDir.AllDirs | QDir.NoDotAndDotDot)
self.setRootPath("")
def main():
app = QApplication(sys.argv)
window = AppWindow()
sys.exit(app.exec_())
if __name__ == '__main__':
main()
如果默认情况下模型将其当前索引初始化为当前目录,则可能会导致第一个问题。这意味着如果您将它 再次 设置为相同的索引,则不会发出行更改信号(因为没有任何更改)。这可以通过直接调用行更改处理程序来解决:
class MyWindow(QMainWindow):
def __init__(self):
super().__init__()
...
focus_path = QDir.currentPath()
focus_index = self.my_model.index(focus_path)
self.my_view.setCurrentIndex(focus_index)
self.current_row_changed()
def current_row_changed(self):
index = self.my_view.currentIndex()
self.my_view.scrollTo(index, QAbstractItemView.EnsureVisible)
self.my_view.resizeColumnToContents(0)
...
关于第二个问题:当你调用scrollTo
时,可能要扩展几个目录才能select需要的索引。这显然会改变第一列的宽度,因此您应该始终在 resizeColumnToContents
之后调用 以获得正确的宽度。
更新:
我认为还有一个问题是时序问题引起的。 QFileSystemModel
在某种程度上必须异步工作,因为它必须向操作系统请求资源,然后等待响应。此外,在收到响应之前,它无法提前准确知道将要接收多少数据,因为文件系统可能在等待期间已更新。潜在地,响应可能包括来自包含数千个文件的巨大目录的数据。因此,为了保持 GUI 响应,数据被分批处理,其大小足以填充当前视图。如果在 window 显示之前 设置了当前索引并且其所有小部件都已完全布局 ,则无法保证视图能够正确调整其列的大小。
这可以通过使用具有小延迟的一次性计时器显式重新调用行更改处理程序来解决。这应该允许视图正确地重新计算其列宽:
...
focus_path = QDir.currentPath()
focus_index = self.my_model.index(focus_path)
self.my_view.setCurrentIndex(focus_index)
QTimer.singleShot(50, self.current_row_changed)
def current_row_changed(self):
index = self.my_view.currentIndex()
self.my_view.scrollTo(index, QAbstractItemView.EnsureVisible)
self.my_view.resizeColumnToContents(0)