Custom Controls


Owner-drawn and extended controls allow you to leverage the .NET Framework's control base to produce slightly customized variations with little effort. Sometimes, however, the standard controls simply don't provide a UI or an implementation that comes close to what you need. In these situations, your best option is to bite the bullet and create a complete custom control from scratch. There are two main kinds of custom controls:

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

  2. Controls that derive from ScrollableControl, which are like controls that derive from Control but also provide built-in support for scrolling

The kind of control you choose depends on the kind of functionality you need.

Deriving Directly from System.Windows.Forms.Control

Consider the AlarmClockComponent from Chapter 9. .NET gives you no controls that offer alarm clock functionality, and none that render a clock face UI to boot. Turning AlarmClockComponent into a custom control to add the UI is the only way to go.

In VS05, you start by right-clicking your project in Solution Explorer and choosing Add | New Item | Custom Control, calling it AlarmClockControl. You get the following skeleton:

// AlarmClockControl.Designer.cs partial class AlarmClockControl {   ...   void InitializeComponent() {...}   ... } // AlarmClockControl.cs using System; using System.Collections.Generic; using System.ComponentModel; using System.Data; using System.Drawing; using System.Text; using System.Windows.Forms; partial class AlarmClockControl : Control {   public AlarmClockControl() {     InitializeComponent();   } protected override void OnPaint(PaintEventArgs e) {   // TODO: Add custom paint code here   // Calling the base class OnPaint   base.OnPaint(pe);  } }


This skeleton derives from the Control base class and provides an override of the virtual OnPaint method responsible for painting its content. It even includes a helpful comment that lets you know where to add your custom code to render your custom control's state.

Notice that the generated constructor includes a call to InitializeComponent; controls, like components, provide a nonvisual design surface for you to drag components onto as required, as shown in Figure 10.14.

Figure 10.14. A New Control's Nonvisual Design Surface


The Windows Forms Designer generates the necessary InitializeComponent code to support use of the nonvisual design surface. In our case, if we want to add a UI to Chapter 9's AlarmComponent, it's likely we'll use the nonvisual design surface. That's because we'll require a timer again to build the AlarmClockControl, as shown in Figure 10.15.

Figure 10.15. A Timer Component Hosted on a Control's Nonvisual Design Surface


After configuring the Timer as needed, we next provide a UI.

Control Rendering

As you look back at the skeleton code generated by the Windows Forms 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 event.

The first option is to write an event handler and register it with the event. This is the only option available when you're not deriving from Control. When you are deriving, the second option is to override the virtual method provided by the base class both to perform any relevant processing and to fire the associated event. By convention, these methods are named OnEventName and take an object of the EventArgs (or EventArgs-derived) class as a parameter. When you override an event method, remember to call 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 the custom AlarmClockControl:

// AlarmClockControl.cs partial class AlarmClockControl : Control {   ...   protected override void OnPaint(PaintEventArgs e) {     Graphics g = e.Graphics;     // Get current date/time     DateTime now = DateTime.Now;     // Calculate required dimensions     Size faceSize = this.ClientRectangle.Size;     int xRadius = faceSize.Width / 2;     int yRadius = faceSize.Height / 2;     double degrees;     int x;     int y;     // Make things pretty     g.SmoothingMode = System.Drawing.Drawing2D.SmoothingMode.AntiAlias;     // Paint clock face     using( Pen facePen = new Pen(Color.Black, 2) )     using( SolidBrush faceBrush = new SolidBrush(Color.White) ) {       g.DrawEllipse(facePen, facePen.Width, facePen.Width,                     faceSize.Width - facePen.Width * 2,                     faceSize.Height - facePen.Width * 2);       g.FillEllipse(faceBrush, facePen.Width, facePen.Width,                     faceSize.Width - facePen.Width * 2,                     faceSize.Height - facePen.Width * 2);     }     // Paint hour hand, minute hand, second hand, and digital time     ...     // Let the base class fire the Paint event     base.OnPaint(e);   }   void timer_Tick(object sender, EventArgs e) {     // Refresh clock face     this.Invalidate();   } }


In this code, the Graphics object passed with the PaintEventArgs to this override is used to paint the clock face and hands to display the current time. To ensure that clients can handle AlarmClockControl's Paint event to overlay its UI with additional UI elements, we call the base class's OnPaint method to fire the Paint event after AlarmClockControl's paint logic executes.

To add a control to the Toolbox, we compile the project that houses the control. Follow the steps discussed in Chapter 9 to see how. Figure 10.16 shows an AlarmClockControl in action.

Figure 10.16. The AlarmClockControl in Action


Although it's pretty, this UI goes only halfway to solving the alarm setting and sounding problem.

Custom Implementation

The original AlarmComponent was gifted with several custom members to enable its operational capability. Controls are equally capable when it comes to implementing custom properties, events, and methods, and AlarmClockControl re-implements one of each from AlarmComponent, including the Alarm property, AlarmSoundedEvent, and the DelayAlarm method:

// AlarmSoundedEventArgs.cs public class AlarmSoundedEventArgs : EventArgs {   DateTime alarm;   public AlarmSoundedEventArgs(DateTime alarm) {     this.alarm = alarm;   }   public DateTime Alarm {     get { return this.alarm; }   } } // AlarmSoundedEventHandler.cs public delegate void AlarmSoundedEventHandler(   object sender, AlarmSoundedEventArgs e); // AlarmClockControl.cs partial class AlarmClockControl : Control {   ...   DateTime alarm = DateTime.MaxValue; // No alarm   DateTime Alarm {     get { return this.alarm; }     set { this.alarm = value; }   }   event AlarmSoundedEventHandler AlarmSounded;   DateTime DelayAlarm(double minutes) {    if( this.alarm < DateTime.MaxValue ) {       this.alarm = this.alarm.AddMinutes(minutes);    }    return this.alarm;  }  ...  void timer_Tick(object sender, EventArgs e) {    // Check to see whether we're within 1 second of the alarm    double seconds = (DateTime.Now - this.alarm).TotalSeconds;    if( (seconds >= 0) && (seconds <= 1) ) {      DateTime alarm = this.alarm;      this.alarm = DateTime.MaxValue; // Show alarm only once      if( this.AlarmSounded != null ) {        // Sound alarm async so clock can keep ticking        this.AlarmSounded.BeginInvoke(          this,          new AlarmSoundedEventArgs(alarm),          null,          null);       }     }     ...   } }


As you would expect, a control that's hosted on the Toolbox can be dragged onto a form and configured by using the Properties window. With a clock face beaming at us and additional controls to set and delay the alarm, we can quickly produce the form shown in Figure 10.17.

Figure 10.17. The Fully Functional AlarmClockControl in Action


The code to make this work is similar to the client code we built for Chapter 9's AlarmComponent:

// AlarmClockControlSampleForm.cs partial class AlarmClockControlSampleForm : Form {   public AlarmClockControlSampleForm() {     InitializeComponent();   }   void setAlarmButton_Click(object sender, EventArgs e) {     this.alarmClockControl.Alarm = this.dateTimePicker.Value;     this.addMinutesButton.Enabled = true;     this.numericUpDown.Enabled = true;   }   void alarmClockControl_AlarmSounded(     object sender, AlarmSoundedEventArgs e) {     System.Media.SystemSounds.Exclamation.Play();     MessageBox.Show("It's " + e.Alarm.ToString() + ". Wake up!");   }   void addMinutesButton_Click(object sender, EventArgs e) {     double minutes = (double)this.numericUpDown.Value;     DateTime newAlarm = this.alarmClockControl.DelayAlarm(minutes);     this.dateTimePicker.Value = newAlarm;   } }


So, for the same code, your users are treated to a more visually appealing experience.

EventChanged

Part of any user experience is choice. One choice that AlarmClockControl users might like to have is to hide or show the second hand, particularly when they've had more than their daily allowance of caffeine and any movement distracts them from their game of Minesweeper. You can easily let users toggle the visibility of the second hand:

// AlarmClockControl.cs partial class AlarmClockControl : Control {   ...   bool showSecondHand = true;   public bool ShowSecondHand {     get { return this.showSecondHand; }     set {        this.showSecondHand = value;        this.Invalidate();     }   }   protected override void OnPaint(PaintEventArgs e) {     Graphics g = e.Graphics;     ...     // Paint second hand, if so configured     if( this.showSecondHand ) {       using( Pen secondHandPen = new Pen(Color.Red, 2) ) {         ...       }     }     ...     string nowFormatted = ( this.showSecondHand ?                             now.ToString("dd/MM/yyyy hh:mm:ss tt") :                             now.ToString("dd/MM/yyyy hh:mm tt"));     ...   } void timer_Tick(object sender, EventArgs e) {   ...   // If we're showing the second hand, we need to refresh every second   if( this.showSecondHand ) {     this.Invalidate();   }   else {     // Otherwise, we need to refresh only every minute, on the minute     if( (DateTime.Now.Second == 59) || (DateTime.Now.Second == 0) ) {       this.Invalidate();     }   }  } }


The ShowSecondHand property controls whether the analog clock's second hand and the digital clock's seconds element are rendered, and it refreshes the UI immediately when changed. Additionally, if the second hand isn't shown, AlarmClockControl refreshes its UI only once a minute.

One advantage of implementing a custom property like ShowSecondHand is the ability it gives you to immediately repaint the control's UI. However, when a property such as Padding from the base Control class is set, there's a little problem: AlarmClockControl's padding does not change, for two reasons.

First, AlarmClockControl's painting logic doesn't take padding into account, something that is easy to update:

// AlarmClockControl.cs partial class AlarmClockControl : Control {   ...   protected override void OnPaint(PaintEventArgs e) {     Graphics g = e.Graphics;     ...     // Calculate required dimensions     Size faceSize = this.ClientRectangle.Size;     ...     // Paint clock face     using( Pen facePen = new Pen(Color.Black, 2) ) {        g.DrawEllipse(          facePen,          facePen.Width + this.Padding.Left,          facePen.Width + this.Padding.Top,          faceSize.Width - facePen.Width * 2,          faceSize.Height - facePen.Width * 2);       ...     }     ...     // Calling the base class OnPaint     base.OnPaint(pe);   }   ... }


Second, even though the painting logic handles padding, setting it via the Properties window doesn't have an immediate effect because we're not forcing a repaint when the Padding property is changed. Instead, we need to wait until AlarmClockControl is requested to repaint itself. If we had implemented the Padding property on AlarmClockControl ourselves, we could apply the principles of drawing and invalidation (from Chapter 5) to keep the control visually up to date by calling Invalidate from its set accessor. However, because the base Control class implements Padding, that option isn't available to us.[9]

[9] Technically, we could shadow (hide) the base class's Padding property with C#'s new keyword, although shadowing is a shady technique; the technique shown here achieves the same effect without creating the confusion of hidden base class members.

Fortunately, the base Control class does implement the Padding property, which, when set, causes Control to fire the PaddingChanged event via the virtual OnPaddingChanged method. We can override OnPaddingChanged to invalidate the UI immediately:

// AlarmClockControl.cs partial class AlarmClockControl : Control {   ...   protected override void OnPaddingChanged(EventArgs e) {     base.OnPaddingChanged(e);     this.Invalidate();   }   ... }


Several eventsincluding BackColorChanged, FontChanged, ForeColorChanged, and CusrorChangeddon't need to be tracked in this fashion because the base class knows to invalidate the client area of the control in those cases for us. Those properties are special.

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, is "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, consider the AlarmClockControl host form, with all its glorious controls on proud display in Figure 10.18.

Figure 10.18. AlarmClockControl Host Form with Many Controls


All the settings for the Form, AlarmClockControl, and other controls 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 use the MS Sans Serif 8.25-point font. The AlarmClockControl control takes its own Font property into account when drawing, and therefore changing its Font property to 9.75-point Impact in the Properties window yields this code:

// AlarmClockControlSampleForm.Designer.cs partial class AlarmClockControlSampleForm {   ...   void InitializeComponent() {     ...     this.alarmClockControl.Font = new Font("Impact", 9.75F);     ...   } }


The result looks like Figure 10.19.

Figure 10.19. Setting the Font Property on the AlarmClockControl Host Form


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 share the same font. Although it's possible to use the Windows Forms Designer to set the fonts for each of the controls individually, it's even easier to leave the controls' fonts alone and set the font on the form:

// AlarmClockControlSampleForm.Designer.cs partial class AlarmClockControlSampleForm {   ...   void InitializeComponent() {     ...     this.Font = new Font("Impact", 9.75F);     ...   } }


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

[10] By default, the form automatically resizes to accommodate the font change. This is a result of scaling, as covered in Chapter 4.

Figure 10.20. 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 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:

// AlarmClockControlSampleForm.Designer.cs partial class AlarmClockControlSampleForm {   ...   void InitializeComponent() {     ...     this.alarmClockControl.Font =       new Font("Times New Roman", 9.75F);     ...     this.Font = new Font("Impact", 9.75F);     ...   } }


Notice that the form's font is set after the AlarmClockControl'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 is used instead of the container's value. The result of the contained AlarmClockControl overriding the ambient Font property is shown in Figure 10.21.

Figure 10.21. 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 right-clicking the desired ambient property in the Properties window and choosing Reset.[11]

[11] A complete discussion of how resetting works is provided in Chapter 11, where you'll learn how to implement your own support for this on custom components and controls.

Ambient properties allow container controls to specify an appearance shared by all contained controls without any special effort. However, a control can also override a property inherited from its container without incident.

Control Input

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

Mouse Input

For example, let's say we wanted to let users click on AlarmClockControl and, as they drag, adjust the color of the current digital time text. We do this by overriding the OnMouseDown, OnMouseMove, and OnMouseUp methods:

// Track whether mouse button is down bool mouseDown = false; protected override void OnMouseDown(MouseEventArgs e) {   this.mouseDown = true;   this.SetForeColor(e);   base.OnMouseDown(e); } protected override void OnMouseMove(MouseEventArgs e) {   if( this.mouseDown ) this.SetForeColor(e);   base.OnMouseMove(e); } protected override void OnMouseUp(MouseEventArgs e) {    this.SetForeColor(e);    this.mouseDown = false;    base.OnMouseUp(e); } void SetForeColor(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); }


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:

namespace System.Windows.Forms {    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 Point Location { get; } // Screen x,y position (New)      public int X { get; } // Current X pos. relative to the screen      public int Y { get; } // Current Y pos. relative to the screen    } }


MouseEventArgs is meant to give you 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:

// Track whether mouse button is down // bool mouseDown = false; // use MouseEventArgs.Button instead protected override void OnMouseDown(MouseEventArgs e) {...} protected override void OnMouseMove(MouseEventArgs e) {...} protected override void OnMouseUp(MouseEventArgs e) {...} void SetForeColor(MouseEventArgs e) {   if( (e.Button & MouseButtons.Left) == MouseButtons.Left ) {     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);   } }


Additional mouse-related input events are MouseEnter, MouseHover, and MouseLeave, which tell you that the mouse is over the control, that it's hovered for "a while" (useful for showing tool tips), 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 seven 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, and MouseClick and MouseDoubleClick, indicate 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 AlarmClockControl class could override the OnKeyPress method:

protected override void OnKeyPress(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; base.OnKeyPress(e); }


The KeyPress event takes a KeyPressEventArgs argument:

namespace System.Windows.Forms {   class KeyPressEventArgs : EventArgs {     public bool Handled { get; set; } // Whether this key is handled     public char KeyChar { get; set; } // Key pressed char (set is New)   } }


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 is i, but if the user presses Shift and the I key, the KeyChar property is I. On the other hand, if the user presses Ctrl+I or Alt+I, we don'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 override the OnKeyDown method:

protected override void OnKeyDown(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;  base.OnKeyDown(e); }


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

namespace System.Windows.Forms {   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 pressed key, w/o the 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     public bool SuppressKeyPress { get; set; } // No KeyPressed                                                // No KeyUp                                                // (New)   } }


By default, the KeyPressed and KeyUp events are still fired even if KeyEventArgs.Handled is set to true by the KeyDown event handler. To prevent these events from being fired, you additionally set KeyEventArgs.SuppressKeyPress to true.

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 is Keys.I, whereas if Ctrl+Shift+F2 is pressed, KeyData is 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 10.1. Also shown is the KeyChar that would be generated in a corresponding KeyPress event.

Table 10.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


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); }


The return from IsInputKey indicates whether 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.

IsInputKey can only be overridden, which is useful when you're handling events in your own custom control or form. However, if you are simply using a control and you'd like to get your fingers into its keyboard events, you can handle the PreviewKeyDown event:

protected override void OnPreviewKeyDown(PreviewKeyDownEventArgs e) {   // Specify the arrow keys as input chars   switch( e.KeyData ) {     case Keys.Up:     case Keys.Left:     case Keys.Down:     case Keys.Right:       e.IsInputKey = true;       return;   }   // The rest can be determined by the base class   base.OnPreviewKeyDown(e); }


Here, we inspect the KeyData property exposed from the PreviewKeyDownEventArgs argument; if we decide that the key we are after should be considered an input key, we set PreviewKeyDownEventArgs.IsInputKey to true. When the IsInputKey property is set here, the call to the IsInputKey override doesn't even occur; the keypress is routed straight to the KeyDown event.

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 alarmClockControl_Click(object sender, EventArgs e) {   if( Control.ModifierKeys == Keys.Control ) {     MessageBox.Show("Ctrl+Click detected");   } }


Scrolling

Deriving from Control provides a broad base of functionality, although scrolling isn't supported. Scrolling is needed when the space that is required by one or more controls is greater than the space provided by a container control. Scrollbars were invented for just this situation. A scrolling control provides scroll bars to allow users to navigate to hidden bits of a control's content.

You could use a custom control to handle the logic involved in creating scroll bars and handling repainting correctly as the user scrolls across the drawing surface, but you're much better off deriving your custom control implementation from ScrollableControl:

class AlarmClockControl : ScrollableControl{...}


When you implement a scrolling control, 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 area of the control that represents the size of its scrollable surface, use the DisplayRectangle property instead. DisplayRectangle is a property of the ScrollableControl class that represents the virtual drawing area. Figure 10.22 shows the difference between ClientRectangle and DisplayRectangle.

Figure 10.22. DisplayRectangle Versus ClientRectangle


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

protected override void OnPaint(PaintEventArgs e) {   ...   // Calculate required dimensions   Size faceSize = this.DisplayRectangle.Size;   ...   // Calling the base class OnPaint   base.OnPaint(pe); }


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

Setting the Scroll Dimension

Unlike ClientRectangle, which is determined by the container of the control, 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 or from the Properties window, as shown in Figure 10.23.

Figure 10.23. Setting the AutoScrollMinSize Property


The AutoScrollMinSize property is used to tell the control when to show the scroll bars. If DisplayRectangle is larger in either dimension than ClientRectangle, scroll bars appear.

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

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 can be controls such as GroupBox or Panel, or they can be custom controls, such as user controls (covered later in this chapter).

If a child control of a scrollable control is partially or completely hidden beyond the edges of the scroll bars, you can force the scrollable control to scroll to show the child control in its entirety. To do this, you invoke ScrollControlIntoView on the scrollable control. This technique is useful when a scrollable control contains so many controls that you need to provide a UI mechanism for quickly navigating among them.

Calling ScrollControlIntoView or allowing similar behavior is not a good idea when users switch away from and back to your form. Such behavior may be surprising to users because the form they switch back to looks different from the one they switched away from. To prevent scrolling in these situations, you set the AutoScrollOffset property of your controls. AutoScrollOffset, of type Point, specifies a location (in relation to the top-left corner of the host scrollable control), and your control scrolls no closer than that point.

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

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 Windows Forms steps in.

The Control base class is roughly equivalent to the concept of a window in the operating system. It's the job of Windows Forms to take each message off the Windows message queue and route it to the Control class responsible for handling it. The base Control class turns the 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 Windows Forms. 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:

public class MyControl : Control {   ...   protected override void WndProc(ref Message m) {      // Process and/or update message      ...      // Let the base class process the message if you don't want to      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:

public partial class MainForm : Form {   ...   public MainForm() {     InitializeComponent();   }   const int WM_NCHITTEST = 0x84; // winuser.h   const int HTCLIENT = 1;   const int HTCAPTION = 2;   protected override void WndProc(ref Message m) {     switch( m.Msg ) {       case WM_NCHITTEST:         // Let the base class have first crack         base.WndProc(ref m);         // If the user clicked on the client area,         // ask the OS to treat it as a click on the caption         if( m.Result.ToInt32() == HTCLIENT ) {           m.Result = (IntPtr)HTCAPTION;         }         break;         default:           base.WndProc(ref m);           break;         }      }   }


This code handles the WM_NCHITTEST message, which is one of the few that Windows Forms doesn't expose as an event. In this case, the code calls to the Windows-provided handler (also known as its window procedure) with 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 takes care of moving the form for us.

There aren't very many 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.




Windows Forms 2.0 Programming
Windows Forms 2.0 Programming (Microsoft .NET Development Series)
ISBN: 0321267966
EAN: 2147483647
Year: 2006
Pages: 216

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