如何使用 PrintDocument 打印 livecharts2 图表?

问题描述 投票:0回答:1

我有几个用 livecharts2 制作的图表。它们需要能够作为单独的图表打印(我可以使用 PrintDialog.PrintVisual 来打印),但也需要与一个文档中的其他测量值一起打印(将使用 pdf 打印机驱动程序将其转换为 pdf。

现在,图表显示正常并且使用 PrintVisual 也可以打印,但是当尝试创建 DocumentPaginator 时,页面仍然很空。我做错了什么?

这是我的视图模型(主要取自 livecharts2 网站上的示例):

using LiveChartsCore;
using LiveChartsCore.SkiaSharpView;
using System.Windows.Controls;
using System.Windows.Input;
using System.Windows.Media;

namespace TestGraphPrintApp
{
    class ViewModel
    {
        private ICommand _printCommand;


        public ISeries[] Series { get; set; }
            = new ISeries[]
            {
                new LineSeries<double>
                {
                    Values = new double[] { 2, 1, 3, 5, 3, 4, 6 },
                    Fill = null
                }
            };

        public ICommand PrintCommand => _printCommand ?? (_printCommand = new RelayCommand<Visual>((param) =>
        {
            var dialog = new PrintDialog();
            bool? result = dialog.ShowDialog();
            if (result.HasValue && result.Value)
            {
                ChartPaginator paginator = new ChartPaginator(this, new System.Windows.Size(dialog.PrintableAreaWidth, dialog.PrintableAreaHeight));
                dialog.PrintDocument(paginator, "test");
            }
        }, obj => true));
    }
}

我知道我不应该在 ViewModel 中执行打印代码,但对于示例来说它会执行。

现在的xaml也大部分取自livecharts2的例子,加上用于打印的datatemplate。

<Window x:Class="TestGraphPrintApp.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:lvc="clr-namespace:LiveChartsCore.SkiaSharpView.WPF;assembly=LiveChartsCore.SkiaSharpView.WPF"        
        xmlns:local="clr-namespace:TestGraphPrintApp"
        mc:Ignorable="d"
        Title="MainWindow" Height="450" Width="800">
    <Window.DataContext>
        <local:ViewModel/>
    </Window.DataContext>
    <Window.Resources>
        <DataTemplate x:Key="PrintSampleChartDataTemplate" DataType="{x:Type local:ViewModel}">           
                <lvc:CartesianChart Series="{Binding Series}" AnimationsSpeed="0" Width="800" Height="600"/>           
        </DataTemplate>
    </Window.Resources>
    
    <StackPanel>
        <lvc:CartesianChart Name="Chart" Width="800" Height="380" AnimationsSpeed="0"
        Series="{Binding Series}">
        </lvc:CartesianChart>
        <Button Content="Print" Width="Auto" HorizontalAlignment="Right" Command="{Binding PrintCommand}" CommandParameter="{Binding ElementName=Chart}"/>
    </StackPanel>   
</Window>

最后但同样重要的是,分页器:

using LiveChartsCore.SkiaSharpView.WPF;
using System;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Documents;

namespace TestGraphPrintApp
{
    class ChartPaginator : DocumentPaginator
    {
        private ViewModel _chart;        
        
        public ChartPaginator(ViewModel chart, Size pageSize)
        {
            _chart = chart;
            PageSize = pageSize;
        }

        public override bool IsPageCountValid => true;
        public override int PageCount => 1;
        public override Size PageSize { get; set; }
        public override IDocumentPaginatorSource Source => null;
        public override DocumentPage GetPage(int pageNumber)
        {            
            if (!(App.Current.MainWindow.Resources["PrintSampleChartDataTemplate"] is DataTemplate template))
            {
                throw new Exception("Could not find a DataTemplate for printing.");
            }            
            var contentPresenter = new ContentPresenter
            {
                Content = _chart,
                ContentTemplate = template,
                Width = 800,
                Height = 600
                
            };
            contentPresenter.Measure(this.PageSize);
            contentPresenter.Arrange(new Rect(0, 0, this.PageSize.Width, this.PageSize.Height));
            contentPresenter.UpdateLayout();            
            return new DocumentPage(contentPresenter, this.PageSize, new Rect(), new Rect());
        }        
    }
}
c# wpf printing document livecharts
1个回答
0
投票

您必须将

FrameworkElement
添加到可视树并在打印之前渲染它。这样它将被正确测量并且
Visual
被正确初始化。当然,将
PrintDialog.PrintVisual
与已经是可视化树的子元素的现有元素一起使用将忽略此要求。

一些设计注意事项:

  • 在 MVVM 环境中,打印必须在 View 中完成。 View Model 类不处理 View 对象,因此不处理 any 类型的对话框。
  • ChartPaginator
    不应明确依赖于应用程序中定义的资源。而是将此类依赖项作为参数传递(例如构造函数或属性参数)。这是因为应该隐藏资源密钥和资源的实际位置 (
    ResourceDictionary
    ) 等细节,以增强设计(例如可扩展性)。
  • 从引用的主
    Application.Current.MainWindow
    外部访问静态
    Window
    属性表示设计味道,通常通过将调用代码移动到主
    Window
    .
  • 来解决
  • 将接受的参数类型限制为
    ViewModel
    (在您的
    ChartPaginator
    类型中)没有好处。一个
    ContentPresenter
    可以显示任何类型(
    object
    )只要提供适当的
    DataTemplate
    。这将使您的课程高度可重用 - 免费。
  • 不要忘记让您的绑定源(例如
    ViewModel
    )实现
    INotifyPropertyChanged
    (即使属性不会改变)

解决方案

要临时将元素添加到可视化树中,您应该创建一个辅助类。这个助手将管理可视化元素的生命周期。这意味着它会将元素插入到可视化树中,然后再将其删除。

这个助手的用法如下:

<VirtualElementHost x:Name="VirtualElementHost" />
// The element that is not a child of the visual tree
var someElementToPrint = new FrameworkElement();

// Dispose the scope object to end the lifetime of the initilaized element
using ElementLifetimeScope elementLifetimeScope = this.VirtualElementHost.CreateVirtualizedElementLifetimeScope();

// Temporarily render the element in order to initialize the Visual properly
await elementLifetimeScope.LoadElementAsync(someElementToPrint);

/* Use the activated element before its lifetime is ended 
   by calling 'ElementLifetimeScope.Dispose', for example via 'using' statement or declaration.
   Disposal will trigger the element to be removed from the visual tree.
*/

VirtualElementHost.cs

public class VirtualElementHost : FrameworkElement
{
  protected override int VisualChildrenCount => this.VisualChildren.Count;
  private VisualCollection VisualChildren { get; }
  private TaskCompletionSource TaskCompletionSource { get; set; }

  public VirtualElementHost()
  {
    this.VisualChildren = new VisualCollection(this);
    this.Visibility = Visibility.Collapsed;
  }

  public ElementLifetimeScope CreateVirtualizedElementLifetimeScope() => new ElementLifetimeScope(this);

  protected override Visual GetVisualChild(int index)
    => index < 0 || index >= this.VisualChildren.Count
      ? throw new ArgumentOutOfRangeException()
      : this.VisualChildren[index];

  protected override Size MeasureOverride(Size constraint)
  {
    var maxDesiredSize = new Size();
    foreach (UIElement child in this.VisualChildren)
    {
      child.Measure(constraint);
      double maxWidth = Math.Max(maxDesiredSize.Width, child.DesiredSize.Width);
      double maxHeight = Math.Max(maxDesiredSize.Height, child.DesiredSize.Height);
      maxDesiredSize = new Size(maxWidth, maxHeight);
    }

    return maxDesiredSize;
  }

  protected override Size ArrangeOverride(Size arrangeBounds)
  {
    foreach (UIElement child in this.VisualChildren)
    {
      child.Arrange(new Rect(child.DesiredSize));
    }

    return arrangeBounds;
  }

  internal void Virtualize(FrameworkElement element)
  {
    this.Visibility = Visibility.Visible;
    _ = this.VisualChildren.Add(element);
    InvalidateMeasure();
  }

  internal void Reset()
  {
    this.Visibility = Visibility.Collapsed;
    this.VisualChildren.Clear();
  }

  private void OnElementLoaded(object sender, RoutedEventArgs e) => this.TaskCompletionSource.SetResult();
}

ElementLifetimeScope.cs

public class ElementLifetimeScope : IDisposable
{
  private bool disposedValue;
  private VirtualElementHost VirtualElementHost { get; }
  private TaskCompletionSource TaskCompletionSource { get; set; }

  internal ElementLifetimeScope(VirtualElementHost virtualElementHost) => this.VirtualElementHost = virtualElementHost;

  public async Task LoadElementAsync(FrameworkElement element)
  {
    element.Loaded += OnElementLoaded;
    this.TaskCompletionSource = new TaskCompletionSource();
    this.VirtualElementHost.Virtualize(element);
    await this.TaskCompletionSource.Task;
  }

  private void OnElementLoaded(object sender, RoutedEventArgs e) => this.TaskCompletionSource.SetResult();

  protected virtual void Dispose(bool disposing)
  {
    if (!this.disposedValue)
    {
      if (disposing)
      {
        this.VirtualElementHost.Reset();
      }

      // TODO: free unmanaged resources (unmanaged objects) and override finalizer
      // TODO: set large fields to null
      this.disposedValue = true;
    }
  }

  // // TODO: override finalizer only if 'Dispose(bool disposing)' has code to free unmanaged resources
  // ~ElementVirtualizer()
  // {
  //     // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method
  //     Dispose(disposing: false);
  // }

  public void Dispose()
  {
    // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method
    Dispose(disposing: true);
    GC.SuppressFinalize(this);
  }
}

因为沿着这条路线走下去会修改现有的可视化树,所以有可能造成布局干扰(短暂但可能可见)。
为了解决这个问题,我建议使用自定义打印对话框,将输出显示为打印预览。这样你就可以优雅地渲染你想要打印的元素。

以下示例基于上述

VirtualElementHost
并展示了如何使用自定义打印对话框来创建允许显示预览的自定义打印流程。
该示例还展示了如何在不违反 MVVM 的情况下实现流程并相应地修改现有类型:

MainWindow.xaml.cs

private async void OnPrintReportButtonClicked(object sender, RoutedEventArgs e)
{
  if (this.Resources["PrintSampleChartDataTemplate"] is not DataTemplate template)
  {
    throw new Exception("Could not find a DataTemplate for printing.");
  }

  // Create the data model for the e.g. graphical report that should be printed
  var contentModel = new ViewModel();
  var contentPresenter = new ContentPresenter
  {
    Content = contentModel,
    ContentTemplate = template,
  };

  // Let the custom print dialog control the print flow (show a preview, interact with the user and finally print it)
  var previewPrintDialog = new PrintPreviewDialog(contentPresenter);
  _ = previewPrintDialog.ShowDialog();
}

MainWindow.xaml

<Window>
  <Window.Resources> 
    <DataTemplate x:Key="PrintSampleChartDataTemplate"
                  DataType="{x:Type local:ViewModel}">
      <lvc:CartesianChart Series="{Binding Series}"
                          AnimationsSpeed="0"
                          Width="800"
                          Height="600" Loaded="CartesianChart_Loaded" />
    </DataTemplate>
  </Window.Resources>
  
  <Button Content="Print Report"
          Click="OnPrintReportButtonClicked" />
</Window>

打印预览对话框.xaml.cs

public partial class PrintPreviewDialog : Window
{
  private FrameworkElement PrintItem { get; }
  private ElementLifetimeScope ElementLifetimeScope { get; set; }

  public PrintPreviewDialog(FrameworkElement printItem)
  {
    this.PrintItem = printItem;
    InitializeComponent();
  }

  protected override async void OnInitialized(EventArgs e)
  {
    base.OnInitialized(e);
    this.ElementLifetimeScope = this.PreviewHost.CreateVirtualizedElementLifetimeScope();
    await this.ElementLifetimeScope.LoadElementAsync(this.PrintItem);
  }

  private async void OnPrintButtonClicked(object sender, RoutedEventArgs e)
  {
    var dialog = new PrintDialog();
    bool? dialogResult = dialog.ShowDialog();
    if (!dialogResult.GetValueOrDefault())
    {
      return;
    }

    var paginator = new ChartPaginator(this.PrintItem, new System.Windows.Size(dialog.PrintableAreaWidth, dialog.PrintableAreaHeight));
    dialog.PrintDocument(paginator, "test");
    this.DialogResult = true;
    this.ElementLifetimeScope.Dispose();
    Close();
  }
}

打印预览对话框.xaml

<Window>
  <StackPanel>
    <StackPanel Orientation="Horizontal">
      <Button IsDefault="True"
              Click="OnPrintButtonClicked" 
              Content="Ok" />
      <Button IsCancel="True" 
              Content="Cancel" />
    </StackPanel>

    <local:VirtualElementHost x:Name="PreviewHost" />
  </StackPanel>
</Window>

ChartPaginator.cs

public class ChartPaginator : DocumentPaginator
{
  private Visual PageContent { get; }

  public ChartPaginator(Visual item, Size pageSize)
  {
    this.PageContent = item;
    this.PageSize = pageSize;
  }

  public override bool IsPageCountValid => true;
  public override int PageCount => 1;
  public override Size PageSize { get; set; }
  public override IDocumentPaginatorSource Source => null;
  public override DocumentPage GetPage(int pageNumber) => new DocumentPage(this.PageContent, this.PageSize, new Rect(), new Rect());
}

ViewModel.cs

class ViewModel : INotifyPropertyChanged
{
  public ViewModel() => this.Series = new ISeries[]
  {
    new LineSeries<double>
    {
      Values = new double[] { 2, 1, 3, 5, 3, 4, 6 },
      Fill = null
    }
  };

  public ISeries[] Series { get; set; }
}
© www.soinside.com 2019 - 2024. All rights reserved.