Click here to Skip to main content
15,879,326 members
Articles / Programming Languages / C#

TextBox with a Keyboard and Mouse UI

Rate me:
Please Sign up or sign in to vote.
4.69/5 (6 votes)
2 Aug 2017CPOL10 min read 15.2K   662   5  
This article discusses an implementation of a scrolling TextBox and a UI that supports key and mouse events.

1. Introduction Table of Contents

When building a user interface that includes a TextBox [^], it is sometimes useful to include a scrollbar and allow the use of various keyboard keys to shift the displayed text up and down. In addition, the UI should react to a continual press of keyboard keys and to the mouse wheel. This article discusses an implementation of a scrolling TextBox and a UI that supports key and mouse events.

A note on typography

In the following discussions, properties that are specified by the developer are displayed in bold black. Variables, used internally by the software are displayed in italicized text.

2. The TextBox Table of Contents

For this article, I am going to use the Visual Studio Designer [^]. The TextBox is dragged from the ToolBox [^] and the following properties are modified as indicated:

  • Name = "contents_TB";
  • BackColor = Color.White;
  • Font = Lucida Console
  • Font style = Regular
  • Font size = 10 point
  • Location = new Point ( 8, 10 );
  • Multiline = true;
  • ReadOnly = true;
  • Size = new Size ( 445, 281 );
  • TabIndex = 0;
  • TabStop = false;
  • Tag = "contents_TB";
  • KeyDown += new KeyEventHandler ( this.TB_KeyDown );
  • KeyUp += new KeyEventHandler ( this.TB_KeyUp );

When ReadOnly is set true, the control's BackColor is set to Gray, something that is not desired. Therefore the BackColor is explicitly set to White. The Font Lucida Console is a monospace font that maintains a consistent spacing of the TextBox contents. With Multiline set true, Font size set to 10, and TextBox Height set to 281px, the TextBox will display 16 lines.

TextBox geometry is calculated by determine_textbox_geometry.

C#
:
const int   SPACE = ( int ) ' ';
const int   TILDE = ( int ) '~';
:
int         maximum_textbox_lines = 0;
:
// ******************************** determine_textbox_geometry

void determine_textbox_geometry ( TextBox text_box )
    {
    int   character_height = 0;
    Font  font = text_box.Font;
    Size  proposed_size = new Size ( int.MaxValue,
                                     int.MaxValue );
                                // for each printing character
                                // determine its size and
                                // revise character_height
    for ( int i = SPACE; ( i <= TILDE ); i++ )
        {
        char    ch = Convert.ToChar ( i );
        Size    size;
        string  str = ch.ToString ( );

        size = TextRenderer.MeasureText (
                                    str,
                                    font,
                                    proposed_size,
                                    TextFormatFlags.Default );
        if ( size.Height > character_height )
            {
            character_height = size.Height;
            }
        }

    maximum_textbox_lines = text_box.Size.Height /
                            character_height;
    }

Although not required for this application, the maximum character width could be easily retrieved from the results of the TextRenderer.MeasureText [^] method.

3. The Vertical Scroll Bar Table of Contents

A Vertical ScrollBar [^] consists of a shaded shaft with an arrow button at each end and a scroll box (sometimes called a thumb) between the arrow buttons.

Image 5

Minimum specifies the scrollbar value at the top of the scrollbar
Clicking the Line up arrow moves the thumb up the number of lines specified in the SmallChange property (defaults to 1)
Clicking in the Page up area moves the thumb up the number of lines specified in the LargeChange property (defaults to 10)
Thumb is the current position (at the property Value)
Clicking in the Page down area moves the thumb down the number of lines specified in the LargeChange property (defaults to 10)
Clicking the Line down arrow moves the thumb down the number of lines specified in the SmallChange property (defaults to 1)
Maximum specifies the scrollbar value at the bottom of the scrollbar

 

At run-time, the vertical scrollbar is placed on the right side of the TextBox. This allows naming the scrollbar as well as specifying some of its properties. The vertical scrollbar is created by initialize_VScrollBar.

C#
:
const int   MAXIMUM_LINES_IN_FILE = 100;
:
const int   VSB_WIDTH = 20;
:
// ************************************* initialize_VScrollBar

// https://msdn.microsoft.com/en-us/library/
//     system.windows.forms.vscrollbar(v=vs.90).aspx

VScrollBar initialize_VScrollBar ( TextBox  text_box )
    {
    VScrollBar vsb = new VScrollBar ( );

    vsb.Name = "vertical_VSB";

    vsb.Location = new Point ( ( text_box.Width - VSB_WIDTH ),
                               0 );
    vsb.Size = new Size ( VSB_WIDTH,
                          ( text_box.Height - 3 ) );

    vsb.Scroll += new ScrollEventHandler ( VSB_Scroll );

    vsb.Cursor = Cursors.Arrow;

    vsb.Minimum = 0;
    vsb.Maximum = MAXIMUM_LINES_IN_FILE;
    vsb.Value = 0;
    vsb.LargeChange = maximum_textbox_lines / 2;

    return ( vsb );
    }

For this application, MAXIMUM_LINES_IN_FILE can be set to 100. In the "real world" the value of FileInfo.Length [^] would be used to set vsb.Maximum.

4. TextBox and ScrollBar together Table of Contents

When provided with a Scrollbar [^], a TextBox can display a data object, such as a document or a bitmap, that is larger than the TextBox's client area. The user can scroll a data object in the client area to bring into view the portions of the object that extend beyond the borders of the TextBox.

Image 7

We can consider the TextBox as a viewport. When the TextBox is displaying the contents of a large text file, then the TextBox can only display a small amount of that file. But if the TextBox can scroll, new portions of the file can come into view as earlier portions that were viewed go out of view.

When the Line down arrow (of the vertical scrollbar) is clicked, the TextBox scrolls down through the contents of the text file by the amount defined by the scrollbar's SmallChange property; when the Page down area (of the vertical scrollbar) is clicked, the TextBox scrolls down through the contents of the text file by the amount defined by the scrollbar's LargeChange property. Likewise for Line up and Page up.

 
Note that there are no direct connections between the vertical scrollbar, created at run time, and the TextBox other than those provided by the programmer. Unless such connections are made, actions taken on the scrollbar (such as Page up, Page down, etc.) are wholly independent of the TextBox, even though the scrollbar control has been added to the TextBox.

The connection between the ScrollBar and the Textbox is accomplished through the ScrollEventHandler [^] named VSB_Scroll.

C#
// ************************************************ VSB_Scroll

void VSB_Scroll ( Object          sender,
                  ScrollEventArgs e )
    {
    VScrollBar  vsb = ( VScrollBar ) sender;

    offset = e.NewValue;
    vsb.Value = offset;

    refill_text_box ( ( TextBox ) vsb.Parent );
    }

When the ScrollEvent is raised, VSB_Scroll is executed (see initialize_VScrollBar, above). The value returned in e.NewValue is the numeric value that represents the new position of the scroll box on the scrollbar control. e.NewValue does not affect the current position of the Thumb until Value is revised. offset is a global variable that will be used to obtain the next block of text to be displayed in the TextBox. After setting offset to the correct value, refill_text_box is invoked. This method, in this case, connects the vertical ScrollBar with the TextBox.

C#
// ******************************************* refill_text_box

void refill_text_box ( TextBox  text_box )
    {
    int             end = 0;
    StringBuilder   sb = new StringBuilder ( );

    if ( offset < 0 )
        {
        offset = 0;
        }
    if ( offset >= ( vertical_VSB.Maximum -
                     maximum_textbox_lines ) )
        {
        offset =  vertical_VSB.Maximum -
                  maximum_textbox_lines;
        }
    vertical_VSB.Value = offset;

    end = Math.Max ( 0,
                     Math.Min ( ( offset +
                                  maximum_textbox_lines ),
                                vertical_VSB.Maximum ) );

    text_box.Suspend ( );       // see ControlExtensions.cs
    text_box.Clear ( );
    for ( int i = offset; ( i < end ); i++ )
        {
        sb.AppendFormat ( "textbox line {0:D2}{1}",
                          ( i + 1 ),
                          Environment.NewLine );
        }
    if ( sb.Length >= Environment.NewLine.Length )
        {
        sb.Length -= Environment.NewLine.Length;
        }

    text_box.Text = sb.ToString ( );
    text_box.Select(0, 0);
    text_box.ScrollToCaret ( );
    text_box.Resume ( );        // see ControlExtensions.cs
    text_box.Visible = true;

    textbox_line_count = text_box.Lines.Length;
    lines_displayed_TB.Text = textbox_line_count.ToString ( );
    maximum_lines_TB.Text = vertical_VSB.Maximum.ToString ( );

    text_box.Focus ( );
    }

First refill_text_box insures that offset is within acceptable bounds and then sets the vertical Scrollbar Value to the result. It also sets the value of end to an appropriate value. At this point the TextBox is ready to be refilled with data. Note that the TextBox data is a simulation of a text file. In a "real world" case, refill_text_box would fill the TextBox with data from a source other than a simulated data file. See Caution, below.

The TextBox Suspend and Resume, based upon the Win32 API LockWindowUpdate [^], disables or enables drawing in the specified window. Both are contained in the Win32 class in the Utilities project, included in the downloads. They are used to eliminate flicker At the end of the first invocation of refill_text_box, the demonstration user interface appears as follows:

Image 8

5. TextBox and keyboard keys Table of Contents

So far, the TextBox display is only affected by the Page up, Page down, Line up, Line down, and Thumb movement in the vertical ScrollBar. To extend control to the keyboard keys, we need to capture the KeyDown [^] and KeyUp [^] events. These two events are triggered when a keyboard key is pressed and released.

Image 10

The keys in which we are interested are:

  • Home
  • Page Up
  • End
  • Page Down
  • Up Arrow
  • Down Arrow

We also want to respond to a key that is being held down (with the exception of Home and End). This requirement will necessitate the use of a timer.

 

6. Timers Table of Contents

There are two classes of Timers: System.Windows.Forms Timers [^] and System.Timers Timers [^]. In our code we will be using the Windows Timer. It has advantages over the Timers Timer in that it executes in the user interface thread. This in turn avoids the need to create a delegate to process the Tick event from the thread on which the Timers Timer executes.

Setting up a timer is relatively simple, especially if it is a Windows Timer. First the timer is declared with its Interval set to the desired repeat rate; its Enabled state set false; and its event handler (Tick) defined as timer_Tick. Once the timer is enabled, timer_Tick will be invoked every Interval.

In the application constructor (TestKeysAndWheel), the OnApplicationExit event handler is defined. Although this event handler is not strictly required, it is declared here so that the timer will be disposed when the application exits.

Every time that timer_Tick executes, it resets the Interval. It continues execution only if key_down is true and scroll_increment is non-zero. scroll_increment can contain a negative value for upward scrolling; a zero value if the key pressed was not one of those in which we are interested; and a positive value for downward scrolling. scroll_increment is further discussed below.

C#
:
using System.Windows.Forms;
    :
    const int   TIMER_REPEAT_DELAY = 400;  // in milliseconds
    :
    bool        key_down = false;
    :
    Timer       timer = null;
    :
    // ******************************* initialize_global_variables

    void initialize_global_variables ( TextBox  text_box )
        {
        :
        key_down = false;
        :
        timer = new Timer
                    {
                    Interval = TIMER_REPEAT_DELAY,
                    Enabled = false
                    };
        timer.Tick += new EventHandler ( timer_tick );
        }
    :
    // ****************************************** TestKeysAndWheel

    public TestKeysAndWheel ( )
        {

        InitializeComponent ( );

        Application.ApplicationExit += new EventHandler (
                                            OnApplicationExit );
        :
        initialize_global_variables ( contents_TB );
        :
        }

    // ***************************************** OnApplicationExit

    void OnApplicationExit ( object    sender,
                             EventArgs e )
        {

        if ( timer != null )
            {
            if ( timer.Enabled )
                {
                timer.Stop ( );
                }
            timer.Dispose ( );
            timer = null;
            }
        }
    :
    // ************************************************ timer_tick

    void timer_tick ( object    sender,
                      EventArgs e )
        {

        timer.Interval = TIMER_REPEAT_DELAY;
        if ( key_down )
            {
            if ( scroll_increment != 0 )
                {
                offset += scroll_increment;
                refill_text_box ( contents_TB );
                }
            }
        }

7. KeyDown and KeyUp events Table of Contents

The KeyDown event is raised when a user presses a keyboard key. The KeyUp event is raised when the user releases the key. The event handlers for these events are TB_KeyDown and TB_KeyUp, respectively.

The key value passed to TB_Down in e.KeyCode is one of values defined in the Keys Enumeration [^]. The first task for TB_KeyDown is to determine what key was pressed and assign an appropriate value to scroll_increment. Note that trigger_timer is set true by default. If e.KeyCode is either Home or End, trigger_timer is set false (no matter how long either Home or End is held down, the TextBox cannot be scrolled above its top or below its bottom).

If trigger_timer is true, then the timer Interval is set to 1 and both key-down and timer Enabled are set true. Interval is set to one to insure that timer_Tick executes almost immediately (Interval may not be set to zero).

If trigger_timer is false, then either the Home or End key was pressed. In that case, the timer does not have to be started, offset can be set to scroll_increment, and refill_text_box can be invoked.

C#
// ************************************************ TB_KeyDown

void TB_KeyDown ( object       sender,
                  KeyEventArgs e )
    {
    Keys    key = e.KeyCode;
    TextBox text_box = ( TextBox ) sender;
    bool    trigger_timer = true;
                                // compute scroll_increment
                                // and either start timer or
                                // directly dispatch refill
    if ( key == Keys.Down )
        {
        scroll_increment = 1;
        }
    else if ( key == Keys.Up )
        {
        scroll_increment = -1;
        }
    else if ( key == Keys.PageDown )
        {
        scroll_increment = maximum_textbox_lines;
        }
    else if ( key == Keys.PageUp )
        {
        scroll_increment = -maximum_textbox_lines;
        }
    else if ( key == Keys.Home )
        {
        trigger_timer = false;
        scroll_increment = -vertical_VSB.Maximum;
        }
    else if ( key == Keys.End )
        {
        trigger_timer = false;
        scroll_increment = vertical_VSB.Maximum;
        }

    if ( scroll_increment != 0 )
        {
        if ( trigger_timer )
            {
            timer.Interval = 1; // cannot be zero
            key_down = true;
            timer.Enabled = true;
            }
        else
            {
            offset += scroll_increment;
            refill_text_box ( text_box );
            }
        }
    }

As long as key_down is true, the timer will continue to repeat its execution. However, when the user releases the key, a KeyUp event is raised. This event is handled by the TB_KeyUp event handler. All that TB_KeyUp must do is to set key_down false, and stop execution of timer_Tick. The latter is accomplished by setting Enabled to false.

C#
// ************************************************** TB_KeyUp

void TB_KeyUp ( object       sender,
                KeyEventArgs e )
    {

    key_down = false;
    timer.Enabled = false;
    }

Combining all of these methods takes on the following form.

Image 13

 

8. TextBox and Mouse Wheel Table of Contents

Every time that the mouse wheel moves, the MouseWheel [^] event is raised. Unfortunately, the Visual Studio 2008 Designer does not include this event in its list of events for either the TextBox or the Form [^]. So the event handler must be declared at run time.

In the following code fragment MouseWheel constants and variables are first declared. Then, if a mouse wheel is detected, the event handler TB_MouseWheel is declared.

TB_MouseWheel event handler responds to each click of the mouse wheel by first determining if the wheel has rotated sufficiently to require TextBox scrolling (e.Delta indicates the amount the mouse wheel has been moved). If so, lines_to_move and offet are computed and refill_text_box is invoked.

C#
    :
using System.Windows.Forms;
    :
    const int   DELTA_UNITS_OF_WHEEL_MOVEMENT = 120;
    :
    bool        mouse_wheel_present_lines = 
                    SystemInformation.MouseWheelPresent;
    int         mouse_wheel_scroll_lines = 
                    SystemInformation.MouseWheelScrollLines;
    :
    // ****************************************** TestKeysAndWheel

    public TestKeysAndWheel ( )
        {
        :
        if ( mouse_wheel_present )
            {
            contents_TB.MouseWheel += new MouseEventHandler ( 
                                            TB_MouseWheel );
            }
        :
        }
    :
    // ********************************************* TB_MouseWheel

    void TB_MouseWheel ( object         sender, 
                         MouseEventArgs e )
        {
        int      lines_to_move = 0;
        TextBox  text_box = ( TextBox ) sender;

        if ( Math.Abs ( e.Delta ) >= 
             DELTA_UNITS_OF_WHEEL_MOVEMENT )
            {
            lines_to_move =
                ( e.Delta * mouse_wheel_scroll_lines ) /
                DELTA_UNITS_OF_WHEEL_MOVEMENT;
            offset = vertical_VSB.Value + lines_to_move;
            refill_text_box ( text_box );
            }
        }

9. Demonstration Table of Contents

A demonstration project has been included in the downloads. Upon execution it displays a TextBox with sixteen lines. As the various key, mouse, or mouse wheel events are raised, the TextBox scrolls appropriately.

The demonstration also includes an Event TextBox that displays what event has most recently been processed. This, along with Lines Displayed and Maximum Lines, are for demonstration purposes only, and should be removed in a "real world" application.

10. Conclusion Table of Contents

This article has presented methods by which a TextBox can be scrolled using the keyboard, mouse, and mouse wheel. It also includes mechanisms to respond to keys that are held down.

11. Caution Table of Contents

In the real world, the code in refill_text_box, specifically the fragment

C#
for ( int i = offset; ( i < end ); i++ )
    {
    sb.AppendFormat ( "textbox line {0:D2}{1}",
                      ( i + 1 ),
                      Environment.NewLine );
    }
if ( sb.Length >= Environment.NewLine.Length )
    {
    sb.Length -= Environment.NewLine.Length;
    }

must be replaced by non-simulation code. In a soon to follow article, this code will be replaced by:

C#
                            // read a screen-full of data
read_data (     file_stream,
                offset,
                buffer,
            ref bytes_read,
            ref eof_input );
lines = bytes_read / MAXIMUM_ENTRIES_PER_LINE;
                            // if remainder is > 0, a
                            // partial line is at the end
                            // of the buffer
remainder = bytes_read % MAXIMUM_ENTRIES_PER_LINE;
ch_buffer.Length = 0;
index = 0;
line_buffer.Length = 0;
                            // process whole lines
for ( int line = 0; ( line < lines ); line++ )
    {
    for ( int j = 0;
            ( j < MAXIMUM_ENTRIES_PER_LINE );
              j++ )
        {
        insert_byte ( ref line_buffer,
                      ref ch_buffer,
                          buffer [ index++ ] );
        }
    complete_line ( ref line_buffer,
                    ref ch_buffer,
                    ref starting_byte,
                    ref contents_TB );
    }
                            // process the remainder
if ( remainder > 0 )
    {
    int  empty_entries = 0;

    for ( int j = 0; ( j < remainder ); j++ )
        {
        insert_byte ( ref line_buffer,
                      ref ch_buffer,
                          buffer [ index++ ] );
        }

    empty_entries = MAXIMUM_ENTRIES_PER_LINE -
                    remainder;
                            // pad end of line_buffer
    for ( int j = 0; ( j < empty_entries ); j++ )
        {
        line_buffer.Append ( "    " );
        }
    complete_line ( ref line_buffer,
                    ref ch_buffer,
                    ref starting_byte,
                    ref contents_TB );
    }

This fragment reads buffer.Length bytes from a FileStream [^] into a buffer starting at offset. This is the type of code that must replace refill_text_box in the real world.

12. References Table of Contents

FileInfo.Length [^]
FileStream Class [^]
Form Class [^]
Keys Enumeration [^]
KeyDown Event [^]
KeyUp Event [^]
LockWindowUpdate [^]
MouseWheel Event [^]
Scrollbar Class [^]
ScrollBar.Value Property [^]
ScrollEventArgs Class [^]
ScrollEventHandler Delegate [^]
TextBox Class [^]
TextRenderer.MeasureText [^]
Timers Timer Class [^]
Visual Studio 2008 Designer [^]
Visual Studio ToolBox [^]
VScrollBar Class [^]
Windows Timer Class [^]

13. Development Environment Table of Contents

The software presented in this article was developed in the following environment:

Microsoft Windows 7 Professional Service Pack 1
Microsoft Visual Studio 2008 Professional
Microsoft .Net Framework Version 3.5 SP1
Microsoft Visual C# 2008

14. History Table of Contents

08/01/2017     Original article

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Software Developer (Senior)
United States United States
In 1964, I was in the US Coast Guard when I wrote my first program. It was written in RPG (note no suffixing numbers). Programs and data were entered using punched cards. Turnaround was about 3 hours. So much for the "good old days!"

In 1970, when assigned to Washington DC, I started my MS in Mechanical Engineering. I specialized in Transportation. Untold hours in statistical theory and practice were required, forcing me to use the university computer and learn the FORTRAN language, still using punched cards!

In 1973, I was employed by the Norfolk VA Police Department as a crime analyst for the High Intensity Target program. There, I was still using punched cards!

In 1973, I joined Computer Sciences Corporation (CSC). There, for the first time, I was introduced to a terminal with the ability to edit, compile, link, and test my programs on-line. CSC also gave me the opportunity to discuss technical issues with some of the brightest minds I've encountered during my career.

In 1975, I moved to San Diego to head up an IR&D project, BIODAB. I returned to school (UCSD) and took up Software Engineering at the graduate level. After BIODAB, I headed up a team that fixed a stalled project. I then headed up one of the two most satisfying projects of my career, the Automated Flight Operations Center at Ft. Irwin, CA.

I left Anteon Corporation (the successor to CSC on a major contract) and moved to Pensacola, FL. For a small company I built their firewall, given free to the company's customers. An opportunity to build an air traffic controller trainer arose. This was the other most satisfying project of my career.

Today, I consider myself capable.

Comments and Discussions

 
-- There are no messages in this forum --