using System.Windows; using System.Windows.Controls; using System.Windows.Documents; using System.Windows.Media; using SheetMe.Designer.ViewModels; using SheetMe.Designer.ViewModels.Controls; namespace SheetMe.Designer.Services; /// /// 인쇄 서비스 — 페이지당 FixedPage 1:1(DIP)로 조립해 인쇄. /// 캔버스와 동일한 DataTemplate 사전(App 리소스)을 사용하므로 화면=인쇄 렌더가 일치한다. /// 용지 그림자 등 편집 크롬 없이 흰 배경 + 컨트롤만 그린다. /// public static class PrintService { #region Methods /// 인쇄 대화상자 → 전체 페이지 인쇄 public static void Print(DesignerViewModel designer, string documentName) { var dialog = new System.Windows.Controls.PrintDialog(); if (dialog.ShowDialog() != true) { return; } var document = BuildFixedDocument(designer); dialog.PrintDocument(document.DocumentPaginator, $"SheetMe — {documentName}"); } /// 페이지 VM 목록 → FixedDocument (미리보기/인쇄 공용) public static FixedDocument BuildFixedDocument(DesignerViewModel designer) { var document = new FixedDocument(); foreach (var page in designer.Pages) { var fixedPage = new FixedPage { Width = page.WidthDip, Height = page.HeightDip, Background = page.PaperBrush, }; fixedPage.Children.Add(BuildPageVisual(page)); var pageContent = new PageContent(); ((System.Windows.Markup.IAddChild)pageContent).AddChild(fixedPage); document.Pages.Add(pageContent); } return document; } /// 페이지 컨트롤층 비주얼 — 크롬 없는 Canvas(그리기 순서 = 컬렉션 순서, 미리보기/인쇄 공용) public static UIElement BuildPageVisual(PageViewModel page) { var canvas = new Canvas { Width = page.WidthDip, Height = page.HeightDip, }; TextOptions.SetTextFormattingMode(canvas, TextFormattingMode.Ideal); // 종이 위 렌더는 레거시 충실 유지 — 앱 테마의 암시 TextBlock 스타일(다크 밝은 글자) 차단 var paperText = new Style(typeof(TextBlock)); paperText.Setters.Add(new Setter(TextBlock.ForegroundProperty, Brushes.Black)); canvas.Resources.Add(typeof(TextBlock), paperText); foreach (var control in page.Controls) { // 인쇄/미리보기에서 빠지는 것 셋: // ① 디자이너 임시 숨김(Hidden — 저장되지 않는 편집 전용 상태) // ② 데이터소스(MDataTable — 런타임 비가시) // ③ 레거시 런타임이 인쇄에서 빼는 것: Visible=False 또는 PrintOutPut=False. // ③ 을 오래 빠뜨려서 '인쇄 출력'을 꺼도 미리보기에 그대로 나왔다 — // 저장 전 검증 수단이 거짓말을 하고 있었다. // 캔버스는 계속 다 보여 준다(편집 중인 것을 못 보면 고칠 수 없다) — 여기만 거른다. if (control.Model.Hidden || control is DataTableViewModel || !SheetMe.Core.Catalog.PrintFilter.IsPrinted(control.Model)) { continue; } var presenter = new ContentPresenter { Content = PrintProjection(control), Width = Math.Max(1, control.Width), Height = Math.Max(1, control.Height), }; Canvas.SetLeft(presenter, control.X); Canvas.SetTop(presenter, control.Y); canvas.Children.Add(presenter); } 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 }