Repository navigation
SharedMessageWindow
Package Dapplo.Windows.Messages. Full version: Window messages.
Many notifications arrive as window messages: clipboard changes, power, session and device changes, hotkeys, raw
input. The SharedMessageWindow is one hidden window, shared by all of Dapplo.Windows, which receives them:
- It's created on first use on its own STA thread with a message loop, and lives until the process exits.
Handlenever returns 0. - At process exit it's destroyed on its own thread (
Shutdown), so delayed rendered clipboard formats are rendered (WM_RENDERALLFORMATS) and survive the process. - It's a hidden top-level window (not message-only), so it also gets broadcasts like
WM_QUERYENDSESSION,WM_POWERBROADCASTandWM_SETTINGCHANGE. -
Messagesis called synchronously in the window procedure, on the window thread. KeepOnNextshort. - Set
HandledandResultsynchronously inOnNextto answer a message. - An exception in a subscriber ends only that subscription; it's published on
SubscriberErrors.
// Called on the SharedMessageWindow thread: keep it short, and never block it
IDisposable subscription = SharedMessageWindow.Messages
.Where(m => m.Msg == WindowsMessages.WM_DISPLAYCHANGE)
.Subscribe(m => Console.WriteLine($"Display changed, new resolution {(int)m.LParam & 0xFFFF}x{((int)m.LParam >> 16) & 0xFFFF}"));
// Stop listening, the window itself stays alive
subscription.Dispose();// Handled and Result must be set synchronously in OnNext, before any ObserveOn
var subscription = SharedMessageWindow.Messages
.Where(m => m.Msg == WindowsMessages.WM_QUERYENDSESSION)
.Subscribe(m =>
{
m.Result = 1; // TRUE: the session may end
m.Handled = true; // don't call DefWindowProc
});Listen(onSetup, onTeardown) runs both callbacks on the window thread, once per subscription, for APIs that register
a window, like RegisterHotKey:
[DllImport("user32", SetLastError = true)]
private static extern bool RegisterHotKey(IntPtr hWnd, int id, uint modifiers, uint virtualKey);
[DllImport("user32", SetLastError = true)]
private static extern bool UnregisterHotKey(IntPtr hWnd, int id);const int hotkeyId = 1;
const uint modControl = 0x0002, modShift = 0x0004, modNoRepeat = 0x4000;
// RegisterHotKey must be called on the thread of the window which gets WM_HOTKEY:
// Listen runs onSetup and onTeardown on the SharedMessageWindow thread
var subscription = SharedMessageWindow.Listen(
onSetup: hwnd =>
{
if (!RegisterHotKey(hwnd, hotkeyId, modControl | modShift | modNoRepeat, (uint)'P'))
{
// The exception goes to OnError of this subscription
throw new System.ComponentModel.Win32Exception();
}
},
onTeardown: hwnd => UnregisterHotKey(hwnd, hotkeyId))
.Where(m => m.Msg == WindowsMessages.WM_HOTKEY && m.WParam == hotkeyId)
.Subscribe(
m => Console.WriteLine("Ctrl+Shift+P pressed"),
ex => Console.WriteLine($"Registering the hotkey failed: {ex.Message}"));
// Disposing the subscription runs onTeardown (UnregisterHotKey) on the window thread
subscription.Dispose();Invoke(action) runs any code on the window thread:
// Run code on the window thread and wait for it, exceptions are rethrown here
bool isWindowThread = false;
SharedMessageWindow.Invoke(hwnd => isWindowThread = SharedMessageWindow.IsWindowThread);
// The handle exists as soon as it's requested, and stays valid until the window is shut down (at process exit)
IntPtr handle = SharedMessageWindow.Handle;Shutdown(timeout) destroys the window on its own thread and waits for its loop; it runs automatically on
AppDomain.ProcessExit (with ProcessExitShutdownTimeout, default 1.5 seconds), after which the window is not
created again. On .NET Framework the same happens on AppDomain.DomainUnload of a non-default AppDomain (e.g. test
hosts), so the window thread has ended before the CLR aborts the threads of that AppDomain. After an explicit call the
next use creates a new window, without the old registrations.
// Happens automatically on AppDomain.ProcessExit, with this timeout (default 1.5 seconds)
SharedMessageWindow.ProcessExitShutdownTimeout = TimeSpan.FromSeconds(1);
// Or at the end of Main, to control the moment and the timeout yourself:
// the window is destroyed on its own thread, delayed rendered clipboard formats are rendered (WM_RENDERALLFORMATS)
bool isShutDown = SharedMessageWindow.Shutdown(TimeSpan.FromSeconds(5));
if (!isShutDown)
{
Console.WriteLine("The window thread didn't end in time, e.g. a delayed renderer is still busy");
}| Feature | Message |
|---|---|
ClipboardNative.OnUpdate, delayed rendering |
WM_CLIPBOARDUPDATE, WM_RENDERFORMAT, WM_RENDERALLFORMATS, WM_DESTROYCLIPBOARD
|
WinEventHook |
WinEvent callbacks |
RawInputMonitor, RawInputDeviceMonitor
|
WM_INPUT, WM_INPUT_DEVICE_CHANGE
|
WindowsSessionListener |
WM_WTSSESSION_CHANGE |
EnvironmentMonitor |
WM_SETTINGCHANGE |
DisplayInfo |
WM_DISPLAYCHANGE, WM_SETTINGCHANGE, WM_DPICHANGED
|
PowerBroadcastListener |
WM_POWERBROADCAST |
ApplicationRestartManager.ListenForEndSession |
WM_QUERYENDSESSION, WM_ENDSESSION
|
DeviceNotification |
WM_DEVICECHANGE |
var sessionListener = new WindowsSessionListener();
// The events are raised on the SharedMessageWindow thread
sessionListener.SessionLockChange += (sender, args) =>
{
if (args.EventType == WtsSessionChangeEvents.WTS_SESSION_LOCK)
{
Console.WriteLine($"Session {args.SessionId} locked");
}
else if (args.EventType == WtsSessionChangeEvents.WTS_SESSION_UNLOCK)
{
Console.WriteLine($"Session {args.SessionId} unlocked");
}
};
sessionListener.SessionLogonChange += (sender, args) =>
Console.WriteLine(args.EventType == WtsSessionChangeEvents.WTS_SESSION_LOGON ? "Logged on" : "Logged off");
// Early at logon the registration can fail, it's retried for about 2 minutes before this is raised
sessionListener.RegistrationFailed += (sender, args) =>
Console.WriteLine($"No session notifications: {args.GetException().Message}");
sessionListener.Start();
// Ignore events for a while, without unregistering
sessionListener.Pause();
sessionListener.Resume();
// Stop listening and unregister
sessionListener.Dispose();For the messages of your own forms or WPF windows use WinProcMessages() (Dapplo.Windows.Forms) or
WinProcMessages() (Dapplo.Windows.Wpf):
// Subclasses the form's window. Runs on the UI thread, you may set Handled / Result.
// The sequence follows handle re-creation and completes when the form is disposed.
var subscription = form.WinProcMessages()
.Where(m => m.Msg == WindowsMessages.WM_NCHITTEST)
.Subscribe(m =>
{
// HTCAPTION: the whole window can be dragged like its title bar
m.Result = new IntPtr(2);
m.Handled = true;
});