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:

 using System; using System.Collections; using System.ComponentModel; using System.Drawing; using System.Data; using System.Windows.Forms; namespace MyCustomControls {   /// <summary>   /// Summary description for CustomControl1.   /// </summary>  public class CustomControl1 : System.Windows.Forms.Control {  public CustomControl1() {     }  protected override void OnPaint(PaintEventArgs pe) {   // TODO: Add custom paint code here   // Calling the base class OnPaint   base.OnPaint(pe);   }   }  } 

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 EventArgs (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:

 public class EllipseLabel : Control {   public EllipseLabel() {     // Required for Designer support     InitializeComponent();   }  protected override void OnPaint(PaintEventArgs pe) {  // Custom paint code     Graphics g = pe.Graphics;     using( Brush foreBrush = new SolidBrush(  this.ForeColor  ) )     using( Brush backBrush = new SolidBrush(  this.BackColor  ) ) {       g.FillEllipse(foreBrush,  this.ClientRectangle  );       StringFormat fmt = new StringFormat();       fmt.Alignment = StringAlignment.Center;       fmt.LineAlignment = StringAlignment.Center;       g.DrawString(  this.Text, this.Font,  backBrush,  this.ClientRectangle,  fmt);     }     // Calling the base class OnPaint     base.OnPaint(pe);   } } 

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 EllipseLabel() {   // Required for Designer support   InitializeComponent();  // Automatically redraw when resized   // (See Chapter 6: Advanced Drawing for ControlStyles details)   this.SetStyle(ControlStyles.ResizeRedraw, true);  } void InitializeComponent() {  this.TextChanged += new EventHandler(this.EllipseLabel_TextChanged);  }  void EllipseLabel_TextChanged(object sender, EventArgs e) {   this.Invalidate();   }  

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 my 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:

 void InitializeComponent() {   ...  this.ellipseLabel1.Font = new Font("Impact", 10F, ...);  ... } 

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:

 void InitializeComponent() {   ...  this.Font = new Font("Impact", 10F, ...);  ... } 

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.

 void InitializeComponent() {   ...  this.ellipseLabel1.Font = new Font("Times New Roman", 10F, ...);  ...  this.Font = new Font("Impact", 10F, ...);  ... } 

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 string prefix = ""; public void ResetPrefix() {   this.Prefix = ""; // Uses Prefix setter } public string Prefix {   get { return this.prefix; }   set {     this.prefix = value;  this.Invalidate();  } } protected override void OnPaint(PaintEventArgs pe) {   ...  g.DrawString(this.prefix + this.Text, ...);  ... } 

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 there, too. For example, if you want to fire an event when the Prefix property changes, you can expose a public property:

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

 // Let clients know of changes in the Prefix property public event EventHandler PrefixChanged; public string Prefix {   get { return this.prefix; }   set {     this.prefix = value;  // Fire PrefixChanged event   if( this.PrefixChanged != null ) {   PrefixChanged(this, EventArgs.Empty);   }  this.Invalidate();   } } 

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 Property Browser, 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:

  public class PrefixEventArgs : EventArgs {   public string Prefix;   public PrefixEventArgs(string prefix) { Prefix = prefix; }   }   public delegate   void PrefixedChangedEventHandler(object sender, PrefixEventArgs e);   public event PrefixedChangedEventHandler PrefixChanged;  public string Prefix {   get { return this.prefix; }   set {     this.prefix = value;     // Fire PrefixChanged event     if( this.PrefixChanged != null ) {  PrefixChanged(this, new PrefixEventArgs(value));  }     this.Invalidate();   } } 

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 bool mouseDown = false; void SetMouseForeColor(MouseEventArgs e) {   int red = (e.X * 255/(this.ClientRectangle.Width - e.X))%256;   if( red < 0 ) red = -red;   int green = 0;   int blue = (e.Y * 255/(this.ClientRectangle.Height - e.Y))%256;   if( blue < 0 ) blue = -blue;   this.ForeColor = Color.FromArgb(red, green, blue); } void EllipseLabel_MouseDown(object sender, MouseEventArgs e) {   mouseDown = true;   SetMouseForeColor(e); } void EllipseLabel_MouseMove(object sender, MouseEventArgs e) {   if( mouseDown ) SetMouseForeColor(e); } void EllipseLabel_MouseUp(object sender, MouseEventArgs e) {   SetMouseForeColor(e);   mouseDown = false; } 

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 : EventArgs {   public MouseButtons Button { get; } // Which buttons are pressed   public int Clicks { get; } // How many clicks since the last event   public int Delta { get; } // How many mouse wheel ticks   public int X { get; } // Current X position relative to the screen   public int Y { get; } // Current Y position relative to the screen } 

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:

 void EllipseLabel_MouseDown(object sender, MouseEventArgs e) {   SetMouseForeColor(e); } void EllipseLabel_MouseMove(object sender, MouseEventArgs e) {  if( (e.Button & MouseButtons.Left) == MouseButtons.Left ) {  SetMouseForeColor(e);  }  } void EllipseLabel_MouseUp(object sender, MouseEventArgs e) {   SetMouseForeColor(e); } 

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:

 void EllipseLabel_KeyPress(object sender, KeyPressEventArgs e) {   Point location = new Point(this.Left, this.Top);   switch( e.KeyChar ) {     case 'i':       --location.Y;       break;     case 'j':       --location.X;       break;     case 'k':       ++location.Y;       break;     case 'l':       ++location.X;       break;   }   this.Location = location; } 

The KeyPress event takes a KeyPressEventArgs argument:

 class KeyPressEventArgs : EventArgs {   public bool Handled { get; set; } // Whether this key is handled   public char KeyChar { get; } // Character value of the key pressed } 

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:

 void TransparentForm_KeyDown(object sender, KeyEventArgs e) {   Point location = new Point(this.Left, this.Top);  switch( e.KeyCode ) {   case Keys.I:   case Keys.Up:  --location.Y;       break;  case Keys.J:   case Keys.Left:  --location.X;       break;  case Keys.K:   case Keys.Down:  ++location.Y;       break;  case Keys.L:   case Keys.Right:  ++location.X;       break;  }  this.Location = location; } 

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

 class KeyEventArgs : EventArgs {   public bool Alt { virtual get; } // Whether Alt is pressed   public bool Control { get; } // Whether Ctrl is pressed   public bool Handled { get; set; } // Whether this key is handled   public Keys KeyCode { get; } // The key being pressed, w/o modifiers   public Keys KeyData { get; } // The key and the modifiers   public int KeyValue { get; } // KeyData as an integer   public Keys Modifiers { get; } // Only the modifiers   public bool Shift { virtual get; } // Whether Shift is pressed } 

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 override bool IsInputKey(Keys keyData) {   // Make sure we get arrow keys   switch( keyData ) {     case Keys.Up:     case Keys.Left:     case Keys.Down:     case Keys.Right:       return true;   }   // The rest can be determined by the base class   return base.IsInputKey(keyData); } 
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

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.

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 checks to see whether the Ctrl key is the only modifier to be pressed during a mouse click event:

 void EllipseLabel_Click(object sender, EventArgs e) {  if( Control.ModifierKeys == Keys.Control ) {  MessageBox.Show("Ctrl+Click detected");  }  } 

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 override void WndProc(ref Message m) {   // Process and/or update message   ...   // Let base class handle it if you don't   base.WndProc(ref m); } 

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 override void WndProc(ref Message m) {   // Let the base class have first crack   base.WndProc(ref m);   int WM_NCHITTEST = 0x84; // winuser.h   if( m.Msg != WM_NCHITTEST ) return;   // If the user clicked on the client area,   // ask the OS to treat it as a click on the caption   int HTCLIENT = 1;   int HTCAPTION = 2;   if( m.Result.ToInt32() == HTCLIENT )     m.Result = (IntPtr)HTCAPTION; } 

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 :  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

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

 protected override void OnPaint(PaintEventArgs pe) {   Graphics g = pe.Graphics;   using( Brush foreBrush = new SolidBrush(this.ForeColor) )   using( Brush backBrush = new SolidBrush(this.BackColor) ) {     g.FillEllipse(foreBrush,  this.DisplayRectangle  );     StringFormat format = new StringFormat();     format.Alignment = StringAlignment.Center;     format.LineAlignment = StringAlignment.Center;     g.DrawString(       this.Text, this.Font, backBrush,  this.DisplayRectangle  , format);   }   base.OnPaint(pe); } 

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

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:

 void ScrollingEllipseLabel_TextChanged(object sender, EventArgs e) {   this.Invalidate();  // Text changed -- calculate new DisplayRectangle   SetScrollMinSize();  } void ScrollingEllipseLabel_FontChanged(object sender, EventArgs e) {  // Font changed -- calculate new DisplayRectangle   SetScrollMinSize();  }  void SetScrollMinSize() {  // Create a Graphics Object to measure with   using( Graphics g = this.CreateGraphics() ) {     // Determine the size of the text     SizeF sizeF = g.MeasureString(this.Text, this.Font);     Size size =       new Size(       (int)Math.Ceiling(sizeF.Width),       (int)Math.Ceiling(sizeF.Height));  // Set the minimum size to the text size   this.AutoScrollMinSize = size;  }  }  

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

Figure 8.20. FileTextBox with a File Name That Does Exist

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 :  TextBox  {  protected override void OnTextChanged(EventArgs e) {  // If the file does not exist, color the text red     if( !File.Exists(this.Text) ) {       this.ForeColor = Color.Red;     }     else { // Make it black       this.ForeColor = Color.Black;     }     // Call the base class     base.OnTextChanged(e);  }  } 

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 to set 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 C#
Windows Forms Programming in C#
ISBN: 0321116208
EAN: 2147483647
Year: 2003
Pages: 136
Authors: Chris Sells

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