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

本文关键字:位图 图像 动画 创建 编程 方式 | 更新日期: 2023-09-27 18:05:03

我想"手动"创建一个包含动画的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?
}

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

    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> 参数一起使用即可更新控件的属性。 但不是你要求的。

不幸的是,

我们既不能扩展System.Drawing.Image也不能扩展System.Drawing.Bitmap因此覆盖image.FrameDimensionsLists和类似成员是毫无疑问的,正如 Hans Passant 所提到的,没有一个 Windows 图像编码器本身支持时间维度。但是,我相信基于此特定案例

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

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

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

动画

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

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

你去吧(简短的回答:-(:

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使用(。通常,只能向编码器添加帧和选项,解码器是唯一可以将此类信息返回给调用方的编码器。