Gastbeitrag: Bubbles in Swing

In vielen modernen Web-Anwendungen findet sich immer wieder eine Komponente, die bei einem Klick aktiviert wird und an Ort und Stelle auftaucht. Sie lässt sich im weitesten Sinne mit dem Wort "Popup", "Balloontip" oder "Bubble" beschreiben, wie zum Beispiel im Google Kalender:

Eine typische Popup Komponente aus dem google Kalender

Solche Komponenten stellen nicht nur Informationen dar, sondern bieten auch Funktionen zur Weiterverarbeitung an und können als Ersatz für viele Dialoge fungieren. Spätestens seit dem Code Bubbles Projekt ist der Nutzen auch für den Endanwender solcher Komponenten ersichtlich.

Die Suche nach einer geeigneten Swing Klasse

In Swing ist keine Komponente speziell für diesen Anwendungsfall vorgesehen, aber es bieten sich einige Kandidaten an. Zu nächst fällt die JToolTip Klasse auf. Wer sich allerdings die Implementierung und die interne Verwendung dieser Klasse innerhalb von Swing anschaut wird schnell feststellen, dass der Implementierungsaufwand für eine Bubble Klasse mit Hilfe von JToolTip sehr umfangreich ausfallen kann. Eine zweite Klasse wäre JPopupMenu, allerdings zeigt diese Klasse nur Menüeinträge an, oder? Falsch, sie kann viel mehr. Ein Blick auf die Vererbungshierarchie der Swing Klassen lässt den Grund erahnen. Beginnen wir mit der AWT Klasse Component:

  • java.awt.Component extends java.lang.Object
  • java.awt.Container extends java.awt.Component
  • javax.swing.JComponent extends java.awt.Container
  • javax.swing.JPopupMenu extends javax.swing.JComponent

Jede Swing Klasse ist eine JComponent und damit auch ein Container. D.h. jede Swing Komponente kann andere Komponenten aufnehmen. Wichtig ist, dass diese Funktionalität nicht mit der eines JPanel verwechselt werden sollte, denn ein JPanel stellt nur seine Komponenten dar, seine Kinder, jedoch nicht sich selbst. Zu dem nutzen einige Swing Komponenten ihre Container-Fähigkeit für spezielle Zwecke, wie zum Beispiel die JToolBar Klasse. Auch JPopupMenu nutzt sie, um JMenuItem’s zu verwalten. D.h. wenn für eine Bubble Klasse ein JPopupMenu verwendet werden soll, dann kann diese Fähigkeit nicht mehr fehlerfrei benutzt werden, aber in den meisten Fällen wird dies nicht nötig sein.

Weiterhin bietet die JPopupMenu Klasse eine pack() und show(Component, int, int) Methode, so dass der Aufwand für eine eigene Implementierung zum Anzeigen der Bubble reduziert wird. Das ist der eigentliche Grund eine JPopupMenu zu verwenden.

Die JBubble Klasse

Zu nächst muss überlegt werden, wie die JPopupMenu Klasse geschickt verwendet wird. Von einer Vererbung ist abzuraten, da ansonsten JBubble jeder Komponente als JPopupMenu zu geordnet werden kann. Daher erbt die Klasse entweder von JComponent oder JPanel. JComponent ist zu bevorzugen, da die JBubble Klasse zwar als eine Art Panel fungiert, aber auf einen eigenen "Zeichner" setzen sollte. Natürlich sind dadurch sehr viele Methoden der JComponent Klasse zu delegieren. Um jedoch das relevante kurz zu halten wird nur ein POJO vorgestellt. (Genau genommen ist es nicht nötig, die Klasse in die Swing Hierarchie einzufügen, aber dazu später mehr)

Die erste Implementierung sieht also folgendermaßen aus:

public class JBubble {

        private JPopupMenu container;

        public JBubble() {
                this(new BorderLayout());
        }

        public JBubble(LayoutManager layout) {
                container = new JPopupMenu();
                setLayout(layout);
        }
}

Nun sollten alle Container-Methoden delegiert werden (add, remove, Listener Methoden usw.), jedoch sind dies sehr viele Methoden, deswegen nur die relevanten:

public void setLayout(LayoutManager mgr) {
        container.setLayout(mgr);
}

public LayoutManager getLayout() {
        return container.getLayout();
}

public Component add(Component comp) {
        return container.add(comp);
}

public void add(Component comp, Object constraints) {
        container.add(comp, constraints);
}

public void remove(Component comp) {
        container.remove(comp);
}

public void removeAll() {
        container.removeAll();
}

Außerdem delegiert die Klasse an pack() und show(Component, int, int):

public void pack() {
        container.pack();
}

public void show(MouseEvent e) {
        show(e.getComponent(), e.getPoint());
}

public void show(Component invoker, Point p) {
        container.show(invoker, p.x, p.y);
}

Die show(MouseEvent) Methode wird zur Bequemlichkeit implementiert und verdeutlicht zu dem, dass die JBubble Klasse über einen MouseListener angezeigt werden sollte. Bereits jetzt kann ein vorläufiges Ergebnis betrachtet werden:

public static final void main(String[] args) {
        // die Bubble
        final JBubble bubble = new JBubble();
        bubble.add(new JButton("page start"), BorderLayout.PAGE_START);
        bubble.add(new JButton("page end"), BorderLayout.PAGE_END);
        bubble.add(new JButton("center"));
        bubble.add(new JButton("line start"), BorderLayout.LINE_START);
        bubble.add(new JButton("line end"), BorderLayout.LINE_END);
        // ein Bereich, der bei einem Klick die Bubble anzeigt
        JPanel clickMe = new JPanel(new BorderLayout());
        JLabel label = new JLabel("click me");
        label.setHorizontalAlignment(SwingConstants.CENTER);
        clickMe.add(label);
        clickMe.setPreferredSize(new Dimension(300, 100));
        clickMe.setBorder(BorderFactory.createLineBorder(Color.RED, 2));
        clickMe.addMouseListener(new MouseAdapter() {
                @Override public void mouseClicked(MouseEvent e) {
                        // zeige die Bubble
                        bubble.show(e);
                }
        });
        // ein Frame, um die Demo anzuzeigen
        JFrame f = new JFrame("JBubble Demo");
        f.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        JPanel content =
                new JPanel(new FlowLayout(FlowLayout.CENTER, 10, 10));
        content.add(clickMe);
        f.setContentPane(content);
        f.pack();
        f.setLocationRelativeTo(null);
        f.setVisible(true);
}

Eine JBubble flexibel implementieren

Soweit so gut, aber was fehlt? Genau! Jedes mal eine neue JBubble zu erzeugen ist unschön, vor allem dann, wenn sich das Aussehen nicht ändert, sondern nur die Inhalte. Trotzdem müssen die Inhalte irgendwie gesetzt werden. Da die spätere Verwendung variable sein soll bieten sich drei Möglichkeiten an:

  • ein Datenmodell einführen,
  • Factory Methoden vorhalten (bspw. createXYZBubble(Object… args)) oder
  • ein BubbleListener.

Factory Methoden sind schön und einfach zu implementieren, aber der Nachteil liegt auf der Hand, denn sie sind nicht flexibel, sondern auf bestimmte Anwendungsfälle beschränkt.

Ein Datenmodell bietet eine gute Aufteilung zwischen der Anzeige und den zugrundeliegenden Inhalten, aber zwingt den Anwender dazu das Modell anpassen zu müssen, wenn sich an den Inhalten etwas ändert, was bei einer Bubble häufig vorkommen könnte.

Bleibt also ein BubbleListener, aber welche Methoden sollte er bieten?

public static interface BubbleListener extends EventListener {
        void onShow(BubbleEvent e);
        void closed(BubbleEvent e);
}

public static class BubbleEvent extends EventObject {

        public BubbleEvent(JBubble source) {
                super(source);
        }

        @Override
        public JBubble getSource() {
                return (JBubble) super.getSource();
        }
}

Es wird eine Methode benötigt, die den Listener benachrichtigt, wenn eine Bubble angezeigt wird und wenn sie geschlossen wird. Dazu wird ebenfalls ein passendes BubbleEvent benötigt, das den Zugriff auf die JBubble bietet. Weiterhin benötigt eine JBubble nun eine neue Methode, um auf die Inhalte zu zugreifen:

public Component[] getComponents() {
        return container.getComponents();
}

Um einen Listener hinzuzufügen wird eine EventListenerList benötigt, aber am Besten wird die vorhandene des JPopupMenu’s benutzt. Dazu wird allerdings eine eigene Klasse benötigt, die den Zugriff auf das interne protected Feld "listenerList" gestattet. Außerdem sind nun Änderungen an dem Konstruktor und der Feldvariable "container" nötig, um den neuen Typ PopUp zu entsprechen. Weiterhin sind "fire"-Methoden zu implementieren und natürlich sollten diese auch ihre Anwendung finden. Hier wird eine Memberklasse verwendet:

private PopUp container;

private class PopUp extends JPopupMenu {

        EventListenerList getListenerList() {
                return listenerList;
        }

        @Override
        public void show(Component invoker, int x, int y) {
                fireOnShow(new BubbleEvent(JBubble.this));
                super.show(invoker, x, y);
        }

        @Override
        public void setVisible(boolean b) {
                super.setVisible(b);
                if (b == false) {
                        fireClosed(new BubbleEvent(JBubble.this));
                }
        }
}

public JBubble(LayoutManager layout) {
        container = new PopUp();
        setLayout(layout);
}

public void addBubbleListener(BubbleListener l) {
        container.getListenerList().add(BubbleListener.class, l);
}

public void removeBubbleListener(BubbleListener l) {
        container.getListenerList().remove(BubbleListener.class, l);
}

public BubbleListener[] getBubbleListener() {
        return container.getListenerList().getListeners(BubbleListener.class);
}

protected void fireOnShow(BubbleEvent e) {
        BubbleListener[] listeners = getBubbleListener();
        for (int i = 0; i < listeners.length; i++) {
                listeners[i].onShow(e);
        }
}

protected void fireClosed(BubbleEvent e) {
        BubbleListener[] listeners = getBubbleListener();
        for (int i = 0; i < listeners.length; i++) {
                listeners[i].closed(e);
        }
}

Das Aussehen

Nun sind grundlegende Funktionalitäten in der Klasse implementiert, aber noch sieht die Bubble nicht schick aus. Außerdem fehlt noch ein Button oben rechts, der das schließen der Bubble erlaubt. Hierzu wird die PopUp Klasse erweitert:

private class PopUp extends JPopupMenu {

        private JPanel content;
        private boolean closed = true;

        PopUp() {
                content = new JPanel();
                content.setBorder(BorderFactory.createEmptyBorder(5, 5, 5, 5));
                JPanel closePane = new JPanel(new FlowLayout(FlowLayout.RIGHT));
                JButton close = new JButton(
                                new ImageIcon(
                                                JBubble.class.getResource("cancel.png")
                                )
                );
                close.setContentAreaFilled(false);
                close.setOpaque(true);
                close.setBackground(null);
                close.setBorder(null);
                close.addActionListener(new ActionListener() {
                        @Override
                        public void actionPerformed(ActionEvent e) {
                                PopUp.this.setVisible(false);
                        }
                });
                closePane.add(close);
                super.setLayout(new BorderLayout());
                super.add(closePane, BorderLayout.PAGE_START);
                super.add(content);
        }

        @Override
        public Component add(Component comp) {
                return content.add(comp);
        }

        @Override
        public void add(Component comp, Object constraints) {
                content.add(comp, constraints);
        }

        @Override
        public void remove(Component comp) {
                content.remove(comp);
        }

        @Override
        public void removeAll() {
                content.removeAll();
        }

        @Override
        public Component[] getComponents() {
                return content.getComponents();
        }

        void setContentLayout(LayoutManager mgr) {
                content.setLayout(mgr);
        }

        LayoutManager getContentLayout() {
                return content.getLayout();
        }

        EventListenerList getListenerList() {
                return listenerList;
        }

        @Override
        public void show(Component invoker, int x, int y) {
                fireOnShow(new BubbleEvent(JBubble.this));
                closed = false;
                super.show(invoker, x, y);
        }

        @Override
        public void setVisible(boolean b) {
                super.setVisible(b);
                if (b == false && !closed) {
                        closed = true;
                        fireClosed(new BubbleEvent(JBubble.this));
                }
        }
}

public void setLayout(LayoutManager mgr) {
        container.setContentLayout(mgr);
}

public LayoutManager getLayout() {
        return container.getContentLayout();
}

Die Klasse bekommt einen speziellen Kontruktor, der das "content" Panel zusammenbaut. Alle Methoden, die in der Klasse JBubble an PopUp delegieren, leiten nun innerhalb der PopUp Klasse selbst an das "content" Panel weiter. Einzige Ausnahmen sind die Methoden setLayout(LayoutManager) und getLayout(), denn diese werden intern in den UI Klassen des JPopupMenu verwendet. Daher müssen nun auch in der Klasse JBubble die entsprechenden Methoden jeweils an die ContentLayout Methoden weiterleiten. Das ist auch ein wesentlicher Grund warum ein Einfügen in die Swing Klassenhierarchie sehr komplex ausfallen könnte und daher hier ein POJO vorgestellt wird.

Außerdem muss ein kleiner Hack implementiert werden, der verhindert, dass beim klicken auf den "close" Button der BubbleListener zweimal benachrichtigt wird. Dafür ist die boolesche Variable "closed" zuständig. Damit der "close" Button etwas ansprechender aussieht wird ein Icon aus der freien Silk-Sammlung verwendet. Hier nun das Ergebnis:

Fazit

Bubbles in Swing sind möglich und gar nicht so schwer. Swing ist toll (wirklich). Wer daran interessiert ist mehr Funktionen einer Bubble zu nutzen, wie Border oder Farben, abgerundete Ecken, "Zeiger" wie bei Sprechblasen aus einem Comic usw. sollte einen Blick auf das Projekt Balloontip werfen.

Download der Quellen: http://netload.in/dateiJL66TtzmID/bubbles_in_swing.zip.htm.

Autor: Michael Szwarc

 

Weiter Gastbeträge sind immer willkommen!

Ähnliche Beiträge

2 Gedanken zu “Gastbeitrag: Bubbles in Swing

  1. Leider wird im Beitrag nicht auf das Fokus-Handling innerhalb des Bubbles
    eingegangen und das Anzeigen von mehreren. Dies lässt sich nur sauber
    mit JLayeredPane/JDialog realisieren. Für ein reiner Info- bzw. „Readonly-Bubble“ mag JPopupMenu reichen, ansonsten kann ich nur empfehlen Implementierungen
    anzuschauen welche sich bewährt haben, z.B. von Intellj IDEA -> git.jetbrains.org

    -rp

  2. @respect privacy
    Das Fokus-Handling kann komplett über die JPopupMenu Klasse geschehen, so lange nur eine Bubble angezeigt wird. Einiges ist auch vordefiniert. Nicht alles, was man sich wünschen würde, zu gegeben.
    Bei mehreren Bubbles sollte keine JPopupMenu Klasse verwendet werden, weil wenn Fokus weg, dann Bubble weg. Im Swing Package gibt es dafür die Klasse PopupFactory. Ich persönlich kann diese Klasse und daran angeschlossene nicht leiden, sehr umständlich in Handhabung.

    Ansonsten bleibt zu sagen, dass ich diese Anleitung für Anfänger geschrieben habe, um zu zeigen das Swing sehr flexibel ist und es manchmal Wege gibt an die man nicht zu erst denkt. 😉

    @Herr Ullenboom
    Danke nochmals für das online stellen. 🙂

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert.