ONDotNet.com    
 Published on ONDotNet.com (http://www.ondotnet.com/)
 See this if you're having trouble printing code examples


C# Key Processing Techniques

by Budi Kurniawan
04/29/2002

The keys on a computer keyboard, unlike those in a conventional typewriter, fall into several categories:

Processing the complete set of keys not only requires the detection of the pressing of individual key, but also the pressing of keys in combination. For this article, I have written two versions of a user control called WhiteBoard. The first version, given in Listing 1, captures keys in the first category; however, it cannot capture function and control keys. The second version, given in Listing 3, captures all keys.

Both versions are covered in the sections "Processing Characters" and "Processing All Keys." Note that in my testing with the current version of the .NET Framework SDK (version 1.0), the KeyDown event in System.Windows.Forms.Control is not triggered when the user presses an arrow key. However, the KeyDown event in System.Windows.Forms.Form is raised when an arrow key is pressed. The Form class extends the ContainerControl class, which extends the ScrollableControl class. The ScrollableControl class is a direct child class of the Control class. The class library reference in the .NET Framework SDK documentation does not state that the KeyDown event or the OnKeyDown method are overriden in any of ContainerControl, ScrollableControl, or Form. Therefore, the behavior of this event in both the Form and the Control classes must be the same. This leads to the following possibilities: either the documentation is not up to date or there is a bug in the class library.

Processing Characters

Related Reading

C# Essentials
By Ben Albahari, Peter Drayton, Brad Merrill

If the user presses a key on the keyboard when a control has focus, three events of the control are triggered. The three key events occur in the following order:

  1. KeyDown. Occurs when the user starts pressing the key, i.e., when the key is down.
  2. KeyPress. Occurs when a key is pressed, after the KeyDown event is triggered.
  3. KeyUp. Occurs when the user releases the key.

The easiest way to capture keyboard input from the user is to use the KeyPress event of a control. The event handler for this event receives a System.Windows.Forms.KeyPressEventArgs object containing two properties:

Since the KeyChar property gives you the character of the key being pressed, displaying the character, for example, is very straightforward. However, some keys do not have visual representation and are not meant to be displayed. The backspace key, for instance, is normally used in a text-based control to delete the character to the left of the caret and move the caret back one character. In this case, you can simply convert the character into an integer and compare the integer with the ASCII value of the character.


char c = e.KeyChar;
int i = (int) c;

The backspace key will have an integer value of 8 and the carriage-return key 13. The use of the KeyPress event is illustrated in the WhiteBoard control displayed in a form in Figure 1.


Figure 1. The WhiteBoard control that captures characters.

The WhiteBoard control extends the System.Windows.Forms.UserControl class and its code is given in Listing 1.

As can be seen in Figure 1, the WhiteBoard control is a two-dimensional array of characters that has a visual interface. The two-dimensional array is represented by the variable board. private char[,] board;

The dimensions are indicated by the variables columnCount and rowCount.


board = new char[columnCount, rowCount];

Every time the OnPaint method of the control is invoked, the value of each element of the array is drawn using the DrawString method of the Graphics object of the control. The location of each character is determined by characterWidth, characterHeight, and lineSpace. The latter indicates the distance between two lines in pixels.


    protected override void OnPaint(PaintEventArgs e)
    {
      Graphics graphics = e.Graphics;
      Font font = new Font("Courier new", characterWidth);
      Brush brush = new SolidBrush(foreColor);
      for (int i=0; i<rowCount; i++) 
      {
        for (int j=0; j<columnCount; j++)
        {
          graphics.DrawString(board[i, j].ToString(), font, brush, 
            new Point(i*characterWidth, j*(lineSpace+characterHeight)));
        }
      }
    .
    .
    .

Like most decent text-based controls, our WhiteBoard control uses a caret to tell the user the location of the current character insertion point. That's right, a caret does not come free. You have to draw your own caret and make it blink (animate). In our WhiteBoard control, the location of the caret is determined by two integers: caretX and caretY. They indicate the horizontal and vertical coordinates in the visual area. Every time the user presses a key, the caret is moved forward by one character. When it reaches the last column of the last line, it will move back to the first column of the first line.

The animation of the caret is achieved by the use of the System.Threading.Thread class. Surprisingly, thanks to the classes in the System.Threading namespace, multi-threaded programming is relatively easy. The Thread class represents a thread in the program. In the WhiteBoard control, we use a thread called caretThread for displaying and animating the caret in the right position: private Thread caretThread;. When the control is created, caretThread must already be ready. Therefore, we start the caret thread in the class's constructor.


caretThread = new Thread(new ThreadStart(ShowCaret));
caretThread.Start();
In the above code, the first line constructs a Thread object and informs the thread that it is to handle the ShowCaret method. The second line starts the thread, i.e., starts executing the ShowCaret method. Before we look at the ShowCaret method, however, bear in mind that you are responsible for stopping and destroying the thread when it is no longer needed. Failure to do so will make the program unable to exit properly. In our WhiteBoard control, this is handled by overriding the control's Dispose method.

protected override void Dispose(bool disposing)
{
  if (disposing) {
    caretThread.Abort();
  }
  base.Dispose(disposing);
}

Therefore, when the control is disposed, the Thread class's Abort method will be called to terminate the thread. The ShowCaret method, as seen in Listing 1, employs an indefinite while loop that makes the thread execute the same piece of code. The code in this while loop is simple. It invalidates the part of the Graphics object that is occupied by the caret. The Update method is then called. This will invoke the OnPaint method, but the control will only redraw the area indicated by the rectangle passed to the Invalidate method. Whether or not the caret is visible is determined by the caretVisible variable. If it is true, the vertical line is drawn. Otherwise, nothing is drawn, making the caret invisible. The rate at which the caret blinks is determined by the value passed to the Sleep method of the Thread class. Here we use 350 (milliseconds). The caretVisible variable is also toggled to create the blinking effect.


      this.Invalidate( new 
            Rectangle(caretX * characterWidth, caretY * (characterHeight + lineSpace),
            caretX * characterWidth + 2 * penWidth, (caretY +1) * (characterHeight + 
            lineSpace)));
          this.Update();					
          Thread.Sleep(350);
          caretVisible = !caretVisible;
The caret itself is drawn when the OnPaint method is called:
      //draw caret here;
      if (caretVisible)
      {
        int x = caretX * characterWidth;
        int y = caretY * (lineSpace + characterHeight);
        graphics.DrawLine(pen, x, y, x, y + lineSpace + characterHeight);
      }

Now, let's discuss the KeyPressed method that is called every time a key is pressed. First, the KeyPressed method must be registered to become the handler of the KeyPress event. We do it in the class's constructor.


this.KeyPress += new KeyPressEventHandler(KeyPressed);

The handling of keys is not that complex. Basically, our simple WhiteBoard control accepts all alphanumeric characters and punctuation marks. The method also detects the keying of backspace (ASCII 8) and makes backspace function properly.


      char c = e.KeyChar;
      int i = (int) c;
      if (i==8)
      {
        if (caretX==0)
        {
          caretX = columnCount - 1;
          caretY--;
          if (caretY<0)
            caretY = rowCount - 1;
        }
        else
        {
          caretX--;
        }
        board[caretX, caretY] = ' ';

For non-backspace characters, the method moves the caret forward by one character.


        board[caretX, caretY] = c;
        caretX++;
        if (caretX == columnCount) 
        {
          caretX = 0;
          caretY++;
          if(caretY== rowCount)
            caretY = 0;
        }
      }

It then invalidates the control so that the Graphics object is repainted. Calling the Invalidate method without an argument will repaint the whole area of the Graphics object.


      this.Invalidate();
      this.Update();

The code in Listing 2 is a form that uses the WhiteBoard control.

Listing 2. A form that uses the WhiteBoard control


using System;
using System.ComponentModel;
using System.Windows.Forms;
using System.Drawing;

namespace KeyProcessor
{
	public class Form1 : System.Windows.Forms.Form
	{
		private KeyProcessor.WhiteBoard whiteBoard;
		private System.ComponentModel.Container components = null;

		public Form1()
		{
			InitializeComponent();
		}

		protected override void Dispose( bool disposing )
		{
			if( disposing )
			{
				if (components != null) 
				{
					components.Dispose();
				}
			}
			base.Dispose( disposing );
		}

		#region Windows Form Designer generated code

    private void InitializeComponent()
		{
      whiteBoard= new WhiteBoard();
      this.SuspendLayout();
      whiteBoard.Location = new Point(20,20);
      whiteBoard.Size = new Size(190, 220);

      this.AutoScaleBaseSize = new System.Drawing.Size(5, 13);
      this.ClientSize = new System.Drawing.Size(292, 273);
      this.Controls.AddRange(new System.Windows.Forms.Control[] {whiteBoard});
      this.Name = "Form1";
      this.Text = "Small Whiteboard";
      this.ResumeLayout(false);

    }
		#endregion

		[STAThread]
		static void Main() 
		{
			Application.Run(new Form1());
		}
	}
}

When you compile and run the code in Listing 2 for the first time, you'll probably get excited that so little code produces a text area with a caret that flashes on and off! After a while, however, you'll notice that our WhiteBoard control is not perfect. Here is a list of the control's imperfections. You can probably add more items to this list.


Figure 2. Pressing the control character and an alphanumeric
character at the same time results in a box.

So, you can see that KeyPress can only handle characters. While this limitation does not create a fuss for small and simple applications, most applications require the proper handling of all keys, not just alphanumeric and punctuation mark keys. The KeyDown event allows you to capture function keys, but not arrow keys. The KeyUp event lets you capture all keys, but this event is only triggered when the user releases the key, by which time it is probably too late to handle the key pressing. The next technique we can resort to is the ProcessDialogKey method, which enables us to capture all keys.

Processing All Keys

The ProcessDialogKey method in the System.Windows.Forms.Control class is called automatically when a key or a combination of keys on the keyboard is pressed. Unlike the KeyPress event, ProcessDialogKey can capture any key, including the Control keys. However, it does not give you the character associated with the key; it only tells you which key is being pressed. For example, it tells you the A key was pressed, but it doesn't tell you whether the character should be the capital or lower-case A. There is a way to check the character case, but this requires more code.

For a simpler solution, ProcessDialogKey can be used in conjunction with the KeyPress event. The KeyPress event is invoked after the ProcessDialogKey method is called. Therefore, you can use a flag to tell the KeyPress event handler whether or not it needs to handle a key press. This flag is controlled by the ProcessDialogKey method. If the key press is a combination of control keys and a character key, the ProcessDialogKey will handle it and reset the flag. On the other hand, if the key pressed is a character key, the ProcessDialogKey will set the flag and let the KeyPress event handler take care of the key press. When overriding the ProcessDialogKey method, you should return true when the key was handled and false otherwise.

Normally, when your ProcessDialogKey method does not process the key, you call the ProcessDialogKey method of the base class and return whatever value is returned by the base class's ProcessDialogKey method. The ProcessDialogKey method receives one of the System.Windows.Forms.Keys enumeration values as its argument. The Keys enumeration allows a bitwise combination of its values. You should look up the .NET Framework class library reference for the list of values of the Keys enumeration.

The argument sent to the ProcessDialogKey method depends on the key(s) being pressed. For instance, if the user pressed the A key, the method will receive Keys.A; if Ctrl+S is pressed, the value sent is the bitwise combination of the Control key and the S key. The down arrow sends Keys.Down, the up arrow Keys.Up, and the right and left arrows Keys.Right and Keys.Left respectively. The F1 sends Keys.F1 and Alt+F4 sends the bitwise combination of Keys.Alt and Keys.F4. When the Shift key is pressed, the method receives Keys.Shift, enabling you to know whether an upper/lower case character is being sent by the user.

The modified version of the WhiteBoard control in Listing 3 overcomes the "flaws" in the code in Listing 1 by also handling control keys via the overriding of the ProcessDialogKey. It uses a flag called keystrokeProcessed to indicate whether or not the KeyPress event handler needs to handle the key press. The code in Listing 3 is similar to the code in Listing 1, except that we now override the ProcessDialogKey method, and that the KeyPress event handler is only executed if the key has not been handled by ProcessDialogKey.

The overriding ProcessDialogKey in Listing 3 captures the following keys:

You can use the code in Listing 2 to see the modified control in action. Figure 3 shows the control with a red foreground color.


Figure 3. The modified control that can handle all keys

View Listing 3: Handling All Keys

Conclusion

Key processing is one of the most important tasks in Windows programming, used to capture and process user keyboard input. The System.Windows.Forms.Control triggers the KeyDown, KeyPress, and KeyUp events when a key is pressed; the easiest way to process a key press is by providing a handler for the KeyPress event. However, the KeyPress event only handles characters, and is useless when a control key or a combination of control and character keys are pressed. For this, the ProcessDialogKey method can be overriden to provide the handling of control keys. The ProcessDialogKey method does not give the character when a character key is pressed, therefore ProcessDialogKey can be used in conjunction with the KeyPress event for simple key press handling.

Budi Kurniawan is a senior J2EE architect and author.


Return to the .NET DevCenter.

Copyright © 2009 O'Reilly Media, Inc.