Рисование пользовательского элемента управления WPF с привязкой данных к изображению - PullRequest
6 голосов
/ 01 апреля 2010

Поэтому я пытаюсь использовать пользовательский элемент управления 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>

И я добавил свойство зависимости типа «Бронирование» в свой пользовательский элемент управления, который, я надеюсь, будет источником для значений базы данных:

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

   }

EDIT:

Я должен добавить, что процесс работает без каких-либо ошибок, но в итоге я получаю каталог, полный простых белых изображений, а не текста или чего-либо еще ... И я с помощью отладчика подтвердил, что мои объекты Booking заполняются с правильными данными ...

РЕДАКТИРОВАТЬ 2:

Сделал что-то, что я должен был сделать давным-давно, установил фон на холсте, но это никак не изменило выходное изображение, поэтому моя проблема, скорее всего, как-то связана с моим кодом для рисования (хотя может что-то не так с моей привязкой данных)

Ответы [ 3 ]

15 голосов
/ 08 апреля 2010

RenderTargetBitmap отображает текущее состояние вашего контроля. В вашем случае ваш элемент управления не инициализирован, поэтому он по-прежнему выглядит белым.

Чтобы ваш код правильно инициализировался перед Render (), вам нужно сделать три вещи:

  1. Убедитесь, что ваш контроль был измерен и организован.
  2. Если ваш элемент управления использует загруженные события, убедитесь, что вы подключены к 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 для Arrange, а не передавать его в определенном размере.

Присоединение источника презентаций

Это необходимо, только если ваш элемент управления или элементы внутри него зависят от события Loaded.

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

Когда HwndSource создается, визуальное дерево шаблона уведомляется о том, что оно «загружено». Блок «using» гарантирует, что шаблон «Unloaded» находится в конце инструкции «using» (последняя закрывающая фигурная скобка). Альтернативой использованию using () может быть использование GC.KeepAlive:

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

Сброс очереди Диспетчера до DispatcherPriority.Render

Просто используйте Dispatcher.Invoke:

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

Это приводит к тому, что пустое действие вызывается после завершения всех операций рендеринга и действий с более высоким приоритетом. Метод Dispatcher.Invoke обрабатывает очередь диспетчера до тех пор, пока она не опустится до уровня Loaded (который находится прямо под Render).

Причина, по которой это необходимо, заключается в том, что многие компоненты пользовательского интерфейса WPF используют очередь Dispatcher для задержки обработки до тех пор, пока элемент управления не будет готов к визуализации. Это значительно сокращает ненужные повторные вычисления визуальных свойств во время связывания и других операций.

Куда добавить этот код

Добавьте все эти три шага после установки контекста данных (template.Booking = ...) и перед вызовом RenderTargetBitmap.Render.

Дополнительные предложения

Существует гораздо более простой способ заставить вашу привязку работать. В коде просто установите бронирование как 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, вы все равно можете упростить свои привязки, установив DataContext на уровне <UserControl> вместо использования 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 нужно только указать текст - все остальное обрабатывается стилем.

2 голосов
/ 08 апреля 2010

Ну, одна из ваших проблем заключается в том, что вам нужно вызвать Measure and Arrange в вашем UserControl перед попыткой рендеринга. Поместите это перед созданием объекта RenderTargetBitmap:

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

Это как минимум заставит ваш UserControl начать рендеринг.

Вторая проблема - привязка данных. Я не смог взломать это; может потребоваться что-то еще, чтобы оценить привязки. Однако вы можете обойти это: если вы устанавливаете содержимое TextBlock напрямую, а не через привязку данных, это работает.

0 голосов
/ 01 апреля 2010

Я думаю, что проблема в привязке, как вы подозреваете.Вместо того, чтобы создавать свойство Booking, попробуйте установить DataContext экземпляра ImageTemplate, а затем в поле «Путь к привязкам» укажите только имя свойства объекта данных, который вы хотите использовать. может не решить вашу проблему, но это более стандартный способ привязки.

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

Должно быть все, что вам нужно, чтобы привязка работала, если вы установили контекст данных наBooking экземпляр.Попробуйте и дайте нам знать, если это работает.

...