wpf 如何使用PrintDocument打印livecharts2图表?

wwwo4jvm  于 2023-03-31  发布在  Echarts
关注(0)|答案(1)|浏览(280)

我有几个图形与livecharts2。他们需要能够打印为单独的图表(我可以做PrintDialog.PrintVisual),但也与其他测量在一个文件(这将是变成一个PDF使用PDF打印机驱动程序。
现在,图表显示良好,使用PrintVisual也可以打印,但是当尝试创建DocumentPaginator时,页面保持非常空。我做错了什么?
下面是我的Viewmodel(主要取自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());
        }        
    }
}
s6fujrry

s6fujrry1#

您必须将FrameworkElement添加到可视化树中并在打印它之前呈现它。这样,它将被正确地测量,并且Visual将被正确地初始化。当然,将PrintDialog.PrintVisual与已经是可视化树的子元素的现有元素一起使用将省略此要求。
一些设计注意事项:

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

溶液
要临时将元素添加到可视化树中,您应该创建一个helper类。此helper将管理可视化元素的生存期。这意味着它将元素插入到可视化树中,然后将其删除。
此帮助器的用法如下所示:

<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();
}

ElementLifetime Scope.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的情况下实现流,并相应地修改现有类型:

主窗口.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();
}

主窗口.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>

PrintPreviewDialog.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();
  }
}

PrintPreviewDialog.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; }
}

相关问题