SWT/JFace Mechanisms
SWT JFace Mechanisms
This chapter explains some of the mechanisms used by SWT/JFace. First, you get an introduction to the implementation of SWT. You learn how SWT is implemented to maintain a rich set of consistent APIs on all platforms, which allows the underlying native window system's look-and-feel to shine. The chapter then covers resource management in SWT and offers useful resource management techniques and practical tips. The chapter then explains how the model-view-controller (MVC) design fits in JFace.
The Implementation of SWT
SWT is a portable Java native UI toolkit. Using the consistent APIs provided by SWT, programs based on SWT can run on all of the SWT-supported platforms. As you have seen, SWT is implemented in Java. Java Native Interface (JNI) is used by SWT to invoke the native window systems from Java. JNI is a standard programming interface for writing Java native methods into native code.
Although both AWT and SWT are Java native UI toolkits, the implementations of them are quite different. AWT defines a common set of Java native methods, and these methods are then implemented on each system. The Java native methods in SWT are completely different, with each surfacing the methods specific to the native window system. All the logics are expressed in Java code for SWT.
The text that follows compares the implementations of AWT and SWT. It provides a good understanding of the implementation of SWT and why it is superior to that of AWT.
The java.awt.Button class of AWT has a setLabel method to set the text displayed on the button. The analogous class and method in SWT are org.eclipse.swt.widgets.Button and setText, respectively.
The code that follows provides details on how the setLabel method is implemented in AWT.
The code excerpt from the java.awt.Button is listed here:
public class Button extends Component implements Accessible { String label; ... /** * Sets the button's label to be the specified */ public void setLabel(String label) { boolean testvalid = false; synchronized (this) { if (label != this.label && (this.label == null || !this.label.equals(label))) { this.label = label; ButtonPeer peer = (ButtonPeer)this.peer; if (peer != null) peer.setLabel(label); testvalid = true; } } // This could change the preferred size of the Component. if (testvalid && valid) invalidate(); } ... }
The Button class in AWT delegates the responsibility of setLabel to its peer widget ButtonPeer. ButtonPeer is an interface:
package java.awt.peer; import java.awt.*; public interface ButtonPeer extends ComponentPeer { void setLabel(String label); }
In order to be portable, ButtonPeer must be implemented on each supported window system. Button's peer widget on Windows is sun.awt.windows.WButtonPeer. WButtonPeer has a native method, setLabel:
class WButtonPeer extends WComponentPeer implements ButtonPeer { ... public native void setLabel(String label); ... }
The implementation of this native method is as follows:
/* * Class: sun_awt_windows_WButtonPeer * Method: setLabel * Signature: (Ljava/lang/String;)V */ JNIEXPORT void JNICALL Java_sun_awt_windows_WButtonPeer_setLabel(JNIEnv *env, jobject self, jstring label) { TRY; PDATA pData; JNI_CHECK_PEER_RETURN(self); AwtComponent* c = (AwtComponent*)pData; const char *labelStr = NULL; // By convention null label means empty string if (label == NULL) { labelStr = ""; } else { labelStr = JNU_GetStringPlatformChars(env, label, JNI_FALSE); } if (labelStr == NULL) { throw std::bad_alloc(); } c->SetText(labelStr); if (label != NULL) JNU_ReleaseStringPlatformChars(env, label, labelStr); CATCH_BAD_ALLOC; }
WButtonPeer's Motif implementation is as follows:
// sun.awt.motif.MButtonPeer class MButtonPeer extends MComponentPeer implements ButtonPeer { ... public native void setLabel(String label); ... }
The corresponding implementation of the native method is as follows:
/* * Class: sun_awt_motif_MButtonPeer * Method: setLabel * Signature: (Ljava/lang/String;)V */ JNIEXPORT void JNICALL Java_sun_awt_motif_MButtonPeer_setLabel (JNIEnv * env, jobject this, jstring label) { struct ComponentData *wdata; char *clabel; XmString xim; AWT_LOCK (); wdata = (struct ComponentData *) JNU_GetLongFieldAsPtr(env, this, mComponentPeerIDs.pData); if (wdata == NULL) { JNU_ThrowNullPointerException (env, "NullPointerException"); AWT_UNLOCK (); return; } if (JNU_IsNull (env, label) || ((*env)->GetStringLength (env, label) == 0)) { xim = XmStringCreateLocalized (""); } else { jobject font = awtJNI_GetFont (env, this); if (awtJNI_IsMultiFont (env, font)) { xim = awtJNI_MakeMultiFontString (env, label, font); } else { if (JNU_IsNull (env, label)) { clabel = emptyString; } else { clabel = (char *) JNU_GetStringPlatformChars (env, label, NULL); if (clabel == NULL) { /* Exception? */ AWT_UNLOCK (); return; } } xim = XmStringCreate (clabel, "labelFont"); if (clabel != emptyString) { JNU_ReleaseStringPlatformChars (env, label, (const char *) clabel);; } } } XtVaSetValues (wdata->widget, XmNlabelString, xim, NULL); XmStringFree (xim); AWT_FLUSH_UNLOCK (); }
AWT defines a common set of APIs that must be implemented on every supported platform. If certain features are not available on one of the supported platforms, they must be sacrificed in order for the toolkit to maintain portability.
SWT takes a different approach. SWT provides a different set of widget classes for each platform; however, only the signatures of the public methods are the same, regardless of their host platforms.
The Windows-specific org.eclipse.swt.widgets.Button is listed here:
package org.eclipse.swt.widgets; import org.eclipse.swt.internal.win32.*; import org.eclipse.swt.*; import org.eclipse.swt.graphics.*; import org.eclipse.swt.events.*; /** * Instances of this class represent a selectable user interface object that * issues notification when pressed and released. */ public class Button extends Control { Image image; static final int ButtonProc; static final TCHAR ButtonClass = new TCHAR (0,"BUTTON", true); static final int CheckWidth, CheckHeight; static { int hBitmap = OS.LoadBitmap (0, OS.OBM_CHECKBOXES); if (hBitmap == 0) { CheckWidth = OS.GetSystemMetrics (OS.IsWinCE ? OS.SM_CXSMICON : OS.SM_CXVSCROLL); CheckHeight = OS.GetSystemMetrics (OS.IsWinCE ? OS.SM_CYSMICON : OS.SM_CYVSCROLL); } else { BITMAP bitmap = new BITMAP (); OS.GetObject (hBitmap, BITMAP.sizeof, bitmap); OS.DeleteObject (hBitmap); CheckWidth = bitmap.bmWidth / 4; CheckHeight = bitmap.bmHeight / 3; } WNDCLASS lpWndClass = new WNDCLASS (); OS.GetClassInfo (0, ButtonClass, lpWndClass); ButtonProc = lpWndClass.lpfnWndProc; } /** * Constructs a new instance of this class given its parent * and a style value describing its behavior and appearance. */ public Button (Composite parent, int style) { super (parent, checkStyle (style)); } int callWindowProc (int msg, int wParam, int lParam) { if (handle == 0) return 0; return OS.CallWindowProc (ButtonProc, handle, msg, wParam, lParam); } ... /** * Sets the receiver's text. */ public void setText (String string) { checkWidget (); if (string == null) error (SWT.ERROR_NULL_ARGUMENT); if ((style & SWT.ARROW) != 0) return; int newBits = OS.GetWindowLong (handle, OS.GWL_STYLE); int oldBits = newBits; newBits &= ~(OS.BS_BITMAP | OS.BS_ICON); if (newBits != oldBits) { OS.SetWindowLong (handle, OS.GWL_STYLE, newBits); } TCHAR buffer = new TCHAR (getCodePage (), string, true); OS.SetWindowText (handle, buffer); } ... } package org.eclipse.swt.internal.win32; import org.eclipse.swt.internal.*; public class OS { ... public static final boolean SetWindowText (int hWnd, TCHAR lpString) { if (IsUnicode) { char [] lpString1 = lpString == null ? null : lpString.chars; return SetWindowTextW (hWnd, lpString1); } byte [] lpString1 = lpString == null ? null : lpString.bytes; return SetWindowTextA (hWnd, lpString1); } public static final native boolean SetWindowTextW (int hWnd, char [] lpString); ... }
The implementation of the native method SetWindowTextW is as follows:
JNIEXPORT jboolean JNICALL OS_NATIVE(SetWindowTextW) (JNIEnv *env, jclass that, jint arg0, jcharArray arg1) { jchar *lparg1=NULL; jboolean rc; NATIVE_ENTER(env, that, "SetWindowTextW ") if (arg1) lparg1 = (*env)->GetCharArrayElements(env, arg1, NULL); rc = (jboolean)SetWindowTextW((HWND)arg0, (LPWSTR)lparg1); if (arg1) (*env)->ReleaseCharArrayElements(env, arg1, lparg1, 0); NATIVE_EXIT(env, that, "SetWindowTextW ") return rc; }
Note that SetWindowTextW in bold font in the preceding code is a method provided by Windows.
The analogous class in Motif is as follows:
package org.eclipse.swt.widgets; import org.eclipse.swt.internal.*; import org.eclipse.swt.internal.motif.*; import org.eclipse.swt.*; import org.eclipse.swt.graphics.*; import org.eclipse.swt.events.*; /** * Instances of this class represent a selectable user interface object that * issues notification when pressed and released. */ public class Button extends Control { String text = ""; Image image, bitmap, disabled; static final byte [] ARM_AND_ACTIVATE; static { String name = "ArmAndActivate"; int length = name.length(); char [] unicode = new char [length]; name.getChars (0, length, unicode, 0); byte [] buffer = new byte [length + 1]; for (int i = 0; i < length; i++) { buffer[i] = (byte) unicode[i]; } ARM_AND_ACTIVATE = buffer; } /** * Constructs a new instance of this class given its parent * and a style value describing its behavior and appearance. */ public Button (Composite parent, int style) { super (parent, checkStyle (style)); } ... /** * Sets the receiver's text. */ public void setText (String string) { checkWidget(); if (string == null) error (SWT.ERROR_NULL_ARGUMENT); if ((style & SWT.ARROW) != 0) return; text = string; char [] text = new char [string.length ()]; string.getChars (0, text.length, text, 0); int mnemonic = fixMnemonic (text); byte [] buffer = Converter.wcsToMbcs (getCodePage (), text, true); int xmString = OS.XmStringParseText ( buffer, 0, OS.XmFONTLIST_DEFAULT_TAG, OS.XmCHARSET_TEXT, null, 0, 0); if (xmString == 0) error (SWT.ERROR_CANNOT_SET_TEXT); if (mnemonic == 0) mnemonic = OS.XK_VoidSymbol; int [] argList = { OS.XmNlabelType, OS.XmSTRING, OS.XmNlabelString, xmString, OS.XmNmnemonic, mnemonic, }; OS.XtSetValues (handle, argList, argList.length / 2); if (xmString != 0) OS.XmStringFree (xmString); } int traversalCode (int key, XKeyEvent xEvent) { return super.traversalCode (key, xEvent) | SWT.TRAVERSE_MNEMONIC; } ... } package org.eclipse.swt.internal.motif; import org.eclipse.swt.internal.*; public class OS { ... public static final synchronized native void XtSetValues(int widget, int[] argList, int numArgs); ... }
The corresponding JNI implementation of the native method is as follows:
// os.c #include "swt.h" #include "os_structs.h" ... JNIEXPORT void JNICALL OS_NATIVE(XtSetValues) (JNIEnv *env, jclass that, jint arg0, jintArray arg1, jint arg2) { jint *lparg1=NULL; NATIVE_ENTER(env, that, "XtSetValues ") if (arg1) lparg1 = (*env)->GetIntArrayElements(env, arg1, NULL); XtSetValues((Widget)arg0, (ArgList)lparg1, arg2); if (arg1) (*env)->ReleaseIntArrayElements(env, arg1, lparg1, 0); NATIVE_EXIT(env, that, "XtSetValues ") } ...
XtSetValues is a method exposed in the Motif toolkit.
As illustrated in the preceding code, SWT provides different classes of Button on Windows, Motif, and other platforms, and the signature of each public method of every Button class is the same. Also, you may notice that SWT tries to put as much as possible the logic into Java code rather than the native code as AWT does. The main strategy here is one-to-one mapping between the Java natives and procedures offered by the native window system. In both implementations of OS.SetWindowText on Windows and OS.XtSetValues on Motif, only the absolutely necessary argument conversion code is included, and nothing extra happens there. The Java call is passed directly to the underlying native window systems. This implies that the equivalent C code is guaranteed to behave completely in the same way.
The implementation strategy of SWT offers a few advantages.
Debugging is made easy with SWT. The developer can find the cause of the bug easily. For example, if setLabel is malfunctioning, the native call made (SetWindowsText for Windows or XtSetValues for Modify) can be traced. The one-to-one mapping enables you to write simple C programs to isolate the bug and submit a bug report to the corresponding toolkit vendors.
For people who implement and maintain SWT, it is very maintainable and scalable, thanks to the one-to-one mapping. Because every native method in Java code has its analogous method in the native window system, native UI programmers can easily see how the Java native method works. Once they are in doubt, they can always refer to the documentation for the native window system because SWT natives are one-to-one mapped with those in the underlying native window system. Introducing new features to SWT is straightforward, too.
Most common low-level widgets are implemented natively everywhere. However, some native window systems lack certain high-level widgets, such as toolbars and trees. In this case, SWT emulates those widgets on the native window systems. For example, a tree widget is available natively on Windows; however, it is missing on Motif. SWT emulates the tree widget on Motif. This emulation technique is used universally by Swing. However, in SWT, only absolutely necessary emulation is employed.
The implementation strategy of SWT enables it to maintain a rich set of consistent APIs on all platforms, allowing the underlying native window system's look-and-feel to shine.
Resource Management with SWT JFace
As you saw in the last section, SWT is an ultra-thin layer perching on the underlying native window system. For native user interface programming with C, programmers have to allocate and free operating system resources "manually." SWT programmers must do the same.
SWT is a Java native UI toolkit. One of the great features of Java is that it has a built-in garbage collection mechanism, which frees Java developers from allocating and freeing objects. When you program with SWT, you can still enjoy this feature, except that you have to manage operating system resources by yourself. Do not panic — the issue of resource management has been addressed very well in SWT/JFace.
Operating System Resources
Operating system resources in SWT refer to native resources used by those (operating system) resourcebased objects. Some of the resource-based objects are Display, Color, Font, GC, Image, Printer, Region, and Widgets.
Most of the classes in the following packages are resource-based:
- org.eclipse.swt.custom: Contains SWT custom widgets
- org.eclipse.swt.graphics: Provides the classes for primitive drawing, graphic handling, image loading/saving, and so on
- org.eclipse.swt.widgets: Contains basic SWT widgets
If you are uncertain about whether an object/class is resource-based using operating system resources, check whether it has a dispose() method. If it does, then it is very likely resource-based. Otherwise, it isn't.
Are any operating system resources used in Swing?
Yes. As introduced in Chapter 1, Swing is built on the top of core AWT libraries and only "high-level" components in Swing are lightweight and peerless. Those "low-level" components are heavyweight, which make use of operating system resources. JFrame is one of the most notable examples. The JavaDoc document for the dispose() method says, "Releases all of the native screen resources used by this Window, its subcomponents, and all of its owned children."
Rules of Operating System Resource Management
In Chapter 1, two simple rules were briefly introduced. We are going to discuss them in greater detail.
Rule 1: If you create it, you must dispose of it
In SWT, all operating system resource classes allocate necessary native resources only in their constructors. No methods other than those constructors are responsible for allocating any operating system resources. This implies that only if you made a call to a constructor of a resource-based SWT class do you need to free the resources later by calling the dispose method of the object.
For example, the code listing that follows draws a rectangle onto a shell:
void draw(Shell shell) { GC gc = new GC(shell); gc.setLineWidth(3); gc.drawRectangle(10, 10, 200, 100); gc.dispose(); }
First, a GC (graphics context) object gc is created based on the specified shell. Then you can adjust settings of the gc object and call the drawRectangle method to draw the rectangle on the shell. After the drawing is done, the dispose method of gc is called to release the operating system resources associated with it.
Some may think that calling the dispose method is unnecessary because the gc object will be eligible for garbage collection when the method exits. It is very important to understand that those operating system resources wrapped by resource-based objects are unchanged when resource-based objects are garbage collected — i.e., if they have not been disposed of, they are still there even if the resource-based (Java) objects are gone. That's why the dispose method has to be called explicitly.
Why not call the dispose method in the finalizers of resource-based objects? In this way, when the resource-based objects are reclaimed by the garbage collector, the operating system resources are disposed of automatically. This strategy seems to be an elegant solution to managing operating system resources, but it causes many problems.
The timing for the finalize method of a garbage collected object is unknown. Additionally, the finalization process may fail if exceptions are thrown. According to the Java Language Specification (JLS), finalization invocations are not ordered. Even if you have unlimited memory, this could cause problems. For example, in the X Window system, a Font can be used by GC to provide text rendering. Due to the unordered nature of finalization, the Font object may be destroyed before the GC object, which leaves the operating system in an indeterminate state. JSL does not specify which thread will be used to perform finalization either. To dispose of operating system resources, the finalizers must be synchronized with the UI thread. This would cause a significant runtime overhead. The list of such problems goes on and on. The troubles caused by this approach may be even more difficult and complicated than the original problem itself.
Object finalization is nondeterministic and error prone. SWT does not rely on it to dispose of operating system resources. In SWT, those native resources must be disposed of explicitly.
How to Dispose?
To dispose of a resource-based object, simply call its dispose() method. All resource-based classes share the same method signature for the dispose method:
public void dispose()
The dispose method can be called many times. All the operating system resources are released when the first call to this method is made, and subsequent calls have no actual effect. It is worth noting that when you dispose of a resource-based object, its children, if any, will be disposed of, too. Rule #2 discusses this in detail.
Once a widget or any other resource-based object is disposed of, calling most of its methods will result in the following exception:
SWTException(ERROR_WIDGET_DISPOSED)
Sometimes, you may need to check whether a resource-based object is disposed of or not by calling the isDisposed method:
public boolean isDispose()
Calling the dispose method is easy; however, the timing to do it must be opportune.
When to Dispose?
Operating system resources should be disposed of as soon as they are not in use any more. As shown in the code snippet in the last section, gc is disposed of immediately after the drawing is done. Resources such as GCs and Printers are almost always created and disposed of within the scope of the same method. Code written in this way is easy to maintain and to debug.
Beware that you do not dispose of resources too early. For example, the following code draws a red rectangle onto the specified shell:
void draw(Shell shell) { GC gc = new GC(shell); gc.setLineWidth(3); Color color = new Color(display, 255, 0, 0); gc.setForeground(color); gc.drawRectangle(10, 10, 200, 100); gc.dispose(); color.dispose(); }
In the preceding code, a Color object color is created. The color is set to the foreground of the gc. Because color is in use by gc, color is disposed of after gc is disposed of. What would happen if color was disposed of first? This may leave gc's foreground as an invalid handle, and as a result, the program could crash.
If you use resource-based objects in a widget, you'd better to clean them up when the widget is disposed of. For example, the code list that follows sets the foreground of a label to red.
Label label = new Label(shell, SWT.NULL); final Color color = new Color(display, 255, 0, 0); label.setForeground(color); label.addDisposeListener(new DisposeListener() { public void widgetDisposed(DisposeEvent e) { color.dispose(); } }); ...
A disposed listener is used to listen for the dispose event. When the label is disposed of, the dispose listener will be notified and the method WidgetDisposed will be called. (More details on event handling are discussed in Chapter 4.)
Do Not Dispose of It If You Did Not Create It
If you did not create a resource, do not dispose of it. This seems obvious, but sometimes it could be ignored in practice. In the last section, you saw how to set the foreground of a label to red. The following code is the alternative to the preceding procedure:
Label label = new Label(shell, SWT.NULL); Color color = display.getSystemColor(SWT.COLOR_RED); label.setForeground(color);
Should the color object here be disposed of with the label disposed? No. Do not dispose of a resource if you never created it. You "create" a resource only when you explicitly call its constructor. In the preceding code, the constructor of the Color class was not called explicitly; thus, you did not create the color object. Because you did not create it, it's not your responsibility to dispose of it. In this case, the SWT library manages the color object. You should not dispose of SWT-managed resource-based objects, or your programs may crash or throw exceptions.
The whole set of SWT APIs is designed with this in mind. For example, the method public void GC.getClipping(Region region) forces the developer to create a Region object in order to retrieve the clipping region data from a GC. Why does not this method simply return a Region object? If so, this rule will be broken and this inconsistence would be a nightmare for programmers. Whether you are developing SWT-based libraries or applications, this rule should always be enforced to make everyone's life easier.
Rule 2: Disposing of the parent disposes of the children
This rule is very straightforward. When you dispose of a parent widget, all of the widget's children will be disposed of also:
- Disposing of a Composite disposes of all of the Composite's children.
- Disposing of a Menu disposes of all of its MenuItems.
- Disposing of a Tree or TreeItem disposes of all of its TreeItems.
- Disposing of a Table disposes of all of its TableColumn and TableItems.
For example, the following code builds up the component tree, as shown in Figure 2-1.
Figure 2-1
shell.setLayout(new GridLayout(2, true)); Label label = new Label(shell, SWT.NULL); label.setText("Label"); Composite composite = new Composite(shell, SWT.NULL); composite.setLayout(new GridLayout()); Button button1 = new Button(composite, SWT.PUSH); button1.setText("Button 1"); Button button2 = new Button(composite, SWT.PUSH); button2.setText("Button 2"); ... shell.dispose();
When the method shell.dispose() is called, the disposal process is performed in the depth-first order. This means that before the shell is fully disposed of, the shell has to dispose of its children (the label and the composite) first. To dispose of the composite, its children, button1 and button2 should be disposed of first. This process repeats recursively until all the components and their children are properly disposed of.
As you can see here, this rule saves you from having to dispose of each item one by one in a complex component tree. Disposing of a top-level widget will dispose of all of its descendant widgets. Also, this rule reflects the fact that a widget (except the top-level shell) existing without a parent is meaningless and useless.
What are the children of a parent widget? For a widget, its children are those widgets depending on it. Most of the child widgets are created in the following way or similarly:
XXX xxx = new XXX(parentWidget, style);
Typically, the first argument in the constructor of a child widget is the parentWidget object. In our example, the composite has two children: button1 and button2.
The following code creates a red background composite with a button as its only child:
Composite composite = new Composite(shell, SWT.NULL); GridLayout layout = new GridLayout(); composite.setLayout(layout); Color color = new Color(display, 255, 0, 0); composite.setForeground(color); Button button = new Button(composite, SWT.PUSH); button.setText("Button"); ... composite.dispose();
Will the color object be disposed of when the composite is being disposed of? No. The color object is not a child of the composite, and thus it will not be disposed of as the button will be.
The accessory resource-based objects that you created (e.g., Colors, Fonts, and Images) will not be disposed of when the parent widget is disposed of. Those resource-based objects created by SWT internally will be managed by themselves automatically. What about the layout object — who should dispose of it? Because the layout is not a resource-based object, like other normal Java objects, it will be garbage collected when it is not referenced by any other objects.
Now you can easily apply this rule to Menus with MenuItems as their children, Trees with TreeItems, and Tables with TableColumns and TableItems.
Shared Menus Side Effect
This rule is wonderful, but it does have a side effect related to Menus only. If the same Menu instance is shared by multiple controls or menu items, disposing of any one of the controls or menu items disposes of the Menu object.
The code that follows creates two composites sharing the same Menu instance:
shell.setLayout(new GridLayout(2, true)); final Composite composite1 = new Composite(shell, SWT.BORDER); composite1.setBackground(display.getSystemColor(SWT.COLOR_BLACK)); final Composite composite2 = new Composite(shell, SWT.BORDER); composite2.setBackground(display.getSystemColor(SWT.COLOR_BLUE)); Menu menu = new Menu(composite1); MenuItem menuItem = new MenuItem(menu, SWT.PUSH); menuItem.setText("Popup menu"); menuItem.addDisposeListener(new DisposeListener() { public void widgetDisposed(DisposeEvent e) { System.out.println("Menu item is disposed."); } }); menuItem.addSelectionListener(new SelectionAdapter() { public void widgetSelected(SelectionEvent e) { System.out.println("Disposing ..."); composite2.dispose(); } }); composite1.setMenu(menu); composite2.setMenu(menu);
The component tree and the UI are shown in Figure 2-2 and Figure 2-3, respectively.
Figure 2-2
Figure 2-3
As shown in the component tree, composite1 and composite2 share the same popup menu menu. When the user right-clicks the button and selects the menu item, the widgetSelect method in the SelectionListener is called. As a result, composite2 is disposed of. The component tree clearly shows that menu is the direct child of composite2; thus menu will be disposed of when composite2 is disposed of. Similarly, menuItem will be disposed of when its parent menu is disposed of. The registered DisposeListener of menuItem is called upon the menuItem being disposed, so you will see the message "Menu item is disposed" printed in the console.
After composite2 is disposed of, only composite1 is shown on the shell. If you try to right-click composite1, no menu will show up because menu has already been disposed of.
If your applications do not dispose of widgets with shared Menus in the middle of running, then you do not have to worry about this issue. If this side effect is undesired, you can work around it by doing one of two things:
- Remove the Menu instance, if any, from the control before calling the control's dispose method:
menuItem.addSelectionListener(new SelectionAdapter() { public void widgetSelected(SelectionEvent e) { System.out.println("Disposing ..."); composite2.setMenu(null); // remove the menu. composite2.dispose(); } });
Just before composite2 is disposed, the menu is removed from it. So menu will be unchanged even if the composite2 is disposed.
- Create a new instance of Menu for each of the controls.
The code Menu menu = new Menu(composite1); may give you the impression that the menu is a child of composite1. Actually, the constructor of Menu behaves quite differently from the constructors of other widgets. Internally, when the constructor is invoked, the parent of the menu is set to the first org.eclipse.swt.widget.Decorations parent of the composite1 in the component tree. The first Decorations parent of a composite is the composite's nearest ancestor that is of type Decorations. In this case, the shell is the first Decorations parent of the composite1. So even if a Menu is not attached to any Controls, it will be disposed of when its parent (i.e., the first Decorations parent of the argument Control) is disposed of.
Managing Fonts and Images with JFace
As a user interface programmer, you need to use fonts and images heavily. When you develop moderate to large native UI applications, you may soon realize that managing fonts and images becomes very challenging. For every font or image you create, you must explicitly dispose of it. Is there a better way to solve this problem? JFace provides a complete solution for you to manage fonts and images easily.
Managing shared resources is a classic programming problem. JFace adopts the classic solution: using central shared registries.
Using the FontRegistry
The FontRegistry (org.eclipse.jface.resource.FontRegistry) is a registry of fonts. A FontRegistry maintains a mapping between symbolic font names and actual font objects. Before retrieving a font from a font registry, you first need to put the font into the font registry with a key. Then you can obtain the font with the corresponding key. You should not dispose of the font after use because the font registry takes care of this. The FontRegistry owns all the font objects registered with it and it will dispose of all the fonts when the Display is disposed of.
Here's an example:
Display display = new Display(); Shell shell = new Shell(display); FontRegistry fontRegistry = new FontRegistry(display); fontRegistry.put("button-text", new FontData[]{new FontData("Arial", 9, SWT.BOLD)} ); fontRegistry.put("code", new FontData[]{new FontData("Courier New", 10, SWT.NORMAL)}); Text text = new Text(shell, SWT.MULTI | SWT.BORDER | SWT.WRAP); text.setFont(fontRegistry.get("code")); text.setForeground(display.getSystemColor(SWT.COLOR_BLUE)); text.setText("public static void main() { System.out.println("Hello"); }"); GridData gd = new GridData(GridData.FILL_BOTH); gd.horizontalSpan = 2; text.setLayoutData(gd); Button executeButton = new Button(shell, SWT.PUSH); executeButton.setText("Execute"); executeButton.setFont(fontRegistry.get("button-text")); Button cancelButton = new Button(shell, SWT.PUSH); cancelButton.setText("Cancel"); cancelButton.setFont(fontRegistry.get("button-text")); shell.pack(); shell.show(); ...
The preceding code creates the user interface shown in Figure 2-4.
Figure 2-4
The FontData class is a lightweight class describing system fonts. A FontData to a Font is analogous to an ImageDescriptor to an Image. A FontData class describes the following properties of a Font:
- name: The face name of the font
- height: The height of the font in points
- style: Style bits, combination of SWT.NORMAL, SWT.ITALIC, and SWT.BOLD
You can treat the FontDatas as lightweight objects, which means they do not need to be disposed of to release operating system resources. The Font itself is resource-based, i.e., if you create it, you must dispose of it. However, as shown in the preceding text, you can employ a FontRegistry to manage allocation and disposal of the fonts.
The following are important methods of the FontRegistry class:
- public void put(String symbolicName, FontData[] fontData): Adds or replaces a font described by an array of FontDatas to this registry with the given symbolic name as the key. In Microsoft Windows platform, there is typically one element in the FontData array; however, you may need to supply several FontDatas in X Windows.
Note that this method does not throw any exception as its ImageRegistry counterpart does. As you may remember, an exception will be thrown if you are trying to replace an existing image descriptor with the image loaded in an ImageDescriptor. Internally, a FontRegistry puts the replaced old Font if any into a stale font list and it will be disposed of, too, when the Display is disposed of.
- public Font get(String symbolicName): Returns the font associated with the given symbolic font name or the default font if no special value is associated with the symbolic name.
- public FontData getFontData(String symbolicName): Returns the font data associated with the specified symbolic name. The default font data will be returned if there is no special value associated with the given symbolic name.
- public boolean hasValueFor(String symbolicName): Returns whether or not this registry has a value for the given symbolic name.
- public FontData[] bestDataArray(FontData[] fonts, Display display): Returns the first (or first several for X Windows) valid FontData object from the given font data array. If none are valid, the first one in the array will be returned regardless.
You can use FontData to specify fonts programmatically; alternatively, you can let the user choose fonts through FontDialog, which is covered in later chapters.
Using the ImageRegistry
ImageRegistry, as the name suggests, is a registry of images. ImageRegistry works much like a font registry. It maintains a mapping between symbolic image names and SWT image objects or lightweight image descriptor objects, which defer the creation of SWT image instances until they are needed. Before using an image, you must first put the image itself or its descriptor to the image registry with a key. Later, you can retrieve the image with the key. You must not dispose of the image after use because the ImageRegistry takes care of this. All the images in the ImageRegistry will be disposed of when the top-level Display object is disposed of.
The image descriptor org.eclipse.jface.resource.ImageDescriptor is a lightweight object that does not store the data of the image but can create the image on demand. The ImageDescriptor provides two ways to construct an image, from a file or from a URL. You could extend it to provide other methods to create images, such as retrieving image data using JDBC from a database.
To see how the ImageRegisiter simplifies image management, you will build a simple file browse, as shown in Figure 2-5.
Figure 2-5
When the user clicks the Browse button, a directory dialog pops up. The user then selects a directory in which to list its file in the table below the button. If a file is a JAR file, the JAR icon is shown; otherwise, the default file icon is used. Of course, you can extend it to add more icons for other file types.
First, create the class and declare/define necessary variables:
public class SimpleFileBrowser { Display display = new Display(); Shell shell = new Shell(display); ImageRegistry imageRegistry; Table table;
Then the Browse button and the table are created:
private void init() { shell.setText("File Browser"); shell.setLayout(new GridLayout(1, true)); Button button = new Button(shell, SWT.PUSH); button.setText("Browse ..."); button.addSelectionListener(new SelectionAdapter() { public void widgetSelected(SelectionEvent e) { DirectoryDialog dialog = new DirectoryDialog(shell, SWT.NULL); String path = dialog.open(); if (path != null) { File file = new File(path); displayFiles(file.list()); } } }); GridData gd = new GridData(GridData.FILL_BOTH); table = new Table(shell, SWT.MULTI); table.setLayoutData(gd);
Note that a SelectionListener has been registered to the Browse button to listen to the selection event. When the button is clicked, the widgetSelected method in the inner class is called. Thus the user can select a directory to list files. The method then invokes the displayFiles method to perform the actual file listing. The displayFiles method is covered shortly.
Then create an instance of ImageRegistry:
imageRegistry = new ImageRegistry();
Remember that icons (the default icon and the JAR icon) are needed to list the files, so you need to put them into the image registry:
ImageDescriptor defaultIcon = ImageDescriptor.createFromFile(null, "img/default.gif"); imageRegistry.put("default", defaultIcon); ImageDescriptor jarIcon = ImageDescriptor.createFromFile(null, "img/jar.gif"); imageRegistry.put("jar", jarIcon); }
For the default icon, an ImageDescriptor (defaultIcon) is created. This ImageDescriptor provides information on how to retrieve the image — from a file named img/default.gif. Then, the ImageDescriptor instance along with the symbolic key is put into the ImageRegistry. The process is repeated for the JAR icon. Note that no images have been loaded at this stage. The ImageRegistry delays the creation of an image until it is needed.
The following code is the implementation of the method displayFiles:
public void displayFiles(String[] files) { // Removes all existing table items. table.removeAll(); for (int i = 0; files != null && i < files.length; i++) { TableItem item = new TableItem(table, SWT.NULL); Image image = null; if (files[i].endsWith(".jar")) { image = imageRegistry.get("jar"); } else { image = imageRegistry.get("default"); } item.setImage(image); item.setText(files[i]); } }
This method first removes all items in the table. For each file, a TableItem is created with the table as its parent. The image icon for a file is set based on its extension. If it is a JAR file, the image is retrieved from the ImageRegistry with the key jar; otherwise, it is retrieved with the key default. Note that for all the non-JAR files, the exact same image (the default icon) is used. Regardless of the number of files and types of files, at most two images are used here. This is one of the advantages of using image registries.
If you do not use an ImageRegistry, the displayFiles method needs to be rewritten as follows:
public void displayFiles(String[] files) { // Disposes all of the images used by the table items first. TableItem[] items = table.getItems(); for(int i=0; items != null && i < items.length; i++) { if(items[i].getImage() != null) items[i].getImage().dispose(); } // Removes all existing table items. table.removeAll(); for (int i = 0; files != null && i < files.length; i++) { TableItem item = new TableItem(table, SWT.NULL); Image image = null; if (files[i].endsWith(".jar")) { image = new Image(display, "img/jar.gif"); } else { image = new Image(display, "img/default.gif"); } item.setImage(image); item.setText(files[i]); } }
The preceding code is not only tedious but also computationally expensive. For each file, an image is created and loaded. Suppose there are 1,000 files under a certain directory, the preceding code needs to create 1,000 images and load 1,000 times and the former version using an ImageRegisitry needs to create only two images and load only twice. In this case, using image registries is clearly the way to go.
The ImageRegistry class is very useful for managing small to moderate images such as icons and so on. However, for large images, you should use it with caution. All the images in an ImageRegistry are disposed of when the top-level Display object is disposed of. Suppose you are developing a graphic editing tool; clearly, you should not put the images being edited into an ImageRegisitry. If you do so, the memory is soon eaten up by the stale images residing in the ImageRegistry.
Another thing you must pay attention to is that, as mentioned in the beginning of this section, you must not dispose of any images in an ImageRegistry.
The four methods of the ImageRegistry class are as follows:
- public void put(String key, ImageDescriptor descriptor): Adds an image descriptor to this ImageRegistry. The image specified in the descriptor will be computed and remembered the first time this entry is retrieved. This method replaces an existing image descriptor only if the corresponding image has not been computed yet; otherwise, an IllegalArgumentException is thrown.
- public void put(String key, Image image): Adds an image to this ImageRegistry. Keep in mind that the image must not be disposed of by the clients after the image has been put into the registry. This seems to break Rule 1 discussed earlier in the chapter. Actually, by making this call, the caller transfers the responsibility of resource management to the ImageRegistry.
- public Image get(String key): Returns the image associated with the specified key, or null if it does not exist.
- public ImageDescriptor getDescriptor(String key): Returns the image descriptor associated with the specified key, or null if it does not exist.
Model View Controller Pattern
Chapter 1 shows that you can program with SWT/JFace in either the traditional way or the MVC way. Here, you get a close look at how the MVC pattern fits in SWT/JFace.
The model-view-controller (MVC) pattern is a well-known user interface design pattern that separates the modeling of the domain, the presentation, and the actions based on the user input into three separate components.
The MVC Architecture
The three main components in the MVC architecture are model, view, and controller.
Model
The model manages the basic behavior and the data of the application domain. It responds to requests for information about its state (usually from the view) and it also responds to commands to change state (usually from the controller). The model may notify views when it changes.
For example, the model component of the List control shown in Figure 2-6 might contain information about the items of the list and the current selected index.
Figure 2-6
View
The view is responsible for displaying information about the model. For the List control shown in Figure 2-6, the view presents all the items and highlights the currently selected item. There could be many different types of views for the same model. For example, you can use a Table instead of a List to display the model. Additionally, the view forwards user input to the controller.
Controller
The controller acts as the intermediary between the view and the model. As a portion of the user interface, it dictates how the components interact with events such as mouse clicks, keyboard events, and so on. For example, when you click the second item in the List, the second item is highlighted. The controller interprets your mouse click and informs the view and model to change appropriately.
MVC Interaction
The interaction of the three components in MVC is illustrated in Figure 2-7. The user takes some input actions and the controller notifies the model to change its state accordingly. The model then records the change as requested and broadcasts to the view (or multiple views) about the change. The view responds by querying the model for its state and updating the display if necessary.
Figure 2-7
Benefits of MVC
Architecting the user interface around the MVC pattern results in the following benefits:
- Supports multiple views: As you have seen, the model does not depend on the view; thus the user interface can display multiple views of the same data at the same time. For example, a visual HTML editor may have source view, visual UI view, and Web view using the same model data: the HTML file.
- Accommodates changes: Changes can be easily made for any component in the MVC. Changes to one aspect of the MVC are not coupled to other aspects, and this makes debugging easy. Also, adding new types of views does not affect the model.
Costs of MVC
The benefits of MVC do not come free. Costs of the MVC include the following:
- Complexity. The new levels of indirection increase the complexity of the solution. The eventdriven nature of MVC also increases the difficulty in debugging programs.
- Limited reuse of the controller. MVC decouples the model from the view, but the control is tied to both the model and the view. This reduces the potential that the controller logic will be reused in other parts of the application or other applications.
UI Delegation
MVC is well defined; however, sometimes separating the viewer from the controller is difficult. Instead, people combine the viewer and the controller into one unit, calling it a representation. This modified version of MVC is often called "user interface delegation."
JFace and MVC
SWT is designed to be efficient and easy to use, so it does not support MVC directly. However, on the top of SWT, JFace does provide excellent MVC support to meet diverse requirements. There are two notable MVC frameworks within JFace.
JFace Viewers
A viewer in JFace is an MVC-based adapter on an SWT widget. A viewer plays the controller role in the MVC paradigm. A viewer's model is comprised of elements, represented by objects. By using a well-defined generic infrastructure, a viewer is capable of handling model input, updates, and selection in terms of elements. The view in the MVC is the SWT widget wrapped by a viewer. The model elements are not displayed directly, and they are mapped to labels containing text and/or an image using the viewer's label provider (ILabelProvider). Figure 2-8 shows how a ListViewer fits in the MVC model.
Figure 2-8
The package org.eclipse.jface.viewers contains the viewer framework, which contains the following abstract classes for viewers:
- Viewer: A viewer is an MVC-based adapter on a widget.
- StructuredViewer: Represents a structure-oriented viewer (such as a tree, a list, or a table) that supports custom sorting, filtering, and rendering.
- AbstractTreeViewer: Represents a tree structure–oriented viewer.
The package also contains the following concrete classes:
- ListViewer: A viewer based on the SWT List control
- TreeViewer: A viewer based on the SWT Tree control
- TableViewer: A viewer based on the SWT Table control
- TableTreeViewer: A viewer based on the SWT TableTree control
- CheckboxTreeViewer: A tree-structured viewer based on the SWT Tree control with Checkboxes on each node
- CheckboxTableViewer: A viewer based on the SWT Table control with Checkboxes on each node
Viewers are discussed further in later chapters.
JFace Text
JFace Text is a full-featured MVC-based framework for creating, manipulating, and displaying text documents. JFace Text is implemented in the package org.eclipse.jface.text and its subpackages.
Details about the JFace Text framework are discussed in Chapter 21.
Summary
The first part of this chapter introduced the implementation of SWT. The superior implementation strategy of SWT enables it to maintain a rich set of consistent APIs on all platforms, allowing the underlying native window system's look-and-feel to shine. The second part discussed resource management with SWT/JFace. The developer is responsible for allocating and freeing operating system resources. To do this, the developer can simply follow two rules: If you created it, you must dispose of it. And disposing of the parent disposes of the children. Fonts and images are operating system resources. SWT provides font and image registries for the developer. MVC and various JFace MVC frameworks are introduced in the last part. With knowledge of SWT implementation and resource management, you are now ready to move forward to the next chapter and develop your first SWT-based applications.