Drawing a WPF UserControl with DataBinding to an Image

跟風遠走 提交于 2020-01-19 07:27:43

问题


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

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)

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>

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

   }

EDIT:

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

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


回答1:


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

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

  1. Make sure your control has been measured and arranged.
  2. If your control uses Loaded events, make sure you are attached to a PresentationSource.
  3. Make sure all DispatcherPriority.Render and above events have completed.

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.

Forcing a Measure/Arrange on your control

This is as simple as:

template.Measure(new Size(double.PositiveInfinity, double.PositiveInfinity));
template.Arrange(new Rect(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.

Attaching a PresentationSource

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 })
{
  ...
}

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

Flushing the Dispatcher queue down to DispatcherPriority.Render

Just use Dispatcher.Invoke:

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

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

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.

Where to add this code

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

Additional suggestions

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

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}" />

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}" />

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>

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.




回答2:


Well, one of your problems is that you need to call Measure and Arrange on your UserControl before trying to render. Put this before creating the RenderTargetBitmap object:

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

That will at least get your UserControl to start rendering.

The second problem is the data binding. I haven't been able to crack that one; there may be something extra you need to do to get the bindings to evaluate. However, you can work around it: If you set the TextBlock contents directly rather than through data binding, it does work.




回答3:


I think the problem is in the binding, as you suspect. Instead of creating a property Booking, try setting the DataContext of the ImageTemplate instance, then set the Path on the bindings to only the property name of the data object you want to use. It may not solve your problem, but it's a more standard way of doing the binding.

<TextBlock ... Text="{Binding Path=Customer}" />

Should be all you need to make the binding work if you set the data context to a Booking instance. Try it and let us know if it works.



来源:https://stackoverflow.com/questions/2557183/drawing-a-wpf-usercontrol-with-databinding-to-an-image

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!