【问题标题】:Pre-select multiple files in a QFileDialog在 QFileDialog 中预选多个文件
【发布时间】:2021-10-06 13:21:49
【问题描述】:

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

This answer 建议应该可以进行多选。

对于这个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 已被选为文件模式,但我发现它只是最后一个被选中的文件具有选择...但我希望在显示对话框时将所有三个文件都选中。

我尝试使用setVisible 来查看是否可以在显示对话框后以某种方式实现多选,但这不起作用。

【问题讨论】:

    标签: python pyqt5 qfiledialog multipleselection


    【解决方案1】:

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

    一开始我考虑使用item view的选择模型,但这不会更新line edit,它负责检查文件是否存在并在这种情况下启用Ok按钮;考虑到这一点,显而易见的解决方案是直接更新行编辑:

        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 信号。

    【讨论】:

    • 谢谢。根据您最初的想法,我发现QFileDialog 中有 3 个QItemSelectionModels,所有这些rowCountrowCount 为 2,即使您显示 3 个文件,所以不确定那里发生了什么。使用“选择”按钮来解决您提到的问题是可能的,但您的这个解决方案非常聪明。
    • @mikerodent 您不应该寻找选择模型,而是寻找项目视图。具体来说,QFileDialog 有两个视图用于显示文件,一个 QListView 和一个 QTreeView,可以使用findChild 及其对象名称(分别为listViewtreeView)访问。一旦你得到视图,你就可以得到它们的选择模型(它们是同步的,所以你选择哪个视图并不重要)。由于您似乎对 QFileDialog 很感兴趣,我建议您研究源代码:code.woboq.org/qt5/qtbase/src/widgets/dialogs/…
    • 请注意,QFileDialog 是为数不多的具有单独 UI 文件的 Qt 类之一,您可以在此处找到它:code.woboq.org/qt5/qtbase/src/widgets/.uic/…
    • 我实际上发现您的(否则非常出色)解决方案存在一个致命缺陷:使用您的解决方案无法有效地取消选择目录中最终选定的文件。看我的回答...
    【解决方案2】:

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

    但实际上当目的正如我所说的那样有一个致命的缺陷:不幸的是,如果你试图取消选择一个目录中最终选择的文件,实际上这个名字是not然后从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()
    

    【讨论】:

      【解决方案3】:

      玩了几个小时后,这是一种在 QFileDialog 中以编程方式预先选择多个文件的好方法:

      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']
      
      

      【讨论】:

      • 感谢您的贡献,但该功能还必须能够检测用户选择或取消选择了哪些文件(即切换了它们的选择)!我的回答就是这样,所以我不确定你的回答是否会增加很多。
      • @mikerodent 抱歉,我错过了将 | QItemSelectionModel.Rows 添加到 select() 方法中,这是允许 QFileDialog 识别所选行所必需的(不确定为什么)。刚刚再次测试,现在对我来说似乎工作正常。
      • 唯一的问题是这种方法仍然没有在文本框中显示选定的文件名,但我确信也有一种方法可以添加它,只是对我来说不是超级必要的做。
      猜你喜欢
      • 2011-09-23
      • 1970-01-01
      • 1970-01-01
      • 2011-01-25
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多