6

I'm trying to open a file in my PySide2 application, but the file dialog always opens below the main window and appears as another application in the launcher. The application's name is "Portal".

I see other answers where the solution is to pass the main window as the first parameter to getOpenFileName(), but that doesn't work for me.

Here's a simple demonstration of the problem:

import sys
from PySide2.QtWidgets import QPushButton, QFileDialog, QApplication


class DemoButton(QPushButton):
    def __init__(self, text):
        super().__init__(text)
        self.clicked.connect(self.on_click)

    def on_click(self):
        file_name, _ = QFileDialog.getOpenFileName(
            self,
            "Open a text file.",
            filter='Text file (*.txt)')
        print(file_name)


def main():
    app = QApplication(sys.argv)
    button = DemoButton("Hello World")
    button.show()
    app.exec_()
    sys.exit()


main()

I thought maybe the parent had to be a QMainWindow, so I tried that:

import sys

from PySide2 import QtWidgets


class MainWindow(QtWidgets.QMainWindow):
    def __init__(self, parent=None):
        super(MainWindow, self).__init__(parent)
        main_widget = QtWidgets.QWidget(self)
        self.setCentralWidget(main_widget)

        # layout initialize
        g_layout = QtWidgets.QVBoxLayout()
        layout = QtWidgets.QFormLayout()
        main_widget.setLayout(g_layout)

        # Add Widgets
        self.exec_btn = QtWidgets.QPushButton('Execute')
        self.exec_btn.clicked.connect(self.find_file)

        # global layout setting
        g_layout.addLayout(layout)
        g_layout.addWidget(self.exec_btn)

    def find_file(self):
        file_name, _ = QtWidgets.QFileDialog.getOpenFileName(
            self,
            "Open a text file.",
            filter='Text file (*.txt)')
        print(file_name)


def main():
    app = QtWidgets.QApplication(sys.argv)
    window = MainWindow()
    window.show()
    app.exec_()
    sys.exit()


main()

The file dialog behaved exactly the same.

I'm using PySide2 5.12.2, Python 3.6.7, and running on Ubuntu 18.04.

eyllanesc
  • 235,170
  • 19
  • 170
  • 241
Don Kirkby
  • 53,582
  • 27
  • 205
  • 286
  • 2
    I cannot reproduce what you describe on my arch linux system. The documented behaviour is that it should produce a modal dialog centred on the parent, and that is exactly what I get. You could try adding `options=QFileDialog.DontUseNativeDialog` to see if that changes anything. (PS: I wonder whether the problem is somehow caused by [xdg-desktop-portal](https://github.com/flatpak/xdg-desktop-portal). Do you have that installed on your system?). – ekhumoro Dec 07 '19 at 15:23
  • 1
    I had the same issue. Using a platform.system() check so that on windows I still get the native dialog – Jim Feb 13 '20 at 19:42

2 Answers2

2

Thanks to ekhumoro's comment, I learned that I can tell PySide2 not to use the native file dialog.

import sys
from PySide2.QtWidgets import QPushButton, QFileDialog, QApplication


class DemoButton(QPushButton):
    def __init__(self, text):
        super().__init__(text)
        self.clicked.connect(self.on_click)

    def on_click(self):
        file_name, _ = QFileDialog.getOpenFileName(
            self,
            "Open a text file.",
            filter='Text file (*.txt)',
            options=QFileDialog.DontUseNativeDialog)
        print(file_name)


def main():
    app = QApplication(sys.argv)
    button = DemoButton("Hello World")
    button.show()
    app.exec_()
    sys.exit()


main()

That fixes the behaviour by bringing the file dialog to the front, but I think the native file dialog looks better. Hopefully, there's another option that can make the native file dialog work properly.

Don Kirkby
  • 53,582
  • 27
  • 205
  • 286
0

I had a similar problem. The issue is in part the default windows behavior, which does not allow applications to steal focus. I was able to solve it by using an external context handler which sets the window focus. This uses the NativeWindow method. This was tested on Windows 11 with Python 3.10.6, PySide6:

Window context handler is needed to bring the QFileDialog forward:

import os
import re
import win32gui
import win32com.client 
from PySide6.QtWidgets import QFileDialog, QApplication
from PySide6 import QtCore

class WindowMgr:
    """ Encapsulates calls to the winapi for window management
        Forces context window to take focus
        Based on: 
         - https://stackoverflow.com/questions/2090464/python-window-activation
         - https://stackoverflow.com/questions/14295337/win32gui-setactivewindow-error-the-specified-procedure-could-not-be-found
    """

    def __init__ (self):
        self._handle = None

    def find_window(self, class_name, window_name=None):
        """find a window by its class_name"""
        self._handle = win32gui.FindWindow(class_name, window_name)
        return self

    def _window_enum_callback(self, hwnd, wildcard):
        """Pass to win32gui.EnumWindows() to check all the opened windows"""
        if re.match(wildcard, str(win32gui.GetWindowText(hwnd))) is not None:
            self._handle = hwnd

    def find_window_wildcard(self, wildcard):
        """find a window whose title matches the wildcard regex"""
        self._handle = None
        win32gui.EnumWindows(self._window_enum_callback, wildcard)
        return self

    def set_foreground(self):
        """put the window in the foreground"""
        shell = win32com.client.Dispatch("WScript.Shell")
        shell.SendKeys('%')  # left shift key sent, this shifts focus from current window
        win32gui.SetForegroundWindow(self._handle)

Next, the file dialog is set using the class-based implementation (not the stand-alone shortcut):

def file_open_save_dialog(
    def_dir: str = ".", 
    title: str = "Open file", 
    filter: str = "file (*.*)", 
    mode: str = 'open'
) -> str:
    if (def_dir is None) or (def_dir == '.'):
        def_dir = os.getcwd()

    app = QApplication.instance()
    if app is None:
        app = QApplication([def_dir])

    file_dialog = QFileDialog()
    if mode == 'open':
        file_dialog.setAcceptMode(QFileDialog.AcceptMode.AcceptOpen)
    elif mode == 'save':
        file_dialog.setAcceptMode(QFileDialog.AcceptMode.AcceptSave)
    elif mode == 'folder':
        file_dialog.setFileMode(QFileDialog.FileMode.Directory)
    else:
        raise NameError(f"Invalid option given to FileDialog mode: {mode}")

    file_dialog.setNameFilter(filter)
    file_dialog.setDirectory(def_dir)
    file_dialog.setWindowModality(QtCore.Qt.WindowModality.ApplicationModal)
    file_dialog.setWindowTitle(title)
    file_dialog.show()
    WindowMgr().find_window_wildcard(title).set_foreground() # <- from above

    if file_dialog.exec():
        filenames = file_dialog.selectedFiles()
        return filenames[0]
    else:
        return ''

Example use:

File Open:

myopenfile = file_open_save_dialog(def_dir="C:\Temp", title="Select file Foo", filter="Text (*.txt)", mode='open')

File Save:

mysavefile = file_open_save_dialog(def_dir="C:\Temp", title="Save to file", filter="Text (*.txt)", mode='save')

Folder select:

myfolder = file_open_save_dialog(def_dir="C:\Temp", title="Select Folder", filter="*", mode='folder')

These are blocking functions. Use threading if you need your UI to remain active during the selection process.

RexBarker
  • 1,456
  • 16
  • 14