65.9K
CodeProject 正在变化。 阅读更多。
Home

WinForms 的旋转器控件

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.87/5 (67投票s)

2006年9月7日

6分钟阅读

viewsIcon

132851

downloadIcon

5801

一个用于 Windows 窗体的旋转控件。

Sample application

引言

如今,RSS 源已相当普遍。我一直在寻找一个 Windows 控件,一个可用于显示 RSS 源数据的旋转控件,但没能找到。所以我的下一步是自己编写一个来完成这项工作。我的想法是创建一个控件,用它来旋转一些显示数据的框架。

使用代码

为了能够使用该控件,您应该在项目中添加对 Rotator.dll 文件的引用。完成后,您将能在工具箱窗口中看到该控件,并可以将其拖放到您的窗体上。这些框架沿 X 或 Y 轴移动,方向为从下到上/从右到左。若要实现反向动画(从上到下/从左到右),您需要更改表示动画期间框架移动量的值的符号。为了看到动画效果,您需要向 Items 集合中添加/插入一些数据。下图展示了用于向集合中添加数据的窗口。

Filling the 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 - 定义了将文本以打字效果动画显示在旋转框架控件中的接口。

Class diagram (main classes available)

现在我将简单解释一下这个控件的工作原理,其余的留给代码来说明。我将从动画开始,解释它是如何实现的。其背后的思想是强制重绘。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
    • 修复:图像大小调整不正确
    • 修复:图像以圆角显示
    • 修复:控件区域存在的问题
© . All rights reserved.