Custom Controls


Owner-draw controls allow a great deal of control over how a control draws itself, but to take full command of how a control acts you must build a custom control . There are three main kinds of custom controls:

  • Controls that derive directly from the Control base class, allowing you to handle your control's input and output completely

  • Controls that derive from ScrollingControl, which are like controls that derive from Control but also provide built-in support for scrolling

  • Controls that derive from an existing control to extend their behavior

The kind of control you choose depends on the kind of functionality you need. If you need something that's fundamentally new, you'll derive from Control or ScrollingControl, depending on whether you need scrolling. Deriving from one of the existing controls is useful if an existing control almost does what you want. The following sections discuss how to build all three kinds of custom controls.

Deriving Directly from the Control Class

In VS.NET, if you right-click on your project in Solution Explorer and choose Add Add New Item Custom Control, you'll get the following skeleton:

 
 Public Class CustomControl1     Inherits System.Windows.Forms.Control     Protected Overrides Sub OnPaint(pe As _       System.Windows.Forms.PaintEventArgs)         MyBase.OnPaint(pe)         ' Add your custom paint code here     End Sub End Class 

This skeleton derives from the Control base class and provides a handler for the Paint event. It even provides a helpful comment letting you know where to add your custom code to render your custom control's state.

Testing Custom Controls

After you've worked with your custom control for a while, you'll want it to show up on the Toolbox so that you can use it in various places. To do this, right-click on the Toolbox and choose Add/Remove Items. [3] To choose a .NET assembly, click on the .NET Framework Component tab and press the Browse button. When you do that, you will get the Customize Toolbox dialog showing the .NET components that VS.NET knows about, as shown in Figure 8.10.

[3] In VS.NET 2002, "Add/Remove Items" is called "Customize Toolbox."

Figure 8.10. Customizing the Toolbox

Choose the assembly that your control lives in and press OK. If you are writing a Windows Forms application and writing your custom control in the same assembly, select the application's .EXE file as the assembly. Even controls from applications are available for reuse, although DLLs are the preferred vehicle for distributing reusable controls.

After you've chosen the assembly to add, the custom controls will be added to the Toolbox, as shown in Figure 8.11.

Figure 8.11. Custom Controls Added to the Toolbox in VS.NET

Although it's possible to customize any of the tabs on the Toolbox, it's handy to have custom tabs for custom controls so that they don't get lost among the standard controls and components. Figure 8.11 shows custom controls organized on the My Custom Controls tab.

When your control is available on the Toolbox, you can drop it onto a Form and use the Property Browser to set all public properties and handle all public events. Because your custom controls inherit from the Control base, all this comes essentially for free. For the details of how to customize your control's interaction with the Designer and the Property Browser, see Chapter 9: Design-Time Integration.

Control Rendering

Looking back at the skeleton code generated by the Designer for a custom control, remember that it handles the Paint event by deriving from the Control base class and overriding the OnPaint method. Because we're deriving from the Control class, we have two options when deciding how to handle a method. The first option is to add a delegate and handle the event. This is the only option available when you're handling a control's event from a container. The second option is to override the virtual method that the base class provides that actually fires the methods. By convention, these methods are named On<EventName> and take an object of the Event-Args (or EventArgs-derived) class. When you override an event method, remember to call to the base class's implementation of the method so that all the event subscribers will be notified.

For example, here's how to implement OnPaint for a custom label-like control:

 
 Class EllipseLabel   Inherits Control   Public Sub New()       ' Required for Designer support       InitializeComponent()   End Sub   Protected Overrides Sub OnPaint(pe As PaintEventArgs)       ' Custom paint code       Dim g As Graphics = pe.Graphics       Dim foreBrush As Brush = New SolidBrush(Me.ForeColor)       Dim backBrush As Brush = New SolidBrush(Me.BackColor)       g.FillEllipse(foreBrush, Me.ClientRectangle)       Dim fmt As StringFormat = New StringFormat()       fmt.Alignment = StringAlignment.Center       fmt.LineAlignment = StringAlignment.Center       g.DrawString(Me.Text, Me.Font, backBrush, Me.ClientRectangle, fmt)       ' Calling the base class OnPaint       MyBase.OnPaint(pe)   End Sub End Class 

In this code, notice how much functionality is available from the base class without the need to add any new properties, methods, or events. In fact, the sheer amount of functionality in the base Control class is too large to list here.

Many of the properties have corresponding <PropertyName>Changed events to track when they change. For example, the state of our custom label-like control depends on the state of the BackColor, ForeColor, Text, Font, and ClientRectangle properties; so when any of these properties changes, we must apply the principles of drawing and invalidation from Chapter 4: Drawing Basics to keep the control visually up-to-date:

 
 Public Sub New()   ' Required for Designer support   InitializeComponent()   ' Automatically redraw when resized   ' (See Chapter 6: Advanced Drawing for ControlStyles details)   Me.SetStyle(ControlStyles.ResizeRedraw, True) End Sub Sub InitializeComponent()   AddHandler Me.TextChanged, AddressOf Me.EllipseLabel_TextChanged End Sub Sub EllipseLabel_TextChanged(sender As Object, e As EventArgs)   Me.Invalidate() End Sub 

In this case, we track when the Text property has changed by using the Designer [4] to set up an event handler for the TextChanged event (saving us from typing in the event handler skeleton or remembering to call the base class). When the text changes, we invalidate our control's client area. However, we don't need to track any of the BackColorChanged, FontChanged, or ForeColorChanged events because the base class knows to invalidate the client area of the control in those cases for us. Those properties are special, as explained next .

[4] Be careful when using the Designer with custom controls. It adds an InitializeComponent method if there isn't already one in the class, but it doesn't add a call from your control's constructor to InitializeComponent, so you must do that manually.

Ambient Properties

The reason that the base class knows to treat some properties specially is that they are ambient properties. An ambient property is one that, if it's not set in the control, will be "inherited" from the container. Of all the standard properties provided by the Control base class, only four are ambient: BackColor, ForeColor, Font, and Cursor. For example, imagine an instance of the EllipseLabel control and a button hosted on a form container, as in Figure 8.12.

Figure 8.12. The EllipseLabel Custom Control Hosted on a Form

All the settings for the Form, the EllipseLabel control, and the Button control are the defaults with respect to the Font property; this means that on our Windows XP machine running at normal- sized fonts, the two controls show with MS Sans Serif 8.25-point font. Because the EllipseLabel control takes its own Font property into account when drawing, changing its Font property to Impact 10-point in the Property Browser yields this code:

 
 Sub InitializeComponent()   ...   Me.ellipseLabel1.Font = New Font("Impact", 10, ...)   ... End Sub 

The result looks like Figure 8.13.

Figure 8.13. Setting the Font Property on the EllipseLabel Control

This works great if you're creating a funhouse application in which different controls have different fonts, but more commonly, all the controls in a container will share the same font. Although it's certainly possible to use the Designer to set the fonts for each of the controls individually, it's even easier to leave the font alone on the controls and set the font on the form:

 
 Sub InitializeComponent()   ...   Me.Font = New Font("Impact", 10, ...)   ... End Sub 

Because the Font property is ambient, setting the font on the container also sets the fonts on the contained controls, as shown in Figure 8.14.

Figure 8.14. Setting the Font Property on the Hosting Form

When you set the Font property on the container and leave the Font property at the default value [5] for the controls, the control "inherits" the Font property from the container. Similarly, a contained control can "override" an ambient property if you set it to something other than the default:

[5] You can return a property to its "default" value in the Property Browser by right-clicking on the property name and choosing Reset.

 
 Sub InitializeComponent()   ...   Me.ellipseLabel1.Font = New Font("Times New Roman", 10, ...)   ...   Me.Font = New Font("Impact", 10, ...)   ... End Sub 

Notice that the form's font is set after the EllipseLabel control's font. It doesn't matter in which order the ambient properties are set. If a control has its own value for an ambient property, that value will be used instead of the container's value. The result of the contained EllipseLabel control overriding the ambient Font property is shown in Figure 8.15.

Figure 8.15. A Contained Control Overriding the Value of the Ambient Font Property

Also, if you need to reset the ambient properties to a default value, you can do this by using the Control class's Reset<PropertyName> methods:

 
 ellipseLabel1.  ResetFont()  

Ambient properties exist to allow containers to specify a look and feel that all the contained controls share without any special effort. However, in the event that a particular control needs to override the property inherited from its container, that can happen without incident.

Custom Functionality

In addition to the standard properties that a control gets from the Control base class, the state that a control must render will come from new public methods and properties that are exposed as they would be exposed from any .NET class:

 
 ' Used to prepend to Text property at output Private myprefix As String = String.Empty Sub ResetPrefix()   Me.Prefix = String.Empty ' Uses Prefix setter End Sub Property Prefix() As String   Get       Return myprefix   End Get   Set       myprefix = Value       Me.Invalidate()   End Get End Property Protected Override Sub OnPaint(pe As PaintEventArgs)   ...   g.DrawString(Me.Prefix & Me.Text, ...)   ... End Sub 

In this case, we've got some extra control state modeled with a string field named "prefix". The prefix is shown just before the Text property when the control paints itself. The prefix field itself is private, but you can affect it by calling the public ResetPrefix method or getting or setting the public Prefix property. Notice that whenever the prefix field changes, the control invalidates itself so that it can maintain a visual state that's consistent with its internal state.

Because the Prefix property is public, it shows up directly in the Property Browser when an instance of the Ellipse Control is chosen on a design surface, as shown in Figure 8.16.

Figure 8.16. A Custom Property in the Property Browser

Custom Events

The Property Browser will show any public property without your doing anything special to make it work. Similarly, any public events [6] will show up in the wizard bars of the code editor window. For example, if you want to fire an event when the Prefix property changes, you can create a public property and expose it:

[6] For an introduction to delegates and events, see Appendix B: Delegates and Events.

 
 ' Let clients know of changes in the Prefix property Event PrefixChanged As EventHandler Property Prefix() As String   Get       Return myprefix   End Get   Set       myprefix = Value       ' Fire PrefixChanged event       If Not Me.PrefixChanged Is Nothing Then           RaiseEvent PrefixChanged(Me, EventArgs.Empty)       End If       Me.Invalidate()   End Set End Property 

Notice that this code exposes a custom event called PrefixChanged of type EventHandler, which is the default delegate type for events that don't need special data. When the prefix field is changed, the code looks for event subscribers and lets them know that the prefix has changed, passing the sender (the control itself) and an empty EventArgs object, because we don't have any additional data to send.

When your control has a public event, it will show up as just another event in the code editor window wizard bars, as shown in Figure 8.17.

Figure 8.17. A Custom Event Shown in the Property Browser

Just like handling any other event, handling a custom event yields a code skeleton for the developer to fill in with functionality ”again, without your doing anything except exposing the event as public.

If, when defining your event, you find that you'd like to pass other information, you can create a custom delegate:

 
 Class PrefixEventArgs   Inherits EventArgs   Private myprefix As String   Public Sub New(prefix As String)       Me.Prefix = prefix   End Sub   Public Delegate Sub PrefixChangedEventHandler(sender As Object, _        e As PrefixEventArgs)   Public Event PrefixChanged As PrefixedChangedEventHandler   Property Prefix() As String       Get           Return myprefix       End Get       Set           myprefix = Value           ' Fire PrefixChanged event           If Not Me.PrefixChanged Is Nothing Then               RaiseEvent PrefixChanged(Me, _             New PrefixEventArgs(Value))           End If           Me.Invalidate()       End Set   End Property End Class 

Notice that the custom delegate we created uses the same pattern of no return value, an object sender argument, and an EventArgs-derived type as the last argument. This is the pattern that .NET follows , and it's a good one for you to emulate with your own custom events. In our case, we're deriving from EventArgs to pass along a PrefixEventArgs class, which derives from EventArgs and sends the new prefix to the event handlers. But you can define new EventArgs-derived classes as appropriate for your own custom controls.

Control Input

In addition to providing output and exposing custom methods, properties, and events, custom controls often handle input, whether it's mouse input, keyboard input, or both.

Mouse Input

For example, if we wanted to let users click on EllipseControl and, as they drag, adjust the color of the ellipse, we could do so by handling the MouseDown, MouseMove, and MouseUp events:

 
 ' Track whether mouse button is down Dim mouseDown As Boolean = False Sub SetMouseForeColor(e As MouseEventArgs)   Dim red As Integer = _      (e.X * 255/(Me.ClientRectangle.Width  e.X)) Mod 256   If red < 0 Then red = -red   Dim green As Integer = 0   Dim blue As Integer = _      (e.Y * 255/(Me.ClientRectangle.Height  e.Y)) Mod 256   If blue < 0 Then blue = -blue   Me.ForeColor = Color.FromArgb(red, green, blue) End Sub Sub EllipseLabel_MouseDown(sender As Object, e As MouseEventArgs)   mouseDown = True   SetMouseForeColor(e) End Sub Sub EllipseLabel_MouseMove(sender As Object, e As MouseEventArgs)   If mouseDown Then SetMouseForeColor(e) End Sub Sub EllipseLabel_MouseUp(sender As Object, e As MouseEventArgs)   SetMouseForeColor(e)   mouseDown = False End Sub 

The MouseDown event is fired when the mouse is clicked inside the client area of the control. The control continues to get MouseMove events until the MouseUp event is fired , even if the mouse moves out of the region of the control's client area. The code sample watches the mouse movements when the button is down and calculates a new ForeColor using the X and Y coordinates of the mouse as provided by the MouseEventArgs argument to the events:

 
 Class MouseEventArgs   Inherits EventArgs   Property Button() As MouseButtons ' Which buttons are pressed   Property Clicks() As Integer ' How many clicks since the last event   Property Delta() As Integer ' How many mouse wheel ticks   Property X() As Integer ' Current X position relative to the screen   Property Y() As Integer ' Current Y position relative to the screen End Class 

MouseEventArgs is meant to provide you with the information you need in order to handle mouse events. For example, to eliminate the need to track the mouse button state manually, we could use the Button property to check for a click of the left mouse button:

 
 Sub EllipseLabel_MouseDown(sender As Object, e As MouseEventArgs)   SetMouseForeColor(e) End Sub Sub EllipseLabel_MouseMove(sender As Object, e As MouseEventArgs)   If ((e.Button And MouseButtons.Left) = MouseButtons.Left) Then       SetMouseForeColor(e)   End If End Sub Sub EllipseLabel_MouseUp(sender As Object, e As MouseEventArgs)   SetMouseForeColor(e) End Sub 

Additional mouse- related input events are MouseEnter, MouseHover, and MouseLeave, which can tell you that the mouse is over the control, that it's hovered for "a while" (useful for showing tooltips), and that it has left the control's client area.

If you'd like to know the state of the mouse buttons or the mouse position outside a mouse event, you can access this information from the static MouseButtons and MousePosition properties of the Control class. In addition to MouseDown, MouseMove, and MouseUp, there are five other mouse-related events. MouseEnter, MouseHover, and MouseLeave allow you to track when a mouse enters, loiters in, and leaves the control's client area. Click and DoubleClick provide an indication that the user has clicked or double-clicked the mouse in the control's client area.

Keyboard Input

In addition to providing mouse input, forms (and controls) can capture keyboard input via the KeyDown, KeyUp, and KeyPress events. For example, to make the keys i, j, k, and l move our elliptical label around on the container, the EllipseLabel control could handle the KeyPress event:

 
 Sub EllipseLabel_KeyPress(sender As Object, e As KeyPressEventArgs)   Dim location As Point = New Point(Me.Left, Me.Top)   Select Case e.KeyChar       Case "i"c           location.Y -= 1       Case "j"c           location.X -= 1       Case "k"c           location.Y += 1       Case "l"c           location.X += 1   End Select   Me.Location = location End Sub 

The KeyPress event takes a KeyPressEventArgs argument:

 
 Class KeyPressEventArgs   Inherits EventArgs   Property Handled() As Boolean ' Whether this key is handled   Property KeyChar() As Char ' Character value of the key pressed End Class 

The KeyPressEventArgs object has two properties. The Handled property defaults to false but can be set to true to indicate that no other handlers should handle the event. The KeyChar property is the character value of the key after the modifier has been applied. For example, if the user presses the I key, the KeyChar will be i, but if the user presses Shift and the I key, the KeyChar property will be I. On the other hand, if the user presses Ctrl+I or Alt+I, we won't get a KeyPress event at all, because those are special sequences that aren't sent via the KeyPress event. To handle these kinds of sequences along with other special characters such as F-keys or arrows, you need the KeyDown event:

 
 Sub Transparentform_KeyDown(sender As Object, e As KeyEventArgs)   Dim location As Point = New Point(Me.Left, Me.Top)   Select Case e.KeyCode       Case Keys.I, Keys.Up           location.Y -= 1       Case Keys.J, Keys.Left           location.X -= 1       Case Keys.K, Keys.Down           location.Y += 1       Case Keys.L, Keys.Right           location.X += 1   End Select   Me.Location = location End Sub 

Notice that the KeyDown event takes a KeyEventArgs argument (as does the KeyUp event), which is shown here:

 
 Class KeyEventArgs   Inherits EventArgs   Property Alt() As Boolean ' Whether Alt is pressed   Property Control() As Boolean ' Whether Ctrl is pressed   Property Handled() As Boolean ' Whether this key is handled   Property KeyCode() As Keys ' The key being pressed, w/o the modifiers   Property KeyData() As Keys ' The key and the modifiers   Property KeyValue() As Integer ' KeyData as an integer   Property Modifiers() As Keys ' Only the modifiers   Property Shift() As Boolean ' Whether Shift is pressed End Class 

Although it looks as if the KeyEventArgs object contains a lot of data, it really contains only one thing: a private field exposed via the KeyData property. KeyData is a bit field of the combination of the keys being pressed (from the Keys enumeration) and the modifiers being pressed (also from the Keys enumeration). For example, if the I key is pressed by itself, KeyData will be Keys.I, whereas if Ctrl+Shift+F2 is pressed, KeyData will be a bitwise combination of Keys.F2, Keys.Shift, and Keys.Control.

The rest of the properties in the KeyEventArgs object are handy views of the KeyData property, as shown in Table 8.1. Also shown is the KeyChar that would be generated in a corresponding KeyPress event.

Even though we're handling the KeyDown event specifically to get special characters, some special characters, such as arrows, aren't sent to the control by default. To enable them, the custom control overrides the IsInputKey method from the base class:

 
 Protected Overrides Function IsInputKey(keyData As Keys) As Boolean   ' Make sure we get arrow keys   Select Case keyData       Case Keys.Up, Keys.Left, Keys.Down, Keys.Right           Return True   End Select   ' The rest can be determined by the base class   Return MyBase.IsInputKey(keyData) End Function 

The return from IsInputKey indicates whether or not the key data should be sent in events to the control. In this example, IsInputKey returns true for all the arrow keys and lets the base class decide what to do about the other keys.

Table 8.1. KeyEventArgs and KeyPressEventArgs Examples

Keys Pressed

KeyData

KeyCode

Modifiers

Alt

Ctrl

Shift

KeyValue

KeyChar

I

Keys.I

Keys.I

Keys.None

false

false

false

73

i

Shift+I

Keys.Shift + Keys.I

Keys.I

Keys.Shift

false

false

true

73

I

Ctrl+Shift+I

Keys.Ctrl + Keys.Shift + Keys.I

Keys.I

Keys.Ctrl + Keys.Shift

false

true

true

73

n/a

Ctrl

Keys.ControlKey + Keys.Ctrl

Keys.ControlKey

Keys.Control

false

true

false

17

n/a

If you'd like to know the state of a modifier key outside a key event, you can access the state in the static ModifierKeys property of the Control class. For example, the following code checks to see whether the Ctrl key is the only modifier to be pressed during a mouse click event:

 
 Sub EllipseLabel_Click(sender As Object, e As EventArgs)   If Control.ModifierKeys = Keys.Control Then       MessageBox.Show("Ctrl+Click detected")   End If End Sub 

Windows Message Handling

The paint event, the mouse and keyboard events, and most of the other events handled by a custom control come from the underlying Windows operating system. At the Win32 level, the events start out life as Windows messages. A Windows message is most often generated by Windows because of some kind of hardware event, such as the user pressing a key, moving the mouse, or bringing a window from the background to the foreground. The window that needs to react to the message gets the message queued in its message queue . That's where WinForms steps in.

The Control base class is roughly equivalent to the concept of a window in the operating system. It's the job of WinForms to take each message off the Windows message queue and route it to the Control responsible for handling the message. The base Control class turns this message into an event, which Control then fires by calling the appropriate method in the base class. For example, the WM_PAINT Windows message eventually turns into a call on the OnPaint method, which in turn fires the Paint event to all interested listeners.

However, not all Windows messages are turned into events by WinForms. For those cases, you can drop down to a lower level and handle the messages as they come into the Control class. You do this by overriding the WndProc method:

 
 Protected Overrides Sub WndProc(ByRef m As Message)   ' Process and/or update message   ...   ' Let base class handle it if you don't   MyBase.WndProc(ByRef m) End Sub 

As a somewhat esoteric example of handling Windows messages directly, the following is a rewrite of the code from Chapter 2: Forms to move the nonrectangular form around the screen:

 
 Protected Overrides Sub WndProc(ByRef m As Message)   ' Let the base class have first crack   MyBase.WndProc(ByRef m)   Dim WM_NCHITTEST as Integer = &H84 ' winuser.h   If m.Msg <> WM_NCHITTEST Then Return   ' If the user clicked on the client area,   ' ask the OS to treat it as a click on the caption   Dim HTCLIENT as Integer = 1   Dim HTCAPTION as Integer = 2   If m.Result.ToInt32() = HTCLIENT Then       m.Result = CType(HTCAPTION, IntPtr)   End If End Sub 

This code handles the WM_NCHITTEST message, which is one of the few that WinForms doesn't expose as an event. In this case, the code calls to the Windows-provided handler for this message to see whether the user is moving the mouse over the client area of the form. If that's the case, the code pretends that the entire client area is the caption so that when the user clicks and drags on it, Windows will take care of moving the form for us.

There aren't a whole lot of reasons to override the WndProc method and handle the Windows message directly, but it's nice to know that the option is there in case you need it.

Scrolling Controls

Although directly inheriting from Control gives you a bunch of functionality, you may find the need to create a control that scrolls. You could use a custom control to handle the logic involved in creating the scrollbar(s) and handling repainting correctly as the user scrolls across the drawing surface. Luckily, though, the .NET Framework provides a class that handles most of these chores for you.

To create a scrolling control, you derive from ScrollableControl instead of Control:

 
 Class ScrollingEllipseLabel   Inherits ScrollableControl   ... 

When you implement a scrolling control, the ClientRectangle represents the size of the control's visible surface, but there could be more of the control that isn't currently visible because it's been scrolled out of range. To get to the entire area of the control, use the DisplayRectangle property instead. DisplayRectangle is a property of the ScrollableControl class that represents the virtual drawing area. Figure 8.18 shows the difference between the ClientRectangle and the DisplayRectangle.

Figure 8.18. DisplayRectangle versus ClientRectangle (See Plate 20)

An OnPaint method for handling scrolling should look something like this:

 
 Protected Overrides Sub OnPaint(pe As PaintEventArgs)   Dim g As Graphics = pe.Graphics   Dim foreBrush As Brush = New SolidBrush(Me.ForeColor)   Dim backBrush As Brush = New SolidBrush(Me.BackColor)   g.FillEllipse(foreBrush, Me.DisplayRectangle)   Dim format As StringFormat = New StringFormat()   format.Alignment = StringAlignment.Center   format.LineAlignment = StringAlignment.Center   g.DrawString(Me.Text, Me.Font, _     backBrush, Me.DisplayRectangle, format)   backBrush.Dispose()   foreBrush.Dispose()   MyBase.OnPaint(pe) End Sub 

The only difference between this OnPaint method and the custom control is that we are painting to the DisplayRectangle instead of the client rectangle.

Setting the Scroll Dimension

Unlike the ClientRectangle, which is determined by the container of the control, the DisplayRectangle is determined by the control itself. The scrollable control gets to decide the minimum when you set the AutoScrollMinSize property from the ScrollableControl base class. For example, the following code uses the control's font settings to calculate the size needed for the scrollable label based on the size of the Text property:

 
 Sub ScrollingEllipseLabel_TextChanged(sender As Object, e As EventArgs)   Me.Invalidate()   ' Text changed  calculate new DisplayRectangle   SetScrollMinSize() End Sub Sub ScrollingEllipseLabel_FontChanged(sender As Object, e As EventArgs)   ' Font changed  calculate new DisplayRectangle   SetScrollMinSize() End Sub Sub SetScrollMinSize()   ' Create a Graphics Object to measure with   Dim g As Graphics = Me.CreateGraphics()   ' Determine the size of the text   Dim mysizeF As SizeF = g.MeasureString(Me.Text, Me.Font)   Dim mysize As Size = New Size(CInt(Math.Ceiling(mysizeF.Width)), _       CInt(Math.Ceiling(mysizeF.Height)))   ' Set the minimum size to the text size   Me.AutoScrollMinSize = mysize End Sub 

The SetScrollMinSize helper measures the size that the text will be in the particular font and then creates a Size structure. The AutoScrollMinSize property of the Size structure is used to tell the control when to show the scrollbars. If the DisplayRectangle is larger in either dimension than the ClientRectangle, scrollbars will appear.

The ScrollableControl base class has a few other interesting properties. The AutoScroll property (set to true by the Designer by default) enables the DisplayRectangle to be a different size than the ClientRectangle. Otherwise, the DisplayRectangle is always the same size as the ClientRectangle.

The AutoScrollPosition property lets you programmatically change the position within the scrollable area of the control. The AutoScrollMargin property is used to set a margin around scrollable controls that are also container controls. The DockPadding property is similar but is used for child controls that dock. Container controls could be controls such as GroupBox or Panel, or they could be custom controls, such as user controls, covered later in this chapter.

If you'd like to know when your scrollable control scrolls, you can handle the HScroll and VScroll events. Except for the scrolling capability, scrollable controls are just like controls that derive from the Control base class.

Extending Existing Controls

If you'd like a control that's similar to an existing control but not exactly the same, you don't want to start by deriving from Control or ScrollableControl and building everything from scratch. Instead, you should derive from the existing control, whether it's one of the standard controls provided by WinForms or one of your own controls.

For example, let's assume that you want to create a FileTextBox control that's just like the TextBox control except that it indicates to the user whether or not the currently entered file exists. Figures 8.19 and 8.20 show the FileTextBox control in use.

Figure 8.19. FileTextBox with a File That Does Not Exist (See Plate 21)

Figure 8.20. FileTextBox with a File Name That Does Exist (See Plate 22)

By putting this functionality into a reusable control, you can drop it on any form without making the form itself provide this functionality. By deriving the FileTextBox from the TextBox base control class, you can get most of the behavior you need without any effort, letting you focus on the interesting new functionality:

 
 Class FileTextBox   Inherits TextBox   Protected Overrides Sub OnTextChanged(e As EventArgs)       ' If the file does not exist, color the text red       If Not(File.Exists(Me.Text)) Then           Me.ForeColor = Color.Red       Else ' Make it black           Me.ForeColor = Color.Black       End If       ' Call the base class       MyBase.OnTextChanged(e)   End Sub 

Notice that implementing FileTextBox is merely a matter of deriving from the TextBox base class (which provides all the editing capabilities that the user will expect) and overriding the OnTextChanged method. (I also could have handled the TextChanged event.) When the text changes, we use the Exists method of the System.IO.File class to check whether the currently entered file exists in the file system and setting the foreground color of the control accordingly . Often, you can easily create new controls that have application-specific functionality using as little code as this because the bulk of the code is provided by the base control class.



Windows Forms Programming in Visual Basic .NET
Windows Forms Programming in Visual Basic .NET
ISBN: 0321125193
EAN: 2147483647
Year: 2003
Pages: 139

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net