我想手动创建一个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?
}
答案 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
)。通常,您只能向编码器添加帧和选项,而解码器是唯一可以将此类信息返回给调用者的。