|
|||||
Recent ArticlesClojure is a relatively new language to appear on the Java Virtual Machine (JVM), although it draws on very mature roots in the form of the LISP langu ... Should You Care About Requirements Engineering? Recently, I (Adil) was invited to participate in a one day seminar on the subject of Requirements Engineering. Whilst I have no direct experience of t ... Tips for Setting Up Your First Business Website To attract all potential customers to your business you need a presence on the web. The problem is that if you haven't set up a website before, you p ... LISP is a general-purpose programming language and is the second-oldest programming language still in use, but how much do you know about it? Did you ... Open Source Tools for Developers: Why They Matter From a developer's point of view use of open-source tools has advantages beyond the obvious economic ones. With the open-source database MySQL in mind ... |
Displaying a Busy Cursor in JavaIntroductionAlmost every application program contains critical points in the workflow where there is a noticeable delay while the computer performs some processing or accesses some information stored elsewhere on the network. Unless there is a visual indication that the program is still working, the user may think that it has crashed. In any case, a "polite" program would communicate that it is servicing the request, but that it may take a little time. The usual metaphor for this in graphical applications is to display a busy cursor, also known as an hourglass (so-called because the default Windows icon is in the shape of a traditional sand-filled hour glass timer). This article discusses how to display the busy cursor in Java applications and what patterns to employ to ensure concise code and a consistent, bug-free application. The BasicsMost guides and books will tell you that it is very easy to change the cursor in a Java application. For example, the following fragment of code sets the cursor to a busy cursor while it performs some processing, and then returns the cursor to the default cursor again afterwards. import java.awt.Cursor; ... // Setting cursor for any Component: component.setCursor(Cursor.getPredefinedCursor(Cursor.WAIT_CURSOR)); doProcessing(); component.setCursor(Cursor.getDefaultCursor()); ... This is a good start but, somewhat surprisingly, this short code fragment contains a bug. The problem is that the main processing method might throw an exception. If the exception is not caught within the processing method, then the Java Virtual Machine unwinds the call stack repeatedly until it finds a Restoring the CursorThankfully, there is an easy way out of this. Java's finally clause is guaranteed to be executed even when an exception is thrown and not caught in the current scope. Therefore the following code fragment is guaranteed to restore the cursor to its default, regardless of whether the processing method terminates normally or throws an exception: try { component.setCursor(Cursor.getPredefinedCursor(Cursor.WAIT_CURSOR)); doProcessing(); } finally { component.setCursor(Cursor.getDefaultCursor()); } This is much better, but there remains a problem in that we have to remember to use the same idiom whenever we want to control the cursor. We are also mixing the cursor control code with the main 'business logic' of the processing. Wouldn't it be great if the cursor control could be factored out as a separate concern? A Cursor Controller ClassI am now going to assume that the occasions when we would wish to display the busy cursor are all instigated by a user action. Therefore, our Java application will already have a This approach is implemented by the following class, which has a single public (and static) method. import java.awt.Component; import java.awt.event.ActionListener; import java.awt.event.ActionEvent; public final class CursorController { public final static Cursor busyCursor = Cursor.getPredefinedCursor(Cursor.WAIT_CURSOR); public final static Cursor defaultCursor = Cursor.getDefaultCursor(); private CursorController() {} public static ActionListener createListener(final Component component, final ActionListener mainActionListener) { ActionListener actionListener = new ActionListener() { public void actionPerformed(ActionEvent ae) { try { component.setCursor(busyCursor); mainActionListener.actionPerformed(ae); } finally { component.setCursor(defaultCursor); } } }; return actionListener; } } The idea is that in your main GUI code, you would write something like the following: class MyApplication extends JFrame { ... JButton button = new JButton("Do It!"); ActionListener doIt = new ActionListener() { public void actionPerformed(ActionEvent ae) { doProcessing(); } }); ActionListener cursorDoIt = CursorController.createListener(this, doIt); button.addActionListener(cursorDoIt); ... } This may seem like a lot of effort, but it will save effort in the long run as all the control for the busy cursor is contained in one place. That means, for example, it is easy to add some code to time the execution of all actions involving the busy cursor and to send those timings to a log file. With this approach I need only write this additional handling code once in the source code, instead of having to add it to all the places in the code where intensive action processing is performed. A Delayed Busy CursorAnother obvious improvement is to wait a little while before changing the busy cursor. There are two reasons for doing this. Firstly, we can never be sure how long it will be before an action is processed. An action that is normally processed within milliseconds might be held up by a garbage collection in the Java Virtual Machine, or some other process on the same machine that is deemed to have a higher priority. Therefore it would be a good idea to divert all actions through a busy cursor controller, even if some of them usually execute very quickly. Secondly, if we divert all actions through our controller, we do not want to constantly change the cursor to busy and then back to its default again; at least, not to the point that the cursor appears to be "twitchy" and is irritating for the user. It is therefore a good idea to divert all actions through a cursor controller, but to set a delay period. If the processing has not finished at the end of the delay period, then we display the busy cursor until processing has finished. This approach is sufficient to eliminate "twitchy" behaviour of the cursor and provides for a consistent user-interface with a good level of user feedback. The following is an enhanced version of CursorController that implements that strategy. As before, we supply an ActionListener to the createListener() method, and it returns an ActionListener. The difference is that this time, the ActionListener uses a Timer to wait for a fixed time interval before displaying a busy cursor. If it takes less than half a second (500 milliseconds) to service the user action, then the busy cursor is not displayed. import java.awt.Component; import java.awt.Cursor; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.util.Timer; import java.util.TimerTask; public class CursorController { public static final Cursor busyCursor = new Cursor(Cursor.WAIT_CURSOR); public static final Cursor defaultCursor = new Cursor(Cursor.DEFAULT_CURSOR); public static final int delay = 500; // in milliseconds private CursorController() {} public static ActionListener createListener(final Component component, final ActionListener mainActionListener) { ActionListener actionListener = new ActionListener() { public void actionPerformed(final ActionEvent ae) { TimerTask timerTask = new TimerTask() { public void run() { component.setCursor(busyCursor); } }; Timer timer = new Timer(); try { timer.schedule(timerTask, delay); mainActionListener.actionPerformed(ae); } finally { timer.cancel(); component.setCursor(defaultCursor); } } }; return actionListener; } } The following class is a test program for the CursorController, above. It displays three buttons and associates an action with each of them. For each of them, the action is simply to "sleep" for a fixed time interval. The intervals are set at one third of a second, two thirds of a second, and a whole second. That means that when you press the first button, you notice a slight delay but the busy cursor is not displayed. When you press the second one, the busy cursor is displayed briefly, and when you press the third one, the busy cursor is displayed for half a second. import java.awt.*; import java.awt.event.*; import javax.swing.*; public class CursorTest extends JFrame { JPanel panel = new JPanel(); JButton wait1 = new JButton("Wait 1/3 of a second"); JButton wait2 = new JButton("Wait 2/3 of a second"); JButton wait3 = new JButton("Wait 1 second"); public CursorTest() { setTitle("Busy Cursor Test"); setSize(400,400); setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE); GridLayout layout = new GridLayout(3,1); panel.setLayout(layout); panel.add(wait1); panel.add(wait2); panel.add(wait3); getContentPane().add(panel); ActionListener wait1ActionListener = delayActionListener(333); ActionListener wait2ActionListener = delayActionListener(666); ActionListener wait3ActionListener = delayActionListener(1000); // Add in the busy cursor ActionListener busy1ActionListener = CursorController.createListener(this, wait1ActionListener); ActionListener busy2ActionListener = CursorController.createListener(this, wait2ActionListener); ActionListener busy3ActionListener = CursorController.createListener(this, wait3ActionListener); wait1.addActionListener(busy1ActionListener); wait2.addActionListener(busy2ActionListener); wait3.addActionListener(busy3ActionListener); setVisible(true); } /** * Creates an actionListener that waits for the specified number of milliseconds. */ private ActionListener delayActionListener(final int delay) { ActionListener listener = new ActionListener() { public void actionPerformed(ActionEvent ae) { try { System.out.printf("Waiting for %d milliseconds\n", new Integer(delay)); Thread.sleep(delay); } catch (InterruptedException ie) { ie.printStackTrace(); } } }; return listener; } public static void main(String[] args) { CursorTest cursorTest = new CursorTest(); } } SummaryThis article has shown that there is a lot more to controlling the cursor effectively than simply invoking a I am sure there is more that one could say. For example, in this article I have assumed that there are only two possible states for the cursor: it can either show the busy cursor or the default cursor. In many applications, this is too simple a model, as a cursor might be a resize cursor, a text cursor, or a cross-hair. In such cases, we would need to remember what the cursor was before switching to the busy cursor, and then restore it again afterwards. I have also not said anything about how to deal with a busy cursor in a multi-threaded application. For the sake of simplicity in a swing application, it's probably better to perform this kind of processing all on the same thread, probably by using Simon White |