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>
109 lines
4.3 KiB
C#
109 lines
4.3 KiB
C#
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
|
|
}
|