NetBeans 6.8 y Visual Library API, ejemplo completo

domingo, 13 de diciembre de 2009
Hace unos días me vi enfrentado en la necesidad de desarrollar una aplicación que permitiera crear diagramas arrastrando iconos desde una paleta. Para esto existen varias bibliotecas de la API de NetBeans que son muy útiles, las cuales permiten desarrollar una aplicación del tipo "Editor visual", como Microsoft Visio o Dia, con el cual podemos generar diagramas y agregar acciones.

Muchos pequeños ejemplos para trabajar con la Visual Library API se pueden encontrar en http://netbeans.sourcearchive.com/documentation/6.0.1/files.html.

También existe este ejemplo en http://java.dzone.com/news/how-create-visual-applications el cual nos indica como agregar un poco de funcionalidad a un widget y este otro ejemplo en http://java.dzone.com/news/how-add-resize-functionality-v que nos dice como agregar la funcionalidad de "Resize" de un widget, el cual modifiqué un poco.

Otro ejemplo muy bueno es éste de acá, que nos muestra como añadir la Common Palette a nuestra aplicación, funcionalidad que se utilizó en este ejemplo.

Primero que todo quiero agradecer la buena voluntad de Geertjan Wielenga y David Kaspar, quienes muy amablemente me ayudaron a entender como funcionan algunas API de NetBeans y con lo cual pude desarrollar un ejemplo que quiero compartir con ustedes.

Lo primero es aclarar que el ejemplo hace uso de la plataforma NetBeans, tanto para su desarrollo como para su ejecución. NetBeans trae la opción de crear una aplicación utilizando un esqueleto llamado NetBeans Platform Application


al cual, en este ejemplo se le dio el nombre de Visual Editor.



Sobre este esqueleto se agregan módulos.


y los módulos se pueden crear como aplicaciones standalone (para cargar en NetBeans IDE) o como módulos para una aplicación del tipo NetBeans Platform Application, el cual es nuestro caso.


La gracia de crear módulos en NetBeans es que pueden ser exportados como archivos nbm, los cuales pueden ser cargados dentro de NetBeans o nuestras aplicaciones del tipo NetBeans Platform Application.

Al crear una aplicación del tipo NetBeans Platform Application, le podemos cargar todos los módulos que queramos lo cual nos permite crear una aplicación extensible y además nos permite exportar la aplicación con binarios listos para su ejecución (script en linux, exe en windows).

Dejo aquí el enlace de descarga del código fuente del ejemplo, el cual iremos comentando a continuación y acá están los binarios.

Para crear una aplicación del tipo editor de diagramas, debemos crear una escena. Una escena es un componente que permite agregar widgets en él y se encarga de manejarlos por nosotros.

En el caso de este ejemplo, creamos una clase llamada MyScene que hereda de la clase GraphScene.

public class MyScene extends GraphScene<MyNode, MyEdge> {

En una escena tenemos Nodos y Edges. Ambos son widgets pero la diferencia es que los Nodos los utilizamos para los iconos representativos que agregamos a la escena y los Edges los utilizamos para crear conexiones entre los nodos, es por eso que creamos las clases MyNode y MyEdge.

Cuando agregamos un nodo a una escena, utilizamos el método addNode al cual le pasamos como argumento un objeto de tipo MyNode (ver método accept de la clase MyScene).

Widget w = MyScene.this.addNode(new MyNode(idGenerator.getNextId(), image, "Object " + (nodeCounter++), point));

cuando se llama el método addNode inmediatamente se dispara una llamada a la implementación del método attachNodeWidget que es donde recién se agrega el Widget a la escena

protected Widget attachNodeWidget(MyNode node) {

MyWidget widget = new MyWidget(this, mainLayer, interactionLayer, connectionLayer, node);

mainLayer.addChild(widget);

setFocusedWidget (widget);

validate();



return widget;

}

La gracia de usar un objeto MyNode es que podemos almacenar y obtener los datos propios del Widget, los cuales podemos utilizar luego para almacenarlos en un archivo xml, con lo cual le agregamos la función de importar y exportar la escena.

En la clase MyWidget agregamos la opción de crear conexiones con otros widgets creando una acción en particular en el constructor:

getActions().addAction(ActionFactory.createExtendedConnectAction(connectionLayer, new MyConnectionProvider(scene)));

Esto utiliza un objeto del tipo MyConnectionProvider el cual se encarga de generar un objeto MyEdge y lo agrega utilizando el método addEdge de la escena. Al llamar al método addEdge se dispara una llamada a la implementación del método attachEdgeWidget de la escena, el cual se encarga de agregar un widget de conexión.

En el caso de este ejemplo se creó una clase para el widget con la imagen (MyWidget) y otra para la conexión (MyConnectionWidget).
Ambos tienen la capacidad de modificar un texto en la escena ya que se les agrego la acción de edición usando createInplaceEditorAction.

Es interesante el poder agregar un menú a un Widget, lo cual nos da la posibilidad de añadir funcionalidad a los widgets. Para esto se utiliza un PopupMenuProvider el cual retorna un JPopupMenu el que a su vez puede contener JMenuItems con la funcionalidad que queramos en cada uno, ver la clase MyWidget.

popupMenuProvider = new PopupMenuProvider() {

public JPopupMenu getPopupMenu (final Widget widget, final Point location) {

return popupMenu;

}

};

Nuestro buen amigo Geertjan Wielenga nos dió un tip muy útil para agregar la funcionalidad de Resize de nuestros Widgets, tip que apliqué en este ejemplo y que podemos ver acá. El único problema es que es que para que ese tip funcione, se debe modificar la clase ImageWidget del API de la Visual Library, lo cual no es muy cómodo. Lo que hice fue crear una clase llamada MyImageWidget a la cual le apliqué el tip de Geertjan. También generé una clase llamada MyIconNodeWidget, la cual hace uso de la clase MyImageWidget (en vez de la clase ImageWidget). La clase MyWidget extiende de MyIconNodeWidget en vez de IconNodeWidget por lo que de esa forma se tiene la funcionalidad de Resize sin necesidad de cargar el código fuente de la API de la Visual Library.

También se agregó la funcionalidad de poder eliminar un widget (MyWidget o MyConnectionWidget) desde la escena presionando la tecla DELETE, para lo cual se creó la clase KeyEventLoggerAction, en donde el código para eliminar el widget es el siguiente:

public State keyReleased(Widget widget, WidgetKeyEvent event) {

if (event.getKeyCode() == KeyEvent.VK_DELETE) {

GraphScene s = (GraphScene)widget.getScene();

if(widget instanceof MyWidget) {

s.removeNode(s.findObject(widget));

return State.CONSUMED;

}

else if(widget instanceof MyConnectionWidget){

widget.removeFromParent();

return State.CONSUMED;

}

}



return State.REJECTED;

}

Lo importante acá es que para eliminar un objeto MyWidget se debe utilizar el método removeNode y en el caso de un objeto MyConnectionWidget se debe utilizar removeFromParent.

La acción de eliminar se agrega en las clases MyWidget y MyConnectionWidget.

Una opción muy útil, y que no he visto en Internet, es la capacidad de guardar la escena para que la podamos cargar en otro momento (importar/exportar). Para esto creé el método saveWidgetsToXML en la clase MyScene y lo que hace es tomar cada uno de los objetos MyWidget, toma la clase MyNode de cada uno y lo pasa a un XML, almacenando sus atributos como texto (incluyendo la imagen, usando Base64). También toma los objetos MyConnectionWidget, de cada uno toma el objeto MyEdge y los almacena en el mismo XML, guardando la relación entre Widgets.

public void saveWidgetsToXML() {

JFileChooser chooser = new JFileChooser ();

chooser.setDialogTitle ("Save Scene As XML");

chooser.setDialogType (JFileChooser.SAVE_DIALOG);

chooser.setMultiSelectionEnabled (false);

chooser.setFileSelectionMode (JFileChooser.FILES_ONLY);

chooser.setFileFilter (new FileFilter() {

public boolean accept (File file) {

if (file.isDirectory ())

return true;

return file.getName ().toLowerCase ().endsWith (".xml"); // NOI18N

}

public String getDescription () {

return "Extensible Markup Language (.xml)"; // NOI18N

}

});

if (chooser.showSaveDialog (new JFrame()) != JFileChooser.APPROVE_OPTION)

return;



File file = chooser.getSelectedFile ();

if (! file.getName ().toLowerCase ().endsWith (".xml")) // NOI18N

file = new File (file.getParentFile (), file.getName () + ".xml"); // NOI18N

if (file.exists ()) {

DialogDescriptor descriptor = new DialogDescriptor (

"File (" + file.getAbsolutePath () + ") already exists. Do you want to overwrite it?",

"File Exists", true, DialogDescriptor.YES_NO_OPTION, DialogDescriptor.NO_OPTION, null);

DialogDisplayer.getDefault ().createDialog (descriptor).setVisible (true);

if (descriptor.getValue () != DialogDescriptor.YES_OPTION)

return;

}



WidgetsXML wxml = new WidgetsXML(file);

wxml.prepareToSave();



List<Widget> list = mainLayer.getChildren();

for(int i=0; i<list.size(); i++) {

MyWidget w = (MyWidget)list.get(i);

MyNode n = w.getNode();

n.setLocation(w.getLocation());

wxml.addMyNode(n);

}



List<Widget> listConn = connectionLayer.getChildren();

for(int i=0; i<listConn.size(); i++) {

MyConnectionWidget w = (MyConnectionWidget)listConn.get(i);

MyEdge e = w.getMyEdge();

wxml.addMyEdge(e);

}



wxml.save();

}

El archivo XML tiene la forma

<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"no\"?>

<raiz>

<my-nodes></my-nodes>

<my-edges></my-edges>

</raiz>

También se agregó la opción de cargar el XML y reconstruir la escena, para lo que se creó el método loadWidgetsFromXML

public void loadWidgetsFromXML() {

JFileChooser chooser = new JFileChooser ();

chooser.setDialogTitle ("Load Scene From XML");

chooser.setDialogType (JFileChooser.OPEN_DIALOG);

chooser.setMultiSelectionEnabled (false);

chooser.setFileSelectionMode (JFileChooser.FILES_ONLY);

chooser.setFileFilter (new FileFilter() {

public boolean accept (File file) {

if (file.isDirectory ())

return true;

return file.getName ().toLowerCase ().endsWith (".xml"); // NOI18N

}

public String getDescription () {

return "Extensible Markup Language (.xml)"; // NOI18N

}

});

if (chooser.showSaveDialog (new JFrame()) != JFileChooser.APPROVE_OPTION)

return;



File file = chooser.getSelectedFile ();



WidgetsXML wxml = new WidgetsXML(file);

wxml.prepareToLoad();



ArrayList<MyNode> myNodes = wxml.getMyNodes();

for(int i=0; i<myNodes.size(); i++) {

MyNode node = myNodes.get(i);

Widget w = MyScene.this.addNode(node);

getSceneAnimator().animatePreferredLocation(w, w.convertLocalToScene(node.getLocation()));

}



ArrayList<MyEdge> myEdges = wxml.getMyEdges();

MyNode source = null;

MyNode target = null;

for(int i=0; i<myEdges.size(); i++) {

MyEdge edge = myEdges.get(i);



for(int j=0; j<myNodes.size(); j++) {

MyNode n = myNodes.get(j);



if(n.getId().equals(edge.getSource()))

source = n;

if(n.getId().equals(edge.getTarget()))

target = n;

}



addEdge(edge);

setEdgeSource(edge, source);

setEdgeTarget(edge, target);

}

}

El detalle de la funcionalidad de los Widgets lo pueden ver leyendo el código fuente de este ejemplo. El código es muy limpio y simple, por lo que no creo que tengan problemas en entenderlo. Si hay dudas, comenten en el blog.

Dejo unas capturas de pantalla para que vean como luce la aplicación.



Saludos !!!