Swing Zeilenanzeige: JPanel als Header für JScrollPane

Status
Nicht offen für weitere Antworten.
Hi,


Vorab: Ich habe noch nicht viel Erfahrung mit GUI-Programmierung, also bitte nicht schlagen!
Ich möchte für meinen Editor in einem JScrollPane die Zeilennummern anzeigen lassen. Allerdings möchte ich auch, dass bei Bedarf einzelne Zeilennummern durch Symbole ersetzt werden können. Diese Symbole sollen dann auch Tooltips enthalten und auf Mausklicks reagieren können. Dafür benutze ich ein JPanel, bei dem nach Änderung der Zeilenanzahl die folgende Methode aufgerufen wird:

Java:
public void changeLineCount(int newval) {
    if (newval < lastLineCount) {
        // Remove some lines

        for (int i = newval ; i < lastLineCount ; i++) {
            remove(i);
        }
    } else {
        // Add some lines

        Element root = target.getDocument().getDefaultRootElement();
        TextUI targetUI = target.getUI();
        int start = lastLineCount == -1 ? 0 : lastLineCount;

        for (int i = start ; i < newval ; i++) {
            Element lElem = root.getElement(i);
            int offset = lElem.getStartOffset();
            Rectangle lineRect = null;

            try {
                lineRect = targetUI.modelToView(target, offset);
                lineRect.width = (int) getSize().getWidth();
            } catch (BadLocationException ex) {
                ex.printStackTrace();
            }

            LineHeader header = null;

            if (headers.size() > i) {
                header = headers.get(i);
            }

            if (header == null) {
                header = new DefaultLineHeader();
                headers.add(header);
            }

            header.setBounds(lineRect);
            header.setLine(i);
            header.shown();
            add(header);
        }
    }


    lastLineCount = newval;
    repaint();
}

Im Prinzip fügt sie also neue LineHeader (JLabels) hinzu oder entfernt sie, jenachdem wie sich die Zeilenanzahl geändert hat.
Das funktioniert aber nur so lange, bis gescrollt werden muss: Scrollt man an den oberen Rand, sieht man alle Labels korrekt, scrollt man weiter runter, werden sie plötzlich nicht mehr angezeigt. Ich denke das liegt daran, dass sich das Panel nicht automatisch vergrößert (ein Aufruf von getSize() bestätigt das), also hab ich einfach in die Funktion changeLineCunt() einen setSize() Aufruf gepackt, der die Höhe des Headers an die Höhe des Viewports anpasst. Leider hat das am Problem trotzdem nichts geändert: Die Labels werden nur bis zum unteren Rand angezeigt, wenn ganz nach oben gescrollt wurde.
Was mache ich falsch?

Danke im Voraus!
 
😳 Äußerst Freud'sch. :lol:

Ich würde mich beim Design eins zu eins an die JTableHeader-Klasse halten. Guck Dir den Quelltext an, genauso auch den Quelltext der BasicTableHeaderUI-Klasse. Und schreib dort so viel wie möglich ab. Ein JLabel pro Zeile ist sehr heftig. Manche Tabellen haben Millionen an Zeilen. Millionen JLabels sind sehr teuer. Deswegen lieber Renderer benutzen, genauso wie das auch der JTableHeader tut.

Ebenius
 
😳 Äußerst Freud'sch. :lol:

Huch, da hab ich wohl halb geschlafen...


Ich würde mich beim Design eins zu eins an die JTableHeader-Klasse halten. Guck Dir den Quelltext an, genauso auch den Quelltext der BasicTableHeaderUI-Klasse. Und schreib dort so viel wie möglich ab. Ein JLabel pro Zeile ist sehr heftig. Manche Tabellen haben Millionen an Zeilen. Millionen JLabels sind sehr teuer. Deswegen lieber Renderer benutzen, genauso wie das auch der JTableHeader tut.

Ich hab mir den Code mal angesehen. Ich hatte gehofft, nicht zu tief in die Abgründe von Swing hinabsteigen zu müssen. Naja, soweit bin ich noch nicht, dass ich mir zutrauen würde das zu durchforschen. Aber soweit ich das verstanden habe werden dort immer nur die sichtbaren Komponenten gezeichnet. Ich habe mal versucht etwas ähnliches nachzubauen:

Java:
public void drawLines() {
    Container parent = getParent();
    Rectangle visible = null;
    removeAll();

    if (parent instanceof JViewport) {
        JViewport vp = (JViewport) parent;
        visible = vp.getViewRect();
    }

    Document doc = target.getDocument();
    Element root = doc.getDefaultRootElement();
    TextUI targetUI = target.getUI();

    try {
        for (int i = 0 ; i < root.getElementCount() ; i++) {
            Element elem = root.getElement(i);
            int start = elem.getStartOffset();
            Rectangle lineRect = targetUI.modelToView(target, start);
            lineRect.setLocation(3, lineRect.y - visible.y);
            lineRect.width = getSize().width;

            if (lineRect.y+lineRect.height < 0) {
                continue;
            }
            if (lineRect.y > visible.height) {
                continue;
            }

            JLabel label = new JLabel(""+(i+1));
            label.setBounds(lineRect);
            add(label);
        }
    } catch (BadLocationException ex) {
        ex.printStackTrace();
    }

    repaint();
}

Das wird aufgerufen, wenn sich die vertikale ScrollBar des JScrollPanes verschiebt. Das klappt auch ganz gut, aber es gibt einen kleinen Anzeigefehler: Wenn ich die ScrollBar mit gedrückter Maustaste verschiebe, dann springt das Zeilennummern-Panel wieder zum Anfang (Zeile 1), sobald ich loslasse. Woran kann das liegen?
 
Google hat mir hier ein halbwegs einfaches Beispiel gefunden. Sieht eigentlich ganz brauchbar aus. Hilft Dir das weiter?

Ich habe mal versucht das ganze so wie dort als Liste zu implementieren. Das funktioniert auch im Prinzip ganz gut, aber bei größeren Zeilenanzahlen ist das viel zu langsam (~10000 Zeilen sollten schon einigermaßen normal angezeigt werden können). Gibt es noch schnellere Wege, bei denen auch Icons anstelle der Zeilennummern stehen und Mausevents abgefangen werden können?
 
Es gibt bestimmt schnellere Wege. Aber das macht mit Sicherheit einiges an Arbeit. 🙁

Das blöde ist, ich hab die Arbeit schonmal gemacht, ein JRowHeader mit UI-Delegate, Renderer, Editor, nicht auf JList sondern auf JComponent basierend. Den Quelltext hab ich aber nicht, weil ich nicht mehr in der selben Firma arbeite. Und rausgeben dürfte ich ihn auch nicht.

Mal schauen, ob ich Dir was zusammenfrickeln kann. Ich meld mich wieder.

Ebenius
 
So - gestern Nacht hab ich dann grad einen JTableRowHeader mit Renderer, ValueProvider und UI-Delegate implementiert. Die Doku lässt noch zu wünschen übrig. Die Event-Behandlung (tableChanged(...) ist noch nicht optimiert, sondern ruft pauschal repaint(), unabhängig vom Event; das sollte man ggf. noch verbessern. Zu Ende getestet ist's auch nicht, können also gut noch ein paar Bugs drin sein. Aber im großen und ganzen sollte's funktionieren.

Das anhängende Archiv enthält diese Klassen/Interfaces und deren Quelltext:
Code:
[com]
   + [ebenius]
      + [widget]
         + [B]JTableRowHeader[/B]                          - [I]Row header implementation for the JTable class to be installed into the row header section of a scroll pane.[/I]
         + [B]JTableRowHeader.DefaultRowHeaderRenderer[/B] - [I]Default row header renderer, rendering the (view) row in a label.[/I]
         + [B]TableRowHeaderValueProvider[/B]              - [I]Value provider for the table row header class.[/I]
         + [plaf]
            + [B]TableRowHeaderUI[/B]                      - [I]UI delegate for the JTableRowHeader class.[/I]
            + [basic]
               + [B]BasicTableRowHeaderUI[/B]              - [I]Default TableRowHeaderUI implementation.[/I]
Hier mal eine kleine und eine erweiterte Test-Klasse.

TestGUI - Minimal
Java:
/* $Id$ */

/* Copyright 2009 Sebastian Haufe

   Licensed under the Apache License, Version 2.0 (the "License");
   you may not use this file except in compliance with the License.
   You may obtain a copy of the License at

       [url]http://www.apache.org/licenses/LICENSE-2.0[/url]

   Unless required by applicable law or agreed to in writing, software
   distributed under the License is distributed on an "AS IS" BASIS,
   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
   See the License for the specific language governing permissions and
   limitations under the License. */

package com.ebenius.widget;

import java.awt.BorderLayout;

import javax.swing.*;

/**
 * Simple test UI for the {@link JTableRowHeader} class.
 */
public class JTableRowHeaderTestGUI {

  /**
   * Test main method.
   * 
   * @param args ignored
   */
  public static void main(String[] args) {
    final JTable table = new JTable(10000, 4);
    final JScrollPane scrollPane = new JScrollPane(table);
    final JTableRowHeader rowHeader = new JTableRowHeader();
    rowHeader.setTable(table);
    scrollPane.setRowHeaderView(rowHeader);
    table.setAutoCreateRowSorter(true);

    final JPanel contentPane = new JPanel(new BorderLayout(6, 6));
    contentPane.add(scrollPane);

    final JFrame f = new JFrame("JTableRowHeaderTestGUI"); //$NON-NLS-1$
    f.setContentPane(contentPane);
    f.pack();
    f.setLocationRelativeTo(null);
    f.setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE);
    f.setVisible(true);
  }
}
TestGUI - Komplexer mit Value Provider, eigenem Renderer, Mouse Listener
Java:
/* $Id$ */

/* Copyright 2009 Sebastian Haufe

   Licensed under the Apache License, Version 2.0 (the "License");
   you may not use this file except in compliance with the License.
   You may obtain a copy of the License at

       [url]http://www.apache.org/licenses/LICENSE-2.0[/url]

   Unless required by applicable law or agreed to in writing, software
   distributed under the License is distributed on an "AS IS" BASIS,
   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
   See the License for the specific language governing permissions and
   limitations under the License. */

package com.ebenius.widget;

import java.awt.BorderLayout;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.net.MalformedURLException;
import java.net.URL;

import javax.swing.*;
import javax.swing.table.DefaultTableModel;
import javax.swing.table.TableModel;

/**
 * Simple test UI for the {@link JTableRowHeader} class.
 */
public class JTableRowHeaderTestGUI {

  /**
   * Test main method.
   * 
   * @param args ignored
   * @throws MalformedURLException
   */
  public static void main(String[] args) throws MalformedURLException {
    /* some test data */
    final DefaultTableModel tableModel = new DefaultTableModel(10000, 2);
    for (int i = 0; i < tableModel.getRowCount(); i++) {
      tableModel.setValueAt("Data Row " + i, i, 0); //$NON-NLS-1$
      tableModel.setValueAt(Math.random() > 0.8 ? "Icon" //$NON-NLS-1$
            : "Row Number", i, 1); //$NON-NLS-1$
    }

    /* sortable table with row header on scroll pane */
    final JTable table = new JTable(tableModel);
    final JScrollPane scrollPane = new JScrollPane(table);
    final JTableRowHeader rowHeader = new JTableRowHeader();
    rowHeader.setTable(table);
    scrollPane.setRowHeaderView(rowHeader);
    table.setAutoCreateRowSorter(true);
    table.setRowHeight(25);

    /* a value provider; an icon for every data row containing my letters only 

       Note: The default renderer shows the visible row number (based on 1) if
       the value provider is null or the value produced by the provider is
       null. */
    final URL iconUrl = new URL("http://i8t.de/znbl3c8z");//$NON-NLS-1$
    final Icon icon = new ImageIcon(iconUrl);
    rowHeader.setValueProvider(new TableRowHeaderValueProvider() {

      public Object getTableRowHeaderValueAt(
            TableModel dataModel,
            int rowIndex) {
        final Object value = dataModel.getValueAt(rowIndex, 1);
        return "icon".equalsIgnoreCase(String.valueOf(value)) //$NON-NLS-1$
              ? icon
              : null;
      }
    });

    /* a cell renderer capable of showing icons */
    rowHeader.setCellRenderer(new JTableRowHeader.DefaultRowHeaderRenderer() {

      /** Serial version UID */
      private static final long serialVersionUID = 7439315331651660028L;

      @Override
      protected void setValue(Object value) {
        if (value instanceof Icon) {
          setText(null);
          setIcon((Icon) value);
          setToolTipText("Icon Row Header Cell"); //$NON-NLS-1$
        } else {
          setIcon(null);
          super.setValue(value);
          setToolTipText("Default Row Header Cell"); //$NON-NLS-1$
        }
      }
    });

    /* info label for mouse events on the row header */
    final JTextArea infoTextField = new JTextArea(5, 40);
    infoTextField.setEditable(false);

    /* inform about mouse / mouse motion events on the row header */
    addMouseInfoListener(rowHeader, infoTextField);

    final JPanel contentPane = new JPanel(new BorderLayout(6, 6));
    contentPane.add(scrollPane, BorderLayout.CENTER);
    contentPane.add(new JScrollPane(infoTextField), BorderLayout.PAGE_END);

    /* test frame */
    final JFrame f = new JFrame("JTableRowHeaderTestGUI"); //$NON-NLS-1$
    f.setContentPane(contentPane);
    f.pack();
    f.setLocationRelativeTo(null);
    f.setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE);
    f.setVisible(true);
  }

  private static void addMouseInfoListener(
        final JTableRowHeader rowHeader,
        final JTextArea infoTextField) {
    final MouseAdapter mouseListener = new MouseAdapter() {

      private String getInfo(MouseEvent e) {
        final JTableRowHeader rowHeader = (JTableRowHeader) e.getSource();
        final int row = rowHeader.rowAtPoint(e.getPoint());
        if (row < 0) {
          return ""; //$NON-NLS-1$
        } else {
          return "Row: " //$NON-NLS-1$
                + row
                + " Header Value: " //$NON-NLS-1$
                + rowHeader.getValueAt(row)
                + '\n';
        }
      }

      @Override
      public void mouseClicked(MouseEvent e) {
        infoTextField.append("[Mouse Clicked] " + getInfo(e)); //$NON-NLS-1$
      }

      @Override
      public void mouseEntered(MouseEvent e) {
        infoTextField.append("[Mouse Entered] " + getInfo(e)); //$NON-NLS-1$
      }

      @Override
      public void mouseExited(MouseEvent e) {
        infoTextField.append("[Mouse Exited] " + getInfo(e)); //$NON-NLS-1$
      }

      @Override
      public void mousePressed(MouseEvent e) {
        infoTextField.append("[Mouse Pressed] " + getInfo(e)); //$NON-NLS-1$
      }

      @Override
      public void mouseReleased(MouseEvent e) {
        infoTextField.append("[Mouse Released] " + getInfo(e)); //$NON-NLS-1$
      }

      @Override
      public void mouseMoved(MouseEvent e) {
        infoTextField.append("[Mouse Moved] " + getInfo(e)); //$NON-NLS-1$
      }

      @Override
      public void mouseDragged(MouseEvent e) {
        infoTextField.append("[Mouse Dragged] " + getInfo(e)); //$NON-NLS-1$
      }
    };
    rowHeader.addMouseListener(mouseListener);
    rowHeader.addMouseMotionListener(mouseListener);
  }
}
Ich hoffe, Du kannst damit etwas anfangen. Wenn nicht: Frag nach!

Ebenius
 

Anhänge

Zuletzt bearbeitet:
Na das ist doch mal was! Ich habe deinen Code inzwischen so umschreiben können, dass er für meine JTextPanes funktioniert. Auch bei großen Zeilenanzahlen sehe ich bisher keine Performanceverluste und alles was ich wollte funktioniert wunderbar. Ich melde mich, falls ich doch noch ein Problem habe.

Vielen Dank für deine Hilfe und die Mühe, die du dir gemacht hast! [bitte Smiley einsetzen]
 
Status
Nicht offen für weitere Antworten.

Neue Themen


Zurück
Oben