WPF在后台线程渲染控件并保存为图片

WPF在后台线程绘制控件并保存为图片

应用情景

实际上,WPF在后台线程绘制控件是很 “脏” 的手法,网上的资料也不多,不是什么值得使用的方案。这里存粹是记录一下。
我遇到一个应用情景是,一个控件是由视频控件和上方的Grid控件组成的,该Grid控件能够根据视频控件的内容动态的显示一些效果,类似于视频滤镜。需求是能够将整个控件的变化导出到视频,而且要支持导出队列,就是多个控件能够同时导出多个视频,同一个控件也可以重复导出。
因为需要导出队列,所以无法使用wpf控件截图的方式,而Grid控件显示的样式又十分复杂,通过简单的线条是很难在后台画出来,因此想到了在后台线程直接画控件并渲染的方法。只要后台cs代码和xaml中的页面保存一致,那导出的视频就是界面显示的效果。

示例代码

点击Button1在独立的STA线程中初始化控件并保存到图片。

private void Button1_Click(object sender, RoutedEventArgs e)
{
    // 在新线程中操作控件
    Task.Factory.StartNew(ThreadFunc).Wait();
}
private void ThreadFunc()
{
    try
    {
        Thread thread = new Thread(() =>
        {
            var control = new Grid();
            Random random = new Random();
            byte a = (byte)random.Next(64, 256);
            byte r = (byte)random.Next(64, 256);
            byte g = (byte)random.Next(64, 256);
            byte b = (byte)random.Next(64, 256);
            control.Background = new SolidColorBrush(Color.FromArgb(a, r, g, b));

            TextBlock textBlock = new TextBlock();
            textBlock.FontSize = 30;
            textBlock.Text = $"A:{a} R:{r} G:{g} B:{b}";
            textBlock.VerticalAlignment = VerticalAlignment.Center;
            textBlock.HorizontalAlignment = HorizontalAlignment.Center;
            control.Children.Add(textBlock);

            // 将要绘制的画布大小传递到最外层控件
            Rect renderRect = new Rect(0, 0, 500, 500);
            control.Arrange(renderRect); // 必须在控件属性设置完之后,绘制到 RenderTargetBitmap 之前调用

            // 渲染控件到 RenderTargetBitmap
            var bitmapRender = new RenderTargetBitmap((int)renderRect.Width, (int)renderRect.Height, 96d, 96d, PixelFormats.Pbgra32);
            bitmapRender.Render(control);

            // RenderTargetBitmap 保存到图片
            var bmpEncoder = new PngBitmapEncoder();
            bmpEncoder.Frames.Add(BitmapFrame.Create(bitmapRender));
            using (Stream fs = File.Create("output.png"))
            {
                bmpEncoder.Save(fs);
            }
        });
        thread.SetApartmentState(ApartmentState.STA); // 控件必须在STA线程中构造
        thread.IsBackground = true;
        thread.Start();
        thread.Join();
    }
    catch (Exception ex)
    {
        Trace.TraceError($"{ex}");
    }
}

一段简单的示例展示了如何完全在一个STA线程中绘制控件,并保存到图片。
但是这段代码存在一个问题。
有时候我们要将初始化和更新分开,这样不必每次都要初始化重新构造控件。
所有的控件只能在线程中创建时初始化一次。如果我们把控件在外部通过控件的Dispatcher.Invoke访问会抛出异常。因为这样创建的Thread是没有消息循环的。

改进方案

为了让控件的初始化和更新属性分开,我们给STA线程手动添加消息循环。
注意ThreadFunc中的添加的Dispatcher.Run();
需要注意的是这个方法是阻塞的(消息循环里的while),添加之后会阻塞STA线程。因此移除了thread.Join()。

点击Button1初始化控件。
点击Button2更新控件背景颜色为随机色,并更新子控件的文本内容为对应argb的颜色值。

private void Button1_Click(object sender, RoutedEventArgs e)
{
    // 在task中初始化控件
    Task.Factory.StartNew(ThreadFunc).Wait();
}

private void Button2_Click(object sender, RoutedEventArgs e)
{
	// 刷新控件
    RefreshControl();
}

private Grid control;

private void ThreadFunc()
{
    try
    {
        Thread thread = new Thread(() =>
        {
            control = new Grid();

            TextBlock textBlock = new TextBlock();
            textBlock.FontSize = 30;
            textBlock.VerticalAlignment = VerticalAlignment.Center;
            textBlock.HorizontalAlignment = HorizontalAlignment.Center;
            control.Children.Add(textBlock);

            Dispatcher.Run(); // 在当前线程运行消息循环,该方法会阻塞线程。
        });
        thread.SetApartmentState(ApartmentState.STA); // 控件必须在STA线程中构造
        thread.IsBackground = true;
        thread.Start();
    }
    catch (Exception ex)
    {
        Trace.TraceError($"{ex}");
    }
}

private void RefreshControl()
{
    if (control == null) return;
    control.Dispatcher.Invoke(() =>
    {
        // 更新控件背景
        Random random = new Random();
        byte a = (byte)random.Next(64, 256);
        byte r = (byte)random.Next(64, 256);
        byte g = (byte)random.Next(64, 256);
        byte b = (byte)random.Next(64, 256);
        control.Background = new SolidColorBrush(Color.FromArgb(a, r, g, b));

        // 更新子控件内容
        TextBlock textBlock = control.Children[0] as TextBlock;
        textBlock.Text = $"A:{a} R:{r} G:{g} B:{b}";

        // 将要绘制的画布大小传递到最外层控件
        Rect renderRect = new Rect(0, 0, 500, 500);
        control.Arrange(renderRect); // 必须在绘制到 RenderTargetBitmap 之前调用

        // 渲染控件到 RenderTargetBitmap
        var bitmapRender = new RenderTargetBitmap((int)renderRect.Width, (int)renderRect.Height, 96d, 96d, PixelFormats.Pbgra32);
        bitmapRender.Render(control);

        // RenderTargetBitmap 保存到图片
        var bmpEncoder = new PngBitmapEncoder();
        bmpEncoder.Frames.Add(BitmapFrame.Create(bitmapRender));
        using (Stream fs = File.Create("output.png"))
        {
            bmpEncoder.Save(fs);
        }
    });
}

总结

如果只是要在后台线程画线,矩形,椭圆等简单的几何形状,可以使用DrawingContext,没有必要大费周章去做这种事情。
虽然后台渲染WPF的UI控件怎么看都不是好方案,但是作为开发也要具备解决各种问题的能力。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值