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:
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 ClassIn 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 ControlsAfter 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.
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 RenderingLooking 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 .
Ambient PropertiesThe 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:
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 FunctionalityIn 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 EventsThe 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:
' 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 InputIn 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 InputFor 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 InputIn 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
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 HandlingThe 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 ControlsAlthough 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 DimensionUnlike 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 ControlsIf 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. |