In this tutorial you will learn a basic design pattern for displaying graphics in Java. Along the way we'll have discussions of other features of Java, including parameterized types, Javadoc and JUnit. We'll start with the basics: how to display a drawing using Java.
Note: much of the following code can be utilized without fully understanding all the underlying details. We'll create two classes, Canvas and Root, which can be implemented and reused, and which contain all the boilerplate for setting up a basic graphics application.
Unit 1: How to Display a Drawing
Goal: Write a program to display this figure:
1. Get a window to draw in: the Canvas class
First off, what is a window? By one definition, a window is: a rectangular area on the screen. A window consists of rows and columns of pixels, similar to the Cartesian plane that you learned in math class. In fact, each column is addressed as an x-coordinate, and each row as a y-coordinate. The difference between the Cartesian plane and a window is that:
- The origin, (0, 0), of window coordinates is the upper-left corner of the window.
- Y-coordinate values increase as you move towards the bottom of the window.
We're going to be using the Java Swing facility to create a window. The components of Swing (literally class JComponent) create windows of different sizes and types. We'll be using a JPanel. To make it possible to draw in our JPanel, we'll create class Canvas, which will be a subclass of JPanel. For now, we'll simply add a constructor that sets the size of the window.
1 2 3 4 5 6 7 8 9 10 11 | import java.awt.Dimension; import javax.swing.JPanel; public class Canvas extends JPanel { public Canvas( int width, int height ) { Dimension dim = new Dimension( width, height ); setPreferredSize( dim ); } } |
Note that we don't directly set the size of the window directly, we set the preferred size. (Yes, there is also a setSize; which you use depends on the layout manager you are using. We set the layout manager in the Root class. See also: Laying Out Components Within a Container, in the Oracle Java Tutorial.)
2. Get a Frame to Hold the Canvas
If you want to show your window on the scree, you will need a frame; in Swing this is encapsulated by the JFrame class. The frame around some portion of a graphical user interface (GUI) is the first thing you see when you start a GUI application. It includes the title bar, minimize, maximize and close buttons, and the resize handles. The body of the frame is occupied by a content pane; in our application, you will display your Canvas inside the content pane.
Our JFrame will be contained in class Root. This class will implement the Runnable interface. Implementing an interface usually means writing one or more methods with given signatures. The Runnable interface requires that you write a method the run method, public void run. (See also: Interfaces, in the Oracle Java Tutorial.) Here's the outline of our Root class:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | public class Root implements Runnable { /** The application frame. */ private JFrame frame = null; /** The window that we will be drawing on. */ private JPanel userPanel = null; public Root( JPanel userPanel ) { this.userPanel = userPanel; } public void start() { // ... } // required by "implements Runnable" public void run() { // ... } } |
The constructor is used to identify the main portion of your application; for us that's an instance of Canvas. The run method is where the GUI is laid out, and the start method causes the run method to be invoked, resulting in the frame being displayed. The reason we need a start method and a run method is because the start method spawns a new thread, and the first thing that's executed in the new thread is the run method. (A thorough knowledge of threads isn't needed for this application, and a discussion of them is beyond the scope of this tutorial. For more information, see Concurrency, in the Oracle Java Tutorial.) There are a couple of class methods in the SwingUtilities class that can be used to start this thread; we will be using SwingUtilities.invokeLater. Here is the start method in its entirety:
1 2 3 4 | public void start()
{
SwingUtilities.invokeLater( this );
}
|
In terms of configuring the GUI, the run method is where the action is. Strictly speaking it isn't necessary for you to have a thorough understanding of the code; you can just copy it into your application. Nevertheless, here's some detail about the operation.
These are the tasks that must be performed by the run method:
- Create a JFrame. The constructor we use will also specify a title to display in the frame's title bar.
- Set a default close operation on the JFrame. Our default close operation will say "on close, exit from the application." This is important; if you skip this step when you close your frame (by picking the close button in the title bar) the frame will disappear, but your application will continue to run!
- Create a content pane (we'll use a JPanel for this).
- Set a layout manager on the content pane. Layout managers are used to fine tune the position and size of components (push buttons, labels, text boxes et al.) in a window. A FlowLayout arranges components from left to right and top to bottom, as space allows.A GridLayout places components in rows and columns, like a spreadsheet. We'll be using a BorderLayout, which divides the pane into five parts called center, North, South, East and West. To begin with we'll be using only the center part to display your Canvas. Later we might use the other parts to introduce Swing components such as push buttons and text boxes to control the application. See also A Visual Guide to Layout Managers, in the Oracle Java Tutorial.
- Layout the GUI components, i.e., establish their positions and sizes.
- Make the frame visible.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 | /** * Required by the Runnable interface. * This method is the place where the initial content * of the frame must be configured. */ public void run() { /* Instantiate the frame. */ frame = new JFrame( "Graphics Frame" ); /* * This will cause your application to be terminated * when the frame is closed. If you forget this step, * when you close the frame it will disappear, * but your application will continue to run. */ frame.setDefaultCloseOperation( JFrame.EXIT_ON_CLOSE ); /* * A layout manager is responsible for fine-tuning the * layout of a panel. For now you should consider this * to be boilerplate for our application. To learn more * about layout managers see the Oracle tutorial at * https://docs.oracle.com/javase/tutorial/uiswing/layout/index.html. * see the JDK documentation. */ BorderLayout layout = new BorderLayout(); contentPane = new JPanel( layout ); /* Make the Canvas a child of the content pane. */ contentPane.add( userPanel, BorderLayout.CENTER ); /* Set the content pane in the frame. */ frame.setContentPane( contentPane ); /* Initiate frame sizing, positioning etc. */ frame.pack(); /* Make the frame visible. */ frame.setVisible( true ); } |
3. Canvas.paintComponent(Graphics graphics): Boilerplate
Every time your window needs to be redrawn Java calls the paintComponent method in the JPanel class. Recall that Canvas is a subclass of JPanel. To draw your graphics you want to override the paintComponent method. When invoking paintComponent, Java passes you a graphics context. This is a crucial element of your application. You use it to paint things with color, to fill and draw the edges of geometric figures, to draw text in a variety of fonts and many other graphical operations. A couple of important words about the graphics context:
- In the beginning, the type if the graphics context was class Graphics. Bazillions of programmers across the known universe wrote paintComponent methods like this: public paintComponent(Graphics graphics). Time went on and the rulers of Java decided that they needed something more advanced than the original Graphics class, so they invented the Graphics2D class... but what to do about all the millions of lines of code that declared paintComponent as having a parameter of type Graphics? The solution was to make Graphics2D a subclass of Graphics, pass the Graphics2D object to paintComponent, and tell everybody that it's really a Graphics2D object, and they can treat it as such if they want the more advanced functionality. So in many modern paintComponent methods you will see the graphics parameter cast to type Graphics2D: (Graphics2D)graphics.
- When paintComponent is done processing, it must make sure that the original graphics object is unchanged. This means, for example, if you change the color in the graphics context you must restore the original color when your done.
- At the start of paintComponent, save a copy of the properties you're planning to change, then put the back at the end:Color saveColor = graphics.getColor();
Font saveFont = graphics.getFont();
// ...
graphics.setColor( saveColor );graphics.setFont( saveFont ); - The strategy we'll use is to make a copy of the graphics context, then make changes to the copy, so that the original remains unchanged. If we do that, it's a good idea to dispose the copy before paintComponent exits. This operation technically is not necessary, but if facilitates the garbage collection process (see the Graphics.dispose method in the Java documentation):Graphics2D gtx = (Graphics2D)graphics.create();
// ...
gtx.dispose();
- Call the paintComponent method in the superclass.
- Make a copy of the graphics context; to facilitate the use of helper methods, I like to put this in an instance variable.
- Paint the window with your favorite background color (see below for a brief discussion of colors); in my sample application, I will store the background color in an instance variable. Note that the user may resize the window while you're not looking. To effectively paint the window, you will need to know its current width and height; fortunately you can get these values from the JPanel superclass. Once again, to facilitate the development of helper methods, I like to put these in instance variables.
- YOUR BRILLIANT GRAPHICS GO HERE!
- Dispose the copy of the graphics context.
Here's a first look at our paintComponent method:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 | import java.awt.Color; import java.awt.Dimension; import java.awt.Graphics; import java.awt.Graphics2D; import javax.swing.JPanel; @SuppressWarnings("serial") public class Canvas extends JPanel { private final Color bgColor = new Color( .9f, .9f, .9f ); private int currWidth; private int currHeight; private Graphics2D gtx; public Canvas( int width, int height ) { Dimension dim = new Dimension( width, height ); setPreferredSize( dim ); } /** * This method is where you do all your drawing. * Note the the window must be COMPLETELY redrawn * every time this method is called; * Java does not remember anything you previously drew. * * @param graphics Graphics context, for doing all drawing. */ @Override public void paintComponent( Graphics graphics ) { // begin boilerplate super.paintComponent( graphics ); currWidth = getWidth(); currHeight = getHeight(); gtx = (Graphics2D)graphics.create(); gtx.setColor( bgColor ); gtx.fillRect( 0, 0, currWidth, currHeight ); // end boilerplate // MAGIC GOES HERE // begin boilerplate gtx.dispose(); // end boilerplate } } |
4. A Primer on Color
Color is a fascinating and potentially complicated topic. For our application we can limit ourselves to the basics.
A color on your display is a combination of red, green and blue values. Each value can be considered somewhere between all the way on and all the way off. Your computer stores each color component in an 8-bit byte. The range of values that can be stored in 8 bits is 0 to 255, so a value of all the way on is equivalent to 255, and all the way off is 0. The Java color class has a constructor that takes three integer values, for the red, green and blue components, respectively. Here are some examples of its use:
new Color( 255, 0, 0 ) // red
new Color( 0, 0, 255 ) // blue
new Color( 255, 182, 193 ) // pink
In Java, it is common to represent integers in hexadecimal format, for example:
Decimal Hexadecimal
0 0x00
255 0xFF
128 0x80
So the above colors can be constructed as:
If you use hexadecimal, it's fairly easy to fit all three values into a single 32-bit integer; for our purposes, the red, green and blue values go in the low-order three bytes and the fourth byte is unused. The color class has a constructor that takes a single integer, incorporating all three components, for example:
If you've ever specified colors for a web site, you have probably seen colors such as
new Color( 0xFF, 0x00, 0x00 ) // red
new Color( 0x00, 0x00, 0xFF ) // blue
new Color( 0xFF, 0xB6, 0xC1 ) // pink
If you use hexadecimal, it's fairly easy to fit all three values into a single 32-bit integer; for our purposes, the red, green and blue values go in the low-order three bytes and the fourth byte is unused. The color class has a constructor that takes a single integer, incorporating all three components, for example:
new Color( 0xFF0000 ) // red
new Color( 0x0000FF ) // blue
new Color( 0xFFB6C1 ) // pink
If you've ever specified colors for a web site, you have probably seen colors such as
#52be80; the numbers following the # are the red, green and blue values for a color, formatted in hexadecimal. To choose a color for your graphics, you can peruse the many web pages giving hexadecimal values for colors, and easily convert them to a Java color, for example:
#CD5C5C new Color( 0xCD5C5C ) // IndianRed
#F08080 new Color( 0xF08080 ) // LightCoral
#FA8072 new Color( 0xFA8072 ) // Salmon
RGB values can also be represented as single-precision floating point values, where 0.0 is all the way off and 1.0 is all the way on:
Note that if all three RGB components are the same, you get a shade of gray, which gives you, for example:
Finally, the Color class has some common colors preformatted as constant variables, such as:
new Color( 0.0f, 0.0f, 0.0f ) // red
new Color( 0.0f, 0.0f, 1.0f ) // blue
new Color( 1.0f, 0.7f, 0.8f ) // pink
Note that if all three RGB components are the same, you get a shade of gray, which gives you, for example:
new Color( 0.00f, 0.00f, 0.00f ) // black
new Color( 0.25f, 0.25f, 0.25f ) // dark gray
new Color( 0.50f, 0.50f, 0.50f ) // medium gray
new Color( 0.75f, 0.75f, 0.75f ) // light gray
new Color( 1.00f, 1.00f, 1.00f ) // white
Finally, the Color class has some common colors preformatted as constant variables, such as:
Color.BLACK
Color.RED
Color.GREEN
Color.PINK
For additional discussion of colors, see the documentation for the Color class, and the W3Schools Color Tutorial.
5. Jumping Ahead: the Main Class
Before finishing the paintComponent method, let's make sure what we have so far works. To this end we'll write a Main class that contains the main method that will launch our application. It's simple enough; all we have to do is instantiate Canvas and Root objects, then call the start method in the Root object. We'll give the Canvas a width of 400 pixels, and a height of 500 pixels:
1 2 3 4 5 6 7 8 9 | public class Main { public static void main(String[] args) { Canvas canvas = new Canvas( 400, 500 ); Root root = new Root( canvas ); root.start(); } } |
If you execute the above code, you should see the figure at the right.
6. Finishing the paintComponent Method
To finish our first graphics application, we have to center a rectangle in the Canvas window, then fill it and draw its edge. The width and height of the rectangle will be 60% of the width and height of the window:
int rectWidth = (int)(currWidth * .6);
int rectHeight = (int)(currHeight * .6);
int rectHeight = (int)(currHeight * .6);
We'll also need edge and fill colors, and an edge width. Rather thank hard-coding them, let's make these values instance variables:
Next: Java Graphics Tools
private final Color bgColor = new Color( .9f, .9f, .9f );
private final Color fillColor = Color.BLUE;
private final Color edgeColor = Color.BLACK;
private final int edgeWidth = 3;
private final Color fillColor = Color.BLUE;
private final Color edgeColor = Color.BLACK;
private final int edgeWidth = 3;
Next we need to find the coordinates of the upper-left corner of the rectangle. To position it horizontally we need to find the difference between the width of the window and the width of the rectangle, then allocate half of it to the left of the rectangle. To position it vertically, we do the same with the heights of the window and rectangle:
int rectXco = (currWidth - rectWidth) / 2;
int rectYco = (currHeight - rectHeight) / 2;
int rectYco = (currHeight - rectHeight) / 2;
In order to draw the edge of the rectangle, we'll need a Stroke object. Stroke is an interface type, so it can't be instantiated directly; we'll need to instantiate a type that implements Stroke. For us, that's BasicStroke. After creating the stroke, it has to be set in the graphics context:
Stroke stroke = new BasicStroke( edgeWidth );
gtx.setStroke( stroke );
Here is our completed paint component method.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 | /** * This method is where you do all your drawing. * Note the the window must be COMPLETELY redrawn * every time this method is called; * Java does not remember anything you previously drew. * * @param graphics Graphics context, for doing all drawing. */ @Override public void paintComponent( Graphics graphics ) { // begin boilerplate super.paintComponent( graphics ); currWidth = getWidth(); currHeight = getHeight(); gtx = (Graphics2D)graphics.create(); gtx.setColor( bgColor ); gtx.fillRect( 0, 0, currWidth, currHeight ); // end boilerplate // Fill and draw a rectangle that is 60% // the current width and height of this window. int rectWidth = (int)(currWidth * .6); int rectHeight = (int)(currHeight * .6); // To center the rectangle horizontally in the window, find // the difference between rectWidth and currWidth, // then allocate half of it to the left of the rectangle. // Similarly, center the rectangle vertically using the // rectHeight and currHeight. int rectXco = (currWidth - rectWidth) / 2; int rectYco = (currHeight - rectHeight) / 2; // To draw the edge of the rectangle you'll need a // Stroke object, which determines the edge width. Stroke stroke = new BasicStroke( edgeWidth ); // Fill the rectangle before drawing the edge. gtx.setColor( fillColor ); gtx.fillRect( rectXco, rectYco, rectWidth, rectHeight ); gtx.setStroke( stroke ); gtx.setColor( edgeColor ); gtx.drawRect( rectXco, rectYco, rectWidth, rectHeight ); // begin boilerplate gtx.dispose(); // end boilerplate } |
No comments:
Post a Comment