如何在WPF中等待渲染线程完成?.NET6+Caliburn.Micro场景
WPF中等待UI渲染完成后截图的解决方案
问题背景
在.NET 6 + Caliburn.Micro的项目中,点击添加按钮新增UI块后立即截图,会出现新块未渲染完成的情况。尝试过Dispatcher.Invoke/BeginInvoke无效,用ContextIdle优先级虽能解决但存在风险,需要更可靠的渲染完成回调方案。
原代码参考
UI标记(XAML)
<Window x:Class="WpfApp1.MainWindow" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:local="clr-namespace:WpfApp1" xmlns:cal="http://www.caliburnproject.org" mc:Ignorable="d" Title="MainWindow" Height="450" Width="800" > <Window.DataContext> <local:MainWindowContext/> </Window.DataContext> <Grid> <Grid.ColumnDefinitions> <ColumnDefinition Width="*"/> <ColumnDefinition Width="*"/> <ColumnDefinition Width="*"/> </Grid.ColumnDefinitions> <ItemsControl ItemsSource="{Binding Blocks}" Grid.Column="0"> <ItemsControl.ItemTemplate> <DataTemplate> <StackPanel Orientation="Vertical"> <Grid Height="30"> <TextBlock Text="{Binding Text}" HorizontalAlignment="Left" VerticalAlignment="Center" Padding="5" Margin="0,0,0,10"/> <Rectangle Fill="Green" Opacity="0.5"/> </Grid> </StackPanel> </DataTemplate> </ItemsControl.ItemTemplate> <ItemsControl.ItemsPanel > <ItemsPanelTemplate> <StackPanel IsItemsHost="True" Orientation="Vertical"/> </ItemsPanelTemplate> </ItemsControl.ItemsPanel> </ItemsControl> <Rectangle Fill="Red" Grid.Column="1" Width="1"/> <StackPanel Grid.Column="2" Orientation="Vertical"> <Button Width="100" Margin="20" Content="AddBlock" cal:Message.Attach="Add"/> <Button Width="100" Margin="20" Content="RemoveBlock" cal:Message.Attach="Remove"/> </StackPanel> </Grid> </Window>
ViewModel代码
using Caliburn.Micro; using ShadowBot.UIAutomation.Utilities; using System; using System.Windows; namespace WpfApp1 { public class MainWindowContext : Screen { public BindableCollection<Block> Blocks { get; set; } = new BindableCollection<Block>(); public MainWindowContext() { Blocks.Add(new Block() { Text = DateTime.Now.ToString() }); } public void Add() { Blocks.Add(new Block() { Text = DateTime.Now.ToString() }); NotifyOfPropertyChange(nameof(Blocks)); Application.Current.Dispatcher.Invoke(() => { using (var bitmap = SnippingTool.Screenshots()) { bitmap.Save("D:\\Desktop\\temp\\test.png", System.Drawing.Imaging.ImageFormat.Png); } }); } public void Remove() { if (Blocks.Count > 0) { Blocks.RemoveAt(Blocks.Count - 1); NotifyOfPropertyChange(nameof(Blocks)); } } } public class Block : PropertyChangedBase { private string _text; public string Text { get => _text; set { _text = value; NotifyOfPropertyChange(nameof(Text)); } } } }
解决方案
方案1:使用Dispatcher.InvokeAsync指定Render优先级
将截图逻辑放在DispatcherPriority.Render优先级的异步调用中,确保UI渲染完成后再执行截图。此方法无需修改View,仅需调整ViewModel的Add方法:
public async void Add() { var newBlock = new Block() { Text = DateTime.Now.ToString() }; Blocks.Add(newBlock); NotifyOfPropertyChange(nameof(Blocks)); // 等待UI完成渲染 await Application.Current.Dispatcher.InvokeAsync(() => {}, DispatcherPriority.Render); using (var bitmap = SnippingTool.Screenshots()) { bitmap.Save("D:\\Desktop\\temp\\test.png", System.Drawing.Imaging.ImageFormat.Png); } }
方案2:监听Item的Loaded事件(MVVM友好)
通过Caliburn.Micro的消息机制,在新增Item的UI元素加载完成后通知ViewModel执行截图。
- 修改XAML的ItemTemplate,给根元素添加Loaded事件绑定:
<DataTemplate> <StackPanel Orientation="Vertical" cal:Message.Attach="[Event Loaded] = [Action OnBlockLoaded($dataContext)]"> <Grid Height="30"> <TextBlock Text="{Binding Text}" HorizontalAlignment="Left" VerticalAlignment="Center" Padding="5" Margin="0,0,0,10"/> <Rectangle Fill="Green" Opacity="0.5"/> </Grid> </StackPanel> </DataTemplate>
- 在ViewModel中添加
OnBlockLoaded方法,并记录最新添加的Block,确保只对刚新增的块触发截图:
private Block _lastAddedBlock; public void Add() { _lastAddedBlock = new Block() { Text = DateTime.Now.ToString() }; Blocks.Add(_lastAddedBlock); NotifyOfPropertyChange(nameof(Blocks)); } public void OnBlockLoaded(Block block) { // 仅当加载的是刚新增的块时执行截图 if (block == _lastAddedBlock) { using (var bitmap = SnippingTool.Screenshots()) { bitmap.Save("D:\\Desktop\\temp\\test.png", System.Drawing.Imaging.ImageFormat.Png); } _lastAddedBlock = null; } }
方案3:强制触发布局更新
在添加Block后,强制让ItemsControl完成布局和渲染,再执行截图:
public void Add() { Blocks.Add(new Block() { Text = DateTime.Now.ToString() }); NotifyOfPropertyChange(nameof(Blocks)); // 获取ItemsControl实例(需通过View绑定或Caliburn的ViewAware获取) var mainWindow = (MainWindow)GetView(); var itemsControl = mainWindow.FindName("BlocksItemsControl") as ItemsControl; itemsControl.UpdateLayout(); itemsControl.ApplyTemplate(); using (var bitmap = SnippingTool.Screenshots()) { bitmap.Save("D:\\Desktop\\temp\\test.png", System.Drawing.Imaging.ImageFormat.Png); } }
注:需给XAML中的ItemsControl添加Name属性:Name="BlocksItemsControl"
方案对比
- 方案1:简单易实现,无需修改View,适合大多数场景。
- 方案2:MVVM更友好,精准监听新增元素的加载状态,避免不必要的截图触发。
- 方案3:直接操作UI元素,适合需要精确控制布局的场景,但需获取UI实例,耦合度略高。
内容的提问来源于stack exchange,提问作者huojian
相关产品推荐
相关产品推荐

