From 12e5ec6452932ab6feea4a373090fcbb3eeb8eae Mon Sep 17 00:00:00 2001 From: Msystech Date: Thu, 20 Aug 2026 17:42:12 +0900 Subject: [PATCH] =?UTF-8?q?=EC=97=B4=EB=A6=AC=EB=8A=94=20=EC=B0=BD?= =?UTF-8?q?=EB=93=A4=EC=9D=98=20=EA=B2=B0=EC=9D=84=20=ED=86=B5=EC=9D=BC?= =?UTF-8?q?=ED=95=9C=EB=8B=A4=20=E2=80=94=20=ED=83=80=EC=9D=B4=ED=8B=80?= =?UTF-8?q?=EB=B0=94=EC=99=80=20=EA=B0=95=EC=A1=B0=20=EB=B2=84=ED=8A=BC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## 창 열둘 중 하나만 우리 옷을 입고 있었다 커스텀 크롬을 가진 것은 메인 셸과 MessageDialogView 둘뿐이었고, 나머지 열한 창(서식 이력·데이터소스 배선·글꼴 관리자·태그 선택·쿼리 편집기·마스크· 환자 선택·데이터소스 결과·레이어 필터·서식 등록·미리보기)은 윈도우 기본 타이틀바를 달고 떴다. 다크 테마에서 어두운 앱 위에 OS 바가 얹히고, 높이·글자·모서리·닫기 버튼 모양이 전부 달라 "다른 프로그램이 열렸다"로 보였다. 열한 창이 이미 전부 ThemedWindow 를 걸고 있었으므로, 그 옆에 템플릿을 가진 ThemedDialogWindow 를 만들고 각 창은 스타일 이름 한 줄만 바꿨다. 창마다 타이틀바 markup 을 복사했다면 다음에 또 갈라진다. ## WindowChrome 은 스타일로 줄 수 없었다 두 번 막혔고 둘 다 같은 뿌리다 — WindowChrome 은 Freezable 인데 창에 붙으면서 스스로 속성을 고친다. Setter.Value 에 두면 열한 창이 한 인스턴스를 나눠 쓰다 두 번째 창부터 "읽기 전용 상태라 속성을 설정할 수 없습니다"로 죽었다. x:Shared="False" 로도 안 됐다 — 공유되는 것은 리소스가 아니라 Setter 가 이미 붙든 값이다. 그래서 ctl:DialogChrome 첨부 속성이 창마다 새 인스턴스를 만들어 붙인다. 템플릿 안 닫기 버튼은 Click 핸들러를 가질 수 없으므로(템플릿에 코드비하인드가 없다) 표준 SystemCommands.CloseWindowCommand 를 쓰고 그 CommandBinding 도 여기서 단다. DialogResult 를 건드리지 않으므로 ShowDialog 는 false 를 돌려주고 호출부의 '취소' 경로가 그대로 탄다. 최대화 보정도 같이 건다. WindowStyle=None 인 창은 최대화하는 순간 작업표시줄을 덮는데 (메인 창에서 겪은 결함이다) 대화상자 열 개가 크기 조절 가능이라 타이틀바 더블클릭으로 그 상태에 들어갈 수 있다. ## 진단이 바로 그 부분에 눈이 없었다 --dialog-shots 는 window.Content 만 찍고 있었다. 그래서 이 결함을 한 번도 보여 준 적이 없다 — MessageDialogView 만 타이틀바가 사진에 나왔는데, 그건 그 창의 바가 Content 안에 있어서였다. 창 전체를 찍도록 바꿨다(템플릿의 PART_Root). 고칠 대상이 사진에 안 나오는 진단은 있으나 마나였고, 실제로 이번에도 첫 캡처가 "고쳤는데 그대로"로 보여 한 번 헛짚게 만들었다. ## 강조 버튼 — 규칙은 이미 있었다 Primary 스타일을 열한 창 중 넷만 쓰고 있었다. 그런데 IsDefault="True" 는 대부분의 창이 이미 주 동작에 달아 두었다 — 규칙을 새로 만들 필요가 없었다. 기본 버튼 = 강조로 맞춘다(서식 이력·서식 등록·마스크·레이어 필터·데이터소스 결과·환자 선택). 글꼴 관리자만 예외로 손으로 골랐다. IsDefault 가 없고 버튼이 셋인데, 강조는 고른 것만 바꾸는 '선택 조합에 적용'에 준다. '전체에 적용'은 문서 전체를 한 번에 바꾸므로 눈이 먼저 가는 자리에 두지 않았다 — 되돌리기가 있어도 되돌릴 생각을 하려면 먼저 알아채야 한다. 환자 선택의 '선택'에는 Primary 만 주고 IsDefault 는 건드리지 않았다 — 검색칸에서 Enter 는 '찾기'여야 한다. ## 판정 - 단위 시험 388 · edit-smoke 376건 전건 통과 - --dialog-shots 69장 전건 통과(창 전체 캡처로 바뀐 뒤에도) - --modal-check · --maxrect · --scale-budget 전건 통과 - --db-render P062 md5 8d683835f5d81e7bb41c79071d6bf954 불변 ## 남은 것 내용 여백은 아직 창마다 다르다 — 설명 문장이 창 위쪽 가장자리에 바짝 붙는 창이 여럿이다. 타이틀바가 생겨 덜 도드라지지만 통일된 것은 아니다. 별도로 잡을 것. Co-Authored-By: Claude Fable 5 --- src/SheetMe.Designer/Controls/DialogChrome.cs | 90 +++++++++++++++++++ .../Diagnostics/DialogShots.cs | 18 ++-- .../Themes/DesignerTheme.xaml | 64 +++++++++++++ .../Views/DataSourceResultDialogView.xaml | 4 +- .../Views/DataTableFieldDialogView.xaml | 2 +- .../Views/FontManagerDialogView.xaml | 7 +- .../Views/LayerTypeFilterDialog.xaml | 4 +- .../Views/MaskPickerDialogView.xaml | 4 +- .../Views/PatientPickerDialogView.xaml | 5 +- src/SheetMe.Designer/Views/PreviewWindow.xaml | 2 +- .../Views/QueryEditorWindow.xaml | 2 +- .../Views/RegisterSheetDialogView.xaml | 4 +- .../Views/SheetHistoryDialogView.xaml | 4 +- .../Views/TagPickerDialogView.xaml | 2 +- 14 files changed, 189 insertions(+), 23 deletions(-) create mode 100644 src/SheetMe.Designer/Controls/DialogChrome.cs diff --git a/src/SheetMe.Designer/Controls/DialogChrome.cs b/src/SheetMe.Designer/Controls/DialogChrome.cs new file mode 100644 index 0000000..02aec05 --- /dev/null +++ b/src/SheetMe.Designer/Controls/DialogChrome.cs @@ -0,0 +1,90 @@ +using System.Windows; +using System.Windows.Input; + +namespace SheetMe.Designer.Controls; + +/// +/// 대화상자 크롬을 켠다 — ThemedDialogWindow 스타일이 스타일 세터로 걸어 준다. +/// +/// 왜 첨부 속성인가. 창 12종에 같은 타이틀바를 주려면 이 필요한데, +/// 템플릿 안의 닫기 버튼은 Click 핸들러를 가질 수 없다(템플릿은 코드비하인드가 없다). +/// 그래서 표준 를 쓰고, 그 명령을 받을 +/// 을 여기서 창에 달아 준다. 창마다 코드를 넣지 않아도 된다. +/// +/// 최대화 보정도 함께 건다. WindowStyle=None 인 창은 최대화하는 순간 +/// 작업표시줄을 덮는다(메인 창에서 실제로 겪은 결함이다). 대화상자 대부분이 크기 조절 가능이라 +/// 타이틀바를 더블클릭하면 그 상태에 들어갈 수 있다 — 크롬을 켜는 이 자리에서 같이 막는다. +/// +public static class DialogChrome +{ + #region Attached Properties + /// 크롬 사용 여부 — 스타일이 True 로 세팅한다 + public static readonly DependencyProperty EnabledProperty = + DependencyProperty.RegisterAttached( + "Enabled", typeof(bool), typeof(DialogChrome), + new PropertyMetadata(false, OnEnabledChanged)); + + public static void SetEnabled(DependencyObject element, bool value) + => element.SetValue(EnabledProperty, value); + + public static bool GetEnabled(DependencyObject element) + => (bool)element.GetValue(EnabledProperty); + #endregion + + #region Methods + private static void OnEnabledChanged(DependencyObject sender, DependencyPropertyChangedEventArgs e) + { + if (sender is not Window window || e.NewValue is not true) + { + return; + } + // 같은 창에 두 번 걸리지 않게 한다 — 스타일이 다시 적용되는 경로가 있다 + foreach (CommandBinding existing in window.CommandBindings) + { + if (existing.Command == SystemCommands.CloseWindowCommand) + { + return; + } + } + // 크롬은 창마다 새로 만든다. WindowChrome 은 Freezable 이고 창에 붙으면서 + // 스스로 속성을 고치므로, 한 인스턴스를 여러 창이 나눠 쓰면 두 번째 창부터 + // "읽기 전용 상태라 속성을 설정할 수 없습니다"로 죽는다. + // 그래서 XAML 스타일 세터로는 줄 수 없다(세터 값은 한 번 평가돼 공유된다). + // + // CaptionHeight 36 = 타이틀바 높이와 같아야 그 띠를 잡고 창을 끌 수 있다. + // GlassFrameThickness 0,0,0,1 = DWM 그림자 유지. + // ResizeBorderThickness 6 은 ResizeMode 가 허용할 때만 실제로 동작한다. + System.Windows.Shell.WindowChrome.SetWindowChrome(window, new System.Windows.Shell.WindowChrome + { + CaptionHeight = 36, + ResizeBorderThickness = new Thickness(6), + GlassFrameThickness = new Thickness(0, 0, 0, 1), + CornerRadius = new CornerRadius(0), + UseAeroCaptionButtons = false, + }); + + window.CommandBindings.Add(new CommandBinding( + SystemCommands.CloseWindowCommand, + (_, args) => + { + // 창의 X 를 누른 것과 같아야 한다 — DialogResult 를 건드리지 않으므로 + // ShowDialog 는 false 를 돌려주고, 호출부의 '취소' 경로가 그대로 탄다 + SystemCommands.CloseWindow(window); + args.Handled = true; + })); + + // 크기 조절 가능한 창만 최대화될 수 있다 — 그때 작업표시줄을 덮지 않게 한다 + if (window.ResizeMode is ResizeMode.CanResize or ResizeMode.CanResizeWithGrip) + { + if (window.IsInitialized) + { + Services.MaximizeToWorkArea.Attach(window); + } + else + { + window.SourceInitialized += (_, _) => Services.MaximizeToWorkArea.Attach(window); + } + } + } + #endregion +} diff --git a/src/SheetMe.Designer/Diagnostics/DialogShots.cs b/src/SheetMe.Designer/Diagnostics/DialogShots.cs index 22f255c..d3a0f68 100644 --- a/src/SheetMe.Designer/Diagnostics/DialogShots.cs +++ b/src/SheetMe.Designer/Diagnostics/DialogShots.cs @@ -390,18 +390,26 @@ public static class DialogShots // 레이아웃·비동기 로딩(Loaded 핸들러)이 한 바퀴 돌게 한다 Pump(); - // 창 자체를 렌더한다 — Content 만 찍으면 Window.Background(B.AppBg)가 빠져 투명이 되고, - // PNG 에서 검정으로 저장돼 "다크처럼 보이는" 착시가 생긴다(라이트 결함이 가려진다). + // 창 전체를 찍는다 — Content 만 찍으면 타이틀바가 통째로 빠진다. + // + // 전에는 Content 를 찍었다. 그래서 창 크롬을 통일하기 전까지 이 진단은 + // "열한 창이 윈도우 기본 타이틀바를 달고 뜬다"는 사실을 한 번도 보여 준 적이 없었다 — + // 고쳐야 할 바로 그 부분에 눈이 없었던 셈이다. 이제 커스텀 타이틀바는 창의 Template + // 안에 있으므로 Content 만 찍으면 여전히 안 보인다. + // + // 배경을 먼저 칠하는 것은 그대로 둔다 — 투명 픽셀이 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 root = (FrameworkElement?)window.Template?.FindName("PART_Root", window) ?? content; + var width = (int)Math.Ceiling(window.ActualWidth > 0 ? window.ActualWidth : window.Width); + var height = (int)Math.Ceiling(window.ActualHeight > 0 ? window.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 }, + context.DrawRectangle(new VisualBrush(root) { Stretch = Stretch.None, AlignmentX = AlignmentX.Left, AlignmentY = AlignmentY.Top }, null, new Rect(0, 0, width, height)); } bitmap.Render(drawing); diff --git a/src/SheetMe.Designer/Themes/DesignerTheme.xaml b/src/SheetMe.Designer/Themes/DesignerTheme.xaml index 68c080f..fe94043 100644 --- a/src/SheetMe.Designer/Themes/DesignerTheme.xaml +++ b/src/SheetMe.Designer/Themes/DesignerTheme.xaml @@ -1404,4 +1404,68 @@ + + + diff --git a/src/SheetMe.Designer/Views/DataSourceResultDialogView.xaml b/src/SheetMe.Designer/Views/DataSourceResultDialogView.xaml index 569c476..5cf8c49 100644 --- a/src/SheetMe.Designer/Views/DataSourceResultDialogView.xaml +++ b/src/SheetMe.Designer/Views/DataSourceResultDialogView.xaml @@ -4,7 +4,7 @@ Title="데이터소스 조회 결과" Width="980" Height="640" MinWidth="760" MinHeight="440" WindowStartupLocation="CenterOwner" ShowInTaskbar="False" - Style="{StaticResource ThemedWindow}"> + Style="{StaticResource ThemedDialogWindow}">