Home
| pfodApps/pfodDevices
| WebStringTemplates
| Java/J2EE
| Unix
| Torches
| Superannuation
|
| About
Us
|
Java GUI Programming Tips and Guidelines
|
This article is written by a programmer for programmers. As programmers we find users a complete nuence. We work to make our programs 'fool proof' but we can't make them 'user proof' because users do things no fool would ever do. Users are inconsistent, in a hurry, they never read the manual, they always want more features, they don't appreciate the work that we have put in to the program. They want the changes done yesterday. However without them there would be no need to write the program.
Broadly there are two types of computer program with a GUI (graphical user interface), games and applications. Games are meant to be a challenge, they do not explain everything, they are ambiguous, they are exciting. Users are expected to think when using them. Applications on the other hand should not be a challenge, they should be obvious, there should be no ambiguity and users should find it second nature to use them. Unfortunately most computer programs, whether they are games or applications fall between these two ideals. This article will only deal with applications.
This article is a work in progress and currently covers the following topics (not all in the same detail).
There are always errors. The first thing you need to decide when designing a GUI (or any program for that matter) is “Where are the errors going to go to?”
Before considering this lets define three types of errors:-
Your programming errors.
System errors.
User actions.
These include such things as nullpointer exceptions, illegal argument exceptions and logical errors. Hopefully they will all throw exceptions rather than just quietly corrupting the data.
These include network errors, disk full errors, out-of-memory errors, etc. As explained in Error Recovery these errors “should be notified to the user but almost never require aborting the application”.
These are not strictly speaking errors, because under my definitions for designing user interfaces “Users never make mistakes, they just change their minds.” (see below for more on this). However from a programmers' point of view you have to deal with the problems of the user trying to enter data that you don't think is valid or of trying to do an action that is not available just now.
Handling programming and system errors is bound up in Error Recovery but you still have to inform the user that their requested action has not been performed. The simplist and worst way of doing this is to write a message to System.out or System.err. Usually in a GUI program System.out and System.err are not visible to the user at all or are shown in some other terminal window. Being a 'good' programmer you would never do this yourself, but what about the other programmers on the team and the external libraries you are using. Some of Sun's Swing programmers are not that good. So you have to catch System.out and System.err. How to Set up Java Logging has a simple method that redirects System.out and System.err to a log file. If you wish you could modify this to display a pop-up dialog window with the message as well, but I have not found that necessary. Once you have redirection in place, I find it convient to use System.out for temporary debugging messages. This redirection also catches those Swing Event thread exceptions that you missed in your code.
Well if you are not going to just write errors to System.out
(except for logging purposes) where are you going to write them so
the user can see them and take action. I recommend two classes to
solve this problem:- i) InputTipManager
to handle input validation errors on text boxes and table cell
editors and ii) IShowErrors
interface be
implemented in the appropriate window/dialog box.
The IShowErrors
interface has just
two methods
public void showError(String message, Throwable error); public void showMessage(String message);
Typically a window implementing this interface would display a
discrete panel with the message. Such as shown here.
The Alert panel pops up inside the window when showError or
showMessage is called and dissapears when the Continue button or the
Escape key is pressed. The movement of the pop-up draws the user's
attention to the message.
There is another example from a Netbeans dialog box.
I think this is a bit too subtle. The purple writing (“Cannot place the project in ... “) on the dark gray background is hard to see. I sometimes don't realize an error message has been displayed particularly if the dialog is full screen. However this can be easily fixed by changing the colour of the text or background. Perhaps making the background of the error message line change to white on display of an error would make the appearance of the error message more noticable.
IShowErrors
is the basic interface
that is needed for showing errors. You may want to extend it to
handle error codes and status and progress bars.
Not all windows will implement this interface and in some cases you may not know which window is currently displying this component. To handle these cases the following utility methods are useful.
public static void showError(Component current, String message, Throwable error) { Container iShowErrorsContainer = null; if (current instanceof IShowErrors) { iShowErrorsContainer = (Container) current; } else { // find first parent that implements IShowErrors iShowErrorsContainer = SwingUtilities.getAncestorOfClass(IShowErrors.class, current); } if (iShowErrorsContainer != null) { // let the ancestor handle it ((IShowErrors) iShowErrorsContainer).showError(message, error); return; } // else no current component or could not find IShowErrors parent. if (!(current instanceof Frame)) { current = SwingUtilities.getAncestorOfClass(Frame.class,current); } if (error == null) { logger.log(Level.SEVERE, "Error: "+message + "\n"); JOptionPane.showMessageDialog(current, message, "Error", JOptionPane.ERROR_MESSAGE); } else { String errorMsg = message + "\n" + toString(error); logger.log(Level.SEVERE, errorMag, error); JOptionPane.showMessageDialog(current, errorMsg, "Error", JOptionPane.ERROR_MESSAGE); } } public static void showMessage(Component current, String message) { Container iShowErrorsContainer = null; if (current instanceof IShowErrors) { iShowErrorsContainer = (Container) current; } else { // find first parent that implements IShowErrors iShowErrorsContainer = SwingUtilities.getAncestorOfClass(IShowErrors.class, current); } if (iShowErrorsContainer != null) { // let the ancestor handle it ((IShowErrors) iShowErrorsContainer).showMessage(message); return; } // else no current component or could not find IShowErrors parent. if (!(current instanceof Frame)) { current = SwingUtilities.getAncestorOfClass(Frame.class,current); } logger.log(Level.SEVERE, "Message: "+message + "\n"); JOptionPane.showMessageDialog(current, message, "Message", JOptionPane.INFORMATION_MESSAGE); } /** * Return toString of object handling null inputs * if obj is a Throwable returns the stack trace as a String. * * @param obj the Object to convert to a String. * @return "NULL" if obj is null, else * the contents of stack trace if obj is a Throwable else * obj.toString() * If obj.toString() throws an exception then that exception and its stack trace are returned */ public static String toString(Object obj) { if (obj == null) { return "NULL"; } Throwable e; if (obj instanceof Throwable) { e = (Throwable)obj; StringWriter strWriter = new StringWriter(); PrintWriter printWriter = new PrintWriter(strWriter); e.printStackTrace(printWriter); return strWriter.toString(); } //else { try { return obj.toString(); } catch (Throwable t) { return (toString(t)); } }
Note that the above methods also log the error messages as well as
showing them. Your other implementations of IShowErrors
should do this as well.
When
preparing and error message, don't just tell the user here has been
an error, or some invalid input. Tell them what they need to do to
fix it. This means error messages are usually not simple. You have to
analyse what happened and decide what the user needs to do the
continue. For example when validating input.
First get someone involved who is not a programmer. Second don't tell them what is not possible to be programmed. After the user testing has come up with a design then sit down and look at how it can be programmed and suggest changes based on what is possible in the time allowed and then feed it back to user testing.
Don't insist on everything being logical. Users are not completely logical. If they, the users, say something seems better the other way round from what you know is the logically correct way, then let them have it the other way round. This is why you need help. It is hard to be a good programmer and to think illogically as well. I experienced this first hand when designing the user interface for Time:Frame. The logic said the time condition should come before the special functions. However the user design come back with the special functions visually above and inside the time condition.
To build the prototype, first get some idea of what needs to be done. Formal 'use cases' may be appropriate, but at least a paragraph saying what needs to happen. Sketch the screens on paper. Get user feedback. Do the users understand from looking at the screen and reading the description of what needs to happen and what they need to do.
Build the screens to show the user. These can be built in PowerPoint if you are considering using non-standard components or using one of the GUI editors provided in Netbeans (my preference) or Eclipse. The GUI editors will let you quickly get a visual display of what the screen will actually look like. Ignore the code behind the components for now.
Get user feedback and when they are happy print the screen and get it signed by the project manager and file it. Changes become more difficult and costly after this point, so make sure everyone is happy with it. That said, you should expect there will be changes as the users give you feedback after actually using the screens.
See Usability 101: Introduction to Usability (Jakob Nielsen's Alertbox) and Misconceptions About Usability (Jakob Nielsen's Alertbox)
Today (2006AD) the common user is web focused. When they come to your application they will most likely have a background of using the Web. This may be their only exposure to computing. So lets look at some of the Web features that the user may expect to find in your application.
Multi-windows, forward/backward buttons if they end up somewhere they did not want to be.
Info always there, no need to save the page because it is on the web. Instant off. No need to save results or even shut down the computer correctly.
Hyperlinks to more info, search functions to find the info they want.
The user never makes mistakes, you the programmer have control over what the user is allowed to do. If the user does something you did not want them to do or that does not make sense, it is your fault. Don't blame the user, fix it. This is not a computer game where the user has to navigate a maze to get the result they want. This is an application designed to assist the user to do a task. Note the words "designed to assist the user". If you were asked to provide personal transport device, a skate board would not be suitable for a person on crutches. When it comes to programming and business logic, most busy users are on crutches.
In order to make your user feel satisfied when using your program you need to treat them with respect, don't be patronising, don't make them feel silly or stupid. I actually say to my users “Users never make mistakes, they just change their minds.” I find they respond well to this approach.
Before you code your interface you need to know your user. What is their background? Have they used similar applications before? What do they think is “normal”?
While a user is never wrong, they sometimes change their mind and want to go back and make a different choice or do it a different way. Your program needs to support them in this with an extensive and comprehensive undo/redo facility. This is sadly missing from most GUI's. Text editors have some level of undo/redo but usually only at the level of document editing not for the application as a whole (i.e. file handling etc.) Database's have roll back facilities but their GUI's are often devoid of undo capabilities. See What's Wrong with Undo/Redo for an improved undo/redo implementation.
The user has a job to do and does not want to be waiting around for your application. This means you should make extensive use of background threads to perform actions. The basic idea is that you should assume actions will suceed almost always. So don't force the user to wait around until you say OK I completed that task sucessfully. However inorder to let the user continue while the background task is running you often have to clone the current data. For example the user is editing a database table. When they edit a record, clone it and send it to the server in the background and let the user continue editing, perhaps the same record. (Note this clone is also userful for your redo action.) It is almost always faster to clone the current data then to do the full save/update operation. So cloning and then running save/update on a background thread gives control back to the user more quickly.
While you are cloning the data and preparing the background thread you need to lock the user out of the appropriate screens. The following methods can be used to block user actions while showing an hour glass.
static private Cursor hourglassCursor = new Cursor(Cursor.WAIT_CURSOR); static private Cursor normalCursor = new Cursor(Cursor.DEFAULT_CURSOR); private boolean waiting = false; private Object waitingLock = new Object(); public boolean setWaitCursor(boolean wait) { synchronized (waitingLock) { if (waiting && wait) { return false; // already waiting so blocked. } if (wait) { setCursor(hourglassCursor); } else { setCursor(normalCursor); } waiting = wait; return true; // success. } } public boolean isWaiting() { synchronized (waitingLock) { return waiting; } }
Then in each Action you want to block you add
if (!setWaitCursor(true)) { return; // blocked } try { // ... action code here } catch (Throwable t) { // ... handle errors here } finally { setWaitingCursor(false); // release block }
Java V5.0 and FutureTalker make background threads easy to implement however you still have to handle error recovery. In the case of editing a database table, if the edit fails you could roll back all the subsequent edits and reload the screen with the current valid data from the database. This is probably the simplist form or recovery. Other recovery actions are possible, it depends on the particular case.
If you have a choice, design your application as a web page. Applets let you do a lot in a web page. If you don't have choice and need a stand alone application that works off line then think about using Java Webstart to install the application and keep it up to date.
Contact Forward Computing and Control by
©Copyright 1996-2020 Forward Computing and Control Pty. Ltd.
ACN 003 669 994