KenD
KenD

Reputation: 5318

Get list of all open windows in .Net Core running on macOS (via NSApplication?)

Getting a list of open windows in .Net Framework on Windows was relatively easy. How can I do the same in .Net Core/.Net 5 or later on macOS?

To clarify, I'm looking for a way to retrieve a list of all open windows owned by any running application/process. I don't have much experience of macOS development - I'm a Windows developer - but I've tried to use the NSApplication as suggested by this answer.

I created a .Net 6.0 Console application in VS2022 on macOS Monterey (12.2), added a reference to Xamarin.Mac and libxammac.dylib as described here - which describes doing this in Xamarin rather than .Net, but I don't see any other option to create a Console application. With the simple code:

 static void Main(string[] args)
    {
        NSApplication.Init();
    }

I get the output

Xamarin.Mac: dlopen error: dlsym(RTLD_DEFAULT, mono_get_runtime_build_info): symbol not found

I've no idea what this means. I'm not even sure this approach has any merit.

Does anyone know if it's possible to use NSApplication from a .Net Core/6.0 application, and if so whether NSApplication will give me the ability to read a system-wide list of open windows? If not, is there another way to accomplish this?

This is only for my own internal use, it doesn't need to be in any way portable or stable outside of my own environment.

Upvotes: 7

Views: 785

Answers (1)

Stephan Schlecht
Stephan Schlecht

Reputation: 27106

In the link you refer to, there is an important note:

... as Xamarin.Mac.dll does not run under the .NET Core runtime, it only runs with the Mono runtime.

Because you try to run Xamarin.Mac.dll under .net-core, you get this dlopen error.

No System-wide List via NSApplication

The linked answer with NSApplication.shared.windows is incorrect if you want to read a system-wide list of open windows. It can only be used to determine all currently existing windows for the application from which the call is made, see Apple's documentation.

Alternative solution

Nevertheless, there are several ways to access the Window information in macOS. One of them could be a small unmanaged C-lib that gets the necessary information via CoreFoundation and CoreGraphics and returns it to C# via Platform Invoke (P/Invoke).

Native Code

Here is example code for a C-Lib that determines and returns the names of the window owners.

WindowsListLib.h

extern char const **windowList(void);
extern void freeWindowList(char const **list);

The interface of the library consists of only two functions. The first function called windowList returns a list with the names of the window owners. The last element of the list must be NULL so that you can detect where the list ends on the managed C# side. Since the memory for the string list is allocated dynamically, you must use the freeWindowList function to free the associated memory after processing.

WindowsListLib.c

#include "WindowListLib.h"
#include <CoreFoundation/CoreFoundation.h>
#include <CoreGraphics/CoreGraphics.h>

static void errorExit(char *msg) {
    fprintf(stderr, "%s\n", msg);
    exit(1);
}

static char *copyUTF8String(CFStringRef string) {
    CFIndex length = CFStringGetLength(string);
    CFIndex size = CFStringGetMaximumSizeForEncoding(length, kCFStringEncodingUTF8) + 1;
    char *buf = malloc(size);
    if(!buf) {
        errorExit("malloc failed");
    }
    if(!CFStringGetCString(string, buf, size, kCFStringEncodingUTF8)) {
        errorExit("copyUTF8String with utf8 encoding failed");
    }
    return buf;
}


char const **windowList(void) {
    CFArrayRef cfWindowList = CGWindowListCopyWindowInfo(kCGWindowListOptionOnScreenOnly,  kCGNullWindowID);
    CFIndex count = CFArrayGetCount(cfWindowList);
    char const **list = malloc(sizeof(char *) * (count + 1));
    if(!list) {
        errorExit("malloc failed");
    }
    list[count] = NULL;
    for(CFIndex i = 0; i < count; i++) {
        CFDictionaryRef windowInfo = CFArrayGetValueAtIndex(cfWindowList, i);
        CFStringRef name = CFDictionaryGetValue(windowInfo, kCGWindowOwnerName);
        if(name) {
            list[i] = copyUTF8String(name);
        } else {
            list[i] = strdup("unknown");
        }
    }
    CFRelease(cfWindowList);
    return list;
}

void freeWindowList(char const **list) {
    const char **ptr = list;
    while(*ptr++) {
        free((void *)*ptr);
    }
    free(list);
}

CGWindowListCopyWindowInfo is the actual function that gets the window information. It returns a list of dictionaries containing the details. From this we extract kCGWindowOwnerName. This CFStringRef is converted to a dynamically allocated UTF-8 string by the function copyUTF8String.

By convention, calls like CGWindowListCopyWindowInfo that contain the word copy (or create) must be released after use with CFRelease to avoid creating memory leaks.

C# Code

The whole thing can then be called on the C# side something like this:

using System.Runtime.InteropServices;

namespace WindowList
{
    public static class Program
    {
        [DllImport("WindowListLib", EntryPoint = "windowList")]
        private static extern IntPtr WindowList();

        [DllImport("WindowListLib", EntryPoint = "freeWindowList")]
        private static extern void FreeWindowList(IntPtr list);

        private static List<string> GetWindows()
        {
            var nativeWindowList = WindowList();
            var windows = new List<string>();
            var nativeWindowPtr = nativeWindowList;
            string? windowName;
            do
            {
                var strPtr = Marshal.ReadIntPtr(nativeWindowPtr);
                windowName = Marshal.PtrToStringUTF8(strPtr);
                if (windowName == null) continue;
                windows.Add(windowName);
                nativeWindowPtr += Marshal.SizeOf(typeof(IntPtr));
            } while (windowName != null);

            FreeWindowList(nativeWindowList);
            return windows;
        }


        static void Main()
        {
            foreach (var winName in GetWindows())
            {
                Console.WriteLine(winName);
            }
        }
    }
}

The GetWindows method fetches the data via a native call to WindowList and converts the C strings to managed strings, then releases the native resources via a call to FreeWindowList.

This function returns only the owner names, such as Finder, Xcode, Safari, etc. If there are multiple windows, the owners will also be returned multiple times, etc. The exact logic of what should be determined will probably have to be changed according to your requirements. However, the code above should at least show a possible approach to how this can be done.

Screenshot

demo

Upvotes: 7

Related Questions