Сложность обновления графического интерфейса в SwingWorker process () с помощью объединенных кусков - PullRequest
0 голосов
/ 03 ноября 2011

Извините, немного долго, но это немного связано ...

SwingWorker работает полностью, как и ожидалось, в моем приложении, за исключением одной запутанной проблемы, которую я изо всех сил пытаюсь решить, в случае, если куски поступают в process (), объединившись, поскольку API ясно заявляет, что это вполне возможно и нормально.

Проблема возникает, например, когда у меня есть JDialog, который начинается со слов «задача выполняется, пожалуйста, подождите»: таким образом, блок публикуется в doInBackground(), который затем поступает в process() и устанавливает JDialog.

Когда длинная задача в doInBackground закончилась, я «публикую» еще 2 команды: одна говорит «изменить сообщение JDialog на« ожидание обновления GUI »», а другая говорит «заполнить JTable результатами, которые я Я посылаю тебе ".

Суть в том, что, если вы отправляете JTable большое количество новых данных для замены вектора его TableModel, Swing может на самом деле занять незначительное время для обновления самого себя ... по этой причине я хочу сказать, пользователь: «длинная задача завершена, но теперь мы ждем, пока Swing обновит графический интерфейс».

Странно то, что если эти две инструкции приходят в виде двух объединенных кусков, я нахожу, что JDialog может обновляться только частично: setTitle ("blab") приводит к изменению заголовка JDialog ... но все другие изменения к JDialog удерживаются ... до тех пор, пока не закончится основное обновление графического интерфейса JTable.

Если я спроектирую вещи так, чтобы в doInBackground была небольшая задержка между публикацией фрагментов, обновления JDialog в порядке. Очевидно, что для объединенных кусков я использую цикл, чтобы проходить их один за другим, поэтому я решил поставить таймер в конце каждого цикла. Это не имело никакого эффекта.

Я также пробовал неисчислимые сочетания «validate», «paint» и «repaint» на JDialog.

Поэтому возникает вопрос: как заставить меня обновлять графический интерфейс внутри процесса () между итерациями, связанными с объединенными кусками.

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) настроить графический интерфейс 2) вывести сообщение с надписью «дождаться завершения задачи» 3) «длинная» задача без EDT 4) изменить сообщение так, что теперь оно говорит «подождите, пока графический интерфейс обновит таблицу» 5) обновление таблицы в графическом интерфейсе (с последующим удалением JDialog / JOptionPane).

Я не понимаю, почему, если вы закомментируете строку Thread.sleep () в doInBackground выше, JDialog ведет себя странно: заголовок затем обновляется, но текст JOptionPane не изменяется, и кнопка «Отмена» не удаляется.

Видно, что разница в том, что без строки Thread.sleep () два чанка объединяются и выполняются один за другим в EDT ... Я пробовал такие вещи, как запуск короткого таймера на конец "цикла обработки чанка" и эксперименты с Thread.yield () ... по сути, я пытаюсь заставить графический интерфейс полностью обновить JDialog и все его компоненты ... ПЕРЕД переходом к обновлению JTable ...

Любые мысли приветствуются.

Ответы [ 3 ]

2 голосов
/ 04 ноября 2011

Когда вы устанавливаете значения в JDialog, Swing планирует события перерисовки. Когда ваш код выполняет построение модели, эти события все еще ждут, пока поток EDT не будет работать. Как только ваша работа закончена, поток простаивает и отсроченные события воспроизводятся.

Итак, попробуйте это:

Вместо непосредственного выполнения кода, который находится в блоке if ( value == UPDATE_TABLE_IN_GUI ), поместите его в метод. Оберните вызов в Runnable и используйте SwingUtilities.invokeLater(), чтобы запланировать его выполнение.

Это позволит EDT обрабатывать события в очереди перед созданием таблицы.

Обновление

EDT имеет очередь Runnables, которую он выполняет. Изменения в очереди компонентов Swing Runnables для последующего выполнения. Это вообще хорошая вещь. Когда вы устанавливаете текст метки, передний план и фон, вам не нужно ждать перерисовки между каждым из них.

EDT не перейдет к следующему Runnable, пока не завершит текущий. Метод process () вызывается из одного из этих Runnables. Таким образом, единственный способ позволить EDT запустить другое обновление - это вернуться с process(). SwingUtilities.invokeLater () - это самый простой способ сделать это.

Что касается заголовка JDialog, некоторые LAF делегируют его собственному оконному менеджеру (X или MS Windows). Вполне вероятно, что название не написано EDT.

2 голосов
/ 05 ноября 2011

Взломал его! - 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 это неправильно, по моему мнению.

Еще одно изменение в приведенном выше коде

Java API для awt.Dialog.show (): «Разрешается показ модальных диалогов из потока диспетчеризации событий, потому чтоИнструментарий гарантирует, что еще один насос событий будет запущен, а тот, который вызвал этот метод, заблокирован ».Это означает, что если 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 и т. Д.

1 голос
/ 04 ноября 2011

Если вы имеете в виду, я устанавливаю весь вектор TableModel одновременно, да, действительно.

Это может быть причиной проблемы. JTable использует визуализаторов в шаблоне веса в полете . Ограничивая обновления видимыми строками, стоимость обновления модели постепенно в пределах process() сводится к минимуму; publish() обычно является ограничивающим скорость шагом, а простые примеры обычно имитируют задержку, используя sleep().

A TableModel, производный от DefaultTableModel, удобен, но он использует (синхронизирован) java.util.Vector для внутреннего использования. AbstractTableModel - это альтернатива, которая обеспечивает большую широту в выбранных структурах данных.

...