Adding Buttons
The first thing we want the user to be able to do is to add buttons. We will make use of a designer verb to do this. For an explanation of designer verbs, see my article Introduction to Designers. We only want one verb, and we'll simply title it "Add Button".
In the code that executes when the user activates this verb, we have to create a button and add it to the collection. This may sound trivial, but this is one of those times when we have to play nice with the other designers. If we simply created a button and added it to the collection, how would the IDE know anything had changed? How would it know WHAT had changed, so the user can undo/redo?
Enter the DesignerTransaction
class. When you perform a significant action (or group of actions)
to something on the design surface, you should wrap it in a transaction. Every transaction has a
friendly name, which appears on the dropdown by the Undo/Redo buttons in the host environment.
Also, every distinct change to make to an object (in this case, the Buttons collection) needs to be
wrapped with a call to OnComponentChanging
and OnComponentChanged
, on the IComponentChangeService
.
Lastly, you should not attempt to instantiate a ColourButton
directly - let the designer host
(another service we'll use) do the creating for you. This ensures that the object is on the design
surface, and it keeps everyone happy. If the ColourButton
class had a designer itself, that would
get created too. I know this all sounds like a lot of work, and it is, but you get used to it and
most of it is boilerplate that can be copy/pasted easily.
VB.NET
Public Overrides ReadOnly Property Verbs() As _
System.ComponentModel.Design.DesignerVerbCollection
Get
Dim v As New DesignerVerbCollection()
'Commands to insert and add buttons
v.Add(New DesignerVerb("&Add Button", AddressOf OnAddButton))
Return v
End Get
End Property
Private Sub OnAddButton(ByVal sender As Object, ByVal e As EventArgs)
Dim button As ColourButton
Dim h As IDesignerHost = DirectCast(GetService(GetType(IDesignerHost)), IDesignerHost)
Dim dt As DesignerTransaction
Dim c As IComponentChangeService = DirectCast(getservice(GetType _
(IComponentChangeService)), IComponentChangeService)
'Add a new button to the collection
dt = h.CreateTransaction("Add Button")
button = DirectCast(h.CreateComponent(GetType(ColourButton)), ColourButton)
c.OnComponentChanging(MyControl, Nothing)
MyControl.Buttons.Add(button)
c.OnComponentChanged(MyControl, Nothing, Nothing, Nothing)
dt.Commit()
End Sub
C#
public override System.ComponentModel.Design.DesignerVerbCollection Verbs
{
get
{
DesignerVerbCollection v = new DesignerVerbCollection();
// Verb to add buttons
v.Add(new DesignerVerb("&Add Button", new EventHandler(OnAddButton)));
return v;
}
}
private void OnAddButton(object sender, System.EventArgs e)
{
ColourButton button;
IDesignerHost h = (IDesignerHost) GetService(typeof(IDesignerHost));
DesignerTransaction dt;
IComponentChangeService c = (IComponentChangeService)
GetService(typeof(IComponentChangeService));
// Add a new button to the collection
dt = h.CreateTransaction("Add Button");
button = (ColourButton) h.CreateComponent(typeof(ColourButton));
c.OnComponentChanging(MyControl, null);
MyControl.Buttons.Add(button);
c.OnComponentChanged(MyControl, null, null, null);
dt.Commit();
}
Note that even after writing all that, our implementation isn't quite complete yet - you can add buttons, and the undo and redo buttons will remove the button from the design surface ok but they won't remove the button from the Buttons collection - we'll come back to that later.
Selecting Buttons
Designers offer a useful method to override, called GetHitTest
. This is passed some coordinates, and
it's up to your logic to let the designer know whether or not to pass the event (usually a click) on
to the control underneath. We will override this method, and see if the mouse cursor is within the
bounds of any of the buttons on the control. If it is, we'll return true.
VB.NET
Protected Overrides Function GetHitTest(ByVal point As System.Drawing.Point) As Boolean
Dim button As ColourButton
Dim wrct As Rectangle
point = MyControl.PointToClient(point)
For Each button In MyControl.Buttons
wrct = button.Bounds
If wrct.Contains(point) Then Return True
Next
Return False
End Function
C#
protected override bool GetHitTest(System.Drawing.Point point)
{
Rectangle wrct;
point = MyControl.PointToClient(point);
foreach (ColourButton button in MyControl.Buttons)
{
wrct = button.Bounds;
if (wrct.Contains(point))
return true;
}
return false;
}
This way, our MouseDown
event in the control will be fired if the user clicks on a button.
In this event, we check if we're in design mode (with the DesignMode
property) and if we are,
find which button the cursor is on. Then we get a reference to ISelectionService
and set the
selection to that button.
VB.NET
Protected Overrides Sub OnMouseDown(ByVal e As System.Windows.Forms.MouseEventArgs)
Dim button As ColourButton
Dim wrct As Rectangle
Dim s As ISelectionService
Dim a As ArrayList
If DesignMode Then
For Each button In Buttons
wrct = button.Bounds
If wrct.Contains(e.X, e.Y) Then
s = DirectCast(GetService(GetType(ISelectionService)), ISelectionService)
a = New ArrayList()
a.Add(button)
s.SetSelectedComponents(a)
Exit For
End If
Next
End If
MyBase.OnMouseDown(e)
End Sub
C#
protected override void OnMouseDown(System.Windows.Forms.MouseEventArgs e)
{
Rectangle wrct;
ISelectionService s;
ArrayList a;
if (DesignMode)
{
foreach (ColourButton button in Buttons)
{
wrct = button.Bounds;
if (wrct.Contains(e.X, e.Y))
{
s = (ISelectionService) GetService(
typeof(ISelectionService));
a = new ArrayList();
a.Add(button);
s.SetSelectedComponents(a);
break;
}
}
}
base.OnMouseDown(e);
}
At this point, clicking on an individual button in the control at design time will select it, and you can even modify its properties in the propertygrid. We've one last piece of code to write before the selection stuff is complete though, and that's filling in the function we created earlier that is called when the selection changes. It's in here that we'll set the highlightedButton variable we created so the selection is indicated visually too.
VB.NET
Friend Sub OnSelectionChanged()
Dim button As ColourButton
Dim newHighlightedButton As ColourButton = Nothing
Dim s As ISelectionService = DirectCast(GetService(GetType _
(ISelectionService)), ISelectionService)
'See if the primary selection is one of our buttons
For Each button In Buttons
If s.PrimarySelection Is button Then
newHighlightedButton = button
Exit For
End If
Next
'Apply if necessary
If Not newHighlightedButton Is highlightedButton Then
highlightedButton = newHighlightedButton
Invalidate()
End If
End Sub
C#
internal void OnSelectionChanged()
{
ColourButton newHighlightedButton = null;
ISelectionService s = (ISelectionService) GetService(typeof(ISelectionService));
// See if the primary selection is one of our buttons
foreach (ColourButton button in Buttons)
{
if (s.PrimarySelection == button)
{
newHighlightedButton = button;
break;
}
}
// Apply if necessary
if (newHighlightedButton != highlightedButton)
{
highlightedButton = newHighlightedButton;
Invalidate();
}
}
We're almost there. We can now add the control to a form, use the designer verb to add buttons, and select those buttons visually, changing their properties in the propertygrid.
Comments