[ Team LiB ] |
9.2 Custom Component DesignersVisual Studio .NET enables controls to customize the Forms Designer. New commands can be added in the property grid. Resize and move drag operations can be altered, and mouse handling within the control can be customized. Design-time adornments may be added (adornments are editing features such as resize handles). Hit testing can be provided for nonrectangular controls. Controls may elect to act as containers of child controls. Controls may even modify the way that properties are presented in the property panel. All these facilities revolve around supplying a custom component designer. A component designer is a class associated with the control class, whose job is to handle design-time interaction with the development environment. It must derive from the ControlDesigner class, which is defined in the System.Windows.Forms.Design namespace. (In fact, any component may have a designer, not just a control. A non-visual component's designer derives from the ComponentDesigner class, which is defined in the System.ComponentModel.Design namespace.)
All controls are required to have an associated designer. If you do not specify one, your control will just use the default, ControlDesigner. But you can choose a custom designer by applying the DesignerAttribute custom attribute to your class as Example 9-4 shows. (DesignerAttribute is defined in the System.ComponentModel namespace. It is not entirely clear what it is doing in there when the System.ComponentModel.Design namespace would have been the more obvious choice.) Example 9-4. Specifying a custom designer for a class// C# code [DesignerAttribute(typeof(MyControlDesigner))] public class MyControl : System.Windows.Forms.Control { . . . } public class MyControlDesigner : System.Windows.Forms.Design.ControlDesigner { . . . } ' VB code <Designer(GetType(MyControlDesigner))> _ Public Class MyControl Inherits System.Windows.Forms.Control . . . End Class Public Class MyControlDesigner Inherits System.Windows.Forms.Design.ControlDesigner . . . End Class Design-time behavior is customized by overriding various methods or properties of the ControlDesigner class. We will now examine each customizable aspect in turn. 9.2.1 Designer VerbsVisual Studio .NET provides extensive support for modifying control properties of all kinds. It has built-in editors for a wide range of standard property types, and because it uses the PropertyGrid, it is easy to provide custom editors for new types—all the techniques discussed in Chapter 8 for customizing the PropertyGrid work just fine for properties of a control. However, for certain kinds of properties, it can still be cumbersome to use the PropertyGrid to perform frequently used operations. So the Forms Editor allows a component designer to add verbs—custom operations available through a single click in the editor. Consider the built-in TabControl class, which allows several user interface panels to be contained inside a standard tabbed view, such as that used by Windows Explorer's file properties windows. The first thing that a developer is likely to want to do with a newly created TabControl is to add some new panels. This can be done by adding new TabPage objects to the control's TabPages property. TabPages is a collection property, so editing this property displays the standard collection editor dialog, which can be used to add new TabPage objects. The TabControl component's requirements are met by the PropertyGrid. However, it is all rather cumbersome—the developer must first locate the TabPages property in the property grid, then two clicks are required to bring up the collection editor, which is a modal dialog and hence must be dismissed once the new pages have been added. This is somewhat inconvenient, given that developers will almost always need to add new pages whenever they use the TabControl. To make life easier for developers, the TabControl therefore defines two designer verbs, one for adding new panels and one for removing existing panels. Figure 9-1 shows how the property grid displays the verbs for a TabControl. Figure 9-1. A property grid with a verbs panelThe Add Tab and Remove Tab verbs in the central panel in Figure 9-1 are also available through the context menu, as shown in Figure 9-2. Figure 9-2. A context menu with verbsIf your custom control could benefit from a similar one-click interface for common but cumbersome operations, it is easy to add designer verbs. Simply override the Verbs property in your custom designer class, as shown in Examples Example 9-5 and Example 9-6. (Note that this property's type, DesignerVerbCollection, is defined in the System.ComponentModel.Designer namespace.) Example 9-5. Adding designer verbs using C#public class MyControlDesigner : ControlDesigner
{
public override DesignerVerbCollection Verbs
{
get
{
DesignerVerb[] verbs = new DesignerVerb[]
{
new DesignerVerb("Add Panel",
new EventHandler(OnAddPanelVerb)),
new DesignerVerb("Remove Panel",
new EventHandler(OnRemovePanelVerb))
};
return new DesignerVerbCollection(verbs);
}
}
private void OnAddPanelVerb(object sender, EventArgs e)
{
MyControl ctl = (MyControl) this.Control;
. . .
}
private void OnRemovePanelVerb(object sender, EventArgs e)
{
MyControl ctl = (MyControl) this.Control;
. . .
}
}
Example 9-6. Adding designer verbs using VBPublic Class MyControlDesigner Inherits System.Windows.Forms.Design.ControlDesigner Public Overrides ReadOnly Property Verbs() As DesignerVerbCollection Get Dim vrbs(1) As DesignerVerb vrbs(0) = New DesignerVerb("Add Panel", _ AddressOf OnAddPanelVerb) vrbs(1) = New DesignerVerb("Remove Panel", _ AddressOf OnRemovePanelVerb) Return New DesignerVerbCollection(vrbs) End Get End Property Private Sub OnAddPanelVerb(sender As Object, e As EventArgs) Dim ctl As MyControl = DirectCast(Me.Control, MyControl) . . . End Sub Private Sub OnRemovePanelVerb(sender As Object, e As EventArgs) Dim ctl As MyControl = DirectCast(Me.Control, MyControl) . . . End Sub End Class The Verbs property must return a collection of DesignerVerb objects, one for each verb that is to appear on the property page. (The easiest way to create a DesignerVerbCollection is to use the constructor that takes a DesignerVerb[] array, as shown here.) Each DesignerVerb object contains two pieces of information: the name of the verb (as it will appear on the property panel and in the context menu) and the method of the component designer that should be called if the verb is invoked. The method to be invoked must be referred to with an EventHandler delegate.
9.2.1.1 DesignerVerb propertiesThe DesignerVerb object has properties that allow you to modify the appearance of your verbs. For example, if you set the Enabled property to false, the verb will be grayed out. Setting the Checked property to true will cause a tick to appear beside the verb on the context menu. (It has no effect on its appearance in the property grid.) You can set these properties when you create the verbs in your Verbs property. You can also modify the properties later on, and the Forms Designer will track these changes. For example, if you change the Enabled property of a verb while your control is selected, the verb's appearance will change appropriately in the property panel. However, every time your control is deselected and reselected, the Designer will read your Verbs property again—it does not cache verbs between selections. This means that any changes you make to your verbs' properties will be lost when the selection changes unless you make your Verbs property return the same objects every time. Although it is possible to write a Verbs property that returns the same set of verbs every time, you should not rely on this technique. The Forms Designer reserves the right to destroy your component designer object at any time and create a new one as needed. For example, if the user closes the Designer window for the form that contains your control and then reopens it, a new instance of your designer class will be created. You should therefore make sure that your Verbs property always creates DesignerVerb objects that are appropriately initialized to be consistent with the control's state. And in general, your designer should be written to assume that it might be destroyed and recreated at any time, and should therefore not rely on being able to store state between operations. 9.2.2 Selection and ResizingWhen writing a custom component designer for a control, your class will normally derive from the ControlDesigner class. This class provides the standard support for selecting, moving, and resizing controls with the mouse. You can influence the way in which these operations work by overriding certain properties and methods. 9.2.2.1 Hit testingBy default, a control can be selected by clicking anywhere inside its bounding rectangle. Because most controls are rectangular, this is reasonable behavior, but for controls with a more unusual shape, it can be confusing. So the Designer lets you modify this behavior by overriding the GetHitTest method in your control designer class. The Designer will call the GetHitTest method repeatedly whenever the mouse pointer is over your control. It uses the return value to decide what kind of mouse cursor to display—if the method returns true, the Designer will display the four-way cursor to indicate that the control can be selected and moved. If the method returns false, the default mouse cursor will be displayed to indicate that the pointer is not considered to be over any control right now. GetHitTest will also be called when the mouse is clicked on your control to determine whether to select the control or not. Examples Example 9-7 and Example 9-8 show a simple custom control that draws an ellipse in its client area, and a corresponding designer with a GetHitTest method that only returns true when the mouse pointer is over the ellipse. Example 9-7. A control designer with hit testing using C#[Designer(typeof(EllipseDesigner))] public class EllipseControl : System.Windows.Forms.Control { public EllipseControl() { SetStyle(ControlStyles.ResizeRedraw, true); } protected override void OnPaint(PaintEventArgs pe) { using (Brush b = new SolidBrush(ForeColor)) { pe.Graphics.FillEllipse(b, ClientRectangle); } base.OnPaint(pe); } } public class EllipseDesigner : ControlDesigner { protected override bool GetHitTest(System.Drawing.Point point) { // Avoid divide-by-zero problems with zero-sized controls if (this.Control.Width == 0 || this.Control.Height == 0) return true; // Map point from screen to client coordinates. PointF p = this.Control.PointToClient(point); // Test for containment by ellipse. double w = this.Control.Width; double h = this.Control.Height; double ratio = w / h; double sx = p.X - w/2; double sy = (p.Y - h/2)*ratio; return (sx*sx + sy*sy) >= w*w/4.0; } } Example 9-8. A control designer with hit testing using VBImports System Imports System.ComponentModel Imports System.Drawing Imports System.Windows.Forms Imports System.Windows.Forms.Design <Designer(GetType(EllipseDesigner))> _ Public Class EllipseControl Inherits System.Windows.Forms.Control Public Sub New() SetStyle(ControlStyles.ResizeRedraw, True) End Sub Protected Overrides Sub OnPaint(pe As PaintEventArgs) Dim b As Brush = New SolidBrush(ForeColor) Try pe.Graphics.FillEllipse(b, ClientRectangle) Finally Dim disp As IDisposable If TypeOf b Is IDisposable Then disp = b disp.Dispose() End If End Try MyBase.OnPaint(pe) End Sub End Class Public class EllipseDesigner : Inherits ControlDesigner Protected Overrides Function GetHitTest( _ point As System.Drawing.Point) As Boolean ' Avoid divide-by-zero problems with 0-sized controls If Me.Control.Width = 0 Or Me.Control.Height = 0 Then Return True End If ' Map point from screen to client coordinates. Dim p As PointF = _ Point.op_Implicit(Me.Control.PointToClient(point)) ' Test for containment by ellipse. Dim w As Double = Me.Control.Width Dim h As Double = Me.Control.Height Dim ratio As Double = w / h Dim sx As Double = p.X - w/2 Dim sy As Double = (p.Y - h/2)*ratio Return (sx*sx + sy*sy) >= w*w/4.0 End Function End Class Examples Example 9-7 and Example 9-8 illustrate a curious feature of GetHitTest. The Point that is passed as a parameter is relative to the top-left corner of the screen. (This is at odds with what the documentation claims at the time this book went to press—it says that the Point will be relative to the top-left corner of the control.) This means that the first thing we must do is convert the Point from screen coordinates to control coordinates. Fortunately, the Control class has a built-in method for doing this: PointToClient. The remainder of the method simply calculates whether the point is contained within the ellipse.
9.2.2.2 Resizing and movingThe ControlDesigner class provides automatic support for resizing and moving controls. You can control this facility by overriding the SelectionRules property in your own designer class. You must return some combination of the values defined in the SelectionRules enumeration, which is defined in the System.Windows.Forms.Design namespace. There is no way to take complete control of the resizing and moving process unless you are prepared to disable the built-in support completely and draw your own adornments. (Returning SelectionRules.None from the SelectionRules property will turn off the standard support, and the next section describes how to add your own adornments. But even then, you will be restricted to drawing adornments that lie within the control's client rectangle.) However, you will normally be able to achieve what you require just by choosing a more restrictive set of selection rules than the default of SelectionRules.AllSizeable | SelectionRules.Moveable (SelectionRules.AllSizeable Or SelectionRules.Moveable in VB), which allows the control to be moved and to be resized in all directions. If you want the designer to draw a border on your control, then no matter what other values you choose from the SelectionRules enumeration, you must include SelectionRules.Visible. If you just specify this in conjunction with SelectionRules.Moveable, your control will have a fixed size, but will be able to be moved around the form. You can also selectively enable sizing of individual edges using the TopSizeable, BottomSizeable, LeftSizeable, and RightSizeable enumeration members. Example 9-9 shows an example SelectionRules property implementation that does not allow the control to be moved, and only allows its left edge to be resized. Example 9-9. Modifying support for moving and resizing// C# code public override SelectionRules SelectionRules { get { return SelectionRules.Visible | SelectionRules.LeftSizeable; } } ' VB code Public Overrides ReadOnly Property SelectionRules() _ As SelectionRules Get Return SelectionRules.Visible Or _ SelectionRules.LeftSizable End Get End Property Figure 9-3 shows the result of Example 9-9. This is how the Forms Designer displays such a control when it is selected. The visual cue is not especially obvious. In case you missed it, the Designer indicates the resizable edge by coloring its center handle white, while coloring the handles that cannot be dragged pale gray. (It is slightly more obvious when using the control in the Designer, because the mouse cursor only changes into a resize cursor when it is over that handle.) Figure 9-3. A control with one resizable edge9.2.3 AdornmentsAn adornment is a user interface feature that is only painted on a control at design time. (Selection outlines and resize handles are examples of built-in adornments.) Adornments are not drawn by the control—they are supplied by the control designer class, enabling you to add extra design-time handles appropriate to your class. To show how to display adornments, we need a control that can make use of extra resize handles beyond the standard ones. We will use a control that displays a box with rounded edges. Examples Example 9-10 and Example 9-11 show the complete source for such a control, and it is shown in action in Figure 9-4. Figure 9-4. A rounded box controlThere is nothing unusual about this control—it uses standard techniques already discussed in previous chapters. It provides a single property called CornerSize (along with the usual associated change notification event and overridable OnCornerSizeChanged method). This property determines how large the curved corners are. This property can be edited using the property grid in the normal way, but we will provide a custom designer that draws a drag handle adornment to allow the corner size to be modified by dragging with the mouse. Example 9-10. A rounded box control using C#[Designer(typeof(RoundedBoxDesigner))] public class RoundedBoxControl : System.Windows.Forms.Control { public RoundedBoxControl() { SetStyle(ControlStyles.ResizeRedraw, true); } [Category("Appearance")] [DefaultValue(10)] public int CornerSize { get { return cornerSizeVal; } set { if (cornerSizeVal != value) { cornerSizeVal = value; OnCornerSizeChanged(EventArgs.Empty); Refresh(); } } } private int cornerSizeVal = 10; [Category("Property Changed")] public event EventHandler CornerSizeChanged; protected virtual void OnCornerSizeChanged(EventArgs e) { if (CornerSizeChanged != null) CornerSizeChanged(this, e); } protected override void OnPaint(PaintEventArgs pe) { // Truncate rounded corner sizes so that the // corners aren't larger than the box. int cwidth = cornerSizeVal*2 > Width ? Width : cornerSizeVal*2; int cheight = cornerSizeVal*2 > Height ? Height : cornerSizeVal*2; Rectangle corner = new Rectangle(0, 0, cwidth, cheight); using (GraphicsPath gp = new GraphicsPath()) { // GraphicsPath.AddArc complains about // zero-sized arcs, so just use a // standard Rectangle in that case. if (cwidth == 0 || cheight == 0) { Rectangle cr = ClientRectangle; if (cr.Width != 0) cr.Width -= 1; if (cr.Height != 0) cr.Height -= 1; gp.AddRectangle(cr); } else { gp.AddArc(corner, 180, 90); corner.X = Width - 1 - cwidth; gp.AddArc(corner, 270, 90); corner.Y = Height - 1 - cheight; gp.AddArc(corner, 0, 90); corner.X = 0; gp.AddArc(corner, 90, 90); gp.CloseFigure(); } using (Pen p = new Pen(ForeColor, 1)) { pe.Graphics.DrawPath(p, gp); } } // Calling the base class OnPaint base.OnPaint(pe); } } Example 9-11. A rounded box control using VB<Designer(GetType(RoundedBoxDesigner))> _ Public Class RoundedBoxControl Inherits System.Windows.Forms.Control Private cornerSizeVal As Integer = 10 Public Sub New() SetStyle(ControlStyles.ResizeRedraw, True) End Sub <Category("Appearance"), _ DefaultValue(10)> _ Public Property CornerSize() As Integer Get Return cornerSizeVal End Get Set If cornerSizeVal <> Value Then cornerSizeVal = Value OnCornerSizeChanged(EventArgs.Empty) Refresh() End If End Set End Property <Category("Property Changed")> _ Public Event CornerSizeChanged(sender As Object, _ e As EventArgs) Protected Overridable Sub OnCornerSizeChanged( _ e As EventArgs) RaiseEvent CornerSizeChanged(Me, e) End Sub Protected Overrides Sub OnPaint(pe As PaintEventArgs) ' Truncate rounded corner sizes so that the ' corners aren't larger than the box. Dim cwidth As Integer If cornerSizeVal*2 > Width Then cwidth = Width Else cwidth = cornerSizeVal*2 End If Dim cheight As Integer If cornerSizeVal*2 > Height Then cheight = Height Else cheight = cornerSizeVal*2 End If Dim corner As New Rectangle(0, 0, cwidth, cheight) Dim gp As New GraphicsPath() Try ' GraphicsPath.AddArc complains about ' zero-sized arcs, so just use a ' standard Rectangle in that case. If cwidth = 0 Or cheight = 0 Then Dim cr As Rectangle = ClientRectangle If cr.Width <> 0 Then cr.Width -= 1 If cr.Height <> 0 Then cr.Height -= 1 gp.AddRectangle(cr) Else gp.AddArc(corner, 180, 90) corner.X = Width - 1 - cwidth gp.AddArc(corner, 270, 90) corner.Y = Height - 1 - cheight gp.AddArc(corner, 0, 90) corner.X = 0 gp.AddArc(corner, 90, 90) gp.CloseFigure() End If Dim p As New Pen(ForeColor, 1) Try pe.Graphics.DrawPath(p, gp) Finally Dim disp As IDisposable If TypeOf p Is IDisposable Then disp = p disp.Dispose() End If End Try Finally Dim disp As IDisposable If TypeOf disp Is IDisposable Then disp = gp disp.Dispose() End If End Try ' Calling the base class OnPaint MyBase.OnPaint(pe) End Sub End Class To draw a drag handle, we simply provide an appropriate designer that overrides the OnPaintAdornments method. This designer class is shown in Examples Example 9-12 and Example 9-13. (Note that the class definition in Examples Example 9-10 and Example 9-11 is marked with the Designer custom attribute. This is how Visual Studio .NET knows to use our RoundedBoxDesigner class.) Example 9-12. Drawing grab handle adornments using C#public class RoundedBoxDesigner : ControlDesigner { private const int grabSize = 7; protected override void OnPaintAdornments(PaintEventArgs pe) { Rectangle grabRect = GetGrabRectangle(); ControlPaint.DrawGrabHandle(pe.Graphics, grabRect, true, true); } private Rectangle GetGrabRectangle() { RoundedBoxControl ctl = (RoundedBoxControl) Control; return new Rectangle(ctl.CornerSize - grabSize/2, 0, grabSize, grabSize); } . . . } Example 9-13. Drawing grab handle adornments using VBPublic Class RoundedBoxDesigner : Inherits ControlDesigner Private Const grabSize As Integer = 7 Protected Overrides Sub OnPaintAdornments( _ pe As PaintEventArgs) Dim grabRect As Rectangle = GetGrabRectangle() ControlPaint.DrawGrabHandle(pe.Graphics, grabRect, _ True, True) End Sub Private Function GetGrabRectangle() As Rectangle Dim ctl As RoundedBoxControl = DirectCast(Control, RoundedBoxControl) Return New Rectangle(CInt(ctl.CornerSize - grabSize/2), _ 0, grabSize, grabSize) End Function . . . End Class With this designer in place, the control will now have an extra grab handle at design time, as shown in Figure 9-5. Figure 9-5. The RoundedBoxControl at design timeThe designer class in Examples Example 9-12 and Example 9-13 is not complete. As it stands, it only draws the grab handle. It does nothing to manage clicks or drags on the handle. To make the handle useful, we must override more methods. First we will want to provide feedback with the mouse cursor—we should display the left-right resize cursor when the mouse is over our drag handle to indicate how it can be moved. This is done by overriding the designer class's OnSetCursor method, as shown in Examples Example 9-14 and Example 9-15. Example 9-14. Modifying the mouse cursor at design time using C#protected override void OnSetCursor() { Point cp = Control.PointToClient(Cursor.Position); if (GrabHitTest(cp)) { Cursor.Current = Cursors.SizeWE; } else base.OnSetCursor(); } private bool GrabHitTest(Point p) { Rectangle grabRect = GetGrabRectangle(); return grabRect.Contains(p); } Example 9-15. Modifying the mouse cursor at design time using VBProtected Overrides Sub OnSetCursor() Dim cp As Point = Control.PointToClient(Cursor.Position) If GrabHitTest(cp) Then Cursor.Current = Cursors.SizeWE Else MyBase.OnSetCursor() End If End Sub Private Function GrabHitTest(p As Point) As Boolean Dim grabRect As Rectangle = GetGrabRectangle() Return grabRect.Contains(p) End Function Note that OnSetCursor does not pass the mouse cursor's position. We must therefore retrieve it from the Cursor class directly. The Cursor class's Position property returns the mouse position in screen coordinates, so we need to translate these into control coordinates using the PointToClient method. Then we test to see if the mouse is over the handle. (The hit test logic has been factored out into a separate method, GrabHitTest, because we will also need to perform hit testing in another method shortly. This in turn uses the GetGrabRectangle method we defined earlier, which is also used to determine where the grab handle is drawn.) Our control will now provide feedback at design time when the user moves the mouse over our drag handle. But we still need to handle the drag operation itself. To do this, we must override three methods: OnMouseDragBegin, OnMouseDragMove, and OnMouseDragEnd. The code is fairly straightforward, with only two minor complications. First, we need to be able to reset the property to its original value if the drag is cancelled. Second, it is good practice to make sure that if the user doesn't click dead in the center of the drag handle, we don't end up making the handle leap to the clicked location. (Naïve handling of the OnMouseDragMove event would cause this to happen—we are using the offset field to avoid this.) Examples Example 9-16 and Example 9-17 show the drag handling code. Example 9-16. Handling adornment mouse events using C#private int oldCornerSize; // Used for handling cancellation private int offset; private bool dragging; protected override void OnMouseDragBegin(int x, int y) { Point dp = Control.PointToClient(new Point(x, y)); if (GrabHitTest(dp)) { RoundedBoxControl ctl = (RoundedBoxControl) Control; oldCornerSize = ctl.CornerSize; offset = oldCornerSize - dp.X; dragging = true; } else base.OnMouseDragBegin(x, y); } protected override void OnMouseDragMove(int x, int y) { if (dragging) { Point dp = Control.PointToClient(new Point(x, y)); int newCornerSize = dp.X - offset; if (newCornerSize < 0) newCornerSize = 0; RoundedBoxControl ctl = (RoundedBoxControl) Control; ctl.CornerSize = newCornerSize; } else base.OnMouseDragMove(x, y); } protected override void OnMouseDragEnd(bool cancel) { if (dragging) { RoundedBoxControl ctl = (RoundedBoxControl) Control; if (cancel) { ctl.CornerSize = oldCornerSize; } else { // Update property in property grid PropertyDescriptor pd = TypeDescriptor.GetProperties(typeof(RoundedBoxControl)) ["CornerSize"]; pd.SetValue(ctl, ctl.CornerSize); } } dragging = false; // Always call base class. base.OnMouseDragEnd(cancel); } Example 9-17. Handling adornment mouse events using VBPrivate oldCornerSize As Integer ' Used for handling cancellation Private offset As Integer Private dragging As Boolean Protected Overrides Sub OnMouseDragBegin(x As Integer, y As Integer) Dim dp As Point = Control.PointToClient(New Point(x, y)) If GrabHitTest(dp) Then Dim ctl As RoundedBoxControl = DirectCast(Control, RoundedBoxControl) oldCornerSize = ctl.CornerSize offset = oldCornerSize - dp.X dragging = True Else MyBase.OnMouseDragBegin(x, y) End If End Sub Protected Overrides Sub OnMouseDragMove(x As Integer, y As Integer) If dragging Then Dim dp As Point = Control.PointToClient(New Point(x, y)) Dim newCornerSize As Integer = dp.X - offset If newCornerSize < 0 Then newCornerSize = 0 Dim ctl As RoundedBoxControl = DirectCast(Control, RoundedBoxControl) ctl.CornerSize = newCornerSize Else MyBase.OnMouseDragMove(x, y) End If End Sub Protected Overrides Sub OnMouseDragEnd(cancel As Boolean) If dragging Then Dim ctl As RoundedBoxControl = DirectCast(Control, _ RoundedBoxControl) If cancel Then ctl.CornerSize = oldCornerSize Else ' Update property in property grid Dim pd As PropertyDescriptor = _ TypeDescriptor.GetProperties( _ GetType(RoundedBoxControl))("CornerSize") pd.SetValue(ctl, ctl.CornerSize) End If End If dragging = False ' Always call base class. MyBase.OnMouseDragEnd(cancel) End Sub Note that all three methods defer to the base class implementation when they are not handling the drag operation (i.e., when the user clicks somewhere other than on the drag handle). This is necessary to make sure that the control can still be moved in the Designer using normal drag and drop. Also note that the OnMouseDragEnd method always calls the base class method, regardless of whether the handle was being dragged or not. This is necessary because otherwise the drag operation will not be completed correctly, and the Forms Designer will start to malfunction. The OnMouseDragEnd method has some slightly strange-looking code that runs when the drag is not cancelled. It obtains a PropertyDescriptor object for the RoundedBoxControl class's CornerSize property, and then uses this to set that property to the value it is already set to. On the face of it, this may seem pointless. However, despite the fact that the CornerSize property raises property change notifications, the property grid appears not to detect the change. Pushing the update through the PropertyDescriptor causes the property grid to refresh its display of the CornerSize property correctly. 9.2.4 ContainmentAs we saw in Chapter 3, all controls are able to contain child controls. But it doesn't always make sense for a control to act as a parent. For example, although you can write code that puts child controls inside a Button, the results are not helpful. Fortunately, the Forms Designer prevents you from placing child controls inside controls that are not designed to act as parents. It only allows children to be added to controls for which it is appropriate, such as Panel or GroupBox. By default, any controls we write will not be treated as containers by the Forms Designer. If you try to drop a control inside one of your custom controls, the new control's parent will be the form, not your control. However, it is easy to make your control behave like a Panel: simply give it a designer that derives from the ParentControlDesigner class. ParentControlDesigner derives from ControlDesigner and provides all the same functionality, and it also signals to the Forms Designer that the control can act as a container. If you do not require any special design-time behavior other than the ability to act as a parent, it is sufficient to choose the ParentControlDesigner class itself as your designer. As Example 9-18 illustrates, there is no need to derive your own designer class. Example 9-18. A simple parent control// C# code
[Designer(typeof(ParentControlDesigner))]
public class MyParentControl : Control
{
. . .
}
' VB code
<Designer(GetType(ParentControlDesigner))> _
Public Class MyParentControl : Inherits Control
. . .
End Class
If you need to supply a designer class for other design-time features, such as painting adornments, you can simply change its base class to be ParentControlDesigner. Example 9-19 shows a modified version of the designer class for our RoundedBoxControl, previously shown in Examples Example 9-12 and Example 9-13. We have changed it to inherit from ParentControlDesigner, which will cause the Forms Designer to allow child controls to be added to it. Example 9-19. A parent control designer// C# code
public class RoundedBoxDesigner : ParentControlDesigner
{
As before
. . .
}
' VB code
Public Class RoundedBoxDesigner
Inherits ParentControlDesigner
. . . As before
End Class
You can be selective about which controls you contain. Your designer class can override the CanParent method. The Forms Designer will call this when the user drags a control over your control. If this method returns false, the Designer will display the no entry mouse cursor to indicate that the control being dragged cannot be dropped into your control. CanParent is an overloaded method. You must override the overload that takes a Control as a parameter. This will be called when a control that is already on the form is being dragged around. (It is not clear when the other overload, which takes a ControlDesigner, is called. You might expect it to be called when a new instance is dragged from the Toolbox. But this is not the case; there appears to be no way of controlling which types of new instances can be added to your control.) 9.2.5 Metadata FilteringIn the previous chapter, we saw how the PropertyGrid control provides a virtual view of an object's properties: although it relies on reflection to determine what properties are present, it has extensibility hooks that allow us to modify what the user sees. We exploited this by writing a type converter to support localization of property names. Because Visual Studio .NET uses the PropertyGrid to edit control properties, we have the same flexibility for the way our controls are presented at design time. On top of this, the Forms Designer provides us with some extra support for common ways of modifying and filtering properties without having to go to the trouble of writing a type converter. If your control already has a designer class associated with it, you can use this to perform many of the tricks that would otherwise require a type converter. In particular, you can hide certain properties, rename them, intercept reads and writes, and even add new properties.
At the center of this mechanism are six methods. Three of these are intended to let you add new properties, events and attributes, and they are, respectively, PreFilterProperties , PreFilterEvents, and PreFilterAttributes. Each is passed a dictionary into which it can add descriptors. (The descriptors you add should be of type PropertyDescriptor, EventDescriptor, and AttributeDescriptor, respectively. These are all defined in the System.ComponentModel namespace.) This dictionary will already contain entries for all the component's real properties, events, or attributes. (Or if the component has an associated type converter, the dictionary will contain whatever that returned.) But the designer class has the option to add to these. The remaining three methods are PostFilterProperties , PostFilterEvents, and PostFilterAttributes. These are passed the same dictionary as before, but this time the method is allowed to remove or modify entries. (In practice, there is currently not much difference between the Pre... and Post... methods—the Post... methods are called directly after the Pre... methods, and you can do whatever you like to the dictionary in either. But you should stick to the rule of only adding entries in the Pre... methods, and performing any modifications or removals in the Post... methods, just in case a future version of the Forms Designer decides to enforce this.) Examples Example 9-20 and Example 9-21 show an implementation of PreFilterProperties that adds an extra property, Fooness, to the control at design time. Although the underlying control will not have such a property, it will still appear in the property panel for the control because the designer class has added it to the dictionary of property descriptors. Note that the designer class itself has provided the implementation of the Fooness property to which the descriptor refers. (The designer class might then use this property to modify its design-time behavior.) Example 9-20. Adding a design-time property using C#protected override void PreFilterProperties(IDictionary properties) { base.PreFilterProperties(properties); properties["Fooness"] = TypeDescriptor.CreateProperty( typeof(MyDesigner), "Fooness", typeof(bool), CategoryAttribute.Design, DesignOnlyAttribute.Yes); } public bool Fooness { get { return fooVal; } set { fooVal = value; } } private bool fooVal; Example 9-21. Adding a design-time property using VBProtected Overrides Sub PreFilterProperties(properties As IDictionary) MyBase.PreFilterProperties(properties) properties("Fooness") = TypeDescriptor.CreateProperty( _ GetType(MyDesigner), _ "Fooness", _ GetType(Boolean), _ CategoryAttribute.Design, _ DesignOnlyAttribute.Yes) End Sub Public Property Fooness As Boolean Get Return fooVal End Get Set fooVal = Value End Set End Property The PropertyDescriptor is created using the CreateProperty factory method supplied by the TypeDescriptor class. As well as specifying the type that really implements the property (the designer class, in this case) along with the name and type of the property, we can also optionally specify a list of custom attributes. (CreateProperty takes a variable length argument list, so you can supply as many attributes as you like.) In this case, we have specified a CategoryAttribute (this will determine which category the property appears under in the PropertyGrid) and the DesignOnlyAttribute, which informs the development environment that this is a design-time property, which it should not attempt to set at runtime. All properties added this way should specify DesignOnlyAttribute.Yes, because otherwise, the Forms Designer will generate code that attempts to set these properties at runtime. Such code will not compile, because these design-time properties are not available at runtime.
9.2.5.1 Shadow propertiesA common design-time requirement is to prevent certain properties from being set at design time. The most obvious example is the Visible property. If the user sets a control's Visible property to false, we don't really want to make the control invisible in the Designer, because this would make it hard to edit. We want the control to remain visible in the Designer, but for the Visible property to be honored at runtime. One solution would be for controls to ignore their Visible property at design time. However, the ControlDesigner class provides a more elegant solution than this, known as shadow properties, that doesn't require special design-time behavior from the control class. The technique for making sure that the underlying property is only set to the specified value at runtime is to replace its PropertyDescriptor in the PostFilterProperties method with one that refers to a property supplied by the designer class. (So it is very similar to the technique of adding a design-time property shown in Examples Example 9-20 and Example 9-21.) This allows the designer class to remember the user's intended setting for the property without having to set that property on the control itself. The property will be set to the intended value at runtime, but not at design time. To save you from having to declare fields for each of the properties you wish to shadow in your designer class, the base ControlDesigner class provides a protected property called ShadowProperties. This is a collection class that holds name/value pairs. As a convenience, if you try to retrieve a value for a name that is not in the collection, it will retrieve the property of the same name from the control itself, which means you don't need to initialize any of your shadow properties either. This makes the code for adding a shadow property relatively simple, as Examples Example 9-22 and Example 9-23 show. Example 9-22. Shadowing the Anchor property using C#protected override void PostFilterProperties( System.Collections.IDictionary properties) { base.PostFilterProperties(properties); properties["Anchor"] = TypeDescriptor.CreateProperty( typeof(RoundedBoxDesigner), (PropertyDescriptor) properties["Anchor"]); } public AnchorStyles Anchor { get { return (AnchorStyles) ShadowProperties["Anchor"]; } set { ShadowProperties["Anchor"] = value; } } Example 9-23. Shadowing the Anchor property using VBProtected Overrides Sub PostFilterProperties( _ properties As System.Collections.IDictionary) MyBase.PostFilterProperties(properties) properties("Anchor") = TypeDescriptor.CreateProperty( _ GetType(RoundedBoxDesigner), _ CType(properties("Anchor"), PropertyDescriptor)) End Sub Public Property Anchor() As AnchorStyles Get Return CType(ShadowProperties("Anchor"), AnchorStyles) End Get Set ShadowProperties("Anchor") = Value End Set End Property This will have the effect of disabling anchoring behavior at design time, but leaving it enabled at runtime. Note that we are using a different version of the CreateProperty factory function. This one takes an existing PropertyDescriptor (the one already in the properties dictionary) and builds a new one based on that. This preserves all the attributes of the original, and merely has the effect of redirecting to our designer class's property.
9.2.6 Designer Host InterfacesThe custom designer classes we have seen so far have been essentially passive. They rely on the Forms Designer to create designer instances when needed, and to call their methods only when particular services are required. But sometimes your designer class will need to be a bit more proactive, explicitly requesting certain services from the Forms Designer. There is a generic mechanism to enable any container to provide arbitrary services on demand to the contained component. A component can request a service from its container by calling the GetService method. (This method is available in both the Component class and the ComponentDesigner class, so both controls and their designer classes can request services.) GetService takes a single parameter, a Type object. This should be the type of an interface, indicating which service is required. If the container does not provide the requested service (or if there is no container, which is typically the case at runtime) GetService will return null or Nothing. But if the service is available, it will return an object that implements the requested service. One of the standard services provided by Visual Studio .NET is ISelectionService. This allows designer classes and controls to find out which items have been selected by the user. Examples Example 9-24 and Example 9-25 illustrate how to use this service to modify the design-time appearance of a control according to whether it is selected or not. The example shows modifications to the RoundedBoxDesigner class we first saw in the C# code in Examples Example 9-12, Example 9-14, and Example 9-16, and in the VB code in Examples Example 9-13, Example 9-15, and Example 9-17. Here we override the Initialize method (which will be called when the designer class is instantiated), and ask the container for the ISelectionService interface. If this service is present (it always will be in Visual Studio .NET), our designer class attaches a handler to the SelectionChanged event. Our designer can now track the selection status of the control, causing the control to be redrawn each time it is selected or deselected. The OnPaintAdornments method has been modified so that it only paints the grab handle when the control is selected. (This is more consistent with the behavior of the standard resize handles and selection outline, which are only drawn when the control is selected.) Example 9-24. Drawing adornments only when selected using C#private ISelectionService iss; public override void Initialize(IComponent component) { // Call base class so that it can initialize itself base.Initialize(component); iss = GetService(typeof(ISelectionService)) as ISelectionService; if (iss != null) { iss.SelectionChanged += new EventHandler(OnSelectionChanged); } else { // ISelectionService is always available in VS.NET, but // if we're being hosted somewhere odd that doesn't have // it, just display adornments at all times. } } protected override void Dispose(bool disposing) { if (disposing && iss != null) { iss.SelectionChanged -= new EventHandler(OnSelectionChanged); } base.Dispose(disposing); } private bool selected = false; private void OnSelectionChanged(object sender, EventArgs e) { bool previouslySelected = selected; selected = iss.GetComponentSelected(this.Control); if (selected != previouslySelected) this.Control.Invalidate(); } protected override void OnPaintAdornments(PaintEventArgs pe) { if (selected) { Rectangle grabRect = GetGrabRectangle(); ControlPaint.DrawGrabHandle(pe.Graphics, grabRect, true, true); } } Example 9-25. Drawing adornments only when selected using VBPrivate iss As ISelectionService Private selected As Boolean = False Public Overrides Sub Initialize(component As IComponent) ' Call base class so that it can initialize itself MyBase.Initialize(component) iss = CType(GetService(GetType(ISelectionService)), _ ISelectionService) If Not iss Is Nothing Then AddHandler iss.SelectionChanged, _ AddressOf OnSelectionChanged Else ' ISelectionService is always available in VS.NET, but ' if we're being hosted somewhere odd that doesn't have ' it, just display adornments at all times. End If End Sub Protected Overrides Overloads Sub Dispose(disposing As Boolean) If disposing And Not iss Is Nothing Then RemoveHandler iss.SelectionChanged, _ AddressOf OnSelectionChanged End If MyBase.Dispose(disposing) End Sub Private Sub OnSelectionChanged(sender As Object, e As EventArgs) Dim previouslySelected As Boolean = selected selected = iss.GetComponentSelected(Me.Control) If selected <> previouslySelected Then Me.Control.Invalidate() End If End Sub Protected Overrides Sub OnPaintAdornments(pe As PaintEventArgs) If selected Then Dim grabRect As Rectangle = GetGrabRectangle() ControlPaint.DrawGrabHandle(pe.Graphics, grabRect, _ True, True) End If End Sub ISelectionService is just one of the many different services available to components and designer classes running in the Forms Designer. Table 9-1 lists the available services and provides a brief description of each service's purpose. These interfaces, which are all defined in the System.ComponentModel.Design namespace, are described in more detail in the reference section.
|
[ Team LiB ] |