Bring the subprocess' window to front

I have a python console app which runs on WIndows. It also has a web GUI which runs in the browser. In my GUI I have buttons which when clicked should open a folder in the Windows file explorer. So, when the button is clicked, I send a command from the browser to the backend (the python console app) which in turn should open the needed file.

The problem is that the explorer’s window appears under the browser’s window (though in front of the console’s window):

[Browser window] (top window)
[Explorer window]
[Console window]

What I’ve tried

I’ve found out that you can bring the window to front using win32 API. Though, you need to know the HWND - the window handle (ID).

I’m opening the file explorer using subprocess, which provides the PID of the spawned process. Knowing the PID we can find all the windows of the process like so:

import win32gui
import win32process
import subprocess
import time


def enumHandler(hwnd, lParam):    
    thread_id, process_id = win32process.GetWindowThreadProcessId(hwnd)
    if process_id == opened_pid:
        print("Found a window:")
        print(hwnd, win32gui.GetWindowText(hwnd))


with subprocess.Popen("notepad.exe") as proc:
    opened_pid = proc.pid
    time.sleep(2)
    win32gui.EnumWindows(enumHandler, None)

The problem with my approach

This works well if the command is "notepad.exe", for example. However, it won’t find any windows if I try the command that I want, which is r"explorer path\to\folder".
I suspect that with a command like this I’m getting the wrong PID from proc.pid. Therefore, my script cannot find any windows associated with it.

Running explorer "path\to\folder" will reuse the session’s main Explorer process, unless you enable the option to “launch folder windows in a separate process”. Even with the latter option, Explorer won’t use an arbitrarily spawned instance. It uses the DCOM launcher service to respawn itself, and the first instance exits immediately.

I’d require the session to have a desktop shell (not headless) and require this shell to be “explorer.exe”. This includes the vast majority of Windows installations, but it’s trivial to check, so it can’t hurt. The function GetShellWindow() returns the handle of the shell window, if there is one.

For the enumeration of top-level windows, if you know it’s a window that’s owned by Explorer, it should have at least the base folder name in the window title.

For example:

import os
import sys
import ctypes

import win32con
import win32api
import win32process
import win32gui

user32 = ctypes.WinDLL('user32', use_last_error=True)
user32.GetShellWindow.restype = ctypes.c_void_p
def get_window_process_path(hwnd):
    _, pid = win32process.GetWindowThreadProcessId(hwnd)
    hprocess = win32api.OpenProcess(
                    win32con.PROCESS_QUERY_LIMITED_INFORMATION, False, pid)
    try:
        return win32process.GetModuleFileNameEx(hprocess, None)
    finally:
        hprocess.close()  

def enum_func(hwnd, param): 
    if param['hwnd'] is None:
        path = os.path.normcase(get_window_process_path(hwnd))
        if path == param['path']:
            text = win32gui.GetWindowText(hwnd)
            if os.path.normcase(os.path.basename(text)) == param['text']:
                param['hwnd'] = hwnd
    return True
shell_name = ''
shell_hwnd = user32.GetShellWindow()
if shell_hwnd:
    shell_path = get_window_process_path(shell_hwnd)
    shell_name = os.path.basename(shell_path)
if os.path.normcase(shell_name) != 'explorer.exe':
    sys.exit('The session shell must be Explorer.')

param = {
    'path': os.path.normcase(shell_path), 
    'text': os.path.normcase(os.path.basename('C:\\Temp')), 
    'hwnd': None,
}

win32gui.EnumWindows(enum_func, param)
>>> win32gui.GetWindowText(param['hwnd'])
'Temp'
1 Like

Thank you for this creative solution! Unfortunately, as of now it fails for my app with the following error:
error: (5, 'OpenProcess', 'Access is denied.')

I googled a little and some people suggest using PROCESS_QUERY_LIMITED_INFORMATION, which your solution already does. I will try to investigate further why this happens.

There’s a window in your desktop session that’s owned by a process that restricts access. You can modify the function to continue to the next window. For example:

import pywintypes

def enum_func(hwnd, param): 
    if param['hwnd'] is None:
        try:
            path = os.path.normcase(get_window_process_path(hwnd))
        except pywintypes.error:
            return True
        if path == param['path']:
            text = win32gui.GetWindowText(hwnd)
            if os.path.normcase(os.path.basename(text)) == param['text']:
                param['hwnd'] = hwnd
    return True