Connecting UWP apps hosted by ApplicationFrameHost to their real processes
Asked Answered
Z

4

12

I am working on an WPF application to monitor my activities on my computer. I use Process.GetProcesses() and some filtering to get the processes I am interested in (example:Calculator) then I record their StartTime. I am also using WIN32/USER32 API method GetForegroundWindow() to get the window the user is using.

The problem is that when the windows are Windows/UWP applications they are always hosted by the process ApplicationFrameHost. So the GetForegroundWindow() method returns that window with a title (example:Calculator), but not the real process being hosted.

What I need is either another way to get the foreground window that includes the real process being hosted, or some way to connect the window to process.

Anyone that knows how to accomplish this? All help would be really appreciated.

Zambia answered 26/9, 2016 at 12:22 Comment(0)
Z
20

I eventually found a way to do this, so I am going answer my own question so maybe someone in the future with the same problem could find it useful.

This is the class with the WinApiFunctions:

public class WinAPIFunctions
{
    //Used to get Handle for Foreground Window
    [DllImport("user32.dll", CharSet = CharSet.Auto)]
    private static extern IntPtr GetForegroundWindow();

    //Used to get ID of any Window
    [DllImport("user32.dll", CharSet = CharSet.Auto)]
    private static extern int GetWindowThreadProcessId(IntPtr hWnd, out int lpdwProcessId);
    public delegate bool WindowEnumProc(IntPtr hwnd, IntPtr lparam);

    [DllImport("user32.dll")]
    [return: MarshalAs(UnmanagedType.Bool)]
    public static extern bool EnumChildWindows(IntPtr hwnd, WindowEnumProc callback, IntPtr lParam);

    public static int GetWindowProcessId(IntPtr hwnd)
    {
        int pid;
        GetWindowThreadProcessId(hwnd, out pid);
        return pid;
    }

    public static IntPtr GetforegroundWindow()
    {
        return GetForegroundWindow();
    }
}

And this is the class I used to test if it would work. I used it in a simple console program that just writes out the name of the process that has current focus:

class FindHostedProcess
{
    public Timer MyTimer { get; set; }
    private Process _realProcess;
    public FindHostedProcess()
    {
        MyTimer = new Timer(TimerCallback, null, 0, 1000);
        Console.ReadKey();
    }

    private void TimerCallback(object state)
    {
        var foregroundProcess = Process.GetProcessById(WinAPIFunctions.GetWindowProcessId(WinAPIFunctions.GetforegroundWindow()));
        if (foregroundProcess.ProcessName == "ApplicationFrameHost")
        {
            foregroundProcess = GetRealProcess(foregroundProcess);
        }
        Console.WriteLine(foregroundProcess.ProcessName);
    }

    private Process GetRealProcess(Process foregroundProcess)
    {
        WinAPIFunctions.EnumChildWindows(foregroundProcess.MainWindowHandle, ChildWindowCallback, IntPtr.Zero);
        return _realProcess;
    }

    private bool ChildWindowCallback(IntPtr hwnd, IntPtr lparam)
    {
        var process = Process.GetProcessById(WinAPIFunctions.GetWindowProcessId(hwnd));
        if (process.ProcessName != "ApplicationFrameHost")
        {
            _realProcess = process;
        }
        return true;
    }
}
Zambia answered 5/10, 2016 at 15:0 Comment(1)
You are a hero!! I was struggling a lot with this.Pensioner
U
3

Chris, there is the alternative way which I have discovered trying to apply your solution to a related problem. While trying to analyse how the ApplicationFrameHost.exe-related stuff worked, I have stumbled upon the documented way of getting the true foreground window / thread and its process by passing 0 instead of the actual thread ID to GetGUIThreadInfo.

It might not fully work for the edge-case scenarios of your problem, but I felt that this might be a useful contribution for people of the future who might face the same problems ;-)

Here is the example of how this can be applied (pseudo C++'ish code):

GUITHREADINFO gti = { sizeof(GUITHREADINFO) };
GetGUIThreadInfo(0, &gti); // <- note the `0`

DWORD processId = 0;
GetWindowThreadProcessId(gti.hwndFocus, &processId);

const auto procName = Util::GetProcessName(processId);

It solved my problem (obtaining the actual keyboard layout + finding the real foreground window) for all more-or-less common apps I have tested it against.

Unionize answered 21/8, 2018 at 9:57 Comment(0)
F
0

Basically, the answer you provided will also fail if the active window is in full screen mode,

for example if you have Skype app opened along with Microsoft Remote Desktop, which is the active one and on full screen mode, EnumChildWindows would return SkypeApp not RDPClient.

and to get this fixed, you should follow the workaround suggested by Ivanmoskalev,

check this out:

[DllImport("user32.dll")]
[return: MarshalAs(UnmanagedType.Bool)]
public static extern bool GetGUIThreadInfo(uint idThread, ref GUITHREADINFO lpgui);

public static IntPtr getThreadWindowHandle(uint dwThreadId)
{
    IntPtr hWnd;

    // Get Window Handle and title from Thread
    var guiThreadInfo = new GUITHREADINFO();
    guiThreadInfo.cbSize = Marshal.SizeOf(guiThreadInfo);

    GetGUIThreadInfo(dwThreadId, ref guiThreadInfo);

    hWnd = guiThreadInfo.hwndFocus;
    //some times while changing the focus between different windows, it returns Zero so we would return the Active window in that case
    if (hWnd == IntPtr.Zero)
    {
        hWnd = guiThreadInfo.hwndActive;
    }
    return hWnd;
}

[DllImport("user32.dll", CharSet = System.Runtime.InteropServices.CharSet.Auto)]
public static extern int GetWindowThreadProcessId(IntPtr windowHandle, out int processId);

static void Main(string[] args)
{
    var current = getThreadWindowHandle(0);
    int processId = 0;
    GetWindowThreadProcessId(current, out processId);
    var foregroundProcess = GetActiveProcess(processId);
}

private static Process GetActiveProcess(int activeWindowProcessId)
{
    Process foregroundProcess = null;
    try
    {
        foregroundProcess = Process.GetProcessById(activeWindowProcessId);
    }
    catch (Exception ex)
    {
        Console.WriteLine(ex);
    }
    if (string.IsNullOrWhiteSpace(GetProcessNameSafe(foregroundProcess)))
    {
        var msg = "Process name is empty.";
        Console.WriteLine(msg);
    }
    return foregroundProcess;
}
Fid answered 26/3, 2019 at 8:58 Comment(0)
F
0

About the same code from Chris Johnsson except that I'm not using Process because it doesn't work very well on the UWP app.

Code for start:

new Timer(TimerCallback, null, 0, 1000);

void TimerCallback(object state)
{
    var process = new ProcessUtils.FindHostedProcess().Process;
    string name = string.Empty;

    if (process.IsPackaged)
    {
        var apps = process.GetAppDiagnosticInfos();
        if (apps.Count > 0)
            name = apps.First().AppInfo.DisplayInfo.DisplayName;
        else
            name = System.IO.Path.GetFileNameWithoutExtension(process.ExecutableFileName);
    }
    else
        name = System.IO.Path.GetFileNameWithoutExtension(process.ExecutableFileName);

    Debug.WriteLine(name);
}

And the FindHostedProcess class where I use ProcessDiagnosticInfo instead of Process

public class FindHostedProcess
{
    public ProcessDiagnosticInfo Process { get; private set; }

    public FindHostedProcess()
    {
        var foregroundProcessID = WinAPIFunctions.GetforegroundWindow();
        Process = ProcessDiagnosticInfo.TryGetForProcessId((uint)WinAPIFunctions.GetWindowProcessId(foregroundProcessID));

        // Get real process
        if (Process.ExecutableFileName == "ApplicationFrameHost.exe")
            WinAPIFunctions.EnumChildWindows(foregroundProcessID, ChildWindowCallback, IntPtr.Zero);
    }

    private bool ChildWindowCallback(IntPtr hwnd, IntPtr lparam)
    {
        var process = ProcessDiagnosticInfo.TryGetForProcessId((uint)WinAPIFunctions.GetWindowProcessId(hwnd));

        if (process.ExecutableFileName != "ApplicationFrameHost.exe")
            Process = process;

        return true;
    }
}

Finally reuse Chris Johnson's WinAPIFunctions class.

Frication answered 12/4, 2020 at 13:11 Comment(2)
Is this UWP only? There is ProcessDiagnosticInfo under System.Diagnostics in WinForms application learn.microsoft.com/en-us/dotnet/api/…Keeton
This is part of the WinRT Api learn.microsoft.com/fr-fr/uwp/api/…. You can use it on WinForm but your app will only run on Windows 10/11 learn.microsoft.com/en-us/windows/apps/desktop/modernize/…Frication

© 2022 - 2024 — McMap. All rights reserved.