0

对不起,有点长,但它有点涉及......

SwingWorker 在我的应用程序中完全按预期工作,除了我正在努力解决的一个棘手问题,如果块到达 process() 合并,正如 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 上尝试了无数的“验证”、“绘制”和“重绘”排列。

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

注意我还尝试了其他方法:如果它们是多个,则重新发布块。这样做的问题是,鉴于事物的异步性质,它可能导致以错误的顺序发布块,就像在 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 中一个接一个地执行......我尝试过在结束时运行一个短 Timer 之类的东西“块处理循环”,并尝试使用 Thread.yield() ......基本上我试图强制 GUI 全面更新 JDialog 及其所有组件......在继续更新 JTable 之前......

任何想法表示赞赏。

4

3 回答 3

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 等中获取。

于 2011-11-05T07:41:38.237 回答
2

当您在 JDialog 上设置值时,Swing 正在调度重绘事件。当您的代码通过构建模型运行时,这些事件仍在等待 EDT 线程空闲。一旦你的工作完成,线程就会空闲并且延迟的事件会播放。

所以,试试这个:

与其直接执行if ( value == UPDATE_TABLE_IN_GUI )块中的代码,不如将其放入方法中。在 a 中包装对它的调用Runnable,并使用它SwingUtilities.invokeLater()来安排执行。

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

更新

EDT 有一个Runnables它执行的队列。对 Swing 组件的更改排队Runnables等待稍后执行。这通常是一件好事。当您设置标签的文本、前景和背景时,您并不想等待它们之间的重绘。

EDT 在完成当前的之前不会继续下Runnable一个。从这些 Runnable 之一调用 process() 方法。因此,让 EDT 运行其他更新的唯一方法是从process(). SwingUtilities.invokeLater() 是最简单的方法。

至于 JDialog 标题,一些 LAF 将其委托给本地窗口管理器(X 或 MS Windows)。标题可能不是由 EDT 绘制的。

于 2011-11-04T12:44:32.687 回答
1

如果您的意思是我是否TableModel一次设置整个向量,是的。

这可能是问题的核心。以享元模式JTable使用渲染器。通过限制对可见行的更新,将模型增量更新的成本降到最低;通常是速率限制步骤,简单的示例通常使用.process()publish() sleep()

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

于 2011-11-03T22:28:51.467 回答