Feb 18 |
Posted by Andrew on 18 February 2013 09:35 PM
|
Screenshots, XPS Printing, X-Axis Text LabelsI can’t recall the number of questions we’ve received on whether SciChart supports render to bitmap (screenshots), printing to XPS or PDF and text labels on X-Axis, so we’ve created a tutorial which encompasses all three! Hurrah! Fortunately both Render-to-Bitmap and XPS printing are both features native to WPF. We present a demonstration application which does these, plus shows a few other neat tricks to get X-Axis text labels on the chart (instead of numeric data). So here it is! Let’s get started. You will need SciChart v1.5.x for this. Downloading the Accompanying Source CodeIn many of our tutorials, we walk you through how to create it from the ground up. In this we’re going to go a little differently. We will talk you through the code so you can discover how we did screenshots and XPS export. First download the source code, you can find i.e. here: ScreenshotsXpsAndXLabels.zip If you don’t have the SciChart trial you will need to download this also and ensure Abt.Controls.SciChart.Wpf.dll is referenced. Next, running the application, you should see this output:
Drawing the Chart – ChartView.xamlLet’s begin to break this application down. The ChartView / ChartViewModel defines the XAML and data to render the chart. Here is the source for ChartView.xaml:
<UserControl x:Class="ScreenshotsAndXpsExport.ChartView" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:s="clr-namespace:Abt.Controls.SciChart;assembly=Abt.Controls.SciChart.Wpf" xmlns:ScreenshotsAndXpsExport="clr-namespace:ScreenshotsAndXpsExport" mc:Ignorable="d" d:DesignHeight="300" d:DesignWidth="300"> <UserControl.Resources> <ScreenshotsAndXpsExport:ChartViewModel x:Key="viewModel"/> </UserControl.Resources> <Grid DataContext="{StaticResource viewModel}"> <s:SciChartSurface x:Name="sciChart" s:ThemeManager.Theme="BlackSteel" ChartTitle="Developer Awesomeness vs. Programming Language" DataSet="{Binding ChartData}"> <s:SciChartSurface.RenderableSeries> <s:FastColumnRenderableSeries SeriesColor="#FF6600" FillColor="#33FF6600" DataPointWidth="0.4" /> </s:SciChartSurface.RenderableSeries> <s:SciChartSurface.YAxis> <s:NumericAxis VisibleRange="0,10" AxisTitle="Awesomeness" AxisAlignment="Left" /> </s:SciChartSurface.YAxis> <s:SciChartSurface.XAxis> <s:NumericAxis VisibleRange="-0.5,4.5" AxisTitle="Programming Language" AutoTicks="False" MajorDelta="1" MinorDelta="0.2" LabelFormatter="{Binding XLabelFormatter}"/> </s:SciChartSurface.XAxis> <s:SciChartSurface.ChartModifier> <s:CursorModifier/> </s:SciChartSurface.ChartModifier> </s:SciChartSurface> </Grid> </UserControl> There’s nothing special here. The SciChartSurface contains a single RenderableSeries (column series), and the YAxis and XAxis are of type NumericAxis. The ScIChartSurface has a single binding of DataSet to ChartData on the ChartViewModel and the DataContext of the control is set to an instance of the ChartViewModel. There is a single ChartModifier – in this case a CursorModifier. Oh wait, I’m wrong! There is something special here. Notice that the XAxis has a binding to LabelFormatter: <s:SciChartSurface.XAxis> <s:NumericAxis VisibleRange="-0.5,4.5" AxisTitle="Programming Language" AutoTicks="False" MajorDelta="1" MinorDelta="0.2" LabelFormatter="{Binding XLabelFormatter}"/> </s:SciChartSurface.XAxis> We will discuss what is a LabelFormatter later, but please make a mental note, we are setting AutoTicks=false, MajorDelta = 1, MinorDelta = 0.2 and binding to ChartViewModel.XLabelFomatter. Drawing the Chart – ChartViewModel.csLet’s have a look at the code for ChartViewModel.cs public class ChartViewModel : INotifyPropertyChanged { public event PropertyChangedEventHandler PropertyChanged; private IDataSeriesSet _chartData; private ILabelFormatter _labelFormatter; public ChartViewModel() { var dataset = new DataSeriesSet<int, double>(); var dataSeries = dataset.AddSeries(); // Create the X-Axis Labels var xAxisLabels = new[] {"Java", "Obj-C", "C#", "Fortran", "C++"}; // We append Y-values (double) and X-values are just indices to the x-axis (string) labels dataSeries.Append(Enumerable.Range(0, 5), new[] { 2.2, 5.3, 9.5, 6.7, 8.4}); ChartData = dataset; // The ILabelFormatter allows us to substitute a string value for a data-value. // In most cases this is used to format a numeric value, e.g. 1.23456 as "1.23", however // we can also use it to transform an integer value to a string (text) value XLabelFormatter = new CustomLabelFormatter(xAxisLabels); } public IDataSeriesSet ChartData { get { return _chartData; } set { _chartData = value; OnPropertyChanged("ChartData"); } } public ILabelFormatter XLabelFormatter { get { return _labelFormatter; } set { _labelFormatter = value; OnPropertyChanged("XLabelFormatter"); } } protected virtual void OnPropertyChanged(string propertyName = null) { PropertyChangedEventHandler handler = PropertyChanged; if (handler != null) handler(this, new PropertyChangedEventArgs(propertyName)); } } Some of this is standard and well known to you by now. For instance, we create a DataSeriesSet with int, double types, append some X,Y values and implement INotifyPropertyChanged. What’s new is we are creating an instance of CustomLabelFormatter. This is a class which implements ILabelFormatter, a new type introduced in ScIChart v1.5. This allows us to substitute integer values on the X-Axis for string labels. If you recall from our screenshot of Programming Language of Developer Awesomeness, the X-Axis had text labels:
Using ILabelFormatter to Format Axis and Cursor LabelsSo how do you use this amazing new feature to format axis labels I hear you say? It’s really simple. Create a class which implements ILabelFormatter and attach (via binding or code) to SciChartSurface.XAxis.LabelFormatter. Here’s the code for CustomLabelFormatter: public class CustomLabelFormatter : ILabelFormatter { private readonly string[] _xLabels; public CustomLabelFormatter(string[] xLabels) { _xLabels = xLabels; } /// <summary> /// Called when the label formatted is initialized as it is attached to the parent axis, with the parent axis instance /// </summary> /// <param name="parentAxis">The parent <see cref="T:Abt.Controls.SciChart.IAxis" /> instance</param> public void Init(IAxis parentAxis) { } /// <summary> /// Called at the start of an axis render pass, before any labels are formatted for the current draw operation /// </summary> public void OnBeginAxisDraw() { } /// <summary> /// Formats a label for the axis from the specified data-value passed in /// </summary> /// <param name="dataValue">The data-value to format</param> /// <returns> /// The formatted label string /// </returns> /// <exception cref="System.NotImplementedException"></exception> public string FormatLabel(IComparable dataValue) { int index = (int) Convert.ChangeType(dataValue, typeof (int)); if (index >= 0 && index < _xLabels.Length) return _xLabels[index]; return index.ToString(); } /// <summary> /// Formats a label for the cursor, from the specified data-value passed in /// </summary> /// <param name="dataValue">The data-value to format</param> /// <returns> /// The formatted cursor label string /// </returns> /// <exception cref="System.NotImplementedException"></exception> public string FormatCursorLabel(IComparable dataValue) { int index = (int)Convert.ChangeType(dataValue, typeof(int)); if (index >= 0 && index < _xLabels.Length) return _xLabels[index]; return index.ToString(); } } The method Init() is called when the LabelFormatter is attached to an axis. Use this method if you need a reference to the axis you are formatting. OnBeginAxisDraw() is called at the start of an axis render, before any labels are formatted for the current draw operation. You can use this to reset any state per-draw. FormatLabel() and FormatCursorLabel() are used to transform a data-value into a string label for Axis and Cursor respectively. In this example we simply convert the DataValue to an integer, and index a string array of labels. Note that the CustomLabelFormatter is created in the ChartViewModel with this code: // Create the X-Axis Labels var xAxisLabels = new[] {"Java", "Obj-C", "C#", "Fortran", "C++"}; // ... // The ILabelFormatter allows us to substitute a string value for a data-value. // In most cases this is used to format a numeric value, e.g. 1.23456 as "1.23", however // we can also use it to transform an integer value to a string (text) value XLabelFormatter = new CustomLabelFormatter(xAxisLabels); So we’re literally taking data-values 0,1,2,3,4 as indexes to a string array of labels. Adding Screenshots – MainView.xaml, MainView.xaml.csNext take a look at MainView.xaml and MainView.xaml.cs. We couldn’t be bothered to create a MainViewModel (naughty), but this code demonstrates the point. Here’s the XAML for MainView: <Window x:Class="ScreenshotsAndXpsExport.MainWindow" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:local="clr-namespace:ScreenshotsAndXpsExport" mc:Ignorable="d" d:DesignHeight="480" d:DesignWidth="640"> <Grid> <Grid.RowDefinitions> <RowDefinition Height="32"/> <RowDefinition Height="*"/> </Grid.RowDefinitions> <StackPanel Orientation="Horizontal" Background="#1D2C35"> <Button Content="Copy to Clipboard" Margin="3" Click="CopyToClipboardClick"></Button> <Button Content="Save to PNG" Margin="3" Click="SaveAsPngClick"></Button> <Button Content="Print to XPS" Margin="3" Click="PrintToXpsClick"></Button> <TextBlock Foreground="#EEE" Margin="8" Text="Or Double-click chart to take screenshot with cursor"></TextBlock> </StackPanel> <local:ChartView Grid.Row="1" x:Name="customUserControl" IsAnimatedOnLoad="True" MouseDoubleClick="CustomUserControl_OnMouseDoubleClick"/> </Grid> </Window> This adds a few buttons (toolbar) above the ScIChartSurface in ChartView. These buttons will be used to take screenshots and print to XPS. Let’s break down the code in MainView.xaml.cs to see how we render to bitmap and print to XPS: To Render to Bitmap on Clipboard… we call an extension method, ExportBitmapToClipboard(). See SciChartSurfaceExtensions.cs below for a definition of this method private void CopyToClipboardClick(object sender, RoutedEventArgs e) { // See SciChartSurfaceExtensions where ExportBitmapToClipboard is defined this.customUserControl.SciChartSurface.ExportBitmapToClipboard(); MessageBox.Show("Copied to Clipboard!"); } To Render to Bitmap and Save PNG… we call an extension method, ExportBitmapToFile(). See SciChartSurfaceExtensions.cs below for a definition of this method. private void SaveAsPngClick(object sender, RoutedEventArgs e) { var dialog = new SaveFileDialog(); dialog.DefaultExt = "png"; dialog.AddExtension = true; dialog.Filter = "Png Files|*.png"; dialog.InitialDirectory = Environment.GetFolderPath(Environment.SpecialFolder.Desktop); if (dialog.ShowDialog() == true) { // See SciChartSurfaceExtensions where ExportBitmapToFile is defined this.customUserControl.SciChartSurface.ExportBitmapToFile(dialog.FileName); Process.Start(dialog.FileName); } } To Print to XPS… we demonstrate something slightly different here. First of all you could print the chart already shown, but what if you wanted to size the chart according to print dimensions and export to XPS or PDF, along with some other text in a report? Or, what if you wanted to export the chart as a screenshot but before showing it, e.g. on server-side code? This method demonstrates how we can do this: private void PrintToXpsClick(object sender, RoutedEventArgs e) { var dialog = new PrintDialog(); if (dialog.ShowDialog() == true) { var size = new Size(dialog.PrintableAreaWidth, dialog.PrintableAreaWidth*3/4); var scs = CreateSciChartSurfaceWithoutShowingIt(size); // And print. This works particularly well to XPS! Action printAction = () => dialog.PrintVisual(scs, "Programmer Awesomeness"); Dispatcher.BeginInvoke(printAction); } } First, use PrintDialog to present the dialog to theuser. This also lets us get the PrintableAreaWidth/PrintableAreaHeight. We want to print our chart at full width but at a 4:3 ratio. Next, the method CreateSciChartSurfaceWithoutShowingIt() is used to instantiate a new ChartView/ChartViewModel but without adding to the visual tree. /// <summary> /// This method demonstrates how we can render to bitmap or print a SciChartSurface which is never shown, /// e.g. if you wanted to create a report on a server, or wanted to grab a screenshot at a different resolution /// to the currently shown size of the chart /// </summary> /// <param name="size"></param> /// <returns></returns> private Visual CreateSciChartSurfaceWithoutShowingIt(Size size) { // Create a fresh ChartView, this contains the ViewModel (declared in XAML) and data var control = new ChartView(); var scs = control.SciChartSurface; // RenderPriority.Immediate is required to print or render to bitmap any SciChartSurface that has not been // added to the Visual Tree. This ensures that re-draw events are processed synchronously // // Note RenderPriority.Immediate should never be used for real-time charts!! scs.RenderPriority = RenderPriority.Immediate; // ApplyTemplate is required on the newly created chart to ensure the theme is applied scs.ApplyTemplate(); // Set the width/height explicitly based on print area scs.Width = size.Width; scs.Height = size.Height; // Force the Loaded event for the SciChartSurface scs.OnLoad(); // Now measure & arrange scs.Measure(new Size(scs.Width, scs.Height)); scs.Arrange(new Rect(new Point(0, 0), scs.DesiredSize)); scs.UpdateLayout(); // Finally trigger a redraw scs.InvalidateElement(); return scs; } The important points to note here are, if you want to render to bitmap or print a SciChartSurface that has not yet been shown or added to the Visual Tree, you must:
At this point you will have a SciChartSurface which is prepared and ready to render to bitmap or print, even though it is not in the Visual Tree. The final step is we use the PrintDialog to print the chart // And print. This works particularly well to XPS! Action printAction = () => dialog.PrintVisual(scs, "Programmer Awesomeness"); Dispatcher.BeginInvoke(printAction); SciChartSurfaceExtensions.cs – Where the Screenshot Magic HappensThe following class exposes two extension methods which may be used to render a SciChartSurface to a bitmap or PNG file. These use purely WPF techniques but apply some tricks (Measure, Arrange, Layout) to ensure the SciChartSurface is ready to render to bitmap.
public static class SciChartSurfaceExtensions { public static void ExportBitmapToClipboard(this SciChartSurface element) { var rtb = ExportToBitmap(element); Clipboard.SetImage(rtb); } public static void ExportBitmapToFile(this SciChartSurface element, string filename) { using (var filestream = new FileStream(filename, FileMode.Create)) { var encoder = new PngBitmapEncoder(); var bitmap = ExportToBitmap(element); encoder.Frames.Add(BitmapFrame.Create(bitmap)); encoder.Save(filestream); filestream.Close(); } } public static BitmapSource ExportToBitmap(this SciChartSurface element) { if (element == null) return null; // Store the Frameworks current layout transform, as this will be restored later var storedTransform = element.LayoutTransform; // Set the layout transform to unity to get the nominal width and height element.LayoutTransform = new ScaleTransform(1, 1); element.Measure(new Size(Double.PositiveInfinity, Double.PositiveInfinity)); element.Arrange(new Rect(new Point(0, 0), element.DesiredSize)); var height = element.ActualHeight + element.Margin.Top + element.Margin.Bottom; var width = element.ActualWidth + element.Margin.Left + element.Margin.Right; // Render to a Bitmap Source, note that the DPI is // changed for the render target as a way of scaling the FrameworkElement var rtb = new RenderTargetBitmap( (int)width, (int)height, 96d, 96d, PixelFormats.Default); // Render a white background in Clipboard var vRect = new Rectangle { Width = width, Height = height, Fill = Brushes.White }; vRect.Measure(element.RenderSize); vRect.Arrange(new Rect(element.RenderSize)); rtb.Render(vRect); rtb.Render(element); // Restore the Framework Element to it’s previous state element.LayoutTransform = storedTransform; element.Measure(new Size(Double.PositiveInfinity, Double.PositiveInfinity)); element.Arrange(new Rect(new Point(0, 0), element.DesiredSize)); return rtb; } } Running the ApplicationOk so now you’ve done the walkthrough, if you haven’t already, I invite you to test the application by capturing screenshots, saving PNG files and printing. Printing to XPS is particularly useful, as this is a Vector format which may be scaled, zoomed or converted to PDF with no loss of quality.
Note that it will become apparent which parts of SciChart are vector (the axis, labels, gridlines, chart titles, cursors) and which are rasterized (the *RenderableSeries) as the series won’t scale as well. Still, you have a method above to render to the desired resolution which should mitigate this somewhat! ![]() XPS is a Vector Format, so zooms with no loss of quality. Note that series are Rasterized to bitmaps by SciChart’s high speed drawing engine so are not in Vector format. So, we hope that clears up the matter of how to render SciChart to bitmaps, or print! If you have any questions or feedback, we’d love to hear it in the comments below. Thanks and enjoy! | |