Let's swing again!

Ein JAR-Viewer mit fortgeschrittenen Swing-Techniken

Kai Tödter


Die Java Foundation Classes (JFC) beziehungsweise das darin enthaltene Swing-Set sind inzwischen ein fester Bestandteil des aktuellen JDK 1.2. Nach einigen Bug-Fix-Releases läuft Swing recht stabil und bietet eine Fülle guter GUI-Komponenten. Dieser Artikel gibt ein paar nützliche Tips und zeigt ausführlich anhand eines JAR-Viewers, wie man in Swing eine Tabelle erzeugen kann, die einen Baum beinhaltet.

Einführung
Dieser Artikel setzt Grundkenntnisse in JFC- bzw. Swing- und Java-Programmierung voraus. Eine Einführung in Swing findet man z. B. in [Tö98]. Im weiteren werden einige nützliche Tips gegeben, wie man z. B. Rollover-Toolbars oder ein Look&Feel-Menü erzeugt. Der Hauptteil dieses Artikels beschäftigt sich mit dem Erstellen einer Tabelle, die einen Baum beinhaltet. Alle vorgestellten Techniken werden an einer kleinen Beispiel-Applikation, einem JAR-Viewer (s. Abb. 1), gezeigt. Der Einfachheit halber habe ich keinen generischen Ansatz gewählt, sondern alle vorgestellten Techniken gleich in den JAR-Viewer integriert. Es sollte aber kein Problem sein, die vorgestellten Techniken für eigene Applikationen zu verwenden. Einen guten Einstieg in die TableTree-Problematik und generische Lösungen findet man auch in [TreeTables1] und [TreeTables2].

Abb. 1: Der JAR-Viewer, unsere kleine Beispiel-Applikation

Rollover-Toolbars
Rollover-Toolbars sind Werkzeugleisten (engl. Toolbars), welche den Rahmen der Toolbar-Schaltflächen (engl. Toolbar Button) erst zeigen, wenn der Benutzer mit der Maus über der Schaltfläche ist. Auch Icons oder Farben können sich dann ändern. Solche Toolbars sind inzwischen sehr beliebt, nicht zuletzt durch den Netscape Communicator oder neuere Microsoft-Produkte. Auch Swing bietet die Möglichkeit, Rollover-Toolbars zu erzeugen. Am einfachsten geht das mit der folgenden Methode:

toolBar.putClientProperty( "JToolBar.isRollover", Boolean.TRUE );

Diese Methode hat allerdings zwei entscheidende Nachteile:

  • Sie funktioniert nur im Metal Look&Feel.
  • Nach Umschalten auf ein anderes Look&Feel, z. B. Windows, und Zurückschalten auf Meal Look&Feel ist die Toolbar auch nicht mehr "rollover".

Eine bessere Lösung, die in allen Look&Feels gleichermaßen funktioniert, ist eine neue Klasse RolloverButton

class RolloverButton extends JButton 
{
  public RolloverButton()
  { 
    setRequestFocusEnabled( false );
    setRolloverEnabled( true );
  }


  protected void paintBorder( Graphics g ) 
  { 
    if( model.isRollover() )
    {
      super.paintBorder( g ); 
    } 
  }
}

Im Konstruktor wird dafür gesorgt, daß die Rollover-Fähigkeit des Buttons aktiviert ist und daß er keinen Fokus-Rahmen bekommt. Die überladene Methode paintBorder() zeichnet die Umrahmung nur, wenn das Modell gerade "rollover" ist. Dies ist nur der Fall, wenn die Maus über dem Button ist, und liefert genau den gewünschten Effekt (s. Abb. 2).

Abb. 2: Die überladene Methode paintBorder() zeichnet die Umrahmung nur, wenn das Modell gerade "rollover" ist.

Look&Feel-Menü
Viele Swing-Applikationen bieten die Möglichkeit, das Look&Feel zur Laufzeit zu wechseln. Mit Hilfe eines kurzen Code-Stücks läßt sich die Fähigkeit schnell einbauen. Zuerst holen wir uns vom UIManager alle installierten Look&Feels:

UIManager.LookAndFeelInfo[] lnfs = UIManager.getInstalledLookAndFeels();

Danach erzeugen wir eine Button-Group und ein Options-Menü:

ButtonGroup lnfGroup = new ButtonGroup();
JMenu optionsMenu = new JMenu( "Options" );
menuBar.add( optionsMenu );

In einer Schleife über alle Look&Feels werden Menü-Einträge erzeugt. Wenn der Name dem aktuellen Look&Feel entspricht, wird der Eintrag selektiert. Danach wird der Look&Feel-Name in der Client-Property "lnf name" des Menü-Eintrags gespeichert. Zum Schluß wird für jeden Eintrag ein Item-Listener als anonyme innere Klasse erzeugt, welcher bei Aktivierung mit Hilfe der SwingUtilities das komplette Look&Feel der Applikation umschaltet, hier der Quellcode:

for( int i = 0; i << lnfs.length; i++ )
{ 
  JRadioButtonMenuItem rbmi = new JRadioButtonMenuItem(lnfs[i].getName());
  optionsMenu.add( rbmi ); 

Nun selektieren wir das aktuelle Look&Feel, nämlich genau dann, wenn der vom UIManager gelieferte Name dem unseres Look&Feels entspricht.

  setSelected( UIManager.getLookAndFeel().getName().equals( lnfs[i].getName() ) ); 

Der Look&Feel-Name wird dann für die spätere Verwendung in der Client-Property "lnf name" gespeichert.

  rbmi.putClientProperty( "lnf name", lnfs[i] ); 

Als nächstes wird der ItemListener erzeugt, dessen Methode itemStateChanged immer dann aufgerufen wird, wenn der entsprechende Menü-Eintrag selektiert wird.

  rbmi.addItemListener( new ItemListener() {
      public void itemStateChanged( ItemEvent ie )
         JRadioButtonMenuItem rbmi2 = 
            (JRadioButtonMenuItem) ie.getSource();

      if( rbmi2.isSelected() ) {

Wenn der Menü-Eintrag selektiert ist, kann das in der Client_property gespeicherte Look&Feel-Info geholt werden:

          UIManager.LookAndFeelInfo info =
             (UIManager.LookAndFeelInfo) rbmi2.getClientProperty( "lnf name" );

Nun muß das Look&Feel noch im UIManager gesetzt werden. Danach muß für jede Komponenten-Wurzel, die das neue Look&Feel bekommen soll, die Methode updateComponentTreeUI der SwingUtilities aufgerufen werden. In unserem Fall ist das unser Haupt-Frame.

          try {
            UIManager.setLookAndFeel( info.getClassName() );
            SwingUtilities.updateComponentTreeUI( JarViewer.this );
         }
         catch( Exception e ) {
            System.err.println( " unable to set UI " +
                                e.getMessage() );
         }
      }
    }
  } );

Da wir alle Look&Feel-Menü-Einträge in einer Gruppe haben wollen, in der jeweils nur ein Element selektiert sein kann, fügen wir den Eintrag noch in die Gruppe lnfGroup ein.

lnfGroup.add( rbmi );
}

Unter Windows sind normalerweise Metal, Windows und Motif Look&Feel verfügbar (s. Abb. 3).

Abb. 3: Metal, Windows und Motif Look&Feel

JAR-Dateien
JAR-Dateien (JAR = Java ARchive) sind Dateien, in denen beliebige Dateien in komprimierter Form zusammengefaßt werden können. Im Gegensatz zu ZIP-Dateien kann eine JAR-Datei ein sogenanntes Manifest enthalten, in dem Zusatzinformationen gespeichert werden können, z. B. ob eine Klasse eine JavaBean ist. Das JDK 1.2 (inzwischen in Java 2 umbenannt) beinhaltet ein eigenes Paket java.util.jar, in dem sich viele nützliche Utilities befinden, um mit JAR-Dateien umzugehen. Für unseren JAR-Viewer brauchen wir nur eine Liste mit JarEntries, über die wir uns später die gebrauchten Informationen holen. Wir möchten eine Baumstruktur mit JarEntries aufbauen. Dazu benötigen wir eine Klasse JarEntryTreeNode, in der wir die JarEntries ablegen können:

public class JarEntryTreeNode extends DefaultMutableTreeNode {
   public JarEntryTreeNode( String name ) {
      super( name );
   }

   public void setJarEntry( JarEntry jarEntry ) {
      this.jarEntry = jarEntry;
   }

   public JarEntry getJarEntry() {
      return jarEntry;
   }

   private JarEntry jarEntry = null;
}

Hier nun die initData()-Methode, die den kompletten Baum erzeugt.

public void initData() {
Zuerst erzeugen wir die Baumwurzel:
   JarEntryTreeNode root = new JarEntryTreeNode( "root" );

   if( jarFile != null ) {
   int size = jarFile.size();
   Enumeration jarEntries = jarFile.entries();
Zur Zwischenspeicherung der Verzeichnisse brauchen wir eine Hash-Tabelle:
   Hashtable nodes = new Hashtable();
In einer Schleife über alle Einträge bauen wir nun den kompletten Baum auf:
   for( int i = 0; i << size; i++ ) {
Zuerst holen wir den nächsten Eintrag, und separieren dann den Dateiteil vom Verzeichnisteil. Ist ein Eintrag z. B. "d:/testdir/test.class", wird der Verzeichnisteil "d:/testdir" und der Dateiteil "test.class".
      JarEntry jarEntry = (JarEntry) jarEntries.nextElement();
      String entryName = jarEntry.getName();
Die Position des letzten Schrägstrichs ist der Punkt, an dem wir den String auseinanderschneiden
      int cutIndex = entryName.lastIndexOf( '/' );

Wenn es überhaupt einen Verzeichnisteil und einen Dateiteil gibt, also der cutIndex ungleich -1 ist, separieren wir den Verzeichnis- und den Dateiteil.

      String dir = null;
      if( cutIndex != -1 ) {  
         dir = entryName.substring( 0, cutIndex );
         entryName = entryName.substring( cutIndex + 1);
      }

Wenn der Dateiteil nicht leer ist, separieren wir alle Verzeichnisse aus dem Verzeichnisteil und bauen eine Hash-Tabelle damit auf.

if( !entryName.equals( "" ) ) {
         JarEntryTreeNode parent = null;
         JarEntryTreeNode lastParent = root;

         if( dir == null )
            parent = root;
         else {
            StringTokenizer tokenizer =
                new StringTokenizer( dir, "/" );
            String dirpath = "";
            while( tokenizer.hasMoreTokens() ) {
               String dirName = tokenizer.nextToken();
               dirpath += dirName;
               parent = (JarEntryTreeNode) nodes.get( dirpath );

Haben wir das Verzeichnis schon in der Hash-Tabelle? Wenn nicht, wird ein neuer Baumknoten für das Verzeichnis eingefügt und in die Hash-Tabelle gepackt.

                if( parent == null ) {
                  parent = new JarEntryTreeNode( dirName );
                  lastParent.insert( parent, 0 );
                  nodes.put( dirpath, parent );
               }
               lastParent = parent;
               dirpath += "/";
            }
         }

Zum Schluß muß der neue JarEntry-Knoten noch zum Verzeichnis-Knoten angefügt werden.

         JarEntryTreeNode node =
                 new JarEntryTreeNode( entryName );
         node.setJarEntry( jarEntry );
         lastParent.add( node );
      }
   }
}

JTree als TableCellRenderer
Cell-Renderer sind verantwortlich für das "Rendern", also die grafische Darstellung, von Komponenten innerhalb von JList, JComboBox, JTable und JTree. Dabei wird die paint()-Methode der Cell-Renderer jedesmal aufgerufen, wenn die entsprechende Komponente dargestellt werden soll. Das hat den Vorteil, daß man nur wenige Komponenten braucht, die für die Darstellung großer Strukturen zuständig sind. Hat man z. B. einen Baum mit 1.000 Knoten, die gleich oder ähnlich aussehen sollen, braucht man nur einen TreeCellRenderer. Als Default-Renderer ist immer eine von JLabel abgeleitete Komponente voreingestellt. Man sollte allerdings im Hinterkopf behalten, daß das eigentliche Zeichnen eines JLabels von der vom entsprechenden Look&Feel abhängigen UI-Komponente ausgeführt wird. Bei Metal Look&Feel also vom MetalLabelUI. Für unseren JAR-Viewer möchten wir einen Baum in einer Tabelle darstellen, also nehmen wir einen JTree als TableCellRenderer und nennen die Klasse JarTreeTableCellRenderer. Damit das Ganze dann auch funktioniert, müssen wir uns einiger Tricks bedienen.

class JarTreeTableCellRenderer extends
    JarTree implements TableCellRenderer {

Wir müssen uns merken, welche Reihe gezeichnet werden soll. Dazu verwenden wir rowToPaint.

    protected int rowToPaint; 
   
   public JarTreeTableCellRenderer( JarFile jarFile ) {
      super( jarFile ); 
  }
Um unschöne Linien zwischen den Baumknoten zu vermeiden, müssen wir die gesamte Tabellenhöhe benutzen.
   public void setBounds( int x, int y, int w, int h ) {
      super.setBounds( x, 0, w, JarTreeTable.this.getHeight() );
   }
Damit nicht immer bei der Baum-Wurzel angefangen wird zu zeichnen, translieren wir die Grafik in y-Richtung. Dazu benutzen wir das Produkt aus der gemerkten Reihe, die gerade zu zeichnen ist, und der Reihenhöhe der Tabelle bzw. des Baumes.
   public void paint( Graphics g ) {
      g.translate( 0, -rowToPaint * getRowHeight() );
      super.paint( g );
   }

Wir nehmen den Tabellenhintergrund auch für den Baum und merken uns die Reihe, welche wir in der paint()-Methode zeichnen wollen.

    public Component getTableCellRendererComponent( JTable table,
                                                    Object value,
                                                    boolean isSelected,
                                                    boolean hasFocus,
                                                    int row, int column) {
      
      if(isSelected)
         setBackground( table.getSelectionBackground() );
      else
         setBackground( table.getBackground() );

Hier merken wir uns die Reihe, welche gerade gezeichnet werden soll und die in der Methode paint verwendet wird (siehe oben).

       rowToPaint = row;
      return this;
   }
}

Das Tabellenmodell
Nun brauchen wir für unsere Tabelle noch das entsprechende Modell, welches auf unseren Baum zugreift. Wir nehmen dafür ein AbstractTableModel und überladen die für uns interessanten Methoden. Den Spaltennamen definieren wir als einfaches String-Array.

final String[] header = { "Entry", "Size", "Date", "Attributes" };

class JarTreeTableModel extends AbstractTableModel {
   public JarTreeTableModel( JTree tree ) {
      this.tree = tree;
   }
Als Spaltenanzahl der Tabelle geben wir die Anzahl der Elemente unseres Tabellen-Headers zurück.
   public int getColumnCount() { 
      return header.length; 
   }
Wenn noch keine Jar-Datei geladen wurde, gibt es auch noch keine Reihen, die in der Tabelle angezeigt werden sollen, ansonsten soll die Tabelle immer die gleiche Anzahl von Reihen besitzen wie unser Baum.
   public int getRowCount() {
      if( jarFile == null )
         return 0;

       return tree.getRowCount();
   }
Die Methode getValueAt() liefert das entsprechende Tabellenzellen-Objekt für eine vorgegebene Zeile und Spalte. In Spalte 0 soll immer unser Baum zurückgegeben werden, ansonsten Eigenschaften des entsprechenden Eintrags in der Jar-Datei, z. B. Größe und Datum.
   public Object getValueAt(int row, int col) {
      if( col == 0 )
         return tree;

      TreePath treePath = tree.getPathForRow( row );
      JarEntryTreeNode node =
         (JarEntryTreeNode) treePath.getLastPathComponent();

      JarEntry jarEntry = node.getJarEntry();
      if( jarEntry != null ) {
         switch( col ) {
         case 1:
            return new Long( jarEntry.getSize() );
         case 2:
            return dateFormat.format( new Date( jarEntry.getTime()));
         case 3:
            try {
               Attributes attr = jarEntry.getAttributes();
               if( attr != null )
                   return attr.entrySet();
            }
            catch( Exception e ) {
            }
            return "";  
         }
      }

      return "";
   }
Als Spaltenname wird der entsprechende String unseres Tabellen-Headers zurückgegeben.
   public String getColumnName(int column) {
      return header[column];
   }

Wenn Spalte 0 gefordert ist, geben wir unsere Baumklasse zurück, ansonsten die Klasse des Tabellenelementes in der ersten Reihe.
   public Class getColumnClass(int c) {
      if( c == 0 )
          return tree.getClass();

      return getValueAt( 0, c ).getClass();
   }
Damit Maus- und Tastatur-Events an unseren Baum weitergeleitet werden, muß für die Spalte des Baumes, also 0, immer true zurückgeliefert werden.
   public boolean isCellEditable(int row, int col) {
      if( col == 0 )
         return true;

       return false;
   }
Zum Schluß merken wir uns den Baum in einem privaten Attribut.
   private JTree tree;
};

Die Tabelle
Als Hauptkomponente nehmen wir eine Tabelle, nämlich die von JTable abgeleitete Klasse JarTreeTable. Wir benutzen unseren JarTreeTableCellRenderer sowohl als Baum wie auch als Renderer für die Tabelle und führen einige Initialisierungen im Konstruktor durch:

...
tree = new JarTreeTableCellRenderer( jarFile );

JarTreeTableModel model = new JarTreeTableModel( tree );
setModel( model );
Wir möchten, daß bei Größenänderungen unseres Hauptfensters nur die letzte Spalte der Tabelle angepaßt wird.
setAutoResizeMode( AUTO_RESIZE_LAST_COLUMN );
Die Spaltenbreiten unserer Tabelle setzen wir auf feste Werte:
getColumnModel().getColumn( 0 ).setPreferredWidth( 400 );
getColumnModel().getColumn( 1 ).setPreferredWidth( 70 );
getColumnModel().getColumn( 2 ).setPreferredWidth( 200 );
getColumnModel().getColumn( 3 ).setPreferredWidth( 300 );
Zum Schluß muß noch unser Cell-Renderer gesetzt werden:
setDefaultRenderer( JarTreeTableCellRenderer.class, tree ); 

Workarounds und Anpassungen
Im Prinzip wären wir jetzt fertig, allerdings müssen wir noch ein paar Workarounds um Swing-Anomalien und einige Anpassungen vornehmen, um die Tabelle mit Baum als Cell-Renderer wirklich zum Laufen zu bringen. Das erste Problem tritt auf, wenn man im Baum einen Teilbaum aufklappt und ein Element selektiert: Die Selektion im Baum und im Rest der Tabelle paßt leider nicht. Das liegt daran, daß Baum und Tabelle verschiedene Selektionsmodelle benutzen. Ein einfacher Workaround ist, dem Baum das Selektionsmodell der Tabelle aufzuzwingen, z. B.:

tree.setSelectionModel( new DefaultTreeSelectionModel() { 
Der folgende Code ist die Erweiterung des Default-Konstruktors für das DefaultTreeSelectionModel unseres Baumes, die Methode setSelectionModel() ist von JarTreeTable, das Attribut listSelectionModel ist von DefaultTreeSelectionModel.
   {
      setSelectionModel( listSelectionModel ); 
   }
}
); 
Diese Lösung reicht für unseren JAR-Viewer, könnte aber in einem generischen Ansatz Probleme bereiten, da der im TreeSelectionModel enthaltene TreePath nicht immer adäquat neu berechnet wird, wenn sich das Tabellen-ListSelectionModell ändert. Eine bessere Lösung wäre ein ListToTreeSelectionModelWrapper, welcher das Gewünschte tut. Ein gut kommentiertes Beispiel für einen solchen Wrapper findet man in [TreeTable2]. Ein weiterer Punkt ist eine konsistente Reihenhöhe in Baum und Tabelle. Dafür muß man aber nur die Methode setRowHeight() überladen und die neue Höhe an die jeweils andere Komponente weiterleiten, z. B. in der Tabelle:
public void setRowHeight( int rowHeight ) { 
   super.setRowHeight( rowHeight ); 
   if( tree != null && tree.getRowHeight() != rowHeight ) {
      tree.setRowHeight( getRowHeight() ); 
   }
}
Für das Anzeigen von Cell-Renderern und Cell-Editoren werden vom UI verschiedene Techniken benutzt. Für den JarTreeTableCellRenderer überladen wir setBounds(), was leider keine funktionierende Lösung für den Editor ist. Aus diesem Grunde müssen wir die Methode getEditingRow() unserer JarTreeTable so überladen, daß diese immer -1 (also nicht editierbar) für die Baum-Spalte zurückliefert.
public int getEditingRow() {
   if( getColumnClass( editingColumn ) ==
       JarTreeTableCellRenderer.class )
      return -1;

   return editingRow;  
}
Ein weiteres Problem ist, daß Tastatureingaben an den Baum weitergeleitet werden, sobald dieser den Fokus hat. Will man nur die Mausereignisse an den Baum weiterleiten, die Tastaturereignisse aber von der Tabelle behandeln lassen, überlädt man einfach die Methode isCellEditable() im TreeTableEditor:
public boolean isCellEditable( EventObject e ) {
   if( e instanceof MouseEvent ) {
      for( int counter = 0; counter << getColumnCount(); counter++ ) {
         if( getColumnClass( counter ) ==
             JarTreeTableCellRenderer.class ){
            MouseEvent me = (MouseEvent)e;
Wir müssen den x-Wert entsprechend der Spaltenposition transformieren, da sich die Spaltenpositionen innerhalb der Tabelle zur Laufzeit ändern können.
            int transX = me.getX()-getCellRect( 0, counter, true ).x;

            MouseEvent newMouseEvent = 
            new MouseEvent( tree, me.getID(),
                   me.getWhen(), me.getModifiers(),
                   transX, me.getY(), me.getClickCount(),
                   me.isPopupTrigger() );

             tree.dispatchEvent( newMouseEvent );
Zum Schluß muß die gesamte Tabelle noch neu berechnet werden.
             JarTreeTable.this.revalidate();
             break;
          }
       }
   }
   return false;
}
Verwendet man diese Technik, ist das Überladen von getEditingRow() hinfällig. Zu guter Letzt müssen noch Anpassungen gemacht werden, die das Look&Feel von Baum und Tabelle betreffen. Die updateUI-Methode muß jeweils überladen werden, da es Abhängigkeiten zwischen Baum und Tabelle gibt. In der Klasse JarTreeTable sieht die Methode folgendermaßen aus:
public void updateUI() {
   super.updateUI();
   if( tree != null ) {
      tree.updateUI();
   }
Die Tabelle soll Vordergrund, Hintergrund und Font des Baums benutzen, also müssen die neuen Fonts und Farben installiert werden.
   LookAndFeel.installColorsAndFont( this, "Tree.background",
                                     "Tree.foreground", "Tree.font" );
}
Im Baum funktioniert das Ganze auf sehr ähnliche Weise:
public void updateUI() {
   super.updateUI();
   JarTreeCellRenderer jtcr = new JarTreeCellRenderer();
   setCellRenderer( jtcr );
Unser Baum soll die Selektionsfarben der Tabelle benutzen.
   if( table != null ) {
      jtcr.setTextSelectionColor(
      UIManager.getColor( "Table.selectionForeground" ) );
      jtcr.setBackgroundSelectionColor(
      UIManager.getColor( "Table.selectionBackground" ) );
   }
}

Download
Leider reicht der Platz in diesem Artikel nicht aus, um die kompletten Quellen des JAR-Viewers zu zeigen, deswegen stelle ich sie unter [JarViewer] zur Verfügung. Dort findet man auch andere Artikel und Vorträge bezüglich Swing bzw. Java GUI.

Fazit
Mit den JFC beziehungsweise Swing ist ein Großteil der für moderne GUIs benötigten Komponenten abgedeckt. Falls man Erweiterungen schaffen will, ist dies oft relativ problemlos möglich. Allerdings muß man, was auch dieser Artikel gezeigt hat, einige Workarounds und Anpassungen vornehmen, wenn man etwas tiefer in die Swing-Architektur einsteigt. Über diese Problematik habe ich schon mit einigen Verantwortlichen des Swing-Teams diskutiert, das Fazit dieser Diskussionen In Swing sind einfache Dinge leicht, schwierige möglich kann ich nur bestätigen.

Literatur und Links
[JarViewer] Die kompletten Quellen des JAR-Viewers, http://www.geocities.com/SiliconValley/2123/java.html
[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
[TreeTables1] P. Milne, Creating TreeTables in Swing, http://java.sun.com/products/jfc/tsc/tech_topics/tables-trees/tables-trees.html
[TreeTables2] S. Violet, K. Walrath, Creating TreeTables: Part 2, http://java.sun.com/products/jfc/tsc/tech_topics/tables-trees/tables_trees_2.html

 

Autor

Kai Tödter arbeitet in der Softwareabteilung der Zentralen Technik der Siemens AG. Er beschäftigt sich mit objektorientiertem Design und Programmierung in Java und C++ sowie dem Design von grafischen Bedienoberflächen. E-Mail: Kai.Toedter@mchp.siemens.de. WWW: http://www.geocities.com/SiliconValley/2123/java.html

 

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