가림막이 실제로는 안 깔리고 있었다 — 판정 시점을 고친다
직전 커밋(d9c1eaa)은 코드만 멀쩡했고 화면에는 아무 일도 일어나지 않았다.
ShowDialog() 는 창을 먼저 보이고(그때 Loaded 가 온다) <b>그다음에</b> 모달 루프에 들어간다.
그래서 Loaded 에서 읽는 ComponentDispatcher.IsThreadModal 은 아직 false 다 —
가드에 걸려 매번 그냥 반환했다. 모달 프레임이 돌기 시작한 뒤로 판정을 미룬다.
한 프레임 늦게 깔리는 것은 눈에 띄지 않는다.
이런 종류는 코드를 다시 읽어서는 안 잡힌다. 열어 봐야 안다.
그래서 --modal-check 를 만들었다: 대화상자를 실제로 ShowDialog 로 띄우고
그 순간 가림막 창이 화면에 존재하는지 센 뒤 닫는다.
이 진단을 만들면서 두 번 헛다리를 짚었고, 둘 다 검사가 스스로를 속인 경우다.
· BeginInvoke 로 세면 그 호출이 Loaded 보다 먼저 큐에 들어가 가림막을 거는 콜백보다
앞서 실행된다 — 항상 0 이 나온다. 타이머로 바꿨다.
· 진단 분기는 OnStartup 의 전역 배선보다 먼저 반환하므로 진단 모드에는 그 훅이 없다.
검사가 같은 배선을 직접 걸어야 프로덕션과 같은 것을 잰다.
실측: Loaded 시점 IsThreadModal=False / 모달 루프 안 True / 떠 있는 동안 가림막 1개 / 닫으면 0개.
게이트: 테스트 267/267, --edit-smoke 0실패, --maxrect 10/10, --dialog-shots 57파일,
--db-render P062 md5 8d683835f5d81e7bb41c79071d6bf954 동일, --modal-check 2/2.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
d9c1eaa293
commit
6ae05b73cd
@@ -277,6 +277,11 @@ public partial class App : Application
|
|||||||
return Diagnostics.SnapShots.Run(args[1]);
|
return Diagnostics.SnapShots.Run(args[1]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (args.Length >= 1 && args[0] == "--modal-check")
|
||||||
|
{
|
||||||
|
return Diagnostics.ModalCheck.Run();
|
||||||
|
}
|
||||||
|
|
||||||
if (args.Length >= 1 && args[0] == "--maxrect")
|
if (args.Length >= 1 && args[0] == "--maxrect")
|
||||||
{
|
{
|
||||||
return Diagnostics.MaximizeCheck.Run(args.Length >= 2 ? args[1] : null);
|
return Diagnostics.MaximizeCheck.Run(args.Length >= 2 ? args[1] : null);
|
||||||
|
|||||||
@@ -0,0 +1,130 @@
|
|||||||
|
using System.Windows;
|
||||||
|
using System.Windows.Threading;
|
||||||
|
|
||||||
|
namespace SheetMe.Designer.Diagnostics;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 모달 가림막 자동 검증 — <c>--modal-check</c>.
|
||||||
|
///
|
||||||
|
/// <b>무엇을 재는가.</b> 대화상자를 실제로 <c>ShowDialog()</c> 로 띄우고,
|
||||||
|
/// 그 순간 화면에 가림막 창이 실제로 존재하는지 센다.
|
||||||
|
///
|
||||||
|
/// <b>왜 필요했나.</b> 처음 만든 판정이 틀렸다. <c>ShowDialog()</c> 는 창을 먼저 보이고
|
||||||
|
/// (그때 Loaded 가 뜬다) <b>그다음에</b> 모달 루프에 들어가므로,
|
||||||
|
/// Loaded 시점의 <c>ComponentDispatcher.IsThreadModal</c> 은 아직 false 다.
|
||||||
|
/// 그래서 가림막이 한 번도 안 깔렸는데 코드는 멀쩡해 보였다 — 눈으로 열어 보기 전엔 모른다.
|
||||||
|
/// </summary>
|
||||||
|
public static class ModalCheck
|
||||||
|
{
|
||||||
|
#region Methods
|
||||||
|
public static int Run()
|
||||||
|
{
|
||||||
|
var lines = new List<string>();
|
||||||
|
var failed = 0;
|
||||||
|
|
||||||
|
void Check(string label, bool ok, string detail = "")
|
||||||
|
{
|
||||||
|
lines.Add($"{(ok ? "PASS" : "FAIL")} {label}{(detail.Length > 0 ? " — " + detail : string.Empty)}");
|
||||||
|
if (!ok)
|
||||||
|
{
|
||||||
|
failed++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
Application.Current.ShutdownMode = ShutdownMode.OnExplicitShutdown;
|
||||||
|
Services.ThemeManager.Apply(light: true);
|
||||||
|
|
||||||
|
// 실제 앱은 OnStartup 에서 이 배선을 건다. 진단 분기는 그보다 먼저 반환하므로
|
||||||
|
// 검사에서 같은 배선을 직접 걸어야 프로덕션과 같은 것을 재게 된다.
|
||||||
|
EventManager.RegisterClassHandler(typeof(Window), FrameworkElement.LoadedEvent,
|
||||||
|
new RoutedEventHandler((sender, _) =>
|
||||||
|
{
|
||||||
|
if (sender is Window loaded)
|
||||||
|
{
|
||||||
|
Services.ModalScrim.Attach(loaded);
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
|
||||||
|
// 뒤에 깔릴 창 — 실제 앱에서는 메인 셸이 이 자리다
|
||||||
|
var owner = new Window
|
||||||
|
{
|
||||||
|
Title = "가림막 검사 — 뒤 창",
|
||||||
|
Width = 600,
|
||||||
|
Height = 400,
|
||||||
|
Left = 80,
|
||||||
|
Top = 80,
|
||||||
|
ShowInTaskbar = false,
|
||||||
|
};
|
||||||
|
owner.Show();
|
||||||
|
Application.Current.MainWindow = owner;
|
||||||
|
|
||||||
|
var dialog = new Window
|
||||||
|
{
|
||||||
|
Title = "가림막 검사 — 대화상자",
|
||||||
|
Width = 300,
|
||||||
|
Height = 200,
|
||||||
|
Owner = owner,
|
||||||
|
ShowInTaskbar = false,
|
||||||
|
WindowStartupLocation = WindowStartupLocation.CenterOwner,
|
||||||
|
};
|
||||||
|
|
||||||
|
var scrimsWhileOpen = -1;
|
||||||
|
var modalAtLoaded = false;
|
||||||
|
dialog.Loaded += (_, _) =>
|
||||||
|
modalAtLoaded = System.Windows.Interop.ComponentDispatcher.IsThreadModal;
|
||||||
|
|
||||||
|
// 대화상자가 떠 있는 동안 창 목록을 세고 닫는다.
|
||||||
|
// BeginInvoke 로는 안 된다 — 이 호출이 Loaded 보다 먼저 큐에 들어가므로
|
||||||
|
// 가림막을 거는 콜백보다 앞서 실행돼 항상 0 을 센다(실제로 그렇게 헛다리를 짚었다).
|
||||||
|
var modalAtProbe = false;
|
||||||
|
var timer = new DispatcherTimer { Interval = TimeSpan.FromMilliseconds(400) };
|
||||||
|
timer.Tick += (_, _) =>
|
||||||
|
{
|
||||||
|
timer.Stop();
|
||||||
|
modalAtProbe = System.Windows.Interop.ComponentDispatcher.IsThreadModal;
|
||||||
|
scrimsWhileOpen = CountScrims();
|
||||||
|
dialog.Close();
|
||||||
|
};
|
||||||
|
timer.Start();
|
||||||
|
|
||||||
|
dialog.ShowDialog();
|
||||||
|
|
||||||
|
lines.Add($"Loaded 시점 IsThreadModal = {modalAtLoaded}");
|
||||||
|
lines.Add($"모달 루프 안 IsThreadModal = {modalAtProbe}");
|
||||||
|
lines.Add($"대화상자가 떠 있는 동안의 가림막 수 = {scrimsWhileOpen}");
|
||||||
|
|
||||||
|
Check("모달일 때 가림막이 깔린다", scrimsWhileOpen == 1,
|
||||||
|
$"실제 {scrimsWhileOpen}개");
|
||||||
|
Check("닫으면 가림막이 걷힌다", CountScrims() == 0, $"실제 {CountScrims()}개");
|
||||||
|
|
||||||
|
owner.Close();
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
lines.Add($"EXCEPTION {ex.GetType().Name}: {ex.Message}");
|
||||||
|
failed++;
|
||||||
|
}
|
||||||
|
|
||||||
|
lines.Add($"결과: 실패 {failed}건");
|
||||||
|
Console.Error.WriteLine(string.Join(Environment.NewLine, lines));
|
||||||
|
return failed == 0 ? 0 : 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>가림막은 투명 + 작업표시줄 미표시 + 소유자 있는 창이다</summary>
|
||||||
|
private static int CountScrims()
|
||||||
|
{
|
||||||
|
var count = 0;
|
||||||
|
foreach (Window window in Application.Current.Windows)
|
||||||
|
{
|
||||||
|
if (window.AllowsTransparency && !window.ShowInTaskbar && window.Owner is not null
|
||||||
|
&& window.WindowStyle == WindowStyle.None)
|
||||||
|
{
|
||||||
|
count++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return count;
|
||||||
|
}
|
||||||
|
#endregion
|
||||||
|
}
|
||||||
@@ -31,9 +31,19 @@ public static class ModalScrim
|
|||||||
/// 이 창이 모달이면 뒤에 가림막을 깐다 — 창이 <b>보이기 시작할 때</b> 불러야 한다.
|
/// 이 창이 모달이면 뒤에 가림막을 깐다 — 창이 <b>보이기 시작할 때</b> 불러야 한다.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public static void Attach(Window window)
|
public static void Attach(Window window)
|
||||||
|
{
|
||||||
|
// 모달 판정을 여기서 바로 하면 안 된다. ShowDialog() 는 창을 먼저 보이고(그때 Loaded 가 온다)
|
||||||
|
// <b>그다음에</b> 모달 루프에 들어가므로, 이 시점의 IsThreadModal 은 아직 false 다.
|
||||||
|
// 실제로 그렇게 만들어서 가림막이 한 번도 안 깔렸다(--modal-check 가 그 상태를 잡았다).
|
||||||
|
// 모달 프레임이 돌기 시작한 뒤에 다시 본다 — 한 프레임 늦게 깔리는 것은 눈에 띄지 않는다.
|
||||||
|
window.Dispatcher.BeginInvoke(new Action(() => AttachIfModal(window)),
|
||||||
|
System.Windows.Threading.DispatcherPriority.Background);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void AttachIfModal(Window window)
|
||||||
{
|
{
|
||||||
// 모달이 아닌 창(메인 셸, 진단 렌더용 오프스크린 창)은 대상이 아니다
|
// 모달이 아닌 창(메인 셸, 진단 렌더용 오프스크린 창)은 대상이 아니다
|
||||||
if (!System.Windows.Interop.ComponentDispatcher.IsThreadModal)
|
if (!System.Windows.Interop.ComponentDispatcher.IsThreadModal || !window.IsVisible)
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user