Kapselung JProgressBar in JTable, aber getValueAt() greift nicht

Hildi0

Mitglied
Hallo, ich möchte euch zuerst mal das Problem zeigen:

Java:
import javax.swing.*;
import javax.swing.table.AbstractTableModel;
import javax.swing.table.TableCellRenderer;
import java.awt.*;
import java.util.ArrayList;
import java.util.List;
import java.util.Random;

public class JTable1 {
    static class Bar extends JProgressBar implements TableCellRenderer {
        private final String description;

        public Bar(String description) {
            this.description = description;
            setStringPainted(true);
        }

        public String getDescription() {
            return description;
        }

        @Override
        public Component getTableCellRendererComponent(JTable table, Object value, boolean isSelected, boolean hasFocus, int row, int column) {
            return this;
        }
    }

    static class Model extends AbstractTableModel {
        List<Bar> bars = new ArrayList<>();

        public void addABar() {
            bars.add(new Bar(String.valueOf((char) (new Random().nextInt(10) + 'a'))));
        }

        public void setTestValue() {
            bars.forEach(bar -> bar.setValue(25));
        }

        @Override
        public int getRowCount() {
            return bars.size();
        }

        @Override
        public int getColumnCount() {
            return 2;
        }

        @Override
        public Object getValueAt(int rowIndex, int columnIndex) {
            if (columnIndex == 0) {
                return bars.get(rowIndex).getDescription();
            }
            if (columnIndex == 1) {
                return bars.get(rowIndex); // does not work
            }
            return null;
        }

        @Override
        public String getColumnName(int column) {
            String[] names = {"Description", "Progress"};
            return names[column];
        }
    }

    public static void main(String[] args) {
        Model model = new Model();
        JTable table = new JTable(model);
        table.getColumnModel().getColumn(1).setCellRenderer(new Bar("something"));
        model.addABar();
        model.addABar();
        JFrame frame = new JFrame();
        frame.add(new JScrollPane(table));
        frame.pack();
        frame.setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE);
        frame.setVisible(true);

        model.setTestValue();
        model.fireTableDataChanged();
    }
}

Der Fortschritt der Bar ist immer 0, denn getValueAt() funktioniert aus einem mir unerklärlichen Grund nicht...

So würde es funktionieren, aber ich frage mich wieso:

Java:
import javax.swing.*;
import javax.swing.table.AbstractTableModel;
import javax.swing.table.TableCellRenderer;
import java.awt.*;
import java.util.ArrayList;
import java.util.List;
import java.util.Random;

public class JTable2 {
    static class Bar extends JProgressBar implements TableCellRenderer {
        private final String description;

        public Bar(String description) {
            this.description = description;
            setStringPainted(true);
        }

        public String getDescription() {
            return description;
        }

        @Override
        public Component getTableCellRendererComponent(JTable table, Object value, boolean isSelected, boolean hasFocus, int row, int column) {
            return this;
        }
    }

    static class Model extends AbstractTableModel {
        List<Bar> bars = new ArrayList<>();

        public void addABar() {
            bars.add(new Bar(String.valueOf((char) (new Random().nextInt(10) + 'a'))));
        }

        public void setTestValue() {
            bars.forEach(bar -> bar.setValue(25));
        }

        @Override
        public int getRowCount() {
            return bars.size();
        }

        @Override
        public int getColumnCount() {
            return 2;
        }

        @Override
        public Object getValueAt(int rowIndex, int columnIndex) {
            if (columnIndex == 0) {
                return bars.get(rowIndex).getDescription();
            }
            if (columnIndex == 1) {
                return bars.get(rowIndex); // does not work
            }
            return null;
        }

        @Override
        public String getColumnName(int column) {
            String[] names = {"Description", "Progress"};
            return names[column];
        }
    }

    public static void main(String[] args) {
        Model model = new Model();
        JTable table = new JTable(model) {
            @Override
            public TableCellRenderer getCellRenderer(int row, int column) {
                if (column == 1) {
                    return (TableCellRenderer) model.getValueAt(row, column);
                }
                return super.getCellRenderer(row, column);
            }
        };
        model.addABar();
        model.addABar();
        JFrame frame = new JFrame();
        frame.add(new JScrollPane(table));
        frame.pack();
        frame.setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE);
        frame.setVisible(true);

        model.setTestValue();
        model.fireTableDataChanged();
    }
}

Weiß da vielleicht jemand bescheid? Vielen Dank. 😊
 
Ganz einfach: Du setzt ja den Renderer
table.getColumnModel().getColumn(1).setCellRenderer(new Bar("something"));
Und diese Instanz wird von JTable verwendet und die liefert einfach eine Referenz auf sich selbst (this) zurück:
Java:
        public Component getTableCellRendererComponent(JTable table, Object value, boolean isSelected, boolean hasFocus, int row, int column) {
            return this;
        }

Da Du den Wert dieser Instanz nicht änderst, wird also immer 0 angezeigt.

Entweder hast Du etwas ganz abstruses vor oder Dir fehlt der Sinnzusammenhang zwischen Model der Renderer. Normalerweise liefert das Model Daten und der Renderer stellt diese dar. Dafür wird das Flyweight-Pattern verwendet, d. h. die Komponente (hier Deine Bar) wird wie ein Stempel verwendet, so dass man mit einer einzigen (EDIT: Instanz der) Komponente alle Zellen zeichnen kann.

Was genau willst Du denn machen?
 
Vielen Dank für Deine Antwort. 🤓 So richtig habe ich noch nicht verstanden, weshalb ich zwingend die Methode getCellRenderer von JTable überschreiben muss...

Was genau willst Du denn machen?
Ich möchte in einer Spalte einer JTable eine JProgressBar anzeigen, die den Fortschritt einer aktuellen Operation (in der Zeile) anzeigt. Dafür soll der Wert der JProgressBar zum Beispiel einmal pro Sekunde aktualisiert werden. Also zum Beispiel soll der Fortschritt von Downloads angezeigt werden.
 
So richtig habe ich noch nicht verstanden, weshalb ich zwingend die Methode getCellRenderer von JTable überschreiben muss...
Musst Du ja nicht 🙂 Ganz grob: Die JTable holt sich vom Model einen Wert (getValueAt), nimmt den registrierten Renderer und ruft getTableCellRendererComponent auf, dabei wird unter anderem der Wert übergeben.

Der Renderer liefert nun eine Komponente zurück. Diese Komponente wird nicht zur JTable hinzugefügt sondern von JTable nur zum Zeichnen verwendet (wie ein Stempel).

Ich möchte in einer Spalte einer JTable eine JProgressBar anzeigen, die den Fortschritt einer aktuellen Operation (in der Zeile) anzeigt. Dafür soll der Wert der JProgressBar zum Beispiel einmal pro Sekunde aktualisiert werden. Also zum Beispiel soll der Fortschritt von Downloads angezeigt werden.
Gut, Du hast also ein TableModel, mit einer Spalte, die Werte z. B. von 0 bis 100 annimmt und diese Werte möchtest Du jetzt in Form eines Fortschrittbalkens anzeigen lassen.

Mom, ich bastel mal schnell was zusammen.
 
So, Du meinst vermutlich etwas in der Richtung:
Java:
import javax.swing.*;
import javax.swing.table.*;
import java.util.Random;

public class Test {
    static class ProgressRenderer implements TableCellRenderer {
        private JProgressBar bar;

        public ProgressRenderer() {
            bar = new JProgressBar();
        }

        @Override
        public JComponent getTableCellRendererComponent(JTable table, Object value, 
                boolean isSelected, boolean hasFocus, int row, int column) {
            int progress = 0;
            if (value != null && value instanceof Number) {
                progress = ((Number) value).intValue();
            }
            bar.setValue(progress);
            return bar;
        }
    }

    public void run() {
        final Random rand = new Random();
        DefaultTableModel model = new DefaultTableModel(new Object[][]{
                {"Task #1", rand.nextInt(100)},
                {"Task #2", rand.nextInt(100)}
            }, 
            new Object[]{"Description", "Progress"}
        );

        JTable table = new JTable(model);
        table.getColumnModel().getColumn(1).setCellRenderer(new ProgressRenderer());
        
        JFrame frame = new JFrame();
        frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        frame.add(new JScrollPane(table));
        frame.pack();
        frame.setVisible(true);

        new Timer(1000, e -> model.setValueAt(rand.nextInt(100), rand.nextInt(2), 1)).start();
    }

    public static void main(String[] args) {
        SwingUtilities.invokeLater(() -> new Test().run());
    }
}
 
Ja, aber jetzt wird ja dieselbe JProgressBar (d.h.: dasselbe Objekt) immer wiederverwendet? Oder ist das das Flyweight-Pattern-Prinzip?
 
Ja, aber jetzt wird ja dieselbe JProgressBar (d.h.: dasselbe Objekt) immer wiederverwendet? Oder ist das das Flyweight-Pattern-Prinzip?
Ja und ja. So funktioniert die JTable.

Nachtrag: wie oben geschrieben wird ein Objekt wie ein Stempel verwendet. Zelle 1: Stempel einstellen, Stempel andrücken, Zelle 2: Stempel einstellen, Stempel andrücken usw.

Durch den Renderer (Flyweight-Pattern) kann die JTable große Datenmengen ohne nennenswerten Speicherverbrauch darstellen. Außerdem ist es so relativ einfach möglich, die Daten nicht im Speicher halten zu müssen.
 

Zurück
Oben