internal/nativewin/dpi_windows.go
Rama main · réplica de lectura del repositorio original.
//go:build windows
package nativewin
// Per-monitor DPI awareness.
//
// Without it Windows renders the process at 96 DPI and bitmap-stretches the
// result to the display's scale: the window is the right SIZE on a 125% or
// 150% laptop, and everything in it is blurry. That is the whole bug this file
// exists to fix, and it is entirely invisible on a 100% display, which is why
// it survived this long.
//
// Awareness is process-wide and one-way, and it changes three things at once:
//
// 1. every dimension the process gives or receives becomes PHYSICAL pixels, so
// a 940-wide window asked for at 150% comes out two thirds the intended
// size unless the caller scales it (Px);
// 2. GetSystemMetrics stops answering for the display the window is on, so the
// frame maths in window_windows.go has to ask the ...ForDpi variants
// instead (systemMetric);
// 3. dragging between monitors of different scales now delivers WM_DPICHANGED,
// which the process must act on or the window keeps its old physical size
// and the page inside it visibly changes scale.
//
// All three are handled here and in window_windows.go. WebView2 itself needs no
// help: once the process is aware, it renders the page at the real device scale
// and the CSS layout follows deviceScaleFactor as it does in a browser.
import (
"sync"
"syscall"
"unsafe"
)
const (
wmDpiChanged = 0x02E0
// DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2, which is -4 as a handle.
// V2 rather than V1 because only V2 DPI-scales the non-client frame and
// sends WM_DPICHANGED for the whole top-level tree, which is what keeps the
// frame insets in wndProc consistent with the rects it is handed.
dpiPerMonitorV2 = ^uintptr(3)
// PROCESS_PER_MONITOR_DPI_AWARE, the Windows 8.1 spelling of the same idea.
processPerMonitorDPIAware = 2
logPixelsX = 88 // GetDeviceCaps index
defaultDPI = 96
)
var (
shcore = syscall.NewLazyDLL("shcore.dll")
setProcessDpiAwarenessContext = user32.NewProc("SetProcessDpiAwarenessContext")
setProcessDpiAwareness = shcore.NewProc("SetProcessDpiAwareness")
setProcessDPIAware = user32.NewProc("SetProcessDPIAware")
getDpiForSystem = user32.NewProc("GetDpiForSystem")
getDpiForWindow = user32.NewProc("GetDpiForWindow")
getSystemMetricsForDpi = user32.NewProc("GetSystemMetricsForDpi")
getDC = user32.NewProc("GetDC")
releaseDC = user32.NewProc("ReleaseDC")
getDeviceCaps = gdi32.NewProc("GetDeviceCaps")
dpiOnce sync.Once
)
// EnableDPIAwareness makes this process per-monitor DPI aware.
//
// It MUST run before the first window exists: awareness is fixed for a window
// when it is created, so a call after webview2.NewWithOptions leaves the window
// that matters behind, aware process or not. Idempotent, and safe to call from
// more than one entry point, because both binaries that show a window want it
// and neither should have to know whether the other already asked.
//
// Every failure is silent and harmless. Three APIs are tried oldest-last: the
// per-monitor-v2 context (Windows 10 1703+), the 8.1 per-monitor enum, then the
// original system-wide switch. If all three fail -- an OS older than any of
// them, or an embedded manifest that already fixed the awareness and makes the
// call ERROR_ACCESS_DENIED -- the process simply stays where it was, which is
// today's behaviour: blurry, not broken.
func EnableDPIAwareness() {
dpiOnce.Do(func() {
if setProcessDpiAwarenessContext.Find() == nil {
if ok, _, _ := setProcessDpiAwarenessContext.Call(dpiPerMonitorV2); ok != 0 {
return
}
}
if setProcessDpiAwareness.Find() == nil {
if hr, _, _ := setProcessDpiAwareness.Call(processPerMonitorDPIAware); hr == 0 {
return
}
}
_, _, _ = setProcessDPIAware.Call()
})
}
// SystemDPI is the DPI a window will be created at, before there is a window to
// ask about. It is the primary display's, which is where a Center:true window
// lands; a second monitor at a different scale is corrected by WM_DPICHANGED
// the moment the window arrives there.
func SystemDPI() int {
if getDpiForSystem.Find() == nil {
if dpi, _, _ := getDpiForSystem.Call(); dpi != 0 {
return int(dpi)
}
}
// Windows 8.1 and older, and the belt-and-braces path if the call above
// ever returns 0: the screen DC's pixels-per-inch is the same number.
hdc, _, _ := getDC.Call(0)
if hdc == 0 {
return defaultDPI
}
defer releaseDC.Call(0, hdc)
if dpi, _, _ := getDeviceCaps.Call(hdc, logPixelsX); dpi != 0 {
return int(dpi)
}
return defaultDPI
}
// windowDPI is the DPI of the display a window is currently on.
func windowDPI(hwnd uintptr) int {
if hwnd != 0 && getDpiForWindow.Find() == nil {
if dpi, _, _ := getDpiForWindow.Call(hwnd); dpi != 0 {
return int(dpi)
}
}
return SystemDPI()
}
// Px converts a dimension written at 96 DPI into the physical pixels an aware
// process has to ask for.
//
// This is what keeps a window size in the source readable: the constants stay
// the size the design was drawn at, and the one place that talks to Win32
// converts. Rounding is to the nearest pixel, so 940 at 150% is 1410 rather
// than 1409.
func Px(n int) int {
return (n*SystemDPI() + defaultDPI/2) / defaultDPI
}
// systemMetric reads a system metric for the display hwnd is on.
//
// GetSystemMetrics answers for the process's awareness context, which for a
// per-monitor-aware process means the PRIMARY display: on a laptop at 150%
// driving an external 100% panel it returns the wrong frame thickness for
// whichever of the two the window is not on. GetSystemMetricsForDpi is the
// per-monitor spelling, and falls back to the old call on Windows 8.1.
func systemMetric(hwnd uintptr, index int) int {
if getSystemMetricsForDpi.Find() == nil {
v, _, _ := getSystemMetricsForDpi.Call(uintptr(index), uintptr(windowDPI(hwnd)))
return int(int32(v))
}
v, _, _ := getSystemMetrics.Call(uintptr(index))
return int(int32(v))
}
// applyDpiChange moves and resizes the window to the rect Windows suggests when
// it crosses onto a display with a different scale.
//
// The suggested rect is not advice to be improved on: it preserves the window's
// apparent size to the user, and ignoring it leaves a window that keeps its old
// physical size while the page inside it re-lays out at the new scale, which
// looks exactly like the app resizing itself at random.
func applyDpiChange(hwnd, lparam uintptr) {
if lparam == 0 {
return
}
r := (*winRect)(unsafe.Pointer(lparam))
_, _, _ = setWindowPos.Call(hwnd, 0,
uintptr(r.Left), uintptr(r.Top),
uintptr(r.Right-r.Left), uintptr(r.Bottom-r.Top),
swpNoZOrder|swpNoActivate)
}