python 为什么QFileDialog.getOpenFileName忽略gnomewwayland上的dir参数?

ryevplcw  于 2023-04-19  发布在  Python
关注(0)|答案(1)|浏览(109)

bounty还有6天到期。回答此问题可获得+100声望奖励。Michael Clerx希望引起更多关注此问题。

我曾经能够在PyQt 5,PyQt 6,PySide 2等中为本地打开文件对话框设置初始目录,使用如下代码:

QtWidgets.QFileDialog.getOpenFileName(
    self,
    'Open file',
    '/home/michael/last_path',
)

其中,3d参数是指定对话框的初始目录的str
在Fedora的最新版本(运行wayland或X11)上,目录参数被默默忽略。
奇怪的是,它仍然适用于getSaveFileName
有人知道发生了什么吗?或者我可以在哪里报告这个问题?(Qt?Fedora devs?)或者类似地,我可以在哪里开始自己修复这个问题?
更新更多版本信息:
目前在Fedora 37上运行Gnome 43.3,但问题已经存在了一段时间,如果我没记错的话,至少可以追溯到Fedora 33。在两台不同的机器上都有同样的问题。在不是我写的软件中也看到过,但也使用了Python和Qt(例如Veusz)。
一开始以为这是Wayland的事情,但在@rede95下面的评论后检查,实际上也发生在X11上。

lztngnrs

lztngnrs1#

您描述的行为可能与Qt中的已知问题有关,Qt是QFileDialog类使用的框架。Qt使用平台插件与底层操作系统交互,此插件可能无法在某些系统或配置上正常工作。
在GNOME与Wayland的情况下,问题可能与Wayland使用与X11不同的协议来在应用程序和显示服务器之间进行通信的事实有关。这可能导致与Qt和其他依赖X11的应用程序的兼容性问题。

**第一种方式-**使用zenitykdialog

要使用zenitykdialog作为回退,请执行以下操作:zenitykdialog分别是用于在GNOME和KDE中创建对话框的命令行实用程序。如果QFileDialog不正确,我们可以使用这些实用程序作为后备。下面是一个示例:

import os, subprocess, platform
# pip install -U zenite

def get_open_file_name():
    if platform.system() == 'Linux':
        if os.environ.get('WAYLAND_DISPLAY'):
            # Fallback to zenity or kdialog
            if subprocess.call(['which', 'zenity']) == 0:
                command = ['zenity', '--file-selection']
            elif subprocess.call(['which', 'kdialog']) == 0:
                command = ['kdialog', '--getopenfilename']
            else:
                raise RuntimeError('Neither zenity nor kdialog found')
            result = subprocess.check_output(command)
            return result.decode().strip()
    # Fallback to QFileDialog
    return QtWidgets.QFileDialog.getOpenFileName(None, "Open file")[0]

此函数检查用户是否使用Wayland显示服务器运行Linux系统。如果是,则检查zenitykdialog命令行实用程序是否可用。如果是,则运行相应的命令以显示文件对话框并返回所选文件路径。如果两个实用程序都不可用,则如果用户没有运行带有Wayland显示服务器的Linux系统,或者如果文件对话框由于任何原因失败,它会回退到使用QFileDialog。

第二种方式-QtWebEngineWidgets.QWebEngineView

使用QtWebEngineWidgets.QWebEnginePage显示自定义文件对话框:QtWebEngineWidgets.QWebEnginePage提供了一种在QWebEngineView小部件中显示自定义HTML页面的方法。我们可以使用它来显示一个自定义文件对话框,该对话框允许我们设置初始目录。下面是一个示例:

from PyQt5 import QtWidgets, QtWebEngineWidgets, QtCore

class CustomFileDialog(QtWebEngineWidgets.QWebEngineView):
    def __init__(self, parent=None):
        super().__init__(parent)
        
        self._current_dir = None
        
        self.setHtml("""
        <html>
            <head>
                <title>Custom File Dialog</title>
            </head>
            <body>
                <input type="file" id="file" webkitdirectory directory />
            </body>
        </html>
        """)
        
        self.page().profile().downloadRequested.connect(self._on_download_requested)
        
    def set_current_directory(self, directory):
        self._current_dir = directory
        
    def _on_download_requested(self, download):
        download.setDirectory(self._current_dir)

这段代码定义了一个CustomFileDialog类,它继承自QtWebEngineWidgets. QWebEngineView。它将视图的HTML内容设置为一个带有“打开目录”按钮的简单文件对话框。当用户单击此按钮时,它将打开设置了目录标志的本机文件对话框,允许用户选择目录。一旦用户选择了目录,downloadRequested信号将由QWebEngineProfile发出。而_on_download_requested方法将文件对话框的初始目录设置为用户选择的目录。
您可以像这样使用此CustomFileDialog类:

class MainWindow(QtWidgets.QMainWindow):
    def __init__(self):
        super().__init__()
        # INJECT IT HERE 
        self._file_dialog = CustomFileDialog(self)
        self._file_dialog.set_current_directory('/home/user/') #  It sets the initial directory for the file dialog to '/home/user/'
        self._file_dialog.page().setBackgroundColor(QtGui.QColor(0, 0, 0, 0))
        
        self.setCentralWidget(self._file_dialog)

第三路-xdg-desktop-portal

xdg-desktop-portal是一个门户,允许应用程序在桌面环境(如GNOME)上显示本机文件对话框。我们可以使用xdg-desktop-portal Python库通过xdg-desktop-portal显示文件对话框。下面是一个示例:

import xdg.DesktopEntry
import xdg.DesktopEntry.Parser
import xdg.DesktopEntry.TrustLevel
import xdg.IconTheme
import xdg.Mime

import gi
gi.require_version('Gtk', '3.0')
gi.require_version('Gdk', '3.0')
gi.require_version('Gio', '2.0')

from gi.repository import Gtk, Gdk, Gio

def show_file_dialog(title, initial_directory):
    dialog = Gtk.FileChooserNative.new(title, None, Gtk.FileChooserAction.OPEN, None, None)
    dialog.set_current_folder(initial_directory)
    response = dialog.run()
    if response == Gtk.ResponseType.OK:
        filename = dialog.get_filename()
        dialog.destroy()
        return filename
    else:
        dialog.destroy()
        return None

def set_initial_directory(dialog, directory):
    app_id = xdg.DesktopEntry.get_('xdg-screensaver').get_filename()
    portal = Gio.DBusProxy.new_for_bus_sync(Gio.BusType.SYSTEM, Gio.DBusProxyFlags.NONE, None, 'org.freedesktop.portal.Desktop', '/org/freedesktop/portal/desktop', 'org.freedesktop.portal.Desktop', None)
    options = Gio.Variant('a{sv}', {
        'current_folder': directory,
        'title': dialog.windowTitle(),
        'parent_window': int(dialog.winId()),
        'accept_label': 'Open',
        'cancel_label': 'Cancel',
        'modal': True,
        'finish_on_close': True,
        'filters': [],
        'extra': Gio.Variant('a{sv}', {
            'show_hidden': True
        })
    })
    portal.OpenFileChooser(options, 'unix:fd:3', app_id)

def on_file_dialog_response(dialog, response):
    if response == Gtk.ResponseType.OK:
        filename = dialog.get_filename()
        print(f'Selected file: {filename}')
    elif response == Gtk.ResponseType.CANCEL:
        print('Dialog cancelled')
    dialog.destroy()

def show_file_dialog_with_portal(title, initial_directory):
    dialog = Gtk.FileChooserDialog(
        title,
        None,
        Gtk.FileChooserAction.OPEN,
        (
            Gtk.STOCK_CANCEL,
            Gtk.ResponseType.CANCEL,
            Gtk.STOCK_OPEN,
            Gtk.ResponseType.OK
        )
    )
    dialog.set_default_response(Gtk.ResponseType.OK)
    dialog.set_current_folder(initial_directory)
    dialog.connect('response', on_file_dialog_response)
    set_initial_directory(dialog, initial_directory)

show_file_dialog_with_portal('Open file', '/home/michael/last_path')

set_initial_directory函数设置文件对话框的初始目录,show_file_dialog_with_portal函数使用xdg-desktop-portal显示文件对话框。此方法允许您在GNOME中使用本机文件对话框

第四路-QPlatformFileDialogHelper

使用QPlatformFileDialogHelper:QPlatformFileDialogHelper是Qt中的一个低级类,它提供了一种在不同平台上实现自定义文件对话框行为的方法。您可以使用它来创建一个自定义文件对话框实现,该实现可以在GNOME上与Wayland一起正常工作。下面是一个示例:

from PyQt5.QtWidgets import QPlatformFileDialogHelper, QFileDialog

class MyFileDialog(QFileDialog):
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        
        if self.testOption(self.DontUseNativeDialog):
            self._helper = QPlatformFileDialogHelper()
            self._helper.setDirectory('/home/Niceman/last_path')

    def showEvent(self, event): #important!
        super().showEvent(event)
        if not self.testOption(self.DontUseNativeDialog):
            self._helper.setDirectory('/home/michael/last_path')

在这个例子中,i自定义文件对话框类使用QPlatformFileDialogHelper来设置初始目录。如果设置了DontUseNativeDialog选项,我们将创建QPlatformFileDialogHelper的新示例并设置初始目录。否则,我们将在QPlatformFileDialogHelper的现有示例中设置初始目录。

**第五种方式-**Qt QML FileDialog

使用Qt QML FileDialog组件:Qt QML FileDialog组件提供了一个独立于平台的文件对话框,您可以在使用PyQt或PySide的Python应用程序中使用该对话框。下面是一个示例:

import QtQuick 2.12
import QtQuick.Controls 2.12

FileDialog {
    id: fileDialog
    title: "Open file"
    folder: "/home/michael/last_path"
    onAccepted: {
        // Do something with the selected file
        console.log("Selected file:", fileUrl)
    }
    onRejected: {
        // The user canceled the file dialog or selected a directory
        console.log("File dialog canceled or directory selected")
    }
}

这段QML代码定义了一个FileDialog组件,你可以在PyQt或PySide应用程序中使用它。它将文件对话框的初始目录设置为/home/michael/last_path。当用户选择一个文件时,会发出onAccepted信号,其中包含所选文件的URL。当用户取消文件对话框或选择一个目录时,会发出onRejected信号。
在PyQt或PySide应用程序中嵌入QML文件对话框:您可以使用QQuickWidget类在PyQt或PySide应用程序中嵌入QML文件对话框。下面是一个示例:

from PyQt5 import QtCore, QtWidgets, QtQuickWidgets

class MainWindow(QtWidgets.QMainWindow):
    def __init__(self):
        super().__init__()
        
        # Load the QML file dialog component
        self.file_dialog = QtQuickWidgets.QQuickWidget()
        self.file_dialog.setResizeMode(QtQuickWidgets.QQuickWidget.SizeRootObjectToView)
        self.file_dialog.setSource(QtCore.QUrl.fromLocalFile('file_dialog.qml'))
        
        # Add the QML file dialog component to the main window
        self.setCentralWidget(self.file_dialog)
        
        # Connect the signals of the QML file dialog component to Python slots
        self.file_dialog.rootObject().accepted.connect(self.handle_accepted)
        self.file_dialog.rootObject().rejected.connect(self.handle_rejected)
        
    @QtCore.pyqtSlot(QtCore.QUrl)
    def handle_accepted(self, file_url):
        # Do something with the selected file
        print(f"Selected file: {file_url.toLocalFile()}")
        
    @QtCore.pyqtSlot()
    def handle_rejected(self):
        # The user canceled the file dialog or selected a directory
        print("File dialog canceled or directory selected")

这段代码定义了一个MainWindow类,用于加载QML文件对话框组件并使用QQuickWidget将其添加到主窗口。

相关问题