使用 DataBinding 将 WPF UserControl 绘制到图像 [英] Drawing a WPF UserControl with DataBinding to an Image

查看:16
本文介绍了使用 DataBinding 将 WPF UserControl 绘制到图像的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

所以我尝试使用 WPF 用户控件从数据集中生成大量图像,其中数据集中的每个项目都会生成图像...

So I'm trying to use a WPF User Control to generate a ton of images from a dataset where each item in the dataset would produce an image...

我希望我能以这样的方式设置它,我可以使用 WPF 数据绑定,并且对于数据集中的每个项目,创建我的用户控件的一个实例,设置与我的数据项相对应的依赖属性,然后将用户控件绘制到图像上,但我在让它全部工作时遇到问题(不确定数据绑定或绘制到图像是否是我的问题)

I'm hoping I can set it up in such a way that I can use WPF databinding, and for each item in the dataset, create an instance of my user control, set the dependency property that corresponds to my data item, and then draw the user control to an image, but I'm having problems getting it all working (not sure whether databinding or drawing to the image is my problem)

对不起,大量的代码转储,但我一直在努力让它工作几个小时,而 WPF 只是不喜欢我(尽管在某些时候必须学习......)

Sorry for the massive code dump, but I've been trying to get this working for a couple of hours now, and WPF just doesn't like me (have to learn at some point though...)

我的用户控件如下所示:

My User Control looks like this:

<UserControl x:Class="Bleargh.ImageTemplate"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:c="clr-namespace:Bleargh"
    x:Name="ImageTemplateContainer"
    Height="300" Width="300">

    <Canvas>
        <TextBlock Canvas.Left="50" Canvas.Top="50"  Width="200" Height="25" FontSize="16" FontFamily="Calibri" Text="{Binding Path=Booking.Customer,   ElementName=ImageTemplateContainer}" />
        <TextBlock Canvas.Left="50" Canvas.Top="100" Width="200" Height="25" FontSize="16" FontFamily="Calibri" Text="{Binding Path=Booking.Location,   ElementName=ImageTemplateContainer}" />
        <TextBlock Canvas.Left="50" Canvas.Top="150" Width="200" Height="25" FontSize="16" FontFamily="Calibri" Text="{Binding Path=Booking.ItemNumber, ElementName=ImageTemplateContainer}" />
        <TextBlock Canvas.Left="50" Canvas.Top="200" Width="200" Height="25" FontSize="16" FontFamily="Calibri" Text="{Binding Path=Booking.Description,ElementName=ImageTemplateContainer}" />
    </Canvas>

</UserControl>

并且我添加了一个类型为Booking"的依赖属性;到我的用户控件,我希望它将成为数据绑定值的来源:

And I've added a dependency property of type "Booking" to my user control that I'm hoping will be the source for the databound values:

public partial class ImageTemplate : UserControl
{
    public static readonly DependencyProperty BookingProperty = DependencyProperty.Register("Booking", typeof(Booking), typeof(ImageTemplate));
    public Booking Booking
    {
        get { return (Booking)GetValue(BookingProperty); }
        set { SetValue(BookingProperty, value); }
    }

    public ImageTemplate()
    {
        InitializeComponent();
    }
}

我使用以下代码来呈现控件:

And I'm using the following code to render the control:

List<Booking> bookings = Booking.GetSome();
for(int i = 0; i < bookings.Count; i++)
{
    ImageTemplate template = new ImageTemplate();
    template.Booking = bookings[i];

    RenderTargetBitmap bitmap = new RenderTargetBitmap(
        (int)template.Width,
        (int)template.Height,
        120.0,
        120.0,
        PixelFormats.Pbgra32);

    bitmap.Render(template);

    BitmapEncoder encoder = new PngBitmapEncoder();
    encoder.Frames.Add(BitmapFrame.Create(bitmap));

    using (Stream s = File.OpenWrite(@"C:CodeBlearghRawImages" + i.ToString() + ".png"))
    {
        encoder.Save(s);
    }
}

我应该补充一点,这个过程没有任何错误,但我最终得到了一个充满纯白色图像的目录,而不是文本或任何东西......我已经使用调试器确认我的 Booking 对象正在被填充使用正确的数据...

I should add that the process works without any errors whatsoever, but I end up with a directory full of plain-white images, not text or anything... And I have confirmed using the debugger that my Booking objects are being filled with the proper data...

编辑 2:

做了一些我早就应该做的事情,在我的画布上设置背景,但这根本没有改变输出图像,所以我的问题肯定与我的绘图代码有关(尽管可能有我的数据绑定也有问题)

Did something I should have done a long time ago, set a background on my canvas, but that didn't change the output image at all, so my problem is most definitely somehow to do with my drawing code (although there may be something wrong with my databinding too)

推荐答案

RenderTargetBitmap 呈现控件的当前状态.在您的情况下,您的控件尚未初始化,因此它仍然显示为白色.

RenderTargetBitmap renders the current state of your control. In your case your control has not initialized so it still appears white.

要让您的代码在 Render() 之前正确初始化,您需要做三件事:

To get your code to initialize properly before Render() you need to do three things:

  1. 确保您的控制得到衡量和安排.
  2. 如果您的控件使用 Loaded 事件,请确保您已附加到 PresentationSource.
  3. 确保所有 DispatcherPriority.Render 及以上事件都已完成.

如果您执行这三件事,您的 RenderTargetBitmap 将与您将控件添加到窗口时的显示方式相同.

If you do these three things your RenderTargetBitmap will come out identically to the way the control appears when you add it to a Window.

在您的控件上强制测量/排列

这很简单:

template.Measure(new Size(double.PositiveInfinity, double.PositiveInfinity));
template.Arrange(new Rect(template.DesiredSize));

此代码强制测量/排列.为宽度和高度传入 double.PositiveInfinity 是最简单的,因为它允许您的 UserControl 选择自己的宽度和高度.如果您显式设置宽度/高度,则无关紧要,但是如果数据大于预期,您的 UserControl 可以选择使用 WPF 的布局系统在必要时自动增长.出于同样的原因,最好将 template.DesiredSize 用于排列而不是传入特定大小.

This code forces measure/arrange. It is simplest to pass in double.PositiveInfinity for the width and height because it allows your UserControl to choose its own Width and Height. If you explicitly set the width/height it doesn't matter much, but this way your UserControl has the option of using WPF's layout system to automatically grow when necessary if the data is larger than expected. By the same token it is better to use template.DesiredSize for the Arrange rather than passing in a specific size.

附加 PresentationSource

仅当您的控件或控件中的元素依赖于 Loaded 事件时才需要这样做.

This is only necessary if your control or elements within your control rely on the Loaded event.

using(var source = new HwndSource(new HwndSourceParameters())
                       { RootVisual = template })
{
  ...
}

当 HwndSource 被创建时,模板的可视化树会被通知它已被加载".using"块确保模板在using"语句(最后一个右花括号)的末尾被卸载".using() 语句的替代方法是使用 GC.KeepAlive:

When the HwndSource is created the visual tree of the template is notified that it has been "Loaded". The "using" block makes sure the template is "Unloaded" at the end of the "using" statement (last closing curly brace). An alternative to a using() statement would be to use GC.KeepAlive:

GC.KeepAlive(new HwndSource(...) { ... });

将 Dispatcher 队列刷新到 DispatcherPriority.Render

只需使用 Dispatcher.Invoke:

Just use Dispatcher.Invoke:

Dispatcher.Invoke(DispatcherPriority.Loaded, new Action(() => {}));

这会导致在所有渲染和更高优先级操作完成后调用空操作.Dispatcher.Invoke 方法处理调度器队列,直到它清空到 Loaded 级别(位于 Render 的正下方).

This causes an empty action to be invoked after all Render and higher priority actions have completed. The Dispatcher.Invoke method processes the dispatcher queue until it is empty down to Loaded level (which is right below Render).

这是必要的原因是许多 WPF UI 组件使用 Dispatcher 队列来延迟处理,直到控件准备好呈现.这显着减少了绑定和其他操作期间不必要的视觉属性重新计算.

The reason this is necessary is that many WPF UI components use the Dispatcher queue to delay processing until the control is ready to render. This significantly cuts down on unnecessary re-computation of visual properties during binding and other operations.

在哪里添加此代码

在设置数据上下文 (template.Booking = ...) 之后和调用 RenderTargetBitmap.Render 之前添加所有这三个步骤.

Add all three of these steps after you set your data context (template.Booking = ...) and before you call RenderTargetBitmap.Render.

其他建议

有一种更简单的方法可以使您的绑定工作.在代码中,只需将预订设置为 DataContext.这消除了使用 ElementName 和 Booking 属性的需要:

There is a much easier way to make your binding work. In code, just set the booking as a DataContext. This removes the need to use ElementName and the Booking property:

foreach(var booking in Booking.GetSome())
{
  var template = new ImageTemplate { DataContext = booking };

  ... code from above ...
  ... RenderTargetBitmap code ...
}

通过使用 DataContext,TextBox 绑定大大简化:

By using the DataContext, the TextBox binding is greatly simplified:

<UserControl ...>
  <Canvas>
    <TextBlock ... Text="{Binding Customer}" />
    <TextBlock ... Text="{Binding Location}" />
    <TextBlock ... Text="{Binding ItemNumber}" />
    <TextBlock ... Text="{Binding Description}" />

如果您有使用 Booking DependencyProperty 的特殊原因,您仍然可以通过在 <UserControl> 级别设置 DataContext 而不是使用 ElementName 来简化绑定:

If you have a particular reason for using the Booking DependencyProperty you can still simplify your bindings by setting the DataContext at the <UserControl> level rather than using ElementName:

<UserControl ...
  DataContext="{Binding Booking, RelativeSource={RelativeSource Self}}">
  <Canvas>
    <TextBlock ... Text="{Binding Customer}" />

为此,我还建议您使用 StackPanel 而不是 Canvas,并且您还应该考虑使用样式来设置字体、文本大小和间距:

I would also recommend you use a StackPanel instead of a Canvas for this purpose, and you should also consider using a style to set the font, text size and spacing:

<UserControl ...
  Width="300" Height="300">

  <UserControl.Resources>
    <Style TargetType="TextBlock">
      <Setter Property="FontSize" Value="16" />
      <Setter Property="FontFamily" Value="Calibri" />
      <Setter Property="Height" Value="25" />
      <Setter Property="Margin" Value="50 25 50 0" />
    </Style>
  </UserControl.Resources>

  <StackPanel>
    <TextBlock Text="{Binding Customer}" />
    <TextBlock Text="{Binding Location}" />
    <TextBlock Text="{Binding ItemNumber}" />
    <TextBlock Text="{Binding Description}" />
  </StackPanel>
</UserControl>

请注意,给定 UserControl 大小和指定的高度和边距,所有布局都是由 WPF 的布局完成的.另请注意,TextBlock 只需要指定 Text —— 其他一切都由样式处理.

Note that all the layout is done by WPF's layout given the UserControl size and the specified height and margin. Also note that the TextBlock only needs to specify the Text -- everything else is handled by the style.

这篇关于使用 DataBinding 将 WPF UserControl 绘制到图像的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆