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

5
我想手动创建一个包含动画的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个回答

5
    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()方法需要一个定时器来选择下一帧并更新控件:
    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。只需使用Animate()方法,并带有一个Action<Image>参数来更新控件的属性。但这并不是你要求的内容。

谢谢您的回答。正如我所说,我可以通过编程模拟任何动画,这就是我从APNG动画中提取帧的例子。然而,GIF解码器能够创建一个单个位图,其中包含了每一个需要的信息以便于对图像进行动画处理,我也想以某种方式实现这一点。在这里,您可以使用TIFF编码器(和解码器,当您从流中加载时)创建一个多页TIFF图像,它会生成一个具有页面维度而非时间维度的位图实例。因此内建的ImageAnimator无法在例如按钮上播放动画。 - György Kőszeg
嗯,是的,这一切都非常明显。Windows图像编码器都不支持时间维度,这就是瓶颈所在。你需要使用那个Animate()方法。效果很好,我建议你使用它。 - Hans Passant
我可以接受使用WIC的解决方案,因为我可以通过使用CopyPixels将IWICBitmapSource转换为位图:http://www.nuonsoft.com/blog/2011/10/17/introduction-to-wic-how-to-use-wic-to-load-an-image-and-draw-it-with-gdi/ - György Kőszeg
GIF解码器无法实现此功能,它依赖于创建图像文件的第三方编码器。您对“窗口图像编码器都不支持时间维度”评论听不懂。包括WIC在内。为什么这样很糟糕在这个问答中有所涉及。 - Hans Passant
不,这个问题实际上是关于编码器而不是解码器的,因为你需要创建多帧位图。我在这方面一无所获,所以我要签退了,祝你好运。 - Hans Passant
显示剩余2条评论

1
很遗憾,我们既不能扩展System.Drawing.Image也不能扩展System.Drawing.Bitmap,因此覆盖image.FrameDimensionsLists和类似成员是不可能的,正如Hans Passant所提到的,Windows图像编码器都不支持时间维度本身。但是,我相信在这种特定情况下,基于以下内容:

我知道如果我手动从任何源获取帧,则可以播放动画,但我想以兼容的方式执行此操作:如果我能生成这样的位图,则可以将其简单地用于按钮、标签、图片框或任何其他现有控件,并且内置的ImageAnimator也可以自动处理它。

我们可以通过实现一个新的类来处理动画,然后将其隐式转换为位图。我们可以使用扩展方法自动为控件激活动画。我知道这种方法有点hacky,但我认为也许值得一提。

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

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

动画控制器:处理控制动画更新。
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();
    }

0

这就是答案(简短回答):

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

现在你可以放心地取消悬赏了,因为在你设置的约束条件下没有解决方案。理论上,你可以实现一个自定义的WIC编解码器,但它需要进行COM注册才能使用(我甚至不确定它是否会被GDI+使用,例如,尽管基于WIC,但WPF仍然受限于内置编解码器),这将引入部署问题,并且并不值得。很奇怪的是,System.Drawing.Image没有虚方法,不能被继承,而ImageAnimator也与其硬编码绑定,但这就是现实。你应该使用“开箱即用”的动画GIF支持,或者使用自己的解决方案 :-).
至于你的好奇心,我认为你从错误的假设开始了

我非常确定可以通过一些WinApi创建这样的图像。这正是GIF解码器所做的。

然后在评论中

无论源格式如何,GIF解码器是如何做到这一点的,我该如何实现相同的结果

并不是这样的。关键词在于解码器。API(或托管的API包装器:-))将在为您提供“图像”服务时调用它。例如,IWICBitmapDecoder :: GetFrameCount方法很可能由GdipImageGetFrameCount(或如果您愿意,可以使用Image.GetFrameCount)使用。通常,您只能向编码器添加帧和选项,而解码器是唯一可以向调用者返回此类信息的对象。


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