I am writing a server side console app in C#/.Net 4.5 that gets some data and creates static chart images that are saved to be displayed by a web server.
I am mostly using the method described here: http://lordzoltan.blogspot.com/2010/09/using-wpf-to-render-bitmaps.html
However, I added a mainContainer.UpdateLayout(); after the Arrange() so that the databindings would update and be visible in the rendered image, as well as a Measure() before it for good... ah, I'm not gonna go there.
Here is the method that does the rendering:
void RenderAndSave(UIElement target, string filename, int width, int height)
{
var mainContainer = new Grid
{
HorizontalAlignment = HorizontalAlignment.Stretch,
VerticalAlignment = VerticalAlignment.Stretch
};
mainContainer.Children.Add(target);
mainContainer.Measure(new Size(width, height));
mainContainer.Arrange(new Rect(0, 0, width, height));
mainContainer.UpdateLayout();
var encoder = new PngBitmapEncoder();
var render = new RenderTargetBitmap(width, height, 96, 96, PixelFormats.Pbgra32);
render.Render(mainContainer);
encoder.Frames.Add(BitmapFrame.Create(render));
using (var s = File.Open(filename, FileMode.Create))
{
encoder.Save(s);
}
}
The target parameter to the method will be an instance of a WPF/XAML UserControl I made - fairly simple at this point, just a grid with some text databinding to a ViewModel object that I assigned to the DataContext.
The saved image on disk looks good EXCEPT for the OxyPlot Plot object - it is entirely white.
Now, when I am in the designer in Visual Studio 2013, I can see it. I have added a design-time DataContext which is the same object that I use at runtime (this is a spike I am doing - the viewmodel is not in its final form yet, just having a bunch of default data while I work out the kinks). In the designer I see the chart as OxyPlot paints it.
Is there anything special I need to do in order to get my rendering to also contain this OxyPlot chart? It is more or less the point of the exercise so it would be awesome to actually get it to show up!
Thanks in advance for any insights and suggestions!
If you're correctly binding data at runtime as well, then it should work.
[STAThread]
static void Main(string[] args)
{
string filename = "wpfimg.png";
RenderAndSave(new UserControl1(), filename, 300, 300);
PictureBox pb = new PictureBox();
pb.Width = 350;
pb.Height = 350;
pb.Image = System.Drawing.Image.FromFile(filename);
Form f = new Form();
f.Width = 375;
f.Height = 375;
f.Controls.Add(pb);
f.ShowDialog();
}
XAML:
<UserControl x:Class="WpfApp92.UserControl1"
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:oxy="http://oxyplot.org/wpf"
xmlns:local="clr-namespace:WpfApp92"
mc:Ignorable="d"
d:DesignHeight="300" d:DesignWidth="300">
<Grid>
<oxy:PlotView Model="{Binding Model}"/>
</Grid>
</UserControl>
CS:
public partial class UserControl1 : UserControl
{
public PlotModel Model { get; set; }
public UserControl1()
{
InitializeComponent();
Model = new PlotModel();
Model.LegendBorderThickness = 0;
Model.LegendOrientation = LegendOrientation.Horizontal;
Model.LegendPlacement = LegendPlacement.Outside;
Model.LegendPosition = LegendPosition.BottomCenter;
Model.Title = "Simple model";
var categoryAxis1 = new CategoryAxis();
categoryAxis1.MinorStep = 1;
categoryAxis1.ActualLabels.Add("Category A");
categoryAxis1.ActualLabels.Add("Category B");
categoryAxis1.ActualLabels.Add("Category C");
categoryAxis1.ActualLabels.Add("Category D");
Model.Axes.Add(categoryAxis1);
var linearAxis1 = new LinearAxis();
linearAxis1.AbsoluteMinimum = 0;
linearAxis1.MaximumPadding = 0.06;
linearAxis1.MinimumPadding = 0;
Model.Axes.Add(linearAxis1);
var columnSeries1 = new ColumnSeries();
columnSeries1.StrokeThickness = 1;
columnSeries1.Title = "Series 1";
columnSeries1.Items.Add(new ColumnItem(25, -1));
columnSeries1.Items.Add(new ColumnItem(137, -1));
columnSeries1.Items.Add(new ColumnItem(18, -1));
columnSeries1.Items.Add(new ColumnItem(40, -1));
Model.Series.Add(columnSeries1);
var columnSeries2 = new ColumnSeries();
columnSeries2.StrokeThickness = 1;
columnSeries2.Title = "Series 2";
columnSeries2.Items.Add(new ColumnItem(12, -1));
columnSeries2.Items.Add(new ColumnItem(14, -1));
columnSeries2.Items.Add(new ColumnItem(120, -1));
columnSeries2.Items.Add(new ColumnItem(26, -1));
Model.Series.Add(columnSeries2);
DataContext = this;
}
}
I don't know anything about this OxyPlat, but I do know that most charts are often rendered using hardware APIs. Hardware acceleration is usually error-prone when working outside the expected environment (i.e. a client showing a visible Desktop window).
On application initialization, try disabling hardware acceleration:
RenderOptions.ProcessRenderMode = RenderMode.SoftwareOnly;
Another possible tweak is to call DoEvents() before you render your bitmap. Possibly with priority set to SystemIdle. This will make sure your DataContext has been successfully bound.
public static void DoEvents(
DispatcherPriority priority = DispatcherPriority.Background)
{
Dispatcher.CurrentDispatcher.Invoke(new Action(delegate { }), priority);
}