diff --git a/src/SheetMe.Designer/Diagnostics/EditSmoke.cs b/src/SheetMe.Designer/Diagnostics/EditSmoke.cs index 8d4fb90..dab4990 100644 --- a/src/SheetMe.Designer/Diagnostics/EditSmoke.cs +++ b/src/SheetMe.Designer/Diagnostics/EditSmoke.cs @@ -17,6 +17,21 @@ public static class EditSmoke { #region Methods /// 스모크 실행 — 결과를 리포트 파일에 기록, 실패 있으면 1 반환 + /// + /// 시각 트리의 요소 수 — 컨테이너 까지 세야 자식 필터가 들었는지 보인다. + /// Canvas 직계만 세면 컨테이너는 늘 1개라 안에서 뭐가 빠졌는지 알 수 없다. + /// + private static int CountVisuals(System.Windows.DependencyObject node) + { + var total = 1; + var count = System.Windows.Media.VisualTreeHelper.GetChildrenCount(node); + for (var i = 0; i < count; i++) + { + total += CountVisuals(System.Windows.Media.VisualTreeHelper.GetChild(node, i)); + } + return total; + } + public static int Run(string reportPath) { var lines = new List(); @@ -270,6 +285,61 @@ public static class EditSmoke $"전 {all} → 후 {afterBox}"); Check("미리보기: 라벨의 대문자 Visible 은 무시한다", afterWrongKey == all - 2, $"전 {all} → 후 {afterWrongKey} (레거시는 이 라벨을 인쇄한다)"); + + // 컨테이너 도 필터를 타는가. + // 위 검사는 전부 최상위였다. Panel·GroupBox 템플릿이 자식을 스스로 그리므로 + // 최상위만 걸러서는 컨테이너 안이 그대로 나왔다 — + // 인쇄 출력을 꺼도 인쇄됐고, 컨테이너 안 MDataTable 은 파란 DB 배지가 종이에 찍혔다. + var boxed = new FormDocument(); + var boxedPage = new FormPage { Width = 720, Height = 856 }; + boxed.Pages.Add(boxedPage); + var host = new ControlElement + { + Type = "Panel", Id = "PanelP", + Bounds = new LayoutRect { X = 20, Y = 20, W = 400, H = 200 }, + }; + foreach (var (id, y) in new[] { ("보이는라벨", 10.0), ("숨긴라벨", 40.0) }) + { + var inner = new ControlElement + { + Type = "Label", Id = id, + Bounds = new LayoutRect { X = 10, Y = y, W = 200, H = 18 }, + }; + inner.Props.SetText("Text", id); + host.Children.Add(inner); + } + host.Children.Add(new ControlElement + { + Type = "MDataTable", Id = "안쪽데이터소스", + Bounds = new LayoutRect { X = 10, Y = 90, W = 120, H = 40 }, + }); + boxedPage.Controls.Add(host); + + var boxedVm = new DesignerViewModel(boxed); + var boxedPanel = (ContainerViewModel)boxedVm.Pages[0].Controls.First(c => c.Type == "Panel"); + + // 레이아웃을 돌려야 템플릿이 실체화된다 — 안 돌리면 Canvas 와 프레젠터만 세고 + // 컨테이너 안은 존재하지도 않아 늘 같은 수가 나온다(실제로 2 → 2 로 걸렸다). + int Rendered() + { + var visual = (System.Windows.FrameworkElement) + Services.PrintService.BuildPageVisual(boxedVm.Pages[0]); + visual.Measure(new System.Windows.Size(720, 856)); + visual.Arrange(new Rect(0, 0, 720, 856)); + visual.UpdateLayout(); + return CountVisuals(visual); + } + var baseline = Rendered(); + + boxedPanel.Children.First(c => c.Model.Id == "숨긴라벨") + .Model.Props.SetText("PrintOutPut", "False"); + var afterHide = Rendered(); + Check("미리보기: 컨테이너 안의 PrintOutPut=False 도 빠진다", afterHide < baseline, + $"요소 {baseline} → {afterHide} (안 줄면 최상위만 거르고 있다)"); + + // 인쇄 필터가 캔버스로 새면 안 된다 — 편집 중인 것을 못 보면 고칠 수 없다 + Check("미리보기: 캔버스는 자식을 그대로 다 보여 준다", + boxedPanel.Children.Count == 3, $"{boxedPanel.Children.Count} (기대 3)"); } // 2) 클릭 선택 — 컨트롤 중심(핸들 반경 밖) diff --git a/src/SheetMe.Designer/Services/PrintService.cs b/src/SheetMe.Designer/Services/PrintService.cs index 56d51fa..30ae448 100644 --- a/src/SheetMe.Designer/Services/PrintService.cs +++ b/src/SheetMe.Designer/Services/PrintService.cs @@ -80,7 +80,7 @@ public static class PrintService } var presenter = new ContentPresenter { - Content = control, + Content = PrintProjection(control), Width = Math.Max(1, control.Width), Height = Math.Max(1, control.Height), }; @@ -90,5 +90,58 @@ public static class PrintService } return canvas; } + + /// + /// 컨테이너를 인쇄용으로 다시 만든 것으로 바꿔 준다. 컨테이너가 아니면 원본 그대로. + /// + /// 왜 필요한가. 위 루프는 최상위만 훑는데 Panel·GroupBox 템플릿이 자식을 스스로 그린다 + /// (ItemsSource="{Binding Children}"). 그래서 여기서 아무리 걸러도 컨테이너 안은 그대로 나왔다 — + /// Panel 안 컨트롤은 '인쇄 출력'을 꺼도 인쇄됐고, 컨테이너 안 MDataTable 은 + /// 파란 DB 배지가 종이에 찍혔다. + /// + /// 왜 플래그가 아닌가. 처음에는 컨테이너 VM 에 '인쇄 필터' 플래그를 켰다 끄는 방법을 썼는데 + /// 듣지 않았다 — WPF 는 템플릿 자식을 레이아웃 시점에 만드는데, + /// 이 함수는 프레젠터만 만들어 두고 반환하므로 자식이 실체화될 때는 이미 플래그가 꺼져 있다. + /// 플래그 수명을 인쇄 비주얼 전체로 늘리면 그동안 캔버스에서도 자식이 사라진다(VM 을 공유하므로). + /// + /// 그래서 같은 모델로 같은 타입의 VM 을 하나 더 만들고 인쇄될 자식만 담는다. + /// 타입이 같으니 템플릿이 그대로 잡히고, 모델이 같으니 배경·테두리·머리글이 전부 똑같이 나온다. + /// 자식 VM 은 새로 만들지 않고 원본을 그대로 담는다 — 해석된 글꼴·색이 유지된다. + /// 캔버스가 쓰는 VM 은 한 글자도 건드리지 않는다. + /// + private static object PrintProjection(ControlViewModel control) + { + if (control is not ContainerViewModel container) + { + return control; + } + ContainerViewModel? copy = container switch + { + PanelViewModel => new PanelViewModel(container.Model), + GroupBoxViewModel => new GroupBoxViewModel(container.Model), + // 모르는 컨테이너 타입은 손대지 않는다 — 잘못 투영하느니 원본을 그리는 쪽이 낫다 + _ => null, + }; + if (copy is null) + { + return control; + } + foreach (var child in container.Children) + { + if (child.Model.Hidden || child is DataTableViewModel + || !SheetMe.Core.Catalog.PrintFilter.IsPrinted(child.Model)) + { + continue; + } + if (PrintProjection(child) is ControlViewModel projected) + { + copy.Children.Add(projected); + } + } + // 사본은 시각 컨텍스트를 아직 못 받았다 — 원본이 해석해 둔 값을 부모 값으로 넘긴다. + // 컨테이너에 자기 글꼴 속성이 있으면 그것이 이기고, 없으면 상속값이 그대로 온다 — 양쪽 다 원본과 같다. + copy.ResolveVisualContext(container.EffectiveFont, container.Foreground); + return copy; + } #endregion }