Introduction
Nowadays, RSS feeds are quite common. I was looking for a Windows control, a rotator control to be used for displaying data from an RSS feed, but couldn't find anything. So my next step was to write something to do the job for me. The idea is to have a control to rotate some frames displaying my data.
Using the code
In order to be able to use the control, you should add a reference to the Rotator.dll file in your project. Once that is done, you will be able to see the control available in the toolbox window, and you can drag and drop it onto your form. The frames are moved on the X or Y axis, down-to-up/right-to-left. Having the animation the other way around up-to-down/left-to-right, you will have to change the sign of the value telling how much to move the frames during animation. In order to see some movement there, you need to add/insert some data into the Items
collection. The following figure shows the windows used for adding data to the collection.
Control properties
The control provides the following properties:
Items
- A collection of RotatorItemData
. This holds the information to be presented on the rotating frames.
TitleTextColor
- The color used for displaying the title of the rotator control.
TitleText
- The control title.
FrameAnimationDelay
- The duration, in milliseconds, to wait before animating the frames.
FrameAnimationStep
- The size, in pixels, by which the frames are moved.
FrameAnimationMode
- Defines the animation mode. For now, it can only be done on either X or Y axis (in the future, probably, will be some different ones).
HeaderBrushType
- Specifies how the background of the header of a frame should be painted, solid or gradient.
HeaderColorOne
- Sets the color used for the frame header background. If the brush is set to be gradient, this will be considered the starting color.
HeaderColorTwo
- Sets the ending color for the header background of the frame if the brush type has been set to gradient.
HeaderTextColor
- The color for the text displayed in a frame header.
HeaderFont
- The font used in drawing the frame text header.
HeaderSize
- The size in pixels of a frame header. The initial size is considered to be 40% of a frame.
InformationBrushType
- The brush type used for filling the main text area of a rotating frame.
InformationColorOne
- The color used for the rotating the frame main text area background. If the brush type is chosen to be gradient, this is considered the starting color.
InformationColorTwo
- Is the pair of the prior mentioned property in the case of a gradient brush type being set.
InformationTextColor
- The text color used in rotating the frame main text area.
InformationFont
- The font used to render the text in the rotating frame main text area.
TextAnimationDelay
- The interval, in milliseconds, used to animate the text in the rotating frame.
Architecture
The main classes defined in this assembly are presented in the following list:
BufferPaintingCtrl
- Inherits the Panel
control, adding support for double buffering.
CornerCtrl
- Defines the control supporting the border, drawing either with normal or rounded corners.
Frame
- Takes the CornerCtrl
a step further by exposing the two background filling types, solid and gradient.
RotatorCtrl
- The control you set on your form.
RotatorFrame
- The control responsible for displaying and animating the data.
RotatorFrameContainer
- The container used for the rotating frames. It has two RotatorFrame
instances which are being moved around.
RotatorFrameTemplate
- Defines the look of the frames. It contains references to the text color, fonts, background color, animation delay, etc.
RotatorItemData
- Contains the data to be displayed into the RotatorFrame
.
RotatorItemDataCollection
- A list of RotatorFrame
objects.
EventNotifier
- Class that raises a notifier event every x milliseconds.
ITextAnimation
- Defines the interface for animating the text.
TypingTextAnimation
- Defines the interface for animating the text displayed in the rotating frame control as it is being typed.
I will explain now a little bit of how this control works, leaving the code to do the rest. I will start with the animation, and explain how it is achieved. The idea behind it is to force repainting. The FrameRotator
creates an EventNotifier
object which is responsible for raising the repainting events.
this.repaintNotifier =
new EventNotifier(template.TextAnimationDelay,
new NotifierEvent(OnNotifierEvent));
Whenever the animation flag is enabled for the frame, the notifier is set to raise events. With every notification made, the main text area of the frame is invalidated, and the control is told to redraw its invalidated part of the client area.
private void OnNotifierEvent(object sender, EventArgs args)
{
this.repaintNotifier.Pause = true;
if (this.Handle.ToInt32() > 0)
{
this.Invoke(RepaintNotifyHandler);
}
}
private void Repaint()
{
this.Invalidate(new Region(infoPath));
this.Update();
}
The RotatorFrame
overrides the event handler for the WM_PAINT
message as it has to paint itself. There is no magic in it. If there is data linked to the frame, it will try to buffer the text size and resize the image; if there is one set for the header part, it will initialize the animation if it hasn't been initialized yet, and will call it to draw the main text area. Here is the code for the Paint
event:
protected override void OnPaint(System.Windows.Forms.PaintEventArgs e)
{
base.OnPaint(e);
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;
if (graphicPath == null)
{
InitializeGraphicPath();
}
e.Graphics.FillPath(frameTemplate.HeaderBrush, headerPath);
e.Graphics.FillPath(frameTemplate.InformationBrush, infoPath);
if (null != data)
{
RectangleF outputHeaderText = headerPath.GetBounds();
float square = Math.Min(outputHeaderText.Width / 2,
outputHeaderText.Height);
if (bufferAdjusment)
{
if (null != localImage)
{
localImage.Dispose();
}
if (data.Image != null)
{
localImage = (Image)data.Image.Clone();
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));
}
if (bufferAdjusment)
{
bufferedTextSize = e.Graphics.MeasureString(data.HeaderText,
frameTemplate.HeaderFont,
outputHeaderText.Size, stringFormat);
bufferAdjusment = false;
}
outputHeaderText =
new RectangleF(new PointF(outputHeaderText.X +
(outputHeaderText.Width - bufferedTextSize.Width) * 0.5f,
outputHeaderText.Y + (outputHeaderText.Height -
bufferedTextSize.Height) * 0.5f), bufferedTextSize);
e.Graphics.DrawString(data.HeaderText, frameTemplate.HeaderFont,
new SolidBrush(frameTemplate.HeaderTextColor),
outputHeaderText, stringFormat);
}
if (enableTextAnimation)
{
if (null == textAnimation)
{
InitializeTextAnimation();
}
textAnimation.Graphics = e.Graphics;
textAnimation.DrawText();
textAnimation.Graphics = null;
this.repaintNotifier.Pause = false;
}
else
{
this.repaintNotifier.Pause = true;
}
}
As I have said, the frame calls the animation in its paint handler to draw the text. But how is the typing text animation accomplished? The animation object is passed the text to be animated whenever new data is available for the frame, as well as the available rectangle to draw the text. An index is stored pointing to the current position within the given text; a call to the Draw
method will only render the first index characters of the screen, and then will increase the index to point to the next character. Once the index has reached the full size of the text to be animated, the AnimationFinished
event is raised. Of course, if the text to be displayed exceeds the available area, it will be trimmed. Here is the code that does that:
public override void DrawText()
{
if (text != null)
{
if (null == brush)
{
brush = new SolidBrush(textColor);
}
if (measureText)
{
measureText = false;
SizeF size = graphics.MeasureString(text, font,
area.Size, stringFormat);
float widthAdjustment = (area.Width - size.Width) / 2;
float heightAdjustment = (area.Height - size.Height) / 2;
area = new RectangleF(new PointF(area.X + widthAdjustment,
area.Y + heightAdjustment), size);
}
if (index >= text.Length)
{
graphics.DrawString(text, font, brush, area);
if (null != AnimationFinished && !eventSignaled)
{
AnimationFinished(this, new EventArgs());
eventSignaled = true;
}
}
else
{
graphics.DrawString(text.Substring(0, index),
font, brush, area);
index++ ;
if (index > text.Length)
{
index = text.Length;
}
SizeF sizeExceed =
graphics.MeasureString(text.Substring(0, index),
font, (int)area.Width, stringFormat);
if (sizeExceed.Height > area.Height)
{
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;
}
}
}
}
As mentioned earlier, the RotatorFrameContainer
has two frames, one to display the current information, and another buffers the next available data. Once their animation ends, the second one becomes the one displaying the data while the first one buffers the next available data. This control also uses an event notifier for moving the frames, as the RotatorFrame
does for the text animation.
private void HandleNotification()
{
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()
{
animating = false;
frameAnimationNotifier.Stop(true);
if (null == queuedChange)
{
SwapRotatorFrames();
SetFramesPosition();
firstFrame.Refresh();
}
else
{
HandleItemsCollectionChanged();
}
}
Because the data collection can be changed while the frames are being animated (and not only then), the RotatorItemDataCollection
defines an event to be raised whenever a change occurs. The frame container subscribes to this one:
private void OnItemsCollectionChanged(object sender,
CollectionChangeArgs args)
{
if (animating)
{
if (null == queuedChange)
{
queuedChange = args;
}
}
else
{
queuedChange = args;
HandleItemsCollectionChanged();
}
}
private void HandleItemsCollectionChanged()
{
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;
}
}
Conclusion
Maybe this control will be handy for some developers out there. If you encounter any problems or require any enhancements, I will be happy to assist you, so any feedback would be appreciated.
History
- September 2006 - Version 1.0.0
- September 2006 - Version 1.2.0
- Fix: Image resizing incorrectly
- Fix: Image shown with round corners
- Fix: Problems with the controls region