Editing a Text Document: The CapsEditor Sample C# Help

The CapsEditor example is designed to demonstrate how the principles of drawing that you have 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 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, then that line will be changed to all uppercase. That is literally all the example does. Even with this limited set of features, you will 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 33-17.

 

Figure 33-17

Figure 33-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 33-18 shows CapsEditor displaying its own source file, Form cs. (A few lines have been double-clicked in this image to convert them to uppercase.)

Figure 33-18

Figure 33-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 does not 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 that it is a text file and does not contain any nonprintable characters.

Begin by adding a using command;

using System;
using System.Collecticns;
using System.ComponentModel;
uingSystem. Drawing;
using System.10;
using system.Windows.Forms;

You will be using the StreamReader class, which is in the System. 10 namespace. Next, you add some fields to the Forml class:

Most of these fields should be self-explanatory. The documentLines field is a List<TextLinelnformation> that contains the actual text of the file that has been read in. Actually, 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 is a List<TextLinelnforrnation> rather than a plain array, you can dynamically add elements to it as you read in a file.

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

class TextLinelnformation
{
public string Text;
public uint Width;

TextLinelnformation looks like a classic case where you would normally use a struct rather than a class because it is just there to group a couple of fields, However, its instances are always accessed as elements of a List<TextLinelnforrnation>, which expects its elements to be stored as reference Each TextLinelnforrnation 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 would 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 change only when other parts of the document are modified in some way.) In this case, you have stored only the Width of the item because the height in this case is just the height of whatever your selected font is. It is the same for all lines of text so there is no point storing the height separately for each one you store it once, in the Forml line Height 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 on)

If you had 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 are treating each line of text as one single item.

Let’s turn to the main menu now.Windows Forms than of GDI+ Add the menu options using the design view in Visual Studio 2008, but rename them menuFile, menuFileOpen, and menuFileExit, Next add event handlers for the File Open and File Exit menu options using the Visual Studio 2008 properties window, The event handlers have their Visual Studio 2008-generated names of menuFileOpen_Cliclt () and menuFileExit_Click ().

Add some extra initializationcode in the Forml () constructor:

You add the event handler here for instances when the user clicks OK in the FileOpen dialog box, You have also set the filter for the Open File dialog box, so that you can load text files only, The example in this case only uses .txt files,in addition to the 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:

The actual definitions of the handlers are pretty standard:

Next, take a look at the LoadFile () method. It 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):

Most of this function is just standard file-reading (see Chapter 25, “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 have read in the file, you set the document Has Data 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 as well as 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 If you do not pass any parameters, it will just mark the entire client area as invalid, If you know that something needs painting, why don’t you just call On Paint () 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 is why:

  1. 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 had directly called a method to do the drawing from the LoadFile () method, then the LoadFile () method would not return until that drawing task was complete. During that time, your application cannot respond to any other events. However, by calling Invalidate ( ), you are 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, then it picks one and calls the corresponding event handler. Although the Paint event might be the only one sitting in the queue (so On Paint () is called immediately anyway), in a more complex application there might be other events that ought to get priority over your Paint event, In particular, when the user has decided to quit the application, this will be marked by a message known as WM_QUIT.
  2. If you have a more complicated, multithreaded application, then you will 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 does all the drawing, no matter what other thread requested the drawing operation. (Whatever thread is responsible for the message queue will be the thread that called Application. Run ().
  3. There is an additional performance-related reason. Suppose that a couple of different requests to draw part of the screen come in at about .he 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 (an then merge the Paint events if appropriate, combining the invalidated areas, so that the painting is only done once.
  4. 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 people 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 is easier to do if you do not have too many pathways into it from other parts of the program.

The bottom line from all of this is that it is good practice to keep all of your painting in the On Paint () 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 have drawn, then you might decide that it’s not worth the overhead of going through Invalidate () and just write a separate drawing routine;

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 CaleulateLineWidths () and CalculateDocumentSize () methods called from LoadFile ():

This method simply runs through each line that has been read in and uses the Graphics .MeasureString () 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 had not been simple enough to easily work out the height and location of each item, then 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 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.

The following is the method that calculates the document size:

This method first checks whether there is any data to be displayed. If there is not, then 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 had wanted to really do it properly, you would have used MeasureString () to check how big that warning actually is.

Once you have 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, from 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 will 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 you should normally do things. In fact, in this case, calling Invalidate () again will only needlessly request a duplicate Paint event. However, this in turn illustrates how Invalidate () gives Windows the chance to optimize performance. The second Paint event will not, in fact, get raised: Windows will see that there is 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 specify the 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 will be a negligible amount of time compared to how long it takes to actually do some painting.

OnPaint()

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

At the heart of this OnPaint () override is a loop that goes through each line of the document, calling Gzaph Lcs . DrawString () to paint each one. The rest of this code is mostly concerned with optimizing the painting – 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 is not, then 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 by calling another method, WorldYCoordinateToLinelndex ().

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 WorldYCoordinateToLinelndex () method, you pass-it the coordinate value (e.ClipRectcngle. Top – scrollPositionY). This is just the top of the clipping region, converted to world coordinates, If the return value is -1, you play it safe and assume that 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 have done all that, you essentially repeat the same process for the bottom of the clipping -rectangle 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 minLinelnClipRegion and maxLinelnClipRegion, 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 WorldYCoordinateToLinelndex (), 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 have wrapped it up in another method, LinelndexToWorldCoordinates (), which returns the required coordinates of the top-left corner of the item, The returned coordinates are world coordinates, but that is 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 to display 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 WorldYCoordinateToLinelndex () and LinelndexToWorldCoordinates () methods referred to in the previous section, as well as a couple of other methods.

First, LinelndexToWorldCoordina tes {) takes a given line index, and works out the world coordinates of the top-left comer of that line, using the known margin and line height:


You also use a method that roughly does the reverse transform in OnPaint ().

WorldYCoordinateToLinelndex () works out the line index, but it takes into account only a vertical world coordinate because it is used to work out the line index corresponding to the top and bottom of the clip region:

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 WorldYCoordinateToLinelndex ( ),.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:

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

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

Although these methods by themselves do not look particularly interesting, they do illustrate a general technique that you will probably need to use often, With GDI+, you will 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 will 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, where should it be displayed? Hence, if you are writing a GDI+ application, you will 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 have 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 are now going to add that functionality 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, covers how to handle user input.) Essentially, you override methods from the Form class that are called from the relevant event handler, in much the same way that On Paint () 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.

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

Note that some of these events overlap. For example, when the user presses a mouse button, the MouseDown event is raised. If the button is immediately released again, then this will raise the MouseUp everft and the Click event. In addition, 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 is all there is to it. It is up to you to think about the logic of precisely what you want to do, The only point to note is that you will probably find yourself doing a bit more logic work with a GDI+ application than you would have with a ‘Windows . Forms application. That is because in a Windows Forms application you are typically responding to high-level events (Text Changed for a text box, for example), By contrast, with GDI+, the events tend to be more elementary – user clicks the mouse or presses the Hotkey, The action your application takes is likely to depend on a sequence of events rather than on a single event. For example, say your application works like Microsoft Word for Windows: to select some text, the user clicks the left mouse button, and then moves the mouse and releases the left mouse button, Your
application receives the MouseDown event, but there is 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 will 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 On MouseUp () method) will need to check whether any dragging took place while the mouse button was down and act accordingly within the method, 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 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, although the coding you do will be tricky because you might need to take into account many 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 eventually some user is going to try it!

The CapsEditor example keeps things very simple, so you do not really have any combinations to think about. The only thing you are going to respond to in the example 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 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 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 Forml object Control.MousePosition to find the mouse position:

protected override void OnDoubleClick(EventArgs e)
{
Point MouseLocation = Control.MousePosition;
II 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,on DoubleClick () event handler does not get called until perhaps half a second or so after the user h~s double-clicked. You do not want such delays because they usually annoy users intensely, but even so, occasionally it does happen and sometimes for reasons beyond the control of your application (a slow computer, for instance), The trouble is that half a second is easily enough time for the mouse to be moved halfway across the screen, in which case your call to Control, MousePosition will-return the completely wrong location!

A better approach 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);
mouseDoubleClickPosition = new Point(e.X, e.Y);

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

You start off by calling PageCoordinatesToLinelndex () 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 is nothing to do – except, of course, call the base class version of OnDoubleClick () to let Windows do any default processing.

Assuming that you have identified a line of text, you can use the string, To Upper () 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 are not too many combinations. You can assume 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 will not overflow to the next line and push out other text below. Your action of converting the line to uppercase will not, therefore, actually change the locations of any of the other items being displayed! That is a big simplification!

‘the next thing the code does is use Graphics .Measure=String() to work out the new width of the text, There are now just two possibilities:

  1. The new width might make your line the longest line and cause the width of the entire document to increase. If that is the case, then you will need to set AutoScrollMinSize to the new size so that the scroll bars are correctly placed.
  2. 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 do not 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 is precisely what the previous code does, Your call to Invalidate () initiates a call to On Paint () when the mouse event handler finally returns, Keeping in mind the earlier comments about the difficulty insetting a break point in On Paint () if you run the sample and set a break point in On Paint () to trap the resultant painting action, then you will find that the paintEventArgs parameter to On Paint () does indeed contain a clipping region that matches the specified rectangle. And because you have overloaded On Paint () to take careful account of the clipping region, only the one required line of text will be repainted.

Posted on November 3, 2015 in Graphics with GDI+

Share the Story

Back to Top