Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

A Rotator Control for WinForms

0.00/5 (No votes)
12 Sep 2006 1  
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