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 Applicational 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 !!!