Adorners, Glyphs, Behavior, and ControlDesigner for WinForms Controls - CodeProject

:

 

Introduction

    Clicking on most controls in the IDE brings up a selection box around the control with handles for resizing. The dotted rectangle and the handles are drawn outside the bounds of the control, so it cannot be duplicated in the control's paint event because the paint event will not draw outside the control's bounds.   

    On a few controls the user can interact directly on the control with the mouse to alter its properties. The SplitContainer draws dotted lines around each of the panels that only appears at DesignTime and allows the user to drag the splitter back and forth to change the SplitterDistance property. The TableLayoutPanel is another example of this. 

    The ControlDesigner has a way of creating these extra adornments and a way to interact with them. 

Background

    On some of my previous controls I used a custom ControlDesigner to enrich the IDE editing experience. The ControlDesigner lets you add Smart Tags and Verbs to the custom control. I found that the SelectionRules could be modified here. Additional graphics could be painted over the control that only appear during DesignTime by Overriding the OnPaintAdornments Event Sub. The GetHitTest and OnMouse Subs allowed interaction with the control during DesignTime beyond just selecting, moving, and resizing the control. See UITypeEditorsDemo Link Below. 

    The basic idea is that the designer paint and mouse routines only happen during DesignMode. While in DesignMode the control will paint itself in its own OnPaint Sub and then the graphics in the OnPaintAdornments would paint on top of that.

    The GetHitTest determines if the cursor is over a location and returns true or false. In the Controls Mouse Events you would have two situations (behaviors) to deal with.

Sub MouseEvent…

    If DesignMode Then

        'Do this DesignTime Stuff

    Else

        'Do this RunTime Stuff

    End If

 End Sub

    In general this worked well for simple interactions. For more complicated interaction or more control over the mouse behavior the Adorner and Glyph are needed. Back when I was first figuring the ControlDesigner out, I didn’t know about separate Adorners and Glyphs. Even today there is little information to be found on these things. I wanted to be able to have multiple adornments to paint over the control for different situations. First the ControlDesigner only has OnMouseEnter, OnMouseHover, and OnMouseLeave. Trying to determine mouse interaction was difficult or impossible so in my search for a solution I stumbled on the Adorner Class.

Overriding the Designers OnPaintAdornments only gives one area to paint in. I could have used If Then Else to paint different things, but using one or more Adorners to paint one or more Glyphs offers much more control and versatility. The Adorner is basically a container to hold a collection of Glyphs and each Glyph is what to draw and how it behaves. I also like that the control itself no longer has to deal with the user interaction for the adornment. The mouse events for the control are exclusively in the control and the mouse events for the adornment are exclusively in the Glyph’s behavior and are also exclusive to any other Glyphs.

The Adorner Class has three basic properties:

  • BehaviorService – Attaches a reference to  System.Windows.Forms.Design.Behavior.BehaviorService
  • Enabled – True or False property to determine if the Adorner can be seen and interacted with
  • Glyphs – Holds a collection of Glyphs.

 

The Glyph Class has two basic properties and two other main elements:

  • Behavior – Reference to which Behavior Class to use
  • Bounds – Rectangle for the location and size of the Glyph
  • Paint holds the details of the Graphics
  • GetHitTest – determines if the mouse is over a location and returns a Cursor

 

Using the code

SimpleControl

Unselected Control

Selected Control

Here is the totally useless SimpleControl.

It has :

  • The SimpleControlDesigner Attached
  • One String Property called Message with Browsable set to false so it can only be changed in code
  • The Paint is overridden to paint the Message text on the control
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

The SimpleControl illustrates a very basic use of Adornments and how to update a control's property from the adornment at designtime. 

SimpleControl - SimpleControlDesigner - UselessAdorner - UselessGlyph - UselessBehavior

Selecting the Control signals the ControlDesigner to enable the Adorner which paints the Glyph and if the mouse is clicked in one of the hotspots the Message property of the control is updated and re-painted in the lower right corner of the control.

NOTE: if you are starting your own new project the System.Design.dll has to be manually added first.

SimpleControlDesigner

This SimpleControlDesigner is very basic.

  • An Adorner, ISelectionService, and a BehaviorService are declared.
  • In the Initialize Sub
    • The reference to the Selection and Behavior services are set. 
    • The adorner is created and added to the Behavior Service
    • The Glyph with its behavior is added to the Adorner's Glyphs collection
  • Lastly the Dispose is overridden to remove the adorner from the behavior service. 
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

Here is where references are set up to pass along the Behavior and Selection services again, plus we get a reference to the Adorner and the associated SimpleControl and SimpleControlDesigner. 

We also wire-up the SelectionService's SelectionChanged and the RelatedControl's Move Events.

The SelectionChanged turns the Adorner on and off depending on the state of selection if the SelectionService's PrimarySelection matches the relatedControl. If this is not checked then all simpleControlAdorners will show at the same time. 

The move event forces the Adorner to invalidate when the control is moved with the Arrow Keys.

Bounds Property

The BehaviorService has a function ControlToAdornerWindow to get the position of the control's window coordinates so we know where to paint the 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 Function

Unlike the GetHitTest in the ControlDesigner this one returns a Cursor instead of Boolean. If it Returns Nothing then nothing happens, but if it returns a cursor then the cursor changes and the Mouse Events will trigger in the 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
Overridden Paint Sub

Using GDI+ Graphics Paint the Glyph over the Control.

UselessBehavior

New Sub

The Designer reference is passed through here. 

OnMouse...Events

Any Mouse behaviors are defined here. For this control there is only 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

The SampleObject is a more complex example that uses five diferent Adorners that turn on at different times based on the interaction with the ChooseAdorner which is always active. To activate the other Adorners click P- for Padding, C- for Corners, F- for Focal Point, and L- for Line from the ChooseAdorner.

Padding

The Padding Adorner lets you grab the edges of the rectangle and change the Padding property with the mouse.

Corners

The Corners Adorner shows a slider bar to adjust the roundness of the corners 

Focal Point

The Focal Point Adorner has two hotspots. One to drag the center point for the circle and one to move left or right to change the radius property.

Line

The Line Adorner has two hot spots. One for each end of the line to move the points around.

I am just getting into what can be done with these, and with such little info out there it will mostly be experimentation going forward.

Origionally I was Enabling and Unenabling each Adorner as needed, but I found as multiple controls are added to the form selecting the controls began to drag and slow down as it had to "look" at every control whether or not its own selection had actually changed. If some of the controls were on a Panel or GroupBox it became iven slower and the controls became very flickery. The selection service is running through all the controls not just the selected and deselected ones and I haven't figured out how to stop it from affecting the controls outside the actual selected and deselected ones. Instead I am now leaving them all on and using a flag to bypass the GetHitTest and Paint if not flagged. (This was Update two)

This helped and seemed to be the fix, but when I tried to apply this to one om my real custom controls selecting a control slowed down to a crawl. Then I noticed the remark in the SelectionChanged Event information on MSDN.


Remarks

Minimize processing when handling this event, because processing that occurs within this event handler can significantly affect the overall performance of the form designer.


 

In Update Three I added a custom property to track the selected control which sees to have blocked the extra processing. The property is not Browsable in the PropertyGrid or in the Intelisense choices in the Editor.

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

Now when the primary control is selected its DesignerSelected property is set to True. If it is not the Primary then it checkes its DesignerSelected Property and if it is true then it will disable that Adorner and leave the other ones alone.

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

So far this is working...

Reference Links

How to use UITypeEditors, Smart Tags, ControlDesigner Verbs, and Expandable Properties to make design-time editing easier.
UITypeEditorsDemo[^]

For a practical example of Adorners I added them to the Custom Button control.
CButton[^]

 

History

  • Version 1.0.0 - April 1 2015
  • Version 2.0.0 - April 3 2015
    • Fixed selection drag especially when in a container
  • Version 3.0.0 - April 5 2015
    • Added DesignerSelected Property to better fix the selection drag problem.