Добавил:
Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:

Visual CSharp .NET Developer's Handbook (2002) [eng]

.pdf
Скачиваний:
35
Добавлен:
16.08.2013
Размер:
4.94 Mб
Скачать

//Add an event handler for document printing details. PD.PrintPage += new PrintPageEventHandler(PD_PrintPage);

//Set up the document for printing.

PD.DocumentName = File2Open;

//Print the document. PD.Print();

//Exit the application. CommandLineOnly = true;

}

}

else

//The file wasn't found, so the command line constructor

//won't do any useful work in this case. MessageBox.Show("Invalid Filename or File Not Found\r\n" +

"(Always supply a path for the filename.)", "Command Line Argument Error", MessageBoxButtons.OK, MessageBoxIcon.Error);

}

As you can see from Listing 4.1, the standard constructor initializes the components and sets the CommandLineOnly field to false. The example requires some means of detecting when it should open the file, print it, and then exit. This field enables the application to perform the additional processing without too many problems. In most cases, you'd use such a variable only if you wanted to provide some form of automatic processing, such as printing. This enables the user to schedule the print job and not worry about finding a number of active application copies when the background tasks complete. (You'll learn how this processing works later in this section.)

The command-line version of the constructor accepts a string array named args as input. After it performs the same processing as the standard version of the constructor, the command-line version of the constructor begins parsing the command-line parameters. Notice the technique used to parse the command line for a filename. It uses the File2Open field to determine when to stop looking for a valid filename. The File2Open field is only set to a filename if the FInfo object contains a valid filename.

Notice the bit of code where we create a new FileInfo() object based on the args string. The entire process is placed within a try...catch structure, because any attempt to open an invalid file will result in an exception. So the purpose of this construction is to catch the error, if it occurs. However, the application doesn't really care if the user passes an invalid filename, because the application will simply ignore it. Consequently, the catch portion of the structure is blank. We'll discuss the try...catch structure in more detail in the "Using Try and Catch" section of the chapter.

Tip Use default command-line switches whenever possible to make your application easier to use and faster for users to learn. For example, if you plan to provide command-line help, always support /?. Likewise, printing normally uses the /p switch. The best way to find the default command-line switches—those used by the majority of Windows

applications—is to check Windows Help and Microsoft resources for the functionality you want to include in your application. Unfortunately, Microsoft has never published a list of recommended application switches.

Parsing out the /p command-line argument is easy. The only consideration is ensuring you capture the argument by setting the input to uppercase so you don't need to check for numerous forms of capitalization. If the constructor detects a /p entry in args, it sets DoPrint to true.

If the user has provided a filename that exists, the constructor calls DoFileOpen() with the name of the valid filename the user provided. This is a situation when the command-line version and the menu version of a function are similar, so using the same common function makes sense.

The else clause of the file existence check displays an error message. The only problem with this approach is that you don't know if the user is around to see the error message. You have several other options in this case. The application could simply exit and not do anything at all. However, this technique is unfriendly because the user will have every expectation that the task completed successfully. Another technique is to write an entry into the event log. Of course, the user has to know to look in the event log for errors to make this method work. Finally, because you're using this method at the command line, the application could write an informative message to the command line and exit. Of course, the command line might not exist after the application exists, so again, you're facing a situation where the user might not see the error message. The remaining code determines if the user wants to load the file or print it. If the user wants to print the file, the constructor provides a custom print routine that doesn't rely on any user interaction. This means the application uses defaults for every aspect of the print job. Notice the last task the code performs is setting the CommandLineOnly field to true. The following code shows how the CommandLineOnly field affects application operation.

private void Form1_Activated(object sender, System.EventArgs e)

{

//Exit the application if we're using the commandline

//only for printing.

if (CommandLineOnly) Close();

}

As you can see, the code closes the form if the user has chosen to use the command-line version of the application to print a file. However, at first glance it's easy to think you could simply close the application from the constructor. The truth is that the application isn't active at this point—the constructor is creating the form. Attempting to close the application in the constructor won't work because there's nothing to close. The Activated event occurs after the constructor returns to the call in Main(), shown here.

static void Main(string[] args)

{

if (args.Length == 0)

// Use the standard constructor. Application.Run(new Form1());

else

// Use the command line constructor. Application.Run(new Form1(args));

}

In short, the command-line version of the constructor opens the file, prints it if necessary, and then returns to the Application.Run(new Form1(args)) statement in Main(). At this point, the application becomes active, and you can close it if desired. The first place you can close the application is in the Form1_Activated() method. It's one of those odd concepts to twist your brain around, but an important concept to remember.

Displaying the Text

Displaying text on screen is a multiple part process. Of course, you have to begin with the proper controls on your form. The example uses a RichTextBox so it can display both plain text and formatted RTF text files. The application will allow you to edit the text, but you can't save it. We'll also use a menu for this application instead of the pushbuttons of the past.

Once you have a user interface to work with, you can create the menu command required to start opening a file. The user begins by selecting a file to open. The code opens the file, reads it, then passes the data to the edit control. Listing 4.2 shows this entire process.

Listing 4.2: Opening and Displaying a File

private String File2Open; // Selected Filename

private void menuFileOpen_Click(object sender, System.EventArgs e)

{

OpenFileDialog Dlg = new OpenFileDialog(); // File Open Dialog

// Set up the File Open Dialog

Dlg.Filter = "Rich Text Format (*.rtf)|*.rtf|" + "Text Files (*.txt)|*.txt";

Dlg.DefaultExt = ".rtf"; Dlg.Title = "Open File Dialog";

//Display the File Open Dialog and obtain the name of a file and

//the file information.

if (Dlg.ShowDialog() == DialogResult.OK)

{

File2Open = Dlg.FileName;

}

else

{

// If the user didn't select anything, return. return;

}

// Open the document. DoFileOpen(File2Open);

}

 

 

private bool DoFileOpen(String Filename)

 

{

FileStrm;

// File Data Stream

Stream

FileInfo

FInfo;

// File Information

String

FileExt;

// File Extension

//Determine the file type. FInfo = new FileInfo(File2Open);

FileExt = FInfo.Extension.ToUpper();

//Set the name of the file on the Title bar. if (Form1.ActiveForm != null)

Form1.ActiveForm.Text = "Command Line Example - Editing: " +

File2Open;

// Open the file and display it.

FileStrm = new FileStream(File2Open, FileMode.Open); if (FileExt == ".RTF")

txtEdit.LoadFile(FileStrm, RichTextBoxStreamType.RichText); else

txtEdit.LoadFile(FileStrm, RichTextBoxStreamType.PlainText);

// Close the file. FileStrm.Close();

return true;

}

Notice that this portion of the example relies on the File2Open field to perform the same function as it did in the constructor—keep track of the open file. It's essential to provide such a field in an application where multiple methods rely on the filename.

Tip Never place any unnecessary spaces in the Filter property for a common dialog. Otherwise, the dialog may think that you want the added space in the filter specification. An entry of "*.txt" could become "*.txt " if you add an extra space, making the filter useless.

The menuFileOpen_Click() method begins by creating a File Open dialog box. The OpenFileDialog class takes care of many details for you. However, you still need to provide some input. The example shows a minimal implementation. Setting changes include creating a filter, a default file extension, and a title for the dialog box. If the user selects a file and clicks Open, the application places the filename in File2Open for future use. If the user clicks any other button, the method returns without doing anything.

This is all the code you need to create the user interface portion of the file opening process. Figure 4.1 shows the File Open dialog created by the code in this example. The example code calls DoFileOpen(), the same method called by the command-line version of the constructor. It's time to discuss the common function used by both the command-line and the menu versions of the file-opening process.

Figure 4.1: C# requires little code to create an interesting and useful File Open dialog box.

DoFileOpen() begins by performing some housekeeping chores. First, it determines the file type and assigns it to FileExt for later use. Second, the code places the current file name (along with other text) in the form title bar. Using the title bar this way ensures the user always knows the current filename.

After it completes the housekeeping chores, DoFileOpen() creates FileStrm, which contains the data displayed in the form. Notice that we use the LoadFile() method to load the data into txtEdit. The default RichTextEdit control provides support for plain text, rich text, plain text with OLE support, rich text without OLE support, and Unicode. You could override this control to provide other forms of support. For example, you could choose to display binary data in a hexadecimal format. Each type of data loading requires a different constant so RichTextEdit will know how to handle it. The example supports plain text and rich text without any of the other permutations. You must check for the RTF extension for the rich text file. However, the plain text form of LoadFile() will support anything, including binary files. Of course, you won't be able to read the data displayed, but LoadFile() won't display an error message either.

The example code ends by closing the file stream. If you've opened the file stream for read/write access, you don't necessarily have to close it at this point. In fact, you might want to leave the file opened and locked to prevent another user from modifying the data. However, adding this support also increases the complexity of the application, so you need to decide how likely it is that the application will see use as a means to modify shared data on a network. Figure 4.2 shows what the application looks like with the sample file (located in the \Chapter 04\CommandLine2\bin\Debug folder) loaded.

Figure 4.2: The example application can load both plain text and rich text files.

Printing the Document

Printing can become quite complex because most data files lend themselves to more than one form of output. In addition, users require flexibility in configuring the output. Finally, most print jobs need to support multiple output scenarios. The three common scenarios include:

Screen (display)

Printer

File

Each of these common scenarios will often support multiple subtypes. For example, the user might call upon your application to provide one level of support for monotone printers (black and white) and another level for color printers. Screen displays often require support for zoom, in-place editing, and other conveniences. In short, it's easy to lose sight of the purpose of the code because you're mired in options.

The printing for this example is simple. The application supports one type of output—a monotone printer. The example is purposely simple so you can see the mechanics of printing before the book becomes mired in supporting all of the options typical applications will require. Listing 4.3 shows the example code for printing.

Listing 4.3: Printing a Document Displayed on Screen

private StringReader Doc2Print; // Reads document line by line.

private void menuFilePrint_Click(object sender, System.EventArgs e)

{

PrintDialog

Dlg = new PrintDialog();

//

Print Dialog

PrintDocument

PD = new PrintDocument();

//

Document Rendering

// Object containing document to print. Doc2Print = new StringReader(txtEdit.Text);

//Add an event handler for document printing details. PD.PrintPage += new PrintPageEventHandler(PD_PrintPage);

//Set up the Print Dialog.

Dlg.PrinterSettings = new PrinterSettings();

// Obtain the print parameters.

if (Dlg.ShowDialog() == DialogResult.OK)

{

//Set up the document for printing. PD.PrinterSettings = Dlg.PrinterSettings; PD.DocumentName = File2Open;

//Print the document.

PD.Print();

}

else

{

// Exit if the user selects Cancel. return;

}

}

private void PD_PrintPage(object sender, PrintPageEventArgs ev)

{

String

TextLine = null;

// Current line of text.

Font

docFont;

// Printing Font

float

yPos;

// Position of text on page.

int

Line = 2;

// Current print line w/margin.

float

LinesPerPage;

// Number of lines per page.

// Create the font.

docFont = new Font("Arial", 12);

// Calculate the number of lines on a page.

LinesPerPage = ev.MarginBounds.Height / docFont.GetHeight();

//Continue printing as long as there is space on the page and

//we don't run out of things to write.

while ((Line - 2 < LinesPerPage &&

(TextLine = Doc2Print.ReadLine()) != null))

{

//Determine the Y position of the print area. yPos = Line * docFont.GetHeight();

//Go to the next line.

Line++;

// Print the line of text. ev.Graphics.DrawString(TextLine,

docFont,

Brushes.Black,

40,

yPos);

}

// Determine if we need to print more text. if (TextLine != null)

ev.HasMorePages = true; else

ev.HasMorePages = false;

}

As you can see, the printing process requires use of two methods: a menu handler (menuFilePrint_Click()) and a print event handler (PD_PrintPage()). Both methods rely on a StringReader field, Doc2Print, to hold the data the application will print. Note that Doc2Print contains the raw data, not the data formatted for printing. The example also ignores any native formatting the raw data might contain.

The menuFilePrint_Click() method begins by creating a Print dialog box. The only configuration issue that you must consider is the PrinterSettings property. The example doesn't do much with the Print dialog box, but it is functional. Figure 4.3 shows the output you'll see.

Figure 4.3: The Print dialog requires little configuration to provide a usable display.

If the user clicks OK, the application will send the data to the printer. Notice that you can still select Print to File in the Print dialog. Windows handles this feature for you. However, the output is a printer-ready file, not data in a user-friendly format. If you want user-friendly file output, you'll need to create it as part of the application.

The Print dialog provides input for the PrintDocument object, PD. Notice that PD also requires an entry for the PrinterSettings property. However, you derive this value from the Print dialog so that any user changes appear in the final document. You also need to provide a filename so the user knows which document is printing.

Earlier, the code used the PrintPageEventHandler(PD_PrintPage) method call to assign the PD_PrintPage() method as a print event handler to PD. The PD.Print() method invokes PD_PrintPage(). In short, you set up the printer document and then fire an event to show that it's ready to print. However, the printer document contains setup information, not data.

Note The reason for using a print event handler is to enable background printing. Windows will continue to call the print routine in the background until there are no more pages to print. This technique helps the user to get back to work more quickly.

The PD_PrintPage() method creates the printed output by combining PD (the settings) with Doc2Print (the data). Creating printer output means working with graphic methods. The example shows the simplest possible print routine for text. The code begins by defining a font

and determining print boundaries. It then creates the graphic output line-by-line. Notice that the PrintPageEventArgs variable, ev, provides the device context used to create the output. Notice that the code always assigns a value to ev.HasMorePages so the application knows whether it needs to call PD_PrintPage() to output the next page of text. You must set this value to true after each page or Windows will assume you don't have any more data to print.

Using Preprocessing Directives

Anyone who's used Visual C++ will know the value of preprocessing directives. C# provides a limited number of preprocessing directives that control the flow of compilation, but don't provide the same level of functionality that Visual C++ provides because C# lacks a true preprocessor. In short, you can use the preprocessor to add or subtract features based on the current configuration, but you can't perform some of the other tasks that Visual C++ can perform. For example, you can't use the directives to create macros. Table 4.1 shows the preprocessor directives that C# supports and provides a short definition for them.

 

Table 4.1: Preprocessing Directives Supported by C#

Directive

 

 

Meaning for C#

 

Short Example

 

 

 

 

 

 

#define

 

 

Creates a symbol used for conditional

 

#define DEBUG

 

 

 

statements such as #if. Never use a variable

 

 

 

 

 

name for a #define.

 

 

#elif

#else

#endif

#endregion

#error

#if

Enables conditional compilation if the

 

#elif (DEBUG)

selected condition is true and the associated

 

 

#if condition is false. Use this directive in

 

 

place of #else if you need to test more than

 

 

one condition. Uses the same operators as

 

 

the #if directive.

 

 

 

 

 

Enables conditional compilation if the

 

#else

associated #if condition is false.

 

 

 

 

 

Ends conditional compilation started with

 

#endif

an #if directive.

 

 

 

 

 

Ends a region created with the #region

 

#endregion

directive.

 

 

 

 

 

Generates an error message. This directive

 

#error This is an error

is normally associated with the #if directive

 

message.

to turn the error generation on and off.

 

 

 

 

 

Enables conditional compilation if the

 

#if (DEBUG)

selected condition is true. You can test for

 

 

equality (==) and inequality (!=). The #if

 

 

directive also supports both and (&&) and

 

 

or (||). Using #if without operators tests for

 

 

the existence of the target symbol.

 

 

#line

 

Modifies the current line number. You can

 

 

also add an optional filename. Using default

 

 

in place of a number returns the embedded

 

 

numbering system to its default state.

#line 200 or #line default or #line 20 "MyFilename"

 

Table 4.1: Preprocessing Directives Supported by C#

 

 

 

 

 

 

Directive

 

 

Meaning for C#

 

Short Example

 

 

 

 

 

 

#region

 

 

Creates a region block within the IDE. Each

 

#region

 

 

 

region block has an associated collapse

 

MyRegionName

 

 

 

feature so you can hide of display the

 

 

 

 

 

region. You can't overlap a region with an

 

 

 

 

 

#if directive. However, the #if can contain

 

 

 

 

 

the region or the region can contain the #if.

 

 

 

 

 

 

 

 

#undef

 

 

Deletes a symbol created with the #define

 

#undef DEBUG

 

 

 

directive.

 

 

 

 

 

 

 

 

#warning

 

 

Generates a warning message. This

 

#warning This is a

 

 

 

directive is normally associated with the #if

 

warning message.

 

 

 

directive to turn the error generation on and

 

 

 

 

 

off.

 

 

 

 

 

 

 

 

Note We'll look at how C# differs from the Visual Studio languages of old. Appendix A contains an analysis of how C# differs from Visual C++, while Appendix B performs the same analysis for Visual Basic users.

As you can see, the list isn't long, but the directives definitely make it easier to work with your code. Listing 4.4 shows a short example of all the directives in use.

Listing 4.4: Using Directives within a C# Application

#define MyDefine #undef MyUndef

using System;

namespace Preprocess

{

class Class1

{

[STAThread]

static void Main(string[] args)

{

#region MyDefine area of code #if (MyDefine)

#error MyDefine is defined #else

#error MyDefine isn't defined #endif

#endregion

#line 1 "Sample Filename"

#region MyUndef area of code #if (!MyDefine && !MyUndef)

#warning Neither MyDefine nor MyUndef are defined. #elif (MyDefine && !MyUndef)

#warning MyDefine is defined, but MyUndef is undefined. #elif (!MyDefine && MyUndef)

#warning MyDefine is not defined, but MyUndef is defined. #else

#warning Both MyDefine and MyUndef are defined.