WPF使用XAML实现报表的一种思路(支持外部加载)
在WPF里做报告这一块以前用过多种实现方式,有用过RDLC、XPS等,看过一些开源的报表控件,跟RDLC的原理都差不多。
最近刚好又要实现报表的功能,就想使用一种新的方法:直接使用XAML实现,直接将界面呈现的内容打印出来。
打印WPF元素
在我前面的文章中,介绍过如何使用PrintDialog打印WPF元素,
https://www.cnblogs.com/zhaotianff/p/7340554.html
借助这种方法,我们可以直接打印整个WPF界面呈现的内容。
简易的报告模板
WPF提供了XamlReader/XamlWriter类来操作Xaml,可以实现XAML加载和保存。
动态加载的Xaml也是支持绑定的,我们可以借助这种模式来实现简易的报告模板功能。
首先我们创建一个报表内容,如下所示
1 <Grid x:Name="grid1"> 2 <Image Source="{Binding Image}" Stretch="Uniform" Margin="0,0,0,20"></Image> 3 <Label Content="{Binding Text}" HorizontalAlignment="Center" VerticalAlignment="Bottom" FontFamily="Arial"></Label> 4 </Grid>
然后我们使用XamlWriter类,将Xaml保存出来
1 var xaml = XamlWriter.Save(this.grid1); 2 System.IO.File.WriteAllText("Template.xaml", xaml);
保存出来的内容如下
可以看到内容这里都是为null,我们可以改成绑定,如下所示:
1 <Grid Name="grid1" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"> 2 <Image Source="{Binding Image}" Stretch="Uniform" Margin="0,0,0,20" /> 3 <Label Content="{Binding Text}" FontFamily="Arial" HorizontalAlignment="Center" VerticalAlignment="Bottom" /> 4 </Grid>
定义一个ViewModel用于绑定
这里创建了几个简单的属性用于界面显示
1 public class ReportViewModel : INotifyPropertyChanged 2 { 3 private string image; 4 5 public string Image 6 { 7 get => this.image; 8 set 9 { 10 this.image = value; 11 RaiseChanged("Image"); 12 } 13 } 14 15 private string text; 16 public string Text 17 { 18 get => this.text; 19 set 20 { 21 this.text = value; 22 RaiseChanged("Text"); 23 } 24 } 25 26 private int width; 27 public int Width 28 { 29 get => this.width; 30 set 31 { 32 this.width = value; 33 RaiseChanged("Width"); 34 } 35 } 36 37 private int height; 38 public int Height 39 { 40 get => this.height; 41 set 42 { 43 this.height = value; 44 RaiseChanged("Height"); 45 } 46 } 47 48 public event PropertyChangedEventHandler? PropertyChanged; 49 50 private void RaiseChanged(string propertyName) 51 { 52 PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); 53 } 54 }
新建一个界面,用于加载这个报告模板显示
1 <Grid Name="grid"> 2 <Grid.RowDefinitions> 3 <RowDefinition/> 4 <RowDefinition Height="30"/> 5 </Grid.RowDefinitions> 6 7 <Button Content="加载报告内容" HorizontalAlignment="Center" Grid.Row="1" VerticalAlignment="Center" Margin="-120,0,0,0" Width="88" Height="28" Click="Button_Click"></Button> 8 <Button Content="打印" HorizontalAlignment="Center" Grid.Row="1" VerticalAlignment="Center" Margin="120,0,0,0" Width="88" Height="28" Click="Button_Click_1"></Button> 9 10 </Grid>
加载报告内容按钮事件
1 private void Button_Click(object sender, RoutedEventArgs e) 2 { 3 var xaml = File.ReadAllText(Environment.CurrentDirectory + "\\Template.xaml"); 4 using (StringReader stringReader = new StringReader(xaml)) 5 { 6 XmlReader xmlReader = XmlReader.Create(stringReader); 7 dynamicGrid = (Grid)XamlReader.Load(xmlReader); //dynamicGrid为全局Grid变量 8 this.grid.Children.Add(dynamicGrid); 9 ReportViewModel reportModel = new ReportViewModel(); 10 reportModel.Image = "https://myfreetime.cn/usr/uploads/2024/4/%E6%B8%85%E5%B9%B3%E8%B0%83%C2%B7%E5%90%8D%E8%8A%B1%E5%80%BE%E5%9B%BD%E4%B8%A4%E7%9B%B8%E6%AC%A2/jk.jpg"; 11 reportModel.Text = "HelloWorld"; 12 reportModel.Width = 300; 13 reportModel.Height = 600; 14 dynamicGrid.DataContext = reportModel; 15 } 16 }
在界面点击加载后,可以看到内容被显示出来了
打印按钮事件
1 private void Button_Click_1(object sender, RoutedEventArgs e) 2 { 3 PrintDialog pd = new PrintDialog(); 4 Window window = Window.GetWindow(dynamicGrid); 5 Point point = dynamicGrid.TransformToAncestor(window).Transform(new Point(0, 0));//获取当前控件 的坐标 6 dynamicGrid.Measure(new Size(dynamicGrid.ActualWidth, dynamicGrid.ActualHeight)); 7 dynamicGrid.Arrange(new Rect(new Size(dynamicGrid.ActualWidth, dynamicGrid.ActualHeight))); 8 pd.PrintVisual(dynamicGrid, ""); 9 dynamicGrid.Arrange(new Rect(point.X, point.Y, dynamicGrid.ActualWidth, dynamicGrid.ActualHeight));//设置为原来的位置 10 }
修改本地的XAML模板文件,就可以实现动态变更模板,例如,我增加Width和Height字段显示
到这一步,我们就实现了一个简易的报告模板功能,一些小型的报表需求应该还是能满足的。最重要的是可以直接使用XAML,开发起来比较方便。
多页报告
多页报告时主要用到FixedDocument对象,首先我们创建一个FixedDocument对象,并设置参数
1 FixedDocument document = new FixedDocument(); 2 //设置纸张大小 3 document.DocumentPaginator.PageSize = new Size(600, 800);
为FixedDocument增加一页
1 //增加一页 2 FixedPage page1 = new FixedPage(); 3 page1.Width = document.DocumentPaginator.PageSize.Width; 4 page1.Height = document.DocumentPaginator.PageSize.Height;
我们这里增加一个Image控件放到第一页
1 //增加一个图像显示到第一页 2 Image image = new Image(); 3 image.Width = document.DocumentPaginator.PageSize.Width; 4 image.Height = document.DocumentPaginator.PageSize.Height; 5 image.Stretch = Stretch.Uniform; 6 image.Source = new BitmapImage(new Uri("https://myfreetime.cn/usr/uploads/2024/4/%E6%B8%85%E5%B9%B3%E8%B0%83%C2%B7%E5%90%8D%E8%8A%B1%E5%80%BE%E5%9B%BD%E4%B8%A4%E7%9B%B8%E6%AC%A2/jk.jpg")); 7 page1.Children.Add(image);
然后将页添加到FixedDocument中
1 //添加页到Document中 2 PageContent page1Content = new PageContent(); 3 ((IAddChild)page1Content).AddChild(page1); 4 document.Pages.Add(page1Content);
用上面一样的方法添加第二页内容
1 FixedPage page2 = new FixedPage(); 2 page2.Width = document.DocumentPaginator.PageSize.Width; 3 page2.Height = document.DocumentPaginator.PageSize.Height; 4 TextBlock page2Text = new TextBlock(); 5 page2Text.Text = "HelloWorld"; 6 page2Text.Width = document.DocumentPaginator.PageSize.Width; 7 page2Text.Height = document.DocumentPaginator.PageSize.Height; 8 page2Text.FontSize = 40; 9 page2Text.HorizontalAlignment = HorizontalAlignment.Center; 10 page2Text.VerticalAlignment = VerticalAlignment.Center; 11 page2.Children.Add(page2Text); 12 PageContent page2Content = new PageContent(); 13 ((IAddChild)page2Content).AddChild(page2); 14 document.Pages.Add(page2Content);
此时我们已经拥有一个多页的文档了,借用PrintDialog的PrintDocument功能,就可以打印多页文档。
1 PrintDialog printDialog = new PrintDialog(); 2 printDialog.PrintDocument(document.DocumentPaginator, "");
打印输出效果如下:
实时预览多页报告
使用DocumentViewer控件,就可以预览FixedDocument内容。
XAML
1 <DocumentViewer x:Name="viewer"></DocumentViewer>
C#
1 this.viewer.Document = document;
显示效果
从模板加载多页报告
我们将前面的模板XAML文件进行升级,使其具备多页的功能,如下:
1 <Pages> 2 <FixedPage xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"> 3 <Grid > 4 <Image Source="{Binding Image}" Stretch="Uniform" Margin="0,0,0,20" /> 5 <Label Content="{Binding Text}" FontFamily="Arial" HorizontalAlignment="Center" VerticalAlignment="Bottom" /> 6 <Label Content="{Binding Width}" FontFamily="Arial" HorizontalAlignment="Left" VerticalAlignment="Bottom" /> 7 <Label Content="{Binding Height}" FontFamily="Arial" HorizontalAlignment="Left" Margin="60,0,0,0" VerticalAlignment="Bottom" /> 8 </Grid> 9 </FixedPage> 10 <FixedPage xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"> 11 <Grid > 12 <Border CornerRadius="10"> 13 <Image Source="{Binding Image}" Stretch="Uniform" Margin="0,0,0,20" /> 14 </Border> 15 </Grid> 16 </FixedPage> 17 </Pages>
因为XamlReader是不能识别Pages标签的,所以我们需要进行简单的处理
1 //加载XAML 2 var xaml = System.IO.File.ReadAllText(Environment.CurrentDirectory + "\\Template.xaml"); 3 StringReader sr = new StringReader(xaml); 4 XDocument doc = XDocument.Load(sr); 5 var pages = doc.Root.Elements();
pages就是FixedPage的集合,此时我们再用XamlReader进行读取就可以识别了。
完整的动态读取代码如下:
1 //加载XAML 2 var xaml = System.IO.File.ReadAllText(Environment.CurrentDirectory + "\\Template.xaml"); 3 StringReader sr = new StringReader(xaml); 4 XDocument doc = XDocument.Load(sr); 5 var pages = doc.Root.Elements(); 6 7 FixedDocument document = new FixedDocument(); 8 //设置纸张大小 9 document.DocumentPaginator.PageSize = new Size(600, 800); 10 11 //动态读取第一页 12 StringReader xamlReader = new StringReader(pages.ElementAt(0).ToString()); 13 XmlReader xmlReader = XmlReader.Create(xamlReader); 14 FixedPage page1 = (FixedPage)XamlReader.Load(xmlReader); 15 var page1Container = page1.Children[0] as Grid; 16 page1Container.Width = document.DocumentPaginator.PageSize.Width; 17 page1Container.Height = document.DocumentPaginator.PageSize.Height; 18 ReportViewModel reportModel = new ReportViewModel(); 19 reportModel.Image = "https://myfreetime.cn/usr/uploads/2024/4/%E6%B8%85%E5%B9%B3%E8%B0%83%C2%B7%E5%90%8D%E8%8A%B1%E5%80%BE%E5%9B%BD%E4%B8%A4%E7%9B%B8%E6%AC%A2/jk.jpg"; 20 reportModel.Text = "HelloWorld"; 21 reportModel.Width = 300; 22 reportModel.Height = 600; 23 //设置ViewModel 24 page1.DataContext = reportModel; 25 26 //添加页到Document中 27 PageContent page1Content = new PageContent(); 28 ((IAddChild)page1Content).AddChild(page1); 29 document.Pages.Add(page1Content); 30 31 //动态读取第二页 32 StringReader xamlReader2 = new StringReader(pages.ElementAt(1).ToString()); 33 XmlReader xmlReader2 = XmlReader.Create(xamlReader2); 34 FixedPage page2 = (FixedPage)XamlReader.Load(xmlReader2); 35 var page2Container = page2.Children[0] as Grid; 36 page2Container.Width = document.DocumentPaginator.PageSize.Width; 37 page2Container.Height = document.DocumentPaginator.PageSize.Height; 38 ReportViewModel reportModel2 = new ReportViewModel(); 39 reportModel2.Image = "https://myfreetime.cn/usr/uploads/2024/4/%E6%B8%85%E5%B9%B3%E8%B0%83%C2%B7%E5%90%8D%E8%8A%B1%E5%80%BE%E5%9B%BD%E4%B8%A4%E7%9B%B8%E6%AC%A2/jk2.jpg"; 40 reportModel2.Text = "HelloWorld"; 41 //设置ViewModel 42 page2.DataContext = reportModel2; 43 44 //添加页到Document中 45 PageContent page2Content = new PageContent(); 46 ((IAddChild)page2Content).AddChild(page2); 47 document.Pages.Add(page2Content); 48 49 viewer.Document = document; 50 sr.Dispose(); 51 xamlReader.Dispose(); 52 xamlReader2.Dispose();
读取完成后,显示在DocumentViewer控件中,效果如下:
到这里基础的打印功能应该都是可以实现了,在给客户使用时,可以通过XAML设计新的报告模板进行分发。
如果需要将报告打印为PDF,可以参考我前面的文章:
https://www.cnblogs.com/zhaotianff/p/17247683.html
示例代码
参考资料:
https://www.nbdtech.com/Blog/archive/2009/03/19/wpf-printing-part-1-printing-visuals.aspx
https://www.nbdtech.com/Blog/archive/2009/04/20/wpf-printing-part-2-the-fixed-document.aspx
https://www.nbdtech.com/Blog/archive/2009/05/19/wpf-printing-part-3-ndash-sizes.aspx
https://www.nbdtech.com/Blog/archive/2009/07/09/wpf-printing-part-4-ndash-print-preview.aspx