SwingWorker进程()使用合并的块更新GUI的难度

时间:2011-11-03 18:11:40

标签: java swing asynchronous swingworker event-dispatch-thread

抱歉,有点长,但有点涉及......

SwingWorker在我的应用程序中完全按预期工作,除了一个棘手的问题,我正在努力解决,如果块进入进程()合并,因为API明确指出是完全可能和正常的。

问题出现了,例如,当我有一个JDialog开始时说“任务正在发生,请稍候”:所以一个块发布在doInBackground(),然后到达process()并设置一个JDialog。

当doInBackground中的冗长任务完成时,我“再发布”2个命令:一个说“将JDialog的消息更改为”等待GUI更新“”,另一个说“用结果填充JTable” “送你”。

关于这一点的一点是,如果你发送一个JTable大量的新数据来替换它的TableModel的向量,Swing实际上可以花费不可忽视的时间来更新自己...因此我想告诉用户:“冗长的任务已经完成,但我们现在正在等待Swing更新GUI”。

奇怪的是,如果这两个指令以2个合并的块的形式到达,我发现JDialog只能部分更新:setTitle(“blab”)导致JDialog的标题被更改......但所有其他更改JDialog被搁置......直到JTable的主GUI更新完成。

如果我设计的东西使得在发布块之间doInBackground稍有延迟,那么JDialog会更新。显然,对于合并的块,我使用循环逐个遍历它们,所以我想在每个循环结束时放置一个Timer。这没有效果。

我也在JDialog上尝试了无数的“验证”,“绘画”和“重绘”的排列。

因此,问题是:如何让我在处理合并块的迭代之间的process()内更新GUI。

NB我也尝试了其他的东西:如果它们是多个,则重新发布块。这样做的问题在于,鉴于事物的异步性,它可能导致块以错误的顺序发布,因为在doInBackground中,不可避免地会继续发布。此外,这种解决方案还不够优雅。

后... 根据要求,这是一个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 );
    }

}

...该程序包括5个阶段:1)设置GUI 2)发出一条消息说“等待任务完成”3)“冗长的”非EDT任务4)更改为消息,以便它现在说“等待GUI更新表”5)更新GUI中的表(然后处理JDialog / JOptionPane)。

我不明白的原因是,如果你在上面的doInBackground中注释掉Thread.sleep()行,那么JDialog表现得很奇怪:标题随后会更新,但是JOptionPane的文本不会改变, “取消”按钮未被删除。

可以看出,不同的是没有Thread.sleep()行,两个块到达合并,并在EDT中一个接一个地执行...我已经尝试过像在运行一个短的定时器结束“块处理循环”,并尝试使用Thread.yield()......本质上我试图强制GUI全面更新JDialog及其所有组件......继续更新JTable ......

任何想法都赞赏。

3 个答案:

答案 0 :(得分:2)

在JDialog上设置值时,Swing正在安排重绘事件。当代码运行构建模型时,这些事件仍在等待EDT线程空闲。完成工作后,线程处于空闲状态,延迟事件就会发生。

所以,试试这个:

不是直接执行if ( value == UPDATE_TABLE_IN_GUI )块中的代码,而是将其放在方法中。在Runnable中对该调用进行换行,并使用SwingUtilities.invokeLater()来安排执行。

这将允许EDT在构建表之前处理排队事件

<强>更新

EDT的队列为Runnables。对Swing组件队列Runnables的更改以供稍后执行。这通常是件好事。设置标签的文本,前景和背景时,您实际上并不想等待每个标签之间的重新绘制。

EDT不会继续下一个Runnable,直到它完成当前的process()。 process()方法是从其中一个Runnables调用的。因此,让EDT运行其他更新的唯一方法是从{{1}}返回。 SwingUtilities.invokeLater()是最简单的方法。

对于JDialog标题,一些LAF将其委托给本机窗口管理器(X或MS Windows)。很可能该标题不是由美国东部时间绘制的。

答案 1 :(得分:2)

破解它! - paintImmediately()可以做到这一点:

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 );

对于任何磕磕绊绊的人并担心下面的语无伦次的评论,我认为可以安全地忽略这个评论是公平的:首先,我认为没有证据表明paintImmediately设计为在EDT之外执行,其次是死锁在并发意义上,只发生在两个线程之间共享的可变对象:因此,在EDT中这些块的循环迭代中,这在我看来是错误的。

对上述代码的另一项更改

awt.Dialog.show()的Java API:“允许从事件调度线程显示模态对话框,因为工具包将确保在调用此方法的事件被阻止时运行另一个事件泵”。这意味着如果DISPLAY_WAIT_FOR_TASK是传递给process()的最后一个块,我们就可以了:另一个事件泵在m_jDialog.setVisible(true)之后运行,并且这个新的事件泵处理对process()的下一个调用。

相反,如果一个块与DISPLAY_WAIT_FOR_TASK合并(即如果另一个在同一个process()调用中跟随它),则代码将在setVisible(true)处阻塞,并且循环将继续处理下一个块只有当JOptionPane被用户“处理”或以编程方式“处理”时。

为了防止这种情况,并在此setVisible()命令之后立即继续运行,必须让单个命令m_jDialog.setVisible(true)在其自己的(非EDT)线程中运行(NB JOptionPane是设计为在EDT或非EDT中运行。

显然,JOptionPane的这个特殊线程可以在现场创建,也可以从可用的线程池,ExecutorService等中获取。

答案 2 :(得分:1)

  

如果你的意思是我一次设置TableModel的整个向量,确实是。

这可能是问题的核心。 JTablerenderers中使用flyweight pattern。通过限制对可见行的更新,最小化在process()内递增更新模型的成本; publish()通常是限速步骤,simple examples通常会使用sleep()来模拟延迟。

TableModel派生的DefaultTableModel很方便,但它在内部使用(同步)java.util.VectorAbstractTableModel是一种允许在所选数据结构中拥有更多自由度的替代方案。