Click here to Skip to main content
Click here to Skip to main content
Go to top

A Rotator Control for WinForms

, 12 Sep 2006
Rate this:
Please Sign up or sign in to vote.
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.

//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.

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:

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:

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.

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:

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

Share

About the Author

Stefan Bocutiu
Software Developer (Senior) Lab49
United Kingdom United Kingdom
No Biography provided

Comments and Discussions

 
QuestionPausing after each rotation? PinmemberMember 450547324-Sep-13 3:34 
AnswerRe: Pausing after each rotation? PinmemberMazin Scotts25-Sep-13 22:15 
GeneralGreat Stuff! Just a question... Pinmemberdawmail3339-Mar-08 2:00 
GeneralMore than one message at the time PinmemberCatalo12-Sep-07 1:34 
QuestionError in X Axis PinmemberAlenzo_Eid1-May-07 23:59 
GeneralGood sample PinmemberWillemM10-Mar-07 0:53 
Generalgreat Pinmemberjocuppens17-Jan-07 23:06 
Questionit works now but get 78 messages not errors Pinmemberjocuppens15-Jan-07 10:46 
AnswerRe: it works now but get 78 messages not errors PinmemberStefan Bocutiu15-Jan-07 10:55 
GeneralVERY GOOD! thanks Pinmembercodeprojectfan25-Dec-06 19:33 
GeneralErratic error in src sample PinmemberUltraWhack27-Nov-06 12:58 
GeneralRe: Erratic error in src sample PinmemberStefan Bocutiu27-Nov-06 13:04 
QuestionRe: Erratic error in src sample PinmemberUltraWhack27-Nov-06 13:09 
GeneralNice PinmemberThe_Myth30-Sep-06 20:00 
GeneralA very well presented article! PinmemberLiam O'Hagan11-Sep-06 11:44 
GeneralNice work PinmemberYuval Naveh11-Sep-06 9:36 
GeneralRe: Nice work PinmemberStefan Bocutiu11-Sep-06 9:55 
GeneralRe: Nice work Pinmembervik2011-Sep-06 20:35 
GeneralFive - 5 - V Pinmemberoykica11-Sep-06 1:27 
GeneralVery good! Pinmembermav.northwind7-Sep-06 22:07 
GeneralRe: Very good! PinmemberStefan Bocutiu7-Sep-06 22:41 
GeneralGood Job Pinmembernorm .net7-Sep-06 20:33 
GeneralThis Rocks!!!! PinmemberPaulC19727-Sep-06 17:10 
GeneralWow!! PinmemberOkta Endy7-Sep-06 16:33 
GeneralSweet! PinmemberRavi Bhavnani7-Sep-06 12:41 
GeneralAWESOME !!!! PinmemberMarcos Meli7-Sep-06 12:06 
Generalvery nice! PinmemberMrBabba7-Sep-06 11:55 

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

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

| Advertise | Privacy | Mobile
Web03 | 2.8.140921.1 | Last Updated 12 Sep 2006
Article Copyright 2006 by Stefan Bocutiu
Everything else Copyright © CodeProject, 1999-2014
Terms of Service
Layout: fixed | fluid