SwingWorker process() GUI 更新困难与合并块

发布于 2024-12-13 22:53:03 字数 7737 浏览 3 评论 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 更新正常。显然,对于合并的块,我使用循环来逐一遍历它们,因此我考虑在每个循环的末尾放置一个计时器。这没有效果。

我还在 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 中相继执行...我尝试过在 Thread.sleep() 末尾运行一个短计时器之类的操作“块处理循环”,并尝试 Thread.yield()...本质上我试图强制 GUI 全面更新 JDialog 及其所有组件...在继续更新 JTable 之前...

任何想法表示赞赏。

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.

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(3

很糊涂小朋友 2024-12-20 22:53:04

如果你的意思是我要立即设置 TableModel 的整个向量,确实是的。

这可能是问题的核心。 JTable 使用 渲染器采用享元模式。通过限制对可见行的更新,可以最小化在 process() 内增量更新模型的成本; publish() 通常是限速步骤,并且 简单 示例通常使用sleep()模拟延迟。

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

If you mean am I setting the entire vector of TableModel at once, yes indeed.

This may be the heart of the problem. JTable uses renderers in a flyweight pattern. By limiting updates to visible rows, the cost of updating the model incrementally within process() is minimized; publish() is usually the rate-limiting step, and simple examples typically simulate latency using sleep().

A TableModel that derives from DefaultTableModel is convenient, but it uses (synchronized) java.util.Vector internally. AbstractTableModel is an alternative that allows more latitude in the chosen data structure(s).

陌伤浅笑 2024-12-20 22:53:03

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

因此,请尝试以下操作:

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

之前处理排队的事件。EDT

这将允许 EDT 在构建表Update

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

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

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

When you are setting values on the JDialog, Swing is scheduling repaint events. When your code runs through building the model, those events are still waiting for the EDT thread to be idle. Once your work is done, the thread is idle and the delayed events play out.

So, try this:

Instead of directly executing the code that's in the if ( value == UPDATE_TABLE_IN_GUI ) block, put it in a method. Wrap a call to that in a Runnable, and use SwingUtilities.invokeLater() to schedule that for execution.

That will allow the EDT to process the queued events before building the table

Update

The EDT has a queue of Runnables that it executes. Changes to Swing components queue Runnables for later execution. This is generally a good thing. When you set a label's text, foreground, and background, you don't really want to wait for a repaint between each of those.

The EDT won't go on to the next Runnable until it finishes the current one. The process() method is called from one of these Runnables. So, the only way to let the EDT run other update is to return from process(). SwingUtilities.invokeLater() is the easiest way to do that.

As for the JDialog title, some LAFs delegate that to the native window manager (X or MS Windows). It's likely that the title isn't being painted by the EDT.

和我恋爱吧 2024-12-20 22:53:03

破解了! - 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)线程中运行(注意 JOptionPane 设计为运行无论是 EDT 还是非 EDT)。

显然,JOptionPane 的这个特殊线程可以当场创建或从可用线程池、ExecutorService 等中征用。

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.

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文