Click here to Skip to main content
15,885,209 members
Articles / Web Development / ASP.NET
Article

A Rotator Control for WinForms

Rate me:
Please Sign up or sign in to vote.
4.87/5 (71 votes)
12 Sep 20066 min read 128.8K   5.8K   174   29
A rotator control for Windows Forms.

Sample application

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.

Filling the items

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.

Class diagram (main classes available)

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.

C#
//create the notifier
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.

C#
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();
}

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:

C#
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;
    }
}

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:

C#
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;
            }
        }
    }
}

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.

C#
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();
    }
}

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:

C#
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;
    }
}

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
    • First release.
  • September 2006 - Version 1.2.0
    • Fix: Image resizing incorrectly
    • Fix: Image shown with round corners
    • Fix: Problems with the controls region

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here


Written By
Software Developer (Senior) Lab49
United Kingdom United Kingdom
This member has not yet provided a Biography. Assume it's interesting and varied, and probably something to do with programming.

Comments and Discussions

 
QuestionHow can turn control contents Right-to-Left for eastern cultures? Pin
yasser hosny21-Nov-16 0:14
yasser hosny21-Nov-16 0:14 
QuestionSuprb... good sample Pin
Bhavesh Patel17-Jul-15 0:21
Bhavesh Patel17-Jul-15 0:21 
QuestionPausing after each rotation? Pin
Member 450547324-Sep-13 3:34
Member 450547324-Sep-13 3:34 
AnswerRe: Pausing after each rotation? Pin
Mazin Scotts25-Sep-13 22:15
Mazin Scotts25-Sep-13 22:15 
GeneralGreat Stuff! Just a question... Pin
dawmail3339-Mar-08 2:00
dawmail3339-Mar-08 2:00 
GeneralMore than one message at the time Pin
Catalo12-Sep-07 1:34
Catalo12-Sep-07 1:34 
QuestionError in X Axis Pin
Alenzo_Eid1-May-07 23:59
Alenzo_Eid1-May-07 23:59 
GeneralGood sample Pin
WillemM10-Mar-07 0:53
WillemM10-Mar-07 0:53 
Generalgreat Pin
jocuppens17-Jan-07 23:06
jocuppens17-Jan-07 23:06 
Questionit works now but get 78 messages not errors Pin
jocuppens15-Jan-07 10:46
jocuppens15-Jan-07 10:46 
AnswerRe: it works now but get 78 messages not errors Pin
Stefan Bocutiu15-Jan-07 10:55
Stefan Bocutiu15-Jan-07 10:55 
GeneralVERY GOOD! thanks Pin
codeprojectfan25-Dec-06 19:33
codeprojectfan25-Dec-06 19:33 
GeneralErratic error in src sample Pin
UltraWhack27-Nov-06 12:58
UltraWhack27-Nov-06 12:58 
GeneralRe: Erratic error in src sample Pin
Stefan Bocutiu27-Nov-06 13:04
Stefan Bocutiu27-Nov-06 13:04 
QuestionRe: Erratic error in src sample Pin
UltraWhack27-Nov-06 13:09
UltraWhack27-Nov-06 13:09 
GeneralNice Pin
AnasHashki30-Sep-06 20:00
AnasHashki30-Sep-06 20:00 
GeneralA very well presented article! Pin
Liam O'Hagan11-Sep-06 11:44
Liam O'Hagan11-Sep-06 11:44 
GeneralNice work Pin
Yuval Naveh11-Sep-06 9:36
Yuval Naveh11-Sep-06 9:36 
GeneralRe: Nice work Pin
Stefan Bocutiu11-Sep-06 9:55
Stefan Bocutiu11-Sep-06 9:55 
GeneralRe: Nice work Pin
vik2011-Sep-06 20:35
vik2011-Sep-06 20:35 
GeneralFive - 5 - V Pin
oykica11-Sep-06 1:27
oykica11-Sep-06 1:27 
GeneralVery good! Pin
mav.northwind7-Sep-06 22:07
mav.northwind7-Sep-06 22:07 
GeneralRe: Very good! Pin
Stefan Bocutiu7-Sep-06 22:41
Stefan Bocutiu7-Sep-06 22:41 
GeneralGood Job Pin
NormDroid7-Sep-06 20:33
professionalNormDroid7-Sep-06 20:33 
GeneralThis Rocks!!!! Pin
Paul Conrad7-Sep-06 17:10
professionalPaul Conrad7-Sep-06 17:10 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.