带过渡效果的图片幻灯片
显示具有各种过渡效果的图片列表
引言
这是一个图片控件,它通过淡入淡出、溶解、滑动等各种过渡效果,在图片列表中循环显示图片。
使用代码
属性
我将此控件的所有特殊属性归类为“效果”。
bool
Autostart
- 如果为 true,则控件加载后立即开始过渡效果。在显示列表中的第一张图片之前,会执行一个最小的
DelayTime
延迟。
ImageEntry
DefaultBitmap
- 在开始过渡之前显示的图片。在过渡开始之前以及在设计器中,都会显示此图片。
int
DelayTime
- 每次过渡之间的时间(以毫秒为单位)。
System.Drawing.ContentAlignment
ImageAlignment
- 在控件内绘制每张图片的对齐方式。
System.Windows.Forms.Border3DStyle
ImageBorder3DStyle
- 如果
ImageBorderStyle
设置为BorderStyle.Fixed3D
,则此属性定义围绕图片绘制的 3D 边框样式。
System.Windows.Forms.BorderStyle
ImageBorderStyle
- 定义围绕图片绘制的边框类型。这与从
UserControl
继承的BorderStyle
属性不同,后者会在整个UserControl
周围绘制边框,而此属性则会在显示的所有已调整大小的图片周围绘制边框。
List<ImageEntry>
Images
- 包含将在控件中显示的图片列表。有关这些对象如何定义的解释,请参见下面的
ImageEntry
类部分。
bool
RandomOrder
- 定义列表是按顺序显示(
False
)还是按随机顺序显示(True
)。
TransitionEffects
TransitionEffect
- 定义在图片之间过渡时使用的效果。选择
TransitionEffects.Random
将为每次过渡选择一个随机效果。
int
TransitionFramesPerSecond
- 每秒绘制的过渡帧数。
int
TransitionTime
- 过渡完成所需的时间(以毫秒为单位)。
TransitionEffects.None
是例外,它会立即显示下一张图片。
方法
此控件只有两个公共方法。
void
Start() - 开始过渡效果。void
Stop() - 停止过渡效果。
事件
void
TransitionStarted(object sender, EventArgs args) - 当控件上的过渡开始时触发。void
TransitionStopped(object sender, EventArgs args) - 当控件上的过渡停止时触发。
对象
class
ImageEntry
表示在控件中显示的图片。
备注
基类 ImageEntry 用于处理在设计时加载或从文件加载的图片。Image
和 Path
属性定义了图片的来源。如果输入了 Path
,则在检索 Image
属性之前,图片实际上不会加载到内存中。
此对象的 SizeMode
属性定义了图片的绘制方式(参见下面的 ImageDrawMode
枚举)。
我定义此对象的根本原因是我编写此控件的应用程序能够从数据库或 URL 获取图片。对于这两种情况,我都对 ImageEntry
对象进行了子类化,并使用图片的数据库 ID 或在线 URL 来初始化它。与父类一样,图片数据本身在实际需要时(即访问 Image
属性时)才会下载。
枚举
enum
ImageDrawMode
描述 ImageEntry
对象中的图片如何在控件中绘制。可能的值为:
- Copy - 图片以其原始尺寸绘制。如果图片超出控件边界,则会被简单地裁剪。
- Stretch - 图片被缩放以适应控件。纵横比不一定保持。
- Zoom - 图片被缩放至尽可能大的尺寸以适应控件,同时保持纵横比。
enum
TransitionEffects
描述从一张图片过渡到下一张图片时使用的效果类型。
- None - 这是唯一一个不需要
TransitionTime
来完成的效果。它只是简单地将一张图片替换为下一张。 - Fade - 当前显示的图片会增加其透明度,同时下一张图片会叠加在其上方并增加其不透明度。同时改变两张图片的原因是这样做可以处理一张或两张图片中的透明像素。
- Dissolve - 下一张图片通过以随机顺序替换所有像素来过渡到当前图片。
- ZoomIn - 下一张图片以由外向内的减小圆圈覆盖当前图片。
- ZoomOut - 下一张图片以由内向外的增大圆圈覆盖当前图片。
- SlideLeft - 下一张图片和当前图片都向左滑动,当前图片从左侧退出,新图片从右侧进入。
- SlideRight - 下一张图片和当前图片都向右滑动,当前图片从右侧退出,新图片从左侧进入。
- SlideUp - 下一张图片和当前图片都向上滑动,当前图片从顶部边缘退出,新图片从底部进入。
- SlideDown - 下一张图片和当前图片都向下滑动,当前图片从底部边缘退出,新图片从顶部进入。
工作原理
我遇到的第一个问题是,我使用的所有图片大小都不同。为了执行过渡,我需要在一个相同大小的画布上进行操作。因此,在任何过渡开始之前,我都会将新图像绘制到一个与 ClientRectangle
大小相同的位图中。由于绘制的图像可能与已有的图像大小不同,因此此位图被创建为 System.Drawing.Imaging.PixelFormat.Format32bppArgb
,并用完全透明的背景进行初始化。我使用 ImageEntry.SizeMode
属性来确定如何在新的位图中绘制图像。
Bitmap newBmp = new Bitmap(clientRectangle.Width, clientRectangle.Height, System.Drawing.Imaging.PixelFormat.Format32bppArgb);
using (Graphics g = Graphics.FromImage(newBmp)) {
Point imgOrigin = Point.Empty;
Size bmpSize = clientRectangle.Size;
Rectangle targetRectangle;
g.SmoothingMode = System.Drawing.Drawing2D.SmoothingMode.AntiAlias;
g.TextRenderingHint = System.Drawing.Text.TextRenderingHint.AntiAlias;
g.PageUnit = GraphicsUnit.Pixel;
if (orig.Image != null) {
if (orig.SizeMode == ImageDrawMode.Zoom) {
double ratio = (double)bmpSize.Width / (double)orig.Image.Width;
double ratioh = (double)bmpSize.Height / (double)orig.Image.Height;
if (ratioh < ratio)
ratio = ratioh;
bmpSize = new Size((int)(ratio * orig.Image.Width), (int)(ratio * orig.Image.Height));
imgOrigin = LocateBitmap(clientRectangle.Size, bmpSize);
targetRectangle = new Rectangle(imgOrigin, bmpSize);
if (_borderStyle == System.Windows.Forms.BorderStyle.Fixed3D)
targetRectangle.Inflate(-SystemInformation.Border3DSize.Width, -SystemInformation.Border3DSize.Height);
else if (_borderStyle == System.Windows.Forms.BorderStyle.FixedSingle)
targetRectangle.Inflate(-1, -1);
g.DrawImage(orig.Image, targetRectangle, new Rectangle(0, 0, orig.Image.Width, orig.Image.Height), GraphicsUnit.Pixel);
}
else if (orig.SizeMode == ImageDrawMode.Copy) {
imgOrigin = LocateBitmap(clientRectangle.Size, orig.Image.Size);
targetRectangle = new Rectangle(imgOrigin, orig.Image.Size);
targetRectangle.Intersect(clientRectangle);
if (_borderStyle == System.Windows.Forms.BorderStyle.Fixed3D)
targetRectangle.Inflate(-SystemInformation.Border3DSize.Width, -SystemInformation.Border3DSize.Height);
else if (_borderStyle == System.Windows.Forms.BorderStyle.FixedSingle)
targetRectangle.Inflate(-1, -1);
g.DrawImageUnscaledAndClipped(orig.Image, targetRectangle);
}
else { // orig.SizeMode == ImageDrawMode.Stretch
targetRectangle = clientRectangle;
if (_borderStyle == System.Windows.Forms.BorderStyle.Fixed3D)
targetRectangle.Inflate(-SystemInformation.Border3DSize.Width, -SystemInformation.Border3DSize.Height);
else if (_borderStyle == System.Windows.Forms.BorderStyle.FixedSingle)
targetRectangle.Inflate(-1, -1);
g.DrawImage(orig.Image, targetRectangle);
}
}
请注意,在将图像绘制到新位图时,会留出空间以允许围绕调整大小的图像绘制边框。
一旦我们有了两个大小和格式完全相同的图像,那么在这两者之间进行过渡就变成了一个问题,即确定每帧过渡中两张图像的差异。我的处理方式是设置一个私有的抽象类“Transition”。每种效果都有一个继承自这个基类的子类。这些类都有一个构造函数,接受两张图片(背景和前景)、过渡所需的时间以及每帧过渡的时间。在每种情况下,构造函数都会设置跟踪过渡阶段所需的任何私有成员。
每个过渡类都声明了 6 个主要方法 - Start、Stop、Step、Finish、Resize 和 Draw,以及两个事件 - Changed 和 Finished。Changed 事件在过渡发生变化时触发;它基本上会使客户端区域失效以强制重绘。Finished 事件在所有过渡帧完成后触发。当捕获到此事件时,Transition 对象将被销毁,并在主线程上启动一个计时器,以允许背景图像保持在屏幕上直到下一个过渡开始(DelayTime
毫秒)。在等待此延迟触发时,会启动一个后台线程来生成下一个前景图像并创建下一个过渡。
除了 NoTransition
之外,所有的 Transition 对象都会创建一个 System.Threading.Timer
来处理过渡的每一帧。因为这不是在主窗口线程上运行的,所以需要小心处理,以确保(a)任何属于主窗口的对象都不会被更改,以及(b)事件在调用之前都会被编组回主线程。第一点很容易 - 过渡只是在其自己的私有成员上工作以生成每一帧。第二点是在基类 Transition 的 RaiseChanged 和 RaisedFinished 方法中完成的。它们都使用相同的模式来确保事件在创建处理程序的同一线程上触发。
protected virtual void RaiseChanged() {
if (Changed != null) {
foreach (Delegate d in Changed.GetInvocationList()) {
ISynchronizeInvoke s = d.Target as ISynchronizeInvoke;
if (s != null && s.InvokeRequired)
s.BeginInvoke(d, new object[] { this, EventArgs.Empty });
else
d.DynamicInvoke(this, EventArgs.Empty);
}
}
}
周围有很多示例和文章讨论这种编组技术,所以我在这里就不详细介绍了。我只是包含了上面的代码来解释我所做的事情。
接下来的几段将概述我执行每种过渡的方式。
TransitionEffects.None
这是最简单的过渡,因为实际上没有显示任何效果。当调用 Start
时,该类会立即调用 Finish
。新图像在 Finished 事件中替换旧图像,并重新启动延迟计时器。
TransitionEffects.Fade
淡入淡出效果通过绘制具有增加透明度因子的背景图像,然后在其上方绘制具有减小透明度因子的前景图像来实现。Start 方法创建两个 ColorMatrix 对象 - 一个应用于前景,另一个应用于背景。前景矩阵的初始透明度因子为 0,背景为 0。每次调用 Step
方法都会根据当前已处理的总过渡的比例来修改这些因子。
public override void Step() {
lock (_sync) {
_currentStep += _stepTime;
_fade = Math.Min(1F, (float)_currentStep / (float)_transitionTime);
_cmBg.Matrix33 = 1F - (_cmFg.Matrix33 = _fade);
}
base.Step();
}
Draw
方法只是绘制具有计算出的透明度的背景图像,然后在其上方绘制具有其透明度因子的前景图像。
public override void Draw(Graphics g) {
lock (_sync) {
if (_transitioning) {
ImageAttributes attr = new ImageAttributes();
if (_back != null) {
attr.SetColorMatrix(_cmBg);
g.DrawImage(_back, _clientRectangle, 0, 0, _clientRectangle.Width, _clientRectangle.Height, GraphicsUnit.Pixel, attr);
}
if (_front != null) {
attr.SetColorMatrix(_cmFg);
g.DrawImage(_front, _clientRectangle, 0, 0, _clientRectangle.Width, _clientRectangle.Height, GraphicsUnit.Pixel, attr);
}
}
else if (_finished)
g.DrawImage(_front, 0, 0);
else
g.DrawImage(_back, 0, 0);
}
}
请注意每个方法上的同步锁。这是因为 Step
方法是从计时器线程调用的,而 Draw
方法是从主线程调用的,我们不希望在绘制中间更改值。这意味着在进行绘制之前,当前步骤的所有更新都已完成,或者如果正在进行绘制,则在绘制完成后才会更新值。所有过渡效果都具有相同的锁定机制。
Resize
方法(从主线程的 OnClientSizeChanged
覆盖调用)只是用调整大小后的新图像替换当前的前景和背景图像。当影响图像外观的任何属性(例如更改边框或图像对齐方式)发生变化时,也会调用此方法,因为所涉及的处理与更改图像大小相同。事实上,我可能应该将此方法命名为“AppearanceChanged”或类似名称,以反映所发生的情况。
TransitionEffects.Dissolve
溶解效果通过以随机顺序将背景图像中的每个像素替换为前景图像中相应的像素,直到所有像素都被替换。这是通过准备一个包含图像中所有像素偏移量的列表来完成的,该列表使用 Fisher-Yates Shuffle 算法进行洗牌。这在构造函数中完成。
_randomPixels = new List<int>(_imageSize);
for (int i = 0; i < _imageSize; ++i)
_randomPixels.Add(i * 4);
for (int i = 0; i < _imageSize; ++i) {
int j = Random.Next(_imageSize);
if (i != j) {
_randomPixels[i] ^= _randomPixels[j];
_randomPixels[j] ^= _randomPixels[i];
_randomPixels[i] ^= _randomPixels[j];
}
}
引用的 _imageSize
成员就是图像的宽度乘以高度。
为了提高绘图过程的速度,DissolveTransition
实例维护了过渡当前状态的位图副本。Step
方法只是从列表中最后一个偏移量处剥离像素偏移量,并用前景像素替换过渡图像中的那些像素。此外,为了速度,图像的位被锁定,像素值直接从位图数据读取和写入。由于所有位图的格式都是 32bppARGB,因此将像素作为 Int32
读取和写入可以完美工作。这比使用 GetPixel 和 SetPixel 方法要快得多。
public override void Step() {
lock (_sync) {
_currentStep += _stepTime;
int endPoint = Math.Min(_imageSize, (int)((long)_imageSize * _currentStep / _transitionTime));
BitmapData src = _front.LockBits(_clientRectangle, ImageLockMode.ReadOnly, PixelFormat.Format32bppArgb);
BitmapData target = _transition.LockBits(_clientRectangle, ImageLockMode.WriteOnly, PixelFormat.Format32bppArgb);
for (int i = _pixelsDissolved; i < endPoint; ++i) {
Marshal.WriteInt32(target.Scan0, _randomPixels[i], Marshal.ReadInt32(src.Scan0, _randomPixels[i]));
}
_transition.UnlockBits(target);
_front.UnlockBits(src);
_pixelsDissolved = endPoint;
}
base.Step();
}
调整过渡的大小非常困难。我不仅必须重新创建随机点列表,还必须使其看起来像是已经完成了大部分过渡。并且我必须在尽可能短的时间内完成所有这些工作。为了达到这个目的,如果图像的大小增加了,我只是将随机像素列表的大小增加到新的大小,并且只洗牌列表的末尾。如果大小减小了,我只需删除任何大于新大小的像素地址。
int newSize = _clientRectangle.Width * _clientRectangle.Height;
if (newSize > _imageSize) {
_randomPixels.Capacity = newSize;
for (int i = _imageSize; i < newSize; ++i) {
_randomPixels.Add(i * 4);
}
for (int i = _imageSize; i < newSize; ++i) {
int j = Random.Next(_imageSize);
if (i != j) {
_randomPixels[i] ^= _randomPixels[j];
_randomPixels[j] ^= _randomPixels[i];
_randomPixels[i] ^= _randomPixels[j];
}
}
}
else if (newSize < _imageSize) {
int maxPoint = (newSize - 1)* 4;
_randomPixels.RemoveAll(i => i > maxPoint);
_randomPixels.TrimExcess();
}
计算已处理像素的相对位置非常复杂,因此非常耗时。所以,我只是将现有的偏移量用于新尺寸,并在它们现在的位置处理像素。此函数中要处理的像素数量是根据它当前所处的步骤,根据新尺寸的图像重新计算的。考虑到图像调整大小时屏幕上发生的所有其他事情以及帧的处理速度,眼睛根本注意不到过渡中的这种不规则性。
TransitionEffects.Zoom...
ZoomIn 和 ZoomOut 过渡的工作方式相同 - 只是它们绘制图像的顺序相反。该技术非常简单。我创建一个 GraphicsPath
对象并向其中添加一个椭圆。然后将正在绘制的 Graphics
对象的剪辑区域设置为该路径,并使用该剪辑区域绘制适当的图像。然后,我将剪辑区域设置为 ClientRectangle 与现有的(椭圆)剪辑区域进行异或运算,并绘制另一张图像。这会在原始椭圆之外的所有区域绘制第二张图像。
对于 ZoomIn 过渡,椭圆最初绘制在一个比 ClientRectangle
大 20% 的矩形中,并且每一步都会减小该矩形的大小。背景图像首先绘制,并裁剪以适合椭圆。然后对剪辑区域进行异或运算(如上所述),并在 ClientRectangle 的剩余部分绘制前景图像。
对于 ZoomOut 过渡,所有操作都是相反的。起始椭圆只是图像中心的一个小点,并且每一步都会将椭圆的大小增加到比客户端矩形大 20%。前景图像首先在椭圆中绘制,然后背景图像绘制以填充客户端的剩余部分。
TransitionEffects.Slide...
滑动效果可能是最简单的。效果的名称表明了图像滑动的方向。背景图像朝着指定的方向滑动,直到完全移出屏幕,而前景图像则紧随其后进入。
滑动过渡的 Step
只是计算图像移动的距离。要显示的计算足够简单,可以留到实际绘制时再进行。它只是创建一个使用移动在适当轴(X 用于左右,Y 用于上下)上的计算距离的矩形,并将每张图像的适当部分绘制到正确的位置。
关注点
继承的属性
在测试控件时,所有在设计器属性网格中出现的虚假属性和事件让我感到厌烦。我开始使用 [Browsable(false)]
属性重载它们,但很快就厌倦了。所以我做了一些研究,然后开发了一种移除包含在此控件中的继承属性的方法。这种模式的概念足够复杂,以至于我认为它值得单独一篇文章 - 所以我发表了 Remove Unwanted Properties and Events from UserControl[^] 。
控件透明度
我一直在寻找使整个控件透明的解决方案,但到目前为止还没有成功。我已经设置了适当的 ControlStyle
,我没有绘制任何背景,并将基控件的 BackColor
设置为 Color.Transparent
,但它似乎只是复制了父级的背景颜色。放在此控件后面的任何东西都会被它覆盖。所以,如果有人能看看这个问题并给我一个答案,将不胜感激。