使用数据绑定在WPF UserControl中绘制图像

7

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

我希望能够设置它,以便我可以使用WPF数据绑定,对于数据集中的每个项目,创建我的用户控件的实例,设置对应于我的数据项的依赖属性,然后将用户控件绘制到图像上,但我在让这一切工作时遇到了问题(不确定是数据绑定还是绘制到图像的问题)。

抱歉,代码很长,但我已经尝试了几个小时,WPF就是不喜欢我(不过总得学习一下...)

我的用户控件如下所示:

<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" 类型的依赖属性,希望它将成为数据绑定值的来源:

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

我正在使用以下代码渲染控件:

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:\Code\Bleargh\RawImages\" + i.ToString() + ".png"))
    {
        encoder.Save(s);
    }
}

编辑:

我应该补充说明,该过程没有任何错误,但最终我得到的是一个充满纯白色图像的目录,而不是文本或其他内容......并且我已经使用调试器确认了我的预订对象被填充了正确的数据......

编辑2:

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

3个回答

16

RenderTargetBitmap会渲染您控件的当前状态。在您的情况下,您的控件尚未初始化,因此仍然显示为白色。

要使代码在Render()之前正确初始化,您需要执行以下三个步骤:

  1. 确保已对控件进行了测量和排列。
  2. 如果您的控件使用Loaded事件,请确保已连接到PresentationSource。
  3. 确保所有DispatcherPriority.Render及以上事件已完成。

如果您完成了这三个步骤,您的RenderTargetBitmap将与将其添加到窗口时控件的外观完全相同。

强制对您的控件进行测量/排列

非常简单,只需:

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

这段代码强制进行测量/排列。将double.PositiveInfinity传递给宽度和高度是最简单的方法,因为它允许您的UserControl选择其自己的宽度和高度。如果您明确设置了宽度/高度,则没有太大关系,但通过这种方式,如果数据比预期更大,您的UserControl可以使用WPF的布局系统自动增长。同理,在排列时最好使用template.DesiredSize而不是传递一个特定的大小。

附加PresentationSource

只有在您的控件或控件内的元素依赖于Loaded事件时才需要执行此操作。

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

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

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

将Dispatcher队列刷新至DispatcherPriority.Render

只需使用Dispatcher.Invoke:

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

当所有Render和更高优先级的操作完成后,这将导致调用一个空的操作。Dispatcher.Invoke方法会处理派发程序队列,直到它被清空为止,在Loaded级别下(该级别位于Render正下方)。

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

添加代码的位置

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

额外的建议

有一种更简单的方法可以使您的绑定工作。只需在代码中将booking设置为DataContext即可。这样就无需使用ElementName和Booking属性:

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

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

通过使用DataContext,TextBox绑定得到了极大的简化:

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

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

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

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

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

请注意,WPF的布局是根据UserControl的大小和指定的高度和边距完成的。同时,请注意TextBlock只需要指定文本--其余所有内容都由样式处理。


哇!谢谢你提供的信息。我根本没有使用OnLoad,但是其他两个提示解决了我的问题...非常感谢...我知道一些XAML完全不对劲,但我只是想让它渲染出来,现在我会修复其中的一些问题...再次感谢。 - LorenVS
2
如果我曾经看到过的话,这是一份应得的赏金。 - RandomEngy
2
我不知道。这个答案有点短,也不是很清楚。如果你能稍微修改和扩展一下,可能会更好些... - user1228
1
针对我的情况,我不得不使用“ApplicationIdle”而非“Loaded”。Dispatcher.Invoke(DispatcherPriority.ApplicationIdle, new Action(() => { })); - Aaron Hoffman
1
这个答案太棒了! :) - Vizu

2

好的,你的一个问题是在尝试渲染之前需要在用户控件上调用Measure和Arrange。请在创建RenderTargetBitmap对象之前添加以下内容:

template.Measure(new Size(template.Width, template.Height));
template.Arrange(new Rect(new Size(template.Width, template.Height)));

这将至少使您的UserControl开始渲染。

第二个问题是数据绑定。我还没有能够解决这个问题;或许需要额外的操作才能使绑定求值。但是,您可以通过以下方法解决:如果直接设置TextBlock的内容而不是通过数据绑定,它是可以工作的。


0
我认为问题在于绑定,正如你所怀疑的那样。不要创建一个名为Booking的属性,试着设置ImageTemplate实例的DataContext,然后在绑定的路径中仅设置数据对象所需的属性名称。这可能不能解决您的问题,但这是一种更标准的绑定方式。
<TextBlock ... Text="{Binding Path=Customer}" />

如果您将数据上下文设置为一个 Booking 实例,那么这应该是使绑定工作所需的全部内容。请尝试并告诉我们它是否有效。


我之前就是这样设置过,但似乎还是没有正常工作...我记不清确切的 XAML 代码来改回去了 :( 我觉得在绘图部分我可能还是做错了什么,因为我已经将用户控件的背景设置为黑色,但仍然得到一张空白图片... - LorenVS
那一定是你的绘图部分了。不幸的是,我对图像处理之类的东西并不了解。抱歉。 - Benny Jobigan

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接