2011-11-03 55 views
0

对不起,有点长,但它是一个有点麻烦...SwingWorker的过程()与聚结块GUI更新困难

SwingWorker的工作完全按预期在我的应用程序,除了一个棘手的问题,我”因为API清楚说明的是完全可能的和正常的,所以如果大块在过程()中合并,则努力解决。

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

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

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

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

如果我设计了一些东西,使doInBackground在发布块之间稍有延迟,那么JDialog会更新OK。显然,通过合并的块,我使用一个循环来一个接一个地循环,所以我想在每个循环的末尾放一个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中一个接一个地执行......我已经尝试过在“块处理循环”结尾处运行一个短的Timer,并尝试使用Thread.yield()...实质上我试图强制GUI全面更新JDialog及其所有组件。在开始更新JTable之前...

任何想法都会被赞赏。

+0

然后提供了一个[SSCCE](http://sscce.org/),其表现出你描述的问题。 – trashgod

+0

@trashgod公平点...将对此工作。我使用的是Jython而不是Java,但会尝试使沙漏起来 –

+1

有一个最小的例子[这里](http://stackoverflow.com/questions/4637215/can-a-progress-bar-be-used-in- a-class-outside-main/4637725#4637725)和[这里](https://sites.google.com/site/drjohnbmatthews/randomdata)。这听起来像你可能一次更新整个模型,而不是逐步更新。 – trashgod

回答

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

的人绊脚石就这个问题和担心下面的语无伦次的评论,我认为这是公平的假设此评论可以安全忽略:首先,我看不到任何paint的地方,现在立即被设计为在EDT之外执行,其次,在并发意义上,死锁只发生在两个线程之间共享可变对象的情况下:因此,在这些块的循环迭代中美国东部时间这是错误的,在我看来。

另一个变化,以上面的代码

Java API来awt.Dialog.show():“这是允许的,显示来自事件调度线程的模式对话框,因为该工具包会确保另一个事件泵运行而调用此方法的则被阻止“。这意味着如果DISPLAY_WAIT_FOR_TASK是传递给process()的最后一个块,我们就可以了:另一个事件泵运行在m_jDialog.setVisible(true)之后,并且这个新的事件泵处理下一个对process()的调用。相反,如果一个块要与DISPLAY_WAIT_FOR_TASK合并(即,如果另一个跟随它在同一个进程()调用中),则代码将在setVisible(true)处阻塞,并且循环将继续处理下一个只有当JOptionPane已被用户“编程”或以编程方式进行时才会有块。

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

显然对于本JOptionPane的特殊螺纹可以当场创建或从可用的线程池,ExecutorService的征等

+0

但您必须测试,总是'if(!SwingUtilities.isEventDispatchThread()){' – mKorbel

+0

呵呵?解释,prosím...你是否说paintImmediately必须从非EDT调用?为什么这样?参考链接将不胜感激! Vd'aka –

+0

<''first language'> 1)paintImmediately()musi byt pocas EDT,2)pozri si moj profil,kde tu sa venujem SwingWorker,vsetky Objekty musis mat predpripravene mimo SwingWorker,toto v tom code je jedna velka blbost,我仍然认为SwingWorker只是关于向JComponents添加/更改/删除值,而不是创建/修改/删除JComponents,仍然最安全的方式是使用Runnable#Thread作为未来 – mKorbel

1

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

这可能是问题的核心。 JTableflyweight pattern中使用renderers。通过限制对可见行的更新,在process()内增量更新模型的成本最小化; publish()通常是速率限制步骤,并且simpleexamples通常使用sleep()来模拟延迟。

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

+0

谢谢......但你认为你可能看看SSCCE吗?正如你所看到的,JDialog在任何事情完成之前都改变了JTable ...神秘的是如何告诉GUI更新它的显示,然后传递到下一个阶段,用一个新的向量填充TableModel .. –

+0

@ Devon_C_Miller的分析似乎是正确的。 – trashgod

2

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

所以,试试这个:

而是直接执行该公司在if (value == UPDATE_TABLE_IN_GUI)块的代码,把它放在一个方法。在Runnable中调用该函数,并使用SwingUtilities.invokeLater()来安排执行。

这将允许EDT建立表

更新

的EDT之前处理排队的事件有Runnables队列,它执行。 Swing组件的更改队列Runnables供以后执行。这通常是一件好事。当你设置一个标签的文本,前景和背景时,你并不是真的想等待它们之间的重画。

美国东部时间将不会继续下一个Runnable,直到它完成当前的一个。 process()方法是从其中一个Runnables调用的。所以,让EDT运行其他更新的唯一方法是从process()返回。 SwingUtilities.invokeLater()是最简单的方法。

至于JDialog标题,一些LAF委托给本地窗口管理器(X或MS Windows)。这个标题很可能没有被美国东部时间彩绘。

+0

+1这是有道理的。 – trashgod

+0

非常感谢,工作确定。几点要点:首先,从EDT内部调用SwingUtilities.invokeLater()似乎很奇怪,但我可以忍受它...第二,我想知道为什么没有某种方式将EDT设置为空闲编程,让事件“发挥出来......”,而且毫无疑问将只是一个谜,但我很疑惑为什么有可能重置JDialog的标题(通过即时更新),但其他事情拒绝让步。出于这个原因,我最初尝试验证(),但无济于事...... –

+0

只是为了阐述“我可以忍受它”:SwingWorker非常棒,因为它允许你将EDT事件置于某种合理的控制之下......通过在进程框架之外重新提交一个Runnable给EDT调度器(块) ,这让我觉得自己是一个财富的人质,这是一种并发混乱的后果,它不是一个美丽的头脑。只是一个想法... –