Upgrade prompt: moving an application to Dapplo.Windows 3.0
This page is written for an AI coding assistant, and works as a checklist for people too. Give it to the assistant together with the application's repository (written with Greenshot in mind, it applies to any consumer). The goal is an application that works with Dapplo.Windows 3.0, not one that merely compiles.
Your task
Upgrade the application from Dapplo.Windows 2.x to 3.0.
Dapplo.Windows 3.0 fixed a large number of interop bugs and deliberately changed APIs whose concept was wrong. Many changes are caught by the compiler (renames, moved types, changed signatures). The dangerous ones compile without complaint but behave differently: a failure branch that never ran now runs, a flag that was ignored now works, a subscriber that ran on the UI thread now runs on another thread, a stored setting reads back differently.
Reference material, in this order of authority:
- The Dapplo.Windows source at the 3.0 tag (read it when in doubt; it is the truth).
CHANGELOG.md: every change, with finding IDs such asA-06.doc/articles/migration-3.0.md: rename tables and before/after code.- The articles in
doc/articlesand their compiled samples insrc/Dapplo.Windows.Example.DocSamples.
Rules
- Fix the cause, don't hide it. No wrappers that re-create old names, no
#pragmaortry { } catch { }to silence new exceptions, no casting away new types. If 3.0 throws where 2.x silently misbehaved, the calling code was wrong: fix the calling code. - Understand each call site. For every use of a changed API, decide what the code meant and choose the 3.0 API that does that. The rename tables give the mechanical mapping; the sections below tell you when the mechanical mapping is not enough.
- Keep behaviour the user sees the same or better. Where 3.0 changes behaviour on purpose (owned windows, DPI scaling, cursor position, hotkey threading), check the user-visible result and adjust the application.
- Don't change Dapplo.Windows to fit the application. If something in the library looks wrong or is missing, note it in your report.
- Small commits by area (packages, windows, input, clipboard, DPI, …), each building and passing tests.
- Report what you changed, every place where you had to decide about behaviour, and the manual checks from the last section that still need a human.
Step 1: Inventory
Before changing anything, find every use of Dapplo.Windows and group it by area. Search for at least:
using Dapplo.Windows
InteropWindow IInteropWindow InteropWindowQuery InteropWindowFactory WindowsEnumerator
GetParent GetChildren GetZOrderedChildren IsTopLevel IsPopup GetTopLevelWindows GetTopWindows
Fill( InteropWindowRetrieveSettings GetInfo( PrintWindow WindowScroller
KeyboardHook MouseHook KeyCombinationHandler KeySequenceHandler KeyOrCombinationHandler TriggerOnKeyUp
KeyboardInputGenerator MouseInputGenerator VirtualKeyCode KeyHelper
ClipboardNative IClipboardAccessToken SetAs ClearContents OnUpdate OnRenderFormat SetDelayedRenderedContent
SharedMessageWindow WindowMessage WindowMessageInfo WinProcListener WinProcFormsMessages WinProcMessages
DpiHandler DpiAwareForm DpiUnawareForm AttachDpiHandler DpiAwarenessContext NativeDpiMethods BitmapScaleHandler
CursorHelper CapturedCursor IconInfo IconHelper GetIcon GetAppLogo DrawCursorOn
HResult Succeeded Failed ThrowOnFailure WindowsVersion MonitorFrom DisplayInfo
NativeRect NativeSize NativePoint IsDocked HasOverlap IsEmpty Intersect TypeConverter
ApplicationRestartManager ListenForEndSession EndSessionMessage InstallerRestartManager RestartManager
SystemStateApi PreventSleep AllowSleep WaitableTimer DwmApi WinMm Shell32Api WinEventHook
Also find everything the application stores that came from Dapplo.Windows types: settings files (sizes, positions, rectangles, hotkeys, enum values), registry values, caches.
Write the inventory down (area, file, what it's used for). You'll need it for the report and for Step 4.
Step 2: Packages and target frameworks
- Update every
Dapplo.Windows.*package to the same 3.0 version. - 3.0 targets
net480andnet10.0-windows.netstandard2.0andnet8.0-windowsare gone. - The core packages no longer depend on WinForms or WPF. Add Dapplo.Windows.Forms where the application uses
DpiAwareForm,AttachDpiHandler(Form/ContextMenuStrip),WinProcListener,WinProcMessages()on aControl,FormsExtensions, orBitmapScaleHandlerwith buttons/menu items. Add Dapplo.Windows.Wpf for WPF windows,ToBitmapSource(), WPF struct conversions (ToRect(),ToNativeRect(), …) andColorizationColor.ToMediaColor(). - The
IInteropWindowicon extensions (GetIcon,GetIconFromWindow,GetAppLogo) are in the Dapplo.Windows package now (namespaceDapplo.Windows.Icons). - All assemblies are strong-named for every target now. Check binding redirects and
InternalsVisibleTo.
Step 3: Mechanical renames
Apply the tables in migration-3.0.md ("Target frameworks and packages", "Renames and moved types") and the
before/after snippets. Let the compiler guide you, but for every changed call read Step 4 for its area first; many
renames come with a behaviour change.
Step 4: Changes that compile but behave differently
Work through every section that applies to your inventory. Each item says what changed, how to find affected code and what to do.
4.1 Errors that used to be hidden
HResultis a signed enum. In 2.xFailed()was never true andSucceeded()always true, so every COM, DWM and DPI failure was reported as success. Find allSucceeded(),Failed(),ThrowOnFailure(), comparisons withHResult.*and any(uint)casts of an HRESULT. For each failure branch that can now run, make sure it does something sensible (log, fall back, tell the user).DwmApichecks such as "is composition enabled", "is the window cloaked" and the DPI awareness calls are the usual ones.- Subscriber exceptions in
SharedMessageWindow,KeyboardHookandMouseHookno longer crash the process. They end that subscription and are published onSharedMessageWindow.SubscriberErrors,KeyboardHook.SubscriberErrorsandMouseHook.SubscriberErrors. Subscribe to all three at startup and log them, otherwise a broken hotkey or clipboard listener just stops silently. - Dialogs (
Dapplo.Windows.Dialogs) throwInvalidOperationExceptionwhen not called on an STA thread. Show them from the UI thread, never fromTask.Runor a thread-pool continuation. - Clipboard
Set*throwsInvalidOperationExceptionwhen the clipboard content belongs to another window, which means the code forgotClearContents(); see 4.6.
4.2 Windows: parent, owner, children and window lists
- Parent vs owner (A-06).
GetParent()/Parent/HasParentare the real parent now (only for child windows). Owned windows (dialogs, tool windows, popups owned by another window) have an owner instead:GetOwner(),Owner,HasOwner. Every use ofGetParentmust be reviewed: "the window this dialog belongs to" isGetOwner(). - Children.
GetChildren()returns only direct children, in Z-order. Code that searched for a nested control (an edit field inside a panel, a browser render window) needsGetDescendants().GetZOrderedChildren()is gone. - Visible application windows.
IsTopLevel/GetTopLevelWindows/IsPopupare nowIsVisibleApplicationWindow/GetVisibleApplicationWindows/IsVisiblePopup, and they accept owned windows (2.x rejected them becauseGetParentreturned the owner). A window picker or "capture window" list can therefore show more windows than before, for example an application's owned tool windows. Decide per call site whether that is wanted; filter withHasOwner/GetOwner()if not. - Window lists are snapshots.
GetTopWindows()returns anIReadOnlyListtaken withEnumWindowsat call time. It no longer skips or repeats windows while the Z-order changes. Re-query instead of holding it long. Fill()caching works. In 2.xFill()ignored its flags: it always refreshed, always auto-corrected bounds, always queried the maximized state. Now values are cached unless you passInteropWindowRetrieveSettings.ForceUpdate, andCacheAll/CacheAllWithChildrendo not auto-correct bounds (useCacheAllAutoCorrector addAutoCorrectValues). Find everyFill(,GetInfo(,GetBounds(… on windows that are reused (for example a window object kept while the user moves or resizes it) and passForceUpdatewhere fresh values are needed.GetInfo(autoCorrect)no longer crops owned dialogs to their owner's bounds. Capture code that relied on the cropped bounds now gets the real window bounds, which is the correct result.- Hung windows. Reading a window's text or title-bar info times out after 500 ms instead of blocking forever. Text can be null for a hung window; handle that.
4.3 Capturing: PrintWindow, cursor and icons
PrintWindow()returns aBitmap(no longer generic), sized from the full window and cropped to the visible (DWM) bounds, rendered withPW_RENDERFULLCONTENTon Windows 8.1+. The 2.x image was shifted by the invisible border, and DirectComposition, Chromium and UWP windows came out black. It returns null for a zero-sized window. Remove any offset correction the application added for the 2.x shift, and handle null.- Cursor capture.
CursorHelper.TryGetCurrentCursorreturns aSizeandHotSpotthat match the captured bitmaps at every DPI and pointer size.DrawCursorOnGraphics/DrawCursorOnBitmaptake the top-left of the cursor image, not the mouse position: passmousePosition - cursor.HotSpot(in the capture's coordinate space). Check the cursor lands exactly where the mouse was, at 100 %, 150 % and 200 % scaling and with an enlarged pointer. IconInfo/IconInfoExexpose raw, non-owningColorBitmap/BitmaskBitmap. Take ownership once withTakeBitmaps(out mask, out color)or free them withDeleteBitmaps(). Never create a SafeHandle per read.- Icons returned by
IconHelperandGetIcon<Icon>()own their handle now (2.x returned a wrapper around a destroyed handle). Dispose them when you're done. OnlyIconandBitmapare supported; other types throwNotSupportedException. For WPF useGetIcon<Bitmap>().ToBitmapSource(). - ICO/CUR writing scales images above 256 pixels down to 256 instead of truncating them.
WindowsVersionreads the real version withRtlGetVersion, also for .NET Framework hosts without a manifest.IsWindows10andIsWindowsVistamean exactly that version now; "10 or later" isIsWindows10OrLater.
4.4 Geometry and stored values
- Right and Bottom are exclusive (Win32
RECT).IsDockedToLeftOf/IsDockedToRightOftreat flush rectangles (a.Right == b.Left) as docked;HasOverlapequalsIntersectsWith(touching is not overlapping, containing is). Remove any ±1 corrections the application added around these. IsEmptyis true for a zero or negative width or height.NativeRect.Unionignores empty rectangles.Intersect2is gone, useIntersect.- Float-to-int conversions are explicit: points floor, sizes round up, rectangles become the smallest containing
integer rectangle. Use
Round()when you want rounding. Values can differ by one pixel from 2.x. NativeSize.CompareTosorts ascending by area (2.x sorted descending). Check every sort of sizes.- Stored settings (important). 2.x's
NativeSizeTypeConverterwroteHeight,Widthand read the values back the other way round. 3.0 reads and writesWidth,Height, so everyNativeSizestored by 2.x comes back swapped. Add a one-time settings migration (a settings version number, swap stored sizes once) or reset those values. All converters now use the invariant culture. - Stored enum numbers:
MonitorFrom,SysColorIndexes.Color3Dface,ProcessAccessRights,DialogDpiChangeBehaviors,ObjectStates.STATE_SYSTEM_VALIDandDesktopAccessRight.GENERIC_ALLchanged values.
4.5 Hotkeys and input
Hooks run on their own thread.
KeyboardHookandMouseHooksubscribers are no longer called on the UI thread. Anything that touches UI, application state or non-thread-safe services must be marshalled:KeyboardHook.KeyboardEvents .Where(handler) // decides Handled synchronously on the hook thread .ObserveOn(SynchronizationContext.Current) // capture/UI work on the UI thread .Subscribe(_ => StartCapture());Setting
Handledmust happen synchronously in theWhere/ subscriber on the hook thread, and quickly (Windows removes a hook that is too slow). Listeners that never setHandledshould useKeyboardEventsNonBlocking/MouseEventsNonBlocking.One handler instance per subscription.
Where(handler)fails (OnError) when the same handler instance is used by another active subscription. If an observable is subscribed more than once, useWhere(() => new KeyCombinationHandler(...)).Trigger mode.
TriggerOnKeyUpis replaced byTriggerMode:KeyDown(default),FirstKeyUp,AllKeysUp. For a hotkey that starts a capture or sends input, preferAllKeysUp: it fires when every key of the combination is released, so the captured application doesn't see a held Ctrl/Shift/Alt and no key gets stuck. Key-up modes never mark events as handled.PrintScreen.
VirtualKeyCode.Snapshotis gone, the key isVirtualKeyCode.PrintScreen(Printis a different, rare key). Check every hotkey definition that means "the Print Screen key".Win key.
VirtualKeyCode.Win(parsed from"win") matches either Windows key; useLeftWin/RightWinonly when a side matters.Stored hotkey strings.
KeyHelper.VirtualKeyCodeFromStringstill accepts the old namesSnapshot,Hangul,HangeulandKanji, andToString()now always producesPrintScreen,Kana,Hanja. Check the application's own hotkey parser/serializer, if it has one, against these names.Key sequences reset after a wrong combination regardless of release order.
Injected input: arrow, navigation, right Ctrl/Alt, Windows and media keys are sent as extended keys, and
KeyCombinationPressreleases keys in reverse order. Mouse coordinates are normalised correctly over the virtual desktop (2.x could jump to the screen edge). Remove workarounds for those bugs.Typing text: use
KeyboardInputGenerator.TypeText(text)(Unicode, any layout). Replace "put text on the clipboard and send Ctrl+V" workarounds; they overwrite the user's clipboard.Key state:
KeyboardState.IsDown(...),IsAnyDown(...),IsToggled(...)replace ownGetAsyncKeyStateP/Invokes.Raw input: HID data is
args.HidData; monitors register when subscribed and unregister when disposed.
4.6 Clipboard
Writing: always replace the content. The recommended form:
ClipboardNative.ReplaceContents(new ClipboardContents() .AddStream(StandardClipboardFormats.DeviceIndependentBitmap, dibStream) .AddStream("PNG", pngStream) .AddUnicodeString(filePath));ReplaceContentsclears first and places all formats or none. The low-levelSet*methods still exist but throw when the content belongs to another application (you forgotClearContents()). Adding to your own content isAddToCurrentContents.Threads: an access token must be used and disposed on the thread that opened it, and you must not
awaitwhile holding it.AccessAsyncopens the clipboard on the thread that continues after theawait. Cancelling throwsOperationCanceledException.Reading:
GetAsStream/TryGetAsStreamreturn a copy that stays valid after the token is disposed.GetAsUnicodeStringno longer returns trailing garbage; remove any trimming workaround.Change notifications:
ClipboardNative.OnUpdateno longer opens the clipboard. The notification carries the sequence number, owner and formats; to read content,ObserveOnanother thread and callAccess()there.Delayed rendering:
OnRenderFormatis gone. Register a renderer per format withClipboardNative.RegisterDelayedRenderer(format, request => …)before announcing the format, and keep the registration alive as long as the content may be pasted. Renderers run synchronously on the shared window thread. Delayed content now survives the application exiting (the shared window is shut down on process exit and Windows asks for the data first).Cloud / history options:
SetCloudClipboardOptions()without arguments places nothing; 2.x silently excluded every copy from clipboard history and cloud sync. Pass explicit values if the application wants to exclude content.
4.7 Window messages
WindowMessageis a class andHandled/Resultnow reach Windows. In 2.x they were set on a copy and ignored. Review every subscriber that setsHandled = true: it now really suppresses the default window procedure, andResultis really returned. Set them synchronously inOnNext; afterObserveOnthe answer is already sent.WindowMessageInfois gone; the Forms/WPFWinProcMessages()andDpiHandleruseWindowMessagetoo.SharedMessageWindow.Handleis never 0 and the window lives for the whole process. Remove "wait until the window exists" code. Do registrations inListen(onSetup, onTeardown)(both run on the window thread) and useSharedMessageWindow.Invoke(hwnd => …)for other code that must run there.WinEventHookevents andPowerBroadcastListener/ session events arrive on that thread.
4.8 DPI
DpiAwarenessContextis a pointer-sized struct; compare withNativeDpiMethods.AreDpiAwarenessContextsEqual.- In 2.x, creating a
DpiAwareFormorDpiHandler, or callingAttachDpiHandler, left the UI thread in Per Monitor v2 awareness permanently. 3.0 only changes it while the form handle is created. If other windows of the application only scaled correctly because of that leak, give the process the right awareness via its manifest (orapp.configfor WinForms on .NET Framework), or create those windows insideNativeDpiMethods.ScopedThreadDpiAwarenessContext. DpiAwareFormno longer swallowsWM_DPICHANGED: WinForms' own Per Monitor v2 scaling andForm.DpiChangedrun. Remove manual font/control scaling inFormDpiHandler.OnDpiChanged, or it scales twice. Without WinForms scaling (.NET Framework without PMv2 configuration) only the window bounds follow Windows' suggested rectangle.DpiCalculator/DpiHandlerscaling rounds instead of truncating (ScaleWithDpi(3, 144)is 5), andDpiHandler.Dpiis 96 until the real DPI is known (IsDpiKnown).EnableDpiAware()falls back to older awareness modes and returns whether the process is really DPI aware.
4.9 Session end, restart, power
ApplicationRestartManager.ListenForEndSession()takes no callbacks. Answer WM_QUERYENDSESSION synchronously inOnNext:m.Veto("reason")to block (the reason shows in Windows' shutdown screen), do nothing to allow. In 2.x "allow" was answered as "block" and nothing reached Windows, so check the application's shutdown and logoff behaviour.WasRestartRequested("/restore")checks the argument you registered.InstallerRestartManager.Shutdown()is graceful by default;RmShutdownType.Forceis opt-in.SystemStateApi.PreventSleep(...)returns a disposableSleepBlockerthat works from any thread;AllowSleep()is gone:using (SystemStateApi.PreventSleep("Recording")) { … }.PowerManagementApi.Shutdown()/Restart()enable the shutdown privilege themselves andShutdownpowers off.
4.10 Smaller items
WinMm:PlayResource(embedded WAVE resource),PlayFile(file),PlayWave(byte[]),PlaySystemSound; all returnbool. APlay("file.wav")call in 2.x never played the file.DwmApi.ColorizationColoris aSystem.Drawing.Color.Shell32Api.TryGetTaskbarPosition(out var data)replacesTaskbarPosition.- Restart Manager types live in
Dapplo.Windows.InstallerManagerwith readable field names. WindowHandles.HWND_TOPMOSTetc. replace magicnew IntPtr(-1)values;User32Api.PostMessage/PostThreadMessagereplace own P/Invokes.
Step 5: Verify
Build, run the unit tests, then work through these checks on a real Windows machine. Report each as done, not applicable, or needs a human.
- Captures (region, window, full screen, last region) at 100 %, 150 % and 200 % scaling, and on two monitors with different scaling. The image is not shifted, not black (try a browser and a UWP app), and the right size.
- Mouse pointer in captures: the pointer is exactly where it was, also with an enlarged pointer.
- Window picker / window capture: the list contains the expected windows (compare with 2.x; owned windows may now appear), a hung application doesn't freeze it, minimized and off-screen windows behave as before.
- Hotkeys: every configured hotkey fires once, with and without modifiers, including PrintScreen variants; no key stays stuck; the captured window doesn't see a held modifier; hotkeys still work after a sleep/resume.
- Settings from 2.x: start with an existing settings file. Stored sizes, positions and hotkeys load correctly (sizes not swapped).
- Clipboard: copy a capture, paste it into Paint, Word/LibreOffice, Outlook and a browser; copy, close the application, then paste; copy while another application holds the clipboard; clipboard history (Win+V) works as the application intends.
- DPI changes: move each window (editor, settings, dialogs) between monitors with different scaling: no double scaling, no blurry text, dialogs keep a sensible size.
- Session end: log off and shut down with unsaved work and without; the application saves/vetoes as intended and does not block shutdown otherwise.
- Dialogs: every open/save/folder dialog opens (from the UI thread).
- Logs: no entries from
SubscriberErrorsduring all of the above.
Report
Finish with:
- the inventory (Step 1) with the status of every item;
- a list of behaviour decisions you made (which call sites, what you chose, why);
- the settings migration you added (or why none was needed);
- the Step 5 checklist with results;
- anything in Dapplo.Windows that looked wrong or was missing.