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

WinForms 控件的 Adorners、Glyphs、Behavior 和 ControlDesigner

starIconstarIconstarIconstarIconstarIcon

5.00/5 (11投票s)

2015 年 3 月 30 日

CPOL

8分钟阅读

viewsIcon

26669

downloadIcon

1571

交互式设计时控件叠加层,使自定义控件的编辑更加容易。

 

引言

    点击 IDE 中的大多数控件时,会在控件周围出现一个带调整大小手柄的选择框。虚线矩形和手柄绘制在控件边界之外,因此无法在控件的 paint 事件中复制,因为 paint 事件不会绘制到控件边界之外。  

    在少数控件上,用户可以直接在控件上用鼠标进行交互以更改其属性。SplitContainer 在每个面板周围绘制虚线,该虚线仅在 DesignTime 时出现,并允许用户来回拖动分隔符以更改 SplitterDistance 属性。TableLayoutPanel 是另一个例子。 

    ControlDesigner 有一种创建这些额外装饰元素并与之交互的方式。 

背景

    在我之前的一些控件中,我使用了自定义的 ControlDesigner 来丰富 IDE 编辑体验。ControlDesigner 允许您向自定义控件添加 Smart TagsVerbs 我发现 SelectionRules 可以在此处修改。可以通过覆盖 OnPaintAdornments 事件子程序,在 DesignTime 期间才出现的额外图形可以绘制在控件之上。GetHitTest 和 OnMouse 子程序允许在 DesignTime 期间与控件进行交互,而不仅仅是选择、移动和调整控件大小。请参阅下面的 UITypeEditorsDemo 链接。 

    基本思想是,设计器的绘制和鼠标例程仅在 DesignMode 期间发生。在 DesignMode 中,控件会在其自己的 OnPaint 子程序中绘制自身,然后 OnPaintAdornments 中的图形会绘制在其之上。

    GetHitTest 确定光标是否在某个位置上,并返回 true 或 false。在控件的鼠标事件中,您将需要处理两种情况(行为)。

Sub MouseEvent…

    If DesignMode Then

        'Do this DesignTime Stuff

    Else

        'Do this RunTime Stuff

    End If

 End Sub

    总的来说,这对于简单的交互效果很好。对于更复杂的交互或对鼠标行为的更多控制,则需要 AdornerGlyph。在我最初弄清楚 ControlDesigner 的时候,我并不知道有单独的 AdornersGlyphs。即使是现在,也几乎找不到关于这些东西的信息。我想让多个 Adorners 在不同情况下绘制在控件之上。首先,ControlDesigner 只有 OnMouseEnterOnMouseHoverOnMouseLeave。试图确定鼠标交互非常困难,甚至不可能,因此在我寻找解决方案的过程中,我偶然发现了 Adorner Class

覆盖设计器的 OnPaintAdornments 只提供了一个绘制区域。我本可以使用 If Then Else 来绘制不同的内容,但是使用一个或多个 Adorners 来绘制一个或多个 Glyphs 提供了更多的控制和通用性。Adorner 本质上是一个容器,用于保存 Glyphs 的集合,而每个 Glyph 定义了要绘制的内容及其行为。我也喜欢控件本身不再需要处理 Adornment 的用户交互。控件的鼠标事件完全在控件中,而 Adornment 的鼠标事件则完全在 Glyph 的行为中,并且也与其他 Glyphs 互斥。

Adorner Class 有三个基本属性:

  • BehaviorService – 附加 System.Windows.Forms.Design.Behavior.BehaviorService 的引用
  • Enabled 一个布尔属性,用于确定 Adorner 是否可见并可交互
  • Glyphs – 保存 Glyphs 的集合。

 

Glyph Class 有两个基本属性和另外两个主要元素:

  • Behavior – 对要使用的 Behavior Class 的引用
  • Bounds – 用于定义 Glyph 的位置和大小的矩形
  • Paint 包含 Graphics 的详细信息
  • GetHitTest – 确定鼠标是否位于某个位置,并返回一个 Cursor

 

使用代码

SimpleControl

未选择的控件

已选择的控件

这是完全无用的 SimpleControl。

它包含

  • 附加的 SimpleControlDesigner
  • 一个名为 Message 的字符串属性,Browsable 设置为 false,因此只能在代码中更改
  • 重写了 Paint 以在控件上绘制 Message 文本
Imports System.ComponentModel

<Designer(GetType(SimpleControlDesigner))>
Public Class SimpleControl
	Inherits UserControl

	Private _Message As String = String.Empty
    <Browsable(False)>
    Public Property Message As String
		Get
			Return _Message
		End Get

		Set(value As String)
			_Message = value
			Invalidate()
		End Set
	End Property

	Protected Overrides Sub OnPaint(e As PaintEventArgs)
		MyBase.OnPaint(e)
		Dim sf As New StringFormat With {
			.Alignment = StringAlignment.Far, 
			.LineAlignment = StringAlignment.Center}

		e.Graphics.DrawString("Select Me", Me.Font, 
							  New SolidBrush(Me.ForeColor), 
							  New Rectangle(0, 0, Me.Width-10, 20), sf)
		e.Graphics.DrawString(Me.Message, Me.Font, 
							  Brushes.Red, 
							  New Rectangle(0, Me.Height - 20, Me.Width-10, 20), sf)
	End Sub

End Class

SimpleControl 演示了 Adornments 的一个非常基本的使用方法,以及如何在设计时从 Adornment 更新控件的属性。 

SimpleControl - SimpleControlDesigner - UselessAdorner - UselessGlyph - UselessBehavior

选择控件会向 ControlDesigner 发出信号,启用 Adorner,Adorner 会绘制 Glyph,如果鼠标点击了其中一个热点,则会更新控件的 Message 属性,并在控件的右下角重绘。

注意:如果您要开始自己的新项目,则必须先手动添加 System.Design.dll。

SimpleControlDesigner

这个 SimpleControlDesigner 非常基础。

  • 声明了一个 Adorner、一个 ISelectionService 和一个 BehaviorService。
  • 在 Initialize 子程序中:
    • 设置了 Selection 和 Behavior 服务的引用。 
    • 创建了 Adorner 并将其添加到 Behavior Service。
    • 将带有其行为的 Glyph 添加到 Adorner 的 Glyphs 集合中。
  • 最后,重写了 Dispose 以从 Behavior Service 中移除 Adorner。 
Imports System.Windows.Forms.Design
Imports System.Windows.Forms.Design.Behavior
Imports System.ComponentModel.Design
Imports System.ComponentModel

Public Class SimpleControlDesigner
	Inherits ControlDesigner

#Region "Declarations"

	Public UselessAdorner As Adorner = Nothing

	Private selectionSvc As ISelectionService = Nothing
	Private behaviorSvc As BehaviorService = Nothing

#End Region

#Region "Initialize/Dispose"

	Public Overrides Sub Initialize(ByVal component As IComponent)

		MyBase.Initialize(component)

		InitializeServices()
		InitializeUselessAdorner()

	End Sub

	Protected Overrides Sub Dispose(ByVal disposing As Boolean)
		If disposing Then
			If (Me.behaviorSvc IsNot Nothing) Then
				' Remove the adorners added by this designer from 
				' the BehaviorService.Adorners collection. 
				Me.behaviorSvc.Adorners.Remove(Me.UselessAdorner)
			End If
		End If

		MyBase.Dispose(disposing)

	End Sub

#End Region

#Region "Init Methods"

	Private Sub InitializeServices()

		' Acquire a reference to ISelectionService. 
		Me.selectionSvc = CType(GetService(GetType(ISelectionService)), ISelectionService)

		' Acquire a reference to BehaviorService. 
		Me.behaviorSvc = CType(GetService(GetType(BehaviorService)), 
								Windows.Forms.Design.Behavior.BehaviorService)

	End Sub


	Private Sub InitializeUselessAdorner()

		If (Not (UselessAdorner) Is Nothing) Then
			UselessAdorner.Glyphs.Clear()
		Else
			UselessAdorner = New Adorner()
			behaviorSvc.Adorners.Add(UselessAdorner)
			UselessAdorner.Glyphs.Add(New UselessGlyph(behaviorSvc, 
													   CType(Control, SimpleControl), 
													   selectionSvc, 
													   Me, 
													   UselessAdorner))
		End If
	End Sub

#End Region

End Class

 

UselessGlyph

New Sub

在这里,设置了将 Behavior 和 Selection 服务再次传递过去的引用,此外,我们还获得了 Adorner 和关联的 SimpleControl 和 SimpleControlDesigner 的引用。 

我们还连接了 SelectionService 的 SelectionChanged 事件和 RelatedControl 的 Move 事件。

SelectionChanged 根据 SelectionService 的 PrimarySelection 是否与 relatedControl 匹配来打开和关闭 Adorner。如果不进行此检查,则所有 simpleControlAdorners 将同时显示。 

当控件使用箭头键移动时,Move 事件会强制 Adorner 无效化。

Bounds 属性

BehaviorService 有一个 ControlToAdornerWindow 函数,用于获取控件窗口坐标的位置,以便我们知道在何处绘制 Glyph。

	Public Overrides ReadOnly Property Bounds() As Rectangle
		Get
			Dim edge As Point = behaviorSvc.ControlToAdornerWindow(relatedControl)

			Return New Rectangle(edge.X, edge.Y, relatedControl.Width, relatedControl.Height)

		End Get

	End Property
	
GetHitTest 函数

与 ControlDesigner 中的 GetHitTest 不同,这个函数返回一个 Cursor 而不是布尔值。如果返回 Nothing,则什么也不发生,但如果返回一个 Cursor,则光标会改变,并且鼠标事件将在 UselessBehavior Class 中触发。 

    	Public Overrides Function GetHitTest(ByVal p As Point) As Cursor
		If Object.ReferenceEquals( _
		Me.selectionSvc.PrimarySelection, _
		Me.relatedControl) Then
			If busyBox.Contains(p) Then
				Return Cursors.WaitCursor

			ElseIf handBox.Contains(p) Then
				Return Cursors.Hand

			ElseIf noBox.Contains(p) Then
				Return Cursors.No

			ElseIf crossBox.Contains(p) Then
				Return Cursors.Cross

			Else
				Return Nothing
			End If
		End If

		Return Nothing

	End Function
重写的 Paint 子程序

使用 GDI+ Graphics 在控件上绘制 Glyph。

UselessBehavior

New Sub

Designer 引用通过此处传递。 

OnMouse...Events

任何鼠标行为都在此处定义。对于这个控件,只有一个 OnMouseDown

		Public Overrides Function OnMouseDown(g As Glyph, button As MouseButtons, mouseLoc As Point) As Boolean
			Dim pGlyph As UselessGlyph = DirectCast(g, UselessGlyph)
			If pGlyph.busyBox.Contains(mouseLoc) Then
				relatedControl.Message = "Busy"
			ElseIf pGlyph.handBox.Contains(mouseLoc) Then
				relatedControl.Message = "Hand"
			ElseIf pGlyph.noBox.Contains(mouseLoc) Then
				relatedControl.Message = "No"
			ElseIf pGlyph.crossBox.Contains(mouseLoc) Then
				relatedControl.Message = "Cross"
			Else
				relatedControl.Message = String.Empty
			End If
			Return True
		End Function

 

SampleObject

SampleObject 是一个更复杂的示例,它使用五个不同的 Adorners,这些 Adorners 在不同的时间基于与 ChooseAdorner 的交互而打开,ChooseAdorner 始终处于活动状态。要激活其他 Adorners,请从 ChooseAdorner 中点击 P- 表示 Padding,C- 表示 Corners,F- 表示 Focal Point,L- 表示 Line。

填充

Padding Adorner 允许您抓取矩形的边缘并通过鼠标更改 Padding 属性。

角点

Corners Adorner 显示一个滑块来调整圆角的圆度。 

Focal Point

Focal Point Adorner 有两个热点。一个用于拖动圆的中心点,另一个用于左右移动以更改半径属性。

Line

Line Adorner 有两个热点。一个用于线条的每个端点,用于移动点。

我才刚开始了解这些可以做什么,由于信息很少,所以今后大部分将是实验。 

最初,我根据需要启用和禁用每个 Adorner,但我发现当表单中添加了多个控件时,选择控件开始变得缓慢而卡顿,因为它必须“查看”每个控件,而不管其自身的选择是否实际发生变化。如果某些控件位于 Panel 或 GroupBox 上,则会变得更加缓慢,控件也会变得非常闪烁。Selection service 正在遍历所有控件,而不仅仅是被选中和取消选中的控件,我还没有找到如何阻止它影响实际被选中和取消选中的控件之外的控件。现在,我将它们全部保留,并使用一个标志来在不被标记时绕过 GetHitTest 和 Paint。(这是更新二)

这有所帮助,并且似乎是解决方案,但当我尝试将其应用于我真实的自定义控件之一时,选择控件的速度慢如蜗牛。然后我注意到 MSDN 上关于 SelectionChanged 事件的注释。


备注

处理此事件时,请最小化处理量,因为在此事件处理程序中发生的操作会显著影响表单设计器的整体性能。


 

在更新三中,我添加了一个自定义属性来跟踪选定的控件,这似乎阻止了额外的处理。该属性在 PropertyGrid 中或在 Editor 的 Intelisense 选择中都不可显示。

	<Browsable(False), EditorBrowsable(EditorBrowsableState.Never)>
	Public Property DesignerSelected As Boolean

现在,当主控件被选中时,其 DesignerSelected 属性被设置为 True。如果不是主控件,它会检查其 DesignerSelected 属性,如果为 true,则会禁用该 Adorner,而将其他 Adorner 保持不变。

	Private Sub selectionService_SelectionChanged( 
	ByVal sender As Object, 
	ByVal e As EventArgs)

		If Me.relatedControl.GetType = GetType(SampleObject) Then

			If Object.ReferenceEquals(Me.selectionSvc.PrimarySelection, Me.relatedControl) Then
				SetBoxes()
				Me.ChooseAdorner.Enabled = True
				relatedControl.DesignerSelected = true
			Else
				If relatedControl.DesignerSelected Then
					Me.ChooseAdorner.Enabled = False
					relatedControl.DesignerSelected = False
			    End If
			End If

		End If

		chooseWhat = eChooseWhat.None

	End Sub

到目前为止,这正在奏效...

参考链接

如何使用 UITypeEditors、Smart Tags、ControlDesigner Verbs 和 Expandable Properties 使设计时编辑更轻松。
UITypeEditorsDemo[^]

为了 Adorners 的实际示例,我将它们添加到了 Custom Button 控件中。
CButton[^]

 

历史

  • 版本 1.0.0 - 2015 年 4 月 1 日
    • 首次发布版本
  • 版本 2.0.0 - 2015 年 4 月 3 日
    • 修复了选择拖动问题,尤其是在容器中时
  • 版本 3.0.0 - 2015 年 4 月 5 日
    • 添加了 DesignerSelected 属性,以更好地修复选择拖动问题。
© . All rights reserved.