Chapter 18. The Notepad Clone


In the course of learning a new operating system or programming interface, there comes a time when the programmer looks at a common application and says, "I could write that program." To prove it, the programmer might even take a stab at coding a clone of the application. Perhaps "clone" is not quite the right word, for the new program isn't anywhere close to a genetic copy. The objective is to mimic the user interface and functionality as close as possible without duplicating copyrighted code!

The Notepad Clone presented in this chapter is very close to the look, feel, and functionality of the Microsoft Windows Notepad program. The only major feature I left out was the Help window. (I'll demonstrate how to implement application Help information in Chapter 25.) Notepad Clone has file I/O, printing, search and replace, a font dialog, and it saves user preferences between sessions.

The Notepad Clone project requires links to the PrintMarginDialog.cs file from the previous chapter and the three files that contribute to the FontDialog: FontDialog.cs, Lister.cs, and TextBoxWithLister.cs. All the other files that comprise Notepad Clone are in this chapter.

Of course, the world hardly needs another plain text editor, but the benefit of writing a Notepad Clone is not purely academic. In Chapter 20 I will add a few files to those shown in this file and create another program named XAML Cruncher. You'll find XAML Cruncher to be a powerful tool for learning and experimenting with Extended Application Markup Language (XAML) throughout Part II of this book. Because two classes in the XAML Cruncher program derive from classes in the Notepad Clone program, Notepad Clone sometimes makes itself more amenable to inheritance with somewhat roundabout code. I'll point out when that's the case.

The first file of the Notepad Clone project is simply a series of C# attribute statements that result in the creation of metadata in the NotepadClone.exe file. This metadata identifies the program, including a copyright notice and version information. You should include such a file in any "real-life" application.

NotepadCloneAssemblyInfo.cs

[View full width]

//--------- ------------------------------------------------ // NotepadCloneAssemblyInfo.cs (c) 2006 by Charles Petzold //------------------------------------------------ --------- using System.Reflection; [assembly: AssemblyTitle("Notepad Clone")] [assembly: AssemblyProduct("NotepadClone")] [assembly: AssemblyDescription("Functionally Similar to Windows Notepad")] [assembly: AssemblyCompany("www.charlespetzold.com")] [assembly: AssemblyCopyright("\x00A9 2006 by Charles Petzold")] [assembly: AssemblyVersion("1.0.*")] [assembly: AssemblyFileVersion("1.0.0.0")]



You don't have to treat this file any differently than you treat the other C# source code files in the project. It's compiled along with everything else. This file is the only file in the Notepad Clone program that is not also in the XAML Cruncher project. XAML Cruncher has its own assembly information file.

I mentioned that Notepad Clone saves user preferences between sessions. These days, XML is the standard format for saving settings. Generally, applications store per-user program settings in the area of isolated storage known as "user application data." For a program named NotepadClone distributed by a company named Petzold and installed by a user named Deirdre, the program settings would be saved in this directory:

\Documents and Settings\Deirdre\Application Data\Petzold\NotepadClone

However, the XAML Cruncher program is downloadable from my Web site using the ClickOnce application installation, and for that type of installation it's recommended that program settings go in the area known as "local user application data":

\Documents and Settings\Deirdre\Local Settings\Application Data\Petzold\NotepadClone

Although both of these areas are specific to the particular user, the regular user application data is intended for roamingthat is, when the user logs on to a different computerwhereas the local user application data is specific to a particular user on the particular computer.

It's handy to define user preference as public fields or properties in a class specific to this purpose. You can then use the XmlSerializer class to automatically save and load the information in XML format. The first step to saving or loading the file is to create an XmlSerializer object based on the class you want to serialize:

XmlSerializer xml = new XmlSerializer(typeof(MyClass)); 


You can then save an object of type MyClass in XML by calling the Serialize method. To specify the file, you'll need an object of type Stream, XmlWriter, or TextWriter (from which StreamWriter descends). The Deserialize method of XmlSerializer converts XML into an object of the type you specified in the XmlSerializer constructor. The Deserialize method returns an object of type object that you can cast into the correct type.

As you might guess, the XmlSerializer class uses reflection to look inside the class you specify in the XmlSerializer constructor and examine its members. According to the documentation, XmlSerializer actually generates code that is executed outside your execution space. For that reason, any class you want serialized must be defined as public. XmlSerializer serializes and deserializes only fields and properties that are defined as public, and it ignores any read-only or write-only members. When deserializing, XmlSerializer creates an object of the proper type using the class's parameterless constructor and then sets the object's properties from the XML elements. XmlSerializer won't work with a class that has no parameterless constructor.

If any serializable members of the class are complex data types, those classes and structures must also be public and must also have parameterless constructors. The XmlSerializer will treat the public read/write properties and fields of these other classes and structures as nested elements. XmlSerializer can also handle properties that are arrays and List objects.

Here is the NotepadCloneSettings class. For convenience and to save space, I've defined the public members as fields rather than properties.

NotepadCloneSettings.cs

[View full width]

//--------- -------------------------------------------- // NotepadCloneSettings.cs (c) 2006 by Charles Petzold //------------------------------------------------ ----- using System; using System.IO; using System.Windows; using System.Windows.Media; using System.Xml.Serialization; namespace Petzold.NotepadClone { public class NotepadCloneSettings { // Default Settings. public WindowState WindowState = WindowState.Normal; public Rect RestoreBounds = Rect.Empty; public TextWrapping TextWrapping = TextWrapping.NoWrap; public string FontFamily = ""; public string FontStyle = new FontStyleConverter() .ConvertToString(FontStyles.Normal); public string FontWeight = new FontWeightConverter() .ConvertToString(FontWeights.Normal); public string FontStretch = new FontStretchConverter() .ConvertToString(FontStretches.Normal); public double FontSize = 11; // Save settings to file. public virtual bool Save(string strAppData) { try { Directory.CreateDirectory(Path .GetDirectoryName(strAppData)); StreamWriter write = new StreamWriter(strAppData); XmlSerializer xml = new XmlSerializer(GetType()); xml.Serialize(write, this); write.Close(); } catch { return false; } return true; } // Load settings from file. public static object Load(Type type, string strAppData) { StreamReader reader; object settings; XmlSerializer xml = new XmlSerializer (type); try { reader = new StreamReader(strAppData); settings = xml.Deserialize(reader); reader.Close(); } catch { settings = type.GetConstructor(System .Type.EmptyTypes).Invoke(null); } return settings; } } }



The class contains two methods. The Save method requires a fully qualified file name and creates StreamWriter and XmlSerializer objects for writing out the fields. The Load method is static because it's responsible for creating an object of type NotepadCloneSettings, either by calling Deserialize or (if the settings file does not yet exist) by using the class's constructor.

The Load method is more complex than it needs to be because I reuse this file and derive from it in the XAML Cruncher project. If I didn't need to reuse the class, the Type parameter to Load wouldn't be required, and the argument to the XmlSerializer constructor would instead be typeof(NotepadCloneSettings). Rather than using reflection to invoke the class's constructor in the catch block, a simple new NotepadCloneSettings() would suffice.

As you can see, the first three fields are two enumerations and a structure. These serialize and deserialize fine. However, most of the font-related properties weren't quite so agreeable. FontStyle, for example, is a structure with no public fields or properties. I explicitly defined these font-related fields in NotepadCloneSettings as string objects, and then initialized them from various classes (such as FontStyleConverter) that are most often used in parsing XAML.

The NotepadClone.cs file is next, and if you think this file looks a little small to be the main file of the project, you're absolutely right. As is normal, my NotepadClone class derives from Window, but I have divided the source code for this class into seven files using the partial keyword. The different files roughly correspond to the top-level menu items. For example, the code in the NotepadClone.Edit.cs file is also part of the NotepadClone class but it handles the creation and event handling of the Edit menu.

It is helpful to keep in mind that these seven files are all part of the same class and have direct access to the methods and fields defined in other files. In particular, the TextBox control that dominates the program's client area is stored in a field named txtbox. Lots of different methods need access to that object.

The NotepadClone.cs file contains the NotepadClone constructor, which begins by accessing some of the assembly metadata defined in the NotepadCloneAssemblyInfo.cs file to obtain the program title (the string "Notepad Clone") and to construct a file path to store the settings. The program uses this approach to make it hospitable to inheritance. The constructor then proceeds by laying out the window with a DockPanel, Menu, StatusBar, and TextBox. Virtually all of the menu construction is relegated to other files.

NotepadClone.cs

[View full width]

//--------------------------------------------- // NotepadClone.cs (c) 2006 by Charles Petzold //--------------------------------------------- using System; using System.ComponentModel; using System.IO; using System.Reflection; using System.Windows; using System.Windows.Controls; using System.Windows.Controls.Primitives; using System.Windows.Input; using System.Windows.Media; namespace Petzold.NotepadClone { public partial class NotepadClone : Window { protected string strAppTitle; // Name of program for title bar. protected string strAppData; // Full file name of settings file. protected NotepadCloneSettings settings; // Settings. protected bool isFileDirty = false; // Flag for file save prompt. // Controls used in main window. protected Menu menu; protected TextBox txtbox; protected StatusBar status; string strLoadedFile; // Fully qualified loaded file name. StatusBarItem statLineCol; // Line and column status. [STAThread] public static void Main() { Application app = new Application(); app.ShutdownMode = ShutdownMode .OnMainWindowClose; app.Run(new NotepadClone()); } public NotepadClone() { // Get this executing assembly to access attributes. Assembly asmbly = Assembly .GetExecutingAssembly(); // Get the AssemblyTitle attribute for the strAppTitle field. AssemblyTitleAttribute title = (AssemblyTitleAttribute)asmbly. GetCustomAttributes(typeof (AssemblyTitleAttribute), false)[0]; strAppTitle = title.Title; // Get the AssemblyProduct attribute for the strAppData file name. AssemblyProductAttribute product = (AssemblyProductAttribute)asmbly. GetCustomAttributes(typeof (AssemblyProductAttribute), false)[0]; strAppData = Path.Combine( Environment.GetFolderPath( Environment.SpecialFolder .LocalApplicationData), "Petzold\\" + product .Product + "\\" + product.Product + " .Settings.xml"); // Create DockPanel as content of window. DockPanel dock = new DockPanel(); Content = dock; // Create Menu docked at top. menu = new Menu(); dock.Children.Add(menu); DockPanel.SetDock(menu, Dock.Top); // Create StatusBar docked at bottom. status = new StatusBar(); dock.Children.Add(status); DockPanel.SetDock(status, Dock.Bottom); // Create StatusBarItem to display line and column. statLineCol = new StatusBarItem(); statLineCol.HorizontalAlignment = HorizontalAlignment.Right; status.Items.Add(statLineCol); DockPanel.SetDock(statLineCol, Dock .Right); // Create TextBox to fill remainder of client area. txtbox = new TextBox(); txtbox.AcceptsReturn = true; txtbox.AcceptsTab = true; txtbox.VerticalScrollBarVisibility = ScrollBarVisibility.Auto; txtbox.HorizontalScrollBarVisibility = ScrollBarVisibility.Auto; txtbox.TextChanged += TextBoxOnTextChanged; txtbox.SelectionChanged += TextBoxOnSelectionChanged; dock.Children.Add(txtbox); // Create all the top-level menu items. AddFileMenu(menu); // in NotepadClone.File.cs AddEditMenu(menu); // in NotepadClone.Edit.cs AddFormatMenu(menu); // in NotepadClone.Format.cs AddViewMenu(menu); // in NotepadClone.View.cs AddHelpMenu(menu); // in NotepadClone.Help.cs // Load settings saved from previous run. settings = (NotepadCloneSettings) LoadSettings(); // Apply saved settings. WindowState = settings.WindowState; if (settings.RestoreBounds != Rect.Empty) { Left = settings.RestoreBounds.Left; Top = settings.RestoreBounds.Top; Width = settings.RestoreBounds.Width; Height = settings.RestoreBounds .Height; } txtbox.TextWrapping = settings .TextWrapping; txtbox.FontFamily = new FontFamily (settings.FontFamily); txtbox.FontStyle = (FontStyle) new FontStyleConverter(). ConvertFromString (settings.FontStyle); txtbox.FontWeight = (FontWeight) new FontWeightConverter(). ConvertFromString (settings.FontWeight); txtbox.FontStretch = (FontStretch) new FontStretchConverter(). ConvertFromString (settings.FontStretch); txtbox.FontSize = settings.FontSize; // Install handler for Loaded event. Loaded += WindowOnLoaded; // Set focus to TextBox. txtbox.Focus(); } // Overridable method to load settings, called from constructor. protected virtual object LoadSettings() { return NotepadCloneSettings.Load (typeof(NotepadCloneSettings), strAppData); } // Event handler for Loaded event: Simulates New command & // possibly loads command-line file. void WindowOnLoaded(object sender, RoutedEventArgs args) { ApplicationCommands.New.Execute(null, this); // Get command-line arguments. string[] strArgs = Environment .GetCommandLineArgs(); if (strArgs.Length > 1) // First argument is program name! { if (File.Exists(strArgs[1])) { LoadFile(strArgs[1]); } else { MessageBoxResult result = MessageBox.Show("Cannot find the " + Path.GetFileName (strArgs[1]) + " file.\r\n\r\n" + "Do you want to create a new file?", strAppTitle, MessageBoxButton.YesNoCancel, MessageBoxImage.Question); // Close the window if the user clicks "Cancel". if (result == MessageBoxResult .Cancel) Close(); // Create and close file for "Yes". else if (result == MessageBoxResult.Yes) { try { File.Create (strLoadedFile = strArgs[1]).Close(); } catch (Exception exc) { MessageBox.Show("Error on File Creation: " + exc .Message, strAppTitle, MessageBoxButton .OK, MessageBoxImage.Asterisk); return; } UpdateTitle(); } // No action for "No". } } } // OnClosing event: See if it's OK to trash the file. protected override void OnClosing (CancelEventArgs args) { base.OnClosing(args); args.Cancel = !OkToTrash(); settings.RestoreBounds = RestoreBounds; } // OnClosed event: Set fields of 'settings' and call SaveSettings. protected override void OnClosed(EventArgs args) { base.OnClosed(args); settings.WindowState = WindowState; settings.TextWrapping = txtbox .TextWrapping; settings.FontFamily = txtbox .FontFamily.ToString(); settings.FontStyle = new FontStyleConverter() .ConvertToString(txtbox.FontStyle); settings.FontWeight = new FontWeightConverter() .ConvertToString(txtbox.FontWeight); settings.FontStretch = new FontStretchConverter() .ConvertToString(txtbox.FontStretch); settings.FontSize = txtbox.FontSize; SaveSettings(); } // Overridable method to call Save in the 'settings' object. protected virtual void SaveSettings() { settings.Save(strAppData); } // UpdateTitle displays file name or "Untitled". protected void UpdateTitle() { if (strLoadedFile == null) Title = "Untitled - " + strAppTitle; else Title = Path.GetFileName (strLoadedFile) + " - " + strAppTitle; } // When the TextBox text changes, just set isFileDirty. void TextBoxOnTextChanged(object sender, RoutedEventArgs args) { isFileDirty = true; } // When the selection changes, update the status bar. void TextBoxOnSelectionChanged(object sender, RoutedEventArgs args) { int iChar = txtbox.SelectionStart; int iLine = txtbox .GetLineIndexFromCharacterIndex(iChar); // Check for error that may be a bug. if (iLine == -1) { statLineCol.Content = ""; return; } int iCol = iChar - txtbox .GetCharacterIndexFromLineIndex(iLine); string str = String.Format("Line {0} Col {1}", iLine + 1, iCol + 1); if (txtbox.SelectionLength > 0) { iChar += txtbox.SelectionLength; iLine = txtbox .GetLineIndexFromCharacterIndex(iChar); iCol = iChar - txtbox .GetCharacterIndexFromLineIndex(iLine); str += String.Format(" - Line {0} Col {1}", iLine + 1, iCol + 1); } statLineCol.Content = str; } } }



Towards the end of the constructor, the NotepadClone class calls a virtual method named LoadSettings to load the settings file, and also installs a handler for the Loaded event. The Loaded event handler is responsible for checking command-line arguments and possibly loading a file. All file-related methods are located in the NotepadClone.File.cs file (coming up).

The NotepadClone.cs file concludes with event handlers for the TextChanged and SelectionChanged events of the TextBox. The TextChanged handler simply sets the isFileDirty flag to true so that the program properly prompts the user to save the file if it's been altered. The SelectionChanged handler is responsible for displaying the line and column numbers of the caret or selection in the status bar.

The AddFileMenu method in the NotepadClone.File.cs file is responsible for assembling the File menu and handling all file-related commands. Many of the items on the File menu have corresponding static properties in the ApplicationCommands class. These items can be handled with command bindings.

NotepadClone.File.cs

[View full width]

//-------------------------------------------------- // NotepadClone.File.cs (c) 2006 by Charles Petzold //-------------------------------------------------- using Microsoft.Win32; using System; using System.IO; using System.Windows; using System.Windows.Controls; using System.Windows.Input; using System.Windows.Media; using System.Printing; namespace Petzold.NotepadClone { public partial class NotepadClone : Window { // Filter for File Open and Save dialog boxes. protected string strFilter = "Text Documents(*.txt)|* .txt|All Files(*.*)|*.*"; void AddFileMenu(Menu menu) { // Create top-level File item. MenuItem itemFile = new MenuItem(); itemFile.Header = "_File"; menu.Items.Add(itemFile); // New menu item. MenuItem itemNew = new MenuItem(); itemNew.Header = "_New"; itemNew.Command = ApplicationCommands.New; itemFile.Items.Add(itemNew); CommandBindings.Add( new CommandBinding (ApplicationCommands.New, NewOnExecute)); // Open menu item. MenuItem itemOpen = new MenuItem(); itemOpen.Header = "_Open..."; itemOpen.Command = ApplicationCommands .Open; itemFile.Items.Add(itemOpen); CommandBindings.Add( new CommandBinding (ApplicationCommands.Open, OpenOnExecute)); // Save menu item. MenuItem itemSave = new MenuItem(); itemSave.Header = "_Save"; itemSave.Command = ApplicationCommands .Save; itemFile.Items.Add(itemSave); CommandBindings.Add( new CommandBinding (ApplicationCommands.Save, SaveOnExecute)); // Save As menu item. MenuItem itemSaveAs = new MenuItem(); itemSaveAs.Header = "Save _As..."; itemSaveAs.Command = ApplicationCommands.SaveAs; itemFile.Items.Add(itemSaveAs); CommandBindings.Add( new CommandBinding (ApplicationCommands.SaveAs, SaveAsOnExecute)); // Separators and printing items. itemFile.Items.Add(new Separator()); AddPrintMenuItems(itemFile); itemFile.Items.Add(new Separator()); // Exit menu item. MenuItem itemExit = new MenuItem(); itemExit.Header = "E_xit"; itemExit.Click += ExitOnClick; itemFile.Items.Add(itemExit); } // File New command: Start with empty TextBox. protected virtual void NewOnExecute(object sender, ExecutedRoutedEventArgs args) { if (!OkToTrash()) return; txtbox.Text = ""; strLoadedFile = null; isFileDirty = false; UpdateTitle(); } // File Open command: Display dialog box and load file. void OpenOnExecute(object sender, ExecutedRoutedEventArgs args) { if (!OkToTrash()) return; OpenFileDialog dlg = new OpenFileDialog(); dlg.Filter = strFilter; if ((bool)dlg.ShowDialog(this)) { LoadFile(dlg.FileName); } } // File Save command: Possibly execute SaveAsExecute. void SaveOnExecute(object sender, ExecutedRoutedEventArgs args) { if (strLoadedFile == null || strLoadedFile.Length == 0) DisplaySaveDialog(""); else SaveFile(strLoadedFile); } // File Save As command; display dialog box and save file. void SaveAsOnExecute(object sender, ExecutedRoutedEventArgs args) { DisplaySaveDialog(strLoadedFile); } // Display Save dialog box and return true if file is saved. bool DisplaySaveDialog(string strFileName) { SaveFileDialog dlg = new SaveFileDialog(); dlg.Filter = strFilter; dlg.FileName = strFileName; if ((bool)dlg.ShowDialog(this)) { SaveFile(dlg.FileName); return true; } return false; // for OkToTrash. } // File Exit command: Just close the window. void ExitOnClick(object sender, RoutedEventArgs args) { Close(); } // OkToTrash returns true if the TextBox contents need not be saved. bool OkToTrash() { if (!isFileDirty) return true; MessageBoxResult result = MessageBox.Show("The text in the file " + strLoadedFile + " has changed\n\n" + "Do you want to save the changes?", strAppTitle, MessageBoxButton .YesNoCancel, MessageBoxImage .Question, MessageBoxResult.Yes); if (result == MessageBoxResult.Cancel) return false; else if (result == MessageBoxResult.No) return true; else // result == MessageBoxResult.Yes { if (strLoadedFile != null && strLoadedFile.Length > 0) return SaveFile(strLoadedFile); return DisplaySaveDialog(""); } } // LoadFile method possibly displays message box if error. void LoadFile(string strFileName) { try { txtbox.Text = File.ReadAllText (strFileName); } catch (Exception exc) { MessageBox.Show("Error on File Open: " + exc.Message, strAppTitle, MessageBoxButton .OK, MessageBoxImage.Asterisk); return; } strLoadedFile = strFileName; UpdateTitle(); txtbox.SelectionStart = 0; txtbox.SelectionLength = 0; isFileDirty = false; } // SaveFile method possibly displays message box if error. bool SaveFile(string strFileName) { try { File.WriteAllText(strFileName, txtbox.Text); } catch (Exception exc) { MessageBox.Show("Error on File Save" + exc.Message, strAppTitle, MessageBoxButton .OK, MessageBoxImage.Asterisk); return false; } strLoadedFile = strFileName; UpdateTitle(); isFileDirty = false; return true; } } }



This file also contains the all-important OkToTrash method, which interrogates the user to determine if it's all right to abandon a file that has some changes made but which hasn't yet been saved.

The two printing-related items on the File menu are handled in the next source code file, NotepadClone.Print.cs. The logic in this file should look fairly similar to the printing code from the previous chapter. The big difference here is that this file uses a class named PlainTextDocumentPaginator, which has several properties, such as Typeface and FaceSize, that are similar to those in FontDialog and which the code here gets from the TextBox. Another PlainTextDocumentPaginator property is TextWrapping, which is the TextBox property that controls how text is wrapped (or not).

NotepadClone.Print.cs

[View full width]

//--------------------------------------------------- // NotepadClone.Print.cs (c) 2006 by Charles Petzold //--------------------------------------------------- using Petzold.PrintWithMargins; // for PageMarginsDialog. using System; using System.Windows; using System.Windows.Controls; using System.Windows.Input; using System.Windows.Media; using System.Printing; namespace Petzold.NotepadClone { public partial class NotepadClone : Window { // Fields for printing. PrintQueue prnqueue; PrintTicket prntkt; Thickness marginPage = new Thickness(96); void AddPrintMenuItems(MenuItem itemFile) { // Page Setup menu item. MenuItem itemSetup = new MenuItem(); itemSetup.Header = "Page Set_up..."; itemSetup.Click += PageSetupOnClick; itemFile.Items.Add(itemSetup); // Print menu item. MenuItem itemPrint = new MenuItem(); itemPrint.Header = "_Print..."; itemPrint.Command = ApplicationCommands.Print; itemFile.Items.Add(itemPrint); CommandBindings.Add( new CommandBinding (ApplicationCommands.Print, PrintOnExecuted)); } void PageSetupOnClick(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; } } void PrintOnExecuted(object sender, ExecutedRoutedEventArgs args) { PrintDialog dlg = new PrintDialog(); // Get the PrintQueue and PrintTicket from previous invocations. if (prnqueue != null) dlg.PrintQueue = prnqueue; if (prntkt != null) prntkt = dlg.PrintTicket; if (dlg.ShowDialog().GetValueOrDefault()) { // Save PrintQueue and PrintTicket from dialog box. prnqueue = dlg.PrintQueue; prntkt = dlg.PrintTicket; // Create a PlainTextDocumentPaginator object. PlainTextDocumentPaginator paginator = new PlainTextDocumentPaginator(); // Set the paginator properties. paginator.PrintTicket = prntkt; paginator.Text = txtbox.Text; paginator.Header = strLoadedFile; paginator.Typeface = new Typeface(txtbox.FontFamily , txtbox.FontStyle, txtbox.FontWeight , txtbox.FontStretch); paginator.FaceSize = txtbox.FontSize; paginator.TextWrapping = txtbox .TextWrapping; paginator.Margins = marginPage; paginator.PageSize = new Size(dlg .PrintableAreaWidth, dlg .PrintableAreaHeight); // Print the document. dlg.PrintDocument(paginator, Title); } } } }



The PlainTextDocumentPaginator obviously does most of the grunt work. I gave the class a text property named Header that Notepad Clone sets to the file name of the loaded file. I also wanted the paginator to print a footer consisting of the page number and the total number of pages, for example, "Page 5 of 15."

The basic formatting occurs in the Format method of PlainTextDocumentPaginator. The method loops through each line of text in the file. (Keep in mind that when wrapping is in effect, each line of text is basically a paragraph.) Each line is passed to the ProcessLine method, which takes wrapping into account to break the text line into one or more printable lines. Each printable line is stored as a PrintLine object in a List collection. After all the PrintLine objects are accumulated, it is easy to determine the number of pages based on the size of the page, the margins, and the total numbers of lines. The Format method concludes by drawing text on the page using DrawText calls.

PlainTextDocumentPaginator.cs

[View full width]

//--------- -------------------------------------------------- // PlainTextDocumentPaginator.cs (c) 2006 by Charles Petzold //------------------------------------------------ ----------- using System; using System.Collections.Generic; using System.Globalization; using System.IO; using System.Printing; using System.Windows; using System.Windows.Controls; using System.Windows.Documents; using System.Windows.Media; namespace Petzold.NotepadClone { public class PlainTextDocumentPaginator : DocumentPaginator { // Private fields, including those associated with public properties. char[] charsBreak = new char[] { ' ', '-' }; string txt = ""; string txtHeader = null; Typeface face = new Typeface(""); double em = 11; Size sizePage = new Size(8.5 * 96, 11 * 96); Size sizeMax = new Size(0, 0); Thickness margins = new Thickness(96); PrintTicket prntkt = new PrintTicket(); TextWrapping txtwrap = TextWrapping.Wrap; // Stores each page as a DocumentPage object. List<DocumentPage> listPages; // Public properties. public string Text { set { txt = value; } get { return txt; } } public TextWrapping TextWrapping { set { txtwrap = value; } get { return txtwrap; } } public Thickness Margins { set { margins = value; } get { return margins; } } public Typeface Typeface { set { face = value; } get { return face; } } public double FaceSize { set { em = value; } get { return em; } } public PrintTicket PrintTicket { set { prntkt = value; } get { return prntkt; } } public string Header { set { txtHeader = value; } get { return txtHeader; } } // Required overrides. public override bool IsPageCountValid { get { if (listPages == null) Format(); return true; } } public override int PageCount { get { if (listPages == null) return 0; return listPages.Count; } } public override Size PageSize { set { sizePage = value; } get { return sizePage; } } public override DocumentPage GetPage(int numPage) { return listPages[numPage]; } public override IDocumentPaginatorSource Source { get { return null; } } // An internal class to indicate if an arrow is to be printed at the // end of the line of text. class PrintLine { public string String; public bool Flag; public PrintLine(string str, bool flag) { String = str; Flag = flag; } } // Formats entire document into pages. void Format() { // Store each line of the document as with LineWithFlag object. List<PrintLine> listLines = new List<PrintLine>(); // Use this for some basic calculations. FormattedText formtxtSample = GetFormattedText("W"); // Width of printed line. double width = PageSize.Width - Margins.Left - Margins.Right; // Serious problem: Abandon ship. if (width < formtxtSample.Width) return; string strLine; Pen pn = new Pen(Brushes.Black, 2); StringReader reader = new StringReader (txt); // Call ProcessLine to store each line in listLines. while (null != (strLine = reader .ReadLine())) ProcessLine(strLine, width, listLines); reader.Close(); // Now start getting ready to print pages. double heightLine = formtxtSample .LineHeight + formtxtSample.Height; double height = PageSize.Height - Margins.Top - Margins.Bottom; int linesPerPage = (int)(height / heightLine); // Serious problem: Abandon ship. if (linesPerPage < 1) return; int numPages = (listLines.Count + linesPerPage - 1) / linesPerPage; double xStart = Margins.Left; double yStart = Margins.Top; // Create the List to store each DocumentPage object. listPages = new List<DocumentPage>(); for (int iPage = 0, iLine = 0; iPage < numPages; iPage++) { // Create the DrawingVisual and open the DrawingContext. DrawingVisual vis = new DrawingVisual(); DrawingContext dc = vis.RenderOpen(); // Display header at top of page. if (Header != null && Header. Length > 0) { FormattedText formtxt = GetFormattedText(Header); formtxt.SetFontWeight (FontWeights.Bold); Point ptText = new Point (xStart, yStart - 2 * formtxt.Height); dc.DrawText(formtxt, ptText); } // Display footer at bottom of page. if (numPages > 1) { FormattedText formtxt = GetFormattedText("Page " + (iPage+1) + " of " + numPages); formtxt.SetFontWeight (FontWeights.Bold); Point ptText = new Point( (PageSize.Width + Margins .Left - Margins.Right - formtxt.Width) / 2, PageSize.Height - Margins .Bottom + formtxt.Height); dc.DrawText(formtxt, ptText); } // Look through the lines on the page. for (int i = 0; i < linesPerPage; i++, iLine++) { if (iLine == listLines.Count) break; // Set up information to display the text of the line. string str = listLines[iLine] .String; FormattedText formtxt = GetFormattedText(str); Point ptText = new Point (xStart, yStart + i * heightLine); dc.DrawText(formtxt, ptText); // Possibly display the little arrow flag. if (listLines[iLine].Flag) { double x = xStart + width + 6; double y = yStart + i * heightLine + formtxt.Baseline; double len = face .CapsHeight * em; dc.DrawLine(pn, new Point (x, y), new Point (x + len, y - len)); dc.DrawLine(pn, new Point (x, y), new Point (x, y - len / 2)); dc.DrawLine(pn, new Point (x, y), new Point (x + len / 2, y)); } } dc.Close(); // Create DocumentPage object based on visual. DocumentPage page = new DocumentPage(vis); listPages.Add(page); } reader.Close(); } // Process each line of text into multiple printed lines. void ProcessLine(string str, double width, List<PrintLine> list) { str = str.TrimEnd(' '); // TextWrapping == TextWrapping.NoWrap. // ------------------------------------ if (TextWrapping == TextWrapping.NoWrap) { do { int length = str.Length; while (GetFormattedText(str .Substring(0, length)).Width > width) length--; list.Add(new PrintLine(str .Substring(0, length), length < str.Length)); str = str.Substring(length); } while (str.Length > 0); } // TextWrapping == TextWrapping.Wrap or TextWrapping.WrapWithOverflow. // ------------------------------------ ------------------------------- else { do { int length = str.Length; bool flag = false; while (GetFormattedText(str .Substring(0, length)).Width > width) { int index = str .LastIndexOfAny(charsBreak, length - 2); if (index != -1) length = index + 1; // Include trailing space or dash. else { // At this point, we know that the next possible // space or dash break is beyond the allowable // width. Check if there's *any* space or dash break. index = str.IndexOfAny (charsBreak); if (index != -1) length = index + 1; // If TextWrapping .WrapWithOverflow, just display the // line. If TextWrapping.Wrap, break it with a flag. if (TextWrapping == TextWrapping.Wrap) { while (GetFormattedText(str.Substring(0, length)). Width > width) length--; flag = true; } break; // out of while loop. } } list.Add(new PrintLine(str .Substring(0, length), flag)); str = str.Substring(length); } while (str.Length > 0); } } // Private method to create FormattedText object. FormattedText GetFormattedText(string str) { return new FormattedText(str, CultureInfo.CurrentCulture, FlowDirection .LeftToRight, face, em, Brushes.Black); } } }



The trickiest part of this class is obviously the ProcessLine method that breaks each line of text into multiple printable lines. This method must potentially break lines into pieces based on the presence of spaces or dashes in the text and the current word-wrap option.

In previous incarnations of editing controls similar to the TextBox, word wrapping was either on or off. Either the editing control wrapped long lines within the borders of the control, or it truncated long lines at the right margin.

In the WPF version of the TextBox, word wrapping is represented by the TextWrapping property, which is set to a member of the TextWrapping enumeration. The members are NoWrap, Wrap, and WrapWithOverflow. This new scheme is attempting to solve a little problem. What happens if the user wants wrapping but a particular word is too long to fit within the TextBox width? With the TextWrapping.Wrap option, as much of the word is displayed as possible, and then the rest of the word is displayed on the next line. With the TextWrapping.WrapWithOverflow option, the long word is not split. In effect, the word overflows into the margin. You can't see it on the screen, but it was my feeling that this last option should let long words spill over into the margin of the page.

Although the difference between Wrap and WrapWithOverflow obviously caused some minor coding headaches, I was more interested in doing something sensible with the NoWrap option. Because this paginator is also a part of XAML Cruncher, I wanted printed output of source code to more closely resemble pages that come out of Visual Studio. If a line is too long for one line, Visual Studio lets it spill over the next line, and draws a little arrow in the margin to indicate the continued line.

That is why PlainTextDocumentPaginator stores each printable line as an object of type PrintLine. The PrintLine class includes a Boolean Flag field that indicates if the little arrow should be drawn at the end of the line. These arrows occur for long lines when the TextWrapping option is NoWrap, but also when the option is Wrap and a long word must be split between two lines.

Interestingly enough, the Windows Notepad program uses the same wrapping logic when printing documents regardless of the current word-wrap setting. After I spent the time doing separate printing logic for the three word-wrap settings, I can only characterize Notepad's approach as "lazy."

Let's move on to the Edit menu, which is a combination of easy stuff and hard stuff. The easy stuff consists of all the clipboard-related commands, which should be familiar to you by now.

NotepadClone.Edit.cs

[View full width]

//-------------------------------------------------- // NotepadClone.Edit.cs (c) 2006 by Charles Petzold //-------------------------------------------------- using System; using System.Windows; using System.Windows.Controls; using System.Windows.Input; namespace Petzold.NotepadClone { public partial class NotepadClone { void AddEditMenu(Menu menu) { // Top-level Edit menu. MenuItem itemEdit = new MenuItem(); itemEdit.Header = "_Edit"; menu.Items.Add(itemEdit); // Undo menu item. MenuItem itemUndo = new MenuItem(); itemUndo.Header = "_Undo"; itemUndo.Command = ApplicationCommands.Undo; itemEdit.Items.Add(itemUndo); CommandBindings.Add(new CommandBinding( ApplicationCommands.Undo, UndoOnExecute, UndoCanExecute)); // Redo menu item. MenuItem itemRedo = new MenuItem(); itemRedo.Header = "_Redo"; itemRedo.Command = ApplicationCommands.Redo; itemEdit.Items.Add(itemRedo); CommandBindings.Add(new CommandBinding( ApplicationCommands.Redo, RedoOnExecute, RedoCanExecute)); itemEdit.Items.Add(new Separator()); // Cut, Copy, Paste, and Delete menu items. MenuItem itemCut = new MenuItem(); itemCut.Header = "Cu_t"; itemCut.Command = ApplicationCommands .Cut; itemEdit.Items.Add(itemCut); CommandBindings.Add(new CommandBinding( ApplicationCommands.Cut, CutOnExecute, CutCanExecute)); MenuItem itemCopy = new MenuItem(); itemCopy.Header = "_Copy"; itemCopy.Command = ApplicationCommands.Copy; itemEdit.Items.Add(itemCopy); CommandBindings.Add(new CommandBinding( ApplicationCommands.Copy, CopyOnExecute, CutCanExecute)); MenuItem itemPaste = new MenuItem(); itemPaste.Header = "_Paste"; itemPaste.Command = ApplicationCommands.Paste; itemEdit.Items.Add(itemPaste); CommandBindings.Add(new CommandBinding( ApplicationCommands.Paste, PasteOnExecute, PasteCanExecute)); MenuItem itemDel = new MenuItem(); itemDel.Header = "De_lete"; itemDel.Command = ApplicationCommands .Delete; itemEdit.Items.Add(itemDel); CommandBindings.Add(new CommandBinding( ApplicationCommands.Delete, DeleteOnExecute, CutCanExecute)); itemEdit.Items.Add(new Separator()); // Separate method adds Find, FindNext, and Replace. AddFindMenuItems(itemEdit); itemEdit.Items.Add(new Separator()); // Select All menu item. MenuItem itemAll = new MenuItem(); itemAll.Header = "Select _All"; itemAll.Command = ApplicationCommands .SelectAll; itemEdit.Items.Add(itemAll); CommandBindings.Add(new CommandBinding( ApplicationCommands.SelectAll, SelectAllOnExecute)); // The Time/Date item requires a custom RoutedUICommand. InputGestureCollection coll = new InputGestureCollection(); coll.Add(new KeyGesture(Key.F5)); RoutedUICommand commTimeDate = new RoutedUICommand("Time/_Date", "TimeDate", GetType(), coll); MenuItem itemDate = new MenuItem(); itemDate.Command = commTimeDate; itemEdit.Items.Add(itemDate); CommandBindings.Add( new CommandBinding(commTimeDate, TimeDateOnExecute)); } // Redo event handlers. void RedoCanExecute(object sender, CanExecuteRoutedEventArgs args) { args.CanExecute = txtbox.CanRedo; } void RedoOnExecute(object sender, ExecutedRoutedEventArgs args) { txtbox.Redo(); } // Undo event handlers. void UndoCanExecute(object sender, CanExecuteRoutedEventArgs args) { args.CanExecute = txtbox.CanUndo; } void UndoOnExecute(object sender, ExecutedRoutedEventArgs args) { txtbox.Undo(); } // Cut event handlers. void CutCanExecute(object sender, CanExecuteRoutedEventArgs args) { args.CanExecute = txtbox.SelectedText .Length > 0; } void CutOnExecute(object sender, ExecutedRoutedEventArgs args) { txtbox.Cut(); } // Copy and Delete event handlers. void CopyOnExecute(object sender, ExecutedRoutedEventArgs args) { txtbox.Copy(); } void DeleteOnExecute(object sender, ExecutedRoutedEventArgs args) { txtbox.SelectedText = ""; } // Paste event handlers. void PasteCanExecute(object sender, CanExecuteRoutedEventArgs args) { args.CanExecute = Clipboard .ContainsText(); } void PasteOnExecute(object sender, ExecutedRoutedEventArgs args) { txtbox.Paste(); } // SelectAll event handler. void SelectAllOnExecute(object sender, ExecutedRoutedEventArgs args) { txtbox.SelectAll(); } // Time/Date event handler. void TimeDateOnExecute(object sender, ExecutedRoutedEventArgs args) { txtbox.SelectedText = DateTime.Now .ToString(); } } }



By comparison, the hard part of the Edit menu involves the commands Find, Find Next, and Replace. These commands involve modeless dialog boxes. The Find and Replace dialog boxes are so similar that I first wrote an abstract class named FindReplaceDialog that contained all the controls common to both the Find and Replace dialogs. The class isn't very complex, and the code is mostly devoted to laying out the controls in a Grid on the surface of the dialog. Because this is a modeless dialog box, it looks a little different from other classes that derive from Window. The dialog box contains a Cancel button, but that's not necessarily required in a modeless dialog box because the user can terminate the dialog by closing the window. Consequently, all the Cancel button does is call Close.

What modeless dialog boxes almost always require are public events. The three events that FindReplaceDialog defines (at the very top of the class) are called FindNext, Replace, and ReplaceAll, and they correspond to the three other buttons in the dialog. These events provide a way for the modeless dialog to notify its owner window that the user has pressed one of these buttons and is expecting something to happen.

FindReplaceDialog.cs

[View full width]

//-------------------------------------------------- // FindReplaceDialog.cs (c) 2006 by Charles Petzold ---------------------------------------------------- using System; using System.Windows; using System.Windows.Controls; namespace Petzold.NotepadClone { abstract class FindReplaceDialog : Window { // Public events. public event EventHandler FindNext; public event EventHandler Replace; public event EventHandler ReplaceAll; // Protected fields. protected Label lblReplace; protected TextBox txtboxFind, txtboxReplace; protected CheckBox checkMatch; protected GroupBox groupDirection; protected RadioButton radioDown, radioUp; protected Button btnFind, btnReplace, btnAll; // Public properties. public string FindWhat { set { txtboxFind.Text = value; } get { return txtboxFind.Text; } } public string ReplaceWith { set { txtboxReplace.Text = value; } get { return txtboxReplace.Text; } } public bool MatchCase { set { checkMatch.IsChecked = value; } get { return (bool)checkMatch.IsChecked; } } public Direction Direction { set { if (value == Direction.Down) radioDown.IsChecked = true; else radioUp.IsChecked = true; } get { return (bool)radioDown.IsChecked ? Direction.Down : Direction.Up; } } // Protected constructor (because class is abstract). protected FindReplaceDialog(Window owner) { // Set common dialog box properties. ShowInTaskbar = false; WindowStyle = WindowStyle.ToolWindow; SizeToContent = SizeToContent .WidthAndHeight; WindowStartupLocation = WindowStartupLocation.CenterOwner; Owner = owner; // Create Grid with three auto-sized rows and columns. Grid grid = new Grid(); Content = grid; for (int i = 0; i < 3; i++) { RowDefinition rowdef = new RowDefinition(); rowdef.Height = GridLength.Auto; grid.RowDefinitions.Add(rowdef); ColumnDefinition coldef = new ColumnDefinition(); coldef.Width = GridLength.Auto; grid.ColumnDefinitions.Add(coldef); } // Find what: Label and TextBox. Label lbl = new Label(); lbl.Content = "Fi_nd what:"; lbl.VerticalAlignment = VerticalAlignment.Center; lbl.Margin = new Thickness(12); grid.Children.Add(lbl); Grid.SetRow(lbl, 0); Grid.SetColumn(lbl, 0); txtboxFind = new TextBox(); txtboxFind.Margin = new Thickness(12); txtboxFind.TextChanged += FindTextBoxOnTextChanged; grid.Children.Add(txtboxFind); Grid.SetRow(txtboxFind, 0); Grid.SetColumn(txtboxFind, 1); // Replace with: Label and TextBox. lblReplace = new Label(); lblReplace.Content = "Re_place with:"; lblReplace.VerticalAlignment = VerticalAlignment.Center; lblReplace.Margin = new Thickness(12); grid.Children.Add(lblReplace); Grid.SetRow(lblReplace, 1); Grid.SetColumn(lblReplace, 0); txtboxReplace = new TextBox(); txtboxReplace.Margin = new Thickness(12); grid.Children.Add(txtboxReplace); Grid.SetRow(txtboxReplace, 1); Grid.SetColumn(txtboxReplace, 1); // Match Case CheckBox. checkMatch = new CheckBox(); checkMatch.Content = "Match _case"; checkMatch.VerticalAlignment = VerticalAlignment.Center; checkMatch.Margin = new Thickness(12); grid.Children.Add(checkMatch); Grid.SetRow(checkMatch, 2); Grid.SetColumn(checkMatch, 0); // Direction GroupBox and two RadioButtons. groupDirection = new GroupBox(); groupDirection.Header = "Direction"; groupDirection.Margin = new Thickness(12); groupDirection.HorizontalAlignment = HorizontalAlignment.Left; grid.Children.Add(groupDirection); Grid.SetRow(groupDirection, 2); Grid.SetColumn(groupDirection, 1); StackPanel stack = new StackPanel(); stack.Orientation = Orientation .Horizontal; groupDirection.Content = stack; radioUp = new RadioButton(); radioUp.Content = "_Up"; radioUp.Margin = new Thickness(6); stack.Children.Add(radioUp); radioDown = new RadioButton(); radioDown.Content = "_Down"; radioDown.Margin = new Thickness(6); stack.Children.Add(radioDown); // Create StackPanel for the buttons. stack = new StackPanel(); stack.Margin = new Thickness(6); grid.Children.Add(stack); Grid.SetRow(stack, 0); Grid.SetColumn(stack, 2); Grid.SetRowSpan(stack, 3); // Four buttons. btnFind = new Button(); btnFind.Content = "_Find Next"; btnFind.Margin = new Thickness(6); btnFind.IsDefault = true; btnFind.Click += FindNextOnClick; stack.Children.Add(btnFind); btnReplace = new Button(); btnReplace.Content = "_Replace"; btnReplace.Margin = new Thickness(6); btnReplace.Click += ReplaceOnClick; stack.Children.Add(btnReplace); btnAll = new Button(); btnAll.Content = "Replace _All"; btnAll.Margin = new Thickness(6); btnAll.Click += ReplaceAllOnClick; stack.Children.Add(btnAll); Button btn = new Button(); btn.Content = "Cancel"; btn.Margin = new Thickness(6); btn.IsCancel = true; btn.Click += CancelOnClick; stack.Children.Add(btn); txtboxFind.Focus(); } // Enable the first three buttons only if there's some text to find. void FindTextBoxOnTextChanged(object sender, TextChangedEventArgs args) { TextBox txtbox = args.Source as TextBox; btnFind.IsEnabled = btnReplace.IsEnabled = btnAll.IsEnabled = (txtbox.Text.Length > 0); } // The FindNextOnClick method calls the OnFindNext method, // which fires the FindNext event. void FindNextOnClick(object sender, RoutedEventArgs args) { OnFindNext(new EventArgs()); } protected virtual void OnFindNext (EventArgs args) { if (FindNext != null) FindNext(this, args); } // The ReplaceOnClick method calls the OnReplace method, // which fires the Replace event. void ReplaceOnClick(object sender, RoutedEventArgs args) { OnReplace(new EventArgs()); } protected virtual void OnReplace(EventArgs args) { if (Replace != null) Replace(this, args); } // The ReplaceAllOnClick method calls the OnReplaceAll method, // which fires the ReplaceAll event. void ReplaceAllOnClick(object sender, RoutedEventArgs args) { OnReplaceAll(new EventArgs()); } protected virtual void OnReplaceAll (EventArgs args) { if (ReplaceAll != null) ReplaceAll(this, args); } // The Cancel button just closes the dialog box. void CancelOnClick(object sender, RoutedEventArgs args) { Close(); } } }



The three events defined by FindReplaceDialog are standard, old-fashioned .NET events. Each event is associated with a protected virtual method whose name begins with the word On followed by the event name. These On methods are responsible for triggering the actual events. In this program, each On method is called by the Click event handler of the button associated with the event. These On methods aren't required, but they are available to override for any class that cares to inherit from FindReplaceDialog.

The FindDialog class derives from FindReplaceDialog and simply hides those controls that don't belong.

FindDialog.cs

[View full width]

//------------------------------------------- // FindDialog.cs (c) 2006 by Charles Petzold //------------------------------------------- using System; using System.Windows; using System.Windows.Controls; namespace Petzold.NotepadClone { class FindDialog : FindReplaceDialog { public FindDialog(Window owner): base(owner) { Title = "Find"; // Hide some controls. lblReplace.Visibility = Visibility .Collapsed; txtboxReplace.Visibility = Visibility .Collapsed; btnReplace.Visibility = Visibility .Collapsed; btnAll.Visibility = Visibility.Collapsed; } } }



The ReplaceDialog class could potentially do more than simply assign its own Title property, but that's all it does here:.

ReplaceDialog.cs

[View full width]

//---------------------------------------------- // ReplaceDialog.cs (c) 2006 by Charles Petzold //---------------------------------------------- using System; using System.Windows; using System.Windows.Controls; namespace Petzold.NotepadClone { class ReplaceDialog : FindReplaceDialog { public ReplaceDialog(Window owner): base (owner) { Title = "Replace"; } } }



The FindReplaceDialog class also makes use of this enumeration for searching down or up through the text:

Direction.cs

namespace Petzold.NotepadClone {     enum Direction     {         Down,         Up     } } 



The NotepadClone.Find.cs file is responsible for creating the three find and replace MenuItem objects for the Edit menu. The Click event handlers for Find and Replace create objects of type FindDialog and ReplaceDialog respectively, and install event handlers for one or more of the custom events defined by the FindReplaceDialog class. This is how the program is notified when the user clicks one of the buttons on the dialog.

NotepadClone.Find.cs

[View full width]

//-------------------------------------------------- // NotepadClone.Find.cs (c) 2006 by Charles Petzold //-------------------------------------------------- using System; using System.Windows; using System.Windows.Controls; using System.Windows.Input; namespace Petzold.NotepadClone { public partial class NotepadClone { string strFindWhat = "", strReplaceWith = ""; StringComparison strcomp = StringComparison.OrdinalIgnoreCase; Direction dirFind = Direction.Down; void AddFindMenuItems(MenuItem itemEdit) { // Find menu item MenuItem itemFind = new MenuItem(); itemFind.Header = "_Find..."; itemFind.Command = ApplicationCommands .Find; itemEdit.Items.Add(itemFind); CommandBindings.Add(new CommandBinding( ApplicationCommands.Find, FindOnExecute, FindCanExecute)); // The Find Next item requires a custom RoutedUICommand. InputGestureCollection coll = new InputGestureCollection(); coll.Add(new KeyGesture(Key.F3)); RoutedUICommand commFindNext = new RoutedUICommand("Find _Next", "FindNext", GetType(), coll); MenuItem itemNext = new MenuItem(); itemNext.Command = commFindNext; itemEdit.Items.Add(itemNext); CommandBindings.Add( new CommandBinding(commFindNext, FindNextOnExecute, FindNextCanExecute)); MenuItem itemReplace = new MenuItem(); itemReplace.Header = "_Replace..."; itemReplace.Command = ApplicationCommands.Replace; itemEdit.Items.Add(itemReplace); CommandBindings.Add(new CommandBinding( ApplicationCommands.Replace, ReplaceOnExecute, FindCanExecute)); } // CanExecute method for Find and Replace. void FindCanExecute(object sender, CanExecuteRoutedEventArgs args) { args.CanExecute = (txtbox.Text.Length > 0 && OwnedWindows.Count == 0); } void FindNextCanExecute(object sender, CanExecuteRoutedEventArgs args) { args.CanExecute = (txtbox.Text.Length > 0 && strFindWhat.Length > 0); } // Event handler for Find menu item. void FindOnExecute(object sender, ExecutedRoutedEventArgs args) { // Create dialog box. FindDialog dlg = new FindDialog(this); // Initialize properties. dlg.FindWhat = strFindWhat; dlg.MatchCase = strcomp == StringComparison.Ordinal; dlg.Direction = dirFind; // Install event handler and show dialog. dlg.FindNext += FindDialogOnFindNext; dlg.Show(); } // Event handler for Find Next menu item. // F3 key invokes dialog box if there's no string to find yet. void FindNextOnExecute(object sender, ExecutedRoutedEventArgs args) { if (strFindWhat == null || strFindWhat .Length == 0) FindOnExecute(sender, args); else FindNext(); } // Event handler for Replace menu item. void ReplaceOnExecute(object sender, ExecutedRoutedEventArgs args) { ReplaceDialog dlg = new ReplaceDialog (this); dlg.FindWhat = strFindWhat; dlg.ReplaceWith = strReplaceWith; dlg.MatchCase = strcomp == StringComparison.Ordinal; dlg.Direction = dirFind; // Install event handlers. dlg.FindNext += FindDialogOnFindNext; dlg.Replace += ReplaceDialogOnReplace; dlg.ReplaceAll += ReplaceDialogOnReplaceAll; dlg.Show(); } // Event handler installed for Find /Replace dialog box "Find Next" button. void FindDialogOnFindNext(object sender, EventArgs args) { FindReplaceDialog dlg = sender as FindReplaceDialog; // Get properties from dialog box. strFindWhat = dlg.FindWhat; strcomp = dlg.MatchCase ? StringComparison.Ordinal : StringComparison .OrdinalIgnoreCase; dirFind = dlg.Direction; // Call FindNext to do the actual find. FindNext(); } // Event handler installed for Replace dialog box "Replace" button. void ReplaceDialogOnReplace(object sender, EventArgs args) { ReplaceDialog dlg = sender as ReplaceDialog; // Get properties from dialog box. strFindWhat = dlg.FindWhat; strReplaceWith = dlg.ReplaceWith; strcomp = dlg.MatchCase ? StringComparison.Ordinal : StringComparison .OrdinalIgnoreCase; if (strFindWhat.Equals(txtbox .SelectedText, strcomp)) txtbox.SelectedText = strReplaceWith; FindNext(); } // Event handler installed for Replace dialog box "Replace All" button. void ReplaceDialogOnReplaceAll(object sender, EventArgs args) { ReplaceDialog dlg = sender as ReplaceDialog; string str = txtbox.Text; strFindWhat = dlg.FindWhat; strReplaceWith = dlg.ReplaceWith; strcomp = dlg.MatchCase ? StringComparison.Ordinal : StringComparison .OrdinalIgnoreCase; int index = 0; while (index + strFindWhat.Length < str.Length) { index = str.IndexOf(strFindWhat, index, strcomp); if (index != -1) { str = str.Remove(index, strFindWhat.Length); str = str.Insert(index, strReplaceWith); index += strReplaceWith.Length; } else break; } txtbox.Text = str; } // General FindNext method. void FindNext() { int indexStart, indexFind; // The starting position of the search and the direction of the search // are determined by the dirFind variable. if (dirFind == Direction.Down) { indexStart = txtbox.SelectionStart + txtbox.SelectionLength; indexFind = txtbox.Text.IndexOf (strFindWhat, indexStart, strcomp); } else { indexStart = txtbox.SelectionStart; indexFind = txtbox.Text .LastIndexOf(strFindWhat, indexStart, strcomp); } // If IndexOf (or LastIndexOf) does not return -1, select the found text. // Otherwise, display a message box. if (indexFind != -1) { txtbox.Select(indexFind, strFindWhat.Length); txtbox.Focus(); } else MessageBox.Show("Cannot find \"" + strFindWhat + "\"", Title, MessageBoxButton .OK, MessageBoxImage.Information); } } }



The Format menu contains two menu items: Word Wrap and Font. In the standard Windows Notepad, the Word Wrap option is checked or unchecked. But in my Notepad Clone, I wanted to expose the three options (TextWrapping.NoWrap, TextWrapping.Wrap, and TextWrapping.WrapWithOverflow), if only to test the printing logic in the PlainTextDocumentPaginator class.

The following class derives from MenuItem and is named WordWrapMenuItem. This is the item that will appear in the Format menu. The Header property is the text "Word Wrap" and Items collection contains three items in a submenu that correspond to the three members of the TextWrapping enumeration. Notice that each of these three items has its Tag property set to one of the TextWrapping members.

WordWrapMenuItem.cs

[View full width]

//------------------------------------------------- // WordWrapMenuItem.cs (c) 2006 by Charles Petzold //------------------------------------------------- using System; using System.Windows; using System.Windows.Controls; using System.Windows.Controls.Primitives; namespace Petzold.NotepadClone { public class WordWrapMenuItem : MenuItem { // Register WordWrap dependency property. public static DependencyProperty WordWrapProperty = DependencyProperty.Register ("WordWrap", typeof(TextWrapping), typeof (WordWrapMenuItem)); // Define WordWrap property. public TextWrapping WordWrap { set { SetValue(WordWrapProperty, value); } get { return (TextWrapping)GetValue (WordWrapProperty); } } // Constructor creates Word Wrap menu item. public WordWrapMenuItem() { Header = "_Word Wrap"; MenuItem item = new MenuItem(); item.Header = "_No Wrap"; item.Tag = TextWrapping.NoWrap; item.Click += MenuItemOnClick; Items.Add(item); item = new MenuItem(); item.Header = "_Wrap"; item.Tag = TextWrapping.Wrap; item.Click += MenuItemOnClick; Items.Add(item); item = new MenuItem(); item.Header = "Wrap with _Overflow"; item.Tag = TextWrapping.WrapWithOverflow; item.Click += MenuItemOnClick; Items.Add(item); } // Set checked item from current WordWrap property. protected override void OnSubmenuOpened (RoutedEventArgs args) { base.OnSubmenuOpened(args); foreach (MenuItem item in Items) item.IsChecked = ( (TextWrapping)item.Tag == WordWrap); } // Set WordWrap property from clicked item. void MenuItemOnClick(object sender, RoutedEventArgs args) { WordWrap = (TextWrapping)(args.Source as MenuItem).Tag; } } }



This class defines a dependency property named WordWrapProperty as the basis for a public WordWrap property. The class references this WordWrap property in its two event handlers. The OnSubmenuOpened method applies a checkmark to the item whose Tag property equals the current value of the WordWrap property. The MenuItemOnClick method sets the WordWrap property to the Tag property of the clicked item.

Notice that the WordWrapMenuItem class contains no reference to the TextBox. Apart from the public constructor, the public WordWrap property, and the public WordWrapProperty field, the class appears to be rather self-contained. How does it interact with the TextWrapping property of the TextBox?

The answer is data binding, as the next installment of the NotepadClone class demonstrates.

NotepadClone.Format.cs

[View full width]

//---------------------------------------------------- // NotepadClone.Format.cs (c) 2006 by Charles Petzold //---------------------------------------------------- using Petzold.ChooseFont; using System; using System.Windows; using System.Windows.Controls; using System.Windows.Data; using System.Windows.Media; namespace Petzold.NotepadClone { public partial class NotepadClone { void AddFormatMenu(Menu menu) { // Create top-level Format item. MenuItem itemFormat = new MenuItem(); itemFormat.Header = "F_ormat"; menu.Items.Add(itemFormat); // Create Word Wrap menu item. WordWrapMenuItem itemWrap = new WordWrapMenuItem(); itemFormat.Items.Add(itemWrap); // Bind item to TextWrapping property of TextBox. Binding bind = new Binding(); bind.Path = new PropertyPath(TextBox .TextWrappingProperty); bind.Source = txtbox; bind.Mode = BindingMode.TwoWay; itemWrap.SetBinding(WordWrapMenuItem .WordWrapProperty, bind); // Create Font menu item. MenuItem itemFont = new MenuItem(); itemFont.Header = "_Font..."; itemFont.Click += FontOnClick; itemFormat.Items.Add(itemFont); } // Font item event handler. void FontOnClick(object sender, RoutedEventArgs args) { FontDialog dlg = new FontDialog(); dlg.Owner = this; // Set TextBox properties in FontDialog. dlg.Typeface = new Typeface(txtbox .FontFamily, txtbox.FontStyle, txtbox .FontWeight, txtbox.FontStretch); dlg.FaceSize = txtbox.FontSize; if (dlg.ShowDialog().GetValueOrDefault()) { // Set FontDialog properties in TextBox. txtbox.FontFamily = dlg.Typeface .FontFamily; txtbox.FontSize = dlg.FaceSize; txtbox.FontStyle = dlg.Typeface.Style; txtbox.FontWeight = dlg.Typeface .Weight; txtbox.FontStretch = dlg.Typeface .Stretch; } } } }



After creating a WordWrapMenuItem object and adding it to the Format item, the program creates a Binding object to bind the TextWrapping property of the TextBox with the WordWrap property of WordWrapMenuItem:

Binding bind = new Binding(); bind.Path = new PropertyPath(TextBox.TextWrappingProperty); bind.Source = txtbox; bind.Mode = BindingMode.TwoWay; itemWrap.SetBinding(WordWrapMenuItem.WordWrapProperty, bind); 


This is not the only way to define this data binding. In the code shown, the TextBox is considered the source of the data and the WordWrapMenuItem is the target of the data. The binding mode is set to TwoWay so that changes in the target are also reflected in the source (which is the normal way that the data changes are reflected). But it's easy to switch around the source and target:

Binding bind = new Binding(); bind.Path = new PropertyPath(WordWrapMenuItem.WordWrapProperty); bind.Source = itemWrap; bind.Mode = BindingMode.TwoWay; txtbox.SetBinding(TextBox.TextWrappingProperty, bind); 


Notice that the last statement now calls the SetBinding method of the TextBox rather than the WordWrapMenuItem.

The easy part of the Format menu is the Font item. (Well, it's easy if you have the FontDialog class from the previous chapter available!) The Click event handler creates a FontDialog object, initializes the Typeface and FaceSize properties from the TextBox, and then updates the TextBox properties if the user clicks OK.

The View menu is fairly trivial. It contains a single Status Bar item that displays or hides the program's status bar. All it needs to do is toggle the Visibility property of the StatusBar between Visibility.Visible and Visibility.Collapsed.

NotepadClone.View.cs

[View full width]

//-------------------------------------------------- // NotepadClone.View.cs (c) 2006 by Charles Petzold //-------------------------------------------------- using System; using System.Windows; using System.Windows.Controls; namespace Petzold.NotepadClone { public partial class NotepadClone { MenuItem itemStatus; void AddViewMenu(Menu menu) { // Create top-level View item. MenuItem itemView = new MenuItem(); itemView.Header = "_View"; itemView.SubmenuOpened += ViewOnOpen; menu.Items.Add(itemView); // Create Status Bar item on View menu. itemStatus = new MenuItem(); itemStatus.Header = "_Status Bar"; itemStatus.IsCheckable = true; itemStatus.Checked += StatusOnCheck; itemStatus.Unchecked += StatusOnCheck; itemView.Items.Add(itemStatus); } void ViewOnOpen(object sender, RoutedEventArgs args) { itemStatus.IsChecked = (status .Visibility == Visibility.Visible); } void StatusOnCheck(object sender, RoutedEventArgs args) { MenuItem item = sender as MenuItem; status.Visibility = item.IsChecked ? Visibility .Visible : Visibility.Collapsed; } } }



We're in the home stretch now. The final item on the top-level menu is Help, but I'm not going to implement the Help Topics item. I'll show you how to create a Help file in Chapter 25. This Help menu displays a single menu item with the text "About Notepad Clone..." and invokes the AboutDialog class when the item is clicked.

NotepadClone.Help.cs

[View full width]

//-------------------------------------------------- // NotepadClone.Help.cs (c) 2006 by Charles Petzold //-------------------------------------------------- using System; using System.Reflection; using System.Windows; using System.Windows.Controls; namespace Petzold.NotepadClone { public partial class NotepadClone { void AddHelpMenu(Menu menu) { MenuItem itemHelp = new MenuItem(); itemHelp.Header = "_Help"; itemHelp.SubmenuOpened += ViewOnOpen; menu.Items.Add(itemHelp); MenuItem itemAbout = new MenuItem(); itemAbout.Header = "_About " + strAppTitle + "..."; itemAbout.Click += AboutOnClick; itemHelp.Items.Add(itemAbout); } void AboutOnClick(object sender, RoutedEventArgs args) { AboutDialog dlg = new AboutDialog(this); dlg.ShowDialog(); } } }



The constructor in the following AboutDialog class begins by accessing the assembly to fish out several attributes that it uses for constructing TextBlock objects. The only part of this file that's hard-coded is the URL of my Web site, which is displayed by a Hyperlink text element, and which is passed to the static Process.Start method to launch your Web browser.

AboutDialog.cs

[View full width]

//-------------------------------------------- // AboutDialog.cs (c) 2006 by Charles Petzold //-------------------------------------------- using System; using System.Diagnostics; // for Process class using System.Reflection; using System.Windows; using System.Windows.Controls; using System.Windows.Documents; using System.Windows.Media; namespace Petzold.NotepadClone { class AboutDialog : Window { public AboutDialog(Window owner) { // Get attributes from assembly. // Get this executing assembly to access attributes. Assembly asmbly = Assembly .GetExecutingAssembly(); // Get the AssemblyTitle attribute for the program name. AssemblyTitleAttribute title = (AssemblyTitleAttribute)asmbly .GetCustomAttributes( typeof(AssemblyTitleAttribute) , false)[0]; string strTitle = title.Title; // Get the AssemblyFileVersion attribute. AssemblyFileVersionAttribute version = (AssemblyFileVersionAttribute)asmbly .GetCustomAttributes( typeof (AssemblyFileVersionAttribute), false)[0]; string strVersion = version.Version .Substring(0, 3); // Get the AssemblyCopyright attribute. AssemblyCopyrightAttribute copy = (AssemblyCopyrightAttribute)asmbly .GetCustomAttributes( typeof (AssemblyCopyrightAttribute), false)[0]; string strCopyright = copy.Copyright; // Standard window properties for dialog boxes. Title = "About " + strTitle; ShowInTaskbar = false; SizeToContent = SizeToContent .WidthAndHeight; ResizeMode = ResizeMode.NoResize; Left = owner.Left + 96; Top = owner.Top + 96; // Create StackPanel as content of window. StackPanel stackMain = new StackPanel(); Content = stackMain; // Create TextBlock for program name. TextBlock txtblk = new TextBlock(); txtblk.Text = strTitle + " Version " + strVersion; txtblk.FontFamily = new FontFamily ("Times New Roman"); txtblk.FontSize = 32; // 24 points txtblk.FontStyle = FontStyles.Italic; txtblk.Margin = new Thickness(24); txtblk.HorizontalAlignment = HorizontalAlignment.Center; stackMain.Children.Add(txtblk); // Create TextBlock for copyright. txtblk = new TextBlock(); txtblk.Text = strCopyright; txtblk.FontSize = 20; // 15 points. txtblk.HorizontalAlignment = HorizontalAlignment.Center; stackMain.Children.Add(txtblk); // Create TextBlock for Web site link. Run run = new Run("www.charlespetzold .com"); Hyperlink link = new Hyperlink(run); link.Click += LinkOnClick; txtblk = new TextBlock(link); txtblk.FontSize = 20; txtblk.HorizontalAlignment = HorizontalAlignment.Center; stackMain.Children.Add(txtblk); // Create OK button. Button btn = new Button(); btn.Content = "OK"; btn.IsDefault = true; btn.IsCancel = true; btn.HorizontalAlignment = HorizontalAlignment.Center; btn.Margin = new Thickness(24); btn.Click += OkOnClick; stackMain.Children.Add(btn); btn.Focus(); } // Event handlers. void LinkOnClick(object sender, RoutedEventArgs args) { Process.Start("http://www .charlespetzold.com"); } void OkOnClick(object sender, RoutedEventArgs args) { DialogResult = true; } } }



The OK button has both its IsDefault and IsCancel properties set to true to allow the user to dismiss the dialog with either the Enter key or the Escape key.

With this file, the Notepad Clone project is complete. As I've already promised, at the beginning of Chapter 20 I'm going to resurrect this project, add a couple of files to it, and turn it into a programming tool named XAML Cruncher. The purpose of the next chapter is to introduce you to XAML and convince you that XAML Cruncher is a valuable tool to have.




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