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
Else
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
Me.behaviorSvc.Adorners.Remove(Me.UselessAdorner)
End If
End If
MyBase.Dispose(disposing)
End Sub
#End Region
#Region "Init Methods"
Private Sub InitializeServices()
Me.selectionSvc = CType(GetService(GetType(ISelectionService)), ISelectionService)
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.