somnambulist
somnambulist

Reputation: 20

How to force IsVisible = true in a windowless (server side) WPF widget?

In a windows service I'm trying to render a WPF Control into a png and the problem is one of the controls involved (namely an Oxyplot PlotView) checks if the control is actually visible and if not does not draw anything. The only way I've found to make IsVisible return true was to place the control inside a window and call Show on the window, with all the consequences of a window actually popping up, which I don't want.

I tried to put the control in a Page but did not help.

Here the basic code I'm using:

var plotView = new PlotView() { Height = height, Width = width, XPS = true, Model = plot };
var view = new ContentControl() { Content = plotView, Width= width, Height = height };

//force bindings
Dispatcher.CurrentDispatcher.Invoke(DispatcherPriority.ApplicationIdle, new DispatcherOperationCallback(_ => { return null; }), null);

view.Measure(new Size(width, height));
view.Arrange(new Rect(0, 0, width, height));
view.UpdateLayout();

var encoder = new PngBitmapEncoder();
var render = new RenderTargetBitmap(width, height, 96, 96, PixelFormats.Pbgra32);
render.Render(view);
encoder.Frames.Add(BitmapFrame.Create(render));
using (var s = File.Open(filename, FileMode.Create))
{
  encoder.Save(s);
}

This is running in an STA thread. Rendering other widgets, which are not a PlotView works, I can see in the debugger how the PlotView does not paint it's visuals because it checks whether it's visible.

Upvotes: -1

Views: 241

Answers (1)

BionicCode
BionicCode

Reputation: 29028

I can suggest two solutions. The point is that UIElement.IsVisible is only set to true (by WPF) when the element becomes a child of the element tree and is potentially visible.

Please note that the goal is to force UIElement.IsVisible to return true.

The first solution is to "fake" that the unrendered element (in your case the PlotView) is a child of the logical tree. It does this by temporary declaring the unrendered element as the new visual root.
The disadvantage is that if the operation takes too long, the Framework.Unloaded event will be raised for the complete element tree of the original visual root (which usually is Window). Then after the visual root is restored the FrameworkElement.Loaded event is raised. In addition we trigger a complete layout pass. This also means if the iterruption is long enough it will be visible to the user.
However, in this scenario the execution time is just a few microseconds, which is why those performance considerations won't apply.

The second solution uses a "virtual" rendering host to make the unrendered element virtually visible. Such a host should be a container that doesn't force it's available space on its children when the available space is zero.
Configuring the host container to have a zero size is mandatory in order to avoid any impact on the layout. This is because we have to insert the host into the visual tree.
The Canvas panel is perfect because it has an initial size zero but doesn't restrict the size of its children. While the Canvas is invisible and doesn't affect the layout, the child elements can still occupy the space they desire. Other containers when set to a zero size, for example the Border, will measure their children using this zero size, which results in the children not considered visible by the layout engine.
The disadvantage is that we must modify the element tree to add an additional dummy element container.

Solution 1

MainWindow.xaml.cs

private async void RenderPlotAsync(Size renderSize, PlotModel plotModel)
{
  var plotView = new PlotView() 
  { 
    Height = renderSize.Height, 
    Width = renderSize.Width, 
    Model = plotModel
  };

  var hwndSource = PresentationSource.FromVisual(this);

  // Force IsVisible to be set to true
  hwndSource.RootVisual = plotView;

  // The VisualBrush will measure the PlotView properly
  var brush = new VisualBrush(plotView);    
  var drawingVisual = new DrawingVisual();
  DrawingContext drawingContext = drawingVisual.RenderOpen();
  drawingContext.DrawRectangle(brush, new Pen(), new Rect(0, 0, 200, 200));
  drawingContext.Close();
  
  var encoder = new PngBitmapEncoder();
  var render = new RenderTargetBitmap(200, 200, 96, 96, PixelFormats.Pbgra32);
  render.Render(drawingVisual);
  encoder.Frames.Add(BitmapFrame.Create(render));
  
  await using FileStream destinationStream = File.Open("image1.png", FileMode.Create);
  encoder.Save(destinationStream);

  // Restore the original visual root
  hwndSource.RootVisual = this;
}

Solution 2

MainWindow.xaml

<Window>
  <Grid x:Name="RootPanel">
  
    <!-- Due to its zero size the Canvas won't affect the layout as it will be invisible -->
    <Canvas x:Name="VirtualRenderHost" />

    <!-- Application content -->
  </Grid>
</Window>

MainWindow.xaml.cs

private async void RenderPlotAsync(Size renderSize, PlotModel plotModel)
{
  var plotView = new PlotView() 
  { 
    Height = renderSize.Height, 
    Width = renderSize.Width, 
    Model = plotModel
  };

  this.VirtualRenderHost.Child = plotView;
  plotView.Loaded += OnPlotViewLoaded;

  // Force IsVisible to be set to true      
  _ = this.VirtualRenderHost.Children.Add(plotView);
}

private async void OnPlotViewLoaded(object sender, EventArgs e)
{    
  var plotView = sender as PlotView;
  plotView.Loaded -= OnPlotViewLoaded;

  var encoder = new PngBitmapEncoder();
  var render = new RenderTargetBitmap(200, 200, 96, 96, PixelFormats.Pbgra32);
  render.Render(plotView);
  encoder.Frames.Add(BitmapFrame.Create(render));

  await using FileStream destinationStream = File.Open("plot_image.png", FileMode.Create);
  encoder.Save(destinationStream);

  // Kill the PlotView instance
  this.VirtualRenderHost.Children.Clear();
}

Upvotes: 0

Related Questions