최대화가 작업표시줄을 덮던 것 — 창을 작업영역에 맞춘다

메인 창은 WindowStyle=None + WindowChrome 으로 크롬을 직접 그린다. 그 조합에서 OS 는
최대화 사각형을 작업영역이 아니라 모니터 전체를 기준으로 잡고 리사이즈 프레임만큼 더 부풀린다.
실측으로 창이 (-7,-7)~(1927,1087) — 작업영역 (0,0)~(1920,1032) 를 55px 넘겨
작업표시줄이 통째로 가려졌다. 표준 크롬 창에는 없는 문제라 WindowStyle=None 창에만 건다.

WM_GETMINMAXINFO 훅 하나로 답한다(Services/MaximizeToWorkArea.cs).
레이아웃·XAML·테마는 건드리지 않았다. 최대화 상태에서 콘텐츠가 밀려 보인다면
답은 Margin 보정이 아니라 이 훅의 계산이 틀린 것이다 — 창 사각형이 정확히 작업영역이면 잘릴 곳이 없다.

같이 처리한 것들:

· 최소 크기가 최대화를 이기는 경우. OS 는 최대화 크기를 최소 트래킹 크기까지 되밀어 올린다.
  그 값은 우리 뒤에 도는 WPF 가 MinWidth/MinHeight(940x640 DIP)로 채우므로,
  작업영역이 그보다 작으면 창이 도로 부풀어 작업표시줄을 다시 덮는다 —
  1366x768 을 125% 로 쓰면 최소 높이 800px 에 작업영역 720px 다.
  그 경우에만 WPF 의 처리를 막고 트래킹 크기를 직접 채운다. 화면에 안 들어가는 최소 크기는
  최소 크기 구실을 못 한다. 보통은 handled=false 로 두어 MinWidth/MinHeight 가 그대로 산다.

· 최대화 중 상단 6px 리사이즈 띠. 전에는 창이 화면 밖으로 밀려 있어 그 띠가 안 보였다.
  이제 타이틀바 위로 올라와 닫기 버튼 윗변이 먹혔다(실측 y=1·3·5 에서 HTTOP).
  최대화된 창은 어차피 가장자리로 크기를 못 바꾸므로 그동안 띠를 걷는다. 여백 보정이 아니라
  히트테스트 영역 보정이다 — 창 사각형은 그대로 작업영역이다.

· 자동 숨김 작업표시줄. 자동 숨김이면 작업영역이 모니터 전체와 같아지고, 그대로 꽉 채우면
  셸이 전체 화면 앱으로 보아 가장자리 호버에 반응하지 않는다 — 작업표시줄이 영영 안 나온다.
  네 변을 각각 ABM_GETAUTOHIDEBAREX 로 보고 1px 씩 물러선다. 폴백은 '한 변도 못 찾았을 때'가
  아니라 '그 변을 못 찾았을 때' 돈다(좌측 서드파티 바 하나 때문에 하단 작업표시줄이
  구제받지 못하던 구조였다). 폴백이 쓰는 두 API 는 주 작업표시줄 전용이라
  보조 모니터는 못 덮는다 — 주석을 그렇게 바로잡았다.

· 재부착 가드. 최대화 중에 두 번 붙으면 그때의 0 을 '복원 시 두께'로 기억해
  복원해도 가장자리를 끌 수 없는 창이 됐다.

진단 종료 코드가 죽어 있었다. 기본 ShutdownMode 는 OnLastWindowClose 라, 창을 띄웠다 전부 닫는
진단은 마지막 창이 닫히는 순간 WPF 가 먼저 Shutdown(0) 을 걸고 뒤이은 Shutdown(코드) 인자를
무시한다 — 리포트에 '실패 1건' 이 찍혀도 프로세스는 0 을 돌려줬다(--maxrect 첫 실행이 그랬다).
--edit-smoke·--db-smoke 도 같은 구멍이었다. 진단 분기 진입부에서 OnExplicitShutdown 으로 바꿨다.
같은 자리에서 진단 모드 UI 예외도 모달 대신 로그+Shutdown(3) 으로 돌린다 —
'진단 모드에서는 모달을 띄우지 않는다'는 규칙을 그보다 먼저 등록되는 예외 핸들러가 깨고 있었다.

신규 진단 --maxrect (10건). 기대값을 베껴 적지 않고 성질을 단언한다 —
모니터 안에 들어가는가, 자동 숨김 없는 변은 작업영역과 같은가, 있는 변은 물러섰는가,
작업표시줄과 겹치지 않는가, 최소 크기가 최대화 크기를 넘지 않는가.
전사(轉寫)하면 같은 착각을 양쪽이 공유하고 양보 폭을 1→2 로 바꾸는 순간 거짓 실패가 난다.
보내는 버퍼는 센티넬로 채운다 — 0 으로 두면 기대값이 (0,0) 인 모니터에서 훅이 아무것도
안 써도 통과한다(실제로 한 번 그렇게 통과했다). 창을 실제로 띄워 최대화·복원까지 해
OS 가 답을 지키는지와 StateChanged 배선까지 본다. 그 경로에서 화면이 한 번 번쩍인다.

실측(1920x1080·96DPI·하단 고정 작업표시줄):
  최대화 (-7,-7)~(1927,1087) → (0,0)~(1920,1032), 침범 55px → 0px
  복원 1440x920 유지, 최소 크기 940x640 유지
  최대화 상단 히트테스트 HTTOP → HTCAPTION

자동 숨김·다중 모니터·125%/150% 배율은 이 개발기에서 재현할 수 없다. 그 환경에서 --maxrect 를
한 번 돌려 봐야 한다.

게이트: 테스트 230/230, --edit-smoke 0건, --query-popup 17/17,
--db-smoke 3000 diff 0·예외 0, --db-render P062 md5 8d683835f5d81e7bb41c79071d6bf954 동일,
--dialog-shots 51개, --maxrect 10/10.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Msystech
2026-08-13 18:52:19 +09:00
co-authored by Claude Opus 5
parent 749de6b566
commit 8a7cc65099
5 changed files with 838 additions and 0 deletions
+26
View File
@@ -20,6 +20,9 @@ public partial class App : Application
/// <summary>Dispatcher 예외 폭주 차단용 — 10초 내 5회면 강제 종료(무한 팝업 루프 방지)</summary>
private readonly Queue<DateTime> recentCrashes = new();
/// <summary>진단 플래그로 기동했는가 — 이 모드에서는 어떤 경로로도 모달을 띄우지 않는다</summary>
private bool diagnosticMode;
protected override void OnStartup(StartupEventArgs e)
{
base.OnStartup(e);
@@ -37,6 +40,13 @@ public partial class App : Application
if (Services.StartupArguments.IsDiagnostic(e.Args))
{
diagnosticMode = true;
// 기본 ShutdownMode 는 OnLastWindowClose 다. 창을 띄웠다 전부 닫는 진단
// (--maxrect, --edit-smoke, --dialog-shots …)은 마지막 창을 닫는 순간 WPF 가 먼저
// Shutdown(0) 을 걸어 버리고, 뒤이은 Shutdown(RunDiagnostic(...)) 인자는 무시된다 —
// 리포트에 '실패 3건' 이 찍혀도 프로세스는 0 을 돌려준다(실제로 그 상태였다).
// 스크립트가 종료코드로 판정하면 실패를 성공으로 읽는다.
ShutdownMode = ShutdownMode.OnExplicitShutdown;
Services.UserSession.InitializeDiagnostic();
Shutdown(RunDiagnostic(e.Args));
return;
@@ -89,6 +99,17 @@ public partial class App : Application
{
var code = Services.AppLog.Error("처리되지 않은 예외(UI)", e.Exception);
// 진단 모드에서는 절대 모달을 띄우지 않는다 — 아래 MessageBox 는 사람이 누르기 전까지 돌아오지 않아
// 무인 실행이 타임아웃까지 매달린다. 이 핸들러가 진단 분기보다 먼저 등록되므로(로그를 남기려고)
// RunDiagnostic 의 "모달 금지" 규칙을 여기서도 지켜야 앞뒤가 맞는다.
if (diagnosticMode)
{
e.Handled = true;
Console.Error.WriteLine($"진단 중 UI 예외 — 오류 코드: {code}");
Shutdown(3);
return;
}
var now = DateTime.UtcNow;
recentCrashes.Enqueue(now);
while (recentCrashes.Count > 0 && (now - recentCrashes.Peek()).TotalSeconds > 10)
@@ -249,6 +270,11 @@ public partial class App : Application
return Diagnostics.DialogShots.Run(args[1]);
}
if (args.Length >= 1 && args[0] == "--maxrect")
{
return Diagnostics.MaximizeCheck.Run(args.Length >= 2 ? args[1] : null);
}
// 진단 모드에서는 모달을 띄우지 않는다 — 스크립트 옵션 오타 하나로 무인 실행이 여기서
// 영원히 멈춘다. WinExe 라 콘솔이 없어 stderr 는 호출자가 리다이렉트할 때만 보이므로
// 로그를 함께 남긴다(--db-render 가 .err.txt 로 남기는 것과 같은 이유).