CSE190L Notes for Monday, 4/2/07

I continued our discussion of Chapter 7 material. We looked at some of the common methods you can call on a JFrame to change the way it looks and behaves: setSize, setTitle, setDefaultCloseOperation. I mentioned that Horstmann generally defines a custom frame class that extends a JFrame. Marty Stepp and I have found over the years that there is less confusion if you instead define a class that "has a" JFrame rather than a class that "is a" JFrame. In other words, this is a case where it's better to use composition than inheritance.

In the old days, you had to use inheritance for JFrames because many of the features that you wanted to change could be set only through inheritance. That's not true anymore. Now most properties of a JFrame that you want to change have "setter" methods that you can call. There are also some odd things that happen when JFrames are constructed in terms of when certain components are actually allocated. You'll run into fewer problems if you avoid using inheritance for the JFrame.

So I mentioned that instead of the kind of driver class that Horstmann writes where he calls setVisible, I'm going to write a custom class that has a method called start that will call setVisible on the JFrame:

        public class MonMain {
            public static void main(String[] args) {
                MonFrame frame = new MonFrame();
                frame.start();
            }
        }
That means that the custom frame class looks like this:

        import javax.swing.*;
        
        public class MonFrame {
            private JFrame frame;

            public MonFrame() {
                frame = new JFrame();
                frame.setTitle("Monday fun");
                frame.setSize(300, 300);
                frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
            }
        
            public void start() {
                frame.setVisible(true);
            }
        }
Someone asked why I introduce a start method. Why not include that code in the frame's constructor? We certainly could do that and some people actually like to do that, but then our main method would become:

        public static void main(String[] args) {
            new MonFrame();
        }
This seems rather odd to me that all we end up doing to start the program is to construct the frame. But this is somewhat a matter of personal taste. There is nothing particularly wrong with doing it this way, I just find it preferable for main to have at least two clear steps (construct the frame, then start it running).

Then I spent some time talking about the JPanel class. The JPanel class performs a double duty in Swing. It is used as a container, so we'll find that we often have JPanels inside of other JPanels as a way to group different user interface components together. JPanel is also used as a canvas for drawing, so if you want to create some kind of custom graphics image, you generally have a JPanel create that image for you.

So the pattern we'll follow is to have a total of three classes:

I began by defining a custom panel class and including a call on setBackground in its constructor to set the background color:

        import java.awt.*;
        import javax.swing.*;
        
        public class MonPanel extends JPanel {
            public MonPanel() {
                setBackground(Color.CYAN);
            }
        }
This meant we had to redefine our frame class to construct a panel and add it to the frame:

        import javax.swing.*;
        
        public class MonFrame {
            private JFrame frame;
            private MonPanel panel;

            public MonFrame() {
                frame = new JFrame();
                frame.setTitle("Monday fun");
                frame.setSize(300, 300);
                frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);

                panel = new MonPanel();
                frame.add(panel);
            }
        
            public void start() {
                frame.setVisible(true);
            }
        }
Anyone who took 142 here will probably remember how the DrawingPanel class worked. Much of what you used there will apply here. As with the DrawingPanel, all drawing on a JPanel is done using an object of type Graphics. You can think of this as being like a paint brush and the panel as being like the canvas. You can request the graphics object for a JPanel by calling getGraphics. One of the drawing commands is drawString, which draws some text on the panel. We found, however, that we got an error if we tried to do this in our frame constructor:

        public MonFrame() {
            frame = new JFrame();
            frame.setTitle("Monday fun");
            frame.setSize(300, 300);
            frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);

            panel = new MonPanel();
            frame.add(panel);
            panel.getGraphics().drawString("hello!", 50, 50);
        }
This generated a NullPointerException. That's because the Graphics object is not constructed until the frame is made visible. So we had to move this line of code after the call on setVisible in the start method:

        public void start() {
            frame.setVisible(true);
            panel.getGraphics().drawString("hello!", 50, 50);
        }
This did what we expected. It painted the given text at position (50, 50) on the screen. I reminded people that in screen coordinates, (0, 0) is in the upper-left corner of the panel with increasing x coordinates moving you right and increasing y coordinates moving you down. So this text was drawn at an x-coordinate 50 over from the left-hand border and 50 down from the top.

I pointed out, though, that this kind of drawing is not the Java way of doing things. Many graphics systems have a memory of what has been drawn, so that when you draw something, it stays there. The DrawingPanel class we used in cse142 had this property. But this is not the norm in Java. The usual thing for a JPanel is to be redrawn often, so the command above turns out to be only temporary. I minimized the window and when I brought it back, the text was gone.

The right way to do things in Java is to override the paintComponent method. When you do so, it is important to call super.paintComponent because it does some work to initialize the panel before you draw on it. So we moved this line of code into our panel class:

        import java.awt.*;
        import javax.swing.*;
        
        public class MonPanel extends JPanel {
            public MonPanel() {
                setBackground(Color.CYAN);
            }
            
            public void paintComponent(Graphics g) {
                super.paintComponent(g);
                g.drawString("hello!", 50, 50);
            }
        }
I added a println statement inside of paintComponent and we saw that it is called often, even though we never call it in our own code. This is something that is natural when you are writing code in a framework like Swing. Lots of code has been written for you in the JFrame and JPanel classes that figures out when to call paintComponent. So even if you never call it, that code that you're accessing through inheritance calls it.

Then we spent some time talking about drawing a rectangle. There is a procedural style command in the Graphics class for doing this where you would say something like:

        g.drawRect(100, 75, 80, 40);
This says to draw a rectangle with upper-left corner (100, 75) and with a width of 80 and a height of 40. This is an old AWT command that isn't officially deprecated, but is considered old-fashioned. When Swing was added to Java, a new graphics 2D library was added as well. Chapter 7 of the book describes the 2D library and that's what you should use for your homework. It's a more object-oriented way of doing things. Instead of telling the Graphics object to perform an action (draw a rectangle), we construct a rectangle object.

In adding the 2D library to Java, Sun decided to keep the old Graphics class mostly intact. They applied the idea of "additive, not invasive, change" by creating a new class called Graphics2D that extends Graphics. In fact, the object passed to paintComponent is of type Graphics2D. Unfortunately, you can't say:

        public void paintComponent(Graphics2D g) {
            ...
        }
This method does not have the same signature as the original paintComponent method, so by including it in our class we'd be defining an extra painting method that nobody calls. So instead, we have to cast the Graphics object to Graphics2D. You end up doing this so much that most people define a special variable that includes the cast:

        public void paintComponent(Graphics2D g) {
            super.paintComponent(g);
            Graphics2D g2 = (Graphics2D) g;

            ...
        }
Using this g2 variable, we can define a rectangle object:

        Rectangle2D r = new Rectangle2D.Double(100, 75, 80, 40);
Rectangle2D is the rectangle class, but it is an abstract class. You can choose between two implementations: one that uses doubles and one that uses floats. Most often we use the double version. As with the call on drawRect, this constructor defines a rectangle with upper-left coordinates (100, 75) and with a width of 80 and a height of 40.

Just constructing the rectangle wasn't enough to make it appear on the screen. We had to instruct the Graphics2D object to do something with it. We included this line of code to make it draw the outline of the rectangle:

        Rectangle2D r = new Rectangle2D.Double(100, 75, 80, 40);
        g2.draw(r);
We then added a couple more lines to have it fill the rectangle with yellow paint:

        Rectangle2D r = new Rectangle2D.Double(100, 75, 80, 40);
        g2.draw(r);
        g2.setPaint(Color.YELLOW);
        g2.fill(r);
That didn't quite work right because the yellow paint covered over the top and left lines of the border. Remember that this is a painting metaphor. You can end up painting over something you really wanted to see. To do this properly, we'd have to fill the rectangle first and then paint the border:

        Rectangle2D r = new Rectangle2D.Double(100, 75, 80, 40);
        g2.setPaint(Color.YELLOW);
        g2.fill(r);
        g2.setPaint(Color.BLACK);
        g2.draw(r);
In the last few minutes of class I passed out handout #3. It contains code for a sample animation. The main class and frame class look similar to what we've been using, although the frame class has two extra methods called addTimer and addSlider. You don't have to understand how these methods work, although we're going to be studying those details soon. The Timer is an object that calls repaint on the panel at regular intervals. Although we override the paintComponent method of a JPanel, you should never call paintComponent directly. Instead, you call repaint and that in turn calls paintComponent. The JSlider object changes the rate at which the panel is repainted. It varies from 1 frame per second at the slow end up to 30 frames per second at the fast end.

The panel has code similar to what we were writing, although it does some extra work. It defines a Font object by specifying the kind of font to use ("Serif"), the style of font (bold) and the font size, which varies as the animation runs. The panel also includes two fields that implement a counter that switches between counting up and counting down. This count is used to vary the size of the text and surrounding box. As a result, the text appears to grow and shrink as the animation runs.


Stuart Reges
Last modified: Fri Apr 6 10:22:44 PDT 2007