JScrollBar + JTextPane с HTML неправильно прокручивается до максимального значения

У меня есть следующая проблема в моем проекте, мне потребовалось некоторое время, чтобы выяснить, что является причиной проблемы, и я могу воспроизвести ее с помощью этого простого кода, который я прикрепил.

Я динамически добавляю контент в JTextPane с помощью HTMLEditorKit. Я отключил автопрокрутку, потому что я хочу управлять им вручную (когда пользователь прокручивает вверх, останавливается и когда запускается событие, чтобы снова активироваться).

Проблема теперь в том, что когда я устанавливаю значение JScrollBar равным его максимальному значению, оно становится другим, только на мгновение после вставки содержимого в HTMLDocument. Когда я запускаю setValue снова во второй раз вручную, он прокручивается до правильного максимального значения.

Кажется, JScrollBar не знает о правильном MaximumValue сразу после добавления в HTMLDocument и только (с задержкой) через некоторое время.

С помощью

caret.setUpdatePolicy(DefaultCaret.ALWAYS_UPDATE);

это не решение, потому что оно также не работает должным образом. Он также не прокручивается до максимального значения, оставляя пиксель просмотра ниже, чего я не хочу.

Вот полный код, воспроизводящий проблему. Если вы нажмете правую кнопку (добавить и прокрутить), он вставит элемент DIV в тело. В тот момент, когда достигается последняя видимая строка, она не прокручивается правильно до последнего максимального значения, последняя строка скрыта. Но когда вы просто нажимаете левую кнопку вручную, чтобы вызвать вторую функцию scrollToEnd(), она корректно прокручивается до максимального значения.

Код:

/*
 * To change this license header, choose License Headers in Project Properties.
 * To change this template file, choose Tools | Templates
 * and open the template in the editor.
 */
package javaapplication26;

import java.io.IOException;
import java.math.BigInteger;
import java.security.SecureRandom;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.swing.text.BadLocationException;
import javax.swing.text.DefaultCaret;
import javax.swing.text.Element;
import javax.swing.text.html.HTMLDocument;
import javax.swing.text.html.HTMLEditorKit;

public class NewJFrame extends javax.swing.JFrame {

    /**
     * Creates new form NewJFrame
     */
    public NewJFrame() {

        initComponents();

        this.setSize(500, 200);
        this.setLocationRelativeTo(null);

        this.jTextPane1.setEditorKit(new HTMLEditorKit());
        this.jTextPane1.setContentType("text/html");

        this.jTextPane1.setText("<html><body><div id=\"GLOBALDIV\"></div></body></html>");

        this.jScrollPane1.setHorizontalScrollBarPolicy(javax.swing.ScrollPaneConstants.HORIZONTAL_SCROLLBAR_NEVER);
        this.jScrollPane1.setVerticalScrollBarPolicy(javax.swing.ScrollPaneConstants.VERTICAL_SCROLLBAR_AS_NEEDED);

        DefaultCaret caret = (DefaultCaret) this.jTextPane1.getCaret();
        caret.setUpdatePolicy(DefaultCaret.NEVER_UPDATE);

        this.jScrollPane1.setAutoscrolls(false);
        this.jTextPane1.setAutoscrolls(false);
    }

    private void scrollToEnd() {

        this.jScrollPane1.getVerticalScrollBar().setValue(this.jScrollPane1.getVerticalScrollBar().getMaximum());
        //this.jTextPane1.setCaretPosition(this.jTextPane1.getDocument().getLength());
    }

    /**
     * This method is called from within the constructor to initialize the form.
     * WARNING: Do NOT modify this code. The content of this method is always
     * regenerated by the Form Editor.
     */
    @SuppressWarnings("unchecked")
    // <editor-fold defaultstate="collapsed" desc="Generated Code">                          
    private void initComponents() {

        jPanel1 = new javax.swing.JPanel();
        jScrollPane1 = new javax.swing.JScrollPane();
        jTextPane1 = new javax.swing.JTextPane();
        jPanel2 = new javax.swing.JPanel();
        jButton1 = new javax.swing.JButton();
        jButton2 = new javax.swing.JButton();

        setDefaultCloseOperation(javax.swing.WindowConstants.EXIT_ON_CLOSE);

        jPanel1.setLayout(new java.awt.BorderLayout());

        jScrollPane1.setViewportView(jTextPane1);

        jPanel1.add(jScrollPane1, java.awt.BorderLayout.CENTER);

        getContentPane().add(jPanel1, java.awt.BorderLayout.CENTER);

        jButton1.setText("Scroll to end");
        jButton1.addActionListener(new java.awt.event.ActionListener() {
            public void actionPerformed(java.awt.event.ActionEvent evt) {
                jButton1ActionPerformed(evt);
            }
        });
        jPanel2.add(jButton1);

        jButton2.setText("Add & scroll");
        jButton2.addActionListener(new java.awt.event.ActionListener() {
            public void actionPerformed(java.awt.event.ActionEvent evt) {
                jButton2ActionPerformed(evt);
            }
        });
        jPanel2.add(jButton2);

        getContentPane().add(jPanel2, java.awt.BorderLayout.PAGE_END);

        pack();
    }// </editor-fold>                        

    private void jButton2ActionPerformed(java.awt.event.ActionEvent evt) {                                         

        try {

            HTMLDocument doc = (HTMLDocument) this.jTextPane1.getDocument();
            HTMLEditorKit editorKit = (HTMLEditorKit) this.jTextPane1.getEditorKit();

            SecureRandom random = new SecureRandom();
            String htmlCode = "<div style=\"background-color: #FFFF22; height: 12px; font-size: 12;\">"+new BigInteger(64, random).toString(64)+"</div>";

            //editorKit.insertHTML(doc, doc.getLength(), htmlCode, 0, 0, null);
            Element element = doc.getElement("GLOBALDIV");

            if (element != null) {
                doc.insertBeforeEnd(element, htmlCode);
            }

            this.scrollToEnd();
        } catch (BadLocationException ex) {
            Logger.getLogger(NewJFrame.class.getName()).log(Level.SEVERE, null, ex);
        } catch (IOException ex) {
            Logger.getLogger(NewJFrame.class.getName()).log(Level.SEVERE, null, ex);
        }
    }                                        

    private void jButton1ActionPerformed(java.awt.event.ActionEvent evt) {                                         

        this.scrollToEnd();
    }                                        

    /**
     * @param args the command line arguments
     */
    public static void main(String args[]) {
        /* Set the Nimbus look and feel */
        //<editor-fold defaultstate="collapsed" desc=" Look and feel setting code (optional) ">
        /* If Nimbus (introduced in Java SE 6) is not available, stay with the default look and feel.
         * For details see http://download.oracle.com/javase/tutorial/uiswing/lookandfeel/plaf.html 
         */
        try {
            for (javax.swing.UIManager.LookAndFeelInfo info : javax.swing.UIManager.getInstalledLookAndFeels()) {
                if ("Nimbus".equals(info.getName())) {
                    javax.swing.UIManager.setLookAndFeel(info.getClassName());
                    break;
                }
            }
        } catch (ClassNotFoundException ex) {
            java.util.logging.Logger.getLogger(NewJFrame.class.getName()).log(java.util.logging.Level.SEVERE, null, ex);
        } catch (InstantiationException ex) {
            java.util.logging.Logger.getLogger(NewJFrame.class.getName()).log(java.util.logging.Level.SEVERE, null, ex);
        } catch (IllegalAccessException ex) {
            java.util.logging.Logger.getLogger(NewJFrame.class.getName()).log(java.util.logging.Level.SEVERE, null, ex);
        } catch (javax.swing.UnsupportedLookAndFeelException ex) {
            java.util.logging.Logger.getLogger(NewJFrame.class.getName()).log(java.util.logging.Level.SEVERE, null, ex);
        }
        //</editor-fold>

        /* Create and display the form */
        java.awt.EventQueue.invokeLater(new Runnable() {
            public void run() {
                new NewJFrame().setVisible(true);
            }
        });
    }

    // Variables declaration - do not modify                     
    private javax.swing.JButton jButton1;
    private javax.swing.JButton jButton2;
    private javax.swing.JPanel jPanel1;
    private javax.swing.JPanel jPanel2;
    private javax.swing.JScrollPane jScrollPane1;
    private javax.swing.JTextPane jTextPane1;
    // End of variables declaration                   
}

Эта замена кода работает, хотя, но оставляет небольшой пробел, также не правильно прокручивая до максимального значения:

this.jTextPane1.setCaretPosition(0);
this.jTextPane1.setCaretPosition(this.jTextPane1.getDocument().getLength());

1 ответ

Решение

Когда вы вставляете div в документ, модель документа обновляется немедленно. Тем не менее JTextPane только получает уведомление о том, что он недействителен и должен быть выложен. Это уведомление создает событие в EDT, которое обрабатывается только после завершения текущего события (вызванного нажатой кнопкой).

Таким образом, в тот момент, когда вы вызываете scrollToEnd()Переоценка JTextPane все еще в ожидании, а высота текстовой панели все еще слишком мала.

Чтобы получить правильную последовательность событий, вам нужно запланировать вызов scrollToEnd() в EDT, используя invokeLater:

SwingUtilities.invokeLater(new Runnable(){
    public void run(){
         scrollToEnd();
    }
});
Другие вопросы по тегам