|
Welcome to the Core Java Technologies Tech Tips for June 11, 2004. Here you'll get tips on using core Java technologies and APIs, such as those in Java 2 Platform, Standard Edition (J2SE).
This issue covers:
More Multithreading in Swing
Printing Components with PrinterJob
These tips were developed using Java 2 SDK, Standard Edition, v 1.4.2.
This issue of the Core Java Technologies Tech Tips is written by Daniel H. Steinberg, Director of Java Offerings for Dim Sum Thinking, Inc, and editor-in-chief for java.net.
See the Subscribe/Unsubscribe note at the end of this newsletter to subscribe to Tech Tips that focus on technologies and products in other Java platforms.
For more Java technology content, visit these sites:
java.sun.com - The latest Java platform releases, tutorials, and
newsletters.
java.net - A web forum for collaborating and building solutions
together.
java.com - The marketplace for Java technology, applications and
services.
MORE MULTITHREADING IN SWING
The December 8, 2003 Tech Tip, Multithreading in Swing, advised when you might use invokeLater() to produce thread-safe code. The following tip complements that earlier tip. The tip starts with an example that introduces the threading problem -- a continuous request for the event thread. You'll see how inserting a call to invokeLater() in this example does not help. Instead, you will use javax.swing.Timer to spawn a separate thread to create a responsive application.
Let's begin with the following example. The example illustrates a program that continues to execute even though its GUI is non-responsive and does not reflect changes in state. The program includes a JFrame that contains a JButton and a JTextField. The JButton has the following ActionListener:
class CountListener implements ActionListener {
int count = 0;
boolean isRunning = false;
public void actionPerformed(ActionEvent e) {
while (isRunning) {
System.out.println(" " + count++);
display.setText(" " + count);
}
display.setText("press button again");
isRunning = !isRunning;
}
}
The JButton and JTextField are appropriately wired to the JButton's ActionListener. To prove that, the String displayed in the JTextField changes to "press button again" the first time a user clicks the JButton.
The second time the user clicks the JButton, and any time after that, the count should be incremented and displayed in both the standard out and in the JTextField. This should continue until the JButton is pressed again. Unfortunately, that is not what happens. When you run the program below, BadThreadEx, you will see the count rise quickly in the console, but the JTextField is not updated. You can't stop the count by clicking the JButton again because the Swing event thread is blocked from receiving any additional button clicks. Notice that even though the program sets the default close operation to EXIT_ON_CLOSE, you are not able to quit the program by closing the JFrame. Instead you need to kill the process from within your console window or from within your IDE.
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.JButton;
import javax.swing.JTextField;
import java.awt.event.ActionListener;
import java.awt.event.ActionEvent;
public class BadThreadEx extends JFrame {
private JTextField display;
public BadThreadEx() {
super("Example of Bad Threading");
JPanel aPanel = new JPanel();
display = new JTextField
("nothing happened yet");
JButton button = new JButton
("Start/Stop Counting");
button.addActionListener(new CountListener());
getContentPane().add(aPanel);
aPanel.add(button);
aPanel.add(display);
pack();
setVisible(true);
setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
}
class CountListener implements ActionListener {
int count = 0;
boolean isRunning = true;
public void actionPerformed(ActionEvent e) {
isRunning = !isRunning;
while (isRunning) {
System.out.println(" " + count++);
display.setText(" " + count);
}
display.setText("press button again");
}
}
public static void main(String[] args) {
new BadThreadEx();
}
}
If you follow the December 2003 tip you might try to solve this problem by using the invokeLater() method -- either from the SwingUtilities class or from the EventQueue class. The following example shows that this will not help in the current case.
Create an inner class named Counter that implements Runnable. The call to the JTextField's setText() method is contained in this inner class's run() method. To track how many times run() is called, the class includes a local counter. The count is also displayed in standard out. To help you distinguish the local count from the global count, the local numbers are preceded by ==>.
class Counter implements Runnable {
int localCount = 0;
public void run() {
System.out.println("==>" + ++localCount);
display.setText(" " + count);
}
}
In the following program, BadInvokeLaterEx, you can see that the invokeLater() method is called from inside of the CountListener's actionPerformed() method.
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.JButton;
import javax.swing.JTextField;
import javax.swing.SwingUtilities;
import java.awt.event.ActionListener;
import java.awt.event.ActionEvent;
public class BadInvokeLaterEx extends JFrame {
private JTextField display;
private int count;
public BadInvokeLaterEx() {
super("Example of Bad invokeLater()");
JPanel aPanel = new JPanel();
display = new JTextField
("nothing happened yet");
JButton button = new JButton
("Start/Stop Counting");
button.addActionListener(new CountListener());
getContentPane().add(aPanel);
aPanel.add(button);
aPanel.add(display);
pack();
setVisible(true);
setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
}
class CountListener implements ActionListener {
boolean isRunning = true;
Runnable counter = new Counter();
public void actionPerformed(ActionEvent e) {
isRunning = !isRunning;
while(isRunning){
//while (count < 5) {
System.out.println(" " + ++count);
//this use of invokeLater() will not help
SwingUtilities.invokeLater(counter);
}
display.setText("press button again");
}
}
class Counter implements Runnable {
int localCount = 0;
public void run() {
System.out.println("==>" + ++localCount);
display.setText(" " + count);
}
}
public static void main(String[] args) {
new BadInvokeLaterEx();
}
}
Compile and run BadInvokeLaterEx. When you run the program it appears to perform worse than the earlier program, BadThreadEx. In BadThreadEx, the numbers appear in your console window, but not in the JTextField. With BadInvokeLaterEx, the numbers still appear in the console and not in the JTextField. In fact, you don't get a visual confirmation in standard out that the run() method has executed. Notice that none of the numbers appear with "==>" on their left. The numbers from the global count that appear in the console do not seem to be progressing as smoothly as before.
The problem is that you are already on the event thread when you make a call to invokeLater(). To get a clearer idea of what is happening, comment out the line:
while(isRunning){
and uncomment the line:
//while (count < 5) {
Then recompile and rerun BadInvokeLaterEx. When you click the button this time, you should see the following output in the console:
1
2
3
4
5
==>1
==>2
==>3
==>4
==>5
The output indicates that the while loop containing the following lines:
System.out.println(" " + ++count);
SwingUtilities.invokeLater(counter);
has executed five times. You can see that run() has been called five times by invokeLater() -- this is indicated by the presence of "==>1" through "==>5". This isn't executed until the initial count has completed. You can replace the number 5 in the line:
while (count < 5)
with larger numbers if you suspect that there has not been enough time to view the results of the call to invokeLater().
The continued action of counting is blocking the invokeLater(), which is being called on the same thread. Only one event can be processed at a time on the event dispatching thread. Clicking a button translates into generating an event on the event dispatching thread. When the event dispatching thread finishes processing all its current events it then processes the button click event. SwingUtilities.invokeLater() places a special event on the event dispatch thread that when processed, invokes run() on the Runnable object. This is why you the see global number first increase, then the local numbers.
Below is a program, TimerEx, that presents the third version of the counting example. In this program, you again create an ActionListener that updates the JTextField in its actionPerformed() method:
class CountListener implements ActionListener {
int count = 0;
public void actionPerformed(ActionEvent e) {
System.out.println(" " + count++);
display.setText(" " + count);
}
}
You create a Timer object and pass it a handle to this ActionListener that it will notify.
Timer timer = new Timer(2, new CountListener());
Finally, you create a mechanism for starting and stopping the counting when the JButton is pressed. You do this by implementing an ActionListener with the following actionPerformed() method.
public void actionPerformed(ActionEvent e) {
if (timer.isRunning()) timer.stop();
else timer.start();
}
When you run the TimerEx program, you will see the counting in both the console window and in the JTextField of your application.
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.JButton;
import javax.swing.JTextField;
import javax.swing.Timer;
import java.awt.event.ActionListener;
import java.awt.event.ActionEvent;
public class TimerEx extends JFrame {
private JTextField display;
public TimerEx() {
super("Introduce a Timer");
JPanel aPanel = new JPanel();
display = new JTextField("nothing happened yet");
JButton button =
new JButton("Start/Stop Counting");
button.addActionListener(new CountRunner());
getContentPane().add(aPanel);
aPanel.add(button);
aPanel.add(display);
pack();
setVisible(true);
setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
}
class CountRunner implements ActionListener {
Timer timer = new Timer(2, new CountListener());
public void actionPerformed(ActionEvent e) {
if (timer.isRunning()) timer.stop();
else timer.start();
}
}
class CountListener implements ActionListener {
int count = 0;
public void actionPerformed(ActionEvent e) {
System.out.println(" " + count++);
display.setText(" " + count);
}
}
public static void main(String[] args) {
new TimerEx();
}
}
In most applications you do not have a situation where a single thread is continuously occupied by an operation, such as the counting loop demonstrated here. In the typical cases, the use of invokeLater() is recommended. However in the special cases as shown in this tip, you need to create a separate thread. You can use the custom SwingWorker class to create the thread. The class is available as a separate download from the Swing Trail of the Java Tutorial. The solution presented in this tip takes advantage of the standard Timer class. This class provides all the functionality you need to set up a thread on which the updates to the JTextField are called.
If you're attending the 2004 JavaOne Conference here are some technical sessions and labs that cover multithreading-related topics:
| TS-1358: |
|
Concurrency Utilities in JDK Software Version 1.5: Multithreading Made Simpler |
| TS-2331: |
|
The New Java Technology Memory Model |
| TS-2853: |
|
Desktop Application Architecture II: Using Threads Correctly and Effectively |
| 7212: |
|
Desktop Client Performance |
PRINTING COMPONENTS WITH PRINTERJOB
The printing capabilities provided by J2SE have been enhanced in almost every major release. Each release introduced additional functionality and flexibility. One of the classes that provides this added functionality and flexibility is PrinterJob. In this tip you will use PrinterJob to print a GUI component. Because components are what make up the UI of your application, this is how you can enable a user to print exactly what they see on the screen. You will be able to set attributes and specify PageFormat options for printing.
To begin, you need a component to print. Here is a component that you can use. It is taken from a previous Tech Tip. The component is a JFrame with three buttons. Each button contains a label with rendered HTML.
import javax.swing.JFrame;
import javax.swing.JButton;
import java.awt.GridLayout;
public class PrintEx extends JFrame {
PrintEx() {
setSize(300, 200);
getContentPane().setLayout
(new GridLayout(1, 3));
getContentPane().add(new JButton(
"<html><h1>1<sup>st</sup></h1>" +
"<hr> <p color=blue> Use Blue</p></html>"));
getContentPane().add(new JButton(
"<html><h1>2<sup>nd</sup></h1>" +
"<hr> <p color=red>Use <b>Red</b></p></html>"));
getContentPane().add(new JButton(
"<html><h1>3<sup>rd</sup></h1><hr>" +
"<p color=green> Use <u>Green</u></p></html>"));
setVisible(true);
}
}
Now that you have a component to print, you need to implement the java.awt.print.Printable interface to print it. This requires you to provide a print() method with the following signature:
public int print(Graphics g, PageFormat format,
int pageIndex)
The int that must be returned is either Printable.PAGE_EXISTS (which indicates that a page exists and has been rendered), or Printable.NO_SUCH_PAGE (which indicates that the pageIndex refers to a page which does not exist, that is, it refers to more pages than are in the printout). Returning this value will terminate the print job. In this example you will print a single page. Also, because the pageIndex starts at 0, your print() method will return PAGE_EXISTS only for pageIndex==0 and NO_SUCH_PAGE for pageIndex > 0.
The argument is the Graphics context that you pass to the paint() method like this:
someComponent.paint(g);
In the following example, OffPagePrint, the PageFormat object is not explicitly used. You will use it in a later example to ensure that the component is painted entirely within the page boundaries.
You do not directly call the print() method in your Printable object. If you examine OffPagePrint, notice that inside the printIt() method, you first create an instance of PrinterJob, associate it with the Printable object using the setPrintable() method, optionally use a print dialog to set the target printer, and then call the PrinterJob.print() method. The printing system then calls your Printable.print() method repeatedly until the job is completed.
import java.awt.print.PrinterJob;
import java.awt.print.PageFormat;
import java.awt.print.Printable;
import java.awt.print.PrinterException;
import java.awt.Graphics;
class OffPagePrint implements Printable {
public void printIt() {
PrinterJob job = PrinterJob.getPrinterJob();
job.setPrintable(this);
if (job.printDialog()) {
try {
job.print();
} catch (PrinterException e) {
e.printStackTrace();
}
}
}
public int print(Graphics g, PageFormat format,
int pagenum) {
if (pagenum > 0) return Printable.NO_SUCH_PAGE;
(new PrintEx()).paint(g);
return Printable.PAGE_EXISTS;
}
public static void main(String[] args) {
new OffPagePrint().printIt();
}
}
Compile and run OffPagePrint and choose to print to a printer or to save to a file. In either case, you will find that only the lower right portion of the component is rendered.
What's going on here? Paper comes in sizes such as Letter and A4. Also a job might specify that it uses that paper in a different orientation such as landscape. Finally, printers cannot typically print on the entire surface of the paper. There's a margin around the edges that is used for gripping the paper. In many cases, the printing convention is to leave a large margin. The area within the margin is known as the printable area or the imageable area. The printing API specifies that (0,0) of the Graphics parameter refers to the top-left of the paper. You now know this can be outside the imageable area. This explains why only part of the component was visible in the previous example. So you need to find the origin of the imageable area and translate to that location. To do this, use the PageFormat class. In fact, you need to use PageFormat for correct printing.
You can use PageFormat methods to set and obtain information about the page being printed. In the OffPagePrint example, the component was printed with an origin at the top left of the actual page. You can get the x and y coordinates of the upper-left point of the area of the page that is available to be painted on. For the x coordinate, use getImageableX(). For the y coordinate, use getImageableY(). Other PageFormat methods, getImageableWidth() and getImageable(Height), give you information about the available area on the page. By comparison, the methods getHeight() and getWidth() return the height and width of the page including the margins that cannot be drawn on.
Using getImageableX() and getImageableY(), you can move the component you are drawing over and down on the page:
g.translate((int) format.getImageableX(),
(int) format.getImageableY());
The getImageableX() and getImageableY() methods return doubles. So you either have to cast the doubles to ints or cast your Graphics object g to type Graphics2D. The result is that top-left corner of the component appears in the top left corner of the imageable area of the page.
In addition, as shown in the program below, OnPagePrint, you can set the orientation of the page as part of setting up the print job. You do this using the PageFormat constants LANDSCAPE, PORTRAIT, or REVERSE_LANDSCAPE:
format.setOrientation(PageFormat.LANDSCAPE);
The only remaining fix for the printing problem is to pass in both the Printable object and the corresponding PageFormat as arguments to setPrintable().
import java.awt.print.PrinterJob;
import java.awt.print.PageFormat;
import java.awt.print.Printable;
import java.awt.print.PrinterException;
import java.awt.Graphics;
import java.awt.Component;
class OnPagePrint implements Printable {
private Component c = new PrintEx();
public void printIt() {
PrinterJob job = PrinterJob.getPrinterJob();
PageFormat format = job.defaultPage();
format.setOrientation(PageFormat.LANDSCAPE);
job.setPrintable(this, format);
if (job.printDialog()) {
try {
job.print();
} catch (PrinterException e) {
e.printStackTrace();
}
}
}
public int print(Graphics g, PageFormat format,
int pagenum) {
if (pagenum > 0) return Printable.NO_SUCH_PAGE;
g.translate((int) format.getImageableX(),
(int) format.getImageableY());
c.paint(g);
return Printable.PAGE_EXISTS;
}
public static void main(String[] args) {
new OnPagePrint().printIt();
}
}
Compile and run OnPagePrint. You'll see that the entire component is rendered on the page. You'll also notice that the page is oriented in Landscape mode so that it is wider than it is tall. This more closely matches the dimensions of the component that you are trying to print.
The next equation contains a simpler calculation used to center the component. For example, to center the component on the page horizontally you can use the following method:
private int calculateOffsetX(PageFormat format) {
return (int) (format.getImageableX() +
(format.getImageableWidth() - c.getWidth())/2);
}
However this works only if the component is smaller than the imageable area. If you need to print a component that is too large to fit on a page, you could scale it by casting the Graphics object to a Graphics2D object and using the scale() method.
The next example, CenteredPrint, makes another change. It uses javax.print.attribute.PrintRequestAttributeSet. You can configure a print job using classes provided in the javax.print.attribute package that was added in J2SE 1.4, and add attribute support to PrinterJob. For example, you can use the class OrientationRequested to set the orientation to LANDSCAPE. The CenteredPrint example demonstrates the use of a PrintRequestAttributeSet and of centering the component on the page.
import javax.print.attribute.PrintRequestAttributeSet;
import javax.print.attribute.HashPrintRequestAttributeSet;
import javax.print.attribute.standard.OrientationRequested;
import java.awt.print.PrinterJob;
import java.awt.print.PageFormat;
import java.awt.print.Printable;
import java.awt.print.PrinterException;
import java.awt.Graphics;
import java.awt.Component;
class CenteredPrint implements Printable {
private Component c = new PrintEx();
public void printIt() {
PrinterJob job = PrinterJob.getPrinterJob();
job.setPrintable(this, job.defaultPage());
// createAttributes initializes orientation
// in print dialog to LANDSCAPE
PrintRequestAttributeSet pras = createAttributes();
if (job.printDialog(pras)) {
try {
job.print(pras);
} catch (PrinterException e) {
e.printStackTrace();
}
}
}
private PrintRequestAttributeSet
createAttributes() {
PrintRequestAttributeSet atts =
new HashPrintRequestAttributeSet();
atts.add(OrientationRequested.LANDSCAPE);
return atts;
}
private int calculateOffsetX(PageFormat format) {
return (int) (format.getImageableX() +
(format.getImageableWidth() -
c.getWidth())/2);
}
private int calculateOffsetY(PageFormat format) {
return (int) (format.getImageableY() +
(format.getImageableHeight() -
c.getHeight())/2);
}
public int print(Graphics g, PageFormat format,
int pagenum) {
if (pagenum > 0) return Printable.NO_SUCH_PAGE;
g.translate(calculateOffsetX(format),
calculateOffsetY(format));
c.paint(g);
return Printable.PAGE_EXISTS;
}
public static void main(String[] args) {
new CenteredPrint().printIt();
}
}
The CenteredPrint program prints the component centered in the middle of a page that uses the landscape orientation.
For more information about printing components, see Printing the Contents of a Component in the 2D Graphics trail of the Java Tutorial.
OTHER RESOURCES
Here are some additional resources for Java technology information:
Chats:
June 15. 11:00 A.M. PDT/6:00 P.M. GMT. Sun Java Desktop System
June 24. 10:00 A.M. PDT/5:00 P.M. GMT. Accelerate Productivity with Sun Java Web Application Framework
|
|
 |
 |
|
|
 |
 |
IMPORTANT: Please read our Licensing, Terms of Use, and Privacy policies:
http://developer.java.sun.com/berkeley_license.html
http://www.sun.com/share/text/termsofuse.html
Privacy Statement: Sun respects your online time and privacy (http://sun.com/privacy). You have received this based on your email preferences. If you would prefer not to receive this information, please follow the steps at the bottom of this message to unsubscribe.
Comments? Send your feedback on the Core Java Technologies Tech Tips to: http://developers.sun.com/contact/feedback.jsp?category=newslet
Subscribe to other Java developer Tech Tips:
- Enterprise Java Technologies Tech Tips. Get tips on using enterprise Java technologies and APIs, such as those in the Java 2 Platform, Enterprise Edition (J2EE).
- Wireless Developer Tech Tips. Get tips on using wireless Java technologies and APIs, such as those in the Java 2 Platform, Micro Edition (J2ME).
To subscribe to these and other Sun Developer Network publications:
- Go to the Sun Developer Network Subscriptions page, choose the newsletters you want to subscribe to and click
"Update".
- To unsubscribe, go to the subscriptions page, uncheck the appropriate checkbox, and click "Update".
ARCHIVES: You'll find the Core Java Technologies Tech Tips archives at:
http://java.sun.com/developer/JDCTechTips/index.html
Copyright 2004 Sun Microsystems, Inc. All rights reserved.
4150 Network Circle, Santa Clara, CA 95054 USA.
This document is protected by copyright. For more information, see:
http://java.sun.com/developer/copyright.html
Java, J2SE, J2EE, J2ME, and all Java-based marks are trademarks or registered trademarks (http://www.sun.com/suntrademarks/) of Sun Microsystems, Inc. in the United States and other countries.
|