如何在无窗口(服务器端)WPF小部件中强制IsVisible = true?

pgccezyw  于 2023-04-22  发布在  其他
关注(0)|答案(1)|浏览(151)

在一个windows服务中,我试图将一个WPF控件渲染成png,问题出在其中一个控件上(即Oxyplot PlotView)检查控件是否实际可见,如果不可见,则不绘制任何内容。我发现使IsVisible返回true的唯一方法是将控件放置在窗口中并在窗口上调用Show,所有窗口实际弹出的结果,我不想这样
我试着把控件放在Page中,但没有帮助。
下面是我使用的基本代码:

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);
}

这是在STA线程中运行的。渲染其他小部件,这不是一个PlotView工作,我可以在调试器中看到PlotView如何不画它的视觉效果,因为它检查它是否可见。

l0oc07j2

l0oc07j21#

我可以建议两种解决方案,重点是UIElement.IsVisible只有在元素成为元素树的子元素并且可能可见时才被设置为true(由WPF)。
请注意,目标是强制UIElement.IsVisible返回true
第一种解决方案是“伪造”未呈现的元素(在本例中为PlotView)是逻辑树的子元素。它通过临时将未呈现的元素声明为新的可视根来实现这一点。
缺点是,如果操作时间过长,则会为原始可视根(通常为Window)的完整元素树引发Framework.Unloaded事件。然后在恢复可视根后,会引发FrameworkElement.Loaded事件。此外,我们会触发完整的布局通道。这也意味着如果迭代足够长,用户将可以看到它。
然而,在这个场景中,执行时间只有几微秒,这就是为什么这些性能考虑因素不适用的原因。
第二种解决方案使用“虚拟”渲染主机来使未渲染的元素虚拟可见。这样的主机应该是一个容器,当可用空间为零时,不会将其可用空间强制分配给其子元素。
为了避免对布局造成任何影响,必须将主机容器配置为零大小。这是因为我们必须将主机插入到可视树中。
Canvas面板是完美的,因为它的初始大小为零,但不限制其子元素的大小。虽然Canvas是不可见的,不会影响布局,但子元素仍然可以占据它们想要的空间。其他容器在设置为零大小时,例如Border,将使用此零大小来测量其子元素。这导致布局引擎不认为子元素可见。
缺点是我们必须修改元素树来添加额外的虚拟元素容器。

方案一

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;
}

方案二

主窗口.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();
}

相关问题