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

悬停点击控件剖析

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.97/5 (19投票s)

2013年11月25日

CPOL

22分钟阅读

viewsIcon

30956

downloadIcon

1487

本文介绍了一个用于实现用户自定义的悬停-单击控件的模板。

引言 TOC

Settings Control

本文对一个用于实现用户自定义的悬停-单击 控件 [^] 的模板进行了详尽的介绍。悬停-单击控件在许多桌面应用程序中都可以找到。例如,在Internet Explorer的右上方会出现三个图像:一个房子(主页)、一个星星(收藏夹)和一个齿轮(工具)。当鼠标悬停在其中一个图像上时,图像的颜色会发生变化。如果鼠标在悬停时单击,则可能会出现一个子菜单或打开一个新页面。

本文介绍的模板的独特性在于:

  • 悬停-单击控件的图形图像在Windows OnPaint [^] 事件之间保持不变,因此无需在这些事件之间重新创建。
  • 该模板使用 变换矩阵 [^] 来平移、旋转和缩放悬停-单击控件的图形图像。

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

用户视角 TOC

每个悬停-单击控件都会显示一个图像,当用户将鼠标指针移到图像上时,该图像的填充颜色会发生变化。当鼠标指针悬停在图像上时,控件将接受用户的鼠标单击并执行相应的操作。

开发者视角 TOC

对于开发者而言,可以将悬停-单击控件 集成到Visual Studio工具箱 [^] 中,并将其放置在Windows窗体表面上。悬停-单击控件最重要的属性是 this.Height,内部称为 control_height,外部称为 Control_Height 属性。所有其他尺寸均从此尺寸派生。

当用户单击悬停-单击控件的图像时,会 引发 [^] UserClicked 事件。在 EventArgs [^] 中没有提供任何值。此事件仅传递用户单击了悬停-单击控件图像的事实。

开发控件 TOC

在开发这些控件的过程中,遵循某种形式的需求至关重要,这通过Microsoft PowerPoint演示文稿得以实现。该演示文稿中的一些幻灯片作为PNG图像包含在本文章中。完整的演示文稿作为可下载文件提供。

如果读者没有安装Microsoft PowerPoint,可以下载免费的 PowerPoint查看器 [^] 。

命名约定 TOC

在本文中将介绍许多悬停-单击控件。为了避免重复“悬停-单击控件”这个短语,当其存在是隐含的情况下,将省略该短语。

目录

符号 TOC 将读者带回目录的顶部。

视觉属性 TOC

Favorites Control Visual Properties

每个悬停-单击控件都有四个属性构成了用户的视觉图像。在右侧的图表中,以收藏夹控件为例,控件的边界是用绿色虚线绘制的。

开发人员通过将控件从工具箱拖动到窗体上的位置来指定控件的左上角。该位置在控件的图形环境中成为 ( 0, 0 )。开发人员通过拖动控件的调整大小句柄或通过在控件的属性对话框中指定其值来指定 Control_Height。控件的宽度设置为等于 control_height。在控件边框和图形图像边缘之间有两像素的边距。

开发人员可以指定 Control_Color(当鼠标指针 在控件上时控件的填充颜色)和 Hover_Color(当鼠标指针 控件上时控件的填充颜色)。Outline 指定是否为控件的边框加上框架。

悬停-单击控件的背景是透明的。没有提供更改此属性的方法。这导致控件只有控件的图形图像可见。

注意
Settings Control Cursor Location

在左侧的图表中,鼠标指针 热点 [^] 位于红点处。

必须将鼠标指针放在控件的图形图像上才能识别悬停。在左侧的图表中,标记为 A 的鼠标指针将被识别为悬停在控件上,而标记为 B 的鼠标指针则不会,即使它在控件边界内。另外请注意,在这种情况下,最内层的圆圈不在悬停区域内。

控件属性 TOC

悬停-单击控件具有以下可供开发者使用的属性:

名称       描述
Control_Color       获取或设置当鼠标指针 不在 控件的图形图像内时控件的填充颜色。默认值为 Color.White。
Control_Height       获取或设置控件的高度。对此属性的任何更改都会导致控件的其他内部值发生更改。默认值为 30 像素。
Hover_Color       获取或设置当鼠标指针 控件的图形图像内时控件的填充颜色。对于设置和主页控件,默认值为 Color.LightSkyBlue;对于收藏夹控件,默认值为 Color.Gold。
Outline       获取或设置控件的图形图像是否以黑色轮廓绘制。默认值为 true

实现 TOC

在实现悬停-单击控件时,有一些必须执行的常见任务。

  • 定义控件委托和事件
  • 执行内存清理
  • 创建控件的图形图像
  • 定义类构造函数
  • 处理事件
    • OnMouseClick
    • OnMouseLeave
    • OnMouseMove
    • OnPaint
    • OnResize
  • 获取/设置控件属性
    • Control_Color
    • Control_Height
    • Hover_Color
    • Outline

除创建控件图形图像外,所有这些任务在不同的悬停-单击控件之间都是相同的。

定义控件委托和事件 TOC

所有悬停-单击控件都指示用户单击了其图像。为了发出此事件,控件包含一个事件声明,该事件在鼠标单击控件图形图像时触发。

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

      public delegate void UserClickedHandler ( Object      sender,
                                                EventArgs   e );

      public event UserClickedHandler UserClicked;
  

UserClickedHandler 委托 [^] 定义了将被 UserClicked 事件调用的方法的签名。该事件有两个参数:sender 和 EventArgs。EventArgs 始终为空。

执行内存清理 TOC

在此模板中,有两个 图形设备接口 (GDI) [^] 对象,control_brushcontrol_region,它们在 OnPaint [^] 方法的调用之间保持不变。当控件被释放时,两者都必须被释放。如果在控件被释放时未能释放 GDI 对象,则会导致 内存泄漏 [^]。memory_cleanup 方法执行此任务。

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

      void memory_cleanup ( object    sender, 
                            EventArgs e )
          {

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

          if ( control_region != null )
              {
              control_region.MakeEmpty ( );
              control_region.Dispose ( );
              control_region = null;
              }
          }
  

在控件的构造函数中出现

      this.Disposed += new EventHandler ( memory_cleanup );
  

此行 注册 [^] 了 memory_cleanup 方法,作为悬停-单击控件本身被释放时的 事件处理程序 [^] 。

创建控件的图形图像 TOC

主页和收藏夹控件的图形图像很简单,并且有很多共同之处。设置控件的图形图像则复杂得多。

图形设备接口 (GDI) [^] TOC

在深入研究绘制悬停-单击控件的具体细节之前,回顾一下图形设备接口(也称为 GDI)的一些特性可能会很有用。这个 Microsoft 产品是 Microsoft 操作系统的一部分,为所有 Windows 组件提供绘图和字符串格式化功能。通过其 API,GDI 向开发者提供了扩展支持。

特别值得注意的是 GDI 执行 GDI 对象 旋转 [^] 、 缩放 [^] 和 平移 [^] 的能力。

在此控件模板中,所有绘图都使用从固定的 CONTROL_HEIGHT 200 像素派生的值进行。绘制的结果比使用当前 control_height(通常小得多)进行绘制更精确。通过以较大的尺寸绘制,然后利用 GDI 缩放方法,我们可以获得一个不受插值错误影响的缩放图像。

例如,设置控件的图形图像包含围绕控件中心圆周排列的齿轮齿。绘制这些齿有两个选项:

选项 1。
Settings Control Calculations.png
对于每个齿,计算齿的四个角的坐标(上图中的点 p0、p1、p2 和 p3);使用这些四点定义的 त्याच्या多边形创建一个 GraphicsPath [^];将 GraphicsPath 添加到区域。处理完所有齿后,将区域作为齿区域返回。程序员负责计算平移、旋转和缩放的图像。
选项 2。
Settings Control Rotate Translate

o 是控件的原点,点 p 是绘制齿后的位置。在这种情况下,旋转角度是 135°。因此,只需在原点绘制一个齿,然后应用 变换矩阵 [^] 。

对于每个齿,计算齿的左上角偏移量 p(在旋转之前);计算旋转角度;计算将导致图像尺寸正确的缩放因子;用偏移量、旋转和缩放填充变换矩阵;将 在控件原点绘制 的齿的图像添加到 GraphicsPath;允许变换矩阵将图像放置在其在路径中的最终位置和方向;将路径添加到齿区域。处理完所有齿后,将区域作为齿区域返回。GDI 负责缩放、旋转和平移图像。

那么使用 GDI 进行缩放、旋转和平移的优势是什么呢?毫无疑问,GDI 的性能比我编写的任何代码都要高效和准确。即使 Control_Height 设置为 30 像素,所有齿的绘制也都正确。在计算坐标和绘制多边形的实验中,我发现有些齿绘制不佳或缺失。

在前面的讨论中,每个控件都使用 GDI 区域 [^] 来包含其图形图像,并以缩放、旋转和平移的状态。区域描述了由矩形和路径组成的图形形状的内部。它是可缩放的。控件使用区域来剪裁绘图操作的输出。这些区域称为剪裁区域。控件还在命中测试操作中使用区域,例如检查一个点是否在区域内。控件可以使用 画笔 [^] 对象来填充区域。

尽管 Microsoft 通常在将 Win32 API 入口点转换为 C# 入口点方面做得相当彻底,但绘制区域(而不是填充)似乎被忽略了。要绘制边框,有必要调用 Win32 API 的 FrameRgn [^] 函数。

主页和收藏夹控件 TOC

主页和收藏夹控件的图形图像最初是在 Microsoft PowerPoint 的一个十乘十的网格上创建的。坐标(以十分之一为单位)是顺时针收集的,从左下点开始。

Constructing Home and Favorites

每个坐标值乘以 200,以获得定义图像多边形的线段端点的坐标。对于主页控件:

      Point [ ]   home = { new Point (  20, 160 ),
                           new Point (  20,  80 ),
                           new Point (   0,  80 ),
                           new Point (  20,  70 ),
                           new Point (  20,  40 ),
                           new Point (  40,  40 ),
                           new Point (  40,  56 ),
                           new Point ( 100,  20 ),
                           new Point ( 200,  80 ),
                           new Point ( 180,  80 ),
                           new Point ( 180, 160 ),
                           new Point ( 120, 160 ),
                           new Point ( 120, 100 ),
                           new Point (  80, 100 ),
                           new Point (  80, 160 ),
                           new Point (  20, 160 ) };
  

对于收藏夹控件:

      Point [ ]   star = { new Point (  40, 200 ),
                           new Point (  62, 125 ),
                           new Point (   0,  80 ),
                           new Point (  75,  80 ),
                           new Point ( 100,   0 ),
                           new Point ( 125,  80 ),
                           new Point ( 200,  80 ),
                           new Point ( 138, 125 ),
                           new Point ( 160, 200 ),
                           new Point ( 100, 155 ),
                           new Point (  40, 200 ) };
  

尽管不是必需的,但最后一个点复制了第一个点,从而闭合了多边形。可以通过调用 CloseFigure [^] 方法来完成闭合。

定义了主页和收藏夹多边形的端点后,就可以创建控件区域。

      // ************************************* create_control_region

      Region create_control_region ( int        new_height,
                                     Point [ ]  points,
                                     Region     control_region )
          {
          GraphicsPath    path = new GraphicsPath ( );
          float           scale = 0.0F;
          Matrix          transform = new Matrix ( );

          control_height = new_height;
          this.Height = control_height;
          this.Width = control_height;

          scale = ( float ) new_height / ( float ) CONTROL_HEIGHT;
          transform.Scale ( scale, scale );

          path.AddPolygon ( points );
          path.Transform ( transform );

          if ( control_region != null )
              {
              control_region.Dispose ( );
              control_region = null;
              }
          control_region = new Region ( path );

          transform.Dispose ( );
          path.Dispose ( );

          return ( control_region );
          }
  

此方法执行以下操作:

  1. 将控件的高度和宽度设置为传递给 new_height 的值。
  2. 计算将用于缩放图形对象的比例,并将结果放入变换矩阵。
  3. 将由 points 数组参数定义的 the polygon 添加到路径中。
  4. 应用变换矩阵。
  5. 删除任何现有的控件区域并创建一个新的控件区域。
  6. 返回新创建的控件区域。

如果正在绘制的控件是主页控件,则 points 数组参数将是 home 数组;如果是收藏夹控件,则 points 数组参数将是 star 数组。

设置控件 TOC

如前所述,设置控件的图形图像很复杂。

Settings Control Construction

控件的图形由两个独立的 区域 [^] 组成:圆环(甜甜圈)区域和齿区域。如前图所示,圆环区域由一个外部区域(实心圆)和一个内部区域(空心圆)组成,两者结合起来形成圆环区域。当圆环区域和齿区域结合时,就得到了控件区域。

annulus_regioncreate_annulus_region 方法创建。

      // ************************************* create_annulus_region

      Region create_annulus_region ( int  control_height )
          {
          int             diameter = 0;
          GraphicsPath    inner_path = new GraphicsPath ( );
          Region          inner_region = null;
          int             origin = 0;
          GraphicsPath    outer_path = new GraphicsPath ( );
          Region          outer_region = null;
          Rectangle       rectangle;
          float           scale = 0.0F;
          Matrix          transform = new Matrix ( );

          scale = ( float ) control_height / 
                  ( float ) CONTROL_HEIGHT;
          transform.Scale ( scale, scale );
                                      // define outer region
          origin = round ( OUTER_CIRCLE_MULTIPLIER *
                           ( float ) CONTROL_HEIGHT ) + 
                           OFFSET;
          diameter = CONTROL_HEIGHT - 2 * origin;
          rectangle = new Rectangle ( 
                          new Point ( origin,
                                      origin ),
                          new Size ( ( diameter - 1 ),
                                     ( diameter - 1 ) ) );
          outer_path.AddEllipse ( rectangle );
          outer_path.Transform ( transform );
          outer_region = new Region ( outer_path );
                                      // define inner region
          origin = round ( INNER_CIRCLE_MULTIPLIER *
                           ( float ) CONTROL_HEIGHT ) + 
                           OFFSET;;
          diameter = CONTROL_HEIGHT - 2 * origin;
          rectangle = new Rectangle ( 
                          new Point ( origin,
                                      origin ),
                          new Size ( ( diameter - 1 ),
                                     ( diameter - 1 ) ) );
          inner_path.AddEllipse ( rectangle );
          inner_path.Transform ( transform );
          inner_region = new Region ( inner_path );
                                      // exclude inner from outer
          outer_region.Exclude ( inner_region );
                                      // dispose of intermediates
          inner_path.Dispose ( );
          inner_region.Dispose ( );
          outer_path.Dispose ( );
          transform.Dispose ( );

          return ( outer_region );
          }
  

此方法执行以下操作:

  1. 计算将用于缩放图形对象的比例,并将计算值放入变换矩阵。对于外部区域和内部区域,scale 的计算值是恒定的。这是绘制圆环时使用的唯一变换(即,没有旋转或平移)。
  2. 创建包含外部圆的外部路径。
  3. 缩放外部路径并将其放入外部区域。
  4. 创建包含内部圆的内部路径。
  5. 缩放内部路径并将其放入内部区域。
  6. 将内部区域从外部区域中排除,从而在外部区域上打开一个孔。
  7. 返回修改后的外部区域,即圆环区域。

辅助方法 round 的形式如下:

      // ***************************************************** round

      // http://en.wikipedia.org/wiki/Rounding

      public static int round ( float parameter )
          {

          return ( ( int ) ( parameter + 0.5F ) );
          }

      // ***************************************************** round

      // http://en.wikipedia.org/wiki/Rounding

      public static int round ( double parameter )
          {

          return ( ( int ) ( parameter + 0.5 ) );
          }
  

该方法是重载的,允许使用 floatdouble 作为参数。

齿区域由 create_teeth_region 创建,该方法实现了前面描述的 第二种选项

      // *************************************** create_teeth_region

      Region create_teeth_region ( int  control_height )
          {
          Point   center;
          int     d;
          double  height_div_2;
          double  height_div_2_squared;
          int     radius = 0;
          Region  region = new Region ( );
          float   scale = 0.0F;
          int     tooth_height;
          int     tooth_width;
          float   tooth_width_div_2;

          scale = ( float ) control_height / 
                  ( float ) CONTROL_HEIGHT;

          region.MakeEmpty ( );       // must do this!!

          radius = round ( ( float ) CONTROL_HEIGHT / 2.0F ) -  
                           OFFSET;
          center = new Point ( radius, radius );

          height_div_2 = ( double ) CONTROL_HEIGHT / 2.0 -
                         2.0 * ( double ) OFFSET;
          height_div_2_squared = height_div_2 * height_div_2;
          d = round ( Math.Sqrt ( height_div_2_squared + 
                                  height_div_2_squared ) );
          tooth_height = round ( ( float ) d / 2.0F );
          tooth_width = round ( 0.2F * ( float ) CONTROL_HEIGHT );
          tooth_width_div_2 = ( float ) tooth_width / 2.0F;

          for ( int i = 0; ( i < NUMBER_TEETH ); i++ )
              {
                                      // do not move transform or
                                      // path outside this loop
              int             beta = 0;
              Rectangle       gear_tooth;
              float           offset_x = ( float ) OFFSET;
              float           offset_y = ( float ) OFFSET;
              GraphicsPath    path = new GraphicsPath ( );
              float           rotate = 0.0F;
              Point           t = new Point ( );
              int             theta = i * DEGREE_INCREMENT;
              Matrix          transform = new Matrix ( );

              t.X = center.X + round ( ( float ) d * 
                                       cos_deg ( theta ) );
              t.Y = center.Y + round ( ( float ) d * 
                                       sin_deg ( theta ) );

              beta = theta - 90;
              if ( beta < 0 )
                  {
                  beta = beta + 360;
                  }

              offset_x = ( float ) t.X + ( float ) OFFSET + 
                         ( tooth_width_div_2 * 
                         cos_deg ( beta ) );
              offset_y = ( float ) t.Y + ( float ) OFFSET + 
                         ( tooth_width_div_2 * 
                         sin_deg ( beta ) );

              rotate = ( float ) theta + 90.0F;
              if ( rotate > 360.0F )
                  {
                  rotate = rotate - 360.0F;
                  }

              transform.Reset ( );
              transform.Translate ( offset_x, offset_y );
              transform.Rotate ( rotate );
              transform.Scale ( scale, scale, MatrixOrder.Append );

              gear_tooth = new_gear_tooth ( tooth_width,
                                            tooth_height );
              gear_tooth.Inflate ( -1, -1 );

              path.Reset ( );
              path.AddRectangle ( gear_tooth );
              path.Transform ( transform );

              region.Union ( path );

              transform.Dispose ( );
              path.Dispose ( );
              }

          return ( region );
          }
  

此方法执行以下操作:

  1. 计算将放入变换矩阵中用于缩放图形对象的比例。对于所有齿,scale 的计算值是恒定的。
  2. 调用 MakeEmpty [^] 初始化目标区域为空内部。请参阅 经验教训
  3. 计算 d,即控件中心到每个齿外缘中心的距离,如 上图 所示。计算值对于所有齿都是恒定的。
  4. 根据计算出的 d 值计算 tooth_heighttooth_width
  5. 对于每个齿:
    • 计算齿外缘中心的点。
    • 计算 x 和 y 方向的平移偏移量。
    • 计算相对于 x 轴的旋转角度。
    • 用计算出的偏移量、旋转和缩放值填充变换矩阵。
    • 在控件原点创建一个齿,并将其尺寸减小一像素,以确保底部和右侧被绘制。
    • 重置路径,将齿添加到路径,并对路径应用平移、旋转和缩放。
    • 将路径添加到区域。
  6. 创建完所有齿后,返回该区域。

辅助方法 new_gear_tooth 如下:

      // ******************************************** new_gear_tooth

      Rectangle new_gear_tooth ( int  width,
                                 int  height )
          {

          return ( new Rectangle ( 0, 0, width, height ) );
          }
  

如前所述,每个齿最初都在控件的原点绘制,然后 create_teeth_region 将其平移、旋转和缩放,以达到最终的位置和方向。

三个允许对整数度进行计算的辅助方法是:

      // ************************************************* deg_2_rad

      public static double deg_2_rad ( int degrees )
          {

          return ( ( ( double ) degrees / 180.0 ) * 
                   System.Math.PI );
          }

      // *************************************************** cos_deg

      public static float cos_deg ( int  degrees )
          {

          return ( ( float ) System.Math.Cos ( deg_2_rad ( 
                                                   degrees ) ) );
          }

      // *************************************************** sin_deg

      public static float sin_deg ( int  degrees )
          {

          return ( ( float ) System.Math.Sin ( deg_2_rad ( 
                                                   degrees ) ) );
          }
  
Unclipped Control Region

如果将 annulus_regionteeth_region 组合在一起,其结果将类似于左侧图。这不是期望的结果。有必要对设置控件应用一个剪裁区域。

此剪裁区域将是一个圆,其宽度和高度与控件本身相同。作为一个以控件中心为中心的圆,它还将起到圆化齿的外边缘的作用。

clipping_regioncreate_clipping_region 方法创建。

      // ************************************ create_clipping_region

      Region create_clipping_region ( int  control_height )
          {
          int             height = CONTROL_HEIGHT - 2 * OFFSET;
          GraphicsPath    path = new GraphicsPath ( );
          Region          region = null;
          float           scale = 0.0F;
          Matrix          transform = new Matrix ( );

          scale = ( float ) control_height / 
                  ( float ) CONTROL_HEIGHT;
          transform.Scale ( scale, scale );

          path.AddEllipse ( new Rectangle ( 
                                new Point ( OFFSET, OFFSET ),
                                new Size ( ( height - 1 ),
                                           ( height - 1 ) ) ) );
          path.Transform ( transform );

          region = new Region ( path );

          path.Dispose ( );
          transform.Dispose ( );

          return ( region );
          }
  

此方法执行以下操作:

  1. 计算将用于缩放剪裁区域的比例,并将计算值放入变换矩阵。
  2. 将剪裁区域添加到路径。
  3. 将变换矩阵应用于路径。
  4. 创建一个区域,将路径添加到该区域。
  5. 返回该区域。
Clipped Control Region

clipping_region 应用于组合的 annulus_regionteeth_region 时,结果将类似于右侧图。这就是期望的图形图像。

现在我们拥有了构成设置控件图形图像的所有组件。create_control_region 收集这些组件并将它们组合成 control_region

      // ************************************* create_control_region

      Region create_control_region ( int     new_height,
                                     Region  control_region )
          {
          Region  annulus_region = null;
          Region  clipping_region = null;
          Region  teeth_region = null;

          control_height = new_height;
          this.Height = control_height;
          this.Width = control_height;

          annulus_region = create_annulus_region ( 
                                  control_height );
          teeth_region = create_teeth_region ( 
                                  control_height );
          clipping_region = create_clipping_region ( 
                                  control_height );

          if ( control_region != null )
              {
              control_region.Dispose ( );
              }
          control_region = new Region ( );
          control_region.MakeEmpty ( );

          control_region.Union ( annulus_region );
          control_region.Union ( teeth_region );
          control_region.Intersect ( clipping_region );
          
          annulus_region.Dispose ( );
          annulus_region = null;
          clipping_region.Dispose ( );
          clipping_region = null;
          teeth_region.Dispose ( );
          teeth_region = null;

          return ( control_region );
          }
  

此方法:

  1. 将控件的高度和宽度设置为传递给 new_height 的值。
  2. 创建 annulus_region
  3. 创建 teeth_region
  4. 创建 clipping_region
  5. 删除任何现有的 control_region 并创建一个新的。
  6. 调用 MakeEmpty [^] 初始化 control_region 为空内部。请参阅 经验教训
  7. annulus_regionteeth_region 合并 [^],生成一个临时的 control_region
  8. 将临时 control_regionclipping_region 求交 [^],从而创建最终的 control_region
  9. 返回最终的 control_region

定义类构造函数 TOC

所有悬停-单击控件的构造函数的形式如下:

      // ********************************************** <control-name>

      public <control-name> ( )
          {

          this.SetStyle ( ( ControlStyles.DoubleBuffer |
                            ControlStyles.UserPaint |
                            ControlStyles.AllPaintingInWmPaint ),
                          true );
          this.UpdateStyles ( );

          this.Disposed += new EventHandler ( memory_cleanup );

          <create_control_region>;
          }
  

<control-name> 是悬停-单击控件的名称(例如,“Favorites”、“Home”、“Settings”)。

SetStyle [^] 启用或禁用作为其参数传递的样式。在这种情况下,正在设置样式。来自 ControlStyles 枚举 [^]

DoubleBuffer     如果为 true,则绘图在缓冲区中执行,完成后,结果输出到屏幕。双缓冲可防止因控件重绘而产生的闪烁。如果设置 DoubleBuffer 为 true,则还应将 UserPaint 和 AllPaintingInWmPaint 设置为 true。
UserPaint 如果为 true,则控件自行绘制,而不是由操作系统绘制。如果为 false,则不会引发 Paint 事件。此样式仅适用于派生自 Control 的类。
AllPaintingInWmPaint 如果为 true,则控件忽略窗口消息 WM_ERASEBKGND 以减少闪烁。此样式仅在 UserPaint 位设置为 true 时应用。

UpdateStyles [^] 强制将新样式应用于控件。

Disposed [^] 标识处理控件处置的方法。请参阅 执行内存清理,上方。

<create_control_region> 使用适当的参数创建控件区域。

Favorites    
control_region = create_control_region ( CONTROL_HEIGHT,
                                         star,
                                         control_region );
Home
control_region = create_control_region ( CONTROL_HEIGHT,
                                         home,
                                         control_region );
设置
control_region = create_control_region ( CONTROL_HEIGHT,
                                         control_region );

处理事件 TOC

一旦实例化,所有悬停-单击控件都由事件驱动。这意味着外部事件(例如,将鼠标悬停在控件上、单击控件等)会导致控件做出反应。对外部事件的响应通过控件的事件处理程序进行。

OnMouseClick 事件处理程序 TOC

当用户单击控件的任何部分时,会引发 MouseClick [^] 事件。但是,鼠标被单击并不意味着单击发生在控件的图形图像内。为了响应用户对控件的单击,我们将使用 OnMouseClick [^] 方法来接收单击通知,并使用 control_region 执行命中测试,以确保单击发生在控件的图形图像上。

      // ********************************************** OnMouseClick

      protected override void OnMouseClick ( MouseEventArgs e )
          {

          base.OnMouseClick ( e );

          if ( control_region != null )
              {
              if ( control_region.IsVisible ( 
                       new Point ( e.X, e.Y ) ) )
                  {
                  if ( UserClicked != null )
                      {
                      UserClicked ( this, EventArgs.Empty );
                      }
                  }
              }
          }
  

OnMouseClick 方法执行以下操作:

  1. 调用基类的 OnMouseClick 方法,以便所有已注册的委托都能接收该事件。
  2. 确保 control_region 存在。
  3. 使用 IsVisible [^] 来确定单击发生时鼠标是否在 control_region 内。
  4. 测试以确保 UserClicked 事件有订阅者。
  5. 如果 UserClicked 事件有订阅者,则引发 UserClicked 事件。

如果一个类希望收到悬停-单击控件中鼠标单击的通知,它必须 注册 [^] 一个 UserClicked 事件处理程序。在 悬停-单击演示 项目中,以设置控件为例,这是通过首先声明 settings_UserClicked 方法用于捕获和处理事件来完成的。

      settings.UserClicked += 
          new Settings.Settings.UserClickedHandler ( 
                                      settings_UserClicked );
  

settings_UserClicked 方法声明如下:

      // ************************************** settings_UserClicked

      void settings_UserClicked ( object    sender, 
                                  EventArgs e )
          {

          if ( settings_dialog == null )
              {
              settings_dialog = new SettingsDialog ( );
              }
          ( ( SettingsDialog ) settings_dialog ).initialize_GUI ( );

          if ( settings_dialog.ShowDialog ( ) == DialogResult.OK )
              {
              // retrieve settings
              }
          else
              {
              // do something with Cancel
              }
          }
  

请注意, settings_UserClicked 方法打开一个对话框,假定用于收集设置。

OnMouseLeave 事件处理程序 TOC

当鼠标指针完全移出控件边界时,会引发 MouseLeave [^] 事件。发生这种情况时,必须重置控件的填充颜色。 OnMouseLeave [^] 方法执行此任务。

        // ********************************************** OnMouseLeave

        protected override void OnMouseLeave ( EventArgs e )
            {

            base.OnMouseLeave ( e );

            if ( hovering )
                {
                if ( control_brush != null )
                    {
                    control_brush.Dispose ( );
                    }
                control_brush = new SolidBrush ( control_color );
                this.Invalidate ( );
                hovering = false;
                }
            }
  

OnMouseLeave 方法执行以下操作:

  1. 调用基类的 OnMouseLeave 方法,以便所有已注册的委托都能接收该事件。
  2. 如果鼠标指针正在悬停(即,在 control_region 中),则:
    • 如果 control_brush 存在,则释放 control_brush
    • 使用 control_color 重新创建 control_brush
    • 调用 this. Invalidate [^] 以重绘控件。
    • 将悬停重置为 false。
OnMouseMove 事件处理程序 TOC

当鼠标指针在控件表面移动时,会引发 MouseMove [^] 事件。发生这种情况时,可能需要重置控件的填充颜色。 OnMouseMove [^] 方法执行此任务。

      // *********************************************** OnMouseMove

      protected override void OnMouseMove ( MouseEventArgs e )
          {
          bool  was_hovering = hovering;

          base.OnMouseMove ( e );

          hovering = false;
          if ( control_region != null )
              {
              hovering = ( control_region.IsVisible ( 
                               new Point ( e.X, e.Y ) ) );
              }

          if ( was_hovering != hovering )
              {
              if ( control_brush != null )
                  {
                  control_brush.Dispose ( );
                  }
              if ( hovering )
                  {
                  control_brush = new SolidBrush ( hover_color );
                  }
              else
                  {
                  control_brush = new SolidBrush ( control_color );
                  }
              this.Invalidate ( );
              }
          }
  

鼠标的坐标包含在传递给 OnMouseMove 方法的 MouseEventArgs 中。

OnMouseMove 方法执行以下操作:

  1. 调用基类的 OnMouseMove 方法,以便所有已注册的委托都能接收该事件。
  2. 测试以确保 control_region 存在。
  3. 如果 control_region 存在,则使用 IsVisible [^] 来确定单击发生时鼠标是否在 control_region 内。
  4. 测试以确保 hovering 的先前值不等于 hovering 的新值
    • 如果 control_brush 存在,则释放 control_brush
    • 创建一个具有适当颜色的新 control_brush
    • 调用 this. Invalidate [^] 以重绘控件。
OnPaint 事件处理程序 TOC

每当系统检测到需要重绘控件时,它都会引发 Paint [^] 事件。Paint 事件可能被引发的原因有很多,包括:

  • 控件大小已更改。
  • 控件的隐藏区域变得可见。
  • 控件本身请求重绘。
当引发 Paint 事件时,必须重绘控件。 OnPaint [^] 方法执行此任务。
      // *************************************************** OnPaint

      protected override void OnPaint ( PaintEventArgs e )
          {

          base.OnPaint ( e );

          e.Graphics.FillRegion ( control_brush, control_region );
          if ( Outline )
              {
              Utilities.FrameRegion.frame_region ( e.Graphics, 
                                                   control_region );
              }
          }
  

OnPaint 方法执行以下操作:

  1. 调用基类的 OnPaint 方法,以便所有已注册的委托都能接收该事件。
  2. 使用 control_brush 填充 control_region
  3. 如果 Outline 属性为 true,则为 control_region 加上框架(绘制边框)。
OnResize 事件处理程序 TOC

每当系统检测到控件的大小正在改变时,它都会引发 ReSize [^] 事件。当引发 ReSize 事件时,必须重绘控件。 OnResize [^] 方法执行此任务。

      // ************************************************** OnResize

      protected override void OnResize ( EventArgs e )
          {

          base.OnResize ( e );

          <create_control_region>;
          this.Invalidate ( );
          }
  

其中 <create_control_region> 使用 适当的参数,如上所述,创建控件区域。

OnResize 方法执行以下操作:

  1. 调用基类的 OnResize 方法,以便所有已注册的委托都能接收该事件。
  2. 重新创建 control_region
  3. 调用 this. Invalidate [^] 以重绘控件。

获取/设置控件属性 TOC

所有悬停-单击控件都有四个属性(参见上面的 Control_Properties)。它们的值通过经典的 Getter Setter 方法收集。这些方法在以下子部分中展示(不加评论)。

Control_Color TOC
      // ********************************************* Control_Color

      [ Category ( "Appearance" ),
        Description ( "Gets/Sets color of control" ),
        DefaultValue ( typeof ( Color ), "White" ),
        Bindable ( true ) ]
      public Color Control_Color
          {

          get
              {
              return ( control_color );
              }
          set
              {
              if ( value != control_color )
                  {
                  control_color = value;
                  this.Invalidate ( );
                  }
              }
          }
  
Control_Height TOC
      // ******************************************** Control_Height

      [ Category ( "Appearance" ),
        Description ( "Gets/Sets control height" ),
        DefaultValue ( typeof ( int ), "CONTROL_HEIGHT" ),
        Bindable ( true ) ]
      public int Control_Height
          {

          get
              {
              return ( control_height );
              }
          set
              {
              if ( value != control_height )
                  {
                  control_region = create_control_region ( 
                                                  value, 
                                                  control_region );
                  this.Invalidate ( );
                  }
              }
          }
  
Hover_Color TOC
      // *********************************************** Hover_Color

      [ Category ( "Appearance" ),
        Description ( "Gets/Sets color of control when mouse hovers" ),
        DefaultValue ( typeof ( Color ), "LightSkyBlue" ),
        Bindable ( true ) ]
      public Color Hover_Color
          {

          get
              {
              return ( hover_color );
              }
          set
              {
              if ( value != hover_color )
                  {
                  hover_color = value;
                  this.Invalidate ( );
                  }
              }
          }
  
Outline TOC
      // *************************************************** Outline

      [ Category ( "Appearance" ),
        Description ( "Gets/Sets whether outline is to be drawn" ),
        DefaultValue ( typeof ( bool ), "true" ),
        Bindable ( true ) ]
      public bool Outline
          {

          get
              {
              return ( outline );
              }
          set
              {
              if ( value != outline )
                  {
                  outline = value;
                  this.Invalidate ( );
                  }
              }
          }
  

经验教训 TOC

在开发这些悬停-单击控件的过程中,我发现了一些以前未曾遇到过的区域(regions)的特性。在我过去的编程经验中,我使用区域的目的与本文中的用法类似。然而,以前的区域是单一对象区域,而不是使用联合(unions)组合的。

我也从未对区域进行过框架化(framing)。区域总是相对于它们的背景可见。显然,对于悬停-单击控件来说,情况并非如此。

Region.MakeEmpty TOC

Region.Union 方法有以下几种形式:

每种形式都会将区域更新为自身与 Region.Union 方法的参数(GDI 对象)的联合。

但是,在下面的片段中情况并非如此:

      Region        final_region = null;
      GraphicsPath  first_path = null;
      Region        first_region = null;
      GraphicsPath  second_path = null;
      Region        second_region = null;

      first_path = new GraphicsPath ( some polygon )
      first_region = new Region ( first_path );
      second_path = new GraphicsPath ( some other polygon )
      second_region = new Region ( second_path );
      final_region = new Region ( );
      final_region.Union ( first_region );
      final_region.Union ( second_region );
  

final_region 填充了监视器。当被查询时,Microsoft 回应:

      Since the constructor creates an infinite region, try this:

        final_region = new Region ( );
        final_region.MakeEmpty ( );
        final_region.Union ( first_region );
        final_region.Union ( second_region );  
  
教训:       在所有使用 Region.Union ( Region ) 组合区域的情况下,请在目标区域的构造函数之后立即添加 Region.MakeEmpty。

应用剪裁区域 TOC

有两种方法可以应用剪裁区域:

  1. 使用 SetClip [^] 方法将剪裁区域应用于通过 PaintEventArgs [^] 传递的 Graphics [^] 对象。
  2. 使用 Region.Intersect [^] 方法将现有区域与剪裁区域合并。
Control Region With Clipping Framed

如果使用第一种方法,然后调用 FrameRgn [^],我们将得到类似左侧图的图像。

似乎 SetClip 方法设置的剪裁区域与 Graphics 对象相关联,而不是与 control_region 相关联。当图像绘制时,剪裁似乎发生在 control_region 上。当进行框架化时,它似乎作用于 control_region,就好像没有应用剪裁一样。

Desired Control Region

如果使用第二种方法,然后调用 FrameRgn,我们将得到类似右侧图的图像。

因此,看来在调用 FrameRgn 方法之前,必须将 clipping_region 应用于 control_region

教训:       在调用 FrameRgn 之前,使用 Region.Intersect 将剪裁区域与控件区域合并。

悬停-单击演示 TOC

Hover-Click Controls Demonstration

演示项目允许开发人员测试悬停-单击控件的工作方式。它还提供了一个框架来测试新的悬停-单击控件。

结论 TOC

本文介绍了一个用于实现用户自定义的悬停-单击控件的模板。

参考文献 TOC

开发环境 TOC

悬停-单击控件在以下环境中开发:

      IrfanView for Windows Version 4.36 [^]
Microsoft Visual C# 2008
Microsoft Visual Studio 2008 Professional
Microsoft .Net Framework Version 3.5 SP1
Microsoft Windows 7 Professional SP1
Microsoft Paint (Windows 7 Professional 的一部分)
Microsoft Office PowerPoint 2003 SP3

历史 TOC

11/26/2013   原始文章
12/04/2013   修改了 OnMouseLeave 方法;为两个 PNG 图像添加了边框;修复了排版错误;向开发环境中添加了项目。
© . All rights reserved.