모달 뒤를 어둡게 — 대화상자가 떠 있다는 것이 보이게

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 <noreply@anthropic.com>
This commit is contained in:
Msystech
2026-08-14 13:48:56 +09:00
co-authored by Claude Opus 5
parent 893d81a40e
commit d9c1eaa293
2 changed files with 110 additions and 0 deletions
+108
View File
@@ -0,0 +1,108 @@
using System.Windows;
using System.Windows.Media;
namespace SheetMe.Designer.Services;
/// <summary>
/// 모달 대화상자 뒤를 어둡게 덮는다.
///
/// <b>왜 필요한가.</b> WPF 의 <c>ShowDialog()</c> 는 입력만 막고 화면은 그대로 둔다.
/// 대화상자가 앱과 같은 밝기로 떠 있으면 "이게 떠 있는 동안은 뒤를 못 만진다"가 안 읽혀서,
/// 뒤를 클릭했다가 아무 반응이 없으면 멈춘 것으로 오해한다.
///
/// <b>왜 호출부마다 안 고쳤나.</b> ShowDialog 호출부가 17곳이고 앞으로 더 는다.
/// 한 곳이라도 빠지면 그 대화상자만 다르게 동작하는데, 그건 눈으로 전수 확인해야만 알 수 있다.
/// 창이 뜨는 순간을 전역으로 잡아 <b>모달일 때만</b> 건다(WindowChromeTheme 이 제목 표시줄 색을
/// 같은 방식으로 거는 것과 같은 자리다).
///
/// 진단 모드는 창을 모달이 아니라 화면 밖에 그냥 띄우므로 이 경로를 타지 않는다 —
/// --dialog-shots 산출물은 영향받지 않는다.
/// </summary>
public static class ModalScrim
{
#region Member Fields
/// <summary>지금 떠 있는 가림막 — 대화상자가 겹쳐 뜨면 그만큼 쌓인다</summary>
private static readonly Stack<Window> Layers = new();
#endregion
#region Methods
/// <summary>
/// 이 창이 모달이면 뒤에 가림막을 깐다 — 창이 <b>보이기 시작할 때</b> 불러야 한다.
/// </summary>
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();
};
}
/// <summary>뒤 창을 정확히 덮는다 — 최대화 상태도 그 사각형 그대로다</summary>
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
}