2017-03-28 2 views
0

Ich habe das folgende Problem in einem Projekt von mir, dauerte eine Weile, um herauszufinden, was das Problem verursacht, und ich kann es mit diesem einfachen Code, den ich anhängen reproduzieren.JScrollBar + JTextPane mit HTML nicht richtig scrollen zum maximalen Wert

Ich bin Inhalt dynamisch zu einem JTextPane mit einem HTMLEditorKit hinzufügen. Ich setze Autoscroll auf Aus, weil ich es manuell steuern möchte (wenn der Benutzer hochscrollt, aufhört und wenn ein Ereignis ausgelöst wird, um wieder aktiviert zu werden).

Das Problem ist jetzt, wenn ich den Wert der JScrollBar auf seinen Maximalwert setzt, ist es ein anderer, gerade der Moment, nachdem Inhalt in das HTMLDocument eingefügt wurde. Wenn ich den setValue erneut ein zweites Mal manuell auslöst, scrollt er auf den korrekten Maximalwert.

Es scheint, dass die JScrollBar nicht über den richtigen maximumValue direkt nach dem Hinzufügen zu dem HTMLDocument und nur eine (verzögerte) Zeit später bekannt ist.

Mit

caret.setUpdatePolicy(DefaultCaret.ALWAYS_UPDATE); 

ist keine Lösung, weil es auch nicht richtig funktioniert. Es scrollt auch nicht bis zum Maximalwert und hinterlässt ein Ansichtspixel darunter, das ich nicht möchte.

Hier ist der vollständige Code reproduziert das Problem. Wenn Sie auf die rechte Schaltfläche klicken (fügen Sie & Bildlauf hinzu), fügt es ein DIV-Element in dem Körper ein. In dem Moment, in dem die letzte sichtbare Zeile erreicht ist, scrollt sie nicht korrekt bis zum letzten Maximalwert, die letzte Zeile ist ausgeblendet. Wenn Sie jedoch manuell auf den linken Knopf klicken, um einen zweiten scrollToEnd() auszulösen, scrollt er korrekt zum Maximalwert.

Code:

/* 
* 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     
} 

Dieser Code Ersatz obwohl funktioniert, lässt aber eine kleine Lücke, auch nicht richtig auf den Maximalwert Scrollen:

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

Antwort

1

Wenn Sie die div in das Dokument einfügen, Das Dokumentmodell wird sofort aktualisiert. Die JTextPane empfängt jedoch nur eine Benachrichtigung, dass es ungültig ist und muss ausgelegt werden. Diese Benachrichtigung erzeugt ein Ereignis auf dem EDT, das nur verarbeitet wird, nachdem das aktuelle Ereignis (ausgelöst durch den angeklickten Knopf) beendet wurde.

So, in dem Moment, in dem Sie scrollToEnd() aufrufen, ist die Revalidierung der JTextPane noch ausstehen, und die Höhe des Textbereichs ist immer noch zu klein.

Um Recht vor, die Abfolge der Ereignisse zu erhalten, müssen Sie die Invokation von scrollToEnd() in der EDT planen, durch invokeLater mit:

SwingUtilities.invokeLater(new Runnable(){ 
    public void run(){ 
     scrollToEnd(); 
    } 
}); 
+0

Vielen Dank! Dies scheint das Problem zu lösen! Ist das der richtige Weg, so etwas zu tun? Was passiert, wenn ich dem Inhalt viele Inhalte dynamisch hinzufüge, etwa ein paar Zeilen pro Sekunde? Würde das nicht "blasen" und ein neues Runnable für jedes scrollToEnd() erstellen? –

+0

Ob dies richtig ist, hängt vom genauen Anwendungsfall ab. Es ist korrekt in dem Sinne, dass die Reihenfolge der Aktualisierung des Modells, dann die Ansicht, ordnungsgemäß funktioniert. Da es die Modellaktualisierung von der Ansichtsaktualisierung trennt, könnte es seltene Anwendungsfälle geben, in denen dies nicht korrekt funktioniert, z. Wenn mehrere in Konflikt stehende Aktualisierungen des Modells die GUI nicht in der richtigen Reihenfolge aktualisieren. Dies ist jedoch sehr unwahrscheinlich, da die EDT-Ereignisse, die das ausführbare Programm ausführen, in der Reihenfolge, in der sie generiert werden, in die Ereigniswarteschlange sortiert werden. –

+0

Für eine höhere Last wie z. B. mehrere Zeilen pro Sekunde sollte dies in Ordnung sein. Das Erstellen der Runnable ist nicht teuer, da es _nicht_ einen neuen Thread erstellt. Die Methode 'run()' der Runnables wird im EDT-Thread aufgerufen, daher wird kein neuer Thread erstellt. Sie erhalten ein neues Objekt für jeden "invokeLater", aber das ist akzeptabel (jedes Swing-Ereignis hat das gleiche Problem). Wenn möglich, wäre es natürlich hilfreich, mehrere Modellupdates in einem einzigen 'scrollToEnd()' Update zu aggregieren, aber das hängt von der Logik ab, die die Dokumentänderungen erzeugt. –