Skip to content

Commit 9d6b50d

Browse files
committed
Show a window maximized before its first show maximized (#75)
Maximize() on a window not shown yet showed it there and then, before its content was ready, and the host's own first show undid it: a Flutter runner's ShowWindow(SW_SHOWNORMAL) on its first frame restores even a maximized window (leanflutter/window_manager#412, #572). The window now stays hidden with the request recorded; whatever shows it, the show is held back and it is shown maximized right after, so it is maximized from its first visible frame. IsMaximized() reports the request; Unmaximize() takes it back. window_startup_maximize_windows_test covers it; a real Flutter app (tools/gui/flutter_window_reports_test.ps1, startup-maximize) now appears maximized and stays so, where it used to revert after a black frame.
1 parent 6dfa108 commit 9d6b50d

4 files changed

Lines changed: 122 additions & 3 deletions

File tree

‎src/platform/windows/window_windows.cpp‎

Lines changed: 31 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,13 @@ static const wchar_t* kFocusReturnWindowProperty = L"NativeAPIFocusReturnWindow"
121121
// hands the foreground back to it. Distinct from the focus-policy property above,
122122
// which only a no-activate window uses and its policy change clears.
123123
static const wchar_t* kBlurReturnWindowProperty = L"NativeAPIBlurReturnWindow";
124+
// Maximize() on a window that is not shown yet: shown later by anyone - a
125+
// Flutter runner's first-frame ShowWindow(SW_SHOWNORMAL) - it appears maximized.
126+
static const wchar_t* kShowMaximizedProperty = L"NativeAPIShowMaximized";
127+
static UINT ShowMaximizedMessage() {
128+
static const UINT message = RegisterWindowMessageW(L"NativeAPIShowMaximized");
129+
return message;
130+
}
124131
// The app-drawn maximize button (MaximizeButtonArea), owned by the HWND.
125132
static const wchar_t* kMaximizeButtonProperty = L"NativeAPIMaximizeButton";
126133
struct MaximizeButtonArea {
@@ -717,6 +724,7 @@ static LRESULT WindowLifetimeMessage(HWND hwnd, UINT message, WPARAM wp, LPARAM
717724
RemovePropW(hwnd, kNonActivatingProperty);
718725
RemovePropW(hwnd, kFocusReturnWindowProperty);
719726
RemovePropW(hwnd, kBlurReturnWindowProperty);
727+
RemovePropW(hwnd, kShowMaximizedProperty);
720728
delete static_cast<MaximizeButtonArea*>(RemovePropW(hwnd, kMaximizeButtonProperty));
721729
RemovePropW(hwnd, kVisualEffectProperty);
722730
RemovePropW(hwnd, kCornerPreferenceProperty);
@@ -740,6 +748,19 @@ static LRESULT WindowLifetimeMessage(HWND hwnd, UINT message, WPARAM wp, LPARAM
740748
if (message == WM_PARENTNOTIFY && LOWORD(wp) == WM_CREATE &&
741749
(GetWindowLongPtrW(hwnd, GWL_EXSTYLE) & WS_EX_NOACTIVATE))
742750
EnumChildWindows(hwnd, InstallFocusPolicyChild, 0);
751+
if (message == WM_WINDOWPOSCHANGING && GetPropW(hwnd, kShowMaximizedProperty)) {
752+
auto* pos = reinterpret_cast<WINDOWPOS*>(lp);
753+
if (pos && (pos->flags & SWP_SHOWWINDOW)) {
754+
// Whatever shows it would show it restored (SW_SHOWNORMAL restores even a
755+
// maximized window): keep it hidden and show it maximized right after.
756+
pos->flags &= ~SWP_SHOWWINDOW;
757+
PostMessageW(hwnd, ShowMaximizedMessage(), 0, 0);
758+
}
759+
}
760+
if (message == ShowMaximizedMessage()) {
761+
if (RemovePropW(hwnd, kShowMaximizedProperty)) ShowWindow(hwnd, SW_SHOWMAXIMIZED);
762+
return 0;
763+
}
743764
if (message == WM_WINDOWPOSCHANGED) {
744765
const auto* pos = reinterpret_cast<const WINDOWPOS*>(lp);
745766
const LRESULT result = DefSubclassProc(hwnd, message, wp, lp);
@@ -1187,12 +1208,19 @@ bool Window::IsOcclusionStateSupported() {
11871208
}
11881209

11891210
void Window::Maximize() {
1190-
if (pimpl_->hwnd_ && !IsMaximized()) {
1191-
ShowWindow(pimpl_->hwnd_, SW_MAXIMIZE);
1211+
if (!pimpl_->hwnd_ || IsMaximized()) return;
1212+
if (!IsWindowVisible(pimpl_->hwnd_)) {
1213+
// SW_MAXIMIZE would show it now, before its content is ready, and the
1214+
// host's own show (a Flutter runner's, on its first frame) would restore
1215+
// it. Shown maximized when it is shown; see kShowMaximizedProperty.
1216+
SetPropW(pimpl_->hwnd_, kShowMaximizedProperty, reinterpret_cast<HANDLE>(1));
1217+
return;
11921218
}
1219+
ShowWindow(pimpl_->hwnd_, SW_MAXIMIZE);
11931220
}
11941221

11951222
void Window::Unmaximize() {
1223+
if (pimpl_->hwnd_ && RemovePropW(pimpl_->hwnd_, kShowMaximizedProperty)) return;
11961224
if (pimpl_->hwnd_ && IsMaximized()) {
11971225
ShowWindow(pimpl_->hwnd_, SW_RESTORE);
11981226
}
@@ -1201,6 +1229,7 @@ void Window::Unmaximize() {
12011229
bool Window::IsMaximized() const {
12021230
if (!pimpl_->hwnd_)
12031231
return false;
1232+
if (GetPropW(pimpl_->hwnd_, kShowMaximizedProperty)) return true; // To be shown maximized.
12041233
WINDOWPLACEMENT wp = {};
12051234
wp.length = sizeof(WINDOWPLACEMENT);
12061235
GetWindowPlacement(pimpl_->hwnd_, &wp);

‎src/window.h‎

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -504,7 +504,10 @@ class Window : public EventEmitter<WindowEvent>, public NativeObjectProvider,
504504
* @brief Maximizes the window to fill the available screen space.
505505
*
506506
* Expands the window to occupy the maximum available area on the screen,
507-
* typically excluding taskbars and docks.
507+
* typically excluding taskbars and docks. A window that is not shown yet
508+
* stays hidden and appears maximized when it is shown, also when its host
509+
* shows it as a restored window (a Flutter runner on its first frame);
510+
* IsMaximized() already reports true, and Unmaximize() takes it back.
508511
*/
509512
void Maximize();
510513

‎tests/CMakeLists.txt‎

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,14 @@ add_executable(screen_layout_test screen_layout_test.cpp)
1515
target_link_libraries(screen_layout_test PRIVATE nativeapi)
1616
add_test(NAME screen_layout_test COMMAND screen_layout_test)
1717

18+
# window_manager#412 / #572 (#75): Maximize() before the first show.
19+
if(WIN32)
20+
add_executable(window_startup_maximize_windows_test window_startup_maximize_windows_test.cpp)
21+
target_link_libraries(window_startup_maximize_windows_test PRIVATE nativeapi comctl32)
22+
add_test(NAME window_startup_maximize_windows_test COMMAND window_startup_maximize_windows_test)
23+
set_tests_properties(window_startup_maximize_windows_test PROPERTIES TIMEOUT 20)
24+
endif()
25+
1826
# window_manager#569 (#77): WS_POPUP windows and their caption.
1927
# window_manager#534 (#78): Application::SetIcon on the application's windows.
2028
if(WIN32)
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
// leanflutter/window_manager#412 / #572 (#75): Maximize() on a window that is
2+
// not shown yet keeps it hidden, and the host's own ShowWindow(SW_SHOWNORMAL)
3+
// - a Flutter runner's, on its first frame - shows it maximized from its very
4+
// first visible frame instead of restoring it. No input.
5+
#include <windows.h>
6+
#include <commctrl.h>
7+
#include <iostream>
8+
#include "nativeapi.h"
9+
10+
namespace {
11+
int failures = 0;
12+
void Check(bool ok, const char* label) {
13+
std::cout << (ok ? "PASS " : "FAIL ") << label << std::endl;
14+
failures += !ok;
15+
}
16+
void Pump() {
17+
MSG message;
18+
for (int i = 0; i < 30; ++i) {
19+
while (PeekMessageW(&message, nullptr, 0, 0, PM_REMOVE)) {
20+
TranslateMessage(&message);
21+
DispatchMessageW(&message);
22+
}
23+
Sleep(10);
24+
}
25+
}
26+
// Whether the window was maximized at the moment it first became visible.
27+
int shown_zoomed = -1;
28+
LRESULT CALLBACK Watch(HWND hwnd, UINT message, WPARAM wp, LPARAM lp, UINT_PTR, DWORD_PTR) {
29+
if (message == WM_WINDOWPOSCHANGED && shown_zoomed < 0 &&
30+
(reinterpret_cast<WINDOWPOS*>(lp)->flags & SWP_SHOWWINDOW))
31+
shown_zoomed = IsZoomed(hwnd) ? 1 : 0;
32+
return DefSubclassProc(hwnd, message, wp, lp);
33+
}
34+
HWND Create() {
35+
WNDCLASSW cls = {};
36+
cls.lpfnWndProc = DefWindowProcW;
37+
cls.hInstance = GetModuleHandleW(nullptr);
38+
cls.hbrBackground = static_cast<HBRUSH>(GetStockObject(GRAY_BRUSH));
39+
cls.lpszClassName = L"NativeApiStartupMaximizeTest";
40+
RegisterClassW(&cls);
41+
return CreateWindowExW(0, cls.lpszClassName, L"nativeapi startup maximize", WS_OVERLAPPEDWINDOW,
42+
200, 200, 600, 400, nullptr, nullptr, cls.hInstance, nullptr);
43+
}
44+
} // namespace
45+
46+
int main() {
47+
SetProcessDPIAware();
48+
nativeapi::Application::GetInstance();
49+
50+
HWND hwnd = Create();
51+
SetWindowSubclass(hwnd, Watch, 1, 0);
52+
nativeapi::Window window(hwnd);
53+
window.Maximize();
54+
Pump();
55+
Check(!IsWindowVisible(hwnd), "Maximize() leaves a window that is not shown yet hidden");
56+
Check(window.IsMaximized(), "but reports it maximized already");
57+
ShowWindow(hwnd, SW_SHOWNORMAL); // The host's show, as a Flutter runner's.
58+
Pump();
59+
Check(IsWindowVisible(hwnd), "the host's show shows it");
60+
Check(shown_zoomed == 1, "maximized from its first visible frame");
61+
Check(IsZoomed(hwnd) && window.IsMaximized(), "and it stays maximized");
62+
window.Unmaximize();
63+
Pump();
64+
Check(!IsZoomed(hwnd), "Unmaximize() restores it");
65+
DestroyWindow(hwnd);
66+
67+
hwnd = Create();
68+
nativeapi::Window second(hwnd);
69+
second.Maximize();
70+
second.Unmaximize();
71+
Check(!second.IsMaximized(), "Unmaximize() before the show takes the request back");
72+
ShowWindow(hwnd, SW_SHOWNORMAL);
73+
Pump();
74+
Check(IsWindowVisible(hwnd) && !IsZoomed(hwnd), "and the window then shows restored");
75+
DestroyWindow(hwnd);
76+
77+
std::cout << (failures ? "FAILED" : "OK") << std::endl;
78+
return failures ? 1 : 0;
79+
}

0 commit comments

Comments
 (0)