Building the Pieces


The overall structure of the project code will be familiar to you. We will create a subclass of java.awt.Frame, called FancySrcFrame, in package fancysrc. The frame will have a panel at North, containing various components. The FancySrcFrame class will be the event listener for all events from all components.

We will divide the work into five pieces. We will develop each piece in turn before assembling everything into the final product. The five pieces are:

  • The menu

  • The file-specification code

  • The color-specification code

  • The display area

  • The painting code

Each piece of the project is discussed in its own section.

The File Menu

Over the years, the software industry has created a substantial number of conventions for interacting with GUI-based programs. Every GUI is different, but they can all be approached in the same way, with the same reasonable expectations. This is enormously beneficial to the community of software users (that's us), because it reduces the amount of time we have to spend learning to use a new program.

The automobile industry is in a similar situation. If you know how to drive a car, you pretty much know how to drive every car. If this were not the case, car rental would be even more stressful than it already is.

One of the standard GUI practices is to install a menu bar in every program's main frame. The leftmost menu is a File menu, whose last item is Exit. In our case, the File menu will have an Open… item. Here we won't worry about how to actually open a file. That's covered in the next section, "Specifying a File." For the moment, our concern is to construct a menu bar with a File menu.

Building and responding to a menu requires techniques that were presented in Chapters 15, "Components," and 16, "Events." The constructor for our main application class (FancySrcFrame, in package fancysrc) will build the menu.

When you write code that builds menus, you might find it helpful to draw a diagram like the one in Figure 17.3.

click to expand
Figure 17.3: Menu schematic

A menu schematic might be trivial for the project at hand, but it makes life much easier if you are creating complicated menu bars, with many menus and submenus. After you write your menu code, you can test all the menus to make sure they match your schematic.

Chapter 15 presented a list of steps for building a menu structure:

  1. Create a menu bar.

  2. Create the menus.

  3. Attach the menus to the menu bar.

  4. Attach the menu bar to the frame.

Here is some code that builds the menu structure:

 1. MenuBar mbar = new MenuBar();  2. Menu fileMenu = new Menu("File");  3. openMI = new MenuItem("Open…");  4. openMI.addActionListener(this);  5. fileMenu.add(openMI);  6. exitMI = new MenuItem("Exit");  7. exitMI.addActionListener(this);  8. fileMenu.add(exitMI);  9. mbar.add(fileMenu); 10. setMenuBar(mbar);

Line 1 creates a menu bar. Lines 2-8 create a menu. Line 9 attaches the menu to the menu bar, and line 10 attaches the menu bar to the frame. Assume the current class implements the java.awt.event.ActionListener interface, so this is a legal argument to the addActionListener() calls in lines 4 and 7.

The variables mbar and fileMenu are declared within the constructor. (Remember, all the preceding code goes in the FancySrcFrame constructor.) However, openMI and exitMI will be declared as variables of the FancySrcFrame class. You'll see why shortly. Meanwhile, can you guess? (Hint: It has something to do with event handling.)

Now that we have a small chunk of code, let's test it. We'll embed it in a test class called MenuTest. This class implements ActionListener, so that lines 4 and 7 will compile. If the code works, we can later copy it verbatim from the test program into our final project code.

Here is the source for MenuTest:

package fancysrc; import java.awt.*; import java.awt.event.*; class MenuTest extends Frame implements ActionListener {   private MenuItem  openMI, exitMI;   MenuTest()   {     MenuBar mbar = new MenuBar();     Menu fileMenu = new Menu("File");     openMI = new MenuItem("Open…");     openMI.addActionListener(this);     fileMenu.add(openMI);     exitMI = new MenuItem("Exit");     exitMI.addActionListener(this);     fileMenu.add(exitMI);     mbar.add(fileMenu);     setMenuBar(mbar);     setSize(250, 100);   }   public void actionPerformed(ActionEvent e)   {   }   public static void main(String[] args)   {     (new MenuTest()).setVisible(true);   } }

The actionPerformed() method doesn't do anything, because for now we just want to check the structure of the menu. We are testing look, not feel. If you want to run MenuTest, it's on your CD-ROM. Just type java fancysrc.MenuTest. It looks like Figure 17.4.


Figure 17.4: Teting the menu's look

The figure matches the menu schematic from Figure 17.3, so apparently the code is good.

Now let's add code to respond to menu activation, so we can test feel as well as look. We just need to put some println() calls in the actionPerformed() method. Later we'll replace the calls with code that actually opens a file or exits the program.

Here's the new version of actionPerformed():

public void actionPerformed(ActionEvent e) {   if (e.getSource() == openMI)     System.out.println("OPEN Menu item");   else     // Must be the "Exit" menu item.     System.out.println("EXIT Menu item"); }

When the test program is run and the two menu items are activated one after another, the output is

OPEN Menu item EXIT Menu item

The output shows that the menu item action events are being handled correctly.

Implementing the exiting code is trivial. We just insert a call to System.exit():

public void actionPerformed(ActionEvent e) {   if (e.getSource() == openMI)     System.out.println("OPEN Menu item");   else     // Must be the "Exit" menu item.     System.exit(0); }

The test code is on your CD-ROM. If you want to run it, type java fancysrc .MenuEventTest. At this point the menu looks right, and its events are being handled properly, so we can move on to the question of how to open a file.

Specifying a File

Most computer programs modify data stored in files. This accounts for the prominent position of the File menu. Selecting a file for a program to process is a very common activity. You would expect file selection to be standardized in some way, and this is indeed the case.

Java provides a class called java.awt.FileDialog, which supports all the functionality needed to help users specify a file. The class is easy to use. As the name implies, it creates a dialog box. A dialog box is a window that is subordinate to its program's main frame, used for brief user interaction. When you delete a file or exit a program, and a box pops up to ask you if you're sure, you are looking at a dialog box.

Many dialog boxes are modal. A modal dialog box consumes all mouse and keyboard input to the program. This implies that you can't continue using the program until you have dealt with the dialog box and dismissed it. Most "Are you sure?" dialog boxes are modal. Java's file dialog box is also modal.

The FileDialog class shares a lot of behavior with the Frame class. This is not surprising, since the classes have a common superclass called Window, as shown in Figure 17.5.

A glance at the API shows that FileDialog class has three constructors:

FileDialog(Frame parent) FileDialog(Frame parent, String title) FileDialog(Frame parent, String title,            int mode)


Figure 17.5: Window, Frame, and FileDialog

The parent argument is the frame over which the dialog box will appear. The title string determines what appears in the dialog box's title bar. The mode specifies whether the dialog box will be used for opening or saving a file. Opening is the default, so you don't have to worry about specifying the mode. (But see Exercise 1 at the end of this chapter.)

Figure 17.6 shows a file dialog box, configured for opening:

click to expand
Figure 17.6: File dialog box configured for opening

Unlike frames, file dialog boxes are created with non-zero width and height, so you don't have to call setSize() on them. However, like frames, they are not visible until you call setVisible(true) on them. When you make this call, the dialog box appears, and the rest of the program's GUI refuses to accept mouse or keyboard input. Moreover, execution of your program pauses. Eventually, the user deals with and dismisses the dialog box. At this point, the rest of the GUI once more accepts input, and execution of your program continues from the line immediately following the setVisible(true) call.

The functionality is complicated, but using file dialog boxes is actually very simple. You construct your dialog box and, at the right moment, call setVisible(true) on it. The next line of code will not execute until a file has been specified (or the user has selected Cancel). There are two useful calls that you can then make on your dialog box, and both methods return strings:

getFile() The getFile() method returns the name of the file the user chose, or null if the dialog box was canceled.

getDirectory() The getDirectory() method returns the name of the chosen directory.

Whenever you learn about a new Java class, it's a good idea to write a practice program that creates an instance of the class and uses it in a way similar to the way you will later be using it in your program. That way you can experiment freely, and there is no danger that you will break your project accidentally by deleting or changing perfectly good code.

Here is a practice program that creates a file dialog box when a button is clicked:

package fancysrc; import java.awt.*; import java.awt.event.*; class FileDialogPractice extends Frame implements ActionListener {   public FileDialogPractice()   {     setLayout(new FlowLayout());     Button btn = new Button("Show me…");     btn.addActionListener(this);     add(btn);     setSize(200, 200);   }   public void actionPerformed(ActionEvent e)   {     FileDialog dia = new FileDialog(this);     dia.setVisible(true);     String fileName = dia.getFile();     if (fileName == null)       System.out.println("You canceled the dialog.");     else       System.out.println("Your chose file " + fileName + "                           in " + dia.getDirectory());   }   public static void main(String[] args)   {     (new FileDialogPractice()).setVisible(true);   } }

Notice the code in actionPerformed():

1. FileDialog dia = new FileDialog(this); 2. dia.setVisible(true); 3. String fileName = dia.getFile();    …

After line 1 executes, processing does not move on to line 2 until the user has dismissed the dialog box. The getFile() call on line 3 returns null if the dialog box was canceled. If you want to try the test program, it's on your CD-ROM. To run it, type java fancysrc.FileDialogPractice.

What should the code do after the file has been specified? We don't know yet, but we will figure it out in good time. At this point, we have code to capture the user's desired input file. Before we worry about processing and displaying the file, let's turn our attention to the remaining GUI-related piece of the puzzle.

Specifying Colors

This section will look at the portion of the GUI that supports color selection. First you'll see a perfectly reasonable design: straightforward, but nothing fancy. Then the design will be improved in stages, ending with code that is elegant and reusable.

Let's start with what we know. We want to users to select from among a small number of colors. The colors must be dark enough that they can be read easily on a white background. That rules out yellow, pink, and several others. Let's settle on these:

  • Black

  • Blue

  • Green

  • Red

  • Cyan

  • Magenta

Cyan and magenta are marginal. For now, we'll include them. We can throw them out later on if they don't look good. If throwing them out proves to be difficult, that's an indication that our design wasn't very flexible.

Our users will have to select from these six colors… twice. Once for the keyword color, and once for the comment color. Of the components that you learned about in Chapter 15, there are two that support making an exclusive selection from a small set of options: choices and radio buttons. We can rule out radio buttons because we would need 12 of them, compared to only two choices. If we used radio buttons, they would dominate the GUI, forcing the control area to be much larger than it needs to be, as shown in Figure 17.7.

click to expand
Figure 17.7: Too many radio buttons

For this situation, choices are much cleaner. Let's assume that our main application class will be called FancySrcFrame and will extend Frame. The class code will include the following declarations:

private Choice keywordChoice, commentChoice;

The choice components should be built in the FancySrcFrame constructor. One way to build them would be like this:

keywordChoice = new Choice(); keywordChoice.add("BLACK"); keywordChoice.add("BLUE"); keywordChoice.add("GREEN"); keywordChoice.add("RED"); keywordChoice.add("CYAN"); keywordChoice.add("MAGENTA"); commentChoice = new Choice(); commentChoice.add("BLUE"); commentChoice.add("BLACK"); commentChoice.add("GREEN"); commentChoice.add("RED"); commentChoice.add("CYAN"); commentChoice.add("MAGENTA"); 

This code can be improved, because every call appears twice. Whenever code is duplicated, consider the alternative of creating a method. The following code is much easier to read and more reliable:

keywordChoice = buildColorChoice(); commentChoice = buildColorChoice(); … private Choice buildColorChoice() {   Choice c = new Choice();   c.add("BLACK");   c.add("BLUE");   c.add("GREEN");   c.add("RED");   c.add("CYAN");   c.add("MAGENTA");   return c; }

The new version is 13 lines long, compared to 14 in the original. That's not much of a difference, but later you might want to add a third color choice, and perhaps a fourth. In the old version, each additional color choice required seven lines, compared to only one line in the new version. Moreover, all choice components created by the buildColorChoice() method will be identical. With the original approach, each time you type the seven repeated lines, you introduce the possibility of a transcription error. Did you notice that in the first block of code, the second choice reverses the order of BLACK and BLUE?

An even cleaner approach uses an array of color names. The following would appear along with the other variables of the FancySrcFrame class:

private String[] colorNames = {   "BLACK", "BLUE", "GREEN", "RED", "CYAN", "MAGENTA" };

Now the buildColorChoice() method is just the following:

private Choice buildColorChoice() {   Choice c = new Choice();   for (int i=0; i<colorNames.length; i++)     c.add(colorNames[i]);   return c; }

If you want to add or remove colors from the set of options, you just edit the contents of colorNames.

The choices will need an item listener. The logical candidate is the FancySrcFrame class. The itemStateChanged() method should cause the display to be repainted, using the new keyword or comment color. Somewhere (we don't need to decide where right now), some code will have to figure out which colors to use, based on the settings of the two choices. One way to do this would be to have a method that returns an instance of Color:

private Color getColorFromChoice(Choice c) {   int index = c.getSelectedIndex();   if (index == 0)     return Color.BLACK;   else if (index == 1)     return Color.BLUE;   else if (index == 2)     return Color.GREEN;   else if (index == 3)     return Color.RED;   else if (index == 4)     return Color.CYAN;   else     return Color.MAGENTA; }

That certainly works, but there is a much cleaner way. First, we'll create an array of colors. For maximum readability, it should appear next to the colorNames array:

private Color[] colors = {   Color.BLACK, Color.BLUE, Color.GREEN,   Color.RED, Color.CYAN, Color.MAGENTA };

To determine the color indicated by a choice component, use the choice's selected index as an index into the colors array:

private Color getColorFromChoice(Choice c) {   int index = c.getSelectedIndex();   return colors[index]; }

We have now worked out one piece of our design. We could go on to work out all our other design decisions, but before we blaze ahead, let's test what we have so far. If it doesn't work, we need to try again. If it works, we aren't committed to it. We reserve the right to improve on our color-specifying design later on.

Since color specification is the first code we will develop, our test will be simple. We don't yet know how we will select the file to be read, or paint lines on the screen, or paint source code on the screen in appropriate colors. So we'll create a program that just implements the color-specifying part of the GUI. To verify that the right colors are being returned from getColorFromChoice(), we'll just draw two squares in the frame. The square on the left will be the keyword color. The square on the right will be the comment color. Figure 17.8 shows the GUI.

click to expand
Figure 17.8: Testing color selection

Here's the code:

package fancysrc; import java.awt.*; import java.awt.event.*; class ColorTest extends Frame implements ItemListener {   private String[] colorNames =   {     "BLACK",     "BLUE",     "GREEN",     "RED",       "CYAN",     "MAGENTA"   };   private Color[] colors =   {     Color.BLACK, Color.BLUE, Color.GREEN,     Color.RED,   Color.CYAN, Color.MAGENTA   };   private Choice  keywordChoice, commentChoice;   public ColorTest()   {     setLayout(new FlowLayout());     add(new Label("Keyword Color:"));     keywordChoice = buildColorChoice();     keywordChoice.addItemListener(this);     add(keywordChoice);     add(new Label("Comment Color:"));     commentChoice = buildColorChoice();     commentChoice.addItemListener(this);     add(commentChoice);     setSize(500, 300);   }   private Choice buildColorChoice()   {     Choice c = new Choice();     for (int i=0; i<colorNames.length; i++)       c.add(colorNames[i]);     return c;   }   private Color getColorFromChoice(Choice c)   {     int index = c.getSelectedIndex();     return colors[index];   }   public void itemStateChanged(ItemEvent e)   {     repaint();   }   public void paint(Graphics g)   {     Color keywordColor = getColorFromChoice(keywordChoice);     g.setColor(keywordColor);     g.fillRect(100, 100, 100, 100);     Color commentColor = getColorFromChoice(commentChoice);     g.setColor(commentColor);     g.fillRect(300, 100, 100, 100);   }   public static void main(String[] args)   {     new ColorTest().setVisible(true);   } }

The class code begins with the arrays colorNames and colors. Notice how each name is aligned vertically with its corresponding color. It's a small touch that creates a visual relationship between the functionally related items.

The itemstateChanged() method just calls repaint(). Remember from Chapter 16 that when you want to paint your display in reaction to user input, you shouldn't directly call paint(). Rather, you should call repaint(), which clears the display and then calls paint(). Our paint() method draws the two squares.

It works. If you want to try it, the code is on your CD-ROM. Just type java fancysrc .ColorTest.

We can't rest on our laurels yet. The code works, ColorTest proves it, but it isn't very object-oriented. The software that supports a single function (color selection) is spread throughout the class. The great thing about object-oriented programming is that it allows you to encapsulate related functionality. Let's see how to encapsulate color selection.

Think about the Choice class. Its getSelectedIndex() method returns an int. A lot of the code in ColorTest is devoted to converting that int to the corresponding color. Life would be a lot easier if Choice had a method called getSelectedColor(). Of course, no such method exists, because Choice is a general-purpose class intended for specifying colors, fonts, font sizes, names, countries, languages, or anything else that any programmer might think of. But we can subclass Choice to create a special-purpose class that does exactly what we want.

We will create a subclass called ColorChoice. The constructor will populate the component with the appropriate strings. The colorNames and colors arrays will go inside the new class, since no other code will need them. We will provide a getSelectedColor() method. The new class looks like this:

package fancysrc; import java.awt.*; public class ColorChoice extends Choice {   private static String[] colorNames =   {     "BLACK",     "BLUE",     "GREEN",     "RED",       "CYAN",     "MAGENTA"   };   private static Color[] colors =   {     Color.BLACK, Color.BLUE, Color.GREEN,     Color.RED,   Color.CYAN, Color.MAGENTA   };   public ColorChoice()   {     for (int i=0; i<colorNames.length; i++)       add(colorNames[i]);   }   public Color getSelectedColor()   {     return colors[getSelectedIndex()];   } }

Notice that the two arrays have been declared as static. Remember that if a variable is static, there is only one copy of it, shared by all instances of the class. We know that there will be two instances of ColorChoice. There is no need to create two identical versions of the arrays, which is what would happen if they were not static. Each instance would have its own version. If the GUI changed later so that there were 25 color choices, there would be 25 identical versions of each array. Duplication of data is always something to be avoided. Here we avoid it by making the arrays static.

Testing the code is much easier. The complicated stuff is now in the ColorChoice class. The test code becomes the following:

package fancysrc; import java.awt.*; import java.awt.event.*; class ColorChoiceTest extends Frame implements ItemListener {   private ColorChoice  keywordChoice, commentChoice;   public ColorChoiceTest()   {     setLayout(new FlowLayout());     add(new Label("Keyword Color:"));     keywordChoice = new ColorChoice();     keywordChoice.addItemListener(this);     add(keywordChoice);     add(new Label("Comment Color:"));     commentChoice = new ColorChoice();     commentChoice.addItemListener(this);     add(commentChoice);     setSize(500, 300);   }   public void itemStateChanged(ItemEvent e)   {     repaint();   }   public void paint(Graphics g)   {     Color keywordColor = keywordChoice.getSelectedColor();     g.setColor(keywordColor);     g.fillRect(100, 100, 100, 100);     Color commentColor = commentChoice.getSelectedColor();     g.setColor(commentColor);     g.fillRect(300, 100, 100, 100);   }   public static void main(String[] args)   {     new ColorChoiceTest().setVisible(true);   } }

The arrays are gone. The variables keywordChoice and commentChoice are now declared as type ColorChoice. We can call addItemListener() on them, just as if they were instances of Choice, because they inherit all the event-processing functionality of Choice. If you want to run the code, type java fancysrc.ColorChoiceTest. The GUI looks just like the earlier test GUI, so there's no need for a screenshot.

Now we can rest on our laurels! The source for class ColorChoice is less than 30 lines long, and look at what it can do:

  • Look good.

  • Behave exactly like a standard Choice.

  • Be manipulated by a layout manager.

  • Send out item events when activated.

  • Report the selected color.

That's not a bad resume for such a small class. But we can't rest on our laurels all day. It's time to develop the rest of the code.

The Main Display Area

Most of the GUI work is now complete. We still have to create the check box that requests lines, but we'll get to that a bit later. Now we're going to step back and look at the big picture.

Our display will be involved a lot of painting. The painting examples you saw in Chapter 14 all involved painting a frame. You saw subclasses of java.awt.Frame with specialized versions of the paint() method. Later you saw that when user input makes it necessary to revise the display, you should call your frame's repaint() method, which causes the screen to be cleared and the paint() method to be called.

As it happens, the repaint() mechanism works for certain other component types in addition to frames. There is a class called java.awt.Canvas that has no inherent appearance at all. If you construct a canvas and install it in a GUI, you won't see anything worth mentioning. That's okay, because you never actually put a canvas in a GUI. You create a subclass of Canvas, with a paint() method that draws whatever you want, and it is the subclass that you use in your GUI.

We will use a Canvas subclass, called FancySrcCanvas, for our main display area. The frame that contains everything will use its default Border layout manager. The control components (the Show lines check box and the two color choices) will go in a panel at North, and the canvas will be at Center, as shown in Figure 17.9.

click to expand
Figure 17.9: GUI layout

The FancySrcCanvas will need to redisplay itself whenever the user changes the file, the Show lines preference, or the keyword or comment color. The GUI code will detect all these changes. The FancySrcCanvas class needs a method that the GUI can call when it's time to redisplay. Let's call this method reconfigure(). It will need four arguments:

  • A string representing the name of the new source file.

  • A boolean that controls whether or not lines should be displayed.

  • Colors for keywords.

  • Colors for comments.

The method should not paint directly to the screen, because painting is always relegated to the paint() method. Our reconfigure() method will simply record its four arguments and then call repaint(). This will trigger a behind-the-scenes chain of events that will clear the canvas and call paint(). When paint() runs, it will know what to do (what file to read, what colors to use, whether it should underline), because it will read the values stored by reconfigure().

We can now write the skeleton of FancySrcCanvas:

public class FancySrcCanvas extends Canvas {   private String   fileName;   private boolean  showLines;   private Color    keywordColor, commentColor;   FancySrcCanvas()   {     // To do   }   void reconfigure(String file, boolean line,                    Color kColor, Color cColor)   {     fileName = file;     showLines = line;     keywordColor = kColor;     commentColor = cColor;     repaint();   }   public void paint(Graphics g)   {     // To do   } }

The body of the constructor and the paint() method have been left for later. The constructor will be trivial, but paint(), as you might expect, will be substantial.

Once again, as you saw with the ColorChoice class, subclassing allows us to create clean, encapsulated code. If we did not use a canvas subclass, the painting would happen in the paint() method of the main frame subclass. This painting code would be jumbled in along with all the other code. With subclassing, we know that all the painting code, and nothing except the painting code, is to be found in FancySrcCanvas.

Painting Colored Code

Now we have a workable concept for the FancySrcCanvas class, so we can fill in the details. The boring details go in the constructor. The interesting ones go in the paint() method.

Let's dispense with the boring details first. We need to choose a font and make some decisions about how to lay out the lines of text. The values we'll use here are somewhat arbitrary. We need to decide on a y coordinate for the topmost line of code. (Remember, when you paint text, you specify the text's baseline, not the top of the text). We need to decide how much vertical space to leave between consecutive lines of code, and we need to choose an x coordinate for the text. Figure 17.10 shows how text will be positioned.

click to expand
Figure 17.10: Positioning text

We'll use a plain monospaced 16-point font. (Remember, monospaced fonts are always best for displaying source code.) The topmost baseline will be at 20. Every line of text will be 18 pixels below the previous line. The x-coordinate of all text will be 9. These values were arrived at after a fair amount of boring experimentation. The result is the following constructor for FancySrcCanvas:

FancySrcCanvas() {   font = new Font("Monospaced", Font.PLAIN, 16);   topBaseline = 20;   leftMargin = 9;   verticalSpace = 18; } 

With the font we have chosen, each character is 10 pixels wide. This number will be extremely useful later on. For now, can you guess why it's important?

There is a riddle that brings a knowing gleam to the eyes of experienced programmers, even though it isn't very funny. How do you fit five elephants into a Volkswagen Beetle? Answer: two in the front, three in the back. It isn't a good riddle, but it's a good example of top-down development, where you begin with an overall design idea, breaking each piece down into successively more refined designs until there is nothing left to do but implement your solution. Of course, if the design doesn't work, it's not the fault of the elephants.

We will take a top-down approach to developing the paint() method of FancySrcCanvas, starting with what we know. We know that the horizontal lines must be painted if showLines is true. We also know that the text must be painted if a source file has been specified. That is, if fileName, which is initialized to null, is no longer null. That's all we know, but it's enough to start. Here's our paint() method:

public void paint(Graphics g) {   if (showLines)     paintLines(g);   if (fileName != null)     paintText(g); }

Now we have to create the paintLines() and paintText() methods. paintLines() seems easy. Starting from the topmost baseline, horizontal lines must be drawn across the entire width of the canvas. Lines must be drawn verticalSpace pixels apart, down to the bottom of the canvas. It would all be simple, if only we knew how wide and tall the canvas is.

Fortunately, Canvas has a getSize() method that returns an instance of java.awt.Dimension. This very simple class has variables width and height. So the code can use getSize().width and getSize().height to determine the size of the canvas.

Here is the paintLines() code:

private void paintLines(Graphics g) {   g.setColor(Color.lightGray);   int height = getSize().height;   int width = getSize().width;   for (int y=topBaseline; y<height; y+=verticalSpace)     g.drawLine(0, y, width, y); } 

Now it's time to write paintText(). We don't yet know how we're going to draw text in three colors, but we don't have to know. This is top-down development. Let's stay with what we do know. There's a Java source file whose name is found in the variable fileName. We know that paintText() will need to read each line in turn from that file and paint the line. So here is the method, with the issue of painting multicolored text deferred for later consideration:

 1.  private void paintText(Graphics g)  2.  {  3.    g.setFont(font);  4.  5.    try  6.    {  7.      // Create the readers.  8.      FileReader fr = new FileReader(fileName);  9.      LineNumberReader lnr = new LineNumberReader(fr); 10. 11.      // Read & display. 12.      String s = "xx";   // Anything but null 13.      int y = topBaseline; 14.      while (s != null) 15.      { 16.         s = lnr.readLine(); 17.         if (s == null) 18.           break; 19.         paintOneSourceLine(g, s, y); 20.         y += verticalSpace; 21.       } 22. 23.       // Close the readers. 24.       lnr.close(); 25.       fr.close(); 26.     } 27. 28.     catch (IOException x) 29.     { 30.       System.out.println("Trouble!" + x.getMessage()); 31.   } 32. }

The method uses a file reader chained to a line number reader. You were introduced to readers in Chapter 13, "File Input and Output." The while loop in lines 14-21 reads lines of text from the file until the readLine() call on line 16 returns null, indicating that the end of the file has been reached. (On line 12, s has to be initialized to any non-null string, so that the loop won't terminate the first time through.)

The variable y determines the baseline of the next line of text to be painted. On line 13, y is initialized to topBaseline. Every pass through the loop, it is incremented by verticalSpace (line 20).

Text is painted on line 19, where a call is made to paintOneSourceLine(). We'll write this method shortly. Its arguments are the Graphics object, the string to be painted (s), and the y-coordinate of the text (y).

We have deferred thinking about how to paint multicolored source text until we could create a good structure for the paint() method of FancySrcCanvas. That structure is now in place, so it's time to decide how to paint the code. Here's the skeleton of paintOneSourceLine():

private void paintOneSourceLine(Graphics g,                                 String srcLine,                                 int y)   {     …   }

Here's the strategy. First, the method will paint the entire text line in black, whether or not it contains any keywords or comments. Then the line will be inspected to see if contains any keywords or comments. If so, part of the text will be painted again, in the appropriate color. As you will see, there are methods in the String class that make this easy.

Let's start by painting the entire line in black. We don't have to call setFont() because that call was made already, in paintText():

// First paint entire line in black. g.setColor(Color.black); g.drawString(srcLine, leftMargin, y);

That was easy. Now to detect and render comments. Comments begin with a double slash (//) and continue through the end of the line. So the code needs to answer the following questions:

Does the string contain a double slash?

If so, where is the double slash?

If the answer to the first question is "no," there is no comment to paint.

Fortunately, class String has a method called indexOf(). Its argument is another string. If the argument string appears anywhere in the executing object string, the method returns the position of the argument string within the executing object string. For example, if s1 is whether and s2 is the, s1.indexOf(s2) is 3. If the argument string does not appear in the executing object string, indexOf() returns -1. For example, if s1 is whether and s2 is heather, s1.indexOf(s2) is -1.

The comment-painting code uses another method of the String class: substring(). You were introduced to this method in Chapter 12, "The Core Java Packages and Classes." When called with a single int argument, it returns the portion of the string beginning at the argument position. For example, if s1 is whether, s1.substring(2) is ether.

Here is the code that paints comments:

1. // Paint comment (if any). 2. int commentIndex = srcLine.indexOf("//"); 3. if (commentIndex >= 0) 4. { 5.   g.setColor(commentColor); 6.   String comment = srcLine.substring(commentIndex); 7.   int x = charIndexToX(commentIndex); 8.   g.drawString(comment, x, y); 9. }

To illustrate how this code works, consider what happens when srcLine is

height += 25; // Increment height

The comment begins at character position 14, so commentIndex is 14. On line 6, comment is //increment height. This is the string that is overpainted in the comment color, at line 8.

Line 7 makes a call to charIndexToX(), which returns the x-coordinate where the comment will be painted. This value must be calculated exactly, so that the new text will exactly overwrite the black text. This method is

private int charIndexToX(int charIndex) {   return leftMargin + 10*charIndex; }

Earlier in this section, you read that in a 16-point monospaced font, each char is 10 pixels wide. This implies that, for example, the 18th character in any line is 180 pixels to the right of the 0th character. And the 0th character is always painted at leftMargin. So the x-coordinate of the nth character is leftMargin + 10*n. This is the formula used by charIndexToX().

So far our paintOneSourceLine() code is

private void paintOneSourceLine(Graphics g,                                 String srcLine,                                 int y) {   // First paint entire line in black.   g.setColor(Color.black);   g.drawString(srcLine, leftMargin, y);   // Paint comment (if any).   int commentIndex = srcLine.indexOf("//");   if (commentIndex >= 0)   {     g.setColor(commentColor);     String comment = srcLine.substring(commentIndex);     int x = charIndexToX(commentIndex);     g.drawString(comment, x, y);   }   …

We are ready to deal with keywords, but we have to be careful. If we just search for keywords and overwrite them in the right color, we could get confounded by a line like this:

x = 16;  // try to while away the time

The code contains no Java keywords, but the comment does. When the line is being searched for keywords, the search should not include the comment. This will not guarantee that the code will never erroneously color non-keyword text, but it guards against one common situation. (Making the keyword search 100% foolproof would be a daunting task. It would overwhelm the code and would seriously reduce the learning value of the project. Not searching comments will be enough for our purposes. Exercise 5 at the end of this chapter invites you to think more about the problem.)

If the software is going to search for keywords, it needs to know which strings are keywords. The FancySrcCanvas class needs an array of strings that are Java keywords. Here it is:

private String[] keywords = {   "abstract", "boolean", "break", "byte", "case", "catch",   "char", "class", "continue", "default", "double", "do",   "else", "extends", "false", "final", "float", "for",   "if", "implements", "import", "instanceof", "int",   "interface", "long", "new", "null", "package", "private",   "protected", "public", "return", "short", "static",   "super", "switch", "this", "throws", "throw", "true",   "try", "void", "while" };

Actually, the list is incomplete. It only includes Java keywords that were introduced in this book. There are a handful of others. Strictly speaking, null, true, and false are not keywords, but something similar.

Here is the skeleton of the remainder of the paintOneSourceLine() code:

// Search every position in string, through comment, // for any keyword. g.setColor(keywordColor); int lastCharPosition = srcLine.length()-1; if (commentIndex >= 0)   lastCharPosition = commentIndex - 1; for (int index=0; index<=lastCharPosition; index++) {   … } 

The for loop will search every position in the line of code, through lastCharPosition, to see if it begins with any entry in the keywords array. If the line does not contain a double-slash comment, lastCharPosition is set to the last character position in the line. If a double- slash comment is present, lastCharPosition is set to the last character position before the comment. For example, suppose the source line is

x = new Line();// Construct a line

The for loop will check each of the following substrings:

x = new Line();  = new Line(); = new Line();  new Line(); new Line(); ew Line(); w Line();  Line(); Line(); ine(); ne(); e(); (); ); ;

Each of the substrings will be compared against each entry in the keywords array. The code will use two methods of String that were presented in Chapter 12, substring() and startsWith(). The substring() method takes an int argument. It returns the portion of t he original string beginning at the specified index. For example, if s1 is Meryl Streep, s1 .substring(9) is eep. The startsWith() method takes a string argument. It returns true if the original string starts with the argument string. For example, if s1 is Meryl Streep and s2 is Me, s1.startsWith(s2) is true.

Now the body of the for loop can be filled in:

 1. for (int index=0; index<=lastCharPosition; index++)  2. {  3.   // Search at this position for every keyword.  4.   String sub = srcLine.substring(index);  5.   for (int i=0; i<keywords.length; i++)  6.   {  7.     if (sub.startsWith(keywords[i]))  8.     {  9.       int x = charIndexToX(index); 10.       g.drawString(keywords[i], x, y); 11.       break;  // Can't be any more keywords here 12.     } 13.   } 14. }   

Recall that the Graphics object has already had its color set to the keyword color. Line 9 makes use of the charIndexToX() method, which was written for the comment-painting code, to compute where to overdraw the keyword string.

That's all for the FancySrcCanvas class. Here is the whole class listing, all in one place:

package fancysrc; import java.io.*; import java.awt.*; class FancySrcCanvas extends Canvas {   private String[] keywords =   {     "abstract", "boolean", "break", "byte", "case", "catch",     "char", "class", "continue", "default", "do", "double",     "else", "extends", "false", "final", "float", "for",     "if", "implements", "import", "instanceof", "int",     "interface", "long", "new", "null", "package", "private",     "protected", "public", "return", "short", "static",     "super", "switch", "this", "throws", "throw", "true",     "try", "void", "while"   };   private Font                font;   private int                 topBaseline;   private int                 leftMargin;   private int                 verticalSpace;   private String              fileName;   private boolean             showLines;   private Color               keywordColor, commentColor;   FancySrcCanvas()   {     font = new Font("Monospaced", Font.PLAIN, 16);     topBaseline = 20;     leftMargin = 9;     verticalSpace = 18;   }   void reconfigure(String file, boolean line,                    Color kColor, Color cColor)   {     fileName = file;     showLines = line;     keywordColor = kColor;     commentColor = cColor;     repaint();   }   public void paint(Graphics g)   {     if (fileName == null)       return;     if (showLines)       paintLines(g);     paintText(g);   }   private void paintLines(Graphics g)   {     g.setColor(Color.lightGray);     int height = getSize().height;     int width = getSize().width;     for (int y=topBaseline; y<height; y+=verticalSpace)       g.drawLine(0, y, width, y);   }   private void paintText(Graphics g)   {     g.setFont(font);     try     {       // Create the readers.       FileReader fr = new FileReader(fileName);       LineNumberReader lnr = new LineNumberReader(fr);       // Read & display.       String s = "";   // Anything but null       int y = topBaseline;       while (s != null)       {         s = lnr.readLine();         if (s == null)           break;         paintOneSourceLine(g, s, y);         y += verticalSpace;       }       // Close the readers.       lnr.close();       fr.close();     }     catch (IOException x)     {       System.out.println("Trouble! " + x.getMessage());     }   }   private void paintOneSourceLine(Graphics g,                                   String srcLine, int y)   {     // First paint entire line in black.     g.setColor(Color.black);     g.drawString(srcLine, leftMargin, y);     // Paint comment (if any).     int commentIndex = srcLine.indexOf("//");     if (commentIndex >= 0)     {       g.setColor(commentColor);       String comment = srcLine.substring(commentIndex);       int x = charIndexToX(commentIndex);       g.drawString(comment, x, y);     }     // Search every position in string, through comment,     // for any keyword.     g.setColor(keywordColor);     int lastCharPosition = srcLine.length();     if (commentIndex >= 0)       lastCharPosition = commentIndex - 1;     for (int index=0; index<=lastCharPosition; index++)     {       // Search at this position for every keyword.       String sub = srcLine.substring(index);       for (int i=0; i<keywords.length; i++)       {         if (sub.startsWith(keywords[i]))         {           int x = charIndexToX(index);           g.drawString(keywords[i], x, y);           break;  // Can't be any more keywords here         }       }     }   }   private int charIndexToX(int charIndex)   {     return leftMargin + 10*charIndex;   } }




Ground-Up Java
Ground-Up Java
ISBN: 0782141900
EAN: 2147483647
Year: 2005
Pages: 157
Authors: Philip Heller

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