Win32 Interop and Native Access

📖 8 min read

Why Win32 Interop Still Matters

WinUI 3 is built on the Windows App SDK and runs as a standard Win32 process. That architectural reality means the entire Win32 API surface is available to your application, and certain scenarios require it. Windows Runtime APIs cover a broad range of application needs, but they do not expose everything. File pickers require an owner window handle to display correctly on multi-monitor setups. System tray integration relies on Shell_NotifyIcon, a Win32 Shell API with no WinRT equivalent. Setting a custom window icon programmatically at runtime involves posting WM_SETICON messages directly to the window procedure. Global keyboard shortcuts use RegisterHotKey. These are not obscure edge cases; they appear regularly in desktop application development.

Understanding how WinUI 3 bridges managed code to native Win32 lets you work confidently at each layer, choosing the right tool for each situation rather than fighting the platform.


HWND Access

Every WinUI 3 window has an underlying Win32 window handle, called an HWND. The WindowNative.GetWindowHandle method from the WinRT.Interop namespace retrieves it.

using WinRT.Interop;

var hwnd = WindowNative.GetWindowHandle(this); // 'this' is your Window

You call this method when a Win32 or COM API requires a parent or owner window. The most common case is the file picker. WinUI 3’s FileOpenPicker and FileSavePicker include an Initialize method that accepts the HWND, which tells the dialog which window owns it and ensures correct positioning and z-order behavior.

var picker = new FileOpenPicker();
InitializeWithWindow.Initialize(picker, WindowNative.GetWindowHandle(this));
picker.FileTypeFilter.Add(".txt");
StorageFile file = await picker.PickSingleFileAsync();

Without initializing the picker with the HWND, the dialog may appear behind the application window or fail on some system configurations. The same pattern applies to FolderPicker, FileSavePicker, and several other WinRT pickers and dialogs that display native windows.


CsWin32 Source Generator

Writing PInvoke declarations by hand is tedious and error-prone. The CsWin32 source generator, available as the Microsoft.Windows.CsWin32 NuGet package, automates this entirely. You add the package to your project and create a file called NativeMethods.txt in the project root. Each line in that file names a Win32 function, constant, or type you want to use.

LoadImage
SendMessage
Shell_NotifyIconW
RegisterHotKey
UnregisterHotKey
DestroyIcon
NOTIFYICONDATAW
WM_SETICON
WM_HOTKEY
IMAGE_ICON

CsWin32 reads that file at build time and generates a static PInvoke class containing correctly-typed, safe wrappers for each requested symbol. The generated code handles marshaling, SAL annotations from the Windows headers, and safe handle types automatically. You call the generated wrappers exactly as you would call any other C# method.

// Generated by CsWin32 from NativeMethods.txt
var hIcon = PInvoke.LoadImage(
    hInst: HINSTANCE.Null,
    name: "Assets/app.ico",
    type: GDI_IMAGE_TYPE.IMAGE_ICON,
    cx: 32, cy: 32,
    fuLoad: IMAGE_FLAGS.LR_LOADFROMFILE);

PInvoke.SendMessage(
    new HWND(hwnd),
    PInvoke.WM_SETICON,
    new WPARAM(0), // ICON_SMALL
    new LPARAM(hIcon.Value.Value));

CsWin32 significantly reduces the surface area for interop bugs. The generated types mirror the Win32 type system closely, so you pass an HWND where Win32 expects an HWND rather than casting an IntPtr and hoping you got the size right. For most Win32 interop in a WinUI 3 application, CsWin32 should be the first tool you reach for.


Manual PInvoke

CsWin32 covers a large portion of the Win32 API surface, but some older or less-documented functions may not be included. For those cases, you write PInvoke declarations manually using LibraryImport (preferred in .NET 7+) or DllImport.

using System.Runtime.InteropServices;

internal static partial class NativeMethods
{
    [LibraryImport("user32.dll", SetLastError = true)]
    internal static partial nint SetWindowLongPtrW(nint hWnd, int nIndex, nint dwNewLong);

    [LibraryImport("user32.dll", SetLastError = true)]
    internal static partial nint GetWindowLongPtrW(nint hWnd, int nIndex);
}

LibraryImport uses source generation rather than runtime marshaling, which improves performance and reduces AOT-incompatibility risks. DllImport remains valid and is often easier to read for simple declarations, but LibraryImport is the forward-looking choice.

When dealing with native resources like icons, brushes, or GDI objects, wrap them in a SafeHandle subclass rather than storing raw handles as IntPtr. SafeHandle ensures the underlying resource is freed even if an exception occurs, and it participates correctly in garbage collection finalization.

internal sealed class SafeIconHandle : SafeHandleZeroOrMinusOneIsInvalid
{
    public SafeIconHandle() : base(ownsHandle: true) { }

    protected override bool ReleaseHandle()
    {
        return PInvoke.DestroyIcon(new HICON(handle));
    }
}

Marshaling considerations become important when passing strings or structures across the managed/native boundary. Prefer [MarshalAs(UnmanagedType.LPWStr)] for Unicode strings in DllImport scenarios. With LibraryImport, you annotate string parameters with [MarshalAs(UnmanagedType.LPWStr)] or use MemoryMarshal utilities for performance-critical paths.


COM Interop

A meaningful portion of the Windows platform is still exposed through COM rather than WinRT. Shell APIs like ITaskbarList3 for taskbar progress indicators, IFileOperation for file system operations with progress reporting, and various legacy third-party components use COM interfaces. WinUI 3 applications can call COM components through Runtime Callable Wrappers or by defining COM interface projections directly in C#.

For COM interfaces defined in Windows, you can often find existing ComImport definitions in the community or define them yourself. The pattern involves declaring the interface with [ComImport], [Guid], and [InterfaceType] attributes, then obtaining an instance through CoCreateInstance or by casting an object that implements the interface.

[ComImport]
[Guid("ea1afb91-9e28-4b86-90e9-9e9f8a5eefaf")]
[InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
internal interface ITaskbarList3
{
    void HrInit();
    void AddTab(IntPtr hwnd);
    void DeleteTab(IntPtr hwnd);
    void ActivateTab(IntPtr hwnd);
    void SetActiveAlt(IntPtr hwnd);
    void MarkFullscreenWindow(IntPtr hwnd, [MarshalAs(UnmanagedType.Bool)] bool fFullscreen);
    void SetProgressValue(IntPtr hwnd, ulong ullCompleted, ulong ullTotal);
    void SetProgressState(IntPtr hwnd, int tbpFlags);
}

You obtain the COM object by activating the TaskbarList CLSID and casting to ITaskbarList3. Interface casting in COM maps directly to QueryInterface under the hood; the .NET runtime handles the call automatically when you cast a ComObject reference to a defined COM interface.

When COM is overkill or unavailable, WinRT provides modern equivalents for many Shell operations through namespaces like Windows.Storage and Windows.UI.Shell. Use COM when you need capabilities those APIs do not cover, or when integrating with existing COM-based components that your application cannot replace.


XAML Islands

XAML Islands let you embed WinUI 3 XAML controls inside existing Win32, WinForms, or WPF applications. This is the primary mechanism for incremental modernization: rather than rewriting an application from scratch, you replace specific regions of the legacy UI with WinUI 3 controls while the rest of the application continues to run as before.

The core type is DesktopWindowXamlSource, which creates a WinRT XAML rendering surface that you attach to an existing HWND. The XAML source has its own HWND that you embed inside the host window’s layout, sizing and positioning it as the host application changes dimensions.

var xamlSource = new DesktopWindowXamlSource();
var interop = xamlSource.As<IDesktopWindowXamlSourceNative>();

// Attach to the parent HWND
interop.AttachToWindow(parentHwnd);

// Retrieve the child HWND to position in the parent
interop.get_WindowHandle(out var childHwnd);

// Set your WinUI 3 content
xamlSource.Content = new MyWinUI3Control();

The IDesktopWindowXamlSourceNative COM interface handles the bridge between the WinRT XAML source and the Win32 windowing system. You size and position the child HWND using SetWindowPos as the host window resizes.

XAML Islands come with meaningful limitations. Keyboard focus and tab navigation require additional handling since the XAML island is an isolated island of WinRT messaging within a Win32 message loop. Only one WindowsXamlManager instance can exist per thread, so all XAML island activity on a thread shares a single manager. System themes and accessibility contexts are generally respected, but deep integration with the host application’s visual chrome requires extra coordination. For new applications, building directly in WinUI 3 is significantly simpler; XAML Islands are specifically valuable when a full rewrite is not feasible.


WinRT Interop from WinUI 3

WinUI 3 applications access Windows Runtime APIs through the same Windows.* namespaces familiar from UWP development, but with some important differences. The WinRT projection in .NET 6+ uses CsWinRT rather than the UWP-era C++/WinRT projections. The API surface is largely the same, but assembly packaging differs: you reference WinRT APIs through the Microsoft.Windows.SDK.Contracts package or through the Windows App SDK itself rather than through the UWP framework.

Target platform version matters here. WinRT APIs introduced after a certain Windows release are not available on older OS versions. The Windows App SDK handles this for its own APIs through version checks, but for OS WinRT APIs you should call Windows.Foundation.Metadata.ApiInformation.IsTypePresent or IsMethodPresent before using APIs that may not exist on the user’s version of Windows.

if (ApiInformation.IsTypePresent("Windows.UI.ViewManagement.ApplicationView"))
{
    // This API exists on the current OS version
}

The WinRT.Interop namespace provides several helper methods for bridging WinRT objects to native contexts. Beyond WindowNative.GetWindowHandle, it includes InitializeWithWindow.Initialize for activating WinRT objects that require an owner window, which applies to pickers, share contracts, and print dialogs.


Common Interop Patterns

Several interop scenarios appear frequently enough in WinUI 3 desktop applications that they are worth addressing directly.

Setting a custom window icon requires sending WM_SETICON with both the small (16x16) and large (32x32 or 48x48) icon handles to the window. You load the icon with LoadImage specifying LR_LOADFROMFILE for a file path or LR_LOADFROMRESOURCE for embedded resources, then post the message through SendMessage.

System tray integration uses Shell_NotifyIconW with the NOTIFYICONDATAW structure. You call it with NIM_ADD to add the icon, NIM_MODIFY to update it (changing the tooltip or icon image), and NIM_DELETE when the application exits. System tray messages arrive through a callback window message, so you either subclass your main window procedure or create a hidden message-only window to receive them. CsWin32 generates correct wrappers for Shell_NotifyIconW and the associated constants.

Global hotkeys use RegisterHotKey with a unique identifier, modifier flags, and a virtual key code. The OS posts WM_HOTKEY messages to the window you register against. You process those messages in the window procedure. Remember to call UnregisterHotKey with the same identifier when the window closes.

Custom window messages follow the same SendMessage and PostMessage patterns from Win32. For internal application messaging between threads or components, WM_APP through WM_APP + 0x3FFF is the reserved range for application-defined messages. You retrieve the message identifier with PInvoke.RegisterWindowMessage if you need a globally unique message that won’t collide with other applications.

Each of these patterns benefits from CsWin32 for declaration generation, SafeHandle subclasses for any resources that need cleanup, and careful attention to threading since WinUI 3’s XAML dispatcher runs on the UI thread while Win32 message processing may arrive on different threads.

Found this guide helpful? Share it with your team:

Share on LinkedIn