Editing A Text Document: The Capseditor Sample


Editing A Text Document: The Capseditor Sample

You now come to the extended example in this chapter. The CapsEditor example is designed to demonstrate how the principles of drawing that you've learned so far have to be applied in a more realistic context. The CapsEditor example does not require any new material, apart from responding to user input via the mouse, but it shows how to manage the drawing of text so that the application maintains performance while ensuring that the contents of the client area of the main window are always kept up to date.

The CapsEditor program is functionally quite simple. It allows the user to read in a text file, which is then displayed line by line in the client area. If the user double-clicks any line, that line will be changed to all uppercase. That's literally all the example does. Even with this limited set of features, you'll find that the work involved in making sure everything is displayed in the right place while considering performance issues is quite complex. In particular, you have a new element here: the contents of the document can change — either when the user selects the menu option to read a new file, or when she double-clicks to capitalize a line. In the first case you need to update the document size so the scroll bars still work correctly, and you have to redisplay everything. In the second case, you need to check carefully whether the document size has changed, and what text needs to be redisplayed.

This section starts by reviewing the appearance of CapsEditor. When the application is first run, it has no document loaded and resembles Figure 25-17.

image from book
Figure 25-17

The File menu has two options: Open, which evokes OpenFileDialog when selected and reads in whatever file the user clicks, and Exit, which closes the application when clicked. Figure 25-18 shows CapsEditor displaying its own source file, Form1.cs. (A couple of lines have been double-clicked to convert them to uppercase.)

image from book
Figure 25-18

The sizes of the horizontal and vertical scroll bars are correct. The client area will scroll just enough to view the entire document. CapsEditor doesn't try to wrap lines of text — the example is already complicated enough as is. It just displays each line of the file exactly as it is read in. There are no limits to the size of the file, but you are assuming it is a text file and doesn't contain any non-printable characters.

Begin by adding a using command:

using System; using System.Collections.Generic; using System.ComponentModel; using System.Data; using System.Drawing; using System.Text; using System.Windows.Forms; using System.IO; 

This is because you'll be using the StreamReader class, which is in the System.IO namespace. Next, you add some fields to the Form1 class:

#region Constant fields private const string standardTitle = "CapsEditor"; // default text in titlebar private const uint margin = 10; // horizontal and vertical margin in client area #endregion #region Member fields private ArrayList documentLines = new ArrayList();   // the 'document' private uint lineHeight;        // height in pixels of one line private Size documentSize;      // how big a client area is needed to  // display document private uint nLines;            // number of lines in document private Font mainFont;          // font used to display all lines private Font emptyDocumentFont; // font used to display empty message private Brush mainBrush = Brushes.Blue;  // brush used to display document text private Brush emptyDocumentBrush = Brushes.Red; // brush used to display empty document message private Point mouseDoubleClickPosition; // location mouse is pointing to when double-clicked private OpenFileDialog fileOpenDialog = new OpenFileDialog();  // standard open file dialog private bool documentHasData = false;  // set to true if document has some data in it #endregion 

Most of these fields should be self-explanatory. The documentLines field is an ArrayList that contains the actual text of the file that has been read in. In a real sense, this is the field that contains the data in the document. Each element of documentLines contains information for one line of text that has been read in. Because it's an ArrayList, rather than a plain array, you can dynamically add elements to it as you read in a file. Note also that you've used #region preprocessor directives to block bits of the program to make it easier to edit.

As previously mentioned, each documentLines element contains information about a line of text. This information is actually an instance of another class, TextLineInformation:

 class TextLineInformation { public string Text; public uint Width; } 

TextLineInformation looks like a classic case where you'd normally use a struct rather than a class because it's just there to group a couple of fields. However, its instances are always accessed as elements of an ArrayList, which expects its elements to be stored as reference types, so declaring TextLineInformation as a class makes things more efficient by saving a lot of boxing and unboxing operations.

Each TextLineInformation instance stores a line of text — and that can be thought of as the smallest item that is displayed as a single item. In general, for each similar item in a GDI+ application, you'd probably want to store the text of the item, as well as the world coordinates of where it should be displayed and its size (the page coordinates will change frequently, whenever the user scrolls, whereas world coordinates will normally only change when other parts of the document are modified in some way). In this case you've only stored the Width of the item. The reason is because the height in this case is just the height of whatever your selected font is. It's the same for all lines of text so there's no point storing it separately for each one; you store it once, in the Form1.lineHeight field. As for the position, well, in this case, the x coordinate is just equal to the margin, and the y coordinate is easily calculated as:

margin + lineHeight*(however many lines are above this one)

If you'd been trying to display and manipulate, say, individual words instead of complete lines, then the x position of each word would have to be calculated using the widths of all the previous words on that line of text, but the intent is to keep it simple here, which is why you're treating each line of text as one single item.

Let's turn to the main menu now. This part of the application is more the realm of Windows Forms (see Chapter 23) than of GDI+. Add the menu options using the design view in Visual Studio 2005, but rename them as menuFile, menuFileOpen, and menuFileExit. Next add event handlers for the File Open and File Exit menu options using the Visual Studio 2005 Properties window. The event handlers have their Visual Studio 2005–generated names of menuFileOpen_Click() andmenuFileExit_Click().

Add some extra initialization code in the Form1() constructor:

public Form1() {    InitializeComponent(); CreateFonts(); fileOpenDialog.FileOk += new  System.ComponentModel.CancelEventHandler( this.OpenFileDialog_FileOk); fileOpenDialog.Filter = "Text files (*.txt)|*.txt|C# source files (*.cs)|*.cs"; } 

The event handler added here is for when the user clicks OK in the File Open dialog box. You have also set the filter for the Open File dialog box, so that you can only load text files — the example opted for .txt files as well as C# source files, so you can use the application to examine the source code for the samples.

CreateFonts() is a helper method that sorts out the fonts you intend to use:

 private void CreateFonts() { mainFont = new Font("Arial", 10); lineHeight = (uint)mainFont.Height; emptyDocumentFont = new Font("Verdana", 13, FontStyle.Bold); } 

The actual definitions of the handlers are pretty standard stuff:

 protected void OpenFileDialog_FileOk(object Sender, CancelEventArgs e) { this.LoadFile(fileOpenDialog.FileName); } protected void menuFileOpen_Click(object sender, EventArgs e) { fileOpenDialog.ShowDialog(); } protected void menuFileExit_Click(object sender, EventArgs e) { this.Close(); } 

Next, take a look the LoadFile() method. It's the method that handles the opening and reading of a file (as well as ensuring a Paint event is raised to force a repaint with the new file):

 private void LoadFile(string FileName) { StreamReader sr = new StreamReader(FileName); string nextLine; documentLines.Clear(); nLines = 0; TextLineInformation nextLineInfo; while ( (nextLine = sr.ReadLine()) != null) { nextLineInfo = new TextLineInformation(); nextLineInfo.Text = nextLine; documentLines.Add(nextLineInfo); ++nLines; } sr.Close(); documentHasData = (nLines>0) ? true : false; CalculateLineWidths(); CalculateDocumentSize(); this.Text = standardTitle + " - " + FileName; this.Invalidate(); } 

Most of this function is just standard file-reading stuff (see Chapter 34, "Manipulating Files and the Registry"). Note that as the file is read, you progressively add lines to documentLines ArrayList, so this array ends up containing information for each of the lines in order. After you've read in the file, you set the documentHasData flag, which indicates whether there is actually anything to display. Your next task is to work out where everything is to be displayed, and, having done that, how much client area you need to display the file — the document size that will be used to set the scroll bars. Finally, you set the title bar text and call Invalidate(). Invalidate() is an important method supplied by Microsoft, so the next section discusses its use first, before examining the code for the CalculateLineWidths() and CalculateDocumentSize() methods.

The Invalidate() Method

Invalidate() is a member of System.Windows.Forms.Form. It marks an area of the client window as invalid and, therefore, in need of repainting, and then makes sure a Paint event is raised. Invalidate()has a couple of overrides: you can pass it a rectangle that specifies (in page coordinates) precisely which area of the window needs repainting, or if you don't pass any parameters it'll just mark the entire client area as invalid.

You might wonder why you would do it this way. If you know that something needs painting, why don't you just call OnPaint() or some other method to do the painting directly? The answer is that in general, calling painting routines directly is regarded as bad programming practice — if your code decides it wants some painting done, you should call Invalidate(). Here's why:

  • Drawing is almost always the most processor-intensive task a GDI+ application will carry out, so doing it in the middle of other work holds up the other work. With the example, if you'd directly called a method to do the drawing from the LoadFile() method, then the LoadFile() method wouldn't return until that drawing task was complete. During that time, your application can't respond to any other events. On the other hand, by calling Invalidate() youare simply getting Windows to raise a Paint event before immediately returning from LoadFile(). Windows is then free to examine the events that are in line to be handled. How this works internally is that the events sit as what are known as messages in a message queue. Windows periodically examines the queue, and if there are events in it, it picks one and callsthe corresponding event handler. Although the Paint event might be the only one sitting in the queue (so OnPaint() gets called immediately anyway), in a more complex application there might be other events that ought to get priority over your Paint event. In particular, if the user has decided to quit the application, this will be marked by a message known as WM_QUIT.

  • Related to the previous point, if you have a more complicated, multithreaded application, you'll probably want just one thread to handle all the drawing. Using Invalidate() to route all drawing through the message queue provides a good way of ensuring that the same thread(whatever thread is responsible for the message queue, this will be the thread that called Application.Run()) does all the drawing, no matter what other thread requested the drawing operation.

  • There's an additional performance-related reason. Suppose a couple of different requests to draw part of the screen come in at about the same time. Maybe your code has just modified the document and wants to ensure the updated document is displayed, while at the same time the user has just moved another window that was covering part of the client area out of the way. By calling Invalidate(), you are giving Windows a chance to notice that this has occurred. Windows can then merge the Paint events if appropriate, combining the invalidated areas, so that the painting is only done once.

  • Finally, the code to do the painting is probably going to be one of the most complex parts of the code in your application, especially if you have a very sophisticated user interface. The guys who have to maintain your code in a couple of years time will thank you for having kept your painting code all in one place and as simple as you reasonably can — something that's easier to do if you don't have too many pathways into it from other parts of the program.

The bottom line from all this is that it is good practice to keep all your painting in the OnPaint() routine, or in other methods called from that method. However, you have to strike a balance; if you want to replace just one character on the screen and you know perfectly well that it won't affect anything else that you've drawn, then you might decide that it's not worth the overhead of going through Invalidate() and just write a separate drawing routine.

Note

In a very complicated application, you might even write a full class that takes responsibility for drawing to the screen. A few years ago when MFC was the standard technology for GDI-intensive applications, MFC followed this model, with a C++ class, C<ApplicationName>View, that was responsible for painting. However, even in this case, this class had one member function, OnDraw(), which was designed to be the entry point for most drawing requests.

Calculating Item Sizes and Document Size

This section returns to the CapsEditor example and examines the CalculateLineWidths() and CalculateDocumentSize() methods called from LoadFile():

 private void CalculateLineWidths() { Graphics dc = this.CreateGraphics(); foreach (TextLineInformation nextLine in documentLines) { nextLine.Width = (uint)dc.MeasureString(nextLine.Text, mainFont).Width; } } 

This method simply runs through each line that has been read in and uses the Graphics.Measure String() method to work out and store how much horizontal screen space the string requires. You store the value because MeasureString() is computationally intensive. If the CapsEditor sample hadn't been simple enough to easily work out the height and location of each item, this method would almost certainly have needed to be implemented in such a way as to compute all those quantities too.

Now that you know how big each item on the screen is and you can calculate where each item goes, you are in a position to work out the actual document size. The height is basically the number of lines multiplied by the height of each line. The width will need to be worked out by iterating through the lines to find the longest. For both height and width, you will also want to make an allowance for a small margin around the displayed document to make the application look more attractive.

Here's the method that calculates the document size:

 private void CalculateDocumentSize() { if (!documentHasData) { documentSize = new Size(100, 200); } else { documentSize.Height = (int)(nLines*lineHeight) + 2*(int)margin; uint maxLineLength = 0; foreach (TextLineInformation nextWord in documentLines) { uint tempLineLength = nextWord.Width + 2*margin; if (tempLineLength > maxLineLength) maxLineLength = tempLineLength; } documentSize.Width = (int)maxLineLength; } this.AutoScrollMinSize = documentSize; } 

This method first checks whether there is any data to be displayed. If there isn't, you cheat a bit and use a hard-coded document size, which is big enough to display the big red <Empty Document> warning. If you'd wanted to really do it properly, you'd have used MeasureString() to check how big that warning actually is.

Once you've worked out the document size, you tell the Form instance what the size is by setting the Form.AutoScrollMinSize property. When you do this, something interesting happens behind the scenes. In the process of setting this property, the client area is invalidated and a Paint event is raised, for the very sensible reason that changing the size of the document means scroll bars will need to be added or modified and the entire client area will almost certainly be repainted. Why is that interesting? If you look back at the code for LoadFile(), you'll realize that the call to Invalidate() in that method is actually redundant. The client area will be invalidated anyway when you set the document size. The explicit call to Invalidate() was left in the LoadFile() implementation to illustrate how in general you should normally do things. In fact in this case, all calling Invalidate() again will do is needlessly request a duplicate Paint event. However, this in turn illustrates how Invalidate() gives Windows the chance to optimize performance. The second Paint event won't in fact get raised: Windows will see that there's a Paint event already sitting in the queue and will compare the requested invalidated regions to see if it needs to do anything to merge them. In this case, both Paint events will specifythe entire client area, so nothing needs to be done, and Windows will quietly drop the second Paint request. Of course, going through that process will take up a little bit of processor time, but it'll be a negligible amount of time compared to how long it takes to actually do some painting.

OnPaint()

Now that you've seen how CapsEditor loads the file, it's time to look at how the painting is done:

 protected override void OnPaint(PaintEventArgs e) { base.OnPaint(e); Graphics dc = e.Graphics; int scrollPositionX = this.AutoScrollPosition.X; int scrollPositionY = this.AutoScrollPosition.Y; dc.TranslateTransform(scrollPositionX, scrollPositionY); if (!documentHasData) { dc.DrawString("<Empty document>", emptyDocumentFont, emptyDocumentBrush, new Point(20,20)); base.OnPaint(e); return; } // work out which lines are in clipping rectangle int minLineInClipRegion =  WorldYCoordinateToLineIndex(e.ClipRectangle.Top – scrollPositionY); if (minLineInClipRegion == -1) minLineInClipRegion = 0; int maxLineInClipRegion =  WorldYCoordinateToLineIndex(e.ClipRectangle.Bottom – scrollPositionY); if (maxLineInClipRegion >= this.documentLines.Count || maxLineInClipRegion == -1) maxLineInClipRegion = this.documentLines.Count-1; TextLineInformation nextLine; for (int i=minLineInClipRegion; i<=maxLineInClipRegion ; i++) { nextLine = (TextLineInformation)documentLines[i]; dc.DrawString(nextLine.Text, mainFont, mainBrush, this.LineIndexToWorldCoordinates(i)); } } 

At the heart of this OnPaint() override is a loop that goes through each line of the document, calling Graphics.DrawString() to paint each one. The rest of this code is mostly to do with optimizing the painting — the usual stuff about figuring out what exactly needs painting instead of rushing in and telling the graphics instance to redraw everything.

You begin by checking if there is any data in the document. If there isn't, you draw a quick message saying so, call the base class's OnPaint() implementation, and exit. If there is data, then you start looking at the clipping rectangle. The way you do this is by calling another method, WorldYCoordinateToLineIndex(). This method is examined next, but essentially it takes a given y position relative to the top of the document, and works out what line of the document is being displayed at that point.

The first time you call the WorldYCoordinateToLineIndex() method, you pass it the coordinate value(e.ClipRectangle.Top - scrollPositionY). This is just the top of the clipping region, converted to world coordinates. If the return value is –1, you play safe and assume you need to start at the beginning of the document (this is the case if the top of the clipping region is within the top margin).

Once you've done all that, you essentially repeat the same process for the bottom of the clipping rectangle, in order to find the last line of the document that is inside the clipping region. The indices of the first and last lines are respectively stored in minLineInClipRegion and maxLineInClipRegion, so then you can just run a for loop between these values to do your painting. Inside the painting loop, you actually need to do roughly the reverse transformation to the one performed by WorldYCoordinateToLineIndex(). You are given the index of a line of text, and you need to check where it should be drawn. This calculation is actually quite simple, but you've wrapped it up in another method, LineIndexToWorldCoordinates(), which returns the required coordinates of the top-left corner of the item. The returned coordinates are world coordinates, but that's fine, because you have already called TranslateTransform() on the Graphics object so that you need to pass it world, rather than page, coordinates when asking it todisplay items.

Coordinate Transforms

This section examines the implementation of the helper methods that are written in the CapsEditor sample to help you with coordinate transforms. These are the WorldYCoordinateToLineIndex() and LineIndexToWorldCoordinates() methods referred to in the previous section, as well as a couple of other methods.

First, LineIndexToWorldCoordinates() takes a given line index, and works out the world coordinates of the top-left corner of that line, using the known margin and line height:

 private Point LineIndexToWorldCoordinates(int index) { Point TopLeftCorner = new Point( (int)margin, (int)(lineHeight*index + margin)); return TopLeftCorner; } 

You also use a method that roughly does the reverse transform in OnPaint(). WorldYCoordinate ToLineIndex() works out the line index, but it only takes into account a vertical world coordinate. This is because it is used to work out the line index corresponding to the top and bottom of the clip region:

 private int WorldYCoordinateToLineIndex(int y) { if (y < margin) return -1; return (int)((y-margin)/lineHeight); } 

There are three more methods, which will be called from the handler routine that responds to the user double-clicking the mouse. First, you have a method that works out the index of the line being displayed at given world coordinates. Unlike WorldYCoordinateToLineIndex(), this method takes into account the x and y positions of the coordinates. It returns –1 if there is no line of text covering the coordinates passed in:

 private int WorldCoordinatesToLineIndex(Point position) { if (!documentHasData) return -1; if (position.Y < margin || position.X < margin) return -1; int index = (int)(position.Y-margin)/(int)this.lineHeight; // check position isn't below  if (index >= documentLines.Count) return -1; // now check that horizontal position is within this line TextLineInformation theLine =  (TextLineInformation)documentLines[index]; if (position.X > margin + theLine.Width) return -1; // all is OK. We can return answer return index; } 

Finally, on occasions you also need to convert between line index and page, rather than world, coordinates. The following methods achieve this:

 private Point LineIndexToPageCoordinates(int index) { return LineIndexToWorldCoordinates(index) +  new Size(AutoScrollPosition); } private int PageCoordinatesToLineIndex(Point position) { return WorldCoordinatesToLineIndex(position - new  Size(AutoScrollPosition)); } 

Note that when converting to page coordinates, you add the AutoScrollPosition, which is negative.

Although these methods by themselves don't look particularly interesting, they do illustrate a general technique that you'll probably need to use often. With GDI+, you'll often find yourself in a situation where you have been given specific coordinates (for example the coordinates of where the user has clicked the mouse) and you'll need to figure out what item is being displayed at that point. Or it could happen the other way around — given a particular display item, whereabouts should it be displayed? Hence, if you are writing a GDI+ application, you'll probably find it useful to write methods that do the equivalent of the coordinate transformation methods illustrated here.

Responding to User Input

So far, with the exception of the File menu in the CapsEditor sample, everything you've done in this chapter has been one way: the application has talked to the user by displaying information on the screen. Almost all software of course works both ways: the user can talk to the software as well. You're now going to add that facility to CapsEditor.

Getting a GDI+ application to respond to user input is actually a lot simpler than writing the code to draw to the screen (Chapter 23 covers how to handle user input). Essentially, you override methods from the Form class that get called from the relevant event handler, in much the same way that OnPaint() is called when a Paint event is raised.

The following table lists the methods you might want to override when the user clicks or moves the mouse.

Method

Called when...

OnClick(EventArgs e)

Mouse is clicked.

OnDoubleClick(EventArgs e)

Mouse is double-clicked.

OnMouseDown(MouseEventArgs e)

Left mouse button pressed.

OnMouseHover(MouseEventArgs e)

Mouse stays still somewhere after moving.

OnMouseMove(MouseEventArgs e)

Mouse is moved.

OnMouseUp(MouseEventArgs e)

Left mouse button is released.

If you want to detect when the user types in any text, you'll probably want to override the methods listed in the following table.

Method

Called When...

OnKeyDown(KeyEventArgs e)

A key is pressed.

OnKeyPress(KeyPressEventArgs e)

A key is pressed and released.

OnKeyUp(KeyEventArgs e)

A pressed key is released.

Note that some of these events overlap. For example, if the user presses a mouse button this will raise the MouseDown event. If the button is immediately released again, this will raise the MouseUp event and the Click event. Also, some of these methods take an argument that is derived from EventArgs rather than an instance of EventArgs itself. These instances of derived classes can be used to give more information about a particular event. MouseEventArgs has two properties, X and Y, which give the device coordinates of the mouse at the time it was pressed. Both KeyEventArgs and KeyPressEventArgs have properties that indicate which key or keys the event concerns.

That's all there is to it. It's up to you to think about the logic of precisely what you want to do. The only point to note is that you'll probably find yourself doing a bit more logic work with a GDI+ application than you would have with a Windows.Forms application. That's because in a Windows.Forms application you are typically responding to high-level events (TextChanged for a text box, for example). By contrast with GDI+, the events tend to be more elementary — user clicks the mouse or presses the H key. The action your application takes is likely to depend on a sequence of events rather than a single event. For example, say your application works like Word for Windows, where in order to select some textthe user clicks the left mouse button, then moves the mouse and releases the left mouse button. Your application receives the MouseDown event, but there's not much you can do with this event except record that the mouse was clicked with the cursor in a certain position. Then, when the MouseMove event is received, you'll want to check from the record whether the left button is currently down, and if so highlight text as the user selects it. When the user releases the left mouse button, your corresponding action(in the OnMouseUp() method) will need to check whether any dragging took place while the mouse button was down and act accordingly. Only at this point is the sequence complete.

Another point to consider is that, because certain events overlap, you will often have a choice of which event you want your code to respond to.

The golden rule really is to think carefully about the logic of every combination of mouse movement or click and keyboard event that the user might initiate, and ensure that your application responds in a way that is intuitive and in accordance with the expected behavior of applications in every case. Most of your work here will be in thinking rather than in coding, though the coding you do will be tricky because you might need to take into account a lot of combinations of user input. For example, what should your application do if the user starts typing in text while one of the mouse buttons is held down? It might sound like an improbable combination, but sooner or later some user is going to try it!

The CapsEditor example keeps things very simple, so you don't really have any combinations to think about. The only thing you are going to respond to is when the user double-clicks, in which case you capitalize whatever line of text the mouse pointer is hovering over.

This should be a fairly simple task, but there is one snag. You need to trap the DoubleClick event, but the previous table shows that this event takes an EventArgs parameter, not a MouseEventArgs parameter. The trouble is that you'll need to know where the mouse is when the user double-clicks if you are to identify correctly the line of text to be capitalized — and you need a MouseEventArgs parameter to do that. There are two workarounds. One is to use a static method implemented by the Form1 object, Control.MousePosition, to find out the mouse position:

 protected override void OnDoubleClick(EventArgs e) { Point MouseLocation = Control.MousePosition; // handle double click } 

In most cases, this will work. However, there could be a problem if your application (or even some other application with a high priority) is doing some computationally intensive work at the moment the user double-clicks. It just might happen in that case that the OnDoubleClick() event handler doesn't get called until perhaps half a second or so after the user has double-clicked. You don't really want delays like that, because they usually annoy users intensely, but even so, occasionally it does happen and sometimes for reasons beyond the control of your app (a slow computer, for instance). Trouble is, half a second is easily enough time for the mouse to get moved halfway across the screen, in which case your call to Control.MousePosition will return the completely wrong location!

A better way here is to rely on one of the many overlaps between mouse-event meanings. The first part of double-clicking a mouse involves pressing the left button down. This means that if OnDoubleClick() is called, you know that OnMouseDown() has also just been called, with the mouse at the same location. You can use the OnMouseDown() override to record the position of the mouse, ready for OnDoubleClick(). This is the approach taken in CapsEditor:

 protected override void OnMouseDown(MouseEventArgs e) { base.OnMouseDown(e); this.mouseDoubleClickPosition = new Point(e.X, e.Y); } 

Now look at the OnDoubleClick() override. There's quite a bit more work to do here:

 protected override void OnDoubleClick(EventArgs e) { int i = PageCoordinatesToLineIndex(this.mouseDoubleClickPosition); if (i >= 0) { TextLineInformation lineToBeChanged = (TextLineInformation)documentLines[i]; lineToBeChanged.Text = lineToBeChanged.Text.ToUpper(); Graphics dc = this.CreateGraphics(); uint newWidth =(uint)dc.MeasureString(lineToBeChanged.Text, mainFont).Width; if (newWidth > lineToBeChanged.Width) lineToBeChanged.Width = newWidth; if (newWidth+2*margin > this.documentSize.Width) { this.documentSize.Width = (int)newWidth; this.AutoScrollMinSize = this.documentSize; } Rectangle changedRectangle = new Rectangle( LineIndexToPageCoordinates(i), new Size((int)newWidth, (int)this.lineHeight)); this.Invalidate(changedRectangle); } base.OnDoubleClick(e); } 

You start off by calling PageCoordinatesToLineIndex() to work out which line of text the mouse pointer was hovering over when the user double-clicked. If this call returns –1, then you weren't over any text, so there's nothing to do; except, of course, call the base class version of OnDoubleClick() to let Windows do any default processing.

Assuming you've identified a line of text, you can use the string.ToUpper() method to convert it to uppercase. That was the easy part. The hard part is figuring out what needs to be redrawn where. Fortunately, because this example is simple, there aren't too many combinations. You can assume for a start that converting to uppercase will always either leave the width of the line on the screen unchanged or increase it. Capital letters are bigger than lowercase letters; therefore, the width will never go down. You also know that because you are not wrapping lines, your line of text won't overflow to the next line and push out other text below. Your action of converting the line to uppercase won't, therefore, actually change the locations of any of the other items being displayed. That's a big simplification!

The next thing the code does is use Graphics.MeasureString() to work out the new width of the text. There are now just two possibilities:

  • The new width might make your line the longest line, and cause the width of the entire document to increase. If that's the case you'll need to set AutoScrollMinSize to the new size so that the scroll bars are correctly placed.

  • The size of the document might be unchanged.

In either case, you need to get the screen redrawn by calling Invalidate(). Only one line has changed; therefore, you don't want to have the entire document repainted. Rather, you need to work out the bounds of a rectangle that contains just the modified line, so that you can pass this rectangle to Invalidate(), ensuring that just that line of text will be repainted. That's precisely what the previous code does. Your call to Invalidate() initiates a call to OnPaint() when the mouse event handler finally returns. Keeping in mind the earlier comments about the difficulty in setting a break point in OnPaint(), if you run the sample and set a break point in OnPaint() to trap the resultant painting action, you'll find that the PaintEventArgs parameter to OnPaint() does indeed contain a clipping region that matches the specified rectangle. And because you've overloaded OnPaint() to take careful account of the clipping region, only the one required line of text will be repainted.




Professional C# 2005
Pro Visual C++ 2005 for C# Developers
ISBN: 1590596080
EAN: 2147483647
Year: 2005
Pages: 351
Authors: Dean C. Wills

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