Spice Up Console Apps
by Bill McCarthy and Brian Noyes
|
Technology Toolbox
VB.NET, C#, XML, VB6, VB5
|
Q: Make Console.Read()Return From Any Key
It's great that VB.NET makes writing console apps so easy compared to VB6, but I find limitations in what I can do. I want my application to respond as the user presses any key. I've tried using Console.Read(), but it doesn't seem to return unless the user presses the Enter key only. Is this a bug, or do I need to do something else? Also, can I change the color of the text in the console window?
A: .NET's Console class is simply a wrapper for the Win32 API. You can write your own wrapper class in VB6 or VB.NET that extends and enhances this basic functionality, but you can't inherit from the .NET Console class because it's sealed. You must write a separate class instead.
Some of the API calls are reasonably straightforward. Set or get the console window's caption, for example, by creating a Text property in your class that simply calls the Win32 API functions SetConsoleTitle and GetConsoleTitle, respectively.
Getting and setting the console text forecolor and backcolor is a little more complex (see Figure 1). You must specify both forecolor and backcolor even if you want to set only one of them. This requirement doesn't match typical object-property-to-single-field coding practices, but you can work around that by retrieving the colors in the Property Get and calling on that Property Get in the other color's Property Let/Set (this works for both VB6 and VB.NET). You retrieve the colors by calling the GetConsoleScreenBufferInfo API and returning the wAttributes member, which is a 16-bit integer value. The forecolor value is stored in the integer's lower four bits (&H0 to &HF), and the backcolor value is stored in the next four bits (&H10 to &HF0). The code for setting and retrieving the console colors is based in part on code posted by Mattias Sjögren to the public newsgroups.
The code becomes more complex in the Console.Read() function. The VS.NET help file states that Console.Read() "will not return until the read operation is terminated (for example, by the user pressing the enter key)," so this behavior is by design. You must work around this limitation if you want your code to respond to any key.
The main reason .NET's Console.Read() behaves this way is probably because it's easier. If you turn off the ENABLE_LINE_INPUT flag with the SetConsoleMode API, the ReadConsole API returns when any key is pressed—but then you must echo the keystrokes to the output console manually because the ENABLE_ECHO_INPUT flag requires the ENABLE_LINE_INPUT flag to be set.
You can echo the keystrokes to the output console manually with the PeekConsoleInput API. You use this API to read directly from the console's input buffer, store the records returned in an array, then flush the buffer immediately by calling the FlushConsoleInputBuffer API. You then loop through the records and echo the keystrokes to the console's output buffer. You can use the WriteConsole API to write to the output buffer for the ASCII keys. But for keys such as Backspace, Delete, and left and right arrows, you must use the SetConsoleCursorPosition API to set the cursor position in the output screen to the correct place, and if necessary, overwrite the existing output to clear it.
The SetConsoleCursorPosition API expects a COORD structure to be passed to it ByVal. You can do this easily in VB.NET. But in VB6, you must pass a Long (32-bit integer) as the second parameter because of a user-defined type (UDT) limitation—VB6 doesn't let you pass a UDT out to an API ByVal. The API expects the actual UDT's value rather than a pointer to it, so you use a 32-bit Long with the high word specifying the y coordinate and the low word specifying the x coordinate. Note: The API declaration as specified in the WINAPI.txt file that ships with VB6 is incorrect and doesn't work.
As soon as you allow the user to use the left and right arrow keys, handling the string that represents the buffer gets trickier. The easiest way to deal with the complexities is to split the string into two—one as the string before the cursor position and the other as the string after the cursor position. You can do this with a little help from the Read function (see Listing 1).
Once your Read function reads from the input buffer and writes to the output buffer, you wrap it in a loop that watches for a set of termination criteria, such as receiving the Enter key or a termination request from elsewhere in your application. You can receive and block a termination request by using a callback function and specifying its address to the SetConsoleCtrlHandler API. In your callback function, you can set a flag that the Read and ReadLine functions use to determine if they should return immediately. The callback function returns 1 to prevent immediate termination of your application.
This might seem an excessive amount of work merely to read from any key, but you get another benefit from echoing the keystrokes manually: proper code cleanup and resource release. The ReadConsole API is synchronous, so it doesn't return until something is in the input buffer. The disadvantage of this synchronous behavior: If a user requests to close your program, your program can't respond properly, so you can't ensure proper cleanup and resource release. When the user tries to close the console window, Windows issues an exit-process command if your application doesn't handle the close request—the equivalent of calling End in VB6. I won't go into the debate about the evils of using End; suffice it to say, your code should clean up properly and release all resources. Alternatives to using the synchronous ReadConsole API or .NET's Console. Read() function help you do that. If you use PeekConsoleInput, your code can read any key and still shut down correctly.
Finally, to use a console window from VB6, you must either allocate a console to the GUI application or change the application type to a console application. Associate your application's process with a console by calling the AllocConsole API in the class-initialize event and the FreeConsole API in the class-terminate event. This allows you to use the console from the VB6 IDE. Alternatively, change the bit flag in the executable binary so it runs as a console application (see Resources).
A nice VB.NET feature you might want to include in your VB6 class is the ability to specify parameters in the Console.Write() function (see the clsConsole.cls file in the sample for the equivalent VB6 code). The function works with most VB vartypes except UDTs—because another VB6 limitation is that you can't pass a UDT as a Variant unless you declare it in ActiveX.exe. Fortunately, VB.NET's structures address most of these limitations. —B.M.
Q: Display XML in a DataGrid
I have an XML fragment that contains record data I want to display in a DataGrid. How do I get this data into the DataGrid for display? My data contains simple string and numeric data like this, so I don't use a schema to control the data types:
<?xml version="1.0" encoding="utf-8" ?>
<employees>
<employee>
<Name>Joe Bagodonuts</Name>
<Position>Senior Analyst</Position>
<Salary>70,000</Salary>
</employee>
<employee>
<Name>Al Knowing</Name>
<Position>Corporate Psychic</Position>
<Salary>25,000</Salary>
</employee>
</employees>
A: The DataGrid is by far one of the .NET Framework's most useful and powerful controls. You display data in a DataGrid control by binding the control to data contained in a DataSet. You want to pump your XML fragment into a DataSet and bind that DataSet to the DataGrid for display (see Figure 2).
The way to do this has changed between VS.NET beta 1 and beta 2. In beta 1, the DataSet class exposed Xml and XmlSchema properties you could simply set. In beta 2, you must put your XML fragment into a stream to pass into the DataSet, but it's still a piece of cake. Simply construct a StringReader and pass it the string that contains your XML. Then pass this StringReader to the DataSet.ReadXml() method. Calling DataGrid.SetDataBinding() updates the data in the grid and refreshes the view:
DataSet myDataSet = new DataSet();
// Get XML fragment from form field or somewhere
string xml = myForm.Text;
StringReader sr = new StringReader(xml);
myDataSet.ReadXml(sr);
dataGrid1.SetDataBinding(myDataSet,
"tablename");
If the XML fragment read in has a schema defined, the DataSet uses that schema to figure out the defined tables and columns. If a schema is not included, the DataSet infers one from the layout of elements in the XML. You must figure out the equivalent element in your table's XML because that's the name you must pass to the SetDataBinding() method so it can select the right table from the DataSet (even though you'll often have only one). If you pass in null for the table name, the grid still works, but it starts out with a treeview of table names you must select from to populate the grid with the table's content. —B.N.
Q: Launch Another App
How can I launch an application from within a .NET application? Do I have to use the Windows API somehow, or does the .NET Framework offer a way to do this in managed code?
A: The class you seek, called Process, resides in the System.Diagnostics namespace. Launch an application using this class simply by calling the static version of the Start() method and passing in a filename and path to the application you want to run. This is equivalent to typing the application name into the Run dialog from Windows' Start menu:
Process.Start("Notepad.exe");
As you do with the Run command, you can also give the Start() method a filename that has a registered extension. The Process class runs the host application and passes the filename to it as a command-line parameter, just as if you had double-clicked on the file in Windows Explorer. You can also pass parameters or other arguments to the program with the Start() method:
Process.Start("regsvr32.exe",
"-u C:\\SimpleReceiver2Svr.dll");
If you want a little more control over the application you start, simply create an instance of the class, set the StartInfo property on that instance, and call the Start() method to execute the application:
Process myproc = new Process();
myproc.StartInfo = new
ProcessStartInfo("Notepad.exe");
myproc.Start();
These three lines perform the same thing as the single one presented earlier, but this way you have a living reference to the Process object and can do other things such as access debug information about the process, determine how many threads and how much memory it's using, or simply Kill() it. But beware of the latter: Kill(), like the Windows API equivalent, is always fraught with the danger of resource leaks. —B.N.
About the Authors
Bill McCarthy is a VB MVP. He is also an Ozzie, and he reckons that says it all. E-mail Bill at .
Brian Noyes is the software engineering manager for Digital Access Corp. He is an MCSD with more than 10 years of software and systems engineering experience. He is the author of available from MightyWords (www.mightywords.com/go/fawcette), as well as a technical editor and frequent contributor to and .NET Magazine. E-mail him at .
|