Sorry, bit long, but it is a bit involved...
SwingWorker works entirely as expected in my app, except for one knotty problem which I'm struggling to solve, in the event that chunks arrive in process() coalesced, as the API clearly states is perfectly possible and normal.
The problem comes, for example, when I have a JDialog which starts by saying "task happening, please wait": so a chunk is published in doInBackground()
which then arrives in process()
and sets up a JDialog.
When the lengthy task in doInBackground has finished I "publish" 2 more commands: one says "change the message of the JDialog to "waiting for GUI to update"", and the other says "populate the JTable with the results I'm sending you".
The point about this is that, if you are sending a JTable a large amount of new data to replace its TableModel's vector, Swing can actually take a non-negligible time to udpate itself... for that reason I want to tell the user: "the lengthy task has finished, but we're now waiting for Swing to update the GUI".
What is strange is that if these two instructions arrive as 2 coalesced chunks I find that the JDialog is only capable of being partially updated: setTitle( "blab" ) results in the JDialog's title being changed... but all other changes to the JDialog are put on hold ... until the main GUI update of the JTable has finished.
If I engineer things so that there is a slight delay in doInBackground between publishing the chunks the JDialog updates OK. Obviously, with coalesced chunks I am using a loop to go through them one by one, so I thought of putting a Timer at the end of each loop. This had no effect.
I've also tried innumerable permutations of "validate" and "paint" and "repaint" on the JDialog.
The question therefore is: how get I get the GUI to update itself within process() between iterations dealing with coalesced chunks.
NB I also tried something else: republishing chunks if they are multiple. The trouble with this is that, given the asynchronous nature of things, it can result in chunks being published in the wrong order, as back in doInBackground, inevitably, things are continuing to be published. Plus, this kind of solution is just inelegant.
later... as requested, here is an SSCCE:
import javax.swing.*;
import javax.swing.table.*;
import java.awt.*;
import java.util.*;
class Coalescence extends SwingWorker<Object, Object> {
int DISPLAY_WAIT_FOR_TASK = 0; int DISPLAY_WAIT_FOR_GUI_UPDATE = 1; int UPDATE_TABLE_IN_GUI = 2; int SET_UP_GUI = 3;
private Object[][] m_dataTable;
private JTable m_table;
private JFrame m_frame;
private JOptionPane m_pane;
private JDialog m_jDialog;
private FontMetrics m_fontMetrics;
private Dimension m_intercellSpacing;
@Override
protected Object doInBackground() throws Exception {
publish( SET_UP_GUI );
publish( DISPLAY_WAIT_FOR_TASK );
Random rand = new Random();
String s = "String for display, one two three four five six seven eight";
m_dataTable = new Object[ 20000 ][];
for( int i = 0; i < 20000; i++ ){
Object[] row = new Object[ 20 ];
for( int j = 0; j < 20; j++ ){
// random length string - so column width computation has something to do...
int endIndex = rand.nextInt( 40 );
row[ j ] = s.substring( 0, endIndex);
}
m_dataTable[ i ] = row;
// slow the "lengthy" non-EDT task artificially for sake of SSCCE
if( i % 10 == 0 )
Thread.sleep( 1L );
}
publish( DISPLAY_WAIT_FOR_GUI_UPDATE );
// *** LINE TO COMMENT OUT ***
Thread.sleep( 100L );
publish( UPDATE_TABLE_IN_GUI );
return null;
}
protected void process( java.util.List<Object> chunks){
p( "no chunks " + chunks.size() );
// "CHUNK PROCESSING LOOP"
for( int i = 0, n_chunks = chunks.size(); i < n_chunks; i++ ){
int value = (Integer)chunks.get( i );
p( "processing chunk " + value );
if( value == SET_UP_GUI ){
m_frame = new JFrame();
m_frame.setPreferredSize( new Dimension( 800, 400 ));
m_frame.setVisible( true );
JScrollPane jsp = new JScrollPane();
jsp.setBounds( 10, 10, 600, 300 );
m_frame.getContentPane().setLayout( null );
m_frame.getContentPane().add( jsp );
m_table = new JTable();
jsp.setViewportView( m_table );
m_frame.pack();
m_fontMetrics = m_table.getFontMetrics( m_table.getFont() );
m_intercellSpacing = m_table.getIntercellSpacing();
}
else if( value == DISPLAY_WAIT_FOR_TASK ){
m_pane = new JOptionPane( "Waiting for results..." );
Object[] options = { "Cancel" };
m_pane.setOptions( options );
// without these 2 sQLCommand, just pressing Return will not cause the "Cancel" button to fire
m_pane.setInitialValue( "Cancel" );
m_pane.selectInitialValue();
m_jDialog = m_pane.createDialog( m_frame, "Processing");
m_jDialog.setVisible( true );
}
else if ( value == DISPLAY_WAIT_FOR_GUI_UPDATE ){
// this if clause changes the wording of the JDialog/JOptionPane (and gets rid of its "Cancel" option button)
// because at this point we are waiting for the GUI (Swing) to update the display
m_pane.setOptions( null );
m_pane.setMessage( "Populating..." );
m_jDialog.setTitle( "Table being populated...");
}
else if ( value == UPDATE_TABLE_IN_GUI ){
Object[] headings = { "one", "two", "three", "four", "five", "six", "one", "two", "three", "four", "five", "six",
"one", "two", "three", "four", "five", "six", "19", "20" };
m_table.setModel( new javax.swing.table.DefaultTableModel( m_dataTable, headings ));
// lengthy task which can only be done in the EDT: here, computing the preferred width for columns by examining
// the width (using FontMetrics) of each String in each cell...
for( int colIndex = 0, n_cols = 20; i < n_cols; i++ ){
int prefWidth = 0;
javax.swing.table.TableColumn column = m_table.getColumnModel().getColumn( colIndex );
int modelColIndex = m_table.convertColumnIndexToModel( colIndex );
for( int rowIndex = 0, n_rows = m_table.getRowCount(); rowIndex < n_rows; rowIndex++ ){
Object cellObject = m_table.getModel().getValueAt( rowIndex, modelColIndex );
DefaultTableCellRenderer renderer = (DefaultTableCellRenderer)m_table.getCellRenderer( rowIndex, colIndex );
int margins = 0;
if( renderer instanceof Container ){
Insets insets = renderer.getInsets();
margins = insets.left + insets.right ;
}
Component comp = renderer.getTableCellRendererComponent( m_table, cellObject, true, false, rowIndex, colIndex);
if( comp instanceof JLabel ){
String cellString = ((JLabel)comp).getText();
int width = SwingUtilities.computeStringWidth(m_fontMetrics, cellString) + margins;
// if we have discovered a String which is wider than the previously set widest width String... change prefWidth
if( width > prefWidth ){
prefWidth = width;
}
}
}
prefWidth += m_intercellSpacing.width;
column.setPreferredWidth(prefWidth);
// slow things in EDT down a bit (artificially) for the sake of this SSCCE...
try {
Thread.sleep( 20L );
} catch (InterruptedException e) {
e.printStackTrace();
}
}
m_jDialog.dispose();
}
}
}
public static void main( String[] a_args ){
Coalescence c = new Coalescence();
c.execute();
try {
c.get();
} catch ( Exception e) {
e.printStackTrace();
}
}
static void p( String s ){
System.out.println( s );
}
}
... the program consists of 5 stages: 1) set up the GUI 2) put up a message saying "wait for the task to complete" 3) the "lengthy" non-EDT task 4) a change to the message so that it now says "wait for GUI to update the table" 5) updating of the table in GUI (followed by disposal of the JDialog/JOptionPane).
The thing I don't understand is why, if you comment out the Thread.sleep() line in doInBackground above, the JDialog behaves oddly: the title is then updated, but the text of the JOptionPane does not change, and the "Cancel" button is not removed.
It can be seen that the difference is that without the Thread.sleep() line, the two chunks arrive coalesced, and are performed one after another in the EDT... I have tried things like running a short Timer at the end of the "chunk processing loop", and experimenting with Thread.yield()... essentially I am trying to force the GUI to update the JDialog and all its components comprehensively ... BEFORE moving on to update the JTable...
Any thoughts appreciated.
Cracked it! - paintImmediately() does the magic:
m_pane.setOptions(null);
m_pane.setMessage("Populating...");
m_jDialog.setTitle("Table being populated...");
Rectangle rect = m_jDialog.getContentPane().getBounds();
((JComponent)m_jDialog.getContentPane()).paintImmediately( rect );
later
for anyone stumbling on this and worried about the incoherent comment below, I think it is fair to assume this comment can be safely ignored: firstly, I see no evidence anywhere that paintImmediately is designed to execute outside the EDT, and secondly deadlock, in a concurrency sense, occurs only with a mutable object shared between two threads: thus, in a loop iteration of these chunks in the EDT this is wrong, in my opinion.
Another change to the above code
Java API for awt.Dialog.show(): "It is permissible to show modal dialogs from the event dispatching thread because the toolkit will ensure that another event pump runs while the one which invoked this method is blocked". What this means is that if DISPLAY_WAIT_FOR_TASK is the last chunk delivered to process() we are OK: another event pump runs following m_jDialog.setVisible( true ), and this new event pump handles the next call to process().
Conversely, if a chunk were to be coalesced with DISPLAY_WAIT_FOR_TASK (i.e. if another follows it in the same process() call), the code would block at setVisible( true ), and the loop would move on to process the next chunk only when the JOptionPane had been "disposed" by the user or programatically.
To prevent this, and enable things to continue to run immediately after this setVisible() command, it is necessary to have the single command m_jDialog.setVisible( true ) run in its own (non-EDT) Thread (NB JOptionPane is designed to run in either the EDT or a non-EDT).
Obviously this special Thread for the JOptionPane can be created on the spot or enlisted from an available thread pool, ExecutorService, etc.