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

You were right - skipping the windows that caused the error helped. However, for some reason after your code completes the param['hwnd'] is still None. I didn’t investigate further and ended up using a workaround: before opening the explorer I bring the console window to front and activate it. This way the explorer’s window appears on top (though, the console window is now in between the explorer and the web browser). Thank you for your effort.

Explorer should have at least the base name of the opened path, whatever it is, in the title bar text. It works like a charm for me when I open an Explorer window for “C:\Temp”. Of course, one does have to wait for Explorer to create the window. If you enumerate the top-level windows immediately after executing Explorer, it probably won’t find anything.

Thank you for clarifying, I think I was executing this code immediately after issuing the command for explorer.

You might not be able to depend on manipulating the console window. The most common case that’s a concern is when a pseudoconsole (i.e. ConPTY) is used, such as Windows Terminal. In a pseudoconsole session, GetConsoleWindow() returns a handle for a fake console window that’s invisible and empty. In earlier versions of the pseudoconsole implementation, basic ShowWindow() operations on this fake window had no effect. In April of last year, Michael Niksa implemented a backend to relay some window operations back to the ConPTY terminal. He wrote up the design details in the document “Show Hide operations on GetConsoleWindow via PTY”. You should verify that your approach works when running under Windows Terminal. This matters because Windows Terminal can be set as the default for all console sessions in Windows 11, and it’s increasingly popular.

There are a couple other noteworthy factors regarding GetConsoleWindow(). The first is that the console session may have been created with the flag CREATE_NO_WINDOW, in which case GetConsoleWindow() returns NULL. The second is that maybe your script doesn’t have a console session, which is the case if it’s run via “pythonw.exe” or created with the flag DETACHED_PROCESS. The second case can be addressed, if you so wish, by calling AllocConsole().

Thanks for the additional information. As far as I understand, I should not face such issues for two reasons:

  1. I can control the way my app is being launched. I even ship my app along with the console which should be used (currently this is ConEmu).
  2. My software is for non-programmers, so they have no interest to experiment with different consoles and ways to start the app.

This allows me to adjust the code for the specific console (even for the specific version of it). Namely, what I had to do was get the current process ID and then move up the hierarchy a couple of times to find the needed parent process which owns the visible console window.