SqlView goes SWT


May 2004

download source

This article outlines my conversion of a Swing application to the new, open-source Standard Widget Toolkit. If you are brand new to SWT, you are in for some pleasant surprises. This article assumes you have some understanding of basic SWT techniques. If you do not, you might want to look at a previous article, Getting Started with SWT.


SashForms and TabFolders
ToolBars
BusyIndicator and wait icons
Implementing dialogs
Tables
Lessons learned

A History of SqlView

SqlView is a database query utility. You can issue SELECT, UPDATE, DELETE, CREATE TABLE -- just about anything that a JDBC driver will allow you to do. Sound familiar? There are a lot of similar tools out there and I hate to re-create the wheel when I don't have to.

But every once in a while, you need to write your own tool. There were some features I wanted and couldn't find anywhere else:

  • I needed a viewer that would save my SQL and let me re-use it in another session.
  • The SQL buffer would let me highlight and execute.
  • For various reasons, I did not want a tool that opened a database connection and left it open.
  • I wanted the results dumped in a textual display that I could copy and paste.
  • Above all, I wanted to experiment with minimalist GUI design. I've seen many similar tools which, to my mind, are all too busy. I wanted something simple and functional.

    That said, I use SqlView daily. When I need a new feature, it seems simple enough to just add it. When the whole thing doesn't work quite right, I change it.

    A SWT love affair

    Recently, I have become enamoured with SWT, the open-source toolkit that drives Eclipse. It is fast, lightweight, flexible and -- sad to say, but I will say it nonetheless -- much more visually appealing that either AWT or Swing. Meanwhile, it is also a hot technology -- everyone seems to be getting into it.

    SqlView suffers from a Swing front end that I created with the IBM VisualAge GUI builder. I could live with the plain display, but I was always rather ashamed of the code that VisualAge pumped out. But since it was quick to write, I lived with it.

    The port to SWT game me the opportunity to add new features that I really wanted:

  • a dialog that would let me change database drivers, urls, username and passwords on the fly.
  • an editor that supported Undo. The Swing JTextArea doesn't come with this feature built it, although it does support cut and paste. SWT's Text widget has full support for cut, copy, paste and undo.
  • a series of tabbed editors that would let me slip into a Jython command line so that I could execute some Java code against the database.
  • I never got around to threading off calls to the database using the Swing version. This often meant displays would not paint properly, particularly in SQL calls which tool a while to execute. Using SWT, I could easily thread off SQL calls.

    SashForm, TabFolder, Text

    The main display area of SqlView is a Text editor at the top of the application window and a read-only results area at the bottom display. In the Swing version, a SplitPane was utilized to let a user resize the two widgets. This is almost essential for an application like SqlView because, at one time or another, you want to see more of the editor; at another moment, you are trying to view as many results as possible. In SWT, this functionality is provided with a SashForm.

    To create a SashForm, you need to pass the parent control and an argument that states whether the sash should move vertically or horizontally.

    	SashForm form = new SashForm(shell,SWT.VERTICAL);
    	form.setLayoutData(new GridData(GridData.FILL_BOTH));
    

    In the second line above, the SashForm utilizes a LayoutData specification that lets the SashForm grab all available horizontal and vertical space in the Window. This allows it to resize as the Window resizes.

    The TabFolder and Text placed on the SashForm adopt the behaviour of expanding as the SashForm does.

    	TabFolder tabFolder = new TabFolder (form, SWT.BORDER);
    			
    	TabItem tItem = new TabItem (tabFolder, SWT.NULL);
    	tItem.setText ("SQL");
    
    	sqlText = new Text(tabFolder,SWT.MULTI|SWT.WRAP|SWT.V_SCROLL|SWT.BORDER );
    	sqlText.setText(this.sqlData.getFileText(FILENAME));
    	sqlText.setFont(font);
    	sqlText.setEditable(true);
    	tItem.setControl(sqlText);
    

    TabItem objects are placed on the TabFolder. You need to call the setText() method to place a caption on the top of the folder and then call setControl() to pass a reference to the widget you want placed on each TabItem. In the above example, the sqlText widget is placed on the first TabItem.

    This is repeated to create the second TabItem ("Script").

    	TabItem tItem2 = new TabItem (tabFolder, SWT.NULL);
    	tItem2.setText ("Script");
    
    	
    	scriptText = new Text(tabFolder,SWT.MULTI|SWT.WRAP|SWT.V_SCROLL|SWT.BORDER );
    	scriptText.setText("Type Script Here...");
    	scriptText.setFont(font);
    	scriptText.setEditable(true);
    	tItem2.setControl(scriptText);
    

    Once the TabFolder is created, the SashForm has a no-wrap Text Widget placed below the TabFolder.

    	resultText = new Text(form,SWT.MULTI|SWT.V_SCROLL|SWT.H_SCROLL|SWT.BORDER );
    	resultText.setText(this.sqlData.getConnectionProperties());
    	resultText.setFont(font);
    	resultText.setEditable(false);
    

    It is important to note that this widget has the SashForm as its parent, just as the TabFolder. The Text widget is specified as multi-line, scrolling in both directions and containing a border.

    Adding a ToolBar

    The Toolbar can contain any number of ToolItems, each representing a Button, CheckBox, Combo Dropdown, Radio Button or Separator. The latter is used as a spacing mechanism to organize groups of buttons. The ToolBar can be created again by passing a reference to the Shell.

    	ToolBar bar = new ToolBar (shell, SWT.BORDER);
    	bar.setLayoutData(new GridData(GridData.HORIZONTAL_ALIGN_FILL));
    

    Placement of the ToolBar, whether to the top, bottom or side, is a factor of how you place it against adjacent Widgets. For example, SqlView uses a one-column GridLayout. It has a ToolBar and a SashForm as children. If you create the SashForm first, the ToolBar resides at the bottom of the screen. If you create the ToolBar first, it sits at the top. Unlike other windowing systems, SWT treats the ToolBar like any other component. The downside is that if you want a floatable or moving ToolBar, you will have to break out some serious drag-and-drop code.

    The second line above, where you set the LayoutData with a new GridData, is essential for getting the ToolBar to fill all horizontal space in the Shell. If you don't want this, the ToolBar remains only as wide as its child widgets.

    The next step is to add Buttons or Separators to the ToolBar. The layout is left-to-right and, unless you specify something else, the Button consumes only as much real estate as they require.

    	ToolItem sqlButton = new ToolItem (bar, SWT.PUSH);
    	sqlButton.setText (RUN_SQL);
    	sqlButton.addSelectionListener(this);
    

    As with all SWT widgets,You specify the type of ToolItem in the constructor. You can also opt for SWT.PUSH, SWT.CHECK, SWT.RADIO, SWT.SEPARATOR, SWT.DROP_DOWN . Beyond that, the ToolItems are no more than Buttons.

    Separators are a little different.

    new ToolItem(bar,SWT.SEPARATOR);
    

    The above code will give you about 5 pixels.

    BusyIndicator, your friendly hourglass icon...

    A common condition in a Swing app -- and not altogether rare in a GUI app -- is to experience a non-painting window condition when the main thread has invoked some long process that takes minutes to return. With Swing, the screen goes gray and remains unresponsive until the app completes. SWT is a little more forgiving, but it will eventually stop refreshing the screen.

    The correct way to handle this in Swing or SWT is to start the lengthy process in its own thread, so that the GUI can repaint as necessary. In most windowing systems, an hourglass icon is presented during the wait.

    In SWT, to create the effect of the Operating System's default Busy state, you can utilize a BusyIndicator object, passing it a Display and a Runnable. In this version of SqlView, the Runnable's run() method is responsible for spawning the lengthy SQL call.

    This sounds simple enough, but really isn't simple at all. If you check the Eclipse site, they provide an example in which a notifier thread spins off a Worker thread. This is all done in-line and seems a little confusing. In SqlView, a top-level Runnable, called BusyNotifier kicks off yet another Runnable-type object, called SqlDataNotifer, that actually does the SQL call. While that other thread is processing the top-level sits in a while loop waiting for its child thread to set a signal.

    From the main thread, things are pretty straightforward.

    	SqlDataNotifier sdn = new SqlDataNotifier(this.sqlData,sqlText.getSelectionText());
    	BusyNotifier bn = new BusyNotifier(display, shell, sdn);
    	BusyIndicator.showWhile(display, bn);
    	this.resultText.setText(sdn.getResults());
    
    

    The BusyNotifier Runnable waits for the SqlDataNotifier to indicate it is finished the call.

    	new Thread( worker).start();
    	
    	while (!worker.isDone() && !shell.isDisposed()) {
    	   if (!display.readAndDispatch())
    		display.sleep();
    	   }
    	}
    

    The SqlDataNotifier's run() method is equally straightforward.

    	long duration = System.currentTimeMillis();
    	results = sqlData.getData(sql);
    	duration = System.currentTimeMillis() - duration;
    	sqlData.result =  results + "\n********************\n Ticks: " + duration ;
    	this.done = true;
    

    What is not straightforward is the communication between the 3 threads. The BusyNotifier requires a reference to the Display, the Shell and the SqlDataNotifier. This BusyNotifier is useful in other applications because it has no reference to other classes, except SqlDataNotifier. I have tried to ease this dependency by creating an interface called Notifiable, which forces SqlDataNotifier to implement an isDone() method.

    The BusyIndicator doesn't give you much control over the shape of the mousepointer. However, it behaves much more reliably than some implementations that merely start a mousepointer before and after the lengthy call. The main benefit is that the display is able to handle repaint calls from the OS. You won't get the white vanishing display that makes you wonder if the app had gone into a state of confusion. The repaints are OK. You can even (despite the hourglass...) click on another button and kick off another event. The original event will continue using its own thread and update the GUI appropriately when it returns.

    GOTCHA: A problem with the use of inner classes for events is that you have to ensure that all external references, like the sqlData variable above, is declared final. This normally isn't a problem, except that in SWT, if you want to reference a Text that was created inside a SashForm and placed on a Shell, the Text, SashForm and Shell need to be declared final. In more complicated layouts, this scheme developes into a world of finality that is not pleasant. I find it better to create data structures with the "final" keyword and have the Control references them outside the inner class. The lesson here is that in a real, complex application, you might want to avoid the inner classes.

    Creating dialogs

    To allow the user to change properties on the fly, I wanted to implement a little modal Dialog box that contained a property sheet table and a Close button.

    In ST, all dialog windows should subclass the abstract org.eclipse.swt.widgets.Dialog which provides two constructors, one that accepts a Shell or another that takes a Shell and a style integer. The Shell is necessary to root the dialog in a chain of windows stemming from the main Shell. The style integers will help set up preferences for the dialog. It should be one of SWT.APPLICATION_MODAL, SWT.MODELESS, SWT.PRIMARY_MODAL, SWT.SYSTEM_MODAL. For the purpose of this application, I wanted the dialog to appear over the main window, rendering its parent non-actionable while it was showing. Therefore, I chose SWT.APPLICATION_MODAL.

    For the purpose of SqlView properties, I supplied one constructor that accepted a Shell, a style int and a Properties file. I also supplied a method called open() that performed the main Dialog display code:

    	Shell shell = new Shell(this.getParent(), SWT.DIALOG_TRIM | SWT.APPLICATION_MODAL); 
    	shell.setText("Properties...");
    	shell.setBounds(this.getCenteredRectangle());
    	shell.open(); 
    		
    	this.showWidgets(shell);
    		
    	Display display = this.getParent().getDisplay(); 
    	while (!shell.isDisposed()) { 
    		if (!display.readAndDispatch()) 
    			display.sleep(); 
    		} 
    	return properties; 
    
    

    Like its parent window, the PropertiesDialog open() method starts up the Shell, invokes some positioning code, then falls into a check and sleep mode until the dialog's Shell is disposed.

    The Dialog needs to be displayed relative to its parent. For SqlView, the objective was to center the dialog within the main window, unless the parent was itself positioned off-screen. To center the dialog, it is necessary to pass its setBounds() method a Rectangle indicating the top, left, right and bottom position. Since the dialog was designed for a 200 by 200 pixel width, this code accomplishes the positioning:

    	Rectangle parentRect = this.getParent().getBounds();
    	int x = (int)((parentRect.width + parentRect.x)/2);
    	int y = (int)((parentRect.height + parentRect.y)/2);
    		
    	return new Rectangle(x,y,200, 200 ); 
    

    It is also possible to create window positioning code using the Display's getBounds() method, which will return something like Rectangle {0, 0, 1400, 1050} indicating the screen width and height.

    Tables and how to edit them

    SWT Table objects are a handy mechanism for displaying and editing tabular data sets like a SqlViewProperties file. Unlike Swing's JTable, the display is fast. However, it now lacks some of the refinement of JTable, particularly the ability to create specialized Renderer objects, those unique widgets that customize the table (for example, a Renderer widget could display both Text and Image in one cell).

    The SWT table implementation requires a relationship between a Table and an array of TableItem objects. You could create one Table instance and 24 TableItems to show a 6 by 4 grid.

    	final Table table = new Table(shell, SWT.FULL_SELECTION | SWT.HIDE_SELECTION);
    	table.setBounds(0, 0, 200, 140);
    	table.setLinesVisible(true);
    
    	final TableItem[] items = new TableItem[properties.size() ]; // an item for each key and value.
    

    To populate the TableItems, SqlView get an Enumeration of Property keys and then iterates TableItem array.

    	Enumeration en = properties.propertyNames();
    	int i = 0;
    	while (en.hasMoreElements()){
    		key = (String) en.nextElement();
    		items[i] = new TableItem(table, SWT.NONE);
    		itemText = new String[2];
    		itemText[0] = key;
    		itemText[1] = (String) properties.get(key);
    		items[i].setText(itemText);
    		i++;
    	}
    

    This creates a grid with two evenly distributed columns. For SqlView, property key Strings were generally much shorter than the values, so it was necessary to set the first column narrower than the second.

    	new TableColumn(table, SWT.NONE).setWidth(80);
    	new TableColumn(table, SWT.NONE).setWidth(120);
    
    

    A user needs to edit the second column, but leave the Property keys read-only. Of course, the default Table is read-only. So, a TableEditor must intercede to support editing. TableEditor objects resemble Text objects, but, in fact, they wrap Text objects. You use the Control obtained from this getEditor() method to set any new text.

    	final TableEditor editor = new TableEditor(table);
    	editor.horizontalAlignment = SWT.LEFT;
    	editor.grabHorizontal = true;
    	editor.minimumWidth = 50;
    	// editing the second column
    	final int EDITABLECOLUMN = 1;
    
    	table.addSelectionListener(new SelectionAdapter() {
    		public void widgetSelected(SelectionEvent e) {
    			// Clean up any previous editor control
    			Control oldEditor = editor.getEditor();
    			if (oldEditor != null) oldEditor.dispose();
    
    			// Identify the selected row
    			TableItem item = (TableItem)e.item;
    			if (item == null) return;
    
    			// The control that will be the editor must be a child of the Table
    			Text newEditor = new Text(table, SWT.NONE);
    			newEditor.setText(item.getText(EDITABLECOLUMN));
    			newEditor.addModifyListener(new ModifyListener() {
    				public void modifyText(ModifyEvent e) {
    					Text text = (Text) editor.getEditor();
    					editor.getItem().setText(EDITABLECOLUMN, text.getText());
    				}
    			});
    			newEditor.selectAll();
    			newEditor.setFocus();
    			editor.setEditor(newEditor, item, EDITABLECOLUMN);
    		}
    	});
    
    

    Lessons learned

    Implementing a new SWT face to the old Swing application was relatively easy. For one thing, most of the application grunt work was done in the SqlData class, which handles the JDBC connections and resultsets. The SWT work, even for a newbie, isn't that difficult. There are a few patterns, like the Widget constructors that require a reference to a parent, that take a while to get used to. Most of the development is a lot like Java Swing or AWT. Events are similar and SWT LayoutManagers are no more contankerous than Swing's.

    However,there are a couple of things about SWT that require close attention. Unlike most Java APIs, including most of Swing, you cannot rely on the JVM to clean up resources. SWT provides a strong mechanism in the Shell and Widget constructor chain so that when, utimately, the top-level Shell is disposed, all child widgets are disposed as well.

    However, there are resources like Fonts, that are not included in the Shell chain. For this reason, you should note carefully this code, which ends the application.

    	while (!shell.isDisposed ()) {
    		if (!display.readAndDispatch ()) {
    			temp = sqlText.getText();
    			display.sleep ();
    		} 
    	} 
    		
    	//we are shutting down. Save the buffer to file
    	this.sqlData.write(temp, FILENAME);
    		
    	this.getMonospacedFont().dispose();
    	display.dispose ();
    

    You need to keep references to all resources like the monospaced Font above. This is even more important for Images you create in the application. Without this rigor in closing down resources, you leave potential memory leaks.

    Another gotcha -- when you run the SqView class, you need to set a System propertyso that SWT can utilize the SWT runtime libraries. To do this, you can set an argument on the command line. -Djava.library.path="C:\SWT" is sufficient. However, if you are running some other jars that require this variable, you may run into trouble. For example, I attempted this technique using the IBM DB2 JDBC driver. When I went to run a SQL command, I got an error "No suitable driver." As it turned out, DB2 needed to point the java.library.path variable to "C:\SQLLIB\BIN" and I needed to append it to the command line argument.


    Copyright (c)2004 Gervase Gallant gervasegallant@yahoo.com