在 QFileDialog 中预先 select 多个文件

Pre-select multiple files in a QFileDialog

当显示“选择文件”对话框时,我想预先 select 项目中的文件,这些文件已经配置为该项目的“一部分”,因此用户可以 select新文件或 unselect 现有(即先前选择的)文件。

This answer 建议多个 selection 应该是可能的。

对于这个MRE,请制作3个文件并将它们放在合适的位置ref_dir:

from PyQt5 import QtWidgets
import sys

class Window(QtWidgets.QWidget):
    def __init__(self):
        super(Window, self).__init__()
        self.button = QtWidgets.QPushButton('Test', self)
        self.button.clicked.connect(self.handle_button)
        layout = QtWidgets.QVBoxLayout(self)
        layout.addWidget(self.button)

    def handle_button(self):
        options = QtWidgets.QFileDialog.Options()
        options |= QtWidgets.QFileDialog.DontUseNativeDialog
        ref_dir = 'D:\temp'
        files_list = ['file1.txt', 'file2.txt', 'file3.txt']
        fd = QtWidgets.QFileDialog(None, 'Choose project files', ref_dir, '(*.txt)')
        fd.setFileMode(QtWidgets.QFileDialog.ExistingFiles)
        fd.setOptions(options)
        # fd.setVisible(True)
        for file in files_list:
            print(f'selecting file |{file}|')
            fd.selectFile(file)
        string_list = fd.exec()
        print(f'string list {string_list}')

if __name__ == '__main__':
    app = QtWidgets.QApplication(sys.argv)
    window = Window()
    window.show()
    sys.exit(app.exec_())

不幸的是,尽管 ExistingFiles 已被选为文件模式,但我发现它只是最后一个文件 selected 具有 selection... 但我希望在显示对话框时 select 所有三个都被编辑。

我尝试用 setVisible 进行试验,看看是否可以在显示对话框后以某种方式实现多个 selection,但这没有用。

由于使用了非本机文件对话框,我们可以访问其子窗口小部件来控制其行为。

起初我考虑使用项目视图的选择模型,但这不会更新行编辑,它负责检查文件是否存在并在这种情况下启用确定按钮;考虑到这一点,显而易见的解决方案是直接更新行编辑:

    def handle_button(self):
        # ...
        existing = []
        for file in files_list:
            if fd.directory().exists(file):
                existing.append('"{}"'.format(file))
        lineEdit = fd.findChild(QtWidgets.QLineEdit, 'fileNameEdit')
        lineEdit.setText(' '.join(existing))
        if fd.exec():
            print('string list {}'.format(fd.selectedFiles()))

这种方法的唯一缺点是不会发送 fileSelectedfilesSelected 信号。

Musicamante 的回答非常非常有帮助,特别是表明选择实际上是通过用路径字符串填充 QLE 来触发的。

但实际上当目的如我所说时存在致命缺陷:不幸的是,如果您尝试取消选择目录中最终选择的文件,实际上这个名称是而不是 然后从 QLE 中删除。事实上,如果 QLE 设置为空白,则会禁用“选择”按钮。所有这一切都是设计使然:QFileDialog 的功能是 “打开”或“保存”,而不是“修改”。

但我确实找到了解决方案,其中涉及找到列出目录中文件的QListView,然后在其选择模型上使用信号。

这满足的另一件事是当您更改目录时会发生什么:显然,您随后希望根据在该目录中找到(或未找到)的项目文件来更新选择。事实上,我已经更改了“选择”按钮的文本,以表明“修改”是游戏的名称。

fd = QtWidgets.QFileDialog(app.get_main_window(), 'Modify project files', start_directory, '(*.docx)')
fd.setFileMode(QtWidgets.QFileDialog.ExistingFiles)
fd.setViewMode(QtWidgets.QFileDialog.List)

fd.setLabelText(QtWidgets.QFileDialog.Reject, '&Cancel')
fd.setLabelText(QtWidgets.QFileDialog.Accept, '&Modify')
fd.setOptions(options)

file_name_line_edit = fd.findChild(QtWidgets.QLineEdit, 'fileNameEdit')
list_view = fd.findChild(QtWidgets.QListView, 'listView')

# utility to cope with all permutations of backslashes and forward slashes in path strings:
def split_file_path_str(path_str):
    dir_path_str, filename = ntpath.split(path_str)
    return dir_path_str, (filename or ntpath.basename(dir_path_str))

fd.displayed_dir = None
sel_model = list_view.selectionModel()
def sel_changed():
    if not fd.displayed_dir:
        return

    selected_file_paths_in_shown_dir = []
    sel_col_0s = sel_model.selectedRows()
    for sel_col_0 in sel_col_0s:
        file_path_str = os.path.join(fd.displayed_dir, sel_col_0.data())
        selected_file_paths_in_shown_dir.append(file_path_str)
        already_included = file_path_str in self.files_list
        if not already_included:
            fd.project_files_in_shown_dir.append(file_path_str)
            
    # now find if there are any project files which are now NOT selected
    for project_file_path_str in fd.project_files_in_shown_dir:
        if project_file_path_str not in selected_file_paths_in_shown_dir:
            fd.project_files_in_shown_dir.remove(project_file_path_str)
            
sel_model.selectionChanged.connect(sel_changed)

def file_dlg_dir_entered(displayed_dir):
    displayed_dir = os.path.normpath(displayed_dir)
    
    # this is set to None to prevent unwanted selection processing triggered by setText(...) below 
    fd.displayed_dir = None
    
    fd.project_files_in_shown_dir = []
    existing = []
    for file_path_str in self.files_list:
        dir_path_str, filename = split_file_path_str(file_path_str)
        if dir_path_str == displayed_dir:     
            existing.append(f'"{file_path_str}"')
            fd.project_files_in_shown_dir.append(file_path_str)
    file_name_line_edit.setText(' '.join(existing))            
    fd.displayed_dir = displayed_dir
    
fd.directoryEntered.connect(file_dlg_dir_entered)

# set the initially displayed directory...
file_dlg_dir_entered(start_directory)

if fd.exec():
    # for each file, if not present in self.files_list, add to files list and make self dirty
    for project_file_in_shown_dir in fd.project_files_in_shown_dir:
        if project_file_in_shown_dir not in self.files_list:
            self.files_list.append(project_file_in_shown_dir)
            # also add to list widget...
            app.get_main_window().ui.files_list.addItem(project_file_in_shown_dir)
            if not self.is_dirty():
                self.toggle_dirty()
    
    # but we also have to make sure that a file has not been UNselected...
    docx_files_in_start_dir = [f for f in os.listdir(fd.displayed_dir) if os.path.isfile(os.path.join(fd.displayed_dir, f)) and os.path.splitext(f)[1] == '.docx' ]
    for docx_file_in_start_dir in docx_files_in_start_dir:
        docx_file_path_str = os.path.join(fd.displayed_dir, docx_file_in_start_dir)
        if docx_file_path_str in self.files_list and docx_file_path_str not in fd.project_files_in_shown_dir:
            self.files_list.remove(docx_file_path_str)
            list_widget = app.get_main_window().ui.files_list
            item_for_removal = list_widget.findItems(docx_file_path_str, QtCore.Qt.MatchExactly)[0]
            list_widget.takeItem(list_widget.row(item_for_removal))
            if not self.is_dirty():
                self.toggle_dirty()

经过几个小时的尝试,这里有一个以编程方式在 QFileDialog 中预先 select 多个文件的非常好的方法:

from pathlib import Path
from PyQt5.QtCore import QItemSelectionModel
from PyQt5.QtWidgets import QFileDialog, QListView

p_files = Path('/path/to/your/files')

dlg = QFileDialog(
    directory=str(p_files),
    options=QFileDialog.DontUseNativeDialog)

# get QListView which controls item selection
file_view = dlg.findChild(QListView, 'listView')

# filter files which we want to select based on any condition (eg only .txt files)
# anything will work here as long as you get a list of Path objects or just str filepaths
sel_files = [p for p in p_files.iterdir() if p.suffix == '.txt']

# get selection model (QItemSelectionModel)
sel_model = file_view.selectionModel()

for p in sel_files:

    # get idx (QModelIndex) from model() (QFileSystemModel) using str of Path obj
    idx = sel_model.model().index(str(p))

    # set the active selection using each QModelIndex
    # IMPORTANT - need to include the selection type
    # see dir(QItemSelectionModel) for all options
    sel_model.select(idx, QItemSelectionModel.Select | QItemSelectionModel.Rows)

dlg.exec_()

dlg.selectedFiles()
>>> ['list.txt', 'of.txt', 'selected.txt', 'files.txt']