WinForms 的旋转器控件






4.87/5 (67投票s)
2006年9月7日
6分钟阅读

132851

5801
一个用于 Windows 窗体的旋转控件。
引言
如今,RSS 源已相当普遍。我一直在寻找一个 Windows 控件,一个可用于显示 RSS 源数据的旋转控件,但没能找到。所以我的下一步是自己编写一个来完成这项工作。我的想法是创建一个控件,用它来旋转一些显示数据的框架。
使用代码
为了能够使用该控件,您应该在项目中添加对 Rotator.dll 文件的引用。完成后,您将能在工具箱窗口中看到该控件,并可以将其拖放到您的窗体上。这些框架沿 X 或 Y 轴移动,方向为从下到上/从右到左。若要实现反向动画(从上到下/从左到右),您需要更改表示动画期间框架移动量的值的符号。为了看到动画效果,您需要向 Items
集合中添加/插入一些数据。下图展示了用于向集合中添加数据的窗口。
控件属性
该控件提供以下属性
Items
-RotatorItemData
的集合。它包含了要在旋转框架上显示的信息。TitleTextColor
- 用于显示旋转控件标题的颜色。TitleText
- 控件的标题。FrameAnimationDelay
- 在播放框架动画前等待的持续时间(以毫秒为单位)。FrameAnimationStep
- 框架移动的步长(以像素为单位)。FrameAnimationMode
- 定义动画模式。目前,只能在 X 或 Y 轴上进行(未来可能会有其他模式)。HeaderBrushType
- 指定框架头部的背景应如何绘制,是纯色还是渐变。HeaderColorOne
- 设置用于框架头部背景的颜色。如果画刷设置为渐变,这将被视为起始颜色。HeaderColorTwo
- 如果画刷类型已设置为渐变,则此属性设置框架头部背景的结束颜色。HeaderTextColor
- 框架头部中显示文本的颜色。HeaderFont
- 绘制框架文本头部时使用的字体。HeaderSize
- 框架头部的大小(以像素为单位)。初始大小被认为是框架的 40%。InformationBrushType
- 用于填充旋转框架主文本区域的画刷类型。InformationColorOne
- 用于旋转框架主文本区域背景的颜色。如果画刷类型选择为渐变,这被视为起始颜色。InformationColorTwo
- 在设置了渐变画刷类型的情况下,与前述属性配对的结束颜色。InformationTextColor
- 在旋转框架主文本区域中使用的文本颜色。InformationFont
- 用于在旋转框架主文本区域中渲染文本的字体。TextAnimationDelay
- 用于播放旋转框架中文本动画的时间间隔(以毫秒为单位)。
架构
此程序集中定义的主要类如下列表所示
BufferPaintingCtrl
- 继承自Panel
控件,增加了双缓冲支持。CornerCtrl
- 定义了支持边框的控件,可以使用直角或圆角进行绘制。Frame
- 在CornerCtrl
的基础上更进一步,提供了两种背景填充类型:纯色和渐变。RotatorCtrl
- 您放置在窗体上的控件。RotatorFrame
- 负责显示和播放数据动画的控件。RotatorFrameContainer
- 用于旋转框架的容器。它有两个RotatorFrame
实例,这两个实例会四处移动。RotatorFrameTemplate
- 定义框架的外观。它包含对文本颜色、字体、背景色、动画延迟等的引用。RotatorItemData
- 包含要在RotatorFrame
中显示的数据。RotatorItemDataCollection
-RotatorFrame
对象的列表。EventNotifier
- 每隔 x 毫秒触发一个通知事件的类。ITextAnimation
- 定义文本动画的接口。TypingTextAnimation
- 定义了将文本以打字效果动画显示在旋转框架控件中的接口。
现在我将简单解释一下这个控件的工作原理,其余的留给代码来说明。我将从动画开始,解释它是如何实现的。其背后的思想是强制重绘。FrameRotator
创建一个 EventNotifier
对象,该对象负责触发重绘事件。
//create the notifier
this.repaintNotifier =
new EventNotifier(template.TextAnimationDelay,
new NotifierEvent(OnNotifierEvent));
每当框架的动画标志被启用时,通知器就会被设置为触发事件。每次发出通知,框架的主文本区域就会被设为无效,并告知控件重绘其客户区的无效部分。
private void OnNotifierEvent(object sender, EventArgs args)
{
this.repaintNotifier.Pause = true;
if (this.Handle.ToInt32() > 0)
{
//invoke the delegate; thread safe access
this.Invoke(RepaintNotifyHandler);
}
}
private void Repaint()
{
this.Invalidate(new Region(infoPath));
this.Update();
}
RotatorFrame
重写了 WM_PAINT
消息的事件处理程序,因为它需要自行绘制。这其中并没有什么魔法。如果框架有关联的数据,它会尝试缓冲文本大小并调整图像大小;如果头部设置了图像,它会初始化动画(如果尚未初始化),并调用它来绘制主文本区域。以下是 Paint
事件的代码
protected override void OnPaint(System.Windows.Forms.PaintEventArgs e)
{
base.OnPaint(e);
//set up some flags
e.Graphics.CompositingQuality =
System.Drawing.Drawing2D.CompositingQuality.HighQuality;
e.Graphics.InterpolationMode =
System.Drawing.Drawing2D.InterpolationMode.HighQualityBicubic;
e.Graphics.SmoothingMode =
System.Drawing.Drawing2D.SmoothingMode.AntiAlias;
e.Graphics.TextRenderingHint =
System.Drawing.Text.TextRenderingHint.AntiAlias;
//check to see if the path has been initialized
if (graphicPath == null)
{
InitializeGraphicPath();
}
//draw header
e.Graphics.FillPath(frameTemplate.HeaderBrush, headerPath);
//draw header
e.Graphics.FillPath(frameTemplate.InformationBrush, infoPath);
//is there data linked to this object
if (null != data)
{
RectangleF outputHeaderText = headerPath.GetBounds();
float square = Math.Min(outputHeaderText.Width / 2,
outputHeaderText.Height);
//do we need to resize the image
if (bufferAdjusment)
{
if (null != localImage)
{
localImage.Dispose();
}
if (data.Image != null)
{
localImage = (Image)data.Image.Clone();
//resize the image displayed in the header
if (localImage.Width > square || localImage.Height >
outputHeaderText.Height)
{
int maxOne = (int)(Math.Max(0, square * 100 /
localImage.Width));
int maxTwo = (int)(Math.Max(0,
outputHeaderText.Height * 100 /
localImage.Height));
maxOne = Math.Min(maxOne, maxTwo);
localImage = ImageResize.Resize(localImage, maxOne);
}
}
}
if (null != localImage)
{
outputHeaderText =
new RectangleF(outputHeaderText.Left +
localImage.Width + (square - localImage.Width) / 2,
outputHeaderText.Top, outputHeaderText.Width -
localImage.Width, outputHeaderText.Height);
e.Graphics.DrawImage(localImage,
new PointF((Math.Max( square, outputHeaderText.Width / 2) -
localImage.Width) / 2,
(outputHeaderText.Height - localImage.Height) / 2));
}
//recalculate the text size if necessary
if (bufferAdjusment)
{
bufferedTextSize = e.Graphics.MeasureString(data.HeaderText,
frameTemplate.HeaderFont,
outputHeaderText.Size, stringFormat);
bufferAdjusment = false;
}
//set the header area
outputHeaderText =
new RectangleF(new PointF(outputHeaderText.X +
(outputHeaderText.Width - bufferedTextSize.Width) * 0.5f,
outputHeaderText.Y + (outputHeaderText.Height -
bufferedTextSize.Height) * 0.5f), bufferedTextSize);
//render the header text
e.Graphics.DrawString(data.HeaderText, frameTemplate.HeaderFont,
new SolidBrush(frameTemplate.HeaderTextColor),
outputHeaderText, stringFormat);
}
//is animation enabled
if (enableTextAnimation)
{
//initialize animation if necessary
if (null == textAnimation)
{
InitializeTextAnimation();
}
//set the graphics object
textAnimation.Graphics = e.Graphics;
//render the main text
textAnimation.DrawText();
textAnimation.Graphics = null;
this.repaintNotifier.Pause = false;
}
else
{
//pause the repaint event notifier
this.repaintNotifier.Pause = true;
}
}
正如我所说,框架在其绘制处理程序中调用动画来绘制文本。但打字文本动画是如何实现的呢?当有新数据可用于框架时,动画对象会接收到要播放动画的文本以及可用于绘制文本的矩形。一个索引被存储起来,指向给定文本中的当前位置;调用 Draw
方法将只渲染屏幕上索引位置之前的字符,然后将索引增加以指向下一个字符。一旦索引达到要播放动画的文本的总长度,就会触发 AnimationFinished
事件。当然,如果待显示的文本超出了可用区域,它将被裁剪。以下是实现这一功能的代码
public override void DrawText()
{
if (text != null)
{
if (null == brush)
{
brush = new SolidBrush(textColor);
}
//calculate the text size if needed
if (measureText)
{
measureText = false;
//stringFormat.LineAlignment = StringAlignment.Center;
SizeF size = graphics.MeasureString(text, font,
area.Size, stringFormat);
//center the text within the given rectangle
float widthAdjustment = (area.Width - size.Width) / 2;
float heightAdjustment = (area.Height - size.Height) / 2;
//set the new area
area = new RectangleF(new PointF(area.X + widthAdjustment,
area.Y + heightAdjustment), size);
}
if (index >= text.Length)
{
graphics.DrawString(text, font, brush, area);
//raise the event if necessary
if (null != AnimationFinished && !eventSignaled)
{
AnimationFinished(this, new EventArgs());
eventSignaled = true;
}
}
else
{
//draw part of the text
graphics.DrawString(text.Substring(0, index),
font, brush, area);
//set the new step of the animation
index++ ;
if (index > text.Length)
{
index = text.Length;
}
SizeF sizeExceed =
graphics.MeasureString(text.Substring(0, index),
font, (int)area.Width, stringFormat);
//if text exceeds the available area don't draw it
if (sizeExceed.Height > area.Height)
{
//get last empty character
//of the text being rendered and add the "..."
int lastSpace = text.LastIndexOf(' ');
if (lastSpace == text.Length - 1)
{
lastSpace = text.LastIndexOf(' ', lastSpace);
}
if (lastSpace < 0)
{
lastSpace = 0;
}
text = text.Substring(0, lastSpace) + "...";
index = text.Length;
}
}
}
}
如前所述,RotatorFrameContainer
有两个框架,一个用于显示当前信息,另一个用于缓冲下一个可用的数据。一旦它们的动画结束,第二个框架就变成显示数据的框架,而第一个框架则缓冲下一个可用的数据。这个控件也使用一个事件通知器来移动框架,就像 RotatorFrame
用于文本动画一样。
private void HandleNotification()
{
//move frames
this.SuspendLayout();
if (animationMode == RotatorControlAnimationMode.YAxis)
{
int landMark = Height / 8;
landMark = landMark - (Height - 8 * landMark);
if ((animationStep >= 0 && (secondFrame.Top <= landMark))
|| (animationStep < 0 && (secondFrame.Top >= landMark)))
{
StopFrameAnimation();
}
else
{
if (animationStep >= 0)
{
if (secondFrame.Top - animationStep <= landMark)
{
firstFrame.Top = landMark - Height;
secondFrame.Top = landMark;
}
else
{
firstFrame.Top -= (int)animationStep;
secondFrame.Top -= (int)animationStep;
}
}
else
{
if (secondFrame.Top - animationStep >= landMark)
{
firstFrame.Top = landMark - Height;
secondFrame.Top = landMark;
}
else
{
firstFrame.Top -= (int)animationStep;
secondFrame.Top -= (int)animationStep;
}
}
}
}
else
{
int landMark = Width / 4;
landMark = landMark - (Width - 4 * landMark);
if ((animationStep >= 0 &&
(secondFrame.Left - animationStep < 0)) ||
(animationStep < 0 && (secondFrame.Left -
animationStep > 0)))
{
StopFrameAnimation();
}
else
{
firstFrame.Left -= (int)animationStep;
secondFrame.Left -= (int)animationStep;
}
}
this.ResumeLayout(false);
}
private void StopFrameAnimation()
{
//animation is stopped
animating = false;
//stop the background thread raising the notification events
frameAnimationNotifier.Stop(true);
//is there a change to the collection!?
if (null == queuedChange)
{
//swap frames
SwapRotatorFrames();
//set the frames location
SetFramesPosition();
//repaint first frame
firstFrame.Refresh();
}
else
{
//there is a change to the collection
HandleItemsCollectionChanged();
}
}
因为在框架播放动画时(以及其他时候)数据集合可能会发生变化,所以 RotatorItemDataCollection
定义了一个事件,在发生变化时触发。框架容器订阅了这个事件
private void OnItemsCollectionChanged(object sender,
CollectionChangeArgs args)
{
//if animating store the change
if (animating)
{
if (null == queuedChange)
{
queuedChange = args;
}
}
else
{
//if not animationg do the updates
queuedChange = args;
HandleItemsCollectionChanged();
}
}
private void HandleItemsCollectionChanged()
{
//is there a change to the collection
if (queuedChange != null)
{
switch (queuedChange.ChangeType)
{
case ChangeType.ItemAdded:
if (queuedChange.Count == 1)
{
InitializeData();
firstFrame.EnableTextAnimation = true;
}
break;
case ChangeType.ItemsRemoved:
firstFrame.Data = null;
firstFrame.ResetText();
firstFrame.EnableTextAnimation = false;
firstFrame.Visible = false;
secondFrame.Data = null;
secondFrame.ResetText();
secondFrame.EnableTextAnimation = false;
secondFrame.Visible = false;
SetFramesPosition();
break;
case ChangeType.ItemUpdate:
if (currentIndex == queuedChange.Index)
{
firstFrame.Data = items[currentIndex];
}
break;
case ChangeType.ItemRemoved:
if (queuedChange.Count == 0)
{
firstFrame.Data = null;
firstFrame.ResetText();
firstFrame.EnableTextAnimation = false;
firstFrame.Visible = false;
secondFrame.Data = null;
secondFrame.ResetText();
secondFrame.EnableTextAnimation = false;
secondFrame.Visible = false;
SetFramesPosition();
}
else
{
if (currentIndex == queuedChange.Index)
{
BufferNextData(currentIndex);
SwapRotatorFrames();
BufferNextData(currentIndex + 1);
}
}
break;
}
queuedChange = null;
}
}
结论
或许这个控件对某些开发者会很有用。如果您遇到任何问题或需要任何增强功能,我将很乐意为您提供帮助,因此欢迎任何反馈。
历史
- 2006年9月 - 版本 1.0.0
- 首次发布。
- 2006年9月 - 版本 1.2.0
- 修复:图像大小调整不正确
- 修复:图像以圆角显示
- 修复:控件区域存在的问题