以编程方式创建动画位图图像
本文关键字:位图 图像 动画 创建 编程 方式 | 更新日期: 2023-09-27 18:05:03
我想"手动"创建一个包含动画的System.Drawing.Bitmap
实例。
要创建的Bitmap
实例应满足以下条件:
- 它是一个动画(
image.FrameDimensionsLists
具有时间维度( - 它有多个框架 (
image.GetFrameCount(dimension) > 1
( - 我可以得到帧之间的延迟(
image.GetPropertyItem(0x5100).Value
(
我很确定可以通过一些WinApi创建这样的图像。这也是 GIF 解码器实际所做的。
我知道如果我通过手动操作从任何来源获得帧,我可以播放动画,但我想以兼容的方式进行:如果我能生成这样的位图,我可以简单地在Button
、Label
、PictureBox
或任何其他现有控件上使用它,内置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
使用(。通常,只能向编码器添加帧和选项,解码器是唯一可以将此类信息返回给调用方的编码器。