{"id":1046,"date":"2011-07-25T09:53:19","date_gmt":"2011-07-25T07:53:19","guid":{"rendered":"http:\/\/www.tutego.de\/blog\/javainsel\/2011\/07\/gastbeitrag-bubbles-in-swing\/"},"modified":"2011-07-25T17:58:12","modified_gmt":"2011-07-25T15:58:12","slug":"gastbeitrag-bubbles-in-swing","status":"publish","type":"post","link":"https:\/\/www.tutego.de\/blog\/javainsel\/2011\/07\/gastbeitrag-bubbles-in-swing\/","title":{"rendered":"Gastbeitrag: Bubbles in Swing"},"content":{"rendered":"<p>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\u00e4sst sich im weitesten Sinne mit dem Wort &quot;Popup&quot;, &quot;Balloontip&quot; oder &quot;Bubble&quot; beschreiben, wie zum Beispiel im <a href=\"https:\/\/www.google.com\/calendar\/\" target=\"_blank\">Google Kalender<\/a>:<\/p>\n<p> <img decoding=\"async\" alt=\"Eine typische Popup Komponente aus dem google Kalender\" src=\"http:\/\/i55.tinypic.com\/1491stj.png\" \/>   <\/p>\n<p>Solche Komponenten stellen nicht nur Informationen dar, sondern bieten auch Funktionen zur Weiterverarbeitung an und k\u00f6nnen als Ersatz f\u00fcr viele Dialoge fungieren. Sp\u00e4testens seit dem <a href=\"http:\/\/www.andrewbragdon.com\/codebubbles_site.asp\" target=\"_blank\">Code Bubbles Projekt<\/a> ist der Nutzen auch f\u00fcr den Endanwender solcher Komponenten ersichtlich.<\/p>\n<p><!--more--><\/p>\n<h4>Die Suche nach einer geeigneten Swing Klasse<\/h4>\n<p>In Swing ist keine Komponente speziell f\u00fcr diesen Anwendungsfall vorgesehen, aber es bieten sich einige Kandidaten an. Zu n\u00e4chst f\u00e4llt 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\u00fcr eine Bubble Klasse mit Hilfe von JToolTip sehr umfangreich ausfallen kann. Eine zweite Klasse w\u00e4re JPopupMenu, allerdings zeigt diese Klasse nur Men\u00fceintr\u00e4ge an, oder? Falsch, sie kann viel mehr. Ein Blick auf die Vererbungshierarchie der Swing Klassen l\u00e4sst den Grund erahnen. Beginnen wir mit der AWT Klasse Component:<\/p>\n<ul>\n<li>java.awt.Component extends java.lang.Object <\/li>\n<li>java.awt.Container extends java.awt.Component <\/li>\n<li>javax.swing.JComponent extends java.awt.Container <\/li>\n<li>javax.swing.JPopupMenu extends javax.swing.JComponent <\/li>\n<\/ul>\n<p>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\u00e4t 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\u00e4higkeit f\u00fcr spezielle Zwecke, wie zum Beispiel die JToolBar Klasse. Auch JPopupMenu nutzt sie, um JMenuItem&#8217;s zu verwalten. D.h. wenn f\u00fcr eine Bubble Klasse ein JPopupMenu verwendet werden soll, dann kann diese F\u00e4higkeit nicht mehr fehlerfrei benutzt werden, aber in den meisten F\u00e4llen wird dies nicht n\u00f6tig sein.<\/p>\n<p>Weiterhin bietet die JPopupMenu Klasse eine pack() und show(Component, int, int) Methode, so dass der Aufwand f\u00fcr eine eigene Implementierung zum Anzeigen der Bubble reduziert wird. Das ist der eigentliche Grund eine JPopupMenu zu verwenden.<\/p>\n<h4>Die JBubble Klasse<\/h4>\n<p>Zu n\u00e4chst muss \u00fcberlegt 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 &quot;Zeichner&quot; setzen sollte. Nat\u00fcrlich 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\u00f6tig, die Klasse in die Swing Hierarchie einzuf\u00fcgen, aber dazu sp\u00e4ter mehr)<\/p>\n<p>Die erste Implementierung sieht also folgenderma\u00dfen aus:<\/p>\n<pre class=\"prettyprint\">public class JBubble {\n\n        private JPopupMenu container;\n\n        public JBubble() {\n                this(new BorderLayout());\n        }\n\n        public JBubble(LayoutManager layout) {\n                container = new JPopupMenu();\n                setLayout(layout);\n        }\n}<\/pre>\n<p>Nun sollten alle Container-Methoden delegiert werden (add, remove, Listener Methoden usw.), jedoch sind dies sehr viele Methoden, deswegen nur die relevanten:<\/p>\n<pre class=\"prettyprint\">public void setLayout(LayoutManager mgr) {\n        container.setLayout(mgr);\n}\n\npublic LayoutManager getLayout() {\n        return container.getLayout();\n}\n\npublic Component add(Component comp) {\n        return container.add(comp);\n}\n\npublic void add(Component comp, Object constraints) {\n        container.add(comp, constraints);\n}\n\npublic void remove(Component comp) {\n        container.remove(comp);\n}\n\npublic void removeAll() {\n        container.removeAll();\n}<\/pre>\n<p>Au\u00dferdem delegiert die Klasse an pack() und show(Component, int, int):<\/p>\n<pre class=\"prettyprint\">public void pack() {\n        container.pack();\n}\n\npublic void show(MouseEvent e) {\n        show(e.getComponent(), e.getPoint());\n}\n\npublic void show(Component invoker, Point p) {\n        container.show(invoker, p.x, p.y);\n}<\/pre>\n<p>Die show(MouseEvent) Methode wird zur Bequemlichkeit implementiert und verdeutlicht zu dem, dass die JBubble Klasse \u00fcber einen MouseListener angezeigt werden sollte. Bereits jetzt kann ein vorl\u00e4ufiges Ergebnis betrachtet werden:<\/p>\n<pre class=\"prettyprint\">public static final void main(String[] args) {\n        \/\/ die Bubble\n        final JBubble bubble = new JBubble();\n        bubble.add(new JButton(&quot;page start&quot;), BorderLayout.PAGE_START);\n        bubble.add(new JButton(&quot;page end&quot;), BorderLayout.PAGE_END);\n        bubble.add(new JButton(&quot;center&quot;));\n        bubble.add(new JButton(&quot;line start&quot;), BorderLayout.LINE_START);\n        bubble.add(new JButton(&quot;line end&quot;), BorderLayout.LINE_END);\n        \/\/ ein Bereich, der bei einem Klick die Bubble anzeigt\n        JPanel clickMe = new JPanel(new BorderLayout());\n        JLabel label = new JLabel(&quot;click me&quot;);\n        label.setHorizontalAlignment(SwingConstants.CENTER);\n        clickMe.add(label);\n        clickMe.setPreferredSize(new Dimension(300, 100));\n        clickMe.setBorder(BorderFactory.createLineBorder(Color.RED, 2));\n        clickMe.addMouseListener(new MouseAdapter() {\n                @Override public void mouseClicked(MouseEvent e) {\n                        \/\/ zeige die Bubble\n                        bubble.show(e);\n                }\n        });\n        \/\/ ein Frame, um die Demo anzuzeigen\n        JFrame f = new JFrame(&quot;JBubble Demo&quot;);\n        f.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);\n        JPanel content =\n                new JPanel(new FlowLayout(FlowLayout.CENTER, 10, 10));\n        content.add(clickMe);\n        f.setContentPane(content);\n        f.pack();\n        f.setLocationRelativeTo(null);\n        f.setVisible(true);\n}<\/pre>\n<p><img decoding=\"async\" src=\"http:\/\/i54.tinypic.com\/2hz1el4.png\" \/> <\/p>\n<h4>Eine JBubble flexibel implementieren<\/h4>\n<p>Soweit so gut, aber was fehlt? Genau! Jedes mal eine neue JBubble zu erzeugen ist unsch\u00f6n, vor allem dann, wenn sich das Aussehen nicht \u00e4ndert, sondern nur die Inhalte. Trotzdem m\u00fcssen die Inhalte irgendwie gesetzt werden. Da die sp\u00e4tere Verwendung variable sein soll bieten sich drei M\u00f6glichkeiten an:<\/p>\n<ul>\n<li>ein Datenmodell einf\u00fchren, <\/li>\n<li>Factory Methoden vorhalten (bspw. createXYZBubble(Object&#8230; args)) oder <\/li>\n<li>ein BubbleListener. <\/li>\n<\/ul>\n<p>Factory Methoden sind sch\u00f6n und einfach zu implementieren, aber der Nachteil liegt auf der Hand, denn sie sind nicht flexibel, sondern auf bestimmte Anwendungsf\u00e4lle beschr\u00e4nkt.<\/p>\n<p>Ein Datenmodell bietet eine gute Aufteilung zwischen der Anzeige und den zugrundeliegenden Inhalten, aber zwingt den Anwender dazu das Modell anpassen zu m\u00fcssen, wenn sich an den Inhalten etwas \u00e4ndert, was bei einer Bubble h\u00e4ufig vorkommen k\u00f6nnte.<\/p>\n<p>Bleibt also ein BubbleListener, aber welche Methoden sollte er bieten?<\/p>\n<pre class=\"prettyprint\">public static interface BubbleListener extends EventListener {\n        void onShow(BubbleEvent e);\n        void closed(BubbleEvent e);\n}\n\npublic static class BubbleEvent extends EventObject {\n\n        public BubbleEvent(JBubble source) {\n                super(source);\n        }\n\n        @Override\n        public JBubble getSource() {\n                return (JBubble) super.getSource();\n        }\n}<\/pre>\n<p>Es wird eine Methode ben\u00f6tigt, die den Listener benachrichtigt, wenn eine Bubble angezeigt wird und wenn sie geschlossen wird. Dazu wird ebenfalls ein passendes BubbleEvent ben\u00f6tigt, das den Zugriff auf die JBubble bietet. Weiterhin ben\u00f6tigt eine JBubble nun eine neue Methode, um auf die Inhalte zu zugreifen:<\/p>\n<pre class=\"prettyprint\">public Component[] getComponents() {\n        return container.getComponents();\n}<\/pre>\n<p>Um einen Listener hinzuzuf\u00fcgen wird eine EventListenerList ben\u00f6tigt, aber am Besten wird die vorhandene des JPopupMenu&#8217;s benutzt. Dazu wird allerdings eine eigene Klasse ben\u00f6tigt, die den Zugriff auf das interne protected Feld &quot;listenerList&quot; gestattet. Au\u00dferdem sind nun \u00c4nderungen an dem Konstruktor und der Feldvariable &quot;container&quot; n\u00f6tig, um den neuen Typ PopUp zu entsprechen. Weiterhin sind &quot;fire&quot;-Methoden zu implementieren und nat\u00fcrlich sollten diese auch ihre Anwendung finden. Hier wird eine Memberklasse verwendet:<\/p>\n<pre class=\"prettyprint\">private PopUp container;\n\nprivate class PopUp extends JPopupMenu {\n\n        EventListenerList getListenerList() {\n                return listenerList;\n        }\n\n        @Override\n        public void show(Component invoker, int x, int y) {\n                fireOnShow(new BubbleEvent(JBubble.this));\n                super.show(invoker, x, y);\n        }\n\n        @Override\n        public void setVisible(boolean b) {\n                super.setVisible(b);\n                if (b == false) {\n                        fireClosed(new BubbleEvent(JBubble.this));\n                }\n        }\n}\n\npublic JBubble(LayoutManager layout) {\n        container = new PopUp();\n        setLayout(layout);\n}\n\npublic void addBubbleListener(BubbleListener l) {\n        container.getListenerList().add(BubbleListener.class, l);\n}\n\npublic void removeBubbleListener(BubbleListener l) {\n        container.getListenerList().remove(BubbleListener.class, l);\n}\n\npublic BubbleListener[] getBubbleListener() {\n        return container.getListenerList().getListeners(BubbleListener.class);\n}\n\nprotected void fireOnShow(BubbleEvent e) {\n        BubbleListener[] listeners = getBubbleListener();\n        for (int i = 0; i &lt; listeners.length; i++) {\n                listeners[i].onShow(e);\n        }\n}\n\nprotected void fireClosed(BubbleEvent e) {\n        BubbleListener[] listeners = getBubbleListener();\n        for (int i = 0; i &lt; listeners.length; i++) {\n                listeners[i].closed(e);\n        }\n}<\/pre>\n<h4>Das Aussehen<\/h4>\n<p>Nun sind grundlegende Funktionalit\u00e4ten in der Klasse implementiert, aber noch sieht die Bubble nicht schick aus. Au\u00dferdem fehlt noch ein Button oben rechts, der das schlie\u00dfen der Bubble erlaubt. Hierzu wird die PopUp Klasse erweitert:<\/p>\n<pre class=\"prettyprint\">private class PopUp extends JPopupMenu {\n\n        private JPanel content;\n        private boolean closed = true;\n\n        PopUp() {\n                content = new JPanel();\n                content.setBorder(BorderFactory.createEmptyBorder(5, 5, 5, 5));\n                JPanel closePane = new JPanel(new FlowLayout(FlowLayout.RIGHT));\n                JButton close = new JButton(\n                                new ImageIcon(\n                                                JBubble.class.getResource(&quot;cancel.png&quot;)\n                                )\n                );\n                close.setContentAreaFilled(false);\n                close.setOpaque(true);\n                close.setBackground(null);\n                close.setBorder(null);\n                close.addActionListener(new ActionListener() {\n                        @Override\n                        public void actionPerformed(ActionEvent e) {\n                                PopUp.this.setVisible(false);\n                        }\n                });\n                closePane.add(close);\n                super.setLayout(new BorderLayout());\n                super.add(closePane, BorderLayout.PAGE_START);\n                super.add(content);\n        }\n\n        @Override\n        public Component add(Component comp) {\n                return content.add(comp);\n        }\n\n        @Override\n        public void add(Component comp, Object constraints) {\n                content.add(comp, constraints);\n        }\n\n        @Override\n        public void remove(Component comp) {\n                content.remove(comp);\n        }\n\n        @Override\n        public void removeAll() {\n                content.removeAll();\n        }\n\n        @Override\n        public Component[] getComponents() {\n                return content.getComponents();\n        }\n\n        void setContentLayout(LayoutManager mgr) {\n                content.setLayout(mgr);\n        }\n\n        LayoutManager getContentLayout() {\n                return content.getLayout();\n        }\n\n        EventListenerList getListenerList() {\n                return listenerList;\n        }\n\n        @Override\n        public void show(Component invoker, int x, int y) {\n                fireOnShow(new BubbleEvent(JBubble.this));\n                closed = false;\n                super.show(invoker, x, y);\n        }\n\n        @Override\n        public void setVisible(boolean b) {\n                super.setVisible(b);\n                if (b == false &amp;&amp; !closed) {\n                        closed = true;\n                        fireClosed(new BubbleEvent(JBubble.this));\n                }\n        }\n}\n\npublic void setLayout(LayoutManager mgr) {\n        container.setContentLayout(mgr);\n}\n\npublic LayoutManager getLayout() {\n        return container.getContentLayout();\n}<\/pre>\n<p>Die Klasse bekommt einen speziellen Kontruktor, der das &quot;content&quot; Panel zusammenbaut. Alle Methoden, die in der Klasse JBubble an PopUp delegieren, leiten nun innerhalb der PopUp Klasse selbst an das &quot;content&quot; Panel weiter. Einzige Ausnahmen sind die Methoden setLayout(LayoutManager) und getLayout(), denn diese werden intern in den UI Klassen des JPopupMenu verwendet. Daher m\u00fcssen nun auch in der Klasse JBubble die entsprechenden Methoden jeweils an die ContentLayout Methoden weiterleiten. Das ist auch ein wesentlicher Grund warum ein Einf\u00fcgen in die Swing Klassenhierarchie sehr komplex ausfallen k\u00f6nnte und daher hier ein POJO vorgestellt wird.<\/p>\n<p>Au\u00dferdem muss ein kleiner Hack implementiert werden, der verhindert, dass beim klicken auf den &quot;close&quot; Button der BubbleListener zweimal benachrichtigt wird. Daf\u00fcr ist die boolesche Variable &quot;closed&quot; zust\u00e4ndig. Damit der &quot;close&quot; Button etwas ansprechender aussieht wird ein Icon aus der freien <a href=\"http:\/\/www.famfamfam.com\/lab\/icons\/silk\/\" target=\"_blank\">Silk-Sammlung<\/a> verwendet. Hier nun das Ergebnis:<\/p>\n<p><img decoding=\"async\" src=\"http:\/\/i54.tinypic.com\/16gaf04.png\" \/> <\/p>\n<h4>Fazit<\/h4>\n<p>Bubbles in Swing sind m\u00f6glich 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, &quot;Zeiger&quot; wie bei Sprechblasen aus einem Comic usw. sollte einen Blick auf das Projekt <a href=\"http:\/\/balloontip.java.net\/\" target=\"_blank\">Balloontip<\/a> werfen.<\/p>\n<p>Download der Quellen: <a href=\"http:\/\/netload.in\/dateiJL66TtzmID\/bubbles_in_swing.zip.htm\">http:\/\/netload.in\/<u><\/u>dateiJL66TtzmID\/bubbles_in_<u><\/u>swing.zip.htm<\/a>.<\/p>\n<p>Autor: Michael Szwarc<\/p>\n<p>&#160;<\/p>\n<p><strong>Weiter Gastbetr\u00e4ge sind immer willkommen!<\/strong><\/p>\n","protected":false},"excerpt":{"rendered":"<p>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\u00e4sst sich im weitesten Sinne mit dem Wort &quot;Popup&quot;, &quot;Balloontip&quot; oder &quot;Bubble&quot; beschreiben, wie zum Beispiel im Google Kalender: Solche Komponenten stellen nicht nur Informationen dar, sondern bieten auch Funktionen zur Weiterverarbeitung [&hellip;]<\/p>\n","protected":false},"author":1,"featured_media":0,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"_jetpack_memberships_contains_paid_content":false,"footnotes":"","_links_to":"","_links_to_target":""},"categories":[64],"tags":[],"class_list":["post-1046","post","type-post","status-publish","format-standard","hentry","category-gastbeitrag"],"jetpack_featured_media_url":"","jetpack_sharing_enabled":true,"_links":{"self":[{"href":"https:\/\/www.tutego.de\/blog\/javainsel\/wp-json\/wp\/v2\/posts\/1046","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/www.tutego.de\/blog\/javainsel\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/www.tutego.de\/blog\/javainsel\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/www.tutego.de\/blog\/javainsel\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/www.tutego.de\/blog\/javainsel\/wp-json\/wp\/v2\/comments?post=1046"}],"version-history":[{"count":4,"href":"https:\/\/www.tutego.de\/blog\/javainsel\/wp-json\/wp\/v2\/posts\/1046\/revisions"}],"predecessor-version":[{"id":1050,"href":"https:\/\/www.tutego.de\/blog\/javainsel\/wp-json\/wp\/v2\/posts\/1046\/revisions\/1050"}],"wp:attachment":[{"href":"https:\/\/www.tutego.de\/blog\/javainsel\/wp-json\/wp\/v2\/media?parent=1046"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/www.tutego.de\/blog\/javainsel\/wp-json\/wp\/v2\/categories?post=1046"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/www.tutego.de\/blog\/javainsel\/wp-json\/wp\/v2\/tags?post=1046"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}