Let's swing again!

Neue JFC Features in der
Java 2 Standard Edition, Version 1.3

Kai Tödter

Die Java Foundation Classes (JFC) beziehungsweise das darin enthaltene Swing-Set sind ein fester Bestandteil der aktuellen Java 2 Standard Edition, Version 1.3. Nach einigen Bug-Fix-Releases sind die meisten der Kinderkrankheiten geheilt. Im neuen "Kestrel"-Release kommen eine Fülle von neuen Features und einige kleinere Änderungen hinzu, die in diesem Artikel ausführlich vorgestellt werden.

Einführung
Wer die Entwicklung der JFC bzw. Swing verfolgt hat, wird festgestellt haben, dass die angebotenen Features immer ausgereifter wurden und inzwischen kaum noch Wünsche offen sind. Sicherlich können die JFC nicht alle Anforderungen abdecken, die meisten professionellen Software-Pakete werden aber mit der Funktionalität des aktuellen Standes gut leben können. Was die Performance und die Effizienz der Komponenten angeht, kann allerdings noch einiges getan werden. Die Version 1.3 der J2SE geht einen großen Schritt in diese Richtung und kann einiges halten, was auf der JavaOne '99 [JavaOne] versprochen wurde. Dieser Artikel geht ausführlich auf die wichtigsten neuen Features ein.

JTree
Im JTree gibt es einigen Neuerungen, z.B. einige neue Properties, für die es die üblichen Getter- und Setter-Methoden gibt. Man kann jetzt mit der Methode setToggleClickCount( int ) die Anzahl der Maus-Klicks festlegen, die zum Expandieren/Kollabieren eines Baumknotens benötigt werden. Mit den Methoden setLeadSelectionPath( TreePath ) und setAnchorSelectionPath( Treepath ) hat man jetzt mehr Kontrolle über Selektionen innerhalb von Bäumen. Der Anchor Path ist derjenige TreePath, mit dem eine Selektion beginnt und er wird als Bezugspunkt für alle Erweiterungen einer Selektion genommen. Der Lead Path wiederum ist der Path, welcher zuletzt zu einer Selektion hinzugefügt wurde. In Abbildung 1 ist z.B. "John Coltrane" der Anchor Path (mit erstem Mausklick selektiert) und "I Thought About You" der Lead Path (mit zweitem Mausklick + Shift selektiert). Mit der Methode setExpandsSelectedPaths( boolean ) ist es nun möglich, festzulegen, ob ein Teilbaum expandiert werden soll, wenn er programmtechnisch selektiert wird. Defaultmässig ist diese Property auf true gesetzt. Weiterhin kann man mit der Methode removeDescendantTogglePath( Enumeration ) festlegen, ob sich die aktuelle Selektion ändern soll, wenn ein Baumknoten expandiert oder kollabiert wird. Eine kleine aber feine Änderung gibt es beim DefaultTreeCellRenderer: Das Feld hasFocus ist jetzt protected. Das macht es für eine Unterklasse, die nur die paint()-Methode überladen will, möglich, auf alle releventen Properties direkt zuzugreifen. Früher war das Feld privat und man musste den umständlichen Weg über getTreeCellRendererComponent gehen. Die letzt kleine Änderung betrifft die Methode insureUniqueness() im DefaultTreeSelektionModel. Diese ist jetzt obsolet, bleibt aber wegen Rückwärtskompatibilität im API enthalten.


Abbildung 1: Baum mit Selektion

JTable
Bei den Tabellen gibt es einige Performance- und Layoutverbesserungen. Die Header Renderer der Spalten sind nun Teil des Tabellen Headers (Klasse JTableHeader). Da dadurch ähnliche Header Renderer pro Tabelle verwaltet werden, wird der Footprint kleiner und die Performance deutlich verbessert. Alle Methoden von JTableHeader mit "UpdateTableInRealTime" im Namen sind nun deprecated und man kann jetzt einen DefaultRenderer des Typs TableCellRenderer setzen. Eine weitere lang ersehnte Neuerung der JFC-Tabellen, nämlich dass verschieden Zeilen unterschiedliche Höhen haben können, ist endlich realisiert worden (Siehe Abbildung 2). Dafür steht die Methode setRowHeight( int row, int height ) zur Verfügung. Um die Performance von Tabellen-Spalten, welche Zeilen mit variabler Höhe beinhalten, in zukünftigen Versionen zu verbessern, könnte die neue Utility-Klasse SizeSequence benutzt werden. Das Erstellen von nichtstandard Editor-Komponenten wird durch die neue Klasse AbstractCellEditor sehr erleichtert. Die existierende Klasse DefaultCellEditor ist nun von AbstractCellEditor abgeleitet.


Abbildung 2: Tabelle mit unterschiedlichen Zeilenhöhen

JSpiltPane
Wenn sich die Größe einer JSplitPane ändert, wird nur die Größe der rechten/unteren Komponente vergrößert. Das gibt den Effekt, als ob die linke/obere Komponente unverändert bleibt. Man kann allerdings über die Methode setResizeWeight( double ) eine Gewichtung einstellen, wie die Größen der Komponenten nach der Größenänderung der JSplitPane aufgeteilt werden sollen. Um einige Layout-Probleme mit der Umrandung der JSpliPane-Komponenten und des Deviders zu umgehen, kann man jetzt die Umrandung der Klasse BasicSplitPaneDevider mit der Methode setBorder( Border ) explizit setzen.

JFileChooser
Der JFileChooser besitzt eine neue Property isAcceptAllFileFilter, mit der man den AcceptAll File-Filter (*.*) kontrollieren kann. Dieser File Filter ist jetzt defaultmässig in der Filter-Combobox eingestellt. Eine weitere Neuerung ist, dass man jetzt die OK- und Cancel-Buttons entfernen kann. Das macht immer dann sehr viel Sinn, wenn der File Chooser in eigene Komponenten integrieren werden soll. Mit der Methode setControlButtonsAreShown( boolean ) kann die Sichtbarkeit dieser Buttons kontrolliert werden.

Buttons

Eine JCheckBox wird in Tabellen oder Listen oft als CellRenderer benutzt. Sowie das Java- wie auch das Windows-Look&Feel spezifizieren, dass CheckBoxen mit "2D Flat Border" und nicht mit "3D Border" gerendert werden sollen, wenn sie als CellRenderer benutzt werden. Mit der Methode setBorderPaintedFlat( boolean ) von JCheckBox kann man dieses Aussehen jetzt leicht einstellen. Abbildung 3 zeigt normale und flache CheckBoxen in den 3 gängigen Look & Feels. Während man beim Windows und beim Motif Look & Feel ein deutlich flacheres Aussehen feststellt, ändert sich beim Java Look & Feel nichts. Das mag entweder daran liegen, dass das Java Look & Feel ja sowieso schon recht "Flat" ist. Vielleicht liegt es aber auch nur an meiner JDK 1.3 Beta Version. Mal sehen, wie es im Final Release ausschaut. Um einfacher an die Information über Gruppenzugehörigkeit zu gelangen, besitzt das DefaultButtonModel jetzt die Methode getGroup().


Abbildung 3: "Flat" CheckBoxen

JTabbedPane
Die JTabbedPane hat eine neue (indexed) Property toolTipTextAt. Mit der Methode setToolTipTextAt( int index, String toolTipText ) kann man jetzt den Tooltiptext für jedes "Tab" der JTabbedPane setzen.

Text
Im Text-Paket gibt es einige kleinere und größere Änderungen. Z.B. wurde ein Schreibfehler korrigiert: HTMLEditorKit.insertAtBoundry wird zu HTMLEditorKit.insertAtBoundary. Um die Behandlung von DokumentEvents zu vereinfachen, ist die Default-Funktionalität von CompositeView in die Klasse View gewandert. Unterklassenbildung wird dadurch sehr vereinfacht, sowie das Management der DokumentEvents mit den folgenden protected Methoden:

  • boolean updateChildren( DokumentEvent.ElementChange, DokumentEvent e, ViewFactory vf )
  • void forwardUpdate( DokumentEvent.ElementChange, DokumentEvent e, Shape a, ViewFactory vf )
  • void forwardUpdateToView( View v, DokumentEvent e, Shape a, ViewFactory vf )
  • void updateLayout( DokumentEvent.ElementChange, DokumentEvent e, Shape a )
Weiterhin sind auch die public Methoden zur Verwaltung der Kinder eines Views von CompositeView zu View gewandert:
  • void removeAll()
  • void remove( int i)
  • void insert( int offset, View v )
  • void replace( int offset, int length, View[] views )
Die Klasse BoxView hat ein Argument axis, an welches Unterklassen bisher nicht herangekommen sind. Dieses Argument hat eventuell auch Auswirkungen auf das Layout. Deswegen sind die folgenden public Methoden zur Klasse javax.swing.text.BoxView hinzugekommen:
  • int getAxis()
  • void set Axis( int axis )
  • void layout Changed( int axis )

Die Klasse ParagraphView ist nun eine Unterklasse von FlowView, wodurch Dinge wie das textuelle Umfließen einer Form und andere Arten von "Flows" vereinfacht werden. Im ParagraphView werden jetzt die Paragraph-spezifischen Flow-Strategien, wie Einrückung der ersten Zeile, behandelt. Die Klasse javax.swing.text.View hat deswegen auch die neue Methode getViewIndex( int pos, Position.Bias b ). Im HTML-Bereich hat sich einiges getan. Z.B. gab es in der Klasse javax.swing.text.html.FormView zwei public static final Strings SUBMIT und RESET. Diese sind jetzt deprecated und die entsprechenden Werte können über die UIManager-Properties FormView.submitButtonText und FormView.resetButtonText geändert werden. Bei der Serialisierung im HTML-Paket sind auch kleinere Erweiterungen vorgenommen worden. Die Klasse javax.swing.text.html.parser.ParserDelegator implementiert jetzt das Serializable-Interface. Die Klasse AbstractWriter wird als Basis-Klasse benutzt, wenn man ein Text-Dokument in spezifischer Form ausgeben will. Z.B. ist HTMLWriter von AbstractWriter abgeleitet, um HTML auszugeben. Das Problem bestand darin, dass viele Methoden private waren, wodurch die Unterklassenbildung ungemein erschwert wurde. Die folgenden API-Änderungen in AbstractWriter machen die Unterklassenbildung und somit auch den HTMLWriter deutlich einfacher:

  • public int getStartOffset()
  • public int getEndOffset()
  • protected Writer getWriter()
  • protected int getLineLength()
  • protected void setCurrentLineLength( int length )
  • protected int getCurrentLineLength()
  • protected boolean isLineEmpty()
  • protected void setCanWrapLines( boolean newValue )
  • protected boolean getCanWrapLines()
  • public void setLineSeparator( String value )
  • public String getLineSeparator()
  • protected int getIndentLevel()
  • protected void writeLineSeparator() throws IOException
  • protected void write( char[] chars, int startIndex, int length ) throws IOException
  • protected void output( char[] content, int start, int length ) throws IOException

Das HTML-Paket unterstützt Cascading Style Sheets (CSS 1). Die folgenden Methoden erlauben dem Entwickler, verschiedene Style Sheets miteinander zu verknüpfen:

  • public synchronized void addStyleSheet( StyleSheet ss )
  • public synchronized void removeStyleSheet( StyleSheet ss )
  • public StyleSheet[] getStyleSheets()

Es gibt noch einige weitere Änderungen, wie die neuen Klassen ZoneView und AsyncBoxView. Detaillierte Informationen findet man in der J2SE-Dokumentation unter jdk1.3\docs\guide\swing\TextChanges.html.

Scrolling
Interaktives Scrollen ist vielleicht der wichtigste Faktor im subjektiven Eindruck der gesamten Swing-Performance. In der neuen Version 1.3 wird in der Klasse JViewPort eine neue Default-Optimierung, genannt BLIT_SCROLL_MODE, eingeführt, welche Scroll-Bereiche per Bit-Blit kopiert. Der protected Member backingStore ist nun deprecated.


Abbildung 4: SwingSet2 Demo: Scrollen mit BLIT_SCROLL_MODE

Drucken
In früheren Versionen wurde die print-Methode von JComponent nicht überladen. Drucken war also nichts anderes als eine Display-Ausgabe in einen Double-Buffer. Jetzt gibt es extra Methoden für das Drucken von Komponenten:

  • protected void printComponent( Graphics g )
  • protected void printChildren( Graphics g )
  • protected void printBorder( Graphics g )

Eingabe-Verifikation
Es gibt einen neue Klasse javax.swing.InputVerifier, die man dazu verwenden kann, den Inhalt einer Komponente, z.B. eines Textfeldes, zu validieren. Jetzt kann einfach ein eigener InputVerifier mit der Methode setInputVerifier() (von JComponent) gesetzt werden. Bevor der Focus auf eine andere Komponente gesetzt werden kann, wird die Methode shouldYieldFocus() des InputVerifiers aufgerufen. Der Focus wird nur transferiert, wenn diese Methode true zurückliefert.

Internationalisierung
Um die nächste visuell repräsentierte Modell-Position herauszubekommen, auf welcher ein Caret plaziert werden kann, bietet die Klasse GlyphView.GlyphPainter jetzt die Methode

  • public int getNextVisualPositionFrom( GlyphView v, int pos, Position.Bias b, Shape a, int direction, Position.Bias[] biasRet ) throws BadLocationException

Borders
Es war früher nicht möglich, mit Hilfe der BorderFactory eine "raised etched Border" zu erzeugen. Um konsistent zu bleiben, wurden deshalb die folgenden Methoden zur BoderFactory hinzugefügt:

  • public static Border createEtchedBorder( int type )
  • public static Border createEtchedBorder( int type, Color highlight, Color shadow )

Um LineBoders mit abgerundeten Ecken zu erzeugen, gibt es jetzt einen neuen Konstruktor:

  • public LineBorder( Color color, int thickness, boolean roundedCorners )

Um eine Inkonsistenz zu beseitigen, ist die Klasse TableHeaderBorder vom Paket plaf.metal.MetalUtils in das Paket javax.swing.plaf.metal.MetalBorders gewandert.

Actions
Actions werden in Swing benutzt, um an einer Stelle Funktionalität zu implementieren, die von verschiedenen Komponenten, wie z.B. Menüs, Toolbars usw. benutzt werden kann. Ändert die Action ihren Status, spiegelt sich das auch meist in der visuellen Repräsentation der entsprechenden Komponente wieder, die die Action benutzt: Menü-Items und Toolbar-Icons werden z.B. disabled dargestellt, wenn die Action disabled wird. Um das Erzeugen von Komponenten, welche auf Actions basieren, zu erleichtern, wurden die drei Container JToolBar, JMenu und JPopupMenu mit den Methoden add( Action a ) angereichert, die jeweils das entsprechende Control (JButton, JMenuItem, JMenuItem) aus der Action erzeugen und zurückliefern. Daraus entstehen eine Reihe von Problemen, z.B. die Unverträglichkeit mit GUI-Buildern, da die Container.add()-Methode überladen wird und die zu enge Bindung der Konfigurationsmöglichekeiten an die Container-Klasse. Deswegen gibt es ein neues API in der Klasse AbstractButton, welche wiederum die Basisklasse für JButton, JMenuItem etc. ist:

  • public void setAction( Action a )
  • public Action getAction()
  • protected void configurePropertiesFromAction( Action a )
  • protected PropertyChangeListener createActionPropertyChangeListener()

Weiterhin haben folgende Klassen jetzt einen Konstruktor, der eine Action als Parameter verlangt:

  • JButton
  • JCheckBox
  • JRadioButton
  • JToggleButton
  • JMenuItem
  • JMenu
  • JCheckBoxMenuItem
  • JRadioButtonMenuItem

Die Containerklassen JToolBar, JPopupMenu und JMenu haben jeweils eine neue Methode:

  • JToolBar: protected JButton createActionComponent( Action a )
  • JPopupMenu: protected JMenuItem createActionComponent( Action a )
  • JMenu: protected JMenuItem createActionComponent( Action a )

Zu guter letzt gibt es noch zwei neue Konstanten

  • public static final String ACCELERATOR_KEY
  • public static final String MNEMONIC_KEY

und eine neue Methode public Object[] getKeys(), um dem Entwickler die Möglichkeit zu geben, herauszufinden, welchen Tasten für eine Action gesetzt worden sind.

JToolbar
Um ungedockten Toolbars jetzt einen Titel zu geben, hat die Klasse JToolBar zwei neue Konstruktoren:

  • public JToolBar( String name )
  • public JToolBar( String name , int orientation )


Abbildung 5: Ungedockte ToolBar mit Titel

Menüs
Bei den Menüs hat sich recht wenig geändert. Die Methode getPopupMenuOrigin() von JMenu und die Methoden installComponents( JMenuItem menuItem ) und uninstallComponents( JMenuItem menuItem ) sind jetzt protected. Weiterhin gibt es jetzt in den Klassen JPopupMenu und javax.swing.plaf.PopupMenuUI die public Methode isPopupTrigger( MouseEvent e ), die einen boolean zurückliefert.

JFrame
Endlich gibt es die Konstante EXIT_ON_CLOSE, die man in der Methode setDefaultCloseOperation( int operation ) verwenden kann.

JInternalFrame
Die Klasse JInternalFrame hat einige Erweiterungen bekommen. Hinzugekommen ist die Methode setLayer( int ), mit Hilfe derer man die Ebene setzten kann, in der sich der interne Frame befindet. Intern wurde ein Memory Leak gefixed: Alle Listener der BasicInternalFrameTitlePane werden jetzt entfernt, wenn sie nicht mehr gebraucht werden. Weiterhin hat sich das Verhalten der Default Schließ-Operation geändert, nämlich von HIDE_ON_CLOSE auf DISPOSE_ON_CLOSE. Auch die Konstante EXIT_ON_CLOSE wurde definiert. Die Methode doDefaultCloseAction() ist jetzt public. Um das Rechteck zu erhalten, dass das interne Frame in seiner normalen Größe einnimmt, gibt es jetzt die Methode getNormalBounds(). Diese Größe kann man natürlich mit setNormalBounds() auch setzen. Zwei weitere Methoden von JInternal Frame sind getFocusOwner(), welches im aktiven Zustand des internen Frames dasjenige Kind zurückliefert, welches gerade den Focus besitzt. Die Methode restoreSubcomponentFocus() wird vom UI benutzt, um bei Aktivierung des internen Frames den Focus vor der Deaktivierung wieder herzustellen. Zu guter letzt hat der InternalFrameEvent die Methode getInternalFrame() bekommen.


Abbildung 6: Interne Frames im neuen SwingSet2 Demo

Listeners
Um endlich herausfinden zu können, welche Listener von verschiedenen Klassen benutzt werden, wurde die Methode getListeners() zu folgenden Klassen hinzugefügt:

  • javax.swing.Timer javax.swing.AbstractListModel
  • javax.swing.DefaultBoundedRangeModel
  • javax.swing.DefaultButtonModel
  • javax.swing.DefaultListSelectionModel
  • javax.swing.DefaultSingleSelectionModel
  • javax.swing.table.AbstractTableModel
  • javax.swing.table.DefaultTableColumnModel
  • javax.swing.tree.DefaultTreeModel
  • javax.swing.tree.DefaultTreeSelectionModel
  • javax.swing.text.AbstractDocument
  • javax.swing.text.DefaultCaret
  • javax.swing.event.EventListenerList

Performance
Einer der Hautgründe für Swings langsame Startup-Performance war, dass der UIManager für jede Komponente, die ein UI-Delegate benutzt, das entsprechende Look & Feel lädt. Dafür mussten die Default-Tabellen geladen werden, welche die UI-Defaults für die entsprechende Komponente enthielten. Er wurden auch einige falsche Annahmen über die Kosten von Objekt-Instantiierungen und für das Laden von Klassen getroffen. In einem neuen Verfahren wird jetzt die Klasse UIDefaultProxy benutzt, was z.B. in einem "Hello Word"-Beispiel dazu führt, dass das Laden von ca. 90 Klassen vermieden wird, nachdem nur eine einzige Klasse geladen wurde! Die Scrolling-Performance im Allgemeinen wurde auch wesentlich verbessert (Siehe auch Abschnitt Scrolling). Das Verschieben von internen Frames hat sich durch den neuen Drag-Mode LIVE_DRAG_MODE auch sichtbar verschnellert. Der neue Drag-Mode wird folgendermaßen gesetzt: desktop.setDragMode( JDesktopPane.LIVE_DRAG_MODE ). Insgesamt scheint es so, als ob sich die gesamte Swing-Performance verbessert hat.

Fazit
Im neuen Kestrel-Realease der Java 2 Standard Edition, Version 1.3, hat sich einiges getan. Endlich sind viele der Kinderkrankheiten geheilt, die meisten der Anforderungen an ein modernes GUI erfüllt und auch die Performance macht deutliche Fortschritte. Ich bin sehr gespannt, in welche weiteren Verbesserungen die den nächsten Versionen der Java Foundation Classes zu finden sein werden.

Literatur und Links
[Swing] Swing, http://java.sun.com/products/jdk/awt/swing
[Tö98] K. Tödter, Professionelle grafische Benutzungsoberflächen mit den Java Foundation Classes,
in: JavaSpektrum, Januar/Februar 1998 
[JavaOne] M. C. Franz & K. Tödter, Rückblick auf die JavaOne,
in: JavaSpektrum, September/Oktober 1999 

 

Home | Java | Forum | Media | Misc | Links | Contact | Sitemap | Search
© Kai Tödter 2008