별도 창 전수 점검 — 암시 Window 스타일이 파생 클래스에 적용된 적이 없었다
사용자 지적: "다른 창으로(새창으로 열리는) 실행하는 항목들도 모두 점검해줘". [근본 원인 — 대화상자 전체가 다크에서 흰 창이었다] WPF 는 암시 스타일을 요소의 **정확한 런타임 타입**으로만 찾는다. 저장소의 모든 창이 Window 를 상속한 클래스(SheetOpenDialogView : Window, QueryEditorWindow : Window …)라 DesignerTheme.xaml 의 <Style TargetType="Window"> 는 **단 한 번도 적용된 적이 없다**. 결과: 창 배경이 WPF 기본 흰색으로 남고, 그 위 글자는 암시 TextBlock 스타일(B.Ink #EDEDED)을 정상적으로 받는다 → **다크 테마에서 흰 바탕에 흰 글자**. 쿼리 편집기의 변수 목록처럼 라벨이 통째로 사라지는 화면이 나왔다. 라이트에서 눈에 덜 띈 이유는 우연이다 — 기본 흰색(#FFFFFF)이 의도값 B.AppBg(#F5F5F5)와 거의 같아서다. 그래서 지금까지 라이트만 보고는 발견되지 않았다. 수정: 스타일에 x:Key="ThemedWindow" 를 주고 창 10종에 직접 걸었다(코드로 짓는 ColorPickerWindow 는 생성자에서 TryFindResource). 순수 Window 인스턴스용 암시 스타일은 BasedOn 으로 남겼다. 앞으로 창을 추가할 때 잊지 않도록 스타일 위에 경고 주석을 붙였다. [신규 진단 --dialog-shots <출력폴더>] 이 결함은 정적 분석으로는 못 잡았다(앞선 감사에서 "대화상자들은 암시 Window 스타일을 그대로 받는다"고 잘못 결론냈다). 실입력 주입은 화면 잠금·세션 격리 상태에서 OS 가 거부한다 (SetCursorPos 무시, SendKeys "Access is denied" — 이번에도 중간부터 막혔다). 그래서 창을 화면 밖(-10000)에 띄워 RenderTargetBitmap 으로 찍는 진단을 만들었다. 창 8종 × 2테마 = 16장을 한 번에 남기고, 각 창의 **해석된 Background 와 Style 적용 여부를 텍스트로 함께 보고**한다 — 픽셀만 보면 원인을 못 가린다(실제로 이 한 줄이 원인을 확정했다). 표본 데이터는 실제 사용 시와 같은 형태로 넣었다(빈 껍데기를 찍으면 의미가 없다). 함정 2개를 코드에 남겼다. · 기본 ShutdownMode 가 OnLastWindowClose 라 찍고 닫는 순간 앱이 종료된다 → OnExplicitShutdown. · Window.Content 만 렌더하면 창 배경이 빠져 투명(=PNG 검정)이 되고, 다크처럼 보이는 착시로 라이트 결함이 가려진다 → 배경을 먼저 칠하고 그 위에 콘텐츠를 그린다. [함께 고친 것 — 여러 줄 입력이 세로 가운데 정렬] 쿼리 편집기의 SQL 이 큰 상자 한가운데 떠 있었다. 템플릿 트리거가 ScrollViewer 의 VerticalContentAlignment 만 Stretch 로 바꿨는데, 텍스트를 배치하는 건 TextBox 자신의 VerticalContentAlignment 이라 아무 효과가 없었다. Style.Triggers 로 옮겨 Top 으로 두고 여러 줄일 때 Padding 도 넉넉히 준다. 인스펙터의 여러 줄 속성 행에도 함께 적용된다. 검증: 창 8종 × 2테마 전부 재렌더해 배경 확인(다크 #0E0E0E / 라이트 #F5F5F5, 미리보기만 의도대로 B.CanvasBg). 테스트 124/124, edit-smoke 실패 0, **종이 렌더 P062 픽셀 대조 차이 0**. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
ff87353d0d
commit
308197c15e
@@ -0,0 +1,189 @@
|
||||
using System.IO;
|
||||
using System.Windows;
|
||||
using System.Windows.Media;
|
||||
using System.Windows.Media.Imaging;
|
||||
using SheetMe.Core.Models;
|
||||
using SheetMe.Data.Stores;
|
||||
using SheetMe.Designer.Services;
|
||||
using SheetMe.Designer.ViewModels;
|
||||
|
||||
namespace SheetMe.Designer.Diagnostics;
|
||||
|
||||
/// <summary>
|
||||
/// 별도 창(대화상자) 오프스크린 스냅샷 — <c>--dialog-shots <출력폴더></c>.
|
||||
///
|
||||
/// 창 10종을 다크·라이트 양 테마로 열어 PNG 로 남긴다. 목적은 하나다:
|
||||
/// <b>테마 대비 결함을 사람 손 없이 재현 가능하게 확인한다.</b>
|
||||
/// 실입력(마우스·키보드) 주입은 화면 잠금·세션 격리 상태에서 OS 가 거부하지만
|
||||
/// 이 경로는 창을 화면 밖에 띄워 RenderTargetBitmap 으로 찍으므로 그 제약을 받지 않는다.
|
||||
///
|
||||
/// 창은 화면 밖(-10000)에 <c>Show()</c> 로 띄운다 — 레이아웃이 실제로 돌아야 템플릿·트리거가
|
||||
/// 적용된 상태로 찍힌다(Measure/Arrange 만으로는 Window 크롬이 구성되지 않는다).
|
||||
/// 모든 창은 찍은 뒤 즉시 닫는다.
|
||||
/// </summary>
|
||||
public static class DialogShots
|
||||
{
|
||||
#region Methods
|
||||
public static int Run(string outputDirectory)
|
||||
{
|
||||
try
|
||||
{
|
||||
Directory.CreateDirectory(outputDirectory);
|
||||
// 기본 ShutdownMode 는 OnLastWindowClose 라, 찍고 닫는 순간 마지막 창이 사라져 앱이 종료된다
|
||||
// (그 뒤 Application.Current 가 null 이 되어 테마 교체에서 NRE). 명시 종료로 바꾼다.
|
||||
Application.Current.ShutdownMode = ShutdownMode.OnExplicitShutdown;
|
||||
var lines = new List<string>();
|
||||
|
||||
foreach (var light in new[] { false, true })
|
||||
{
|
||||
ThemeManager.Apply(light);
|
||||
var suffix = light ? "light" : "dark";
|
||||
foreach (var (name, factory) in Factories())
|
||||
{
|
||||
try
|
||||
{
|
||||
var window = factory();
|
||||
Capture(window, Path.Combine(outputDirectory, $"{name}-{suffix}.png"));
|
||||
// 창 배경이 실제로 무엇으로 해석됐는지 함께 남긴다 — 픽셀만 보면 원인을 못 가린다.
|
||||
// WPF 는 암시 스타일을 요소의 '정확한 타입'으로만 찾으므로, Window 를 상속한
|
||||
// 대화상자에는 <Style TargetType="Window"> 가 적용되지 않는다(기본 흰 배경이 된다).
|
||||
var background = window.Background is SolidColorBrush solid
|
||||
? solid.Color.ToString()
|
||||
: window.Background?.ToString() ?? "(null)";
|
||||
var styled = window.Style is not null ? "스타일적용" : "스타일없음";
|
||||
lines.Add($"OK {name}-{suffix} 배경={background} {styled}");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
lines.Add($"FAIL {name}-{suffix} — {ex.GetType().Name}: {ex.Message}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
File.WriteAllText(Path.Combine(outputDirectory, "_report.txt"),
|
||||
string.Join(Environment.NewLine, lines));
|
||||
Console.WriteLine(string.Join(Environment.NewLine, lines));
|
||||
return lines.Any(l => l.StartsWith("FAIL", StringComparison.Ordinal)) ? 1 : 0;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.Error.WriteLine($"대화상자 스냅샷 실패: {ex}");
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>창 생성기 목록 — 실제 사용 시와 같은 인자로 만든다(빈 껍데기를 찍으면 의미가 없다)</summary>
|
||||
private static List<(string Name, Func<Window> Factory)> Factories()
|
||||
{
|
||||
var designer = SampleDesigner();
|
||||
return new List<(string, Func<Window>)>
|
||||
{
|
||||
("01-sheet-open", () => new Views.SheetOpenDialogView(_ => SampleSheets())),
|
||||
("02-sheet-history", () => new Views.SheetHistoryDialogView("S999", "표본 서식", SampleVersions())),
|
||||
("03-query-editor", () => new Views.QueryEditorWindow("데이터소스",
|
||||
"SELECT ChtNum, PatNam FROM P_PatMst\nWHERE ChtNum = <<M.CMM.HISOperatingInfo.bzPatientInfo.ChtNum>>")),
|
||||
("04-tag-picker", () => new Views.TagPickerDialogView("동작 태그", SampleTags(), SampleTags()[1])),
|
||||
("05-mask-picker", () => new Views.MaskPickerDialogView("0000년 90월 90일")),
|
||||
("06-font-manager", () => new Views.FontManagerDialogView(designer)),
|
||||
("07-register-sheet", () => new Views.RegisterSheetDialogView("S999", "표본 서식")),
|
||||
("08-preview", () => new Views.PreviewWindow(designer)),
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>화면 밖에 띄워 레이아웃을 돌린 뒤 PNG 로 찍고 닫는다</summary>
|
||||
private static void Capture(Window window, string pngPath)
|
||||
{
|
||||
window.WindowStartupLocation = WindowStartupLocation.Manual;
|
||||
window.Left = -10000;
|
||||
window.Top = -10000;
|
||||
window.ShowInTaskbar = false;
|
||||
window.Show();
|
||||
window.UpdateLayout();
|
||||
// 레이아웃·비동기 로딩(Loaded 핸들러)이 한 바퀴 돌게 한다
|
||||
Pump();
|
||||
|
||||
// 창 자체를 렌더한다 — Content 만 찍으면 Window.Background(B.AppBg)가 빠져 투명이 되고,
|
||||
// PNG 에서 검정으로 저장돼 "다크처럼 보이는" 착시가 생긴다(라이트 결함이 가려진다).
|
||||
var content = (FrameworkElement)window.Content;
|
||||
var width = (int)Math.Ceiling(content.ActualWidth > 0 ? content.ActualWidth : window.Width);
|
||||
var height = (int)Math.Ceiling(content.ActualHeight > 0 ? content.ActualHeight : window.Height);
|
||||
var bitmap = new RenderTargetBitmap(Math.Max(1, width), Math.Max(1, height), 96, 96, PixelFormats.Pbgra32);
|
||||
|
||||
var drawing = new DrawingVisual();
|
||||
using (var context = drawing.RenderOpen())
|
||||
{
|
||||
context.DrawRectangle(window.Background ?? Brushes.Transparent, null, new Rect(0, 0, width, height));
|
||||
context.DrawRectangle(new VisualBrush(content) { Stretch = Stretch.None, AlignmentX = AlignmentX.Left, AlignmentY = AlignmentY.Top },
|
||||
null, new Rect(0, 0, width, height));
|
||||
}
|
||||
bitmap.Render(drawing);
|
||||
|
||||
var encoder = new PngBitmapEncoder();
|
||||
encoder.Frames.Add(BitmapFrame.Create(bitmap));
|
||||
using (var stream = File.Create(pngPath))
|
||||
{
|
||||
encoder.Save(stream);
|
||||
}
|
||||
window.Close();
|
||||
}
|
||||
|
||||
/// <summary>디스패처 큐를 비운다(Loaded/바인딩 갱신 반영)</summary>
|
||||
private static void Pump()
|
||||
{
|
||||
for (var i = 0; i < 3; i++)
|
||||
{
|
||||
System.Windows.Threading.Dispatcher.CurrentDispatcher.Invoke(
|
||||
() => { }, System.Windows.Threading.DispatcherPriority.ContextIdle);
|
||||
}
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region Methods - 표본 데이터
|
||||
private static List<SheetSummary> SampleSheets() => new()
|
||||
{
|
||||
new SheetSummary("C020", "협진기록지", true, true, "A", "의사기록"),
|
||||
new SheetSummary("C030", "Doctor's Order List", false, true, "A", "의사기록"),
|
||||
new SheetSummary("P001", "간호 기록지", true, true, "E", "간호기록"),
|
||||
new SheetSummary("F004", "임상생리신경검사", true, false, "D", "검사결과"),
|
||||
new SheetSummary("Z001", "분류 없는 서식", false, true, string.Empty, "(미분류)"),
|
||||
};
|
||||
|
||||
private static List<DesignVersionInfo> SampleVersions() => new()
|
||||
{
|
||||
new DesignVersionInfo(52443, "202608121030", "011825", false),
|
||||
new DesignVersionInfo(52380, "202607161422", "011825", true),
|
||||
new DesignVersionInfo(52241, "202607160901", "MSYS", true),
|
||||
};
|
||||
|
||||
private static string[] SampleTags() => new[]
|
||||
{
|
||||
"SetPatientName", "SetChartNumber", "EnableControl", "SetVisitDate", "ClearValue",
|
||||
};
|
||||
|
||||
/// <summary>폰트 관리자·미리보기용 표본 문서 — 컨트롤이 있어야 목록이 비지 않는다</summary>
|
||||
private static DesignerViewModel SampleDesigner()
|
||||
{
|
||||
var document = new FormDocument { FormId = "S999", Title = "표본 서식" };
|
||||
var page = Core.Serialization.LegacyXmlSerializer.CreateEmptyPage(1);
|
||||
page.Controls.Add(NewControl("Label", "Label1", "환자명", 40, 40, 120, 24));
|
||||
page.Controls.Add(NewControl("TextBox", "TextBox1", string.Empty, 170, 40, 200, 26));
|
||||
page.Controls.Add(NewControl("Label", "Label2", "생년월일", 40, 80, 120, 24));
|
||||
document.Pages.Add(page);
|
||||
return new DesignerViewModel(document);
|
||||
}
|
||||
|
||||
private static ControlElement NewControl(string type, string id, string text, double x, double y, double w, double h)
|
||||
{
|
||||
var element = new ControlElement { Type = type, Id = id };
|
||||
element.Bounds.X = x;
|
||||
element.Bounds.Y = y;
|
||||
element.Bounds.W = w;
|
||||
element.Bounds.H = h;
|
||||
if (text.Length > 0)
|
||||
{
|
||||
element.Props.SetText("Text", text);
|
||||
}
|
||||
return element;
|
||||
}
|
||||
#endregion
|
||||
}
|
||||
Reference in New Issue
Block a user