14

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!

jsanalytics
  • 13,058
  • 4
  • 22
  • 43
Rune Jacobsen
  • 9,907
  • 11
  • 58
  • 75
  • Givens the wide array of platforms supported, I would suspect that the problem is in how the OxyPlot WPF control was implemented. Perhaps it is a WinForms control embedded in a WPF control, which may be problematic. I do not know if that helps. – Phillip Scott Givens Jul 11 '15 at 10:34
  • You could be right about this, of course - there is the chance that there is no way to do this without the control existing in a full WPF environment. In that case, I will go looking for another charting component. However, if I could just find out what the designer does that I do not, then I am hopeful that I could trigger it to paint itself.. – Rune Jacobsen Jul 11 '15 at 11:04
  • @swiszcz: Could you explain why the answers provided here won't work for you? – Dirk Vollmar Dec 19 '17 at 13:17

2 Answers2

5

If you're correctly binding data at runtime as well, then it should work.

enter image description here

 [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;
    }
}
jsanalytics
  • 13,058
  • 4
  • 22
  • 43
  • Thanks for your answer, and sorry for my late comment on it. Well, your example obviously works. And I have two examples (proprietary code, unfortunately) where it doesn't... I hope I'll find some time in a near future to produce a minimal reproducible example to find out when (and why) it's not working. – Maciek Świszczowski Dec 21 '17 at 00:00
  • You're very welcome. Sorry to hear about that... Certainly post a [MCVE](https://stackoverflow.com/help/mcve) whenever you have a chance, and send me a note about it, I'll be happy to look at it again. Cheers. – jsanalytics Dec 21 '17 at 00:05
3

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);
}
l33t
  • 18,692
  • 16
  • 103
  • 180