以编程方式创建动画位图图像

时间:2015-07-28 11:54:29

标签: c# winforms animation drawing gdi+

我想手动创建一个System.Drawing.Bitmap实例,其中包含一个动画。

要创建的Bitmap实例应符合以下条件:

  • 这是一个动画(image.FrameDimensionsLists有一个时间维度)
  • 它有多个框架(image.GetFrameCount(dimension) > 1
  • 我可以获得帧之间的延迟(image.GetPropertyItem(0x5100).Value

我很确定可以通过一些WinApi创建这样的图像。这就是GIF解码器实际上也能做到的。

我知道我可以播放动画,如果我通过手动操作来自任何来源的帧,但我想以兼容的方式进行:如果我可以生成这样的位图,我可以简单地使用它ButtonLabelPictureBox或任何其他现有控件,内置ImageAnimator也可以自动处理。

大多数类似主题建议将帧转换为动画GIF;但是,这不是一个好的解决方案,因为它不能处理真彩色和半透明度(例如APNG动画)。

更新:经过一番探索,我了解到我可以使用WIC实现解码器;但是,我不想在Windows中注册新的解码器,它使用COM,如果可能的话我想避免使用它。最后我没有提到IWICBitmapSource,我仍然需要转换为Bitmap

更新2 :我已经设定了赏金。如果您可以实施以下方法,那么您就是赢家:

public void Bitmap CreateAnimation(Bitmap[] frames, int[] delays)
{
    // Any WinApi is allowed. WIC is also allowed, but not preferred.
    // Creating an animated GIF is not an acceptable answer. What if frames are from an APNG?
}

3 个答案:

答案 0 :(得分:3)

    public void Bitmap CreateAnimation(Bitmap[] frames, int[] delays)

对此类预期实施设置严格限制并不是很明智。从技术上讲,利用TIFF图像格式,它可以存储多个帧。然而,它们不是基于时间的,只有GIF编解码器支持。需要一个额外的参数,以便在需要渲染下一个图像时更新控件。像这样:

    public static Image CreateAnimation(Control ctl, Image[] frames, int[] delays) {
        var ms = new System.IO.MemoryStream();
        var codec = ImageCodecInfo.GetImageEncoders().First(i => i.MimeType == "image/tiff");

        EncoderParameters encoderParameters = new EncoderParameters(2);
        encoderParameters.Param[0] = new EncoderParameter(System.Drawing.Imaging.Encoder.SaveFlag, (long)EncoderValue.MultiFrame);
        encoderParameters.Param[1] = new EncoderParameter(System.Drawing.Imaging.Encoder.Quality, (long)EncoderValue.CompressionLZW);
        frames[0].Save(ms, codec, encoderParameters);

        encoderParameters = new EncoderParameters(1);
        encoderParameters.Param[0] = new EncoderParameter(System.Drawing.Imaging.Encoder.SaveFlag, (long)EncoderValue.FrameDimensionPage);
        for (int i = 1; i < frames.Length; i++) {
            frames[0].SaveAdd(frames[i], encoderParameters);
        }
        encoderParameters.Param[0] = new EncoderParameter(System.Drawing.Imaging.Encoder.SaveFlag, (long)EncoderValue.Flush);
        frames[0].SaveAdd(encoderParameters);

        ms.Position = 0;
        var img = Image.FromStream(ms);
        Animate(ctl, img, delays);
        return img;
    }

Animate()方法需要Timer来选择下一帧并更新控件:

    private static void Animate(Control ctl, Image img, int[] delays) {
        int frame = 0;
        var tmr = new Timer() { Interval = delays[0], Enabled = true };
        tmr.Tick += delegate {
            frame++;
            if (frame >= delays.Length) frame = 0;
            img.SelectActiveFrame(FrameDimension.Page, frame);
            tmr.Interval = delays[frame];
            ctl.Invalidate();
        };
        ctl.Disposed += delegate { tmr.Dispose(); };
    }

样本用法:

    public Form1() {
        InitializeComponent();
        pictureBox1.Image = CreateAnimation(pictureBox1,
            new Image[] { Properties.Resources.Frame1, Properties.Resources.Frame2, Properties.Resources.Frame3 },
            new int[] { 1000, 2000, 300 });
    }

更明智的方法是完全降低返回值要求,这样您就不必生成TIFF。只需使用带有Action<Image>参数的Animate()方法即可更新控件的属性。但不是你要求的。

答案 1 :(得分:1)

不幸的是,我们既不能延长System.Drawing.Image也不会延伸System.Drawing.Bitmap,因此覆盖image.FrameDimensionsLists和类似的成员是不可能的,正如Hans Passant提到的那样,Windows图像编码器都不支持时间维度本身。但是我相信这个基于

的具体案例
  

我知道如果我有任何帧,我可以播放动画   通过手动执行来源,但我想以兼容的方式执行此操作:   如果我可以生成这样的Bitmap,我可以简单地在Button上使用它,   Label,PictureBox或任何其他现有控件,以及内置控件   ImageAnimator也可以自动处理它。

我们可以通过实现一个可以处理动画然后隐式地将其转换为Bitmap的新类来逃避。我们可以使用扩展方法自动激活控件的动画。我知道这种方法很简陋,但我想也许值得一提。

这是一个粗略的实现和示例用法。

AnimatedBitmap:根据提供的序列处理基于帧和时间的动画:

 public class Sequence
    {
        public Image Image { get; set; }
        public int Delay { get; set; }
    }

    public class AnimatedBitmap:IDisposable
    {
        private readonly Bitmap _buffer;
        private readonly Graphics _g;
        private readonly Sequence[] _sequences;
        private readonly CancellationTokenSource _cancelToken;

        public event EventHandler FrameUpdated;

        protected void OnFrameUpdated()
        {
            if (FrameUpdated != null)
                FrameUpdated(this, EventArgs.Empty);
        }

        public AnimatedBitmap(int width, int height, params Sequence[] sequences)
        {
            _buffer = new Bitmap(width, height, PixelFormat.Format32bppArgb) {Tag = this};

            _sequences = sequences;
            _g=Graphics.FromImage(_buffer);
            _g.CompositingMode=CompositingMode.SourceCopy;

            _cancelToken = new CancellationTokenSource();
            Task.Factory.StartNew(Animate
                , TaskCreationOptions.LongRunning
                , _cancelToken.Token);
        }

        private void Animate(object obj)
        {
            while (!_cancelToken.IsCancellationRequested)
                foreach (var sequence in _sequences)
                {
                    if (_cancelToken.IsCancellationRequested)
                        break;

                    _g.Clear(Color.Transparent);
                    _g.DrawImageUnscaled(sequence.Image,0,0);
                    _g.Flush(FlushIntention.Flush);
                    OnFrameUpdated();
                    Thread.Sleep(sequence.Delay);
                }

            _g.Dispose();
            _buffer.Dispose();
        }

        public AnimatedBitmap(params Sequence[] sequences)
            : this(sequences.Max(s => s.Image.Width), sequences.Max(s => s.Image.Height), sequences)
        {
        }

        public void Dispose()
        {
            _cancelToken.Cancel();
        }

        public static implicit operator Bitmap(AnimatedBitmap animatedBitmap)
        {
            return animatedBitmap._buffer;
        }

        public static explicit operator AnimatedBitmap(Bitmap bitmap)
        {
            var tag = bitmap.Tag as AnimatedBitmap;
            if (tag != null)
                return tag;

            throw new InvalidCastException();
        }

        public static AnimatedBitmap CreateAnimation(Image[] frames, int[] delays)
        {
            var sequences = frames.Select((t, i) => new Sequence {Image = t, Delay = delays[i]}).ToArray();
            var animated=new AnimatedBitmap(sequences);
            return animated;
        }
    }

AnimationController:处理控件动画更新

public static class AnimationController
{
    private static readonly List<Control> Controls =new List<Control>();
    private static CancellationTokenSource _cancelToken;

    static AnimationController()
    {
        _cancelToken = new CancellationTokenSource();
        _cancelToken.Cancel();
    }

    private static void Animate(object arg)
    {
        while (!_cancelToken.IsCancellationRequested)
        {
            Controls.RemoveAll(c => !(c.BackgroundImage.Tag is AnimatedBitmap));

            foreach (var c in Controls)
            {
                var control = c;
                if (!control.Disposing)
                    control.Invoke(new Action(() => control.Refresh()));
            }

            Thread.Sleep(40);
        }
    }

    public static void StartAnimation(this Control control)
    {
        if (_cancelToken.IsCancellationRequested)
        {
            _cancelToken = new CancellationTokenSource();
            Task.Factory.StartNew(Animate
                , TaskCreationOptions.LongRunning
                , _cancelToken.Token);
        }

        Controls.Add(control);
        control.Disposed += Disposed;
    }

    private static void Disposed(object sender, EventArgs e)
    {
        (sender as Control).StopAnimation();
    }

    public static void StopAnimation(this Control control)
    {
        Controls.Remove(control);
        if(Controls.Count==0)
            _cancelToken.Cancel();
    }

    public static void SetAnimatedBackground(this Control control, AnimatedBitmap bitmap)
    {
        control.BackgroundImage = bitmap;
        control.StartAnimation();
    }
}

以下是示例用法:

    public Form1()
    {
        InitializeComponent();

        var frame1 = Image.FromFile(@"1.png");
        var frame2 = Image.FromFile(@"2.png");

        var animatedBitmap= new AnimatedBitmap(
            new Sequence {Image = frame1, Delay = 33},
            new Sequence {Image = frame2, Delay = 33}
            );

        // or we can do
        //animatedBitmap = AnimatedBitmap.CreateAnimation(new[] {frame1, frame2}, new[] {1000, 2000});

        pictureBox1.SetAnimatedBackground(animatedBitmap);
        button1.SetAnimatedBackground(animatedBitmap);
        label1.SetAnimatedBackground(animatedBitmap);
        checkBox1.SetAnimatedBackground(animatedBitmap);

        //or we can do
        //pictureBox1.BackgroundImage = animatedBitmap;
        //pictureBox1.StartAnimation();
    }

答案 2 :(得分:0)

你去(简答:-):

public void Bitmap CreateAnimation(Bitmap[] frames, int[] delays)
{
    throw new NotSupportedException();
}

说真的,你现在可以安全地删除赏金,因为没有你设置的约束的解决方案。从理论上讲,你可以实现一个自定义WIC编解码器,但它需要COM注册才能被使用(例如,我甚至不确定它是否会被GDI +使用,尽管基于WIC,WPF仍然停留在内置的编解码器中),会引入部署问题而且不值得。奇怪的是System.Drawing.Image没有虚拟方法,无法继承,ImageAnimator是硬编码绑定到它,但这是怎么回事。你应该和#34;开箱即用&#34;动画Gif支持,或使用您自己的解决方案:-)。
你的好奇心如何,我认为你刚开始做错了假设

  

我非常确定可以通过一些WinApi创建这样的图像。这就是GIF解码器实际上也能做到的。

然后在评论中

  

GIF解码器如何做到这一点,无论源格式如何,我如何获得相同的结果

它没有。这里的关键词是解码器。 API(或托管的API包装器:-))在提供&#34; image&#34;时会调用它。为您服务。例如,IWICBitmapDecoder::GetFrameCount方法最有可能使用GdipImageGetFrameCount方法(如果您愿意,可以使用Image.GetFrameCount)。通常,您只能向编码器添加帧和选项,而解码器是唯一可以将此类信息返回给调用者的。