TextBox with a Keyboard and Mouse UI






4.69/5 (6 votes)
This article discusses an implementation of a scrolling TextBox and a UI that supports key and mouse events.
- Download TestKeysAndWheel Demonstration Executable - 11.3 KB
- Download TestKeysAndWheel Project - 12.2 KB
- Download Utilities Project containing some useful methods - 5.3 KB
Table of Contents
- 1. Introduction
- 2. The TextBox
- 3. The Vertical Scroll Bar
- 4. TextBox and ScrollBar together
- 5. TextBox and keyboard keys
- 6. Timers
- 7. KeyDown and KeyUp events
- 8. TextBox and Mouse Wheel
- 9. Demonstration
- 10. Conclusion
- 11. Caution
- 12. References
- 13. Development Environment
- 14. History
The symbol returns the reader to the top of the Table of Contents.
1. Introduction
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
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.
:
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
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.
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.
:
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
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.
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.
// ************************************************ 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.
// ******************************************* 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:
5. TextBox and keyboard keys
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.
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
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.
:
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
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.
// ************************************************ 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.
// ************************************************** 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.
8. TextBox and Mouse Wheel
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.
:
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
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
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
In the real world, the code in refill_text_box, specifically the fragment
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:
// 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
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
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
08/01/2017 | Original article |