воскресенье, 10 февраля 2013 г.

Проигрывание GIF-ок в WPF приложении

Классически, на форумах MSDN, в одном из топиков, задали фопрос, как в WPF приложении заставить gif-изображение показывать не только первый кадр, а всю анимацию. Ответ под катом.
Итак, создаем пустой WPF проект, кидаем в него картинку (у меня это Source.gif), говорим, что ее надо копировать в папку с приложение, и что делать с ней ничего не надо:
Делаем разметку формы:

<Grid>
    <Image x:Name="imBackGround" />
</Grid>
Код:

public partial class MainWindow : Window
{
    public MainWindow()
    {
        InitializeComponent();
        Loaded += new RoutedEventHandler(MainWindow_Loaded);
    }

    void MainWindow_Loaded(object sender, RoutedEventArgs e)
    {
        BitmapImage bi = new BitmapImage();
        bi.BeginInit();
        bi.UriSource = new Uri(@"/source.gif", UriKind.RelativeOrAbsolute);
        bi.EndInit();
        imBackGround.Source = bi;
    }
}
Запускаем. Картинка есть, но как и ожидалось, без анимации. Начинаем лечение.
Сделаем загрузку нашего изображения через Bitmap. Для этого, нам придеться подключить библиотеку System.Drawing.

Bitmap _bitmap;

private BitmapSource GetSource()
{
    if (_bitmap == null)
    {
        _bitmap = new Bitmap("source.gif");
    }
    IntPtr handle = IntPtr.Zero;
    handle = _bitmap.GetHbitmap();
    return Imaging.CreateBitmapSourceFromHBitmap(
            handle, IntPtr.Zero, Int32Rect.Empty, BitmapSizeOptions.FromEmptyOptions());
}

Зачем такие сложности? Я, если честно, не знаю, но как сделать анимацию gif-ок без Bitmap, я не нашел.
Правим загрузку изображения и включаем на нем анимацию:
BitmapSource _source;

void MainWindow_Loaded(object sender, RoutedEventArgs e)
{
    _source = GetSource();
    imBackGround.Source = _source;
    ImageAnimator.Animate(_bitmap, OnFrameChanged);
}
Метод ImageAnimator.Animate переключает в _bitmap текущее изображение на следующий кадр. По окончании переключения, вызывается метод передаваемый в Animate вторым параметром. Именно в нем и будет изменяться кадр в нашем Image-е. Делаем это так:

private void FrameUpdatedCallback()
{
    ImageAnimator.UpdateFrames();
    if (_source != null)
        _source.Freeze();
    _source = GetSource();
    imBackGround.Source = _source;
    InvalidateVisual();
}
Если внимательно читали или пытались повторить, то увидели, что имя этого метода не совпадает с именем метода передаваемого в ImageAnimator.Animate. Действительно, ведь OnFrameChanged будет вызываться в потоке отличном от потока создавшего визуальные элементы. Вот нам в нем и придеться сделать перевызов метода FrameUpdatedCallback в правильном потоке:

private void OnFrameChanged(object sender, EventArgs e)
{
    Dispatcher.BeginInvoke(DispatcherPriority.Normal,
                            new Action(FrameUpdatedCallback));
}
Все. Запускаем и смотрим анимацию.

20 комментариев:

  1. я попробовал сделать как вы писали но к сожалению , не работает.
    вот код:

    using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Text;
    using System.Windows;
    using System.Windows.Controls;
    using System.Windows.Data;
    using System.Windows.Documents;
    using System.Windows.Input;
    using System.Windows.Media;
    using System.Windows.Media.Imaging;
    using System.Windows.Navigation;
    using System.Windows.Shapes;
    using System.Windows.Media.Animation;
    using System.Drawing;
    using System.Drawing.Imaging;
    using System.Windows.Interop;
    using System.Windows.Threading;

    namespace WpfApplication1
    {
    ///
    /// Interaction logic for MainWindow.xaml
    ///
    public partial class MainWindow : Window
    {
    Bitmap _bitmap;
    BitmapSource _source;

    public MainWindow()
    {
    InitializeComponent();
    Loaded += new RoutedEventHandler(MainWindow_Loaded);
    }

    private BitmapSource GetSource()
    {
    if (_bitmap == null)
    {
    _bitmap = new Bitmap("earth.gif");
    }
    IntPtr handle = IntPtr.Zero;
    handle = _bitmap.GetHbitmap();
    return Imaging.CreateBitmapSourceFromHBitmap(handle, IntPtr.Zero, Int32Rect.Empty, BitmapSizeOptions.FromEmptyOptions());

    }

    void MainWindow_Loaded(object sender, RoutedEventArgs e)
    {
    _source = GetSource();
    imBackground.Source = _source;
    ImageAnimator.Animate(_bitmap, OnFrameChanged);
    }

    private void FrameUpdatedCallback()
    {
    ImageAnimator.UpdateFrames();
    if (_source == null)
    {
    _source.Freeze();
    _source = GetSource();
    imBackground.Source = _source;
    InvalidateVisual();
    }
    }


    private void OnFrameChanged(object sender, EventArgs e)
    {
    Dispatcher.BeginInvoke(DispatcherPriority.Normal, new Action(FrameUpdatedCallback));
    }

    }
    }

    ОтветитьУдалить
    Ответы
    1. Что конкретно не работает? Не компилируется, возникает ошибка во время выполнения, не анимируется изображение?

      Удалить
  2. Все компилируется, Появляется картинка ,
    но без анимации (без движения кадров).

    ОтветитьУдалить
    Ответы
    1. Вы скобки неправильно расставили. Вот так должно быть:
      private void FrameUpdatedCallback()
      {
      ImageAnimator.UpdateFrames();
      if (_source == null)
      {
      _source.Freeze();
      }
      _source = GetSource();
      imBackground.Source = _source;
      InvalidateVisual();
      }

      Удалить
  3. Большое Спасибо, теперь все работает.
    А можно (и если да то как) , так же использовать,
    картинку gif для бэкграунда окна (this.Background) ?

    ОтветитьУдалить
    Ответы
    1. Да.
      Нужно заменить
      imBackGround.Source = _source;
      на
      this.Background = new ImageBrush(_source);

      Удалить
  4. Я делаю игру на WPF, получилось установить картинку в canvas,
    при загрузки окна появляется картинка и все работает (подгружается правда, но это не страшно)
    спустя определенное время (по замыслу игры) на canvas появляется textblock с кнопкой,
    при нажатии на которую весь content игры обновляется (+ canvas.Children.Clear()) , картинка же пропадает,
    не подскажите в чем проблема?

    ОтветитьУдалить
  5. Этот комментарий был удален автором.

    ОтветитьУдалить
  6. столкнулся с еще одной проблемой,
    если долго воспроизводить gif файл (по вашей схеме)

    и при этом есть еще потоки(музыка, таймеры...)
    то на каком то этапе программа вылетает с
    эксепшином GDI+ (out of memory).

    не подскажите что это может быть?

    ОтветитьУдалить
  7. Здравствуйте!
    Все работает, спасибо. Только не понятно, а как теперь остановить анимацию и сменить на другую не gif картинку?

    ОтветитьУдалить
    Ответы
    1. Добрый день.
      Кроме метода Animate, есть еще метод StopAnimate. Он останавливает анимацию. Для замены картинки присвойте ее в imBackGround.Source

      Удалить
  8. А как воспроизвести эту gif анимацию в SplashScreen ?

    ОтветитьУдалить
  9. Добрый день, у меня почему-то нет анимации, gif отображается просто как картинка и происходит утечка памяти, и исключение в этой строке ImageAnimator.UpdateFrames();

    ОтветитьУдалить
    Ответы
    1. Добрый день.
      Какое исключение?

      Удалить
    2. Так, анимацию запустил, но происходит утечка памяти через десяток секунд и exeption OutOfMemory http://s019.radikal.ru/i635/1701/94/95fd4718cc70.png

      Удалить
  10. Все работает, но сильно пожирает память.

    ОтветитьУдалить
  11. при проигрывании гифки оперативка сжирается в геометрической прогрессии, в следствии чего - вылетает исключение

    ОтветитьУдалить
  12. Добрый день, уважаемый автор!
    Хочу ответить, что частое использование метода CreateBitmapSourceFromHBitmap расточительно по отношению к памяти. Я запустил вашим способом анимацию, она проработала минут 15, потом вышло исключение, что памяти не хватает.
    Используйте метод CreateBitmapSourceFromHBitmap только в редких местах. Не используйте данный метод для обновления каждого кадра. В WPF только автоматический сборщик мусора очищает память, выделенную этим методом. Вызывать вручную сборщик мусора - это косяк. Для постоянных изменений рекомендуется использовать WinForms-компоненты - там работа с памятью более адекватная. Для gif-анимаций такой способ, я считаю, не рекомендуется к использованию. Если ваша программа вылетает только из-за одной анимации - это серьезный косяк, согласитесь?

    ОтветитьУдалить
    Ответы
    1. Добрый день, соглашусь. Не смотрел на производительность этого решения :( Стояла задача, реализовать. Не подскажите на какой версии Framework-а проверяли?

      Удалить
  13. Этот комментарий был удален автором.

    ОтветитьУдалить