Skip to content

Commit bf94800

Browse files
committed
Fix maximised windows becoming larger than host display when reverted to normal
We're now rebounding the placement rect. I'm also scaling this rect to DPI of target monitor based on local experimentation, although it remains to be seen if this is required. Will do some testing
1 parent 26ba243 commit bf94800

File tree

3 files changed

+30
-19
lines changed

3 files changed

+30
-19
lines changed

src/common.py

Lines changed: 25 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,12 @@ def str_to_op(op_name: str) -> Callable[[Any, Any], bool]:
102102
raise ValueError(f'invalid operation {op_name!r}')
103103

104104

105+
def dpi_scale(x: int, dpi: int) -> int:
106+
"""Scale a number according to a DPI"""
107+
# DPI / standard DPI = scaling factor, eg: 144 / 96 = 1.5 = 150% in windows settings
108+
return int(x / (dpi / 96))
109+
110+
105111
class JSONFile:
106112
def __init__(self, file: str, *a, **kw):
107113
self._log = logging.getLogger(__name__).getChild(self.__class__.__name__ + '.' + str(id(self)))
@@ -444,27 +450,32 @@ def set_pos(self, rect: Rect, placement: Optional[Placement] = None):
444450
try:
445451
# adjust the offset for the monitor that the window is going to end up on, since it might change
446452
# if that monitor's DPI is different
447-
offset = int(
448-
self.get_border_and_shadow_thickness()
449-
# get scaling factor
450-
/ (
451-
# DPI / standard DPI = scaling factor, eg: 144 / 96 = 1.5 = 150% in windows settings
452-
GetDpiForMonitor(win32api.MonitorFromPoint(rect[:2], win32con.MONITOR_DEFAULTTONEAREST).handle)[0]
453-
/ 96
454-
)
453+
target_display_dpi = GetDpiForMonitor(
454+
win32api.MonitorFromPoint(rect[:2], win32con.MONITOR_DEFAULTTONEAREST).handle # type: ignore
455455
)
456+
offset = dpi_scale(self.get_border_and_shadow_thickness(), target_display_dpi)
457+
456458
# check if Window will fit on the Display it's being moved to. If not, adjust the rect to fit
457-
rect = self.rebound(rect, to_rect=self.get_closest_display_rect(rect[:2]), offset=offset)
459+
target_display_rect = self.get_closest_display_rect(rect[:2])
460+
rect = self.rebound(rect, to_rect=target_display_rect, offset=offset)
458461

459462
resizable = self.is_resizable()
460463
# if the window is not resizeable, make sure we don't resize it by preserving the w + h
461464
# includes 95 era system dialogs and the Outlook reminder window
462465
w, h = size_from_rect(rect) if resizable else self.get_size()
463466
# remake rect with the bounds adjusted coords
464467
rect = (*rect[:2], rect[0] + w, rect[1] + h)
465-
if placement and not resizable:
466-
# override the placement for non-resizable windows to avoid setting wrong size for unminimised state
467-
placement = (*placement[:-1], (*rect[:2], rect[0] + w, rect[1] + h))
468+
if placement:
469+
if not resizable:
470+
# override the placement for non-resizable windows to avoid setting wrong size for unminimised state
471+
placement = (*placement[:-1], (*rect[:2], rect[0] + w, rect[1] + h))
472+
elif placement[1] == win32con.SW_SHOWMAXIMIZED:
473+
# rebound placement rect so that when user drags window away from maximised position it doesn't
474+
# suddenly expand to some silly size
475+
np_rect = self.rebound(placement[4], to_rect=target_display_rect, offset=offset)
476+
# DPI scale it. From experimentation this worked best but I don't have any docs to back it up
477+
np_rect = Rect(dpi_scale(i, target_display_dpi) for i in np_rect)
478+
placement = (*placement[:-1], np_rect)
468479

469480
if placement:
470481
win32gui.SetWindowPlacement(self.id, placement)
@@ -474,12 +485,12 @@ def set_pos(self, rect: Rect, placement: Optional[Placement] = None):
474485
log.error('err moving window %s : %s' % (win32gui.GetWindowText(self.id), e))
475486

476487
def get_border_and_shadow_thickness(self):
477-
'''
488+
"""
478489
Get the size of the window's resizable border and drop shadow in pixels.
479490
480491
Unlike `WindowType.get_border_and_shadow_thickness`, this function is based on the actual
481492
shadow size of the window subject to the DPI of the monitor the window is on.
482-
'''
493+
"""
483494
# DWMWA_EXTENDED_FRAME_BOUNDS = 9 says every StackOverflow answer, and it's the 9th item in this enum:
484495
# https://learn.microsoft.com/en-us/windows/win32/api/dwmapi/ne-dwmapi-dwmwindowattribute
485496
efb = DwmGetWindowAttribute(self.id, 9)

src/main.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
import ctypes
22
import logging
33
import os
4-
import re
54
import signal
65
import time
76
from typing import Optional

src/win32_extras.py

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -21,15 +21,16 @@ def DwmGetWindowAttribute(hwnd: int, attr: int):
2121
shcore = ctypes.WinDLL('shcore')
2222

2323

24-
def GetDpiForMonitor(monitor: int):
24+
def GetDpiForMonitor(monitor: int) -> int:
2525
'''
2626
Exposes the `shcore.GetDpiForMonitor` function but takes care of the ctypes noise.
2727
2828
See: https://learn.microsoft.com/en-gb/windows/win32/api/shellscalingapi/nf-shellscalingapi-getdpiformonitor
2929
'''
3030
dpi_x = ctypes.c_uint()
31-
dpi_y = ctypes.c_uint()
32-
shcore.GetDpiForMonitor(monitor, 0, ctypes.byref(dpi_x), ctypes.byref(dpi_y)) # MDT_EFFECTIVE_DPI
33-
return dpi_x.value, dpi_y.value
31+
shcore.GetDpiForMonitor(monitor, 0, ctypes.byref(dpi_x), ctypes.byref(ctypes.c_uint())) # MDT_EFFECTIVE_DPI
32+
# from MSDocs: "The values of *dpiX and *dpiY are identical"
33+
return dpi_x.value
34+
3435

3536
__all__ = ['DwmGetWindowAttribute', 'GetDpiForMonitor']

0 commit comments

Comments
 (0)