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

UserControl (SliderControl) 剖析

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.87/5 (41投票s)

2013年5月8日

CPOL

15分钟阅读

viewsIcon

60262

downloadIcon

3431

本文将分步介绍如何实现一个名为 SliderControl 的 UserControl。

简介 [^]

Slider Control

本文分步介绍了如何实现一个名为 SliderControl 的 UserControl

在以下讨论中,由开发人员指定的属性将以粗体混合大小写文本显示。软件内部使用的变量将以斜体小写文本显示。

用户视角

该控件允许用户拖动箭头(管的右侧),并将其向上或向下移动。垂直移动仅限于管的顶部和底部。箭头的填充颜色会随箭头所指管中的颜色而变化。当用户移动箭头时,箭头右侧的文本会随之改变,以指示当前值。

开发者视角

对于开发人员来说,滑块控件可以集成到 Visual Studio 工具箱中,并放置在 Windows 窗体表面。控件最重要的属性是this.Height,在内部称为control_height。所有其他维度都由此维度派生。

每当用户更改当前值时,都会引发SliderValueChanged 事件。请注意,这与检测箭头的移动不同。current_value是一个整数。因此,箭头位置的微小变化可能会导致一个小数。该小数(使用四舍五入)四舍五入到最接近的整数,并与之前的current_value进行比较。只有当它们不同时,才会引发 SliderValueChanged 事件。新的current_value将在 SliderValueEventArgs 中返回。

开发控件

在开发控件的过程中,遵循某种形式的需求至关重要。对于 SliderControl,这是通过 Microsoft PowerPoint 演示文稿实现的。该演示文稿中的幻灯片作为 PNG 图像并入本文。完整的演示文稿作为可下载文件提供。

如果读者未安装 Microsoft PowerPoint,可免费下载 PowerPoint Viewer

目录

符号 [^] 将读者返回到目录顶部。

视觉属性 [^]

Slider Control Properties

滑块控件具有许多构成用户视觉图像的属性。在右侧的图中,控件的边界以红色绘制。

开发人员通过将控件从工具箱拖到窗体上的位置来指定控件的左上角。该位置在控件的图形环境中变为 ( 0, 0 )。开发人员通过拖动控件的调整大小手柄来指定control_heightcontrol_width计算为

control_width = ( int ) ( ( float ) control_height / 4.0F + 0.5F );

请注意,control_widthcontrol_height决定。改变两者的唯一方法是使用调整大小手柄。

控件边界和每个控件组件之间有一个边框。该边框根据control_height计算为

offset = ( int ) ( ( float ) control_height / 100.0F + 0.5F );

一旦计算出offsettube_height就可以计算为

tube_height = control_height - 2 * offset;

管的宽度可以指定或计算。

  1. 指定。

    为了在窗体上实现多个滑块控件的共同视觉呈现,开发人员可能希望跳过tube_width的计算(例如在滑块控件演示中)。这可以通过将ForceTubeWidth设置为true并提供TubeWidth的值来实现。如果ForceTubeWidth未设置为true,则指定TubeWidth的值将被忽略。

  2. 计算。

    如果ForceTubeWidth未设置为true,则计算tube_width的值。为了计算tube_width,使用常数 HEIGHT_TO_WIDTH

    HEIGHT_TO_WIDTH = 0.030612244897959;

    并且tube_width计算为

    tube_width = ( int ) ( ( double ) tube_height * HEIGHT_TO_WIDTH + 0.5 );

管子使用线性渐变画笔着色,并使用由MaximumColorMidpointColorMinimumColor指定的颜色混合

在彩色管的左侧,是一组垂直的标签,范围从MaximumValueMinimumValue。标签值由Increment分隔。标签与控件的左边界和彩色管之间由offset分隔。MaximumValue必须大于MinimumValue,并且MinimumValue必须大于或等于零。

实现 [^]

Slider Control Components

滑块控件是用户绘制控件。控件的某些部分在调用控件的OnPaint方法时绘制。

控件的图形,其轮廓以红色绘制,由两个不同的图形图像组成:背景图形,以蓝色勾勒并填充蓝色图案;以及指示器图形,以绿色勾勒并填充绿色图案。

绘制后,除非ControlBackgroundColorForceTubeWidthTubeWidthMaximumColorMidpointColorMinimumColorMaximumValueMinimumValueIncrement发生变化或控件大小调整,否则背景图形无需重新绘制。当背景图形重新绘制或用户更改当前值箭头的位置时,指示器图形必须重新绘制。

背景图形 [^]

Slider Control Background Graphic

背景图形由彩色管和管左侧的一组标签组成。管本身由两个大小相等、由一个矩形连接的端点圆组成。

请注意,端点圆的半径是tube_width/2。绘制后,端点圆会分别填充MaximumColorMinimumColor

矩形使用线性渐变画笔填充,该画笔通过MidpointColorMaximumColorMinimumColor定义颜色混合

标签源自MaximumValueMinimumValueIncrement的值。生成标签后,使用MeasureText计算所有标签的最大尺寸label_size

有五个点P0P1P2P3P4是固定的,基于属性值。这些点完全定义了背景图形的内容。回想一下之前,所有图形对象的原点是控件的左上角

P0   管的左上角以及MaximumValue端点圆的边界矩形的左上角。
P1   管子的左下角。
P2   彩色矩形的上部中心。
P3   彩色矩形的下部中心。
P4   MinimumValue端点圆的边界矩形的左上角。

这些点中最重要的是P0,因为所有其他点都源自它。用于计算这些值的序列是

  1. 更新管尺寸。
  2. 更新背景标签。
  3. 更新背景几何图形。

尽管名称略有不当,update_tube_dimensions 是

        // ************************************ update_tube_dimensions

        void update_tube_dimensions ( )
            {

            offset = round ( ( float ) control_height / 100.0F );

            tube_height = control_height - 2 * offset;
            if ( !force_tube_width )
                {
                tube_width = round ( ( double ) tube_height * 
                                     HEIGHT_TO_WIDTH );
                }
            arrow_width = round ( 1.5 * ( double ) tube_width );
            }
  

辅助函数round返回其参数的整数值,使用四舍五入到最接近的整数。round被重载以接受floatdouble类型的参数。

当定义背景的属性之一发生变化时,必须更新背景标签。

        // ********************************** update_background_labels

        void update_background_labels ( )
            {
            int     available_height = 0;
            float   font_size = FONT_SIZE;
            Font    new_font = new Font ( FONT_FAMILY, FONT_SIZE );
            int     needed_height = 0;

            new_font = new Font ( FONT_FAMILY, font_size );
            labels = create_background_labels ( MaximumValue,
                                                MinimumValue,
                                                Increment);
            label_size = determine_maximum_label_size ( labels, 
                                                        new_font );
                                        // force vertical fit
            update_tube_dimensions ( );
            available_height = control_height - 
                               ( 2 * offset ) - tube_width;
            needed_height = labels.Length * label_size.Height;
            while ( needed_height > available_height )
                {
                font_size -= 0.1F;
                new_font = new Font ( FONT_FAMILY, font_size ); 
                label_size = determine_maximum_label_size ( 
                                                        labels, 
                                                        new_font );
                needed_height = labels.Length * label_size.Height;
                }
            label_font = new_font;
            }
  

两个辅助函数create_background_labelsdetermine_maximum_label_size执行其名称所示的功能。一旦标签创建并初步调整大小,update_background_labels会继续测试以确保标签在available_height内垂直适配。如果标签不适配,则会减小字体大小直到标签适配,

最后,计算点P0P1P2P3P4

        // ******************************** update_background_geometry

        void update_background_geometry ( )
            {

            update_tube_dimensions ( );

            P0.X = offset + label_size.Width + offset;
            P0.Y = offset;

            P1.X = P0.X;
            P1.Y = control_height - offset;

            P2.X = P0.X + round ( ( double ) tube_width / 2.0 );
            P2.Y = P0.Y + round ( ( double ) tube_width / 2.0 );

            P3.X = P2.X;
            P3.Y = P1.Y - round ( ( double ) tube_width / 2.0 );

            P4.X = P0.X;
            P4.Y = P1.Y - tube_width;
            }
  

此时,背景图形的几何形状已定义。绘制背景的顺序是

  1. ControlBackgroundColor填充控件。
  2. 绘制背景标签。
  3. 绘制端点圆(先MaximumValue,然后MinimumValue)。
  4. 绘制彩色管。

这个序列可以在draw_background_graphic中找到

        // *********************************** draw_background_graphic

        void draw_background_graphic ( Graphics  graphics )
            {
            Brush               brush;
            Rectangle           end_point_rectangle = 
                                    new Rectangle ( );
            LinearGradientBrush linear_gradient_brush;
            Rectangle           rectangle;

                                    // background labels
            draw_background_labels ( graphics );
                                    // endpoints
            end_point_rectangle.Size = new Size ( tube_width, 
                                                  tube_width );
                                    // maximum endpoint
            brush = new SolidBrush ( MaximumColor );
            end_point_rectangle.Location = P0;
            graphics.FillEllipse ( brush, end_point_rectangle );
            brush.Dispose ( );
                                    // minimum endpoint
            brush = new SolidBrush ( MinimumColor );
            end_point_rectangle.Location = P4;
            graphics.FillEllipse ( brush, end_point_rectangle );
            brush.Dispose ( );
                                    // gradient tube
            rectangle = new Rectangle ( new Point ( P0.X, P2.Y ),
                                        new Size ( tube_width, 
                                                   P3.Y - P2.Y ) );
                                    // inflate to account for the 
                                    // right and bottom off by one 
                                    // value in Rectangles
            rectangle.Inflate ( 1, 1 );
            linear_gradient_brush = create_linear_gradient_brush (
                                            MaximumColor,
                                            MidpointColor,
                                            MinimumColor );
            graphics.FillRectangle ( linear_gradient_brush, 
                                     rectangle );
            linear_gradient_brush.Dispose ( );
            }
  

辅助函数draw_background_labels

        // ************************************ draw_background_labels

        void draw_background_labels ( Graphics graphics )
            {
            Point           location = new Point ( 0, 0 );
            TextFormatFlags text_format_flags;
            int             vertical_offset = 0;

            text_format_flags = ( TextFormatFlags.Right |
                                  TextFormatFlags.VerticalCenter );

            vertical_offset = ( P3.Y - P2.Y ) / ( labels.Length - 1 );

            location.X = P0.X - ( offset + label_size.Width );
            location.Y = P2.Y - ( label_size.Height / 2 );

            foreach ( string label in labels )
                {
                Rectangle   rectangle = new Rectangle ( location,
                                                        label_size );
                TextRenderer.DrawText ( graphics, 
                                        label, 
                                        label_font, 
                                        rectangle, 
                                        Color.Black, 
                                        text_format_flags );
                location.Y += vertical_offset;
                }
            }
  

辅助函数create_linear_gradient_brush

        // ****************************** create_linear_gradient_brush

        LinearGradientBrush create_linear_gradient_brush (
                                            Color  maximum_color,
                                            Color  midpoint_color,
                                            Color  minimum_color )
            {
            LinearGradientBrush  brush;
            ColorBlend           color_blend = new ColorBlend ( ); 
            Rectangle            rectangle;

            rectangle = new Rectangle ( new Point ( P0.X, P2.Y ),
                                        new Size ( tube_width, 
                                                   tube_height ) );
            rectangle.Inflate ( 1, 1 );
            brush = new LinearGradientBrush ( 
                                    this.ClientRectangle,
                                    maximum_color,
                                    minimum_color,
                                    LinearGradientMode.Vertical );

            color_blend.Positions = new float [ ]
                                        {
                                        0.0F,
                                        0.5F,
                                        1.0F
                                        };

            color_blend.Colors = new Color [ ]
                                    {
                                    maximum_color,
                                    midpoint_color,
                                    minimum_color 
                                    };

            brush.InterpolationColors = color_blend;

            return ( brush );
            }
  

此时,背景图形已绘制完毕,并可传输到OnPaint PaintEventArgs中提供的Graphics对象。除非背景图形属性发生变化,否则我们不需要执行背景图形计算。

指示器图形 [^]

Slider Control Indicator Graphic

指示器图形由当前值箭头和箭头右侧的包含current_value的标签组成。P2在背景图形中的值固定了指示器图形的左侧。

用户通过将光标放置在箭头的边界内、按下并按住鼠标按钮之一、向上或向下拖动光标(以及箭头),最后释放鼠标按钮来与箭头交互。如果光标位于箭头内,控件会通过响应OnMouseDownOnMouseMoveOnMouseUp事件来响应这些用户操作。

Slider Control Arrow Geometry

指示器图形由draw_indicator_graphic绘制

        // ************************************ draw_indicator_graphic

        void draw_indicator_graphic ( Graphics  graphics )
            {

            update_indicator_geometry ( );

            draw_current_value_string ( graphics );
            draw_arrow ( graphics );
            }
  

在绘制指示器图形之前,必须更新指示器图形的几何形状。

        // ********************************* update_indicator_geometry

        void update_indicator_geometry ( )
            {
            int     dy = 0;
            float   percent = 0.0F;
            float   percent_down = 0.0F;
            int     pixels = 0;
            int     pixels_down = 0;
            double  theta = INTERIOR_ANGLE * Math.PI / 180.0;

            update_tube_dimensions ( );
                                       // solve initialization problem
            if ( CurrentValue < MinimumValue )
                {
                CurrentValue = MaximumValue - Increment;
                }

            P5.X = P2.X + round ( ( double ) tube_width / 2.0 ) +
                          offset;
            pixels = round ( ( float ) ( P3.Y - P2.Y ) );
            percent = Math.Abs ( ( float ) ( current_value -
                                             MinimumValue ) / 
                                 ( float ) ( MaximumValue - 
                                             MinimumValue ) );
            percent_down = 1.0F - percent;
            pixels_down = round ( ( percent_down * 
                                  ( float ) pixels ) );
            P5.Y = P2.Y + pixels_down;

            dy = round ( ( double ) arrow_width * 
                         Math.Sin ( theta ) );

            P6.X = P5.X + arrow_width;
            P6.Y = P5.Y - dy;

            P7.X = P6.X;
            P7.Y = P5.Y + dy;
            }
  

常数 INTERIOR_ANGLE(20 度)的值是通过实验获得的。我做的第一件事就是画箭头。随着时间的推移,我发现 20 度是平衡的、令人愉悦的,并且尺寸足够大,允许用户捕捉箭头(用于拖动)。

修改指示器几何图形后,绘制箭头右侧的标签。

        // ********************************* draw_current_value_string

        void draw_current_value_string ( Graphics graphics )
            {
            Brush           brush;
            string          current_value_string;
            TextFormatFlags flags;
            Point           location;
            Rectangle       rectangle;

            brush = new SolidBrush ( Color.Black );
            current_value_string = current_value.ToString ( );
            flags = ( TextFormatFlags.Left |
                      TextFormatFlags.VerticalCenter );
            location = new Point ( P6.X + offset, 
                                   P5.Y - ( label_size.Height / 2 ) );

            rectangle = new Rectangle ( location, label_size );
            TextRenderer.DrawText ( graphics, 
                                    current_value_string, 
                                    label_font, 
                                    rectangle, 
                                    Color.Black, 
                                    flags );
            }
  

TextRendererTextFormatFlags用于在边界矩形左侧和垂直居中绘制标签。

最后我们可以画箭头了。

        // ************************************************ draw_arrow

        void draw_arrow ( Graphics graphics )
            {
            Point [ ]       arrow_outline = new Point [ 3 ];
            GraphicsPath    arrow_path = null;
            Brush           brush;
            int             i = 0;
            Pen             pen;
            Color           value_color;

            update_geometry ( );

            value_color = background.ColorAtLocation (
                              new Point ( P5.X - ( tube_width / 2 ),
                                          P5.Y ) );
            brush = new SolidBrush ( value_color );
            pen = new Pen ( Color.Black, 1.0F );

            arrow_outline [ i++ ] = P5;
            arrow_outline [ i++ ] = P6;
            arrow_outline [ i++ ] = P7;

            arrow_path = new GraphicsPath ( FillMode.Alternate );
            arrow_path.AddLines ( arrow_outline );
            arrow_path.CloseFigure ( );
                                        // arrow_region is used for 
                                        // hit testing
            arrow_region = new Region ( arrow_path );
                                        // draw arrow outline
            graphics.DrawPolygon ( pen, arrow_outline );
                                        // fill arrow outline
            graphics.FillPolygon ( brush, arrow_outline );

            arrow_path.Dispose ( );
            brush.Dispose ( );
            pen.Dispose ( );
            }
  

箭头的轮廓由P5P6P7定义。数组arrow_outline存储轮廓。从该数组定义一个GraphicsPath,并从该路径定义一个Region。该区域的IsVisible方法将用于命中测试。箭头的轮廓使用DrawPolygon绘制。

箭头使用FillPolygon填充。

此时,指示器图形已绘制完毕,并可传输到OnPaint PaintEventArgs中提供的 Graphics 对象。当背景图形重新绘制或用户移动箭头时,指示器图形会重新绘制。

事件处理 [^]

大多数编程语言中一个比较隐晦的特性是感兴趣的操作的信号传递方式。许多语言都有这种机制。但由于 SliderControl 是用 C# 实现的,因此本讨论将仅限于该语言。

SliderValueChanged [^]

除非 SliderControl 能够告知(发出信号)其父级用户更改了current_value(通过向上或向下拖动箭头),否则它将毫无用处。为了发出此事件的信号,SliderControl 包含SliderValueChanged事件的声明。

        // ******************************** control delegate and event

        public delegate void SliderValueChangedHandler ( 
                                Object                      sender,
                                SliderValueChangedEventArgs e );

        public event SliderValueChangedHandler SliderValueChanged;
  

委托SliderValueChangedHandler定义了将由SliderValueChanged事件调用的方法的签名。该事件有两个参数:发送者和自定义 EventArgs,SliderValueChangedEventArgs

    // ***************************** class SliderValueChangedEventArgs

    public class SliderValueChangedEventArgs : EventArgs
        {
        public int  SliderValue;

        // ******************************* SliderValueChangedEventArgs

        public SliderValueChangedEventArgs ( int slider_value )
            {

            SliderValue = slider_value;
            }

        } // class SliderValueChangedEventArgs
  

SliderValueChangedEventArgs返回当前的SliderValue。每当 SliderControl 检测到current_value的值发生变化时,它就会调用trigger_slider_value_changed_event

        // ************************ trigger_slider_value_changed_event

        void trigger_slider_value_changed_event ( int  current_value )
            {

            if ( SliderValueChanged != null )
                {
                SliderValueChanged ( this,
                                     new SliderValueChangedEventArgs (
                                             current_value ) );
                }
            }
  

SliderValueChanged事件可以有零个或多个订阅者。测试

            if ( SliderValueChanged != null )
  

是为了确保至少有一个订阅者订阅了SliderValueChanged事件。未能进行此测试可能会导致异常,这是用户控件中应避免的情况。

如果一个类希望收到 SliderControl 的current_value值变化的通知,它必须注册一个事件处理程序。在TestSliderControl 演示项目中,有三个 SliderControl 实例。每个实例都连接到事件处理程序。这通过首先声明方法slider_SC_SliderValueChanged将用于捕获事件来实现

            heater_AC_SC.SliderValueChanged +=
                new SliderControl.SliderControl.
                        SliderValueChangedHandler (
                            SliderValueChanged );

            spa_SC.SliderValueChanged +=
                new SliderControl.SliderControl.
                        SliderValueChangedHandler (
                            SliderValueChanged );

            cruise_control_SC.SliderValueChanged +=
                new SliderControl.SliderControl.
                        SliderValueChangedHandler (
                            SliderValueChanged );
  

slider_SC_SliderValueChanged方法声明如下

        // ****************************** slider_SC_SliderValueChanged

        void SliderValueChanged ( 
                    object                                    sender, 
                    SliderControl.SliderValueChangedEventArgs e )
            {
            string                      new_value;
            SliderControl.SliderControl slider_control;

            slider_control = ( SliderControl.SliderControl ) sender;
            new_value = e.SliderValue.ToString ( );

            switch ( slider_control.Tag.ToString ( ).ToLower ( ) )
                {
                case "heater_ac":
                    heater_ac_value_TB.Text = new_value;
                    break;

                case "spa":
                    spa_value_TB.Text = new_value;
                    break;

                case "cruise_control":
                    cruise_control_value_TB.Text = new_value;
                    break;

                default:

                    break;
                }
            }
  

在设计时,SliderControl 的每个实例都被赋予一个唯一的Tag值。slider_SC_SliderValueChanged方法使用此值来区分 SliderControl 的多个实例。在此示例中,SliderValue的新值放置在 TextBox 中。可以使用SliderValue中返回的值执行任意数量的其他操作。

滑动箭头 [^]

用户通过如前述方式上下移动箭头来与 SliderControl 交互。用户的交互通过OnMouseDownOnMouseMoveOnMouseUp事件的事件处理程序检测到。

OnMouseDown 事件处理程序 [^]
        // *********************************************** OnMouseDown

        protected override void OnMouseDown ( MouseEventArgs e )
            {

            base.OnMouseDown(e);

            if ( arrow_region.IsVisible ( new Point ( e.X, e.Y ) ) )
                {
                                    // cursor is in the arrow
                arrow_being_dragged = true;
                if ( current_value_changed ( e.Y ) )
                    {
                    trigger_slider_value_changed_event ( 
                                                    current_value );
                    this.Invalidate ( );
                    }
                }
            }
  

SliderControl 的OnMouseDown事件处理程序是经典的。

  1. 调用基类的OnMouseDown方法,以便所有注册的委托都接收到该事件。
  2. 测试以确保光标在draw_arrow方法中定义的arrow_region中。
  3. 如果光标在arrow_region中,则将变量arrow_being_dragged设置为true,并测试以确保与箭头关联的值与current_value不同。判断新值是否与current_value不同的测试在current_value_changed方法中执行。
  4. 如果current_value_changed返回true,则调用trigger_slider_value_changed_event,控件将重新绘制自身。

current_value_changed实现为

        // ************************************* current_value_changed

        bool current_value_changed ( int y )
            {
            int     old_current_value = current_value;
            float   percent;
            float   value_down;
            int     pixels;
            int     pixels_down;
            bool    value_changed = false;

            if ( P5.Y != y )
                {
                if ( y > P3.Y ) 
                    {
                    y = P3.Y;
                    }
                if ( y < P2.Y )
                    {
                    y = P2.Y;
                    }
                pixels = P3.Y - P2.Y;
                pixels_down = y - P2.Y; 
                percent = ( float ) pixels_down / ( float ) pixels;
                value_down = percent * ( float ) ( MaximumValue - 
                                                   MinimumValue );
                current_value = round ( ( float ) MaximumValue - 
                                        value_down );
                value_changed = ( old_current_value != 
                                  current_value );
                }

            return ( value_changed );
            }
  

请注意,current_value在此方法中进行了修改。

OnMouseMove 事件处理程序 [^]
        // *********************************************** OnMouseMove

        protected override void OnMouseMove ( MouseEventArgs e )
            {

            base.OnMouseMove ( e );

            if ( arrow_being_dragged )
                {
                if ( current_value_changed ( e.Y ) )
                    {
                    trigger_slider_value_changed_event ( 
                                                    current_value );
                    this.Invalidate ( );
                    }
                }
            }
  

SliderControl 的OnMouseMove事件处理程序很简单。如果arrow_being_draggedtrue,则表示用户仍按住鼠标按钮并正在上下拖动箭头。在这种情况下,会调用current_value_changed,如果它返回true,则会调用trigger_slider_value_changed_event,并且控件会重新绘制自身。

OnMouseUp 事件处理程序 [^]
        // ************************************************* OnMouseUp

        protected override void OnMouseUp ( MouseEventArgs e )
            {

            base.OnMouseUp ( e );

            arrow_being_dragged = false;
            }
  

当用户释放鼠标按钮时,会调用OnMouseUp事件处理程序。反过来,此处理程序只是将arrow_being_dragged设置为false。这会导致在用户再次按下鼠标按钮之前,所有未来的鼠标移动都将被忽略。请注意,SliderControl 不需要重新绘制。

OnPaint 事件处理程序 [^]

        // *************************************************** OnPaint

        protected override void OnPaint ( PaintEventArgs e )
            {

            base.OnPaint ( e );

            e.Graphics.Clear ( ControlBackgroundColor );

            if ( ( this.Height != control_height ) ||
                 ( this.Width != control_width ) )
                {
                int  width;

                revise_background_graphic = true;
                control_height = this.Height;
                control_width = round ( ( float ) control_height / 
                                        4.0F ); 
                update_geometry ( );
                width = offset + label_size.Width + offset + 
                        tube_width + offset + arrow_width + offset +
                        label_size.Width + offset;
                if ( width > control_width )
                    {
                    control_width = width;
                    }
                this.Size = new Size ( control_width, 
                                       control_height );
                }

            if ( ( background == null ) || revise_background_graphic )
                {
                if ( revise_background_graphic )
                    {
                    revise_background_graphic = false;
                    }
                create_background_graphic ( );
                draw_background_graphic ( background.Graphic );
                }
            background.RenderGraphicsBuffer ( e.Graphics );

            create_indicator_graphic ( );
            draw_indicator_graphic ( indicator.Graphic );

            indicator.RenderGraphicsBuffer ( e.Graphics );
            }
  

每当系统识别出某个控件需要重新绘制时,它就会通过引发 OnPaint 事件来通知控件。OnPaint 事件可能因多种原因而引发,其中一些原因包括

  • 光标在控件表面移动。
  • 控件大小调整。
  • 控件的隐藏区域变得可见。
  • 控件自身请求。

例如,OnMouseDownOnMouseMove事件处理程序都通过调用Invalidate来触发控件的重绘。此外,当ControlBackgroundColorCurrentValueForceTubeWidthIncrementMaximumColorMaximumValueMidpointColorMinimumColorMinimumValueTubeWidth中的任何一个发生更改时,也会调用 Invalidate。

被调用时,SliderControl 的 OnPaint 事件处理程序会执行以下操作

  1. 调用基类的OnPaint方法,以便所有注册的委托都接收到该事件。
  2. 彻底清除控件上现有的图形。
  3. 确定控件的高度或宽度是否已更改。如果是,则首先将revise_background_graphic设置为true,从而强制重新绘制背景图形,然后计算并设置控件的新大小。
  4. 如果背景为nullrevise_background_graphictrue,则重新创建并重绘背景。请注意,如果这些条件都不是true,则背景保持不变。
  5. 将背景绘制到屏幕上。
  6. 创建并重新绘制指示器图形。
  7. 将指示器绘制到屏幕上。

图形缓冲区 [^]

GraphicsBuffer 类包含一个屏幕外位图,用于无闪烁地绘制图形对象。尽管 .Net 提供了双缓冲图形功能,但对于 SliderControl 而言,这有些大材小用。

通常我将 GraphicsBuffer 包含在我正在实现的控件中。但是,由于该类在 SliderControl 软件之外也很有用,因此我将其作为单独的源文件 (GraphicsBuffer.cs) 包含在下载中。

成员 [^]

GraphicsBuffer 的成员是

        Bitmap      bitmap;
        Graphics    graphics;
        int         height;
        int         width;
  

bitmap是用于保存最终显示在屏幕上的图像的屏幕外对象。绘制通过graphics执行,其在 GraphicsBuffer 内部定义为

        graphics = Graphics.FromImage ( bitmap );
  

以及heightwidth包含bitmap维度。

构造函数 [^]

        // ******************************************** GraphicsBuffer

        public GraphicsBuffer ( )
            {

            width = 0;
            height = 0;
            }
  

GraphicsBuffer 构造函数创建一个新的空 GraphicsBuffer 对象。例如

        background = new GraphicsBuffer ( );
  

        indicator = new GraphicsBuffer ( );
  

方法 [^]

一旦构造了 GraphicsBuffer,就可以使用方法和属性来完善和使用它。

创建图形缓冲区 [^]
        // ************************************** CreateGraphicsBuffer

        public bool CreateGraphicsBuffer ( int width,
                                           int height )
            {
            bool  success = true;

            if ( graphics != null )
                {
                graphics.Dispose ( );
                graphics = null;
                }

            if ( bitmap != null )
                {
                bitmap.Dispose ( );
                bitmap = null;
                }

            this.width = 0;
            this.height = 0;

            if ( ( width == 0 ) || ( height == 0 ) )
                {
                success = false;
                }
            else
                {
                this.width = width;
                this.height = height;

                bitmap = new Bitmap ( this.width, this.height );
                graphics = Graphics.FromImage ( bitmap );

                success = true;
                }

            return ( success );
            }
  

CreateGraphicsBuffer是GraphicsBuffer构造函数之后要调用的第一个方法。此方法通过删除之前调用CreateGraphicsBuffer的任何残余物来完成创建过程;从指定的heightwidth创建内存中的位图;并将图形位图关联起来。

删除图形缓冲区 [^]
        // ************************************** DeleteGraphicsBuffer

        public GraphicsBuffer DeleteGraphicsBuffer ( )
            {

            if ( graphics != null )
                {
                graphics.Dispose ( );
                graphics = null;
                }

            if ( bitmap != null )
                {
                bitmap.Dispose ( );
                bitmap = null;
                }

            width = 0;
            height = 0;

            return ( null );
            }
  

在 SliderControl 中,每当要重新绘制背景图形时,都会执行以下代码

        // ********************************* create_background_graphic

        void create_background_graphic ( )
            {

            if ( background != null )
                {
                background = background.DeleteGraphicsBuffer ( );
                }
            background = new GraphicsBuffer ( );
            background.CreateGraphicsBuffer ( control_width,
                                              control_height );
            background.Graphic.SmoothingMode = SmoothingMode.
                                               HighQuality;
            background.ClearGraphics ( ControlBackgroundColor );
            }
  

类似的重绘指示器图形的代码也会执行。

        // ********************************** create_indicator_graphic

        void create_indicator_graphic ( )
            {

            if ( indicator != null )
                {
                indicator = indicator.DeleteGraphicsBuffer ( );
                }
            indicator = new GraphicsBuffer ( );
            indicator.CreateGraphicsBuffer ( control_width,
                                             control_height );
            indicator.Graphic.SmoothingMode = SmoothingMode.
                                              HighQuality;
            }
  

当调用 SliderControl memory_cleanup事件处理程序时,也会执行DeleteGraphicsBuffer

        // ******************************************** memory_cleanup

        void memory_cleanup ( object    sender,
                              EventArgs e )
            {

            if ( arrow_region != null )
                {
                arrow_region.Dispose ( );
                arrow_region = null;
                }
                                        // DeleteGraphicsBuffer 
                                        // returns null
            if ( background != null )
                {
                background = background.DeleteGraphicsBuffer ( );
                }

            if ( indicator != null )
                {
                indicator = indicator.DeleteGraphicsBuffer ( );
                }
            }
  

memory_cleanup连接到 ApplicationExit 事件,并在 SliderControl 构建时执行。

            Application.ApplicationExit += 
                            new EventHandler ( memory_cleanup );
            memory_cleanup ( this, EventArgs.Empty );
  
渲染图形缓冲区 [^]
        // ************************************** RenderGraphicsBuffer

        public void RenderGraphicsBuffer ( Graphics graphic )
            {

            if ( bitmap != null )
                {
                graphic.DrawImage (
                            bitmap,
                            new Rectangle ( 0, 0, width, height ),
                            new Rectangle ( 0, 0, width, height ),
                            GraphicsUnit.Pixel );
                }
            }
  

RenderGraphicsBuffer将 GraphicsBuffer 位图绘制到其参数中指定的图形。它使用 .Net Graphics 类的DrawImage方法,并在 SliderControl OnPaint 事件处理程序中为背景和指示器图形调用。

            background.RenderGraphicsBuffer ( e.Graphics );
            :
            :
            indicator.RenderGraphicsBuffer ( e.Graphics );
  

此处e是 OnPaint 事件处理程序的PaintEventArgs参数。当调用RenderGraphicsBuffer时,屏幕图像会更新。

清除图形 [^]
        // ********************************************* ClearGraphics

        public void ClearGraphics ( Color background_color )
            {

            Graphic.Clear ( background_color );
            }
  

ClearGraphics清除整个绘图表面并用指定的background_color填充。

ColorAtLocation [^]
        // ******************************************* ColorAtLocation

        public Color ColorAtLocation ( Point location )
            {
            Color  color = Color.Black;

            if ( ( location.X <= this.width ) && 
                 ( location.Y <= this.height ) )
                {
                color = this.bitmap.GetPixel ( location.X,
                                               location.Y );
                }

            return ( color );
            }
  

ColorAtLocation返回图形位图中给定location处的 GDI Color。如果location在图形位图之外,则返回黑色。

GraphicsBufferExists [^]
        // ************************************** GraphicsBufferExists

        public bool GraphicsBufferExists
            {

            get
                {
                return ( graphics != null );
                }
            }
  

如果图形对象存在,GraphicsBufferExists返回true;否则返回false

属性 [^]

GraphicsBuffer 只有一个属性。

图形 [^]
        // *************************************************** Graphic

        public Graphics Graphic
            {

            get
                {
                return ( graphics );
                }
            }
  

Graphic返回当前的 Graphics 对象。此对象用于在位图上绘制图形。

滑块控件演示 [^]

Test Slider Control

回想前面,管子的宽度可以指定或计算。

在右侧的图中,三个管子需要其中两个指定管子的宽度。在 spa 和巡航控制 SliderControls 的属性页上,ForceTubeWidth设置为trueTubeWidth设置为 16。该值 (16) 来自加热器/空调 SliderControl 的TubeWidth。这些设置的效果是确保所有三个管子在窗体上显示时具有相同的宽度。

结论 [^]

本文介绍了滑块控件的逐步实现指南。

参考文献 [^]

开发环境 [^]

SliderControl 在以下环境中开发

      Microsoft Windows 7 Professional Service Pack 1
      Microsoft Visual Studio 2008 Professional
      Microsoft .Net Framework Version 3.5 SP1
      Microsoft Visual C# 2008

历史 [^]

05/08/2013   原始文章
05/14/2013   继承自 UserControl 更改为 Control,版本为 1.2
© . All rights reserved.