Skip to content

Commit 12f988d

Browse files
Frostziemicheljung
authored andcommitted
Improve maximized window handling.
Use native `IsZoomed` API to detect maximized windows. Correctly calculate top inset when maximized using native APIs and DPI scaling. Adjust `WM_NCCALCSIZE` logic to properly handle resize borders in both maximized and normal. Fix incorrect `bottom` using `getX()`` for height in `Rect` class.
1 parent 02dfd82 commit 12f988d

File tree

3 files changed

+84
-34
lines changed

3 files changed

+84
-34
lines changed

src/main/java/ch/micheljung/fxwindow/DecorationWindowProcedure.java

Lines changed: 65 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@
99
import javafx.scene.Node;
1010
import javafx.scene.layout.Region;
1111
import javafx.scene.robot.Robot;
12-
import javafx.stage.Stage;
1312

1413
import java.util.ArrayList;
1514
import java.util.List;
@@ -61,8 +60,7 @@ class DecorationWindowProcedure implements WinUser.WindowProc {
6160

6261
private final BaseTSD.LONG_PTR defaultWindowsProcedure;
6362
private final Features features;
64-
private WinDef.POINT ptMaxSize;
65-
private WinDef.POINT ptMaxPosition;
63+
private HWND hwnd;
6664

6765
DecorationWindowProcedure(WindowController windowController, BaseTSD.LONG_PTR defaultWindowsProcedure, Features features) {
6866
this.windowController = windowController;
@@ -72,7 +70,7 @@ class DecorationWindowProcedure implements WinUser.WindowProc {
7270
references.add(this);
7371
}
7472

75-
private static HitTestResult hitTest(Rect window, Point mouse, WindowController controller, boolean allowTopResize) {
73+
private static HitTestResult hitTest(Rect window, Point mouse, WindowController controller, boolean allowTopResize, HWND hwnd) {
7674
Region controlBox = controller.controlBox;
7775
Region titleBar = controller.getTitleBar();
7876

@@ -99,22 +97,24 @@ private static HitTestResult hitTest(Rect window, Point mouse, WindowController
9997
}
10098
}
10199

102-
// Maximized, 'top' is a negative value which needs to be compensated here
103-
int topInset = ((Stage) controller.windowRoot.getScene().getWindow()).isMaximized() ? -top : 0;
100+
// When maximized, window extends beyond screen bounds
101+
// Using Windows API instead of JavaFX's isMaximized() for accurate multi-monitor
102+
int topInset = getTopInset(hwnd);
103+
104104
if (result == HitTestResult.HTCLIENT && mouse.y <= top + topInset + frameDragHeight) {
105105
result = HitTestResult.CAPTION;
106106
}
107107

108108
if (result != HitTestResult.HTCLIENT && (
109-
isMouseOn(mouse, controlBox, topInset, window)
110-
|| isMouseOn(mouse, icon, topInset, window)
111-
|| controller.getNonCaptionNodes().stream().anyMatch(node -> isMouseOn(mouse, node, topInset, window)))) {
109+
isMouseOn(mouse, controlBox)
110+
|| isMouseOn(mouse, icon)
111+
|| controller.getNonCaptionNodes().stream().anyMatch(node -> isMouseOn(mouse, node)))) {
112112
return HitTestResult.HTCLIENT;
113113
}
114114
return result;
115115
}
116116

117-
private static boolean isMouseOn(Point mouse, Node node, int topInset, Rect window) {
117+
private static boolean isMouseOn(Point mouse, Node node) {
118118
if (node == null || !node.isManaged()) {
119119
return false;
120120
}
@@ -128,25 +128,56 @@ private static boolean isMouseOn(Point mouse, Node node, int topInset, Rect wind
128128

129129
// Can't use bounds.contains() because it deals with doubles not integers, which causes rounding issues
130130
return scaledMouseX >= bounds.getMinX() && scaledMouseX <= bounds.getMaxX()
131-
&& scaledMouseY >= bounds.getMinY() + (double) topInset
132-
&& scaledMouseY <= bounds.getMaxY() + (double) topInset;
131+
&& scaledMouseY >= bounds.getMinY()
132+
&& scaledMouseY <= bounds.getMaxY();
133133
}
134134

135-
// I have no idea how to properly detect whether it's maximized because the taskbar will interfere
136-
private boolean isMaximized(RECT rect) {
137-
if (ptMaxPosition == null) {
138-
return false;
135+
private static int getTopInset(HWND hwnd) {
136+
if (hwnd == null || !user32Ex.IsZoomed(hwnd)) {
137+
return 0;
138+
}
139+
140+
WinDef.RECT windowRect = new WinDef.RECT();
141+
if (!user32Ex.GetWindowRect(hwnd, windowRect)) {
142+
return 0;
143+
}
144+
145+
WinDef.RECT clientRect = new WinDef.RECT();
146+
if (!user32Ex.GetClientRect(hwnd, clientRect)) {
147+
return 0;
148+
}
149+
150+
WinDef.POINT clientTopLeft = new WinDef.POINT();
151+
clientTopLeft.x = clientRect.left;
152+
clientTopLeft.y = clientRect.top;
153+
if (!user32Ex.ClientToScreen(hwnd, clientTopLeft)) {
154+
return 0;
155+
}
156+
157+
int topInsetPx = clientTopLeft.y - windowRect.top;
158+
if (topInsetPx <= 0) {
159+
return 0;
139160
}
140-
short count = 0;
141-
count += (rect.top == ptMaxPosition.y) ? 1 : 0;
142-
count += (rect.left == ptMaxPosition.x) ? 1 : 0;
143-
count += ((rect.right - ptMaxPosition.x) == ptMaxSize.x) ? 1 : 0;
144-
count += ((rect.bottom - ptMaxPosition.y) == ptMaxSize.y) ? 1 : 0;
145-
return count > 2;
161+
162+
int dpi = user32Ex.GetDpiForWindow(hwnd);
163+
if (dpi == 0) {
164+
dpi = 96;
165+
}
166+
double scale = dpi / 96.0;
167+
return (int) Math.round(topInsetPx / scale);
168+
}
169+
170+
/**
171+
* Uses the Windows API IsZoomed to properly detect if a window is maximized
172+
*/
173+
private boolean isMaximized(HWND hwnd) {
174+
return user32Ex.IsZoomed(hwnd);
146175
}
147176

148177
@Override
149178
public LRESULT callback(HWND hwnd, int uMsg, WPARAM wparam, LPARAM lParam) {
179+
this.hwnd = hwnd;
180+
150181
LRESULT lresult;
151182

152183
switch (uMsg) {
@@ -166,15 +197,18 @@ public LRESULT callback(HWND hwnd, int uMsg, WPARAM wparam, LPARAM lParam) {
166197
if (wparam.intValue() != 0) {
167198
User32Ex.NCCALCSIZE_PARAMS nCalcSizeParams = new User32Ex.NCCALCSIZE_PARAMS(new Pointer(lParam.longValue()));
168199
int resizeBorderThickness = windowController.getResizeBorderThickness();
169-
// Window is maximized in which case the stage goes off-screen at the top. To avoid that, we set top to 0
170-
// but this must only be done when the window is actually being maximized. When the user drags to restore,
171-
// 'top' usually is still negative but in that case 'top' must not be set to 0.
172-
if (isMaximized(nCalcSizeParams.rgrc[0])) {
173-
nCalcSizeParams.rgrc[0].top = Math.max(nCalcSizeParams.rgrc[0].top, 0);
200+
201+
if (isMaximized(hwnd)) {
202+
nCalcSizeParams.rgrc[0].top += resizeBorderThickness;
203+
nCalcSizeParams.rgrc[0].left += resizeBorderThickness;
204+
nCalcSizeParams.rgrc[0].right -= resizeBorderThickness;
205+
nCalcSizeParams.rgrc[0].bottom -= resizeBorderThickness;
206+
} else {
207+
nCalcSizeParams.rgrc[0].left += resizeBorderThickness;
208+
nCalcSizeParams.rgrc[0].right -= resizeBorderThickness;
209+
nCalcSizeParams.rgrc[0].bottom -= resizeBorderThickness;
174210
}
175-
nCalcSizeParams.rgrc[0].right -= resizeBorderThickness;
176-
nCalcSizeParams.rgrc[0].bottom -= resizeBorderThickness;
177-
nCalcSizeParams.rgrc[0].left += resizeBorderThickness;
211+
178212
nCalcSizeParams.write();
179213
return WVR_VALIDRECTS;
180214
}
@@ -184,8 +218,6 @@ public LRESULT callback(HWND hwnd, int uMsg, WPARAM wparam, LPARAM lParam) {
184218
lresult = user32Ex.CallWindowProc(defaultWindowsProcedure, hwnd, uMsg, wparam, lParam);
185219
User32Ex.MinMaxInfo minMaxInfo = new User32Ex.MinMaxInfo(new Pointer(lParam.longValue()));
186220
minMaxInfo.read();
187-
ptMaxSize = minMaxInfo.ptMaxSize;
188-
ptMaxPosition = minMaxInfo.ptMaxPosition;
189221
return lresult;
190222

191223
default:
@@ -199,6 +231,6 @@ public LRESULT callback(HWND hwnd, int uMsg, WPARAM wparam, LPARAM lParam) {
199231
private LRESULT hitTest() {
200232
Point2D mousePosition = robot.getMousePosition();
201233

202-
return new LRESULT(hitTest(new Rect(windowController.getStage()), new Point(mousePosition), windowController, features.isAllowTopResize()).windowsValue);
234+
return new LRESULT(hitTest(new Rect(windowController.getStage()), new Point(mousePosition), windowController, features.isAllowTopResize(), hwnd).windowsValue);
203235
}
204236
}

src/main/java/ch/micheljung/fxwindow/Rect.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,6 @@ public Rect(Stage stage) {
1919
left = (int) stage.getX();
2020
right = (int) (stage.getX() + stage.getWidth());
2121
top = (int) stage.getY();
22-
bottom = (int) (stage.getX() + stage.getHeight());
22+
bottom = (int) (stage.getY() + stage.getHeight());
2323
}
2424
}

src/main/java/ch/micheljung/fxwindow/User32Ex.java

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,24 @@ public interface User32Ex extends User32 {
3232
/** Call a window procedure. */
3333
LRESULT CallWindowProc(LONG_PTR proc, HWND hWnd, int uMsg, WPARAM uParam, LPARAM lParam);
3434

35+
/**
36+
* Determines whether a window is maximized.
37+
* @see <a href="https://learn.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-iszoomed">IsZoomed documentation</a>
38+
*/
39+
boolean IsZoomed(HWND hWnd);
40+
41+
/**
42+
* Determines the DPI for a window.
43+
* @see <a href="https://docs.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-getdpiforwindow">GetDpiForWindow documentation</a>
44+
*/
45+
int GetDpiForWindow(HWND hwnd);
46+
47+
/**
48+
* Converts the client-area coordinates of a specified point to screen coordinates.
49+
* @see <a href="https://learn.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-clienttoscreen">ClientToScreen documentation</a>
50+
*/
51+
boolean ClientToScreen(HWND hwnd, POINT clientTopLeft);
52+
3553
@Structure.FieldOrder({"attribute", "data", "sizeOfData"})
3654
class WindowCompositionAttributeData extends Structure implements Structure.ByReference {
3755
public int attribute;

0 commit comments

Comments
 (0)