From d9c1eaa293425fe4742e3630e1595bc53127db62 Mon Sep 17 00:00:00 2001 From: Msystech Date: Fri, 14 Aug 2026 13:48:56 +0900 Subject: [PATCH] =?UTF-8?q?=EB=AA=A8=EB=8B=AC=20=EB=92=A4=EB=A5=BC=20?= =?UTF-8?q?=EC=96=B4=EB=91=A1=EA=B2=8C=20=E2=80=94=20=EB=8C=80=ED=99=94?= =?UTF-8?q?=EC=83=81=EC=9E=90=EA=B0=80=20=EB=96=A0=20=EC=9E=88=EB=8B=A4?= =?UTF-8?q?=EB=8A=94=20=EA=B2=83=EC=9D=B4=20=EB=B3=B4=EC=9D=B4=EA=B2=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit WPF 의 ShowDialog 는 입력만 막고 화면은 그대로 둔다. 대화상자가 앱과 같은 밝기로 떠 있으면 "이게 떠 있는 동안은 뒤를 못 만진다"가 안 읽혀서, 뒤를 클릭했다가 아무 반응이 없으면 멈춘 것으로 오해한다. 창이 뜨는 순간을 전역으로 잡아 모달일 때만 반투명 검정 창을 뒤에 깐다. 호출부마다 고치지 않은 이유는 ShowDialog 가 17곳이고 앞으로 더 늘기 때문이다 — 한 곳이라도 빠지면 그 대화상자만 다르게 동작하는데 그건 눈으로 전수 확인해야만 알 수 있다. 제목 표시줄 색을 거는 자리(WindowChromeTheme)와 같은 곳에 나란히 걸었다. 모달 판정은 ComponentDispatcher.IsThreadModal 이다. 진단 모드는 창을 모달이 아니라 화면 밖에 그냥 띄우므로 이 경로를 안 탄다 — --dialog-shots 57파일 그대로다. 가림막은 뒤 창의 이동·크기·상태 변화를 따라간다. 안 따라가면 창을 옮겼을 때 어두운 사각형만 제자리에 남는다. 최대화된 창은 Left/Top 이 '복원했을 때의 값'이라 그대로 쓰면 엉뚱한 데 깔리므로 화면 좌표를 직접 물어 DIP 로 되돌린다. 대화상자가 겹쳐 뜨면 그만큼 쌓이고, 각자 닫힐 때 제 것만 걷는다. 게이트: 테스트 267/267, --edit-smoke 0실패, --query-popup 17/17, --maxrect 10/10, --db-smoke 3000 diff 0·예외 0, --db-render P062 md5 8d683835f5d81e7bb41c79071d6bf954 동일, --dialog-shots 57파일. Co-Authored-By: Claude Opus 5 --- src/SheetMe.Designer/App.xaml.cs | 2 + src/SheetMe.Designer/Services/ModalScrim.cs | 108 ++++++++++++++++++++ 2 files changed, 110 insertions(+) create mode 100644 src/SheetMe.Designer/Services/ModalScrim.cs diff --git a/src/SheetMe.Designer/App.xaml.cs b/src/SheetMe.Designer/App.xaml.cs index a655471..a34d533 100644 --- a/src/SheetMe.Designer/App.xaml.cs +++ b/src/SheetMe.Designer/App.xaml.cs @@ -77,6 +77,8 @@ public partial class App : Application if (sender is Window window) { Services.WindowChromeTheme.Apply(window, !Services.ThemeManager.IsLight); + // 모달이면 뒤를 어둡게 — 여기서 한 번 걸어야 새 대화상자에서 빠지지 않는다 + Services.ModalScrim.Attach(window); } })); diff --git a/src/SheetMe.Designer/Services/ModalScrim.cs b/src/SheetMe.Designer/Services/ModalScrim.cs new file mode 100644 index 0000000..88c636b --- /dev/null +++ b/src/SheetMe.Designer/Services/ModalScrim.cs @@ -0,0 +1,108 @@ +using System.Windows; +using System.Windows.Media; + + +namespace SheetMe.Designer.Services; + +/// +/// 모달 대화상자 뒤를 어둡게 덮는다. +/// +/// 왜 필요한가. WPF 의 ShowDialog() 는 입력만 막고 화면은 그대로 둔다. +/// 대화상자가 앱과 같은 밝기로 떠 있으면 "이게 떠 있는 동안은 뒤를 못 만진다"가 안 읽혀서, +/// 뒤를 클릭했다가 아무 반응이 없으면 멈춘 것으로 오해한다. +/// +/// 왜 호출부마다 안 고쳤나. ShowDialog 호출부가 17곳이고 앞으로 더 는다. +/// 한 곳이라도 빠지면 그 대화상자만 다르게 동작하는데, 그건 눈으로 전수 확인해야만 알 수 있다. +/// 창이 뜨는 순간을 전역으로 잡아 모달일 때만 건다(WindowChromeTheme 이 제목 표시줄 색을 +/// 같은 방식으로 거는 것과 같은 자리다). +/// +/// 진단 모드는 창을 모달이 아니라 화면 밖에 그냥 띄우므로 이 경로를 타지 않는다 — +/// --dialog-shots 산출물은 영향받지 않는다. +/// +public static class ModalScrim +{ + #region Member Fields + /// 지금 떠 있는 가림막 — 대화상자가 겹쳐 뜨면 그만큼 쌓인다 + private static readonly Stack Layers = new(); + #endregion + + #region Methods + /// + /// 이 창이 모달이면 뒤에 가림막을 깐다 — 창이 보이기 시작할 때 불러야 한다. + /// + public static void Attach(Window window) + { + // 모달이 아닌 창(메인 셸, 진단 렌더용 오프스크린 창)은 대상이 아니다 + if (!System.Windows.Interop.ComponentDispatcher.IsThreadModal) + { + return; + } + + var behind = window.Owner ?? Application.Current?.MainWindow; + if (behind is null || ReferenceEquals(behind, window) || !behind.IsVisible) + { + return; + } + + var scrim = new Window + { + WindowStyle = WindowStyle.None, + ResizeMode = ResizeMode.NoResize, + AllowsTransparency = true, + Background = new SolidColorBrush(Color.FromArgb(0x66, 0, 0, 0)), + ShowInTaskbar = false, + ShowActivated = false, + IsHitTestVisible = false, + Focusable = false, + WindowStartupLocation = WindowStartupLocation.Manual, + Owner = behind, + }; + Cover(scrim, behind); + // 뒤 창이 움직이거나 크기가 바뀌면 가림막도 따라가야 한다 — 안 그러면 어두운 사각형만 남는다 + void Follow(object? sender, EventArgs e) => Cover(scrim, behind); + behind.LocationChanged += Follow; + behind.SizeChanged += Follow; + behind.StateChanged += Follow; + + scrim.Show(); + // 가림막을 나중에 띄웠으니 대화상자를 다시 앞으로 올린다 + window.Activate(); + Layers.Push(scrim); + + window.Closed += (_, _) => + { + behind.LocationChanged -= Follow; + behind.SizeChanged -= Follow; + behind.StateChanged -= Follow; + if (Layers.Count > 0) + { + Layers.Pop(); + } + scrim.Close(); + }; + } + + /// 뒤 창을 정확히 덮는다 — 최대화 상태도 그 사각형 그대로다 + private static void Cover(Window scrim, Window behind) + { + if (behind.WindowState == WindowState.Minimized) + { + scrim.Visibility = Visibility.Collapsed; + return; + } + scrim.Visibility = Visibility.Visible; + + // 최대화된 창은 Left/Top 이 '복원했을 때의 값'이라 그대로 쓰면 가림막이 엉뚱한 데 깔린다. + // 화면 좌표를 직접 물어 DIP 로 되돌린다. + var origin = new Point(behind.Left, behind.Top); + if (PresentationSource.FromVisual(behind) is { CompositionTarget: { } target }) + { + origin = target.TransformFromDevice.Transform(behind.PointToScreen(new Point(0, 0))); + } + scrim.Left = origin.X; + scrim.Top = origin.Y; + scrim.Width = behind.ActualWidth > 0 ? behind.ActualWidth : behind.Width; + scrim.Height = behind.ActualHeight > 0 ? behind.ActualHeight : behind.Height; + } + #endregion +}