NetBeans Platform - czyli jak budować szybko i skutecznie standardowe aplikacje biurkowe
Wśród wielu programistów panuje przekonanie, że język Java nie nadaje się do tworzenia aplikacji biurkowych. Argumentują to słabą wydajnością aplikacji napisanych w Javie bądź wyglądem odbiegającym od wyglądu aplikacji uruchamianych w danym systemie.
Ten artykuł ma na celu próbę pokazania, że w Javie można budować z dobrym efektem standardowe aplikacje biurkowe. Przykładem niech będzie choćby środowisko programistyczne NetBeans IDE, które w całości jest napisane właśnie w Javie.
Zastanówmy się więc nad stworzeniem przykładowej aplikacji biurkowej. Chcielibyśmy, aby miała ona w miarę nowoczesny i przyjazny wygląd, możliwość prostej aktualizacji itp. itd. Jednocześnie, jak większość programistów, chcielibyśmy zrobić to jak najmniejszym nakładem pracy. I tutaj z pomocą przychodzi nam firma Sun Microsystem i jej produkt NetBeans Platform.
Za przykład niech posłuży nam aplikacja, która wykorzystuje komponent JXMapKit do pokazania położenia geograficznego kilku wybranych grup JUG (Java User Group).
Rysunek 1: Aplikacja JUG Locations
Co to właściwie jest platforma NetBbeans?
Najkrócej można zdefiniować, że jest to szkielet aplikacyjny oparty na bibliotece Swing. Dzięki temu możemy wykorzystywać wszelkie dostępne w sieci komponenty Swingowe w naszej aplikacji.
Aplikacja zbudowana na platformie NetBeans jest podzielona na moduły (plugin). Każdy z modułów posiada swoją nazwę, wersję oraz listę zależności od innych modułów. W każdej chwili aplikacja może być rozszerzona poprzez dodanie do niej nowego modułu.
Nasza przykładowa aplikacja składa się z trzech modułów: Map, Actions oraz swingx (Rysunek 2).
Rysunek 2: Moduły składowe aplikacji
DataObject, Node, ExplorerManager
Obiekty danych (DataObjects), węzły (Nodes) oraz ExplorerManager to chyba jedne z najbardziej pomocnych mechanizmów platformy. Obiekty danych to własne obiekty encyjne, specyficzne dla naszej aplikacji. Węzeł jest warstwą prezentacji danych (sam w sobie nie jest obiektem zawierającym dane; przykrywa DataObject). Platforma NetBeans oferuje nam komponenty, takie jak tabele, listy czy drzewka, potrzebne do prezentacji struktur reprezentowanych przez węzły. Do najpopularniejszych należą BeanTreeView, ListView czy ContextTreeView. ExplorerManager zarządza zaznaczaniem i nawigacją po tych komponentach.
Dla węzłów są już gotowe metody obsługujące operacje „przeciągnij i puść”, kopiuj, wklej, menu kontekstowe.
Obiektem danych w naszej aplikacji jest klasa JUGDataObject:
package org.myorg.node; import java.awt.Image; import org.jdesktop.swingx.mapviewer.GeoPosition; public class JUGDataObject { private GeoPosition position; private String leaders; private String page; private String name; private Image image; public JUGDataObject(String name, GeoPosition position, String leaders, String page, Image image){ this.name = name; this.position = position; this.leaders = leaders; this.page = page; this.image = image; } public String getName() { return name; } public String getLeaders() { return leaders; } public String getPage() { return page; } public GeoPosition getPosition() { return position; } public Image getImage() { return image; } }
Węzłem jest klasa JUGNode będąca rozszerzeniem klasy abstrakcyjnej org.openide.nodes.AbstractNode .
package org.myorg.node; import org.openide.nodes.AbstractNode; import org.openide.nodes.Children; import org.openide.nodes.PropertySupport; import org.openide.nodes.Sheet; import org.openide.util.Exceptions; import org.openide.util.lookup.Lookups; public class JUGNode extends AbstractNode { public JUGNode() { super(Children.create(new JUGChildFactory(), true)); setDisplayName("JUG"); } public JUGNode(JUGDataObject dataObject) { super(Children.LEAF, Lookups.singleton( dataObject)); setDisplayName(dataObject.getName()); } @Override public Image getIcon(int arg0) { JUGDataObject jug = getLookup().lookup(JUGDataObject.class); Image image = Utilities.loadImage("org/myorg/node/jlogo.png"); if(jug != null){ image = jug.getImage(); } return image; } @Override protected Sheet createSheet() { Sheet sheet = Sheet.createDefault(); Sheet.Set set = Sheet.createPropertiesSet(); JUGDataObject jug = getLookup().lookup(JUGDataObject.class); if (jug != null) { try { Property name = new PropertySupport.Reflection(jug, String.class, "getName", null); Property leaders = new PropertySupport.Reflection(jug, String.class, "getLeaders", null); Property site = new PropertySupport.Reflection(jug, String.class, "getPage", null); name.setName("Nazwa"); leaders.setName("Założyciele"); site.setName("Strona"); set.put(name); set.put(leaders); set.put(site); } catch (NoSuchMethodException ex) { Exceptions.printStackTrace(ex); } sheet.put(set); } return sheet; } }
W domyślnym konstruktorze węzła JUGNode wywołujemy konstruktor klasy nadpisywanej, gdzie jako argument podajemy, jakie dzieci powinien posiadać tworzony przez nas węzeł ( super(Children.create(new JUGChildFactory(), true)) ). Potomkowie są tworzeni przez obiekt klasy JUGChildFactory przedstawiony poniżej. Drugi argument (w tym przypadku wartość true) informuje czy tworzenie potomków ma się odbywać w osobnym wątku. Jest to przydatne, jeśli budowanie drzewa zajmuje trochę czasu – jeśli np. informacje na temat obiektów są pobierane z bazy danych lub z jakiegoś serwisu internetowego. W takim przypadku klasa Children zadba za nas o to, aby na czas budowania drzewa wyświetlić informację np. „Please wait...”.
package org.myorg.node; import java.util.List; import org.jdesktop.swingx.mapviewer.GeoPosition; import org.openide.nodes.ChildFactory; import org.openide.nodes.Node; public class JUGChildFactory extends ChildFactory<JUGDataObject>{ @Override protected boolean createKeys(List<JUGDataObject> list) { list.add(new JUGDataObject( "Polish JUG", new GeoPosition(50.061822,19.937181), "G. Duda, R. Holewa and A. Nowak", "http://java.pl/", Utilities.loadImage("org/myorg/node/pjug.gif"))); list.add(new JUGDataObject( "Szczecin JUG", new GeoPosition(53.416667,14.583333), "Leszek Gruchala", "http://szczecin.jug.pl/", Utilities.loadImage("org/myorg/node/jlogo.png"))); list.add(new JUGDataObject( "Warszawa JUG", new GeoPosition(52.211780,20.982340), "Jacek Laskowski", "http://www.warszawa.jug.pl", Utilities.loadImage("org/myorg/node/jlogo.png"))); list.add(new JUGDataObject( "Western Australian JUG", new GeoPosition(-31.95227,115.85007), "Michael Mok", "https://wajug.dev.java.net", Utilities.loadImage("org/myorg/node/jlogo.png"))); return true; } @Override protected Node createNodeForKey(JUGDataObject dataObject) { return new JUGNode(dataObject); } }
Obiekt klasy JUGDataObject przechowuje informacje na temat konkretnej grupy, czyli nazwę grupy, jej założycieli, adres strony, logo w postaci ikonki oraz położenie geograficzne.
Klasa JUGChildFactory nadpisuje dwie metody z ChildFactory: createKeys i createNodeForKey. Pierwsza z nich odpowiada za utworzenie kluczy (DataObjects) a druga, która jest wywoływana osobno dla każdego wcześniej utworzonego klucza, tworzy obiekt węzła reprezentujące obiekty danych. W klasie tej, zamiast tworzyć w kodzie obiekty dla poszczególnych grup, możemy napisać metodę, która będzie odczytywać informację o grupach javowych ze strony https://svweb-jug.dev.java.net/kml/jug-leaders.kml.
System okien
Platforma NetBeans daje nam do dyspozycji specjalny komponent okna zwany TopComponent. W skrócie można przyjąć, że jest to odpowiednik JPanel. Możemy na nim rozkładać komponenty, ustawiać layout w taki sam sposób jak to czynimy w Swingu właśnie z JPanel. Każdy TopComponent w aplikacji może być dokowany lub oddokowywany, można zmieniać jego rozmiar lub położenie. Położenie jest inaczej zwane jako tryb (mode) i dla przykładu standardowe okno explorera po lewej stronie to tryb Explorer, tryb Properties to pionowe okienko po prawej stronie, a tryb Editor to okienko umiejscowione centralnie.
W naszej przykładowej aplikacji mamy stworzone dwa okna: okno z drzewkiem grup oraz okno mapy. Okno Properties jest standardowo dostępne w platformie. Do jego obsługi wystarczy nadpisać metodę createSheet() klasy AbstractNode (patrz klasa JUGNode).
Rysunek 3: Przykład tworzenia okna TopComponent z drzewkiem
Na rysunku (Rysunek 3) pokazano w jaki sposób można utworzyć okno z drzewkiem wykorzystując TopComponent oraz BeanTreeView (BeanTreeView dziedziczy po klasie JScrollPane). Korzystając z kreatora dla utworzenia nowego komponentu typu „Window Component” wystarczy tylko, że dodamy w konstruktorze nowo utworzonej klasy informację, jak ma być budowane drzewko (podajemy obiekt, który ma pełnić rolę korzenia drzewa) oraz dodajemy implementację interfejsu ExplorerManager.Provider (wprowadza on tylko jedną metodę getExplorerManager()).
package org.myorg.tree; ... final class JUGTreeTopComponent extends TopComponent implements ExplorerManager.Provider { ... private ExplorerManager manager = new ExplorerManager(); private JUGTreeTopComponent() { initComponents(); setName(NbBundle.getMessage( JUGTreeTopComponent.class, "CTL_JUGTreeTopComponent")); setToolTipText(NbBundle.getMessage( JUGTreeTopComponent.class, "HINT_JUGTreeTopComponent")); <strong>manager.setRootContext(new JUGNode()); associateLookup(ExplorerUtils.createLookup( manager, getActionMap()));</strong> } ... public ExplorerManager getExplorerManager() { return manager; } }
W powyższym kodzie przedstawiono te linijki kodu, które trzeba dodać po zakończeniu kreatora. Nieistotne fragmenty kodu zastąpiono wielokropkiem(...). Widzimy tutaj przykład użycia ExplorerManager'a, o którym była już wcześniej mowa (dla przypomnienia - zarządza zaznaczaniem). Zaznaczony węzeł (a właściwie związany z nim obiekt danych) jest rejestrowany w globalnym rejestrze – Lookup. Okno mapy implementuje interfejs LookupListener, który nasłuchuje zmian generowanych między innymi przez ExplorerManager w Lookup.
package org.myorg.map; ... import org.jdesktop.swingx.JXMapKit; final class MapTopComponent extends TopComponent implements LookupListener { private JXMapKit mapKit; private MapTopComponent() { initComponents(); setName(NbBundle.getMessage( MapTopComponent.class, "CTL_MapTopComponent")); setToolTipText(NbBundle.getMessage( MapTopComponent.class, "HINT_MapTopComponent")); mapKit = new JXMapKit(); add(mapKit, BorderLayout.CENTER); } @Override public void componentOpened() { Lookup.Template tmp = new Lookup.Template( JUGDataObject.class); result = Utilities.actionsGlobalContext(). lookup(tmp); result.addLookupListener(this); } @Override public void componentClosed() { result.removeLookupListener(this); result = null; } ... private Lookup.Result result = null; public void resultChanged( LookupEvent lookupEvent) { Lookup.Result r = (Lookup.Result) lookupEvent.getSource(); Collection c = r.allInstances(); if (!c.isEmpty()) { JUGDataObject jug = (JUGDataObject) c.iterator().next(); mapKit.setAddressLocation(jug.getPosition()); } } }
MapTopComponent nasłuchuje wszelkich zmian obiektu typu JUGDataObject w globalnym rejestrze Lookup i jeśli nastąpiła zmiana, odczytuje dane na temat nowego obiektu i wyświetla jego dane geograficzne ( mapKit.setAddressLocation(jug.getPosition())) w kontrolce mapy.
Akcje i menu
Menu i paski narzędzi to główne udogodnienia dla użytkownika aplikacji. Menu jest zorganizowane hierarchicznie i podzielone według intuicyjnych kryteriów (np. Edycja, Plik, Narzędzia, Widok,...). Każdy moduł określa, gdzie w menu mają się pojawić akcje przez ten moduł wprowadzane.
Zarządzanie stanem akcji, które zależą od stanu obiektów wprowadzonych przez inne moduły, byłoby niezwykle trudne z wykorzystaniem typowych akcji dostarczanych przez Swinga. Dlatego platforma NetBeans daje nam do dyspozycji własne, bogatsze klasy akcji. Przykładem niech będzie klasa JUGDescAction. Jest ona rozszerzeniem klasy CookieAction. W skrócie można powiedzieć, że jest to akcja, której stan zależy od aktualnie zaznaczonego węzła (lub obiektu danych DataObject). W naszym przykładzie akcja JUGDescAction jest dostępna, jeśli jest zaznaczony węzeł dla JUGDataObject.
package org.myorg.actions; public final class JUGDescAction extends CookieAction { protected void performAction( Node[] activatedNodes) { JUGDataObject dataObject = activatedNodes[0].getLookup().lookup( JUGDataObject.class); if(dataObject != null) { String text = "<html><center>" + dataObject.getLeaders() + "<br>" + dataObject.getPage() + "</center></html>"; JLabel label = new JLabel(text); DialogDescriptor dd = new DialogDescriptor(label, dataObject.getName()); DialogDisplayer.getDefault().notify(dd); } } ... protected Class[] cookieClasses() { return new Class[]{JUGDataObject.class}; } @Override protected String iconResource() { return "org/myorg/actions/pjug.gif"; } ... }
Po implementacji nowej klasy akcji należy ją zarejestrować w pliku layer.xml, który jest głównym plikiem konfiguracyjnym aplikacji zbudowanej na platformie (tzw. System File System). W większości przypadków nie trzeba robić tego ręcznie, kreator zrobi to za nas.
Platforma NetBeans jest produktem sprawdzonym przez tysiące programistów wykorzystujących na co dzień w swojej pracy NetBeans IDE. Dzięki dużemu wsparciu ze strony Sun Microsystem, platforma i produkty na niej oparte są stale rozwijane. Nie do przecenienia jest także wsparcie ze strony firm trzecich udostępniających najróżniejsze moduły. Wykorzystując w swojej pracy platformę możemy zaoszczędzić dużo cennego czasu i zamiast zajmować się interfejsem użytkownika możemy skupić się nad jeszcze lepszą implementacją logiki aplikacji. Dodatkowym argumentem jest to, że są dostępne kody źródłowe NetBeans IDE, gdzie zawsze można podglądnąć w jaki sposób programiści NetBeans IDE wykorzystują mechanizmy platformy.
Nie ma jeszcze komentarzy.