如何使用定时器对图像应用淡入淡出过渡效果?
How to apply a fade transition effect to Images using a Timer?
我正在尝试在两个 PictureBox 控件之间进行淡入淡出过渡。
每次时间过去时,我都使用计时器使用 GetPixel
和 SetPixel
更改两个 PictureBoxes 的不透明度。
在这个阶段,问题是这段代码导致异常:
System.InvalidOperationException: object is currently in use elsewhere
我试图修改克隆的位图,而不是直接对设置为控件的图像属性的位图进行操作,但无论如何都行不通。
这是我的代码:
public Bitmap changeOpacity(Bitmap pic, int opacity)
{
for (int w = 0; w < pic.Width; w++)
{
for (int h = 0; h < pic.Height; h++)
{
Color c = pic.GetPixel(w, h);
Color newC = Color.FromArgb(opacity, c);
pic.SetPixel(w, h, newC);
}
}
return pic;
}
public void CrossFade(PictureBox pictureOut, PictureBox pictureIn, int duration)
{
int outChange = 255; // opacity of pictureOut
int inChange = 0; // opacity of pictureIn
int change = 55; // change of opacity
fadeTimer.Interval = 10; // this timer's type is System.Timers.Timer
Bitmap bmp = new Bitmap(pictureIn.Image);
// make the pictureIn transparent first
pictureIn.Image = changeOpacity((Bitmap)bmp.Clone(), 0);
fadeTimer.Elapsed += (sender, e) => CrossFadeEvent(sender, e, pictureOut, pictureIn, outChange, inChange, change);
fadeTimer.Start();
}
// being called every time interval
private void CrossFadeEvent(Object source, System.Timers.ElapsedEventArgs e, PictureBox pictureOut, PictureBox pictureIn, int oChange, int iChange, int change)
{
if (iChange <= 255)
{
oChange -= change;
iChange += change;
textBox1.Text = iChange.ToString();
pictureOut.Image = changeOpacity((Bitmap)pictureOut.Image.Clone(), oChange);
pictureIn.Image = changeOpacity((Bitmap)pictureIn.Image.Clone(), iChange);
}
else if (iChange > 255)
{
pictureIn.Image = changeOpacity((Bitmap)pictureOut.Image.Clone(), 255);
fadeTimer.Stop();
}
}
这里有一些问题需要解决:
→ fadeTimer.Interval = 10;
:
您(可能)使用了错误的计时器:System.Timers.Timer's Elapsed is raised in a ThreadPool Thread. Unless you have set the SynchronizingObject 到 Control 对象,然后用于编组处理程序调用,在处理程序中引用 Control 会导致麻烦(跨线程违规异常)。
在这种情况下,您可以改用 System.Windows.Forms.Timer:它的 Tick
事件在 UI 线程中引发。
此外,计时器间隔太低。 System.Windows.Forms.Timer
的标准(官方)分辨率是 55ms
(高于 System.Timers.Timer
)。您最终会遇到重叠事件。
→ GetPixel()
/ SetPixel()
:
不能用于此任务。当顺序调用以设置多个像素时,这些方法太慢了。它们用于修改一小组像素(或仅一个),而不是修改整个图像的像素。
Bitmap.LockBits() 是用来设置位图颜色字节的常用工具。
→ pictureOut.Image = changeOpacity((Bitmap)pictureOut.Image.Clone(), oChange);
:
您正在使用控件的图像 属性 来提供源位图,然后您使用相同的源设置相同的 属性 提供源,修改。
这永远不会给你完全褪色图像,你在找麻烦。
有一个简单的工具可用于轻松完成此任务:ColorMatrix class。 class 处理标准的 5x5 矩阵,提供一些允许设置矩阵组件值的简化工具。
[3, 3]
处的5x5矩阵分量(Matrix3x3
)表示所有RGB
分量的Alpha值。
ColorMatrix
class 使用接受 ImageAttributes
对象作为参数的 SetColorMatrix() method of the ImageAttributes
class, which is then passed to the Graphics.DrawImage() 重载应用于位图。
因为这个 Fade 过程在其他情况下可能有用,我认为创建一个扩展方法是个好主意:它添加了一个新的 SetOpacity()
方法到位图 class.
如果淡入淡出效果需要,它还可以同时更改 Gamma。
剩下的就是加载两个Bitmap,创建一个Timer,设置一个sensible Interval
(这里是100ms
)然后在surface上绘制Bitmap两个 PictureBox 控件(此处为三个,用于测试简单的混合效果)。
不透明度 increment/decrement 值设置为 .025f
,因此不透明度每秒变化 1/4
0.0f-1.0f
最大范围。根据需要进行调整。
由于 GIF 动画只能使用 256 色,质量下降
▶ 将扩展 class 添加到项目中。
▶ 设置一个包含 3 个 PictureBox 控件的表单,并将您在此处找到的 3 个事件句柄分配给它们中的每一个。
▶ 不要在设计时将位图分配给图片框。位图在 运行 时间加载,如示例代码所示。
▶ 添加一个按钮来启动定时器。
▶ 当 fading
过程终止时,您立即重新启动定时器,因为它 倒回 本身(淡入淡出重新开始,对每个位图和混合位图应用相反的效果) .
保持诊断工具处于打开状态:您会发现即使多次重复该操作,您也不会浪费 1 MB 的内存。
using System.Drawing;
using System.IO;
using System.Windows.Forms;
public partial class FormBitmaFadeTest : Form
{
Bitmap sourceBmp1 = null;
Bitmap sourceBmp2 = null;
Bitmap fadeBmp1 = null;
Bitmap fadeBmp2 = null;
float opacity1 = 0.0f;
float opacity2 = 1.0f;
float increment = .025f;
Timer timer = null;
public FormBitmaFadeTest()
{
InitializeComponent();
if (components == null) components = new System.ComponentModel.Container();
components.Add(timer);
string image1Path = [Source Image 1 Path];
string image2Path = [Source Image 2 Path];
sourceBmp1 = (Bitmap)Image.FromStream(new MemoryStream(File.ReadAllBytes(image1Path)));
sourceBmp2 = (Bitmap)Image.FromStream(new MemoryStream(File.ReadAllBytes(image2Path)));
fadeBmp1 = sourceBmp1.Clone() as Bitmap;
fadeBmp2 = sourceBmp2.Clone() as Bitmap;
timer = new Timer() { Interval = 100 };
timer.Tick += this.TimerTick;
}
private void TimerTick(object sender, EventArgs e)
{
opacity1 += increment;
opacity2 -= increment;
if ((opacity1 >= 1.0f || opacity1 <= .0f) || (opacity2 >= 1.0f || opacity2 <= .0f)) {
increment *= -1;
timer.Stop();
}
fadeBmp1?.Dispose();
fadeBmp2?.Dispose();
fadeBmp1 = sourceBmp1.SetOpacity(opacity1);
fadeBmp2 = sourceBmp2.SetOpacity(opacity2);
pictureBox1.Invalidate();
pictureBox2.Invalidate();
pictureBox3.Invalidate();
}
private void pictureBox1_Paint(object sender, PaintEventArgs e)
{
if (fadeBmp1 == null) return;
var units = GraphicsUnit.Pixel;
e.Graphics.DrawImage(fadeBmp1, new RectangleF(PointF.Empty, pictureBox1.ClientSize), fadeBmp1.GetBounds(ref units), units);
}
private void pictureBox2_Paint(object sender, PaintEventArgs e)
{
if (fadeBmp2 == null) return;
var units = GraphicsUnit.Pixel;
e.Graphics.DrawImage(fadeBmp2, new RectangleF(PointF.Empty, pictureBox2.ClientSize), fadeBmp2.GetBounds(ref units), units);
}
private void pictureBox3_Paint(object sender, PaintEventArgs e)
{
if (fadeBmp1 == null || fadeBmp2 == null) return;
var units = GraphicsUnit.Pixel;
e.Graphics.DrawImage(fadeBmp2, new RectangleF(PointF.Empty, pictureBox3.ClientSize), fadeBmp2.GetBounds(ref units), units);
e.Graphics.DrawImage(fadeBmp1, new RectangleF(PointF.Empty, pictureBox3.ClientSize), fadeBmp1.GetBounds(ref units), units);
}
}
扩展方法:
Extension Methods (C# Programming Guide)
using System.Drawing;
using System.Drawing.Imaging;
public static class BitmapExtensions
{
static float[][] fadeMatrix = {
new float[] {1, 0, 0, 0, 0},
new float[] {0, 1, 0, 0, 0},
new float[] {0, 0, 1, 0, 0},
new float[] {0, 0, 0, 1, 0},
new float[] {0, 0, 0, 0, 1}
};
public static Bitmap SetOpacity(this Bitmap bitmap, float Opacity, float Gamma = 1.0f)
{
var mx = new ColorMatrix(fadeMatrix);
mx.Matrix33 = Opacity;
var bmp = new Bitmap(bitmap.Width, bitmap.Height);
using (var g = Graphics.FromImage(bmp))
using (var attributes = new ImageAttributes()) {
attributes.SetGamma(Gamma, ColorAdjustType.Bitmap);
attributes.SetColorMatrix(mx, ColorMatrixFlag.Default, ColorAdjustType.Bitmap);
g.Clear(Color.Transparent);
g.DrawImage(bitmap, new Rectangle(0, 0, bmp.Width, bmp.Height),
0, 0, bitmap.Width, bitmap.Height, GraphicsUnit.Pixel, attributes);
return bmp;
}
}
}
我正在尝试在两个 PictureBox 控件之间进行淡入淡出过渡。
每次时间过去时,我都使用计时器使用 GetPixel
和 SetPixel
更改两个 PictureBoxes 的不透明度。
在这个阶段,问题是这段代码导致异常:
System.InvalidOperationException: object is currently in use elsewhere
我试图修改克隆的位图,而不是直接对设置为控件的图像属性的位图进行操作,但无论如何都行不通。
这是我的代码:
public Bitmap changeOpacity(Bitmap pic, int opacity)
{
for (int w = 0; w < pic.Width; w++)
{
for (int h = 0; h < pic.Height; h++)
{
Color c = pic.GetPixel(w, h);
Color newC = Color.FromArgb(opacity, c);
pic.SetPixel(w, h, newC);
}
}
return pic;
}
public void CrossFade(PictureBox pictureOut, PictureBox pictureIn, int duration)
{
int outChange = 255; // opacity of pictureOut
int inChange = 0; // opacity of pictureIn
int change = 55; // change of opacity
fadeTimer.Interval = 10; // this timer's type is System.Timers.Timer
Bitmap bmp = new Bitmap(pictureIn.Image);
// make the pictureIn transparent first
pictureIn.Image = changeOpacity((Bitmap)bmp.Clone(), 0);
fadeTimer.Elapsed += (sender, e) => CrossFadeEvent(sender, e, pictureOut, pictureIn, outChange, inChange, change);
fadeTimer.Start();
}
// being called every time interval
private void CrossFadeEvent(Object source, System.Timers.ElapsedEventArgs e, PictureBox pictureOut, PictureBox pictureIn, int oChange, int iChange, int change)
{
if (iChange <= 255)
{
oChange -= change;
iChange += change;
textBox1.Text = iChange.ToString();
pictureOut.Image = changeOpacity((Bitmap)pictureOut.Image.Clone(), oChange);
pictureIn.Image = changeOpacity((Bitmap)pictureIn.Image.Clone(), iChange);
}
else if (iChange > 255)
{
pictureIn.Image = changeOpacity((Bitmap)pictureOut.Image.Clone(), 255);
fadeTimer.Stop();
}
}
这里有一些问题需要解决:
→ fadeTimer.Interval = 10;
:
您(可能)使用了错误的计时器:System.Timers.Timer's Elapsed is raised in a ThreadPool Thread. Unless you have set the SynchronizingObject 到 Control 对象,然后用于编组处理程序调用,在处理程序中引用 Control 会导致麻烦(跨线程违规异常)。
在这种情况下,您可以改用 System.Windows.Forms.Timer:它的 Tick
事件在 UI 线程中引发。
此外,计时器间隔太低。 System.Windows.Forms.Timer
的标准(官方)分辨率是 55ms
(高于 System.Timers.Timer
)。您最终会遇到重叠事件。
→ GetPixel()
/ SetPixel()
:
不能用于此任务。当顺序调用以设置多个像素时,这些方法太慢了。它们用于修改一小组像素(或仅一个),而不是修改整个图像的像素。
Bitmap.LockBits() 是用来设置位图颜色字节的常用工具。
→ pictureOut.Image = changeOpacity((Bitmap)pictureOut.Image.Clone(), oChange);
:
您正在使用控件的图像 属性 来提供源位图,然后您使用相同的源设置相同的 属性 提供源,修改。
这永远不会给你完全褪色图像,你在找麻烦。
有一个简单的工具可用于轻松完成此任务:ColorMatrix class。 class 处理标准的 5x5 矩阵,提供一些允许设置矩阵组件值的简化工具。
[3, 3]
处的5x5矩阵分量(Matrix3x3
)表示所有RGB
分量的Alpha值。
ColorMatrix
class 使用接受 ImageAttributes
对象作为参数的 SetColorMatrix() method of the ImageAttributes
class, which is then passed to the Graphics.DrawImage() 重载应用于位图。
因为这个 Fade 过程在其他情况下可能有用,我认为创建一个扩展方法是个好主意:它添加了一个新的 SetOpacity()
方法到位图 class.
如果淡入淡出效果需要,它还可以同时更改 Gamma。
剩下的就是加载两个Bitmap,创建一个Timer,设置一个sensible Interval
(这里是100ms
)然后在surface上绘制Bitmap两个 PictureBox 控件(此处为三个,用于测试简单的混合效果)。
不透明度 increment/decrement 值设置为 .025f
,因此不透明度每秒变化 1/4
0.0f-1.0f
最大范围。根据需要进行调整。
由于 GIF 动画只能使用 256 色,质量下降
▶ 将扩展 class 添加到项目中。
▶ 设置一个包含 3 个 PictureBox 控件的表单,并将您在此处找到的 3 个事件句柄分配给它们中的每一个。
▶ 不要在设计时将位图分配给图片框。位图在 运行 时间加载,如示例代码所示。
▶ 添加一个按钮来启动定时器。
▶ 当 fading
过程终止时,您立即重新启动定时器,因为它 倒回 本身(淡入淡出重新开始,对每个位图和混合位图应用相反的效果) .
保持诊断工具处于打开状态:您会发现即使多次重复该操作,您也不会浪费 1 MB 的内存。
using System.Drawing;
using System.IO;
using System.Windows.Forms;
public partial class FormBitmaFadeTest : Form
{
Bitmap sourceBmp1 = null;
Bitmap sourceBmp2 = null;
Bitmap fadeBmp1 = null;
Bitmap fadeBmp2 = null;
float opacity1 = 0.0f;
float opacity2 = 1.0f;
float increment = .025f;
Timer timer = null;
public FormBitmaFadeTest()
{
InitializeComponent();
if (components == null) components = new System.ComponentModel.Container();
components.Add(timer);
string image1Path = [Source Image 1 Path];
string image2Path = [Source Image 2 Path];
sourceBmp1 = (Bitmap)Image.FromStream(new MemoryStream(File.ReadAllBytes(image1Path)));
sourceBmp2 = (Bitmap)Image.FromStream(new MemoryStream(File.ReadAllBytes(image2Path)));
fadeBmp1 = sourceBmp1.Clone() as Bitmap;
fadeBmp2 = sourceBmp2.Clone() as Bitmap;
timer = new Timer() { Interval = 100 };
timer.Tick += this.TimerTick;
}
private void TimerTick(object sender, EventArgs e)
{
opacity1 += increment;
opacity2 -= increment;
if ((opacity1 >= 1.0f || opacity1 <= .0f) || (opacity2 >= 1.0f || opacity2 <= .0f)) {
increment *= -1;
timer.Stop();
}
fadeBmp1?.Dispose();
fadeBmp2?.Dispose();
fadeBmp1 = sourceBmp1.SetOpacity(opacity1);
fadeBmp2 = sourceBmp2.SetOpacity(opacity2);
pictureBox1.Invalidate();
pictureBox2.Invalidate();
pictureBox3.Invalidate();
}
private void pictureBox1_Paint(object sender, PaintEventArgs e)
{
if (fadeBmp1 == null) return;
var units = GraphicsUnit.Pixel;
e.Graphics.DrawImage(fadeBmp1, new RectangleF(PointF.Empty, pictureBox1.ClientSize), fadeBmp1.GetBounds(ref units), units);
}
private void pictureBox2_Paint(object sender, PaintEventArgs e)
{
if (fadeBmp2 == null) return;
var units = GraphicsUnit.Pixel;
e.Graphics.DrawImage(fadeBmp2, new RectangleF(PointF.Empty, pictureBox2.ClientSize), fadeBmp2.GetBounds(ref units), units);
}
private void pictureBox3_Paint(object sender, PaintEventArgs e)
{
if (fadeBmp1 == null || fadeBmp2 == null) return;
var units = GraphicsUnit.Pixel;
e.Graphics.DrawImage(fadeBmp2, new RectangleF(PointF.Empty, pictureBox3.ClientSize), fadeBmp2.GetBounds(ref units), units);
e.Graphics.DrawImage(fadeBmp1, new RectangleF(PointF.Empty, pictureBox3.ClientSize), fadeBmp1.GetBounds(ref units), units);
}
}
扩展方法:
Extension Methods (C# Programming Guide)
using System.Drawing;
using System.Drawing.Imaging;
public static class BitmapExtensions
{
static float[][] fadeMatrix = {
new float[] {1, 0, 0, 0, 0},
new float[] {0, 1, 0, 0, 0},
new float[] {0, 0, 1, 0, 0},
new float[] {0, 0, 0, 1, 0},
new float[] {0, 0, 0, 0, 1}
};
public static Bitmap SetOpacity(this Bitmap bitmap, float Opacity, float Gamma = 1.0f)
{
var mx = new ColorMatrix(fadeMatrix);
mx.Matrix33 = Opacity;
var bmp = new Bitmap(bitmap.Width, bitmap.Height);
using (var g = Graphics.FromImage(bmp))
using (var attributes = new ImageAttributes()) {
attributes.SetGamma(Gamma, ColorAdjustType.Bitmap);
attributes.SetColorMatrix(mx, ColorMatrixFlag.Default, ColorAdjustType.Bitmap);
g.Clear(Color.Transparent);
g.DrawImage(bitmap, new Rectangle(0, 0, bmp.Width, bmp.Height),
0, 0, bitmap.Width, bitmap.Height, GraphicsUnit.Pixel, attributes);
return bmp;
}
}
}