Skip to content

Fix OverflowException in WindowChromeWorker._HandleNCHitTest when dragging across different-DPI monitors - #1997

Open
bobo198504 wants to merge 5 commits into
QL-Win:masterfrom
bobo198504:fix/window-drag-overflow-multimonitor
Open

Fix OverflowException in WindowChromeWorker._HandleNCHitTest when dragging across different-DPI monitors#1997
bobo198504 wants to merge 5 commits into
QL-Win:masterfrom
bobo198504:fix/window-drag-overflow-multimonitor

Conversation

@bobo198504

@bobo198504 bobo198504 commented Sep 6, 2026

Copy link
Copy Markdown

Problem

Dragging the preview window onto a monitor with a different DPI — e.g. a non-primary 4K display — crashed QuickLook with:

System.OverflowException: 算术运算导致溢出。
   at System.Windows.Shell.WindowChromeWorker._HandleNCHitTest(WM uMsg, IntPtr wParam, IntPtr lParam, Boolean& handled)
   at System.Windows.Shell.WindowChromeWorker._WndProc(...)
   at System.Windows.Interop.HwndSource.PublicHooksFilterMessage(...)

This is the crash reported in #1996 (crashes when dragging the preview window on a non-primary monitor). It happens on any preview type — the trigger is the shared ViewerWindow, not a specific plugin.

Root cause

The overflow is raised by WPF (.NET Framework 4.6.2) WindowChromeWorker._HandleNCHitTest on the WM_NCHITTEST message. I decompiled the installed PresentationFramework.dll and confirmed that _HitTestNca and DpiHelper.DevicePixelsToLogical/DeviceRectToLogical contain no overflowable (int) casts — the bad arithmetic lives entirely inside WPF's per-window DPI math, which becomes inconsistent as the window is dragged onto a monitor with a different DPI. That inconsistency can't be corrected from the outside, so the safe fix is to avoid the offending hit-test entirely.

Changes

  • ViewerWindow: answer WM_NCHITTEST with HTCLIENT to bypass the overflowing WindowChrome hit-test, and restore window dragging manually (WM_NCLBUTTONDOWN + HT CAPTION) on the title area, since Window.DragMove() depends on a hit-test result.
  • ViewerWindow: apply the WM_DPICHANGED suggested rect so the HWND and WPF geometry stay in sync when the window moves between monitors.
  • App / SHCore: opt into Per-Monitor V2 DPI awareness (SetProcessDpiAwarenessContext) with a V1 fallback.
  • WindowHelper / ViewerWindow.Actions: clamp MoveWindow coordinates/size and sanitize non-finite window sizes so a degenerate rect can never reach Win32/WPF.

Fixes #1996.

Summary by Sourcery

Prevent cross-monitor DPI preview crashes while improving monitor-aware window placement and MediaInfo presentation.

New Features:

  • Render MediaInfo metadata in a structured, aligned table with localized field labels and copy support.

Bug Fixes:

  • Prevent preview-window crashes when moving or resizing across monitors with different DPI settings, including monitors with negative coordinates.
  • Correct monitor selection for previews opened from files on the desktop by using the cursor's monitor.

Enhancements:

  • Improve per-monitor DPI handling and preserve window geometry during cross-monitor movement.
  • Harden window positioning against invalid, degenerate, or out-of-range coordinates and sizes.
  • Restore title-bar dragging and resize hit testing independently of WPF WindowChrome.

…gging across different-DPI monitors

Dragging the preview window onto a monitor with a different DPI (e.g. a
non-primary 4K display) crashed with an arithmetic overflow inside WPF's
WindowChromeWorker hit-test. The crash was triggered on WM_NCHITTEST, whose
DPI math can't be made safe from the outside, so:

- Short-circuit WM_NCHITTEST to HTCLIENT in ViewerWindow, and restore window
  dragging by hand (WM_NCLBUTTONDOWN + HT CAPTION) on the title area.
- Apply the WM_DPICHANGED suggested rect so the HWND and WPF geometry stay in
  sync as the window moves between monitors.
- Opt into Per-Monitor V2 DPI awareness (SetProcessDpiAwarenessContext) with a
  V1 fallback.
- Clamp MoveWindow coordinates/size and sanitize non-finite window size so a
  degenerate rect can never reach Win32/WPF.

Fixes QL-Win#1996
@sourcery-ai

sourcery-ai Bot commented Sep 6, 2026

Copy link
Copy Markdown

Reviewer's Guide

The PR prevents the .NET Framework 4.6.2 WindowChrome WM_NCHITTEST overflow during cross-DPI monitor dragging by bypassing WPF hit-testing, restoring native title-bar dragging, synchronizing WM_DPICHANGED geometry, enabling Per-Monitor V2 DPI awareness with fallback, and sanitizing all window coordinates and sizes.

Sequence diagram for cross-DPI viewer window dragging

sequenceDiagram
    actor User
    participant ViewerWindow
    participant WPF as WindowChromeWorker
    participant Win32
    participant Monitor

    User->>ViewerWindow: TitleArea_MouseLeftButtonDown
    ViewerWindow->>Win32: ReleaseCapture()
    ViewerWindow->>Win32: SendMessage(WM_NCLBUTTONDOWN, HTCAPTION)
    User->>Monitor: Drag window across DPI boundary
    Monitor-->>ViewerWindow: WM_DPICHANGED(suggestedRect)
    ViewerWindow->>Win32: MoveWindow(suggestedRect)
    User->>ViewerWindow: Window message WM_NCHITTEST
    ViewerWindow-->>WPF: HTCLIENT
    Note over ViewerWindow,WPF: WPF WindowChrome hit-testing is bypassed
Loading

Flow diagram for safe viewer window geometry

flowchart TD
    Input["Requested window size and position"] --> Size["PositionWindow"]
    Size --> Finite["FinitePositive"]
    Finite --> Move["MoveWindow"]
    Move --> Clamp["ToInt32Clamped"]
    Clamp --> Rect["Clamped coordinates and minimum dimensions"]
    Rect --> Win32["User32.MoveWindow"]
Loading

File-Level Changes

Change Details Files
Bypass the .NET Framework WindowChrome non-client hit-test path and reimplement title-bar dragging through native messages.
  • Return HTCLIENT for WM_NCHITTEST to prevent the overflowing WPF hit-test.
  • Start dragging with WM_NCLBUTTONDOWN/HTCAPTION from the title area.
  • Skip manual dragging for borderless windows.
QuickLook/ViewerWindow.xaml.cs
Synchronize window geometry with per-monitor DPI transitions and enable Per-Monitor V2 awareness.
  • Apply the WM_DPICHANGED suggested rectangle.
  • Use SetProcessDpiAwarenessContext with a Windows 10 V2 capability check and fall back to SetProcessDpiAwareness.
  • Add the DPI-awareness context interop declaration.
QuickLook/ViewerWindow.xaml.cs
QuickLook/App.xaml.cs
QuickLook/NativeMethods/SHCore.cs
Prevent invalid or oversized geometry from reaching Win32 and WPF arithmetic.
  • Clamp pixel coordinates and dimensions to Int32-safe values with minimum physical window sizes.
  • Clamp transformed pixel coordinates.
  • Replace NaN, infinity, and non-positive requested sizes with valid fallbacks.
QuickLook.Common/Helpers/WindowHelper.cs
QuickLook/ViewerWindow.Actions.cs

Assessment against linked issues

Issue Objective Addressed Explanation
#1996 Prevent QuickLook from crashing with an OverflowException when dragging a video preview window across monitors with different DPI, particularly onto a non-primary 4K display.
#1996 Ensure the preview window remains draggable and its geometry stays synchronized when moved between monitors with different DPI settings.

Possibly linked issues


Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey - I've found 1 issue

Prompt for AI Agents
Please address the comments from this code review:

## Individual Comments

### Comment 1
<location path="QuickLook/ViewerWindow.xaml.cs" line_range="222-226" />
<code_context>
+        // overflow is in WPF's own DPI math, so no condition on the window rect can reliably
+        // detect it). Dragging still works because the content panels use WM_NCLBUTTONDOWN(HT
+        // CAPTION) directly rather than relying on a hit test result.
+        if (msg == WM_NCHITTEST)
+        {
+            handled = true;
+            return new IntPtr(1); // HTCLIENT
+        }
+
+        if (msg == WM_DPICHANGED && lParam != IntPtr.Zero)
</code_context>
<issue_to_address>
**issue (bug_risk):** Returning HTCLIENT for every WM_NCHITTEST prevents WindowChrome from reporting resize hit-test zones such as HTLEFT, HTRIGHT, HTTOP, and HTBOTTOM. Because ViewerWindow remains configured with ResizeMode="CanResize", users can no longer resize the preview window through its borders.

**Triggers:** When the user attempts to resize the preview window from any border or corner.

**Suggested fix:** Handle only the problematic hit-test path, or implement the WindowChrome resize hit-test zones and initiate native resizing manually instead of returning HTCLIENT for every point.
</issue_to_address>

Sourcery assessment

Approval pending. 1 finding to address first.

Blocking findings: QuickLook/ViewerWindow.xaml.cs:226


Sourcery is free for open source - if you like our reviews please consider sharing them ✨

Comment thread QuickLook/ViewerWindow.xaml.cs
… WM_NCHITTEST

Sourcery correctly flagged that returning HTCLIENT for every WM_NCHITTEST
disabled the resize borders while ResizeMode=CanResize. Report HTLEFT/HTRIGHT/
HTTOP/HTBOTTOM and the corner zones at the window edges, and HTCLIENT elsewhere,
still bypassing the WindowChrome hit-test that overflows during cross-DPI drags.

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sourcery assessment

Approved.

…t32()

The OverflowException that remained on monitors above/left of the primary was in our
own WndProc, not WPF. IntPtr.ToInt32() compiles to conv.u8 + conv.ovf.i4; when a screen
coordinate is negative Windows sign-extends lParam's high 32 bits, so ToInt32() sees a
huge unsigned value and overflows. Decode the packed signed 16-bit mouse coordinates via
ToInt64() + an unchecked low-32-bit cast and sign extension, which never overflows.

Also make the WndProc hook re-attachable (for HwndSource recreation after a display
change) and detach it in OnClosing.
@bobo198504

Copy link
Copy Markdown
Author

Update: root cause identified precisely

After field-testing across several monitor arrangements, the actual overflow is now confirmed. It is not in WPF's _HandleNCHitTest arithmetic as I first assumed — it's in our own WM_NCHITTEST handler, and the log trace proved it (top frame: QuickLook.ViewerWindow.WndProc).

Root cause

IntPtr.ToInt32() in .NET Framework compiles to:

conv.u8        ; m_value as unsigned 64-bit
conv.ovf.i4    ; checked conversion to int32

On a 64-bit process, when the mouse is on a monitor above or left of the primary (negative screen coordinate), Windows sign-extends WM_NCHITTEST's lParam so its high 32 bits are all 1. ToInt32() then treats it as a ~1.8×10¹⁹ unsigned value and conv.ovf.i4 throws OverflowException.

This is exactly why it reproduced with the monitor above the primary (Y negative) but not left (X negative keeps the low 16-bit half negative while lParam stays a 32-bit positive value). It is monitor-arrangement/coordinate-sign dependent, not resolution dependent — so any arrangement with a negative-coordinate monitor (2-3× 8K included) is covered.

Fix

Decode the packed signed 16-bit mouse coordinates using lParam.ToInt64() + an unchecked low-32-bit cast + manual sign extension, instead of ToInt32():

int v = unchecked((int)lParam.ToInt64());
int lo = v & 0xFFFF;
int hi = (v >> 16) & 0xFFFF;
int mx = (lo & 0x8000) != 0 ? lo - 0x10000 : lo;
int my = (hi & 0x8000) != 0 ? hi - 0x10000 : hi;

This is pure int math with no checked conversion, so it can never overflow. Verified by decompiling the built exe: WndProc now contains only conv.i4 (unchecked), no conv.ovf.

Also

  • Made the WndProc hook re-attachable so it survives HwndSource recreation after a display change, and detach it in OnClosing.
  • Field-tested: any monitor arrangement (above/left/right/primary) now drags without crashing, resizing and title-bar dragging intact.

…und window

GetCurrentDesktopRectInPixel and GetCurrentScaleFactor picked the monitor from
GetForegroundWindow(). When the selected file sits on the desktop, the foreground
window is the desktop (Progman), which always resolves to the primary monitor, so a
file on a secondary desktop would open its preview on the wrong screen. Use the cursor
position (Screen.FromPoint / MonitorFromPoint) instead, which is where the user is
looking when they press Space.
@bobo198504

Copy link
Copy Markdown
Author

Additional fix: preview opened on the wrong monitor for files on a secondary desktop

While testing, a file selected on a secondary monitor's desktop would open its preview on the primary monitor. This only happened for desktop files — files selected inside Explorer on the same monitor were positioned correctly.

Root cause

GetCurrentDesktopRectInPixel() and GetCurrentScaleFactor() determined the monitor from GetForegroundWindow(). When the selected file sits on the desktop, the foreground window is the desktop window (Progman), and Screen.FromHandle/DWM always resolve it to the primary monitor, so the preview landed there.

Fix

Determine the monitor from the cursor position instead — when the user presses Space, the cursor is on the monitor where the file is:

  • WindowHelper.GetCurrentDesktopRectInPixel()Screen.FromPoint(Cursor.Position)
  • DisplayDeviceHelper.GetCurrentScaleFactor()MonitorFromPoint(GetCursorPos())

Added MonitorFromPoint, GetCursorPos and a POINT struct to the User32 bindings. Verified: desktop files now preview on the correct secondary monitor, Explorer-on-negative-monitor still works.

…e-race overflow

MediaInfoViewer:
- Set MediaInfo's output language from the UI culture so field labels render in the
  user's language (e.g. Chinese), with a fallback to English.
- Translate PE/EXE field labels that the zh-CN language CSV is missing, and normalize
  full-width fallback labels (Linker_Version → Linker_Version).
- Replace the fixed-width text block with a two-column grid (SharedSizeGroup) so every
  label/value pair aligns pixel-perfectly regardless of CJK/ASCII mixing, with a bold
  section header and a dedicated colon column.

ViewerWindow:
- Stop removing the WM_NCHITTEST hook in OnClosing. Removing it before base.OnClosing
  leaves the still-alive window unguarded for an instant, so a WM_NCHITTEST (e.g. mouse
  resting on the close button) can fall through to WPF's WindowChromeWorker and overflow.
  The HwndSource releases the hook chain when the window actually closes.
@bobo198504

Copy link
Copy Markdown
Author

Additional fixes

1. MediaInfo viewer: localized output + aligned table layout

  • Localized field labels — MediaInfo's output language now follows the UI culture (Chinese, Japanese, etc.) via Option("Language", ...), falling back to English for unsupported cultures.
  • PE/EXE field translations — the zh-CN language CSV ships missing several PE field labels (Linker_Version, Subsystem_Name, Subsystem_Version, ...), which made MediaInfo emit full-width fallbacks (Linker_Version). Those are normalized (NFKC) and translated, and the nested (Profile) token is localized too.
  • Aligned table layout — replaced the fixed-width TextBox with a two-column Grid (SharedSizeGroup) so the label column width is shared across all rows and lines up pixel-perfectly, with bold section headers and a dedicated colon column (so wrapped values never collide with the colon).

2. Close-button overflow race

OnClosing previously removed the WM_NCHITTEST hook before base.OnClosing, leaving the still-alive window unguarded for an instant. A WM_NCHITTEST arriving then (e.g. the mouse resting on the close button) fell through to WPF's WindowChromeWorker._HandleNCHitTest and could overflow. The hook is now left attached; HwndSource releases it when the window actually closes.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

crashes when dragging video preview window on non-primary monitor

1 participant