Kostiantyn's Blog

Win32 API: How to open a window from C#

A cup
Windows.
Photo by Tadas Sar on Unsplash

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:

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):

xml
    <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:

c#
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:

c#
[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):

c#
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)

c#
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.

c#
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.

c#
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.

c#

[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.

c#
[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.