.NET/Link Example: WindowSpy (Using the Win32 API)

This example demonstrates calling "unmanaged" Windows DLLs (that is, an old C-style DLLs, not .NET assemblies) from Mathematica using DefineDLLFunction. The DLL used is one from the Win32 API, the set of functions that Windows programs written in C or C++ call directly. Although most of the capabilities programmers need for programming on Windows are available via classes in the .NET Framework, there are some things that can only be done by getting right down to the level of the raw Win32 API. If you need to do it, it's nice to know that it is very simple.

Although it might seem farfetched, this example demonstrates that Mathematica can realistically be used as a systems programming language for Windows. In fact, using DefineDLLFunction to import Windows API functions into Mathematica, you could write an entire native Win32 application in Mathematica!

This example presents a function called WindowSpy, which will remind users of the "Find Window" functionality in the Spy++ utility program that has long been bundled with Microsoft's Visual C++ compiler. The WindowSpy program lets you move the mouse around the screen as it prints out information on the windows that it enters. These windows can belong to any program.

To try this example, evaluate all the initialization cells (you can do this with the menu command Kernel/Evaluation/Evaluate Initialization). Then go to the Example section.

Code and Explanation

Needs["NETLink`"]

Declaring the Windows API Functions

Here are the C-language prototypes for the functions we need, straight out of the Microsoft documentation. All these functions are found in user32.dll:

BOOL GetWindowRect( HWND hWnd, LPRECT lpRect );
int GetWindowText( HWND hWnd, LPTSTR lpString, int nMaxCount );
int GetClassName( HWND hWnd, LPTSTR lpClassName, int nMaxCount );
HWND WindowFromPoint( POINT point );
BOOL GetCursorPos( LPPOINT lpPoint );
SHORT GetKeyState( int nVirtKey );

Before we work through the explanations of what type names to use in DefineDLLFunction to call these functions from Mathematica, note that the easiest thing to do is to search on the internet for sample code in VB. NET or C# that shows declarations for the external functions, and just copy the type names. In Visual Basic .NET, external functions are declared using Declare Function, and in C# they use the extern modifier along with the DLLImport attribute. You can use those terms, along with the DLL function name, as keywords in your search.

DefineDLLFunction understands common Windows types like HWND, BOOL, DWORD, SHORT, etc., so we can use those names directly. The simplest function is GetKeyState:

GetKeyState = DefineDLLFunction["GetKeyState", "user32.dll", "SHORT", {"int"}];

Of course, there are many other ways to specify the type names for all of these functions. Here are some examples for GetKeyState:

(* C- or C#-style type names. *)
   GetKeyState = DefineDLLFunction["GetKeyState", "user32.dll", "short", {"int"}];
(* .NET Framework type names. *)
   GetKeyState = DefineDLLFunction["GetKeyState", "user32.dll", "Int16", {"Int32"}];
(* Visual Basic type names. *)
   GetKeyState = DefineDLLFunction["GetKeyState", "user32.dll", "Short", {"ByVal key As Integer"}];

WindowFromPoint takes a POINT structure. How does this map to .NET? Luckily there is a .NET Point struct that has the same memory layout as the Windows POINT struct. This lets us use the .NET type directly. The return value is an HWND, and DefineDLLFunction understands this type name, so you can use it directly (it is mapped to IntPtr, the .NET class for a generic pointer). Note that because the type System.Drawing.Point is from an assembly that is probably not yet loaded, you use the ReferencedAssemblies option to specify what assembly the type is found in. Alternatively, you can just call LoadNETAssembly["System.Drawing"] in advance to load the needed assembly.

WindowFromPoint = DefineDLLFunction["WindowFromPoint", "user32.dll", "HWND", {"System.Drawing.Point"}, ReferencedAssemblies->{"System.Drawing"}];

The previous line causes the System.Drawing assembly to be loaded, so we could leave out the ReferencedAssemblies option in subsequent declarations that also use the System.Drawing assembly, although we leave it in for completeness.

GetCursorPos takes an LPPOINT, which is a pointer to a POINT, because it needs to fill in the coordinates. In .NET terms, this maps to a Point passed by reference. In general, making a parameter out or ref (ByRef in VB notation) is equivalent to adding one level of indirection. This means that a pointer to a POINT is a Point passed by reference. A "by reference" type in the .NET framework is designated with an & at the end:

GetCursorPos = DefineDLLFunction["GetCursorPos", "user32.dll", "BOOL", {"System.Drawing.Point&"}, ReferencedAssemblies->{"System.Drawing"}];

Here are some alternative ways to write that declaration:

(* C# syntax *)
GetCursorPos = DefineDLLFunction["GetCursorPos", "user32.dll", "bool", {"ref System.Drawing.Point"}, ReferencedAssemblies->{"System.Drawing"}];
(* VB syntax *)
GetCursorPos = DefineDLLFunction["GetCursorPos", "user32.dll", "Boolean", {"ByRef pt As System.Drawing.Point"}, ReferencedAssemblies->{"System.Drawing"}];

GetClassName and GetWindowText both take an LPTSTR, which is basically a string. However, these functions treat this parameter not as an "in" value, but as an "out" value--they write into the string as a means of supplying the desired result. In the typical case where a DLL function takes a string as an "in" argument, you can use any of the type names char*, string, or ByValue s As String, but these won't work to propagate a value back out of the call, as required here. Instead, you use the .NET class System.Text.StringBuilder. This is a standard .NET trick that is documented anywhere you find examples of passing strings to DLL functions.

GetWindowText = DefineDLLFunction["GetWindowText", "user32.dll", "int", {"HWND", "System.Text.StringBuilder", "int"}];
GetClassName = DefineDLLFunction["GetClassName", "user32.dll", "int", {"HWND", "System.Text.StringBuilder", "int"}];

The last function we need is GetWindowRect. Like GetCursorPos, it takes a pointer to a struct (LPRECT). .NET has a Rectangle struct that is the same number of bytes as the Windows RECT struct, but unfortunately it doesn't have the same layout of fields. A RECT is stored as 4 ints: (left, top, right, bottom), whereas a Rectangle is stored as (left, top, width, height). Because the structures are the same size in memory we can use the .NET Rectangle as the type, but we must to do a little translating of the fields when we actually need to use their values, as can be seen later on.

GetWindowRect = DefineDLLFunction["GetWindowRect", "user32.dll", "BOOL", {"HWND", "System.Drawing.Rectangle&"}, ReferencedAssemblies->{"System.Drawing"}];

The WindowSpy Function

Now we  are ready to write the main WindowSpy function.

WindowSpy[] :=
	NETBlock[
		Module[{rect, pt, sb1, sb2, dummyForm, lastWnd, curWnd},
			(* This is the Rectangle we need to pass in to GetWindowRect. It is passed by reference and its fields
			   are filled in by the function call.
			*)
			rect = NETNew["System.Drawing.Rectangle",0,0,0,0];
			(* This is the Point we need to pass in to GetCursorPos. It is passed by reference and its fields
			   are filled in by the function call.
			*)
			pt = NETNew["System.Drawing.Point"];
			(* We need two StringBuilders to pass in to the two calls that return text in an out parameter.
			   We create them to hold 100 chars and then pass 100 as the max string length to the functions.
			*)
			sb1 = NETNew["System.Text.StringBuilder", 100];
			sb2 = NETNew["System.Text.StringBuilder", 100];
			(* It seems that for proper detection of the mouse click that terminates the function, we need
			   to have a form visible. Therefore we create one and position it offscreen.
			*)
			dummyForm = NETNew["System.Windows.Forms.Form"];
			dummyForm@Location = NETNew["System.Drawing.Point", -1000, -1000];
			(* Need to use FormStartPosition->Manual to force the form to respect the Location we just set. *)
			ShowNETWindow[dummyForm, FormStartPosition->Manual];
			
			While[True,
				(* This means "if left mouse button was clicked, break out of the loop." *)
				If[GetKeyState[1] < 0, Break[]];
				GetCursorPos[pt];
				curWnd = WindowFromPoint[pt];
				(* The HWND is returned by WindowFromPoint is an IntPtr, but for comparing one IntPtr
				   to another it is easiest to simply convert them to ints.
				*)
				If[curWnd@ToInt32[] =!= lastWnd@ToInt32[],
					GetClassName[curWnd, sb1, 100];
					GetWindowText[curWnd, sb2, 100];
					GetWindowRect[curWnd, rect];
					Print[StringForm["HWND: `1`     ClassName: `2`       Text: `3`       Top: `4`  Left: `5`  Width: `6`  Height: `7`",
								curWnd@ToInt32[], sb1@ToString[], sb2@ToString[],
								rect@X, rect@Y, rect@Width - rect@X, rect@Height - rect@Y]];
					lastWnd = curWnd
				]
			];
			dummyForm@Dispose[]
		]
	]

Example

WindowSpy[]

Without clicking the mouse, move the mouse around the screen to see information printed about each of the windows it enters. If you are not familiar with Windows programming, you'll be interested to see how literally everything on the screen, even scroll bars and menu bars, is a window. To exit the program, click the mouse.