Common scenarios
Recipes which combine several packages. They are short on purpose: add error handling and disposal as your application needs it.
Namespaces used on this page: Dapplo.Windows.Clipboard, Dapplo.Windows.Desktop, Dapplo.Windows.Enums,
Dapplo.Windows.Input.Enums, Dapplo.Windows.Input.Keyboard, Dapplo.Windows.Messages,
Dapplo.Windows.Messages.Enums, Dapplo.Windows.User32, Dapplo.Windows.User32.Enums,
Dapplo.Windows.Common.Extensions, System.Reactive.Linq, System.Reactive.Concurrency.
Screenshot of the active window with a hotkey
Alt+PrintScreen, but as PNG with transparency on the clipboard. The work is moved off the hook thread with ObserveOn.
// Alt+PrintScreen replacement: capture the active window as PNG onto the clipboard
var subscription = KeyboardHook.KeyboardEvents
.Where(new KeyCombinationHandler(VirtualKeyCode.Menu, VirtualKeyCode.PrintScreen))
// Leave the hook thread before doing the work
.ObserveOn(TaskPoolScheduler.Default)
.Subscribe(_ =>
{
var window = InteropWindowQuery.GetForegroundWindow();
using var bitmap = window.PrintWindow();
if (bitmap == null)
{
return;
}
using var png = new MemoryStream();
bitmap.Save(png, ImageFormat.Png);
png.Position = 0;
using var clipboard = ClipboardNative.Access();
clipboard.ClearContents();
clipboard.SetAsStream("PNG", png);
});
Time tracking: which application is active
// How long is each application in the foreground?
var usage = new Dictionary<string, TimeSpan>();
string currentProcess = null;
var since = DateTimeOffset.Now;
var subscription = WinEventHook.Create(WinEvents.EVENT_SYSTEM_FOREGROUND)
// Don't query the process on the SharedMessageWindow thread
.ObserveOn(TaskPoolScheduler.Default)
.Subscribe(info =>
{
var now = DateTimeOffset.Now;
if (currentProcess != null)
{
usage[currentProcess] = (usage.TryGetValue(currentProcess, out var total) ? total : TimeSpan.Zero) + (now - since);
}
using var process = Process.GetProcessById(InteropWindowFactory.CreateFor(info.Handle).GetProcessId());
currentProcess = process.ProcessName;
since = now;
});
Notice when an application starts
// Get told when Notepad opens a window
var subscription = WinEventHook.WindowCreateDestroyObservable()
.Where(info => info.WinEvent == WinEvents.EVENT_OBJECT_CREATE)
.Select(info => InteropWindowFactory.CreateFor(info.Handle))
.Where(window => window.IsVisibleApplicationWindow() && window.GetClassname() == "Notepad")
.Subscribe(window => Console.WriteLine($"Notepad opened a window: {window.Handle}"));
For the lock screen and logon / logoff see WindowsSessionListener in
Window messages.
Clipboard history
A simple history of copied text. It skips content which asked to be excluded from monitoring (passwords).
var history = new List<string>();
var subscription = ClipboardNative.OnUpdate
// Skip the current content, only log changes
.Skip(1)
.Where(info => info.FormatIds.Contains((uint)StandardClipboardFormats.UnicodeText))
// Respect passwords and other excluded content
.Where(info => !info.Formats.Contains(ClipboardCloudExtensions.ExcludeClipboardContentFromMonitorProcessingFormat))
.ObserveOn(TaskPoolScheduler.Default)
.Subscribe(info =>
{
using var clipboard = ClipboardNative.Access();
if (clipboard.CanAccess)
{
history.Add(clipboard.GetAsUnicodeString());
}
});
Save copied images
var folder = Environment.GetFolderPath(Environment.SpecialFolder.MyPictures);
var subscription = ClipboardNative.OnUpdate
.Skip(1)
.Where(info => info.Formats.Contains("PNG"))
.ObserveOn(TaskPoolScheduler.Default)
.Subscribe(info =>
{
using var clipboard = ClipboardNative.Access();
if (clipboard.CanAccess && clipboard.TryGetAsStream("PNG", out var png))
{
using (png)
using (var file = File.Create(Path.Combine(folder, $"clipboard-{info.Timestamp:yyyyMMdd-HHmmss}.png")))
{
png.CopyTo(file);
}
}
});
Insert text with a hotkey
KeyboardInputGenerator.TypeText types the text as Unicode characters, independent of the keyboard layout.
TriggerMode.AllKeysUp fires when the user released the hotkey, so the text isn't combined with Ctrl and Alt, and it
passes the keys on, so none gets stuck.
// Ctrl+Alt+D types the current date into the active application.
// AllKeysUp fires when the user released all keys of the combination, so the text isn't combined with Ctrl or Alt,
// and the keys are passed on, so no key gets stuck.
var subscription = KeyboardHook.KeyboardEvents
.Where(new KeyCombinationHandler(VirtualKeyCode.Control, VirtualKeyCode.Menu, VirtualKeyCode.KeyD) { TriggerMode = TriggerMode.AllKeysUp })
.ObserveOn(TaskPoolScheduler.Default)
.Subscribe(_ => KeyboardInputGenerator.TypeText(DateTime.Now.ToString("yyyy-MM-dd")));
Some applications ignore typed Unicode characters (games, remote desktop windows). For those, put the text on the clipboard and send Ctrl+V; read the old clipboard content first and put it back after the paste.
Tile windows
// Place the visible windows of the primary display next to each other
var primary = DisplayInfo.AllDisplayInfos.First(display => display.IsPrimary);
// GetVisibleApplicationWindows skips hidden, minimized and untitled windows and tool windows
var windows = InteropWindowQuery.GetVisibleApplicationWindows()
.Where(window => primary.Bounds.Contains(window.GetInfo().Bounds.Location))
.ToList();
if (windows.Count == 0)
{
return;
}
var width = primary.WorkingArea.Width / windows.Count;
for (var i = 0; i < windows.Count; i++)
{
windows[i].Restore();
User32Api.SetWindowPos(windows[i].Handle, IntPtr.Zero,
primary.WorkingArea.Left + i * width, primary.WorkingArea.Top, width, primary.WorkingArea.Height,
WindowPos.SWP_NOZORDER | WindowPos.SWP_NOACTIVATE);
}
Minimize all other windows
var active = InteropWindowQuery.GetForegroundWindow();
foreach (var window in InteropWindowQuery.GetVisibleApplicationWindows().Where(w => w.Handle != active.Handle))
{
window.Minimize();
}
Single instance
A second instance tells the first one to show itself, with a registered message which the SharedMessageWindow of the first instance receives.
// The first instance listens, a second instance broadcasts and exits
uint showMessage = RegisteredWindowMessages.Register("MyApp.ShowMainWindow");
using var mutex = new Mutex(true, "MyApp.SingleInstance", out var isFirstInstance);
if (!isFirstInstance)
{
// HWND_BROADCAST: every top-level window gets it, also the SharedMessageWindow of the first instance
User32Api.PostMessage(WindowHandles.HWND_BROADCAST, showMessage, IntPtr.Zero, IntPtr.Zero);
return;
}
var subscription = SharedMessageWindow.Messages
.Where(m => (uint)m.Msg == showMessage)
.ObserveOn(SynchronizationContext.Current)
.Subscribe(_ => showMainWindow());
Handling many events
WinEvents, mouse moves and clipboard changes can arrive in bursts. Throttle waits for a quiet moment, Sample takes
the latest value every interval, Buffer collects them.
// Location changes arrive by the hundred while a window is dragged: wait until it is quiet for 250ms
var subscription = WinEventHook.Create(WinEvents.EVENT_OBJECT_LOCATIONCHANGE)
.Where(info => info.ObjectIdentifier == ObjectIdentifiers.Window)
.Throttle(TimeSpan.FromMilliseconds(250))
.Subscribe(info => Console.WriteLine("A window stopped moving"));
// Or collect them and process them in batches
var batches = ClipboardNative.OnUpdate
.Buffer(TimeSpan.FromSeconds(1))
.Where(batch => batch.Count > 0)
.Subscribe(batch => Console.WriteLine($"{batch.Count} clipboard changes in the last second"));
More
- A DPI-aware form: DPI awareness
- Update files that are in use: Restart Manager
- Keep the PC awake, or wake it up: Power and system state
- A global hotkey with
RegisterHotKey: Window messages