Table of Contents

Window management

The Dapplo.Windows package wraps a window handle (HWND) in an IInteropWindow, which reads information about the window on demand and caches it. Static classes find windows (InteropWindowQuery, WindowsEnumerator) and report window events (WinEventHook).

dotnet add package Dapplo.Windows

Namespaces used on this page: Dapplo.Windows.Desktop, Dapplo.Windows.Enums, Dapplo.Windows.User32, Dapplo.Windows.User32.Enums, Dapplo.Windows.User32.Structs, Dapplo.Windows.Common.Structs, Dapplo.Windows.App, Dapplo.Windows.Icons, Dapplo.Windows.Software, Dapplo.Windows.Messages, Dapplo.Windows.Messages.Enums, System.Reactive.Linq.

Window information

InteropWindowFactory.CreateFor(handle) wraps a handle, nothing is read at that moment. The Get... extension methods read a value the first time and store it in the window object; later calls return the stored value unless you pass forceUpdate: true.

// Wrap a window handle, nothing is read yet
IInteropWindow window = InteropWindowFactory.CreateFor(User32Api.GetForegroundWindow());
// Or: IInteropWindow window = InteropWindowQuery.GetForegroundWindow();

// The Get... methods read the value once and cache it in the window object, pass forceUpdate: true to read it again
Console.WriteLine($"Title: {window.GetCaption()}");
Console.WriteLine($"Class: {window.GetClassname()}");
Console.WriteLine($"Bounds: {window.GetInfo().Bounds}");
Console.WriteLine($"Client bounds: {window.GetInfo().ClientBounds}");
Console.WriteLine($"Process: {window.GetProcessId()}");
Console.WriteLine($"Visible: {window.IsVisible()}, minimized: {window.IsMinimized()}, maximized: {window.IsMaximized()}");

GetInfo() returns a WindowInfo with Bounds, ClientBounds, Style, ExtendedStyle and more. For top-level windows the bounds are corrected with the DWM "extended frame bounds", so they don't include the invisible resize border that Windows 10 and 11 add around windows.

Reading several values at once

Fill() reads the values you select with InteropWindowRetrieveSettings. It respects the cache, so add ForceUpdate when you need fresh values. The cached values are also available as properties (Caption, Info, IsVisible, ...), which are null when they were not read yet.

// Read several values at once, the default is everything except the children (CacheAllAutoCorrect)
window.Fill();

// Only what you need
window.Fill(InteropWindowRetrieveSettings.Caption | InteropWindowRetrieveSettings.Info);

// ForceUpdate reads the values again, even when they were cached
window.Fill(InteropWindowRetrieveSettings.Info | InteropWindowRetrieveSettings.ForceUpdate);

// The cached values are properties, null when they were not retrieved
string caption = window.Caption;
NativeRect? bounds = window.Info?.Bounds;
Setting Reads
CacheAll caption, class name, info, maximized, minimized, parent, owner, placement, process id, text, visible, scroll info
CacheAllAutoCorrect (default of Fill) the same, with the bounds corrected (DWM frame, clipped to the parent)
CacheAllWithChildren CacheAll plus the direct children, in Z-order
ForceUpdate combine with the above to read again

Finding windows

InteropWindowQuery.GetVisibleApplicationWindows() returns the windows a user sees as application windows, from top to bottom (Z-order): top-level windows (no parent, owned windows are included) which are visible, not minimized, have a title and a size, and are not a tool window, a background store app or one of a few known system windows (the desktop Progman, Button, Dwm). The same test is available as window.IsVisibleApplicationWindow(); window.IsVisiblePopup() is the similar test for visible WS_POPUP windows, which also accepts tool windows and windows without a title. Change the list of ignored classes with InteropWindowQuery.AddIgnoreClass / RemoveIgnoreClass, or pass false to include them.

InteropWindowQuery.GetTopWindows() returns all top-level windows in Z-order (index 0 is the top-most window), without any filter. GetTopWindows(parent) returns the direct children of a window, also in Z-order.

Both are a snapshot: the list is taken at once, with EnumWindows / EnumChildWindows, when you call the method. Windows builds the list before it reports the first window, so the result can't loop, skip or repeat windows when windows are activated, created or destroyed in the meantime (a GetWindow(GW_HWNDNEXT) walk can). The windows in the snapshot can of course still change or disappear afterwards; GetVisibleApplicationWindows() takes the snapshot when it is called and applies its filter while you enumerate the result.

// The application windows the user sees (visible, with a title, not minimized), from top to bottom
foreach (var window in InteropWindowQuery.GetVisibleApplicationWindows())
{
    Console.WriteLine($"{window.GetCaption()} ({window.GetClassname()})");
}

Filter with LINQ, or let WindowsEnumerator filter while it enumerates:

// All visible Notepad windows
var notepads = InteropWindowQuery.GetVisibleApplicationWindows()
    .Where(window => window.GetClassname() == "Notepad")
    .ToList();

// All top-level windows of a process
var ownWindows = InteropWindowQuery.GetWindowsForProcess(Process.GetCurrentProcess().Id);

// Enumerate with a predicate and stop early: the first window with "Dapplo" in the title
var firstMatch = WindowsEnumerator.EnumerateWindows(
        wherePredicate: window => window.GetCaption().Contains("Dapplo"),
        takeWhileFunc: (window, count) => count < 1)
    .FirstOrDefault();
IInteropWindow FindWindowByTitle(string title) =>
    InteropWindowQuery.GetVisibleApplicationWindows()
        .FirstOrDefault(window => window.GetCaption().IndexOf(title, StringComparison.OrdinalIgnoreCase) >= 0);

var calculator = FindWindowByTitle("Calculator");

Children, parent and owner

A child window lives inside its parent (a button in a dialog). A top-level window can have an owner (a dialog belongs to the main window of the application), but it has no parent. Dapplo.Windows keeps both apart: GetParent() never returns the owner, use GetOwner() for that.

// The direct children, from top to bottom (Z-order)
foreach (var child in window.GetChildren())
{
    Console.WriteLine($"Child {child.GetClassname()}");
}

// Children, grandchildren, ...
var descendants = window.GetDescendants();
// The parent is the window a child window lives in, top-level windows have none (IntPtr.Zero)
IInteropWindow parent = window.GetParentWindow();

// The owner is the window a dialog or tool window belongs to, e.g. the main window of the application
IInteropWindow owner = window.GetOwnerWindow();

// All other top-level windows of the same process
var linked = window.GetLinkedWindows();

GetChildren() returns the direct children in Z-order (a snapshot, see above) and stores them in Children; pass forceUpdate: true to read them again. GetDescendants() returns all levels, depth-first (a child is followed by its own descendants), and doesn't store them.

Changing windows

window.Minimize();
window.Maximize();
window.Restore();

// Anything else ShowWindow supports
User32Api.ShowWindow(window.Handle, ShowWindowCommands.Hide);
User32Api.ShowWindow(window.Handle, ShowWindowCommands.ShowNoActivation);
// Move, keeping the size
window.MoveTo(new NativePoint(100, 100));

// Move and resize
User32Api.SetWindowPos(window.Handle, IntPtr.Zero, 100, 100, 800, 600, WindowPos.SWP_NOZORDER | WindowPos.SWP_NOACTIVATE);

// Placement: the normal (restored) bounds and the show state, e.g. to save and restore a layout
WindowPlacement placement = window.GetPlacement();
window.SetPlacement(placement);

After a monitor was removed, windows can end up where nobody sees them. GetVisibleLocation finds a place on one of the current displays:

// Move a window, e.g. after a monitor was disconnected, to a place where it can be seen
if (!window.GetVisibleLocation(out var visibleLocation) || visibleLocation != window.GetInfo().Bounds.Location)
{
    window.MoveTo(visibleLocation);
}

Bringing a window to the front

ToForegroundAsync() restores a minimized window, waits (up to 2 seconds) until it's restored and makes it the foreground window. Windows has rules about which process may change the foreground window; when it refuses, the taskbar button flashes instead.

// Restores a minimized window and makes it the foreground window. Windows may still refuse,
// e.g. when the user is working in another application: then the taskbar button flashes.
await window.ToForegroundAsync();

Z-order and always on top

SetWindowPos changes the Z-order. WindowHandles has the special handles for its hWndInsertAfter argument: HWND_TOP, HWND_BOTTOM, HWND_TOPMOST and HWND_NOTOPMOST (and HWND_MESSAGE and HWND_BROADCAST for other APIs).

bool isTopmost = (window.GetInfo(forceUpdate: true).ExtendedStyle & ExtendedWindowStyleFlags.WS_EX_TOPMOST) != 0;
User32Api.SetWindowPos(window.Handle, isTopmost ? WindowHandles.HWND_NOTOPMOST : WindowHandles.HWND_TOPMOST, 0, 0, 0, 0,
    WindowPos.SWP_NOMOVE | WindowPos.SWP_NOSIZE | WindowPos.SWP_NOACTIVATE);
const WindowPos zOrderOnly = WindowPos.SWP_NOMOVE | WindowPos.SWP_NOSIZE | WindowPos.SWP_NOACTIVATE;

// Send a window behind all other windows, or bring it to the top without activating it
User32Api.SetWindowPos(window.Handle, WindowHandles.HWND_BOTTOM, 0, 0, 0, 0, zOrderOnly);
User32Api.SetWindowPos(window.Handle, WindowHandles.HWND_TOP, 0, 0, 0, 0, zOrderOnly);

// Place a window directly below another one
User32Api.SetWindowPos(window.Handle, other.Handle, 0, 0, 0, 0, zOrderOnly);

// A snapshot of the Z-order, index 0 is the top-most window
var zOrder = InteropWindowQuery.GetTopWindows().Select(w => w.Handle).ToList();
bool isAboveOther = zOrder.IndexOf(window.Handle) < zOrder.IndexOf(other.Handle);

Posting messages

window.PostMessage(...) (or User32Api.PostMessage for a handle) puts a message in the message queue of the window's thread and returns immediately, SendMessage waits until the message was processed. It returns false when the message couldn't be posted, Marshal.GetLastWin32Error() tells why (e.g. the window is gone, or UIPI blocks messages from a process with a lower integrity level). Only post messages with values in wParam / lParam, not with pointers to your memory. User32Api.PostThreadMessage posts to a thread (its message loop) instead of a window.

// Ask a window to close, without waiting: an application which asks "Save changes?" doesn't block the caller
if (!window.PostMessage(WindowsMessages.WM_CLOSE))
{
    Console.WriteLine($"Posting failed, error {Marshal.GetLastWin32Error()}");
}

// Post a registered message to all top-level windows, e.g. to the other instances of your application
uint showMessage = RegisteredWindowMessages.Register("MyApp.ShowMainWindow");
User32Api.PostMessage(WindowHandles.HWND_BROADCAST, showMessage, IntPtr.Zero, IntPtr.Zero);

Window state

Window handles are reused by Windows after a window is destroyed. Check Exists() before you use a handle you kept for a while.

// Handles are recycled, check that the window still exists before working with it
if (!window.Exists())
{
    return;
}
bool isApplicationWindow = window.IsVisibleApplicationWindow();
bool isOwnWindow = window.IsOwnedByCurrentProcess();
// A Windows Store (UWP) app window
bool isApp = window.IsApp();

Screenshots, icons and scrolling

PrintWindow() asks the window to render itself into a Bitmap, so it also works for windows which are covered by other windows. It uses PW_RENDERFULLCONTENT on Windows 8.1 and later, which captures DirectComposition content (browsers, UWP apps) as well. Minimized windows can't be captured. For WPF use PrintWindowAsBitmapSource() from Dapplo.Windows.Wpf.

// Renders the window, also when it's covered by other windows (not when it's minimized).
// The result is cropped to the visible frame, without the invisible resize borders.
using Bitmap bitmap = window.PrintWindow();
bitmap?.Save("window.png", ImageFormat.Png);
// The icon of a window, as Bitmap or Icon
using var smallIcon = window.GetIcon<Bitmap>();
using var largeIcon = window.GetIcon<Icon>(useLargeIcons: true);

GetWindowScroller() returns a WindowScroller for a window with a scroll bar, for example to capture a long page in parts. It returns null when the window can't be scrolled.

// null when the window has no scroll bar
WindowScroller scroller = window.GetWindowScroller();
if (scroller == null)
{
    return;
}
scroller.Start();                 // scroll to the top
while (!scroller.IsAtEnd)
{
    // e.g. capture the visible part here
    if (!scroller.Next())         // one page down
    {
        break;
    }
}
scroller.Reset();                 // back to the original position

Window events

WinEventHook turns WinEvents into observables. The hook is installed when you subscribe and removed when the last subscription is disposed. The events are delivered on the thread of the SharedMessageWindow: keep OnNext short and use ObserveOn for slow work.

The events are also raised for objects inside windows (menus, the caret, list items ...). Check ObjectIdentifier == ObjectIdentifiers.Window (and IsSelf) when you only want windows.

// Created and destroyed top-level and child windows, events arrive on the SharedMessageWindow thread
IDisposable subscription = WinEventHook.WindowCreateDestroyObservable()
    .Subscribe(info =>
    {
        if (info.WinEvent == WinEvents.EVENT_OBJECT_CREATE)
        {
            Console.WriteLine($"Created {info.Handle}");
        }
        else
        {
            // The window is gone, only the handle is left
            Console.WriteLine($"Destroyed {info.Handle}");
        }
    });

// Removes the hook
subscription.Dispose();
var subscription = WinEventHook.WindowTitleChangeObservable()
    .Select(info => InteropWindowFactory.CreateFor(info.Handle))
    .Where(window => window.IsVisibleApplicationWindow())
    .Subscribe(window => Console.WriteLine($"New title: {window.GetCaption(forceUpdate: true)}"));
// The user switched to another window
var subscription = WinEventHook.Create(WinEvents.EVENT_SYSTEM_FOREGROUND)
    .Subscribe(info => Console.WriteLine($"Active: {InteropWindowFactory.CreateFor(info.Handle).GetCaption()}"));
// Moved or resized windows, this also fires for the caret and the cursor: filter on the window itself
var subscription = WinEventHook.Create(WinEvents.EVENT_OBJECT_LOCATIONCHANGE)
    .Where(info => info.ObjectIdentifier == ObjectIdentifiers.Window && info.IsSelf)
    // Many events arrive while dragging, only take the last one
    .Throttle(TimeSpan.FromMilliseconds(100))
    .Subscribe(info => Console.WriteLine($"Moved: {InteropWindowFactory.CreateFor(info.Handle).GetInfo(forceUpdate: true).Bounds}"));

Limit a hook to one process (or thread) when you can, Windows then only calls you for that process:

// Only the events of one process: less work for Windows and for you
var subscription = WinEventHook.Create(WinEvents.EVENT_OBJECT_CREATE, WinEvents.EVENT_OBJECT_DESTROY, process: process.Id)
    .Where(info => info.ObjectIdentifier == ObjectIdentifiers.Window)
    .Subscribe(info => Console.WriteLine($"{process.ProcessName}: {info.WinEvent}"));

Displays

DisplayInfo.AllDisplayInfos lists the monitors; it's updated when displays, resolutions or the work area change.

foreach (var display in DisplayInfo.AllDisplayInfos)
{
    Console.WriteLine($"{display.DeviceName}: {display.Bounds}, work area {display.WorkingArea}, primary: {display.IsPrimary}");
}
// The bounds of all displays together
NativeRect desktop = DisplayInfo.ScreenBounds;

Installed software

// Reads the uninstall information of the registry (64 and 32 bit, machine and user)
foreach (var software in InstallationInformation.InstalledSoftware().Where(s => s.Publisher == "Microsoft Corporation"))
{
    Console.WriteLine($"{software.DisplayName} {software.DisplayVersion}");
}

Tips

  • An IInteropWindow is a snapshot. Values which change (title, bounds, state) need forceUpdate: true when you read them again later.
  • GetTopWindows(), GetVisibleApplicationWindows(), GetChildren() and WindowsEnumerator return a snapshot of the windows at the moment of the call; call them again for the current state.
  • Reading the text of a window which belongs to a hung application times out after 500 ms instead of blocking.
  • Keep WinEvent subscriptions narrow (event range, process) and throttle location events, they are frequent.

See also