Chapter 17. Printing and Dialog Boxes


If you want a good scare, try browsing the System.Printing namespace. You'll see classes related to printer drivers, printer queues, print servers, and printer jobs. The good news about printing is that for most applications you can safely ignore much of the stuff in System.Printing. Most of your printing logic will probably center around the PrintDialog class defined in the System.Windows.Controls namespace.

The major class you'll need from System.Printing is named PrintTicket. Your projects will also need a reference to the ReachFramework.dll assembly to use that class. It's also useful for programs that print to maintain a field of type PrintQueue. That class can also be found in the System.Printing namespace, but it's from the System.Printing.dll assembly. Other printing-related classes are defined in System.Windows.Documents.

The PrintDialog class displays a dialog box, of course, but the class also includes methods to print a single page or to print a multi-page document. In both cases, what you print on the page is an object of type Visual. As you know by now, one important class that inherits from Visual is UIElement, which means that you can print an instance of any class that derives from FrameworkElement, including panels, controls, and other elements. For example, you could create a Canvas or other panel; put a bunch of child controls, elements, or shapes on it; and then print it.

Although printing a panel seems to offer a great deal of flexibility, a more straighforward approach to printing takes advantage of the DrawingVisual class, which also derives from Visual. I demonstrated DrawingVisual in the ColorCell class that's part of the SelectColor project in Chapter 11. The DrawingVisual class has a method named RenderOpen that returns an object of type DrawingContext. You call methods in DrawingContext (concluding with a call to Close) to store graphics in the DrawingVisual object.

The following program, PrintEllipse, is just about the simplest printing program imaginable. A printing program should have something that initiates printing. In this case, it's a button. When you click the button, the program creates an object of type PrintDialog and displays it. In this dialog box, you might choose a printer (if you have more than one) and possibly change some printer settings. Then you click the Print button to dismiss the PrintDialog, and the program begins preparing to print.

PrintEllipse.cs

[View full width]

//--------------------------------------------- // PrintEllipse.cs (c) 2006 by Charles Petzold //--------------------------------------------- using System; using System.Windows; using System.Windows.Controls; using System.Windows.Input; using System.Windows.Media; namespace Petzold.PrintEllipse { public class PrintEllipse : Window { [STAThread] public static void Main() { Application app = new Application(); app.Run(new PrintEllipse()); } public PrintEllipse() { Title = "Print Ellipse"; FontSize = 24; // Create StackPanel as content of Window. StackPanel stack = new StackPanel(); Content = stack; // Create Button for printing. Button btn = new Button(); btn.Content = "_Print..."; btn.HorizontalAlignment = HorizontalAlignment.Center; btn.Margin = new Thickness(24); btn.Click += PrintOnClick; stack.Children.Add(btn); } void PrintOnClick(object sender, RoutedEventArgs args) { PrintDialog dlg = new PrintDialog(); if ((bool)dlg.ShowDialog() .GetValueOrDefault()) { // Create DrawingVisual and open DrawingContext. DrawingVisual vis = new DrawingVisual(); DrawingContext dc = vis.RenderOpen(); // Draw ellipse. dc.DrawEllipse(Brushes.LightGray, new Pen(Brushes.Black, 3), new Point(dlg .PrintableAreaWidth / 2, dlg .PrintableAreaHeight / 2), dlg .PrintableAreaWidth / 2, dlg .PrintableAreaHeight / 2); // Close DrawingContext. dc.Close(); // Finally, print the page. dlg.PrintVisual(vis, "My first print job"); } } } }



ShowDialog is defined as returning a nullable bool. For this particular dialog box, ShowDialog returns true if the user clicks the Print button, false if the user clicks Cancel, and null if the dialog box is closed by clicking the red Close button at the far right of the title bar. The GetValueOrDefault call converts the null return to false so that the result can be safely cast to a bool for the if statement.

If the ShowDialog method returns true, the program creates an object of type DrawingVisual, calls RenderOpen to return a DrawingContext object, and then calls both DrawEllipse and Close on the DrawingContext. Finally, the DrawingVisual is passed to the PrintVisual method of PrintDialog with a text string that identifies the print job in the printer queue.

Notice that the DrawEllipse arguments reference two properties defined by PrintDialog named (somewhat incorrectly) PrintableAreaWidth and PrintableAreaHeight. When the page printed by the PrintEllipse program comes out of your printer, you'll probably see that parts of the ellipse are truncated because they fall in unprintable areas of the page. The PrintableAreaWidth and PrintableAreaHeight properties do not refer to the printable area of the page, but instead indicate the total physical size of the page in device-independent units (1/96 inch).

Toward the bottom of the Print dialog are four radio buttons labeled All, Selection, Current Page, and Pages that indicate what part of the document the user wants printed: the entire document, the current selection, the current page, or a range of pages specified by the user. All radio buttons except the first are disabled by default. The Selection and Current Page buttons cannot be enabled in the initial release of the Windows Presentation Foundation. You can enable the Pages buttons by setting the UserPageRangeEnabled property to true.

You can initialize the radio button that's checked and later obtain the user's choice through the PageRangeSelection property, which is a member of the PageRangeSelection enumeration: AllPages or UserPages. If the PageRangeSelection property equals PageRangeSelection.UserPages (corresponding to the radio button labeled Pages), the PageRanges property of PrintDialog is a collection of objects of type PageRange, which is a structure with PageFrom and PageTo properties.

The Print dialog also includes a Number of Copies field. Enter a number greater than 1 in this field and the PrintVisual method prints multiple copies.

The Print dialog has a button labeled Printer Preferences that you click to invoke the printer-specific property pages. If you select a different page size on those pages, you'll notice by what the program prints that the page size is obviously reflected in the PrintableAreaWidth and PrintableAreaHeight properties. You can also switch the page orientation to landscape, but the effect is not quite noticeable in this program.

The user's requested page orientation, number of copies, and much other information is collected in a property of PrintDialog named PrintTicket of type PrintTicket, a class defined in the System.Printing namespace and stored in the ReachFramework assembly. To use PrintTicket, you'll need a reference to ReachFramework.dll.

If you invoke the PrintDialog in the PrintEllipse program, change a setting (such as the page orientation), print, and then invoke the PrintDialog again, you'll see that the setting has reverted back to its default. These days, polite programs usually preserve user selections such as page orientation between print jobs. To do this, you can define a field of type PrintTicket.

PrintTicket prntkt; 


Of course, prntkt is initially null. When you create an object of type PrintDialog in preparation for displaying the dialog box, you set the dialog's PrintTicket from this field, but only if the field is not null:

PrintDialog dlg = new PrintDialog(); if (prntkt != null)     dlg.PrintTicket = prntkt; 


If the user clicks the Print button in the dialog, you want to save the PrintTicket from the dialog box to the field:

if (dlg.ShowDialog().GetValueOrDefault()) {     prntkt = dlg.PrintTicket;     ... } 


This setting and getting of the PrintTicket ensures that user choices are preserved. If the user opens the Print dialog, sets page orientation to Landscape, clicks OK, and then opens the Print dialog again, the orientation will still be displayed as Landscape.

If the user has multiple printers, these show up in a ComboBox at the top of each of the Print dialog. If the user selects a non-default printer in the Print dialog, you probably want to bring up the Print dialog the next time showing that same printer. You can do this by saving the PrintQueue property of the dialog box as a field of type PrintQueue, and setting that property from the field when the dialog box is displayed. You'll need a reference to the System.Printing assembly to use PrintQueue.

One part of the print equation that's missing from the PrintDialog is a facility for the user to select page margins, so for that job let's create a dialog box specifically for that purpose. Dialog box classes derive from Window and are very similar to other Window-based classes. But there are a few differences.

The constructor of a dialog box class should set the ShowInTaskbar property to false. Very often the dialog box is sized to its contents and the ResizeMode is set to NoResize. You can set the WindowStyle to either SingleBorderWindow or ToolWindow, whichever you prefer. It is no longer considered proper to display a dialog box without a title bar.

The easy way to position a dialog box relative to its owner is by setting the WindowStartupLocation to CenterOwner. Even if you just leave the WindowStartupLocation property at its default setting (which lets Windows itself position the dialog), the Owner property of the dialog box must still be set to the Window object that invoked the dialog. Usually the code looks something like this:

MyDialog dlg = new MyDialog(); dlg.Owner = this; 


If a dialog box with a null owner is displayed and the user switches to another application, the dialog box could end up behind the window that invoked it, but the application's window would still be prohibited from receiving input. It's not a pretty situation.

The CommonDialog class defined in the Microsoft.Win32 namespace (and from which OpenFileDialog and SaveFileDialog are derived) defines a ShowDialog method that requires an Owner argument That's one way to do it. Or, the constructor of the dialog box class could require an Owner argument. If you do that, you have a bit more flexibility in positioning the dialog box. For example, suppose you want to position the dialog box offset one inch from the top left corner of the application window. The constructor code is simply

public MyDialog(Window owner) {     Left = 96 + owner.Left;     Top = 96 + owner.Top;     ... } 


Almost always, a dialog box defines at least one public property for the information that the dialog box obtains from the user and provides to the application. You might even want to define a class or structure specifically to contain all the information that a particular dialog box provides. In the definition of this property, the set accessor initializes the dialog box controls from the value of the property. The get accessor obtains the properties from the controls.

A dialog box contains OK and Cancel buttons, although the OK button might actually be labeled Open, Save, or Print. The OK button has its IsDefault property set to true so that the button is clicked if the user presses the Enter key anywhere within the dialog box. The Cancel button has its IsCancel property set to true, so the button is clicked if the user presses the Escape key.

You don't need to provide a Click handler for the Cancel button. The dialog box is terminated automatically as a result of the true setting of the IsCancel property. The Click handler for the OK button need only set the DialogResult property to true. That terminates the dialog box and causes ShowDialog to return true.

The PageMarginsDialog class contains four TextBox controls to let the user enter left, right, top, and bottom page margins in inches. The class defines a single public property named PageMargins of type Thickness that converts between inches and device-independent units. The get accessor of the PageMargins property can call Double.Parse to convert the contents of the four TextBox controls to numbers without fear because the class disables the OK button until all fields contain valid values.

PageMarginsDialog.cs

[View full width]

//-------------------------------------------------- // PageMarginsDialog.cs (c) 2006 by Charles Petzold //-------------------------------------------------- using System; using System.Windows; using System.Windows.Controls; using System.Windows.Controls.Primitives; namespace Petzold.PrintWithMargins { class PageMarginsDialog : Window { // Internal enumeration to refer to the paper sides. enum Side { Left, Right, Top, Bottom } // Four TextBox controls for numeric input. TextBox[] txtbox = new TextBox[4]; Button btnOk; // Public property of type Thickness for page margins. public Thickness PageMargins { set { txtbox[(int)Side.Left].Text = (value.Left / 96).ToString("F3"); txtbox[(int)Side.Right].Text = (value .Right / 96).ToString("F3"); txtbox[(int)Side.Top].Text = (value.Top / 96).ToString("F3"); txtbox[(int)Side.Bottom].Text = (value .Bottom / 96).ToString("F3"); } get { return new Thickness( Double.Parse(txtbox[ (int)Side.Left].Text) * 96, Double.Parse(txtbox[ (int)Side.Top].Text) * 96, Double.Parse(txtbox[ (int)Side.Right].Text) * 96, Double.Parse(txtbox[ (int)Side.Bottom].Text) * 96); } } // Constructor. public PageMarginsDialog() { // Standard settings for dialog boxes. Title = "Page Setup"; ShowInTaskbar = false; WindowStyle = WindowStyle.ToolWindow; WindowStartupLocation = WindowStartupLocation.CenterOwner; SizeToContent = SizeToContent .WidthAndHeight; ResizeMode = ResizeMode.NoResize; // Make StackPanel content of Window. StackPanel stack = new StackPanel(); Content = stack; // Make GroupBox a child of StackPanel. GroupBox grpbox = new GroupBox(); grpbox.Header = "Margins (inches)"; grpbox.Margin = new Thickness(12); stack.Children.Add(grpbox); // Make Grid the content of the GroupBox. Grid grid = new Grid(); grid.Margin = new Thickness(6); grpbox.Content = grid; // Two rows and four columns. for (int i = 0; i < 2; i++) { RowDefinition rowdef = new RowDefinition(); rowdef.Height = GridLength.Auto; grid.RowDefinitions.Add(rowdef); } for (int i = 0; i < 4; i++) { ColumnDefinition coldef = new ColumnDefinition(); coldef.Width = GridLength.Auto; grid.ColumnDefinitions.Add(coldef); } // Put Label and TextBox controls in Grid. for (int i = 0; i < 4; i++) { Label lbl = new Label(); lbl.Content = "_" + Enum.GetName (typeof(Side), i) + ":"; lbl.Margin = new Thickness(6); lbl.VerticalAlignment = VerticalAlignment.Center; grid.Children.Add(lbl); Grid.SetRow(lbl, i / 2); Grid.SetColumn(lbl, 2 * (i % 2)); txtbox[i] = new TextBox(); txtbox[i].TextChanged += TextBoxOnTextChanged; txtbox[i].MinWidth = 48; txtbox[i].Margin = new Thickness(6); grid.Children.Add(txtbox[i]); Grid.SetRow(txtbox[i], i / 2); Grid.SetColumn(txtbox[i], 2 * (i % 2) + 1); } // Use UniformGrid for OK and Cancel buttons. UniformGrid unigrid = new UniformGrid(); unigrid.Rows = 1; unigrid.Columns = 2; stack.Children.Add(unigrid); btnOk = new Button(); btnOk.Content = "OK"; btnOk.IsDefault = true; btnOk.IsEnabled = false; btnOk.MinWidth = 60; btnOk.Margin = new Thickness(12); btnOk.HorizontalAlignment = HorizontalAlignment.Center; btnOk.Click += OkButtonOnClick; unigrid.Children.Add(btnOk); Button btnCancel = new Button(); btnCancel.Content = "Cancel"; btnCancel.IsCancel = true; btnCancel.MinWidth = 60; btnCancel.Margin = new Thickness(12); btnCancel.HorizontalAlignment = HorizontalAlignment.Center; unigrid.Children.Add(btnCancel); } // Enable OK button only if the TextBox controls have numeric values. void TextBoxOnTextChanged(object sender, TextChangedEventArgs args) { double result; btnOk.IsEnabled = Double.TryParse(txtbox[(int)Side .Left].Text, out result) && Double.TryParse(txtbox[(int)Side .Right].Text, out result) && Double.TryParse(txtbox[(int)Side .Top].Text, out result) && Double.TryParse(txtbox[(int)Side .Bottom].Text, out result); } // Dismiss dialog on OK click. void OkButtonOnClick(object sender, RoutedEventArgs args) { DialogResult = true; } } }



The PageMarginsDialog class is part of the PrintWithMargins project, which also includes the following file. This program demonstrates using PrintQueue and PrintTicket objects to save and transfer settings between multiple invocations of the dialog box. The PageMarginsDialog is displayed when you click the button labeled Page Setup.

PrintWithMargins.cs

[View full width]

//------------------------------------------------- // PrintWithMargins.cs (c) 2006 by Charles Petzold //------------------------------------------------- using System; using System.Globalization; using System.Windows; using System.Windows.Controls; using System.Windows.Input; using System.Windows.Media; using System.Printing; namespace Petzold.PrintWithMargins { public class PrintWithMargins : Window { // Private fields to save information from PrintDialog. PrintQueue prnqueue; PrintTicket prntkt; Thickness marginPage = new Thickness(96); [STAThread] public static void Main() { Application app = new Application(); app.Run(new PrintWithMargins()); } public PrintWithMargins() { Title = "Print with Margins"; FontSize = 24; // Create StackPanel as content of window. StackPanel stack = new StackPanel(); Content = stack; // Create button for Page Setup. Button btn = new Button(); btn.Content = "Page Set_up..."; btn.HorizontalAlignment = HorizontalAlignment.Center; btn.Margin = new Thickness(24); btn.Click += SetupOnClick; stack.Children.Add(btn); // Create Print button. btn = new Button(); btn.Content = "_Print..."; btn.HorizontalAlignment = HorizontalAlignment.Center; btn.Margin = new Thickness(24); btn.Click += PrintOnClick; stack.Children.Add(btn); } // Page Setup button: Invoke PageMarginsDialog. void SetupOnClick(object sender, RoutedEventArgs args) { // Create dialog and initialize PageMargins property. PageMarginsDialog dlg = new PageMarginsDialog(); dlg.Owner = this; dlg.PageMargins = marginPage; if (dlg.ShowDialog().GetValueOrDefault()) { // Save page margins from dialog box. marginPage = dlg.PageMargins; } } // Print button: Invoke PrintDialog. void PrintOnClick(object sender, RoutedEventArgs args) { PrintDialog dlg = new PrintDialog(); // Set PrintQueue and PrintTicket from fields. if (prnqueue != null) dlg.PrintQueue = prnqueue; if (prntkt != null) dlg.PrintTicket = prntkt; if (dlg.ShowDialog().GetValueOrDefault()) { // Save PrintQueue and PrintTicket from dialog box. prnqueue = dlg.PrintQueue; prntkt = dlg.PrintTicket; // Create DrawingVisual and open DrawingContext. DrawingVisual vis = new DrawingVisual(); DrawingContext dc = vis.RenderOpen(); Pen pn = new Pen(Brushes.Black, 1); // Rectangle describes page minus margins. Rect rectPage = new Rect (marginPage.Left, marginPage.Top, dlg .PrintableAreaWidth - (marginPage.Left + marginPage.Right), dlg .PrintableAreaHeight - (marginPage.Top + marginPage.Bottom)); // Draw rectangle to reflect user's margins. dc.DrawRectangle(null, pn, rectPage); // Create formatted text object showing PrintableArea properties. FormattedText formtxt = new FormattedText( String.Format("Hello, Printer! {0} x {1}", dlg .PrintableAreaWidth / 96, dlg .PrintableAreaHeight / 96), CultureInfo.CurrentCulture, FlowDirection.LeftToRight, new Typeface(new FontFamily ("Times New Roman"), FontStyles .Italic, FontWeights.Normal, FontStretches .Normal), 48, Brushes.Black); // Get physical size of formatted text string. Size sizeText = new Size(formtxt .Width, formtxt.Height); // Calculate point to center text within margins. Point ptText = new Point(rectPage.Left + (rectPage .Width - formtxt.Width) / 2, rectPage.Top + (rectPage .Height - formtxt.Height) / 2); // Draw text and surrounding rectangle. dc.DrawText(formtxt, ptText); dc.DrawRectangle(null, pn, new Rect(ptText, sizeText)); // Close DrawingContext. dc.Close(); // Finally, print the page(s). dlg.PrintVisual(vis, Title); } } } }



This program prints a rectangle based on the page margins selected by the user and centers some text within the margins, also surrounded by a rectangle. The text contains the words "Hello, Printer!" followed by the dimensions of the page in inches.

Because PrintVisual prints an object of type Visual, you can put together a page by arranging elements and controls on a panel, such as a Canvas. You can extend this technique into graphics with the Shapes library and you can use TextBlock for displaying text.

Here's a program named PrintaBunchaButtons that prints a Grid panel with a gradient brush background and 25 buttons of various sizes arranged on it.

PrintaBunchaButtons.cs

[View full width]

//---------------------------------------------------- // PrintaBunchaButtons.cs (c) 2006 by Charles Petzold //---------------------------------------------------- using System; using System.Windows; using System.Windows.Controls; using System.Windows.Input; using System.Windows.Media; namespace Petzold.PrintaBunchaButtons { public class PrintaBunchaButtons : Window { [STAThread] public static void Main() { Application app = new Application(); app.Run(new PrintaBunchaButtons()); } public PrintaBunchaButtons() { Title = "Print a Bunch of Buttons"; SizeToContent = SizeToContent .WidthAndHeight; ResizeMode = ResizeMode.CanMinimize; // Create 'Print' button. Button btn = new Button(); btn.FontSize = 24; btn.Content = "Print ..."; btn.Padding = new Thickness(12); btn.Margin = new Thickness(96); btn.Click += PrintOnClick; Content = btn; } void PrintOnClick(object sender, RoutedEventArgs args) { PrintDialog dlg = new PrintDialog(); if ((bool)dlg.ShowDialog() .GetValueOrDefault()) { // Create Grid panel. Grid grid = new Grid(); // Define five auto-sized rows and columns. for (int i = 0; i < 5; i++) { ColumnDefinition coldef = new ColumnDefinition(); coldef.Width = GridLength.Auto; grid.ColumnDefinitions.Add (coldef); RowDefinition rowdef = new RowDefinition(); rowdef.Height = GridLength.Auto; grid.RowDefinitions.Add(rowdef); } // Give the Grid a gradient brush. grid.Background = new LinearGradientBrush(Colors .Gray, Colors.White, new Point(0, 0), new Point(1, 1)); // Every program needs a bit of randomness. Random rand = new Random(); // Fill the Grid with 25 buttons. for (int i = 0; i < 25; i++) { Button btn = new Button(); btn.FontSize = 12 + rand.Next(8); btn.Content = "Button No. " + (i + 1); btn.HorizontalAlignment = HorizontalAlignment.Center; btn.VerticalAlignment = VerticalAlignment.Center; btn.Margin = new Thickness(6); grid.Children.Add(btn); Grid.SetRow(btn, i % 5); Grid.SetColumn(btn, i / 5); } // Size the Grid. grid.Measure(new Size(Double .PositiveInfinity, Double .PositiveInfinity)); Size sizeGrid = grid.DesiredSize; // Determine point for centering Grid on page. Point ptGrid = new Point((dlg .PrintableAreaWidth - sizeGrid.Width) / 2, (dlg .PrintableAreaHeight - sizeGrid.Height) / 2); // Layout pass. grid.Arrange(new Rect(ptGrid, sizeGrid)); // Now print it. dlg.PrintVisual(grid, Title); } } } }



To illustrate this technique most clearly, the program doesn't have some of the features of the previous programsuch as defining margins or saving settings with PrintTicket and PrintQueue.

When a program prints an instance of a class derived from UIElement, a crucial step is required: You must subject the element to layout, which means you must call Measure and Arrange on the object. Otherwise, the object will have a zero dimension and won't show up on the page. The PrintaBunchaButtons program uses infinite dimensions when calling Measure on the Grid object; the alternative is basing the dimensions on the size of the page. When calling Arrange, the program obtains the size of the Grid from its DesiredSize property, calculates a point that puts the Grid in the center of the page, and then calls Arrange with that point and size. Now the Grid is ready to pass to PrintVisual.

You could print a multi-page document by making multiple calls to PrintVisual, but each page would be considered a different print job. To better print a multi-page document, a program calls the PrintDocument method defined by PrintDialog. The arguments to PrintDocument are an instance of a class derived from DocumentPaginator and a text string for the print queue describing the document.

DocumentPaginator is an abstract class defined in System.Windows.Documents. You must define a class that inherits from DocumentPaginator and you must override several properties and methods that DocumentPaginator defines as abstract. One of these methods is GetPage, which returns an object of type DocumentPage. You can easily create a DocumentPage object from a Visual, which means that multi-page printing isn't all that different from single-page printing. Each page of the document is a Visual.

Your DocumentPaginator derivative must override the Boolean read-only IsPageCountValid property, the read-only PageCount property, a read-write PageSize property, the GetPage method (which has a zero-based page number argument and returns an object of type DocumentPage), and the Source property, which can return null.

A DocumentPaginator derivative probably also defines some properties on its own. For example, suppose you want to write a program that prints a banner. Banner programs once printed on continuous stretches of fanfold paper, but these days banner printer programs print one big letter per page. The class that derives from DocumentPaginator probably needs a property of type Text, which a program sets to something like "Happy Birthday to the Greatest Mom in All the World, and Probably Other Planets as Well." The DocumentPaginator derivative should also have a property to specify the font. An approach that doesn't require a bunch of properties involves consolidating most of the font information into an object of type Typeface.

However, if you want to keep your DocumentPaginator derivative a bit simpler, you can probably dispense with the custom properties and instead just define a constructor that accepts this information. The DocumentPaginator object is usually not around long enough to make a difference in which approach you use. You'll probably create the object in response to a click of Print button in PrintDialog, and abandon it after calling PrintDocument.

Here's a class named BannerDocumentPaginator that defines two properties named Text and Typeface.

BannerDocumentPaginator.cs

[View full width]

//--------- ----------------------------------------------- // BannerDocumentPaginator.cs (c) 2006 by Charles Petzold //------------------------------------------------ -------- using System; using System.Globalization; using System.Windows; using System.Windows.Controls; using System.Windows.Documents; using System.Windows.Media; namespace Petzold.PrintBanner { public class BannerDocumentPaginator : DocumentPaginator { string txt = ""; Typeface face = new Typeface(""); Size sizePage; Size sizeMax = new Size(0, 0); // Public properties specific to this DocumentPaginator. public string Text { set { txt = value; } get { return txt; } } public Typeface Typeface { set { face = value; } get { return face; } } // Private function to create FormattedText object. FormattedText GetFormattedText(char ch, Typeface face, double em) { return new FormattedText(ch.ToString() , CultureInfo.CurrentCulture, FlowDirection .LeftToRight, face, em, Brushes.Black); } // Necessary overrides. public override bool IsPageCountValid { get { // Determine maximum size of characters based on em size of 100. foreach (char ch in txt) { FormattedText formtxt = GetFormattedText(ch, face, 100); sizeMax.Width = Math.Max (sizeMax.Width, formtxt.Width); sizeMax.Height = Math.Max (sizeMax.Height, formtxt.Height); } return true; } } public override int PageCount { get { return txt == null ? 0 : txt .Length; } } public override Size PageSize { set { sizePage = value; } get { return sizePage; } } public override DocumentPage GetPage(int numPage) { DrawingVisual vis = new DrawingVisual(); DrawingContext dc = vis.RenderOpen(); // Assume half-inch margins when calculating em size factor. double factor = Math.Min((PageSize .Width - 96) / sizeMax.Width, (PageSize .Height - 96) / sizeMax.Height); FormattedText formtxt = GetFormattedText(txt[numPage], face, factor * 100); // Find point to center character in page. Point ptText = new Point((PageSize .Width - formtxt.Width) / 2, (PageSize .Height - formtxt.Height) / 2); dc.DrawText(formtxt, ptText); dc.Close(); return new DocumentPage(vis); } public override IDocumentPaginatorSource Source { get { return null; } } } }



As the name DocumentPaginator suggests, the class needs to paginate a document. It must determine how many pages the document has and what goes on each page. For this paginator, the page count is simple: It's the number of characters in the text string. However, this paginator must also determine how large these characters should be. This job requires constructing objects of type FormattedText for each letter to determine the height of the letters and the maximum character width.

If you define a constructor that has arguments with all the information the class needs to perform a pagination, the constructor could also contain the pagination logic. Otherwise, if you define properties, you must find another place for the pagination to occur. It seems reasonable to me that the IsPageCountValid property is a good spot because that property is called before anything else. In its IsPageCountValid property, therefore, BannerDocumentPaginator creates FormattedText objects for every character in the text string based on the Typeface property and an em size of 100, and saves the largest size it encounters.

The GetPage method is responsible for returning objects of type DocumentPage, which can be created from an object of type Visual. The method creates a DrawingVisual, opens a DrawingContext, and calculates a multiplicative factor based on the largest character size and the size of the page minus half-inch margins. The method constructs a new FormattedText object and centers it on the page with a call to DrawText. This DrawingVisual is passed to the DocumentPage constructor and GetPage returns the resultant object.

The user interface for the banner program is fairly straightforward:

PrintBanner.cs

[View full width]

//-------------------------------------------- // PrintBanner.cs (c) 2006 by Charles Petzold //-------------------------------------------- using System; using System.Printing; using System.Windows; using System.Windows.Controls; using System.Windows.Input; using System.Windows.Media; namespace Petzold.PrintBanner { public class PrintBanner : Window { TextBox txtbox; [STAThread] public static void Main() { Application app = new Application(); app.Run(new PrintBanner()); } public PrintBanner() { Title = "Print Banner"; SizeToContent = SizeToContent .WidthAndHeight; // Make StackPanel content of window. StackPanel stack = new StackPanel(); Content = stack; // Create TextBox. txtbox = new TextBox(); txtbox.Width = 250; txtbox.Margin = new Thickness(12); stack.Children.Add(txtbox); // Create Button. Button btn = new Button(); btn.Content = "_Print..."; btn.Margin = new Thickness(12); btn.Click += PrintOnClick; btn.HorizontalAlignment = HorizontalAlignment.Center; stack.Children.Add(btn); txtbox.Focus(); } void PrintOnClick(object sender, RoutedEventArgs args) { PrintDialog dlg = new PrintDialog(); if (dlg.ShowDialog().GetValueOrDefault()) { // Make sure orientation is Portrait. PrintTicket prntkt = dlg.PrintTicket; prntkt.PageOrientation = PageOrientation.Portrait; dlg.PrintTicket = prntkt; // Create new BannerDocumentPaginator object. BannerDocumentPaginator paginator = new BannerDocumentPaginator(); // Set Text property from TextBox. paginator.Text = txtbox.Text; // Give it a PageSize property based on the paper dimensions. paginator.PageSize = new Size(dlg .PrintableAreaWidth, dlg .PrintableAreaHeight); // Call PrintDocument to print the document. dlg.PrintDocument(paginator, "Banner: " + txtbox.Text); } } } }



A banner program really works best with a Portrait page orientation, so even if the user selects Landscape, the PrintOnClick method changes it back to Portrait. The method then creates an object of type BannerDocumentPaginator and sets the Text and PageSize properties. The method concludes by passing the initialized BannerDocumentPaginator object to PrintDocument, which does the rest.

Notice that the PrintBanner program doesn't give BannerDocumentPaginator a custom Typeface object. What this program really needs is a font dialog, but unfortunately, the first version of the Windows Presentation Foundation doesn't include one. That leaves me to do the job. I really didn't want to write a font dialog, but I had to.

Font dialogs are generally built from combo boxes with list portions that are always visible. Unfortunately, the ComboBox class in the first version of the Windows Presentation Foundation doesn't support this mode, and my first attempt to mimic such a combo box with an TextBox and a ListBox didn't work very well. I wanted input focus to always stay with the TextBox, but I found it difficult to prevent the ListBox from getting input focus. I then realized that the job must really begin with a simple non-focusable control that looked and acted like a ListBox that I called Lister. This is the first file of the ChooseFont project.

Lister.cs

[View full width]

//--------------------------------------- // Lister.cs (c) 2006 by Charles Petzold //--------------------------------------- using System; using System.Collections; using System.Windows; using System.Windows.Controls; using System.Windows.Input; using System.Windows.Media; namespace Petzold.ChooseFont { class Lister : ContentControl { ScrollViewer scroll; StackPanel stack; ArrayList list = new ArrayList(); int indexSelected = -1; // Public event. public event EventHandler SelectionChanged; // Constructor. public Lister() { Focusable = false; // Make Border the content of the ContentControl. Border bord = new Border(); bord.BorderThickness = new Thickness(1); bord.BorderBrush = SystemColors .ActiveBorderBrush; bord.Background = SystemColors .WindowBrush; Content = bord; // Make ScrollViewer the child of the border. scroll = new ScrollViewer(); scroll.Focusable = false; scroll.Padding = new Thickness(2, 0, 0 , 0); bord.Child = scroll; // Make StackPanel the content of the ScrollViewer. stack = new StackPanel(); scroll.Content = stack; // Install a handler for the mouse left button down. AddHandler(TextBlock .MouseLeftButtonDownEvent, new MouseButtonEventHandler (TextBlockOnMouseLeftButtonDown)); Loaded += OnLoaded; } void OnLoaded(object sender, RoutedEventArgs args) { // Scroll the selected item into view when Lister is first displayed. ScrollIntoView(); } // Public methods to add, insert, etc, items in Lister. public void Add(object obj) { list.Add(obj); TextBlock txtblk = new TextBlock(); txtblk.Text = obj.ToString(); stack.Children.Add(txtblk); } public void Insert(int index, object obj) { list.Insert(index, obj); TextBlock txtblk = new TextBlock(); txtblk.Text = obj.ToString(); stack.Children.Insert(index, txtblk); } public void Clear() { SelectedIndex = -1; stack.Children.Clear(); list.Clear(); } public bool Contains(object obj) { return list.Contains(obj); } public int Count { get { return list.Count; } } // This method is called to select an item based on a typed letter. public void GoToLetter(char ch) { int offset = SelectedIndex + 1; for (int i = 0; i < Count; i++) { int index = (i + offset) % Count; if (Char.ToUpper(ch) == Char .ToUpper(list[index].ToString()[0])) { SelectedIndex = index; break; } } } // SelectedIndex property is responsible for displaying selection bar. public int SelectedIndex { set { if (value < -1 || value >= Count) throw new ArgumentOutOfRangeException("SelectedIndex"); if (value == indexSelected) return; if (indexSelected != -1) { TextBlock txtblk = stack .Children[indexSelected] as TextBlock; txtblk.Background = SystemColors.WindowBrush; txtblk.Foreground = SystemColors.WindowTextBrush; } indexSelected = value; if (indexSelected > -1) { TextBlock txtblk = stack .Children[indexSelected] as TextBlock; txtblk.Background = SystemColors.HighlightBrush; txtblk.Foreground = SystemColors.HighlightTextBrush; } ScrollIntoView(); // Trigger SelectionChanged event. OnSelectionChanged(EventArgs.Empty); } get { return indexSelected; } } // SelectedItem property makes use of SelectedIndex. public object SelectedItem { set { SelectedIndex = list.IndexOf(value); } get { if (SelectedIndex > -1) return list[SelectedIndex]; return null; } } // Public methods to page up and down through the list. public void PageUp() { if (SelectedIndex == -1 || Count == 0) return; int index = SelectedIndex - (int)(Count * scroll .ViewportHeight / scroll.ExtentHeight); if (index < 0) index = 0; SelectedIndex = index; } public void PageDown() { if (SelectedIndex == -1 || Count == 0) return; int index = SelectedIndex + (int)(Count * scroll .ViewportHeight / scroll.ExtentHeight); if (index > Count - 1) index = Count - 1; SelectedIndex = index; } // Private method to scroll selected item into view. void ScrollIntoView() { if (Count == 0 || SelectedIndex == -1 || scroll .ViewportHeight > scroll.ExtentHeight) return; double heightPerItem = scroll .ExtentHeight / Count; double offsetItemTop = SelectedIndex * heightPerItem; double offsetItemBot = (SelectedIndex + 1) * heightPerItem; if (offsetItemTop < scroll.VerticalOffset) scroll.ScrollToVerticalOffset (offsetItemTop); else if (offsetItemBot > scroll .VerticalOffset + scroll.ViewportHeight) scroll.ScrollToVerticalOffset (scroll.VerticalOffset + offsetItemBot - scroll .VerticalOffset - scroll.ViewportHeight); } // Event handler and trigger. void TextBlockOnMouseLeftButtonDown(object sender, MouseButtonEventArgs args) { if (args.Source is TextBlock) SelectedIndex = stack.Children .IndexOf(args.Source as TextBlock); } protected virtual void OnSelectionChanged (EventArgs args) { if (SelectionChanged != null) SelectionChanged(this, args); } } }



Lister is a ContentControl with a Border containing a ScrollViewer containing a StackPanel containing multiple TextBlock items. Near the bottom of the file is a TextBlockOnMouseLeftButtonDown method that sets the SelectedIndex property to the index of the clicked TextBlock. The SelectedIndex property is responsible for maintaining the foreground and background colors of the TextBlock items to indicate which item is currently selected, and to trigger the SelectionChanged event by calling OnSelectionChanged.

The keyboard interface for changing the selected item is handled external to the Lister control in the next class. The TextBoxWithLister class also derives from ContentControl and contains a DockPanel with a TextBox control and a Lister element. The class overrides the OnPreviewKeyDown method to change the selected item based on the cursor movement keys.

TextBoxWithLister.cs

[View full width]

//-------------------------------------------------- // TextBoxWithLister.cs (c) 2006 by Charles Petzold //-------------------------------------------------- using System; using System.Collections; using System.Windows; using System.Windows.Controls; using System.Windows.Input; using System.Windows.Media; namespace Petzold.ChooseFont { class TextBoxWithLister : ContentControl { TextBox txtbox; Lister lister; bool isReadOnly; // Public events. public event EventHandler SelectionChanged; public event TextChangedEventHandler TextChanged; // Constructor. public TextBoxWithLister() { // Create DockPanel as content of control. DockPanel dock = new DockPanel(); Content = dock; // TextBox is docked at top. txtbox = new TextBox(); txtbox.TextChanged += TextBoxOnTextChanged; dock.Children.Add(txtbox); DockPanel.SetDock(txtbox, Dock.Top); // Lister fills remainder of DockPanel. lister = new Lister(); lister.SelectionChanged += ListerOnSelectionChanged; dock.Children.Add(lister); } // Public properties involving the TextBox item. public string Text { get { return txtbox.Text; } set { txtbox.Text = value; } } public bool IsReadOnly { set { isReadOnly = value; } get { return isReadOnly; } } // Other public properties interface with Lister element. public object SelectedItem { set { lister.SelectedItem = value; if (lister.SelectedItem != null) txtbox.Text = lister .SelectedItem.ToString(); else txtbox.Text = ""; } get { return lister.SelectedItem; } } public int SelectedIndex { set { lister.SelectedIndex = value; if (lister.SelectedIndex == -1) txtbox.Text = ""; else txtbox.Text = lister .SelectedItem.ToString(); } get { return lister.SelectedIndex; } } public void Add(object obj) { lister.Add(obj); } public void Insert(int index, object obj) { lister.Insert(index, obj); } public void Clear() { lister.Clear(); } public bool Contains(object obj) { return lister.Contains(obj); } // On a mouse click, set the keyboard focus. protected override void OnMouseDown (MouseButtonEventArgs args) { base.OnMouseDown(args); Focus(); } // When the keyboard focus comes, pass it to the TextBox. protected override void OnGotKeyboardFocus( KeyboardFocusChangedEventArgs args) { base.OnGotKeyboardFocus(args); if (args.NewFocus == this) { txtbox.Focus(); if (SelectedIndex == -1 && lister. Count > 0) SelectedIndex = 0; } } // When a letter key is typed, pass it to GoToLetter method of Lister. protected override void OnPreviewTextInput (TextCompositionEventArgs args) { base.OnPreviewTextInput(args); if (IsReadOnly) { lister.GoToLetter(args.Text[0]); args.Handled = true; } } // Handling of cursor movement keys to change selected item. protected override void OnPreviewKeyDown (KeyEventArgs args) { base.OnKeyDown(args); if (SelectedIndex == -1) return; switch (args.Key) { case Key.Home: if (lister.Count > 0) SelectedIndex = 0; break; case Key.End: if (lister.Count > 0) SelectedIndex = lister .Count - 1; break; case Key.Up: if (SelectedIndex > 0) SelectedIndex--; break; case Key.Down: if (SelectedIndex < lister .Count - 1) SelectedIndex++; break; case Key.PageUp: lister.PageUp(); break; case Key.PageDown: lister.PageDown(); break; default: return; } args.Handled = true; } // Event handlers and triggers. void ListerOnSelectionChanged(object sender, EventArgs args) { if (SelectedIndex == -1) txtbox.Text = ""; else txtbox.Text = lister.SelectedItem .ToString(); OnSelectionChanged(args); } void TextBoxOnTextChanged(object sender, TextChangedEventArgs args) { if (TextChanged != null) TextChanged(this, args); } protected virtual void OnSelectionChanged (EventArgs args) { if (SelectionChanged != null) SelectionChanged(this, args); } } }



This class has two modes that are governed by the IsReadOnly property. If the setting is false, the user is free to type anything into the TextBox, and the program that makes use of this control obtains the user's selection from this TextBox. This is the mode that the FontDialog uses for the font size. The list part of the control should present a bunch of common font sizes, but the user should be allowed to enter anything else.

If IsReadOnly is true, the TextBox always displays the selected item and nothing can be manually entered. Any press of a letter on the keyboard is passed to the GoToLetter method of Lister, which attempts to select the next item beginning with that letter. This mode is good for the font family, font style, font weight, and font stretch boxes that FontDialog displays. To keep this dialog box as simple as possible, I decided that the user shouldn't be able to select or enter anything that's not actually available.

Despite the setting of the IsReadOnly property, the TextBox always maintains the keyboard input focus. The control is notified through an event handler when the user has clicked an item in the list section, but the list section doesn't get the input focus from that action. The OnPreviewKeyDown override provides much of the keyboard interface for the control by handling all the cursor movement keys.

The FontDialog class itself has five TextBoxWithLister controls (to display the font families, styles, weights, stretches, and sizes), five labels identifying those controls, another Label for displaying sample text, and OK and Cancel buttons, but the real complexity comes from the dynamic content of the controls.

The first TextBoxWithLister control displays all the available font families. These are obtained from the static Fonts.SystemFontFamilies method and this control is filled with those families toward the end of the constructor. However, every font family has different available font styles, weights, and stretches. Whenever the user selects a different font family, the FamilyOnSelectionChanged handler in the FontDialog class must clear out three TextBoxWithLister controls and fill them up again based on the FamilyTypeface objects available from the FamilyTypefaces property of the selected FontFamily object.

FontDialog defines just two public properties: A Typeface property encapsulates the FontFamily, FontStyle, FontWeight, and FontStretch properties, and the FaceSize property (of type double) is for the size of the font. (I couldn't call this latter property FontSize because FontDialog itself inherits a FontSize property that governs the size of the font used in controls within the dialog box!)

FontDialog.cs

[View full width]

//------------------------------------------- // FontDialog.cs (c) 2006 by Charles Petzold //------------------------------------------- using System; using System.Collections.Generic; using System.Windows; using System.Windows.Controls; using System.Windows.Input; using System.Windows.Media; namespace Petzold.ChooseFont { public class FontDialog : Window { TextBoxWithLister boxFamily, boxStyle, boxWeight, boxStretch, boxSize; Label lblDisplay; bool isUpdateSuppressed = true; // Public properties. public Typeface Typeface { set { if (boxFamily.Contains(value .FontFamily)) boxFamily.SelectedItem = value .FontFamily; else boxFamily.SelectedIndex = 0; if (boxStyle.Contains(value.Style)) boxStyle.SelectedItem = value .Style; else boxStyle.SelectedIndex = 0; if (boxWeight.Contains(value.Weight)) boxWeight.SelectedItem = value .Weight; else boxWeight.SelectedIndex = 0; if (boxStretch.Contains(value .Stretch)) boxStretch.SelectedItem = value.Stretch; else boxStretch.SelectedIndex = 0; } get { return new Typeface( (FontFamily)boxFamily.SelectedItem, (FontStyle)boxStyle.SelectedItem, (FontWeight)boxWeight.SelectedItem, (FontStretch)boxStretch.SelectedItem); } } public double FaceSize { set { double size = 0.75 * value; boxSize.Text = size.ToString(); if (!boxSize.Contains(size)) boxSize.Insert(0, size); boxSize.SelectedItem = size; } get { double size; if (!Double.TryParse(boxSize.Text, out size)) size = 8.25; return size / 0.75; } } // Constructor. public FontDialog() { Title = "Font"; ShowInTaskbar = false; WindowStyle = WindowStyle.ToolWindow; WindowStartupLocation = WindowStartupLocation.CenterOwner; SizeToContent = SizeToContent .WidthAndHeight; ResizeMode = ResizeMode.NoResize; // Create three-row Grid as content of window. Grid gridMain = new Grid(); Content = gridMain; // This row is for the TextBoxWithLister controls. RowDefinition rowdef = new RowDefinition(); rowdef.Height = new GridLength(200, GridUnitType.Pixel); gridMain.RowDefinitions.Add(rowdef); // This row is for the sample text. rowdef = new RowDefinition(); rowdef.Height = new GridLength(150, GridUnitType.Pixel); gridMain.RowDefinitions.Add(rowdef); // This row is for the buttons. rowdef = new RowDefinition(); rowdef.Height = GridLength.Auto; gridMain.RowDefinitions.Add(rowdef); // One column in main Grid. ColumnDefinition coldef = new ColumnDefinition(); coldef.Width = new GridLength(650, GridUnitType.Pixel); gridMain.ColumnDefinitions.Add(coldef); // Create two-row, five-column Grid for TextBoxWithLister controls. Grid gridBoxes = new Grid(); gridMain.Children.Add(gridBoxes); // This row is for the labels. rowdef = new RowDefinition(); rowdef.Height = GridLength.Auto; gridBoxes.RowDefinitions.Add(rowdef); // This row is for the EditBoxWithLister controls. rowdef = new RowDefinition(); rowdef.Height = new GridLength(100, GridUnitType.Star); gridBoxes.RowDefinitions.Add(rowdef); // First column is FontFamily. coldef = new ColumnDefinition(); coldef.Width = new GridLength(175, GridUnitType.Star); gridBoxes.ColumnDefinitions.Add(coldef); // Second column is FontStyle. coldef = new ColumnDefinition(); coldef.Width = new GridLength(100, GridUnitType.Star); gridBoxes.ColumnDefinitions.Add(coldef); // Third column is FontWeight. coldef = new ColumnDefinition(); coldef.Width = new GridLength(100, GridUnitType.Star); gridBoxes.ColumnDefinitions.Add(coldef); // Fourth column is FontStretch. coldef = new ColumnDefinition(); coldef.Width = new GridLength(100, GridUnitType.Star); gridBoxes.ColumnDefinitions.Add(coldef); // Fifth column is Size. coldef = new ColumnDefinition(); coldef.Width = new GridLength(75, GridUnitType.Star); gridBoxes.ColumnDefinitions.Add(coldef); // Create FontFamily labels and TextBoxWithLister controls. Label lbl = new Label(); lbl.Content = "Font Family"; lbl.Margin = new Thickness(12, 12, 12, 0); gridBoxes.Children.Add(lbl); Grid.SetRow(lbl, 0); Grid.SetColumn(lbl, 0); boxFamily = new TextBoxWithLister(); boxFamily.IsReadOnly = true; boxFamily.Margin = new Thickness(12, 0 , 12, 12); gridBoxes.Children.Add(boxFamily); Grid.SetRow(boxFamily, 1); Grid.SetColumn(boxFamily, 0); // Create FontStyle labels and TextBoxWithLister controls. lbl = new Label(); lbl.Content = "Style"; lbl.Margin = new Thickness(12, 12, 12, 0); gridBoxes.Children.Add(lbl); Grid.SetRow(lbl, 0); Grid.SetColumn(lbl, 1); boxStyle = new TextBoxWithLister(); boxStyle.IsReadOnly = true; boxStyle.Margin = new Thickness(12, 0, 12, 12); gridBoxes.Children.Add(boxStyle); Grid.SetRow(boxStyle, 1); Grid.SetColumn(boxStyle, 1); // Create FontWeight labels and TextBoxWithLister controls. lbl = new Label(); lbl.Content = "Weight"; lbl.Margin = new Thickness(12, 12, 12, 0); gridBoxes.Children.Add(lbl); Grid.SetRow(lbl, 0); Grid.SetColumn(lbl, 2); boxWeight = new TextBoxWithLister(); boxWeight.IsReadOnly = true; boxWeight.Margin = new Thickness(12, 0 , 12, 12); gridBoxes.Children.Add(boxWeight); Grid.SetRow(boxWeight, 1); Grid.SetColumn(boxWeight, 2); // Create FontStretch labels and TextBoxWithLister controls. lbl = new Label(); lbl.Content = "Stretch"; lbl.Margin = new Thickness(12, 12, 12, 0); gridBoxes.Children.Add(lbl); Grid.SetRow(lbl, 0); Grid.SetColumn(lbl, 3); boxStretch = new TextBoxWithLister(); boxStretch.IsReadOnly = true; boxStretch.Margin = new Thickness(12, 0, 12, 12); gridBoxes.Children.Add(boxStretch); Grid.SetRow(boxStretch, 1); Grid.SetColumn(boxStretch, 3); // Create Size labels and TextBoxWithLister controls. lbl = new Label(); lbl.Content = "Size"; lbl.Margin = new Thickness(12, 12, 12, 0); gridBoxes.Children.Add(lbl); Grid.SetRow(lbl, 0); Grid.SetColumn(lbl, 4); boxSize = new TextBoxWithLister(); boxSize.Margin = new Thickness(12, 0, 12, 12); gridBoxes.Children.Add(boxSize); Grid.SetRow(boxSize, 1); Grid.SetColumn(boxSize, 4); // Create Label to display sample text. lblDisplay = new Label(); lblDisplay.Content = "AaBbCc XxYzZz 012345"; lblDisplay.HorizontalContentAlignment = HorizontalAlignment.Center; lblDisplay.VerticalContentAlignment = VerticalAlignment.Center; gridMain.Children.Add(lblDisplay); Grid.SetRow(lblDisplay, 1); // Create five-column Grid for Buttons. Grid gridButtons = new Grid(); gridMain.Children.Add(gridButtons); Grid.SetRow(gridButtons, 2); for (int i = 0; i < 5; i++) gridButtons.ColumnDefinitions.Add (new ColumnDefinition()); // OK button. Button btn = new Button(); btn.Content = "OK"; btn.IsDefault = true; btn.HorizontalAlignment = HorizontalAlignment.Center; btn.MinWidth = 60; btn.Margin = new Thickness(12); btn.Click += OkOnClick; gridButtons.Children.Add(btn); Grid.SetColumn(btn, 1); // Cancel button. btn = new Button(); btn.Content = "Cancel"; btn.IsCancel = true; btn.HorizontalAlignment = HorizontalAlignment.Center; btn.MinWidth = 60; btn.Margin = new Thickness(12); gridButtons.Children.Add(btn); Grid.SetColumn(btn, 3); // Initialize FontFamily box with system font families. foreach (FontFamily fam in Fonts .SystemFontFamilies) boxFamily.Add(fam); // Initialize FontSize box. double[] ptsizes = new double[] { 8, 9 , 10, 11, 12, 14, 16, 18, 20, 22, 24, 26, 28, 36, 48, 72 }; foreach (double ptsize in ptsizes) boxSize.Add(ptsize); // Set event handlers. boxFamily.SelectionChanged += FamilyOnSelectionChanged; boxStyle.SelectionChanged += StyleOnSelectionChanged; boxWeight.SelectionChanged += StyleOnSelectionChanged; boxStretch.SelectionChanged += StyleOnSelectionChanged; boxSize.TextChanged += SizeOnTextChanged; // Initialize selected values based on Window properties. // (These will probably be overridden when properties are set.) Typeface = new Typeface(FontFamily, FontStyle, FontWeight, FontStretch); FaceSize = FontSize; // Set keyboard focus. boxFamily.Focus(); // Allow updates to the sample text. isUpdateSuppressed = false; UpdateSample(); } // Event handler for SelectionChanged in FontFamily box. void FamilyOnSelectionChanged(object sender, EventArgs args) { // Get selected FontFamily. FontFamily fntfam = (FontFamily)boxFamily.SelectedItem; // Save previous Style, Weight, Stretch. // These should only be null when this method is called for the // first time. FontStyle? fntstyPrevious = (FontStyle?)boxStyle.SelectedItem; FontWeight? fntwtPrevious = (FontWeight?) boxWeight.SelectedItem; FontStretch? fntstrPrevious = (FontStretch?)boxStretch.SelectedItem; // Turn off Sample display. isUpdateSuppressed = true; // Clear Style, Weight, and Stretch boxes. boxStyle.Clear(); boxWeight.Clear(); boxStretch.Clear(); // Loop through typefaces in selected FontFamily. foreach (FamilyTypeface ftf in fntfam .FamilyTypefaces) { // Put Style in boxStyle (Normal always at top). if (!boxStyle.Contains(ftf.Style)) { if (ftf.Style == FontStyles .Normal) boxStyle.Insert(0, ftf.Style); else boxStyle.Add(ftf.Style); } // Put Weight in boxWeight (Normal always at top). if (!boxWeight.Contains(ftf.Weight)) { if (ftf.Weight == FontWeights .Normal) boxWeight.Insert(0, ftf .Weight); else boxWeight.Add(ftf.Weight); } // Put Stretch in boxStretch (Normal always at top). if (!boxStretch.Contains(ftf.Stretch)) { if (ftf.Stretch == FontStretches.Normal) boxStretch.Insert(0, ftf .Stretch); else boxStretch.Add(ftf.Stretch); } } // Set selected item in boxStyle. if (boxStyle.Contains(fntstyPrevious)) boxStyle.SelectedItem = fntstyPrevious; else boxStyle.SelectedIndex = 0; // Set selected item in boxWeight. if (boxWeight.Contains(fntwtPrevious)) boxWeight.SelectedItem = fntwtPrevious; else boxWeight.SelectedIndex = 0; // Set selected item in boxStretch. if (boxStretch.Contains(fntstrPrevious)) boxStretch.SelectedItem = fntstrPrevious; else boxStretch.SelectedIndex = 0; // Resume Sample update and update the Sample. isUpdateSuppressed = false; UpdateSample(); } // Event handler for SelectionChanged in Style, Weight, Stretch boxes. void StyleOnSelectionChanged(object sender , EventArgs args) { UpdateSample(); } // Event handler for TextChanged in Size box. void SizeOnTextChanged(object sender, TextChangedEventArgs args) { UpdateSample(); } // Update the Sample text. void UpdateSample() { if (isUpdateSuppressed) return; lblDisplay.FontFamily = (FontFamily)boxFamily.SelectedItem; lblDisplay.FontStyle = (FontStyle)boxStyle.SelectedItem; lblDisplay.FontWeight = (FontWeight)boxWeight.SelectedItem; lblDisplay.FontStretch = (FontStretch)boxStretch.SelectedItem; double size; if (!Double.TryParse(boxSize.Text, out size)) size = 8.25; lblDisplay.FontSize = size / 0.75; } // OK button terminates dialog box. void OkOnClick(object sender, RoutedEventArgs args) { DialogResult = true; } } }



To test out the FontDialog, this ChooseFont program simply creates a Button in the middle of its client area and invokes the dialog whenever the button is clicked. If the user clicks OK, the program sets the window's font properties to those from the dialog box and, of course, the button inherits those properties.

ChooseFont.cs

[View full width]

//------------------------------------------- // ChooseFont.cs (c) 2006 by Charles Petzold //------------------------------------------- using System; using System.Windows; using System.Windows.Controls; using System.Windows.Input; using System.Windows.Media; namespace Petzold.ChooseFont { public class ChooseFont : Window { [STAThread] public static void Main() { Application app = new Application(); app.Run(new ChooseFont()); } public ChooseFont() { Title = "Choose Font"; Button btn = new Button(); btn.Content = Title; btn.HorizontalAlignment = HorizontalAlignment.Center; btn.VerticalAlignment = VerticalAlignment.Center; btn.Click += ButtonOnClick; Content = btn; } void ButtonOnClick(object sender, RoutedEventArgs args) { FontDialog dlg = new FontDialog(); dlg.Owner = this; // Set FontDialog properties from Window. dlg.Typeface = new Typeface(FontFamily , FontStyle, FontWeight , FontStretch); dlg.FaceSize = FontSize; if (dlg.ShowDialog().GetValueOrDefault()) { // Set Window properties from FontDialog. FontFamily = dlg.Typeface.FontFamily; FontStyle = dlg.Typeface.Style; FontWeight = dlg.Typeface.Weight; FontStretch = dlg.Typeface.Stretch; FontSize = dlg.FaceSize; } } } }



Without further ado, here is a better version of the banner printer program. This project requires links to the BannerDocumentPaginator.cs file and the three files used for the FontDialog class: Lister.cs, TextBoxWithLister.cs, and FontDialog.cs. This new version of the program has a Font button that displays the FontDialog. The typeface you specify there is just transferred to the BannerDocumentPaginator. The size you request is ignored, of course, because the BannerDocumentPaginator calculates its own font size based on the size of the page.

PrintBetterBanner.cs

[View full width]

//-------------------------------------------------- // PrintBetterBanner.cs (c) 2006 by Charles Petzold //-------------------------------------------------- using Petzold.ChooseFont; using Petzold.PrintBanner; using System; using System.Printing; using System.Windows; using System.Windows.Controls; using System.Windows.Input; using System.Windows.Media; namespace Petzold.PrintBetterBanner { public class PrintBetterBanner : Window { TextBox txtbox; Typeface face; [STAThread] public static void Main() { Application app = new Application(); app.Run(new PrintBetterBanner()); } public PrintBetterBanner() { Title = "Print Better Banner"; SizeToContent = SizeToContent .WidthAndHeight; // Make StackPanel content of window. StackPanel stack = new StackPanel(); Content = stack; // Create TextBox. txtbox = new TextBox(); txtbox.Width = 250; txtbox.Margin = new Thickness(12); stack.Children.Add(txtbox); // Create Font Button. Button btn = new Button(); btn.Content = "_Font..."; btn.Margin = new Thickness(12); btn.Click += FontOnClick; btn.HorizontalAlignment = HorizontalAlignment.Center; stack.Children.Add(btn); // Create Print Button. btn = new Button(); btn.Content = "_Print..."; btn.Margin = new Thickness(12); btn.Click += PrintOnClick; btn.HorizontalAlignment = HorizontalAlignment.Center; stack.Children.Add(btn); // Initialize Facename field. face = new Typeface(FontFamily, FontStyle, FontWeight, FontStretch); txtbox.Focus(); } void FontOnClick(object sender, RoutedEventArgs args) { FontDialog dlg = new FontDialog(); dlg.Owner = this; dlg.Typeface = face; if (dlg.ShowDialog().GetValueOrDefault()) { face = dlg.Typeface; } } void PrintOnClick(object sender, RoutedEventArgs args) { PrintDialog dlg = new PrintDialog(); if (dlg.ShowDialog().GetValueOrDefault()) { // Make sure orientation is Portrait. PrintTicket prntkt = dlg.PrintTicket; prntkt.PageOrientation = PageOrientation.Portrait; dlg.PrintTicket = prntkt; // Create new DocumentPaginator object. BannerDocumentPaginator paginator = new BannerDocumentPaginator(); // Set Text property from TextBox. paginator.Text = txtbox.Text; // Set Typeface property from field. paginator.Typeface = face; // Give it a PageSize property based on the paper dimensions. paginator.PageSize = new Size(dlg .PrintableAreaWidth, dlg .PrintableAreaHeight); // Call PrintDocument to print the document. dlg.PrintDocument(paginator, "Banner: " + txtbox.Text); } } } }



A banner paginator might serve a purpose in illustrating the basics of printing multiple pages; it's not nearly as useful as a program that arranges text into pages. Such a paginator can be quite complex when multiple typefaces are involved, but even a paginator that handles plain text with a uniform font isn't trivial.

Despite the difficulties, the Notepad Clone presented in the next chapter requires a paginator that handles plain text and word wrapping, and the PlainTextDocumentPaginator class (also in the next chapter) is a significant part of that project.




Applications = Code + Markup. A Guide to the Microsoft Windows Presentation Foundation
Applications = Code + Markup: A Guide to the Microsoft Windows Presentation Foundation (Pro - Developer)
ISBN: 0735619573
EAN: 2147483647
Year: 2006
Pages: 72

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net