Introduction
Here is a good question for the CodeProject poll. How many of us have wanted to write an interpreter for a language of our own? I'm sure that would include most of us. All of us who actually tried to write one would have written it as a command line application. What if I wanted to embed it in a Windows application? Out of that question was born this control, which I call the ShellControl
. It has nothing to do with the OS shell, although it can be used as a UI for one. In fact, the sample application does just that, it gets commands from the user, uses cmd.exe to run it, and displays the results in the UI. Think of it as the command tool window in Visual Studio.
What it is?
The ShellControl
is a .NET User Control that you can embed directly in your Windows Forms applications. It emulates a command line UI, like the DOS shell. It is pretty basic in functionality. It provides the following features:
- Prompt - It provides a prompt, which can be changed anytime. It works like the prompt we are used to see in the DOS shell, i.e., it can't be erased out.
- Command History - It maintains a history of the commands executed so far. The up/down arrow keys can be used to move up/down the history just like the DOS shell.
- Autocompletion - It provides auto completion for the last command. The right arrow key can be used for this purpose.
- All other features of a standard
TextBox
, because the ShellControl
in reality is just a modified TextBox
, it provides all the normal functionality of the standard Windows TextBox
. This includes using the mouse to select text, copy/paste, undo/redo etc.
How to use it?
Pretty simple. Add the control like any normal User Control (Right click on the ToolBox, click Add/Remove Items and Browse to ShellControl.dll). Subscribe to the CommandEnteredEvent
. Do whatever you want with the command text passed in as part of the CommandEnteredEventArgs
object. Here is some sample code:
public Form1()
{
InitializeComponent();
shellControl1.CommandEntered += new
UILibrary.EventCommandEntered(shellControl1_CommandEntered);
}
void shellControl1_CommandEntered(object sender,
UILibrary.CommandEnteredEventArgs e)
{
string command = e.Command;
if (!ProcessInternalCommand(command))
{
ProcessStartInfo startInfo = new ProcessStartInfo("cmd.exe");
startInfo.Arguments = "/C " + e.Command;
startInfo.RedirectStandardError = true;
startInfo.RedirectStandardOutput = true;
startInfo.UseShellExecute = false;
startInfo.CreateNoWindow = true;
Process p = Process.Start(startInfo);
string output = p.StandardOutput.ReadToEnd();
string error = p.StandardError.ReadToEnd();
p.WaitForExit();
if (output.Length != 0)
shellControl1.WriteText(output);
else if (error.Length != 0)
shellControl1.WriteText(error);
}
}
The code above gets the command text and executes it by starting a new cmd.exe process. It then writes back the result to the control. It can't get any simpler, can it?
Other Properties And Methods
Besides the event, the control exposes the following properties and methods:
Properties
Prompt
- Use it to get/set the prompt displayed in the control.
ShellTextForeColor
- Set/Get the color of the foreground text.
ShellTextBackColor
- Set/Get the background color.
ShellTextFont
- Set/Get the font of the console text.
Methods
WriteText(string text)
- Use it to write text to the ShellControl
. You need to remember that the control writes the prompt on the display after every call to WriteText
, so if you are writing multiple lines, be sure to append them to a single string before calling WriteText
.
Clear
- Clears the console, leaving just the prompt.
GetCommandHistory
- Returns a string array of the commands entered so far.
How it works?
The ShellControl
is a class derived from UserControl
. All it does is wrap ShellTextBox
, which derives from TextBox
. Two questions here.
- Why not directly expose
ShellTextBox
?
Because that would expose all properties of the TextBox
, including MultiLine
, Text
, MaxLength
etc. And that would be disastrous because the ShellControl
needs to tightly control the addition of text to make sure that nothing funny happens. For e.g., the ShellControl
needs to make sure that you can't modify text that's above the current line, it needs to ensure that you can't erase the prompt and so on..
- Why derive from
TextBox
, why not aggregate it?
Because ShellTextBox
needs to override WndProc
. And that's necessary to make sure that Cut/Paste/Clear from the Context menu don't do anything silly.
The overridden WndProc
looks like this:
protected override void WndProc(ref Message m)
{
switch(m.Msg)
{
case 0x0302:
case 0x0300:
case 0x000C:
if (!IsCaretAtWritablePosition())
MoveCaretToEndOfText();
break;
case 0x0303:
return;
}
base.WndProc(ref m);
}
As you can see, WM_CLEAR
has no effect, while Cut/Paste are made to operate on the text currently at the prompt.
For controlling key presses, the ShellTextBox
subscribes to the KeyPress
and KeyDown
events of the TextBox
. The KeyPress
event handler handles backspace and Enter keys while other keys are handled by the KeyDown
event handler. A utility class, CommandHistory
, maintains the list of commands entered as well as the current position in that list. The up/down arrow keys use information in that list to display the command history. That section looks like this:
private void ShellControl_KeyDown(object sender, KeyEventArgs e)
{
if (!IsCaretAtWritablePosition() &&
!(e.Control || IsTerminatorKey(e.KeyCode)))
{
MoveCaretToEndOfText();
}
if (e.KeyCode == Keys.Left && IsCaretJustBeforePrompt())
{
e.Handled = true;
}
else if (e.KeyCode == Keys.Down)
{
if (commandHistory.DoesNextCommandExist())
{
ReplaceTextAtPrompt(commandHistory.GetNextCommand());
}
e.Handled = true;
}
else if (e.KeyCode == Keys.Up )
{
if (commandHistory.DoesPreviousCommandExist())
{
ReplaceTextAtPrompt(commandHistory.GetPreviousCommand());
}
e.Handled = true;
}
else
{
}
}
When the enter key is pressed, the ShellTextBox
gets the text currently at the prompt and calls FireCommandEntered
on ShellTextControl
, which in turn fires the CommandEntered
event. That's all there is to it.
Conclusion
Although I'm fairly comfortable in C#, this is my first attempt at creating UserControls. I know this is not going to be the most downloaded control in CodeProject, but I feel it's still useful for certain situations where you want to emulate the command line.
Feedback, comments are welcome.
History
Feb 17 2005 - Initial submission.