Introduction
Modern C# and the .NET platform usually abstracts all the underlying platform stuff away (e.g. see Kestrel, Windows Forms, etc.). But isn't it cool to use the same basic primitives that the teams building the frameworks use? An additional perk is that you can have much more control over that! And it's fun.
Let's see how we can open a native window using Win32 API. And draw something on it.
In order to do that, we will need to follow the next steps:
- Configuring the project
- Registering the window class
- Defining WindowProc function
- Creating a window
- Adding a message loop
- Drawing something on the window
Full Program.cs code is available in the Win32CsharpCreateWindow github repository.
Configuring the project
Firstly, .NET relies on a mechanism called P/Invoke that communicates with the underlying platform. Usually, people call all the code that interacts with the platform - interop code. In my recent post, I showed how to generate the interop code for OBS. Luckily, there is already a Nuget package with generated interop code for Win32 called TerraFX.Interop.Windows. Let's add it to the project.
Additionally, we need to enable Unsafe code, so that we can use the pointers ( yeah, we will need to deal with pointers ).
That can be done by adding a simple AllowUnsafeBlocks
XML tag to your project file (.csproj):
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
Registering the window class
To create a window, one must use a window class to describe the window they are trying to create. Even though there are some system classes already there, but usually we would want to create our own one.
In Win32, we would use the function RegisterClassExW
to register the window class.
The function expects a single argument - pointer to the WNDCLASSEXW
structure that contains all the parameters.
It provides shared settings for every window created with that class.
These settings include a bunch of things, but one of the most important ones is the WndProc (Window Procedure) - a pointer to the function that will receive the messages sent to the window.
We will talk about it in the later section.
Let's imagine we created that function and called it WinProc
.
An additional caveat is that Windows expects UTF-16 encoded characters for the window class name. Luckily, .NET also stores characters as UTF-16 under the hood, so we can capitalise on that.
Another thing to consider, is that all managed objects (meaning things you create on the heap) can be moved by Garbage Collector (GC) at any point.
So we need to say to GC that it can't move the string when we pass it to Win32 API.
In .NET, that concept is called "pinning".
In C# we can use the fixed
keyword to achieve that.
With that knowledge, let's define the window class:
static unsafe HWND CreateWindow() { var className = "windowClass"; fixed (char* classNamePtr = className) // Hey, GC, please don't move the string { var windowClass = new WNDCLASSEXW(); windowClass.cbSize = (uint)sizeof(WNDCLASSEXW); // Size (in bytes) of WNDCLASSEXW structure windowClass.hbrBackground = HBRUSH.NULL; windowClass.hCursor = HCURSOR.NULL; windowClass.hIcon = HICON.NULL; windowClass.hIconSm = HICON.NULL; windowClass.hInstance = HINSTANCE.NULL; windowClass.lpszClassName = (ushort*)classNamePtr; // The UTF-16 window class name windowClass.lpszMenuName = null; windowClass.style = 0; windowClass.lpfnWndProc = &WinProc; // Pointer to WinProc function var classId = RegisterClassExW(&windowClass); } // ... }
Defining WindowProc function
One may wonder - why does Windows need a function pointer for my window to work? Here's the deal - the reason lies in the way the OS communicates with the created windows.
When events connected to the window occur in the operating system or when the OS expects the window to do something, Windows sends a Message to the window (more about messaging here).
- The user pressed the mouse button? Windows sends
WM_LBUTTONDOWN
message. - The user pressed something on the keyboard? Windows sends
WM_KEYDOWN
message. - Windows wants the window to redraw itself? Windows sends
WM_PAINT
message.
WinProc is the function that handles these messages and makes the window do something in response to them. It accepts the window, the message and some additional data related to the message.
Handling all messages in existence is very hard, so Win32 helpfully provides a default handler called DefWindowProcW
that you can call in the procedure.
One gotcha is that it expects you to handle WM_PAINT
(render requests) messages in any case, or it will send you them infinitely.
Thankfully, we can just do an empty draw call via BeginPaint
and EndPaint
function pair.
How can we pass the function as the pointer to the operating system? Well, for that .NET has a [UnmanagedCallersOnly]
attribute.
The attribute makes the compiler validate that the function can be called from unmanaged code safely (in this case, from the OS).
From that, let's define our WinProc function:
[UnmanagedCallersOnly] static unsafe LRESULT WinProc(HWND window, uint message, WPARAM wParam, LPARAM lParam) { // Handling WM_PAINT message if(message == WM.WM_PAINT) { var ps = new PAINTSTRUCT(); var deviceContextHandle = BeginPaint(window, &ps); EndPaint(window, &ps); return 0; // We successfully handled the message } // Ignoring everything else return DefWindowProcW(window, message, wParam, lParam); }
Creating a window
Creating the window is an easy part. We have already registered the class of our window, so all we need to decide now is the style, position and the size of our window.
There are numerous styles that can apply, but a sensible default is the WS_OVERLAPPEDWINDOW
(see here for more styles).
This kind of window has a title bar (with a window menu), border, a sizing border, a button to minimize and maximize the window.
We can make the window visible by default by adding the WS_VISIBLE
flag.
In addition to window styles (where we chose WS_OVERLAPPEDWINDOW
and WS_VISIBLE
), there are also extended window styles. But we are not interested in the latter right now, so we can just pass 0.
In this example, we will choose a 500x500 window opened at position (0, 0):
static unsafe HWND CreateWindow() { var className = "windowClass"; // ... registered window class var windowName = "windowName"; fixed (char* windowNamePtr = windowName) // Hey, GC, please don't move the string fixed (char* classNamePtr = className) // GC, do not move this one too { var width = 500; var height = 500; var x = 0; var y = 0; var styles = WS.WS_OVERLAPPEDWINDOW | WS.WS_VISIBLE; // The window style var exStyles = 0; // Extended styles that we do not care about return CreateWindowExW((uint)exStyles, (ushort*)classNamePtr, // UTF-16 window class name (ushort*)windowNamePtr, // UTF-16 window name (this will be in the title bar) (uint)styles, x, y, // Window initial position width, height, // Window initial size HWND.NULL, HMENU.NULL, HINSTANCE.NULL, null); } }
Adding a message loop
If we run the program at this point, it will exit instantly.
Even if we make it pause via something like while(true)
, it will not behave as we expect.
What is the solution?
When we defined the WinProc procedure, we mentioned that OS communicates with the window via messages. Sometimes, they go directly to the WinProc function. But in most cases, we need to "read" those messages from a message queue and pass them on to the function. The process of getting the messages from the message queue is called "the message loop".
To pick up a message, we need to call GetMessageW
.
Then, to pass this message to our WinProc, we need to call DispatchMessage
.
And that's it! (NB: if we cared about keyboard input, we would also need TranslateMessageW
before dispatching the message)
static unsafe void RunMessageLoop(HWND window) { MSG msg; // Get the message from the message queue while(GetMessageW(&msg, window, 0, 0)) { // Dispatch the message to the WinProc function DispatchMessageW(&msg); } }
Then we just need to call our CreateWindow
and RunMessageLoop
functions.
var window = CreateWindow();
RunMessageLoop(window);
Drawing something on the window
The window is rather bland. What if we could draw something a bit more interesting? Let's, for example, draw the last positions of the mouse which is moving across the window.
Our data structure is pretty trivial - a queue of last mouse positions and the maximum number of them that we should track.
static Queue<MousePoint> LastPositions = new(); const int MaxPositions = 200; record MousePoint(int X, int Y);
To fill the LastPositions
queue, let's handle WM_MOUSEMOVE
message in the WinProc function.
There, we would want to remember the position of the mouse we receive from the OS.
Since that new position should be drawn to the screen, we need to tell the OS that the window should be redrawn by using InvalidateRect
function.
[UnmanagedCallersOnly] static unsafe LRESULT WinProc(HWND window, uint message, WPARAM wParam, LPARAM lParam) { ... if (message == WM.WM_MOUSEMOVE) { var xPos = LOWORD(lParam); // horizontal position var yPos = HIWORD(lParam); // vertical position // Remember what was the last mouse position LastPositions.Enqueue(new MousePoint(xPos, yPos)); // Force OS to draw the window (and send us WM_PAINT message) InvalidateRect(window, null, BOOL.FALSE); return 0; } ... }
Now, we need to update the WM_PAINT handling. We need to do two things:
- Remove the tail by hiding points that were inserted long ago.
- Draw the pixels that are in the queue.
SetPixel
function allows us to do both of those things.
[UnmanagedCallersOnly] static unsafe LRESULT WinProc(HWND window, uint message, WPARAM wParam, LPARAM lParam) { // Handling WM_PAINT message if (message == WM.WM_PAINT) { var ps = new PAINTSTRUCT(); var deviceContextHandle = BeginPaint(window, &ps); // Hide the disappeared pixels by drawing them white // And removing them from the queue of last pixels while (LastPositions.Count > MaxPositions) { var extraPosition = LastPositions.Dequeue(); SetPixel(deviceContextHandle, extraPosition.X, extraPosition.Y, RGB(255, 255, 255)); } // Show the pixels in the queue by drawing them black foreach (var point in LastPositions) { SetPixel(deviceContextHandle, point.X, point.Y, RGB(0, 0, 0)); } EndPaint(window, &ps); return 0; // We successfully handled the message } ... }
And we have the final result (full Program.cs is in the Win32CsharpCreateWindow github repository):
Conclusions
Even though the Win32 API was designed for C, we can easily use it from C# via some interop bindings. In this way, we can take the best from two worlds: utilise the full power of the Windows platform we are developing on and use as much of type safety as C# can provide.