Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How can my app detect a change to another app's window?

In Cocoa on the Mac, I'd like to detect when a window belonging to another app is moved, resized, or repainted. How can I do this?

like image 904
Steve McLeod Avatar asked May 12 '09 17:05

Steve McLeod


1 Answers

You would need to use the Accessibility APIs, which are plain-C, located inside the ApplicationServices framework. For instance:

First you create an application object:

AXUIElementRef app = AXUIElementCreateApplication( targetApplicationProcessID );

Then you get the window from this. You can request the window list and enumerate, or you can get the frontmost window (look in AXAttributeConstants.h for all the attribute names you'd use).

AXUIElementRef frontWindow = NULL;
AXError err = AXUIElementCopyAttributeValue( app, kAXMainWindowAttribute, &frontWindow );
if ( err != kAXErrorSuccess )
    // it failed -- maybe no main window (yet)

Now you can request notification via a C callback function when a property of this window changes. This is a four-step process:

First you need a callback function to receive the notifications:

void MyAXObserverCallback( AXObserverRef observer, AXUIElementRef element,
                           CFStringRef notificationName, void * contextData )
{
    // handle the notification appropriately
    // when using ObjC, your contextData might be an object, therefore you can do:
    SomeObject * obj = (SomeObject *) contextData;
    // now do something with obj
}

Next you need an AXObserverRef, which manages the callback routine. This requires the same process ID you used to create the 'app' element above:

AXObserverRef observer = NULL;
AXError err = AXObserverCreate( applicationProcessID, MyObserverCallback, &observer );
if ( err != kAXErrorSuccess )
    // handle the error

Having got your observer, the next step is to request notification of certain things. See AXNotificationConstants.h for the full list, but for window changes you'll probably only need these two:

AXObserverAddNotification( observer, frontWindow, kAXMovedNotification, self );
AXObserverAddNotification( observer, frontWindow, kAXResizedNotification, self );

Note that the last parameter there is passing an assumed 'self' object as the contextData. This is not retained, so it's important to call AXObserverRemoveNotification when this object goes away.

Having got your observer and added notification requests, you now want to attach the observer to your runloop so you can be sent these notifications in an asynchronous manner (or indeed at all):

CFRunLoopAddSource( [[NSRunLoop currentRunLoop] getCFRunLoop],
                    AXObserverGetRunLoopSource(observer),
                    kCFRunLoopDefaultMode );

AXUIElementRefs are CoreFoundation-style objects, so you need to use CFRelease() to dispose of them cleanly. For cleanliness here, for example, you would use CFRelease(app) once you've obtained the frontWindow element, since you'll no longer need the app.

A note about Garbage-Collection: To keep an AXUIElementRef as a member variable, declare it like so:

__strong AXUIElementRef frontWindow;

This instructs the garbage collector to keep track of this reference to it. When assigning it, for compatibility with GC and non-GC, use this:

frontWindow = (AXUIElementRef) CFMakeCollectable( CFRetain(theElement) );
like image 199
Jim Dovey Avatar answered Sep 21 '22 11:09

Jim Dovey