Desarrollo de aplicaciones visuales - sing-group.orglipido/dojos/swing/swing.pdf · o general, como...
Transcript of Desarrollo de aplicaciones visuales - sing-group.orglipido/dojos/swing/swing.pdf · o general, como...
Desarrollo de aplicaciones visuales Daniel González Peña
17.1 Fundamentos
Los lenguajes de programación de propósito general, como lo es Java, disponen de
librerías para la creación de interfaces gráficas de usuario (GUI, Graphical User
Interface). Estas librerías suelen ser un conjunto de componentes o widgets
(botones, áreas de texto, listas desplegables, menús, etc.) que constituyen lo que se
conoce comúnmente como toolkits. Sin embargo, la existencia de uno o varios
toolkits para un lenguaje de programación, no implica que sean estándar, ni que se
incluyan por defecto con el lenguaje. Así por ejemplo, las librerías GTK+, Qt,
wxWindows, VCL/Kylix asociadas todas ellas a los lenguajes C/C++, son
soluciones tanto libres como propietarias que no están asociadas de forma estándar
a estos lenguajes.
JFC (Java Foundation Classes), es un framework para la creación de interfaces
gráficas de usuario en Java. Está formada por:
• AWT (Abstract Window Toolkit). Permite la integración de los
programas con el sistema de ventanas subyacente, incluyendo APIs para
el “Drag and Drop”.
Capítulo • Java 2D. Permite la creación de gráficos 2D, tratamiento de imágenes,
texto e impresión.
• Componentes Swing. Extiende a AWT incluyendo un conjunto de
componentes (toolkit) para la creación de interfaces visuales. Además
posee un sistema extensible para el manejo del aspecto y comportamiento
(Pluggable Look and Feel).
• Accesibilidad. Permite la creación de aplicaciones accesibles para
usuarios con alguna discapacidad.
• Internacionalización. Permite la creación de aplicaciones que pueden
interactuar con los usuarios utilizando su propio lenguaje y convenciones
(fechas, moneda, etc.).
Es preciso destacar que los componentes Swing se incluyeron más tarde que AWT.
Hasta ese momento se utilizaban otros componentes visuales (botones, áreas de
texto, listas desplegables…) que formaban parte de AWT. En AWT, cada
componente se renderiza y es controlado directamente por un componente nativo
de la plataforma en la que se esté ejecutando el programa en cada momento. Un
componente AWT es, en realidad, un “envoltorio” de un componente nativo. Por
el contrario, los componentes Swing son 100% Java y no requieren la creación de
un componente nativo asociado a cada componente Swing, si no que se dibujan
directamente mediante Java 2D, sin que el sistema subyacente sea consciente de su
existencia en pantalla. Una consecuencia directa de esta diferencia de diseño es que
una aplicación AWT tendrá un aspecto muy parecido al resto de aplicaciones del
sistema operativo donde se ejecute y, por el contrario, una aplicación Swing podrá
mantener un aspecto uniforme independientemente de la plataforma donde se
ejecute.
AWT, la solución más antigua, ha sido muy criticada y actualmente se aconseja el
uso de los componentes Swing frente a los de AWT (una forma típica de
distinguirlos es que los componentes Swing comienzan por “J” y los AWT no, p.
ej.: JBUTTON frente a BUTTON). Swing se distribuye por defecto con el JDK a partir de
su versión 1.2. Se considera una tecnología más orientada a objetos, más
comprensible, más avanzada, menos difícil de utilizar y menos difícil de depurar
que AWT. Sin embargo, en términos estrictos, Swing no es un reemplazo total de
AWT, si no que es un complemento. Es habitual que en un programa Swing se
importen paquetes de AWT para el manejo de eventos, el control de la distribución
y otras tareas.
Frente a AWT y Swing como alternativas estándar para la creación de interfaces
gráficas, existe otra tercera tecnología, más reciente, denominada SWT (Standard
Widget Toolkit). Es software libre y ha sido desarrollada por IBM y ahora es
mantenida por la Fundación Eclipse (http://www.eclipse.org). SWT se puede ver
como un compromiso entre AWT y Swing, en el sentido de que es un API de alto
nivel como Swing a la vez que está implementada “envolviendo” los diferentes
componentes nativos en clases Java, como hace AWT.
Este capítulo trata sobre el desarrollo de aplicaciones visuales con Swing cuyo
aspecto se parece al que muestra la Figura 17.1 cuando se utiliza look and feel por
defecto.
Figura 17.1. Aplicación Swing con el aspecto “Metal”
Obviamente no se van a tratar en profundidad todos los aspectos que puede
proporcionar este framework tan amplio, mostrando una por una todas las API de
cada uno de los componentes, si no que se hará alusión a la mayoría de ellos
mostrando ejemplos básicos de su funcionamiento.
17.2 Aspectos generales de las aplicaciones Swing
17.2.1 Vista rápida de un programa Swing
Antes de entrar en detalle sobre los componentes Swing, se hará una breve
introducción sobre la estructura típica de un programa Swing. Para introducir estos
conceptos básicos se indicarán los pasos a seguir para implementar una aplicación
con el aspecto que muestra la Figura 17.2.
Figura 17.2. Aplicación simple Swing
Los pasos básicos a seguir son:
1. Importar paquetes Swing.
import javax.swing.*; import java.awt.*; import java.awt.event.*;
2. Elegir el aspecto y el comportamiento (Look and Feel).
public static void main(String [] args) { try { UIManager.setLookAndFeel( UIManager.getCrossPlatformLookAndFeelClassName()); } catch (Exception e) { } //Crea y muestra el GUI... }
El fragmento de código anterior se utilizaría el aspecto multi-plataforma, de forma
que la aplicación se vería igual independientemente del sistema operativo que se
esté usando. Este aspecto se conoce comúnmente como “Metal”.
3. Configurar el contenedor de alto nivel.
AApplliiccaacciioonnSSwwiinnggDDeemmoo..jjaavvaa
Todo programa que presente una interfaz Swing contiene al menos un contenedor
Swing de alto nivel. Para la mayoría de los programas, los contenedores de alto
nivel Swing son instancias (directas o que heredan) de JFRAME, JDIALOG, o (para los
applets) JAPPLET. Cada objeto JFRAME implementa una ventana secundaria. Cada
objeto JAPPLET implementaría un área de pantalla de un applet dentro de una
ventana de navegador. Un contenedor de alto nivel Swing proporciona el soporte
que necesitan los componentes Swing para realizar su dibujado y su manejo de
eventos.
public class SwingApplication { ... public static void main(String[] args) { ... JFrame frame = new JFrame("AplicacionSwingDemo"); //...crear los componentes que iran en la ventana... //...agruparlos en un contenedor llamado contents... frame.getContentPane().add(contents, BorderLayout.CENTER); //Terminar de configurar la ventana y mostrarla. frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); frame.pack(); frame.setVisible(true); } }
El fragmento de código anterior utiliza la clase JFRAME directamente creando una
instancia de la misma para luego añadirle componentes utilizando su referencia.
Sin embargo, lo habitual es crear una clase que hereda de alguno de los
contenedores de alto nivel y configurarla en su constructor, tal y como muestra el
siguiente fragmento alternativo:
public class MiVentana extends JFrame { public MiVentana(){ super("AplicacionSwingDemo"); //...crear los componentes que iran en la ventana... //...agruparlos en un contendor llamado contents this.getContentPane().add(contents, BorderLayout.CENTER); //Terminar de configurar la ventana y mostrarla. this.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); this.pack(); } } ... public class Aplicacion{ public static void main(String[] args) { ... MiVentana frame = new MiVentana(); frame.setVisible(true); } }
4. Configurar los botones y las etiquetas.
La aplicación ejemplo únicamente contiene un botón y una etiqueta. Para la
creación del botón bastaría con las siguientes líneas:
JButton button = new JButton("I'm a Swing button!"); button.setMnemonic('i'); button.addActionListener(this);
La primera línea crea el botón. La segunda selecciona la letra ‘i’ como mnemónico
que el usuario puede utilizar para simular un clic del botón. Por ejemplo, si se usa
el Look And Feel “Metal”, tecleando Alt+i resulta en un clic del botón. La tercera
línea registra un manejador de eventos para el clic del botón. Los manejadores de
eventos se tratarán más adelante.
Para iniciar la etiqueta sería:
JButton button = new JLabel(“Numero de clics: 0”);
5. Añadir componentes a contenedores.
Suele ser habitual que los componentes se agrupen en paneles de forma que sea
más sencillo distribuirlos finalmente en la ventana mediante las técnicas de control
de la distribución que se tratarán más adelante. En el ejemplo, tanto el botón como
la etiqueta se agruparán en un panel distribuidos en una rejilla de una única
columna y dos filas, yendo primero el botón y luego la etiqueta.
JPanel pane = new JPanel(); pane.setLayout(new GridLayout(0, 1)); pane.add(button); pane.add(label);
6. Manejar eventos.
Se establecerá un manejador de eventos que atienda la pulsación sobre el botón que
incrementará una variable que cuenta el número de pulsaciones que se han
realizado desde el inicio de la aplicación.
button.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent e) { numClicks++; label.setText(labelPrefix + numClicks); } });
El manejo de eventos se tratará en detalle más adelante.
17.2.2 Jerarquía de componentes
Para entender mejor el funcionamiento de Swing es necesario saber que un
programa Swing puede ser visto como un árbol de componentes. Siguiendo con el
ejemplo de la Figura 17.2 se han utilizado los siguientes componentes:
• Un frame o ventana principal (JFRAME).
• Un panel, algunas veces llamado pane (JPANEL).
• Un botón (JBUTTON).
• Una etiqueta (JLABEL).
El frame es un contenedor de alto nivel. Existe principalmente para proporcionar
espacio para que se dibujen otros componentes Swing. Los otros contenedores de
alto nivel más utilizados son los diálogos (JDIALOG) y los applets (JAPPLET).
El panel es un contenedor intermedio. Su único propósito es simplificar el
posicionamiento del botón y la etiqueta. Otros contenedores intermedios, como los
paneles desplazables (JSCROLLPANE) y los paneles con pestañas (JTABBEDPANE),
típicamente juegan un papel más visible e interactivo en el GUI de un programa.
El botón y la etiqueta son componentes atómicos, es decir, componentes que
existen no para contener otros componentes Swing, sino como entidades auto-
suficientes que representan bits de información para el usuario. Frecuentemente,
los componentes atómicos también obtienen entrada del usuario. El API Swing
proporciona muchos componentes atómicos, incluyendo combo-boxes
(JCOMBOBOX), campos de texto (JTEXTFIELD) y tablas (JTABLE).
La Figura 17.3 muestra un diagrama con el árbol de contenidos de la ventana
mostrada en la Figura 17.2.
Figura 17.3. Árbol de contenidos de una aplicación Swing
El diagrama muestra todos los contenedores creados o usados por el programa,
junto con los componentes que contienen. Si se añadiese una ventana (p. ej.: un
diálogo), la nueva ventana tendría su propio árbol de contenidos, independiente del
mostrado en la Figura 17.3.
17.3 Manejo de eventos
Cada vez que el usuario teclea un carácter o pulsa un botón del ratón, ocurre un
evento (Event). Cualquier objeto puede ser notificado del evento. Únicamente debe
implementar la interfaz apropiada y ser registrado como un oyente (Listener) del
evento apropiado. Los objetos registrados como oyentes se denominan también
manejadores de eventos. Los componentes Swing pueden generar muchas clases
de evento.
Cada evento está representado por un objeto que ofrece información sobre el
evento e identifica la fuente. Las fuentes de los eventos normalmente son
componentes Swing, pero otros tipos de objetos también pueden ser fuente de
eventos. Para atender a un mismo evento sobre una misma fuente se pueden
registrar múltiples oyentes, tal y como muestra la Figura 17.4.
Figura 17.4. Oyentes atendiendo a un evento
Algunos ejemplos típicos de interfaces que definen oyentes y los eventos asociados
se muestran en la Tabla 17.1.
Tabla 17.1. Eventos Swing más habituales
Acción que resulta en el evento Interfaz oyente Objeto Evento
El usuario pulsa un botón, presiona Return mientras se teclea en un campo de texto o elige un ítem de menú.
ACTIONLISTENER ACTIONEVENT
El usuario elige un frame (ventana principal). WINDOWLISTENER WINDOWEVENT El usuario pulsa un botón del ratón mientras el cursor está sobre un componente.
MOUSELISTENER MOUSEEVENT
El usuario mueve el cursor sobre un componente.
MOUSEMOTIONLISTENER MOUSEEVENT
El componente se hace visible. COMPONENTLISTENER COMPONENTEVENT El componente obtiene el foco del teclado. FOCUSLISTENER FOCUSEVENT Cambia la selección de una lista. LISTSELECTIONLISTENER LISTSELECTIONEVENT Cuando se selecciona o deselecciona un elemento de una lista, un check box, un radio button, etc.
ITEMLISTENER ITEMEVENT
17.3.1 Creación de manejadores de eventos
Para implementar un manejador de eventos se deben seguir tres pasos:
1. Definir una clase manejadora de eventos indicando el tipo de oyente que es, en
función del evento que desea atender. Ello se indica, o bien implementando
directamente la interfaz adecuada, o bien heredando de una clase que hereda
de dicha interfaz. Por ejemplo:
public class MiManejador implements ActionListener {
2. Implementar los métodos necesarios del manejador que serán invocados cada
vez que se produzca el evento. Por ejemplo:
public void actionPerformed(ActionEvent e) { System.out.println(“Se produjo un evento”); }
3. Registrar una instancia del manejador como oyente sobre uno o más
componentes. Esto se realiza invocando a métodos que comienzan por ADD*,
por ejemplo:
MiManejador manejador = new MiManejador(); boton.addActionListener(manejador);
Suele ser frecuente que los manejadores de eventos se implementen “en línea”
como clases anónimas y no como una clase aparte. El siguiente fragmento de
código es equivalente a los tres pasos anteriores:
boton.addActionListener(new ActionListener(){ public void actionPerformed(ActionEvent e){ System.out.println(“Se produjo un evento”); } });
17.3.2 Clases adaptadoras
Algunas interfaces de oyentes declaran más de un método. Sin embargo, se puede
estar interesado en definir sólo un subconjunto de ellos. Por ese motivo existen
clases adaptadoras (Adapter) que implementan las interfaces de los oyentes
implementando todos los métodos (sin código). De este modo se podría
implementar un oyente heredando de una clase adaptadora y redefiniendo
únicamente el o los métodos de interés.
Por ejemplo, si se desea capturar el evento de que el ratón pasa por encima de un
componente, se debe crear una clase que implemente MOUSELISTENER. Esta interfaz
define cinco métodos, pero únicamente interesa el método
MOUSEENTERED(MOUSEEVENT E). Sin embargo se está obligado a implementar siempre
todos los métodos de una interfaz. Por ello existe la clase MOUSEADAPTER que ya
implementa esta interfaz con lo que la mejor opción sería que el manejador de
eventos heredase de esta clase y redefiniese únicamente dicho método.
La Tabla 17.2 muestra algunas interfaces oyente y sus métodos, junto con las
clases adaptadoras que las implementan.
Tabla 17.2. Interfaces de oyente, clases adaptadoras y métodos
Interfaz Clase Adaptadora Métodos ACTIONLISTENER ninguna ACTIONPERFORMED CARETLISTENER ninguna CARETUPDATE CHANGELISTENER ninguna STATECHANGE COMPONENTLISTENER COMPONENTADAPTER COMPONENTHIDDEN
COMPONENTMOVED COMPONENTRESIZED COMPONENTSHOWN
CONTAINERLISTENER CONTAINERADAPTER COMPONENTADDED COMPONENTREMOVED
DOCUMENTLISTENER ninguna CHANGEDUPDATE INSERTUPDATE REMOVEUPDATE
FOCUSLISTENER FOCUSADAPTER FOCUSGAINED FOCUSLOST
INTERNALFRAMELISTENER INTERNALFRAMEADAPTER INTERNALFRAMEACTIVATED INTERNALFRAMECLOSED INTERNALFRAMECLOSING INTERNALFRAMEDEACTIVATED INTERNALFRAMEDEICONIFIED INTERNALFRAMEICONIFIED INTERNALFRAMEOPENED
ITEMLISTENER ninguna ITEMSTATECHANGED
KEYLISTENER KEYADAPTER KEYPRESSED KEYRELEASED KEYTYPED
LISTSELECTIONLISTENER ninguna VALUECHANGED MOUSELISTENER MOUSEADAPTER
MOUSEINPUTADAPTER* MOUSECLICKED MOUSEENTERED MOUSEEXITED MOUSEPRESSED MOUSERELEASED
MOUSEMOTIONLISTENER MOUSEMOTIONADAPTER MOUSEINPUTADAPTER*
MOUSEDRAGGED MOUSEMOVED
UNDOABLEEDITLISTENER ninguna UNDOABLEEDITHAPPENED WINDOWLISTENER WINDOWADAPTER WINDOWACTIVATED
WINDOWCLOSED WINDOWCLOSING WINDOWDEACTIVATED WINDOWDEICONIFIED WINDOWICONIFIED WINDOWOPENED
17.3.3 API Action
Suele ser habitual que en una aplicación visual existan varios componentes que
permiten el acceso a la misma funcionalidad. Por ejemplo, en un editor de texto, la
función de “Copiar” se puede alcanzar con el menú “Edición -> Copiar”, pulsando
un botón en la barra de herramientas o mediante un menú contextual. En esos casos
se recomienda el uso de un objeto ACTION para implementar la función.
Un objeto ACTION es un ACTIONLISTENER que proporciona, además de las funciones
propias de un manejador de eventos, un modo centralizado para el manejo del
estado de los componentes que generan los eventos que atiende el objeto ACTION,
como pueden ser botones de barra de herramientas, elementos de un menú, botones
comunes y campos de texto. Por estado se entiende, entre otros: el texto que
muestra el componente, el icono, el mnemónico y el estado
habilitado/deshabilitado.
Para asociar un objeto ACTION a un componente se utiliza el método SETACTION.
Cuando se invoca este método ocurre lo siguiente:
• El estado del componente se actualiza para pasar a utilizar el del objeto
ACTION. Por ejemplo, si al objeto ACTION se le había asociado un texto y un
icono, entonces el texto y el icono del componente pasan a ser los que se
hayan indicado para el objeto ACTION.
• El objeto ACTION se registra como un ACTIONLISTENER del componente.
• Si el estado del objeto ACTION cambia, el estado del componente cambia
automáticamente. Por ejemplo, si se cambia el estado de
habilitado/deshabilitado del objeto ACTION, todos los componentes que
tienen asociado este objeto ACTION cambiarán su estado de
habilitado/deshabilitado.
La Figura 17.5 muestra una aplicación que usa varias ACTION todas ellas colocadas
tanto en un menú (más adelante se verá cómo crear menús), como en botones (más
adelante se verá cómo crear botones).
Figura 17.5. Ejemplo de uso de la API Action
A continuación se verá en diferentes fragmentos de código, cómo se puede obtener
la aplicación de la Figura 17.5.
El siguiente fragmento muestra cómo crear un botón de una barra de herramientas
y un elemento de menú que realizan la misma función:
Action leftAction = new LeftAction(); //El codigo de LeftAction se muestra despues ... button = new JButton(leftAction) ... menuItem = new JMenuItem(leftAction);
Para crear un objeto ACTION, normalmente se crea una subclase de ABSTRACTACTION y
se instancia. En la subclase, se debe implementar el método ACTIONPERFORMED que
atiende al evento de acción. Además es en el constructor de esta subclase donde se
AAccttiioonnDDeemmoo..jjaavvaa
describe la acción, es decir, texto, icono, mnemónico y estado de
habilitado/deshabilitado. Para ello se utiliza el método PUTVALUE de ABSTRACTACTION.
El siguiente ejemplo muestra el código de una clase ACTION concreta denominada
LEFTACTION.
leftAction = new LeftAction("Go left", anIcon, "This is the left button.", new Integer(KeyEvent.VK_L)); ... class LeftAction extends AbstractAction { public LeftAction(String text, ImageIcon icon, String desc, Integer mnemonic) { super(text, icon); putValue(SHORT_DESCRIPTION, desc); putValue(MNEMONIC_KEY, mnemonic); } public void actionPerformed(ActionEvent e) { displayResult("Action for first button/menu item", e); } }
El método PUTVALUE recibe como primer parámetro una constante de tipo STRING que
identifica la propiedad concreta. Los componentes a los que se le asigne la acción
(botones, menús, etc.) obtendrán automáticamente estos valores de la acción e
invocarán métodos propios de forma automática. No todos los componentes usan
todas las propiedades. La Tabla 17.3 muestra las propiedades que se pueden
definir, junto con los componentes que las leerán y el propósito que tienen.
Tabla 17.3. Propiedades de las Acciones
Propiedad Clase que se auto-configura (método llamado)
Propósito
ACCELERATOR_KEY JMENUITEM (SETACCELERATOR) La combinación de teclas que activa la acción.
ACTION_COMMAND_KEY ABSTRACTBUTTON, JCHECKBOX, JRADIOBUTTON
(SETACTIONCOMMAND)
La cadena de texto asociada a este comando.
LONG_DESCRIPTION ninguno Una descripción larga de la acción. Puede ser utilizada para una ayuda contextual.
MNEMONIC_KEY ABSTRACTBUTTON, JMENUITEM, JCHECKBOX, JRADIOBUTTON
(SETTEXT)
El mnemónico de la acción. Es decir, una tecla que activa la acción cuando está visible al usuario.
NAME ABSTRACTBUTTON, JMENUITEM, JCHECKBOX, JRADIOBUTTON
(SETTEXT)
El nombre de la acción.
SHORT_DESCRIPTION ABSTRACTBUTTON, JCHECKBOX, JRADIOBUTTON
(SETTOOLTIPTEXT)
Una descripción corta de la acción.
SMALL_ICON ABSTRACTBUTTON, JMENUITEM
(SETICON) El icono de la acción a utilizar en la barra de herramientas o en un botón.
17.4 Componentes Swing
La API Swing está formada en su mayor parte por los denominados componentes
Swing. Generalmente un componente Swing es una clase (que comienza por ‘J’) y
que representa una entidad que se dibuja en la pantalla. Además de la clase central
del componente, existen clases asociadas a cada componente que se deben conocer
y que se irán viendo en cada caso.
Cada ventana de un programa Swing utiliza al menos un contenedor de alto nivel,
un contenedor intermedio (panel de contenido al menos) y varios componentes
ligeros.
17.4.1 Contenedores de alto nivel
En general, cada aplicación tiene al menos un árbol de contenidos encabezado por
un objeto frame (JFRAME). Cada applet debe tener un árbol de contenido encabezado
por un objeto JAPPLET. Cada ventana adicional de una aplicación o un applet tiene
su propio árbol de contenido encabezado por un frame o dialógo (JFRAME/JDIALOG).
17.4.1.1 Frames
La mayoría de las aplicaciones Swing presentan su GUI principal dentro de un
JFRAME o JAPPLET, un contenedor Swing de alto nivel que proporciona ventanas para
aplicaciones y applets. Un frame tiene decoraciones como un borde, un título, y
botones para cerrar y minimizar la ventana. Un programa típico simplemente crea
un frame, añade componentes al panel de contenido, y quizás añade una barra de
menú. Sin embargo, a través de su panel raíz, JFRAME proporciona soporte para una
mayor personalización.
Para crear una ventana que sea dependiente de otra ventana, es decir, que, por
ejemplo, desaparezca cuando la otra ventana se minimiza, se utiliza un diálogo
(JDIALOG) en vez de un frame. Para crear una ventana que aparece dentro de otra
ventana se utiliza un frame interno (JINTERNALFRAME). Todos ellos se verán más
adelante.
17.4.1.1.1 Añadir componentes a frames
Para incluir componentes en un JFRAME, se añaden a su panel de contenido. Las dos
técnicas más comunes para añadir componentes al panel de contenido de un frame
son.
• Crear un contenedor como un JPANEL, JSCROLLPANE, o un JTABBEDPANE, y
añadirle componentes, luego utilizar JFRAME.SETCONTENTPANE para
convertirlo en el panel de contenido del frame. El siguiente fragmento de
código utiliza esta técnica. El código crea un panel desplazable para
utilizarlo como panel de contenido del frame. Hay una tabla en el panel
desplazable:
public class TableDemo extends JFrame { public TableDemo() { super("TableDemo"); MyTableModel myModel = new MyTableModel(); JTable table = new JTable(myModel); table.setPreferredScrollableViewportSize(new Dimension(500, 70)); //Crear el panel de scroll y añadirle la tabla. JScrollPane scrollPane = new JScrollPane(table); //Añadir el panel de scroll al frame. setContentPane(scrollPane);
• Utilizar JFRAME.GETCONTENTPANE para obtener el panel de contenido del
frame. Añadir componentes al objeto devuelto. El siguiente fragmento de
código utiliza esta técnica:
...//crear los componentes... //obtener el panel de contenido y añadirle los componentes creados: Container contentPane = getContentPane(); // utilizar un manejador de distribución que respete los tamaños contentPane.setLayout(new BoxLayout(contentPane, BoxLayout.Y_AXIS)); contentPane.add(Box.createRigidArea(new Dimension(0, 10))); contentPane.add(controls); contentPane.add(Box.createRigidArea(new Dimension(0, 10))); contentPane.add(emptyArea);
• A partir de la versión 1.5 del JDK no es necesario obtener el panel de
contenido con GETCONTENTPANE para añadirle componentes. Ahora se puede
hacer directamente con el método ADD de JFRAME. En versiones anteriores
este método lanzaba una excepción indicando que no se debía usar
directamente.
El controlador de distribución por defecto para el panel de contenido de un frame
es BORDERLAYOUT (los controladores de distribución se ven más adelante). Cómo se
ha visto en los ejemplos anteriores, se puede invocar al método SETLAYOUT sobre el
panel de contenidos para cambiar su controlador de distribución.
17.4.1.2 Diálogos
Muchas clases Swing soportan diálogos, es decir, ventanas que son más limitadas
que los frames. Todo diálogo depende de un frame. Cuando el frame se destruye,
también se destruyen sus diálogos. Cuando el frame es minimizado, sus diálogos
dependientes también desaparecen de la pantalla. Cuando el frame es maximizado,
sus diálogos dependientes vuelven a la pantalla. Swing proporciona
automáticamente este comportamiento.
Para crear un diálogo, simple y estándar se utiliza JOPTIONPANE. Para crear diálogos
personalizados, se utiliza directamente la clase JDIALOG, la cual se utiliza de forma
muy similar a JFRAME. Otras dos clases, JCOLORCHOOSER y JFILECHOOSER, también
suministran diálogos estándar. Para mostrar un diálogo de impresión se utiliza el
método GETPRINTJOB de la clase TOOLKIT. Se hará especial hincapié en JOPTIONPANE y
JFILECHOOSER por ser los más utilizados.
Un diálogo puede ser modal. Cuando un diálogo modal es visible, bloquea las
entradas del usuario en todas las otras ventanas del programa. Todos los diálogos
que proporciona JOPTIONPANE son modales. Para crear un diálogo no modal,
debemos utilizar directamente la clase JDIALOG, que permite crear tanto diálogos
modales como no modales. Incluso si utilizamos JOPTIONPANE para implementar un
diálogo, estamos utilizando JDIALOG indirectamente.
17.4.1.2.1 Trabajo con JOptionPane
Utilizando JOPTIONPANE se pueden crear muchos tipos de diálogos. Para ello se crea
y se muestra el diálogo utilizando uno de los métodos SHOWXXXDIALOG de
JOPTIONPANE como los que se muestran en la Tabla 17.4.
Tabla 17.4. Tipos de diálogos estándar con JOptionPane
Método Descripción SHOWMESSAGEDIALOG Muestra un diálogo modal con un botón, etiquetado "Aceptar". Se
puede especificar fácilmente el mensaje, el icono y el título que mostrará el diálogo. Para crear un diálogo de este tipo bastaría por ejemplo con: JOptionPane.showMessageDialog(frame, "Esto es un mensaje de
alerta.", “Atención”, JOptionPane.WARNING_MESSAGE); Como se puede observar existe un parámetro para indicar el tipo de mensaje, por el cual Swing asignará un icono adecuado (las opciones son: ERROR_MESSAGE, INFORMATION_MESSAGE, WARNING_MESSAGE, QUESTION_MESSAGE, o
PLAIN_MESSAGE). Ejemplos de este tipo de diálogo se muestran en la Figura 17.6.
SHOWCONFIRMDIALOG Muestra un diálogo modal con dos o tres botones, etiquetados "Sí" y "No" o también “Sí”, “No” y “Cancelar”. Para crear un diálogo de este tipo bastaría por ejemplo con: int opcion = JOptionPane.showConfirmDialog(frame,"Deseas formatear tu disco duro?", "Pregunta", JOptionPane.YES_NO_OPTION); Como se puede observar existe un parámetro para indicar si habrá dos o tres botones utilizando las constantes YES_NO_OPTION o YES_NO_CANCEL_OPTION. Un ejemplo de este tipo de diálogo se muestra en la Figura 17.7.
SHOWINPUTDIALOG Muestra un diálogo modal que obtiene una cadena del usuario. El diálogo de entrada puede mostrar o bien un campo de texto para que el usuario teclee en él, o bien un combo-box no editable, desde el que el usuario puede elegir una de entre varias cadenas. Para crear un diálogo de este tipo bastaría por ejemplo con: Object[] possibilities = {"c++", "java", "pascal"}; String s = (String)JOptionPane.showInputDialog( frame, "El mejor lenguaje de programación es...", "Diálogo de Prueba", JOptionPane.PLAIN_MESSAGE, null,possibilities, "java");
Este tipo de diálogo permite indicar una serie de posibilidades al usuario apareciendo de ese modo un combo-box. Si en lugar de indicar posibilidades se pasa NULL, se proporcionará un área de texto para que el usuario introduzca cualquier cadena. Un ejemplo de este tipo de diálogo se muestra en la Figura 17.8.
SHOWOPTIONDIALOG Muestra un diálogo para que el usuario pulse alguno de sus botones al igual que showConfirmDialog. La diferencia es que permite personalizar los botones que se le ofrecen al usuario a través de un array (showConfirmDialog sólo permitía YES_NO_OPTION y de YES_NO_CANCEL_OPTION). Para crear un diálogo de este tipo bastaría por ejemplo con: Object[] posibilities = {"Como texto plano","Como HTML", "Como PDF", "Cancelar"} int opcion = JOptionPane.showOptionDialog( frame, "Enviar el e-mail como:", "Diálogo de Prueba", JOptionPane.YES_NO_CANCEL_OPTION, JOptionPane.QUESTION_MESSAGE, null, possibilities, "Como HTML");
Este tipo de diálogo mostrará una serie de botones con las diferentes opciones indicadas. Un ejemplo de este tipo de diálogo se muestra en la Figura 17.9.
Todos los métodos mostrados en la Tabla 17.4 están sobrecargados de forma que si
no se desea utilizar algún parámetro (por ejemplo, no se quiere indicar el icono, el
título, etc.), existirá una variante del método para el que no se necesite incluir
dicho parámetro, tomando entonces un valor por defecto. Además existe una
versión SHOWINTERNALXXXDIALOG de cada uno de los métodos de la Tabla 17.4 que
muestra el diálogo como un frame interno a otro.
Figura 17.6. Diálogos estándar creados con showMessageDialog
Figura 17.7. Diálogo creado con showConfirmDialog
Figura 17.8. Diálogo creado con showInputDialog
JJOOppttiioonnPPaanneeDDeemmoo..jjaavvaa
Figura 17.9. Diálogo creado con showOptionDialog
Todos los métodos lanzan un diálogo modal, el cual bloquea el hilo que llama a
dicho método y que no continúa hasta que el usuario introduce una opción. En ese
momento se obtiene el valor de retorno que contiene lo que el usuario ha indicado
(excepto en el caso de SHOWMESSAGEDIALOG, en el que no retorna nada). Para
SHOWCONFIRMDIALOG y SHOWOPTIONDIALOG existen definidas unas constantes que
representan los distintos valores, como por ejemplo, NO_OPTION, YES_OPTION,
CANCEL_OPTION, etc que se pueden usar como sigue:
int opcion = JOptionPane.showConfirmDialog(frame,"Deseas formatear tu disco duro?", "Pregunta", JOptionPane.YES_NO_OPTION); if (opcion == JOptionPane.YES_OPTION){ discoDuro.formatear(); }
17.4.1.2.2 Trabajo con JFileChooser
La clase JFILECHOOSER proporciona un diálogo para elegir un fichero de una lista. Un
selector de ficheros es un componente que podemos situar en cualquier lugar de la
interfaz gráfica de nuestro programa. Sin embargo, normalmente los programas los
muestran en diálogos modales porque las operaciones con ficheros son sensibles a
los cambios dentro del programa. La clase JFILECHOOSER permite mostrar fácilmente
un diálogo modal que contiene un selector de ficheros.
Los selectores de ficheros se utilizan comúnmente para dos propósitos:
• Para presentar una lista de ficheros que pueden ser abiertos por la
aplicación.
• Para permitir que el usuario seleccione o introduzca el nombre de un
fichero a guardar.
Es preciso destacar que el selector de ficheros ni abre ni guarda ficheros.
Simplemente presenta un diálogo para elegir un fichero de una lista. El programa
es responsable de hacer las operaciones que estime oportunas con el fichero, como
abrirlo o guardarlo.
Como la mayoría de los programas necesitan un selector para abrir o guardar
ficheros, la clase JFILECHOOSER proporciona los métodos convenientes para mostrar
estos tipos de selectores de ficheros en un diálogo. La Figura 17.10 muestra el
aspecto de un diálogo para escoger un fichero para abrirlo.
Figura 17.10. Diálogo para guardar un fichero con JFileChooser
El código que crea y muestra el selector de apertura de ficheros es el siguiente
JFileChooser filechooser = new JFileChooser(); int returnVal = filechooser.showOpenDialog(frame); //frame es una instancia de JFrame
Por defecto, un selector de ficheros que no haya sido mostrado anteriormente
muestra todos los ficheros en el directorio del usuario. Se puede especificar un
directorio inicial utilizando uno de los otros constructores de JFILECHOOSER, o se
puede seleccionar el directorio directamente con el método SETCURRENTDIRECTORY.
El siguiente fragmento de código ilustra cómo se obtendría un diálogo para escoger
un fichero para guardar, además de cómo obtener el fichero escogido por el usuario
para hacer el tratamiento que se considere oportuno.
private JFileChooser filechooser = new JFileChooser(); ... int returnVal = filechooser.showSaveDialog(FileChooserDemo.this); if (returnVal == JFileChooser.APPROVE_OPTION) { File file = filechooser.getSelectedFile(); System.out.println(“Guardando el fichero: “+ file.getName()); guardarEn(file); // metodo propio que guarda cierta información en el fichero escogido }
Suele ser interesante tener una única instancia de JFILECHOOSER (por eso en el
ejemplo se muestra como un atributo de una clase) ya que utilizando el mismo
selector de ficheros para abrir y grabar ficheros, el programa consigue los
siguientes beneficios:
• El selector recuerda el directorio actual entre usos, por eso los diálogos de
abrir y grabar comparten el mismo directorio actual.
• Sólo se tiene que personalizar un selector de ficheros, y dicha
personalización se aplicará a las dos versiones, la de apertura y la de
guardado.
• La apertura del diálogo a partir de la segunda vez es más rápida si no se
instancia cada vez.
Cómo se ha visto en los fragmentos de código anteriores, los métodos
SHOWXXXXDIALOG devuelven un entero que indica si el usuario ha seleccionado un
fichero. Podemos utilizar el valor de retorno para determinar si realizar o no la
operación requerida.
Mediante el método GETSELECTEDFILE sobre el selector de ficheros se puede obtener
un ejemplar de FILE, que representa el fichero elegido. El ejemplo obtiene el
nombre del fichero y lo utiliza en un mensaje. Podemos utilizar otros métodos del
objeto FILE para obtener información sobre él.
Una personalización muy común de este tipo de diálogos es el filtrado de ficheros a
escoger. De forma básica se podría indicar si se desea presentar al usuario
únicamente directorios, únicamente ficheros o ambos. Para ello se utiliza el método
SETFILESELECTIONMODE que recibe un parámetro cuyos valores pueden ser:
FILES_ONLY para sólo ficheros, DIRECTORIES_ONLY, para sólo directorios y
FILES_AND_DIRECTORIES, para ambos. Sin embargo, también es frecuente que sea
necesario hacer un filtrado más avanzado a nivel de ficheros, por ejemplo para
aceptar únicamente ficheros con una extensión determinada. Para ello se debe
heredar de la clase FILEFILTER la cual representa un filtro de ficheros con únicamente
dos métodos a implementar:
• BOLEAN ACCEPT(FILE). Debe devolver cierto si el fichero pasado como
parámetro puede ser escogido por el usuario o falso si ese fichero debe ser
filtrado y no se debe mostrar.
• STRING GETDESCRIPTION(). Debe devolver una cadena explicativa del filtro, p.
ej: “Ficheros de Imágenes”.
JFILECHOOSER acepta varios filtros, por lo que se irán añadiendo con
ADDCHOOSABLEFILEFILTER. El siguiente fragmento de código muestra un ejemplo de la
implementación de una subclase de FILEFILTER y de su uso:
class ImageFilter extends FileFilter{ public boolean accept(File f) { if (f.isDirectory()) { return true; } String s = f.getName(); int i = s.lastIndexOf('.'); if (i > 0 && i < s.length() - 1) { String extension = s.substring(i+1).toLowerCase(); if (“tiff”.equals(extension) || “tif”.equals(extension) || “gif”.equals(extension) || “jpeg”.equals(extension) || “jpg”.equals(extension)) { return true; // la extension es de un fichero de imagen } else { return false; // la extension que tiene no es de imagen } return false; // no tiene extension } public String getDescription(){ return “Ficheros de Imagenes”; } } ... // Utilizar el filtro para el JFileChooser filechooser.addChoosableFileFilter(new ImageFilter());
Existen personalizaciones avanzadas, menos comunes, de JFILECHOOSER que
permiten incluir componentes dentro del propio diálogo para por ejemplo incluir
vistas previas de los ficheros que el usuario va escogiendo.
17.4.2 Contenedores intermedios
Los contenedores intermedios son componentes Swing que sólo existen para
contener otros componentes. Técnicamente, las barras de menús pertenecen a esta
categoría, pero se describen en un apartado propio.
JJFFiilleeCChhoooosseerrDDeemmoo..jjaavvaa
Los contenedores intermedios más utilizados son los paneles. Implementados con
la clase JPANEL, los paneles no añaden casi ninguna funcionalidad más allá de las
que tienen los objetos JCOMPONENT. Normalmente se usan para agrupar
componentes, porque los componentes están relacionados o sólo porque agruparlos
hace que la distribución sea más sencilla. Un panel puede usar cualquier
controlador de distribución, y se les puede dotar de bordes fácilmente.
Otros cuatro contenedores Swing proporcionan más funcionalidad. Un JSCROLLPANE
proporciona barras de desplazamiento alrededor de un sólo componente. Un
JSPLITPANE, o panel dividido, es un componente que a su vez permite anidar a dos
componentes separados por una barra vertical u horizontal móvil. Un JTABBEDPANE,
o panel de solapas, muestra sólo un componente a la vez, permitiendo fácilmente
cambiar entre componentes. Un JTOOLBAR, o barra de herramientas, contiene un
grupo de componentes (normalmente botones) en una fila o columna, y
opcionalmente permite al usuario arrastrar la barra de herramientas a diferentes
localizaciones.
El resto de los contenedores intermedios Swing son incluso más especializados.
JINTERNALFRAME, o frame interno, se parece a los JFRAME y tienen un API muy
similar, pero al contrario que los aquéllos deben aparecer dentro de otras ventanas.
JROOTPANE proporciona soporte avanzado a los contenedores de alto nivel.
JLAYEREDPANE permite soportar ordenación en el eje Z de componentes.
17.4.2.1.1 Trabajo con JScrollPane
Un JSCROLLPANE proporciona una vista desplazable de un componente ligero.
Cuando el estado de la pantalla real está limitado, se utiliza un SCROLLPANE para
mostrar un componente que es grande o cuyo tamaño puede cambiar
dinámicamente.
El código para crear un JSCROLLPANE puede ser mínimo. La Figura 17.11 muestra un
programa que utiliza un JSCROLLPANE para ver una salida de texto (JTEXTAREA).
Figura 17.11. Un JScrollPane moviendo un JTextArea
El siguiente fragmento de código muestra cómo se obtendría el programa de la
Figura 17.11.
JTextArea textArea = new JTextArea(5,30); JScrollPane scrollPane = new JScrollPane(textArea); this.add(scrollPane, BorderLayout.CENTER); //this es un JFrame
El programa proporciona el área de texto como argumento al constructor del
JSCROLLPANE estableciendo dicha área de texto como el cliente del panel desplazable
que se encargará de: crear las barras de desplazamiento cuando son necesarias,
redibujar el cliente cuando el usuario se mueve sobre él, etc.
Típicamente se utiliza JSCROLLPANE para el desplazamiento de áreas de texto
(JTEXTAREA), imágenes (IMAGEICON), listas (JLIST), tablas (JTABLE), árboles (JTREE), etc.
Si lo que se necesita es proporcionar desplazamiento básico para un componente
ligero como estos, no se necesitan más conceptos que los vistos hasta ahora. Sin
embargo, un JSCROLLPANE es un objeto altamente personalizable. Se puede
determinar bajo qué circunstancias se mostrarán las barras de desplazamiento.
También se puede decorar con una fila de cabecera, una columna de cabecera y
esquinas. Finalmente se puede crear un cliente de desplazamiento que avisa al
panel desplazable sobre el comportamiento de desplazamiento como los
incrementos de unidad y de bloques.
17.4.2.1.2 Trabajo con JSplitPane
Un JSPLITPANE contiene dos componentes de peso ligero, separados por un divisor.
Arrastrando el divisor, el usuario puede especificar qué cantidad de área pertenece
a cada componente. Un JSPLITPANE se utiliza cuando dos componentes contienen
información relacionada y queremos que el usuario pueda cambiar el tamaño de los
JJSSccrroollllPPaanneeDDeemmoo..jjaavvaa
componentes en relación a uno o a otro. Un uso común de un JSPLITPANE es para
contener una lista para seleccionar un elemento y además una visión de la elección
actual, como por ejemplo sería un programa de correo que muestra una lista con
los mensajes y el contenido del mensaje actualmente seleccionado de la lista.
La Figura 17.12 muestra un programa que utiliza JSPLITPANE.
Figura 17.12. Ejemplo de uso de JSplitPane
El siguiente fragmento de código muestra cómo se trata con JSPLITPANE para crear el
programa de la Figura 17.12.
... // crear una lista desplazable llamada listScrollPane // crear una imagen desplazable llamada listScrollPane ... JSplitPane splitPane = new JSplitPane(JsplitPane.HORIZONTAL_SPLIT); splitPane.setLeftComponent(listScrollPane); splitPane.setRightComponent(pictureScrollPane); splitPane.setOneTouchExpandable(true); // tener botones para ampliar y reducir totalmente // Dar el tamaño mínimo de los components del JSplitPane Dimension minimumSize = new Dimension(100, 50); listScrollPane.setMinimumSize(minimumSize); pictureScrollPane.setMinimumSize(minimumSize); // Dar la posicion inicial y tamaño de la barra divisoria splitPane.setDividerLocation(150); splitPane.setDividerSize(10);
El constructor utiliza SETLEFTCOMPONENT y SETRIGHTCOMPONENT para situar la lista y la
etiqueta de imagen en el JSPLITPANE. Si el JSPLITPANE tuviera orientación vertical, se
podría utilizar SETTOPCOMPONENT y SETBOTTOMCOMPONENT en su lugar.
JSPLITPANE utiliza el tamaño mínimo de sus componentes para determinar hasta
dónde puede el usuario mover el divisor. Un JSPLITPANE no permite que el usuario
haga un componente más pequeño que su tamaño mínimo moviendo el divisor. Sin
JJSSpplliittPPaanneeDDeemmoo..jjaavvaa
embargo se pueden utilizar los “botones explandibles” para ocultar un componente,
si se han activado previamente con el método SETONETOUCHEXPANDABLE.
También se puede controlar con programación la posición de la barra divisoria con
los métodos SETDIVIDERLOCATION. El método SETRESIZEWEIGHT permite indicar qué
proporción de tamaño extra se asigna a cada parte cuando se amplia el tamaño
disponible para el JSPLITPANE.
17.4.2.1.3 Trabajo con JTabbedPane
Con la clase JTABBEDPANE, o panel de solapas, se pueden colocar varios
componentes (normalmente objetos JPANEL) compartiendo el mismo espacio. El
usuario puede elegir qué componente ver seleccionando la pestaña del componente
deseado.
Para crear un panel de solapas, simplemente se instancia un JTABBEDPANE, se crean
los componentes que se desean mostrar, y finalmente se añaden al JTABBEDPANE
utilizando el método ADDTAB.
La Figura 17.13 muestra una aplicación que usa JTABBEDPANE.
Figura 17.13. Ejemplo de uso de JTabbedPane
El fragmento de código necesario para crear la aplicación de la Figura 17.13 es el
siguiente.
JJTTaabbbbeeddPPaanneeDDeemmoo..jjaavvaa
ImageIcon icon = new ImageIcon(getClass().getResource("/icons/load.png")); JTabbedPane tabbedPane = new JTabbedPane(); Component text1 = new JTextArea("Contenido de la Solapa 1"); JScrollPane scroll1 = new JScrollPane(text1); tabbedPane.addTab("Uno", icon, scroll1, "No hace nada"); Component text2 = new JTextArea("Contenido de la Solapa 2"); JScrollPane scroll2 = new JScrollPane(text2); tabbedPane.addTab("Dos", icon, scroll2, "Tampoco hace nada"); tabbedPane.setSelectedIndex(0);
17.4.2.1.4 Trabajo con JToolBar
Un objeto JTOOLBAR crea una barra de herramientas para colocar, normalmente
botones, aunque se pueden colocar cualquier tipo de componentes dispuestos en
una fila o una columna. Normalmente las barras de herramientas proporcionan
acceso a funcionalidades que también se encuentran en ítems de menús, siendo
interesante unificar dichas funcionalidades con la API Action, tratada
anteriormente.
La Figura 17.14 muestra una aplicación que contiene una barra de herramientas
sobre un área de texto.
Figura 17.14. Ejemplo de uso de JToolBar
Por defecto, el usuario puede arrastrar la barra de herramientas a un lateral distinto
de su contenedor o fuera de su propia ventana. La Figura 17.15 muestra cómo
aparece la aplicación después de que el usuario haya arrastrado la barra de
herramientas al lateral derecho de su contenedor y fuera de la ventana. Para hacer
que el arrastre de la barra de herramientas funcione correctamente, la barra debe
estar en un contenedor que use BORDERLAYOUT (ver apartado de controladores de
distribución), y el contenedor sólo debe tener otro componente que esté situado en
el centro.
Figura 17.15. Diferentes ubicaciones de JToolBar
El siguiente código implementa la barra de herramientas.
public JToolBarDemo() { ... JToolBar toolBar = new JToolBar(); addButtons(toolBar); JPanel contentPane = new JPanel(); contentPane.setLayout(new BorderLayout()); contentPane.add(toolBar, BorderLayout.NORTH); ... } protected void addButtons(JToolBar toolBar) { JButton button = null; // primer boton button = new JButton(new ImageIcon("icons/left.png ")); ... toolBar.add(button); // segundo boton button = new JButton(new ImageIcon("icons/center.png")); ... toolBar.add(button); // tercer boton button = new JButton(new ImageIcon("icons/right.png")); ... toolBar.add(button); }
Existen además otros métodos interesantes que proporciona JTOOLBAR. El método
SETFLOATABLE permite establecer si la barra se puede o no desplazar a otras
JJTToooollBBaarrDDeemmoo..jjaavvaa
ubicaciones. El método ADDSEPARATOR añade un hueco entre el último componente
añadido y los sucesivos que se vayan añadiendo.
Como ya se ha comentado, JTOOLBAR permite la colocación de todo tipo de
componentes tal y como se muestra en la Figura 17.16. Es preciso destacar que
para que los componentes queden alineados verticalmente al centro, se debe
utilizar el método SETALIGNMENTY de cada componente.
Figura 17.16. Diferentes componentes en una JToolBar
El siguiente fragmento de código muestra cómo se han ido añadiendo los
componentes hasta obtener la Figura 17.16.
button1.setAlignmentY(CENTER_ALIGNMENT); button2.setAlignmentY(CENTER_ALIGNMENT); button3.setAlignmentY(CENTER_ALIGNMENT); button4 = new JButton("Another button"); button4.setAlignmentY(CENTER_ALIGNMENT); toolBar.add(button1); toolBar.add(button2); toolBar.add(button3); toolBar.addSeparator(); toolBar.add(button4); //fifth component is NOT a button! JTextField textField = new JTextField("A text field");... textField.setAlignmentY(CENTER_ALIGNMENT); toolBar.add(textField);
17.4.2.1.5 Trabajo con JInternalFrame y JDesktopFrame
Con la clase JINTERNALFRAME, se puede mostrar un frame como una ventana dentro
de otra ventana. Para crear un frame interno que parezca un diálogo sencillo, se
pueden utilizar los métodos SHOWINTERNALXXXDIALOG de JOPTIONPANE, como se
JJTToooollBBaarrDDeemmoo22..jjaavvaa
explicó anteriormente. Con JINTERNALFRAME se pueden crear las denominadas
aplicaciones MDI (Multiple Document Interface).
Normalmente, los frames internos se muestran dentro de un JDESKTOPPANE que es
una subclase de JLAYEREDPANE al que se le ha añadido el API para manejar el
solapamiento de múltiples frames internos.
La Figura 17.17 muestra una aplicación que tiene dos frames internos dentro de un
frame normal.
Figura 17.17. Dos JInternalFrame dentro de un JFrame
El siguiente fragmento muestra cómo se trabaja con JINTERNALFRAME para conseguir
la aplicación de la Figura 17.17.
class FrameInterno extends JInternalFrame{ static int openFrameCount = 0; static final int xOffset = 30, yOffset = 30; public FrameInterno() { super("Internal Frame #" + (++openFrameCount), true, //resizable true, //closable true, //maximizable true);//iconifiable // Introducir componentes en el internal frame como si fuera un frame normal...
JJIInntteerrnnaallFFrraammeeDDeemmoo..jjaavvaa
// Establecer la posicion del frame setLocation(xOffset*openFrameCount, yOffset*openFrameCount); setSize(200,200); } } class JInternalFrameDemo extends JFrame{ JDesktopPane desktop; public JInternalFrameDemo(){ super("JIntenalFrameDemo"); desktop = new JDesktopPane(); //a specialized layered pane createFrame(); // Primera ventana createFrame(); // Otra mas setContentPane(desktop); desktop.setPreferredSize(new Dimension(300,300)); } protected void createFrame() { FrameInterno frame = new FrameInterno(); frame.setVisible(true); desktop.add(frame); try { frame.setSelected(true); } catch (java.beans.PropertyVetoException e2) {} } ... }
17.4.3 Componentes ligeros
17.4.3.1 JLabel
JLABEL permite mostrar texto no seleccionable e imágenes. Es útil cuando se
necesita crear un componente que muestre un texto sencillo o una imagen. La
Figura 17.18 muestra el aspecto que tendrían tres etiquetas, la primera con texto e
icono, la segunda con sólo texto y una tercera que sólo es el icono.
Figura 17.18. Ejemplo de JLabel
El siguiente fragmento de código muestra cómo se podría obtener la aplicación de
la Figura 17.18.
ImageIcon icon = new ImageIcon("icons/load.png"); JLabel label1 = new JLabel("Etiqueta con texto e icono", icon, JLabel.CENTER); //Posicion del texto label1.setVerticalTextPosition(JLabel.BOTTOM);
JJLLaabbeellDDeemmoo..jjaavvaa
label1.setHorizontalTextPosition(JLabel.CENTER); JLabel label2 = new JLabel("Etiqueta con solo texto"); JLabel label3 = new JLabel(icon); JPanel panel = new JPanel(); //Añadir las etiquetas al JPanel. panel.add(label1); panel.add(label2); panel.add(label3);
17.4.3.2 JButton
Para crear un botón en Swing, se debe instanciar una de las muchas subclases de la
clase ABSTRACTBUTTON. Esta sección explica el API básico que define la clase
ABSTRACTBUTTON y lo que todos los botones Swing tienen en común. Como la clase
JBUTTON desciende de ABSTRACTBUTTON este apartado muestra a nivel general cómo
funcionan los botones.
La Tabla 17.5 muestra las subclases de ABSTRACTBUTTON definidas en Swing.
Tabla 17.5. Tipos de AbstractButton
Clase Descripción JBUTTON Un botón común. JCHECKBOX Un check box. JRADIOBUTTON Un bóton de radio, normalmente, dentro de un grupo: RADIOGROUP. JMENUITEM Un ítem de menú. JTOGGLEBUTTON Un botón común, pero con estado de selección.
La Figura 17.19 muestra el aspecto de una aplicación con tres botones. Se incluyen
iconos en los botones con el texto colocado en sitios diferentes.
Figura 17.19. Ejemplo de JButton
El siguiente fragmento de código muestra cómo se obtiene la aplicación de la
Figura 17.19.
ImageIcon leftButtonIcon = new ImageIcon("icons/load.png");
JJBBuuttttoonnDDeemmoo..jjaavvaa
ImageIcon middleButtonIcon = new ImageIcon("icons/apply_small.png"); ImageIcon LEFTButtonIcon = new ImageIcon("icons/next.png"); final JButton b1 = new JButton("Desactivar boton del medio", leftButtonIcon); b1.setVerticalTextPosition(AbstractButton.CENTER); b1.setHorizontalTextPosition(AbstractButton.LEFT); b1.setMnemonic('d'); final JButton b2 = new JButton("Boton del medio", middleButtonIcon); b2.setVerticalTextPosition(AbstractButton.BOTTOM); b2.setHorizontalTextPosition(AbstractButton.CENTER); b2.setMnemonic('m'); final JButton b3 = new JButton("Habilitar el boton del medio", LEFTButtonIcon); //Posicion por defecto CENTER, LEFT. b3.setMnemonic('e'); b3.setEnabled(false); //Listen for actions on buttons 1 and 3. b1.addActionListener(new ActionListener(){ public void actionPerformed(java.awt.event.ActionEvent e) { b2.setEnabled(false); b1.setEnabled(false); b3.setEnabled(true); } }); b3.addActionListener(new ActionListener(){ public void actionPerformed(java.awt.event.ActionEvent e) { b2.setEnabled(true); b1.setEnabled(true); b3.setEnabled(false); } });
El modo de implementar el manejo de eventos depende del tipo de botón utilizado
y de cómo se utiliza. Generalmente, se implementa un ACTIONLISTENER, que es
notificado cada vez que el usuario pulsa el botón, Para un JCHECKBOX normalmente
se utiliza un ITEMLISTENER, que es notificado cuando el JCHECKBOX es seleccionado o
deseleccionado.
17.4.3.3 JCheckBox
JCHECKBOX permite la creación de botones tipo check box. Swing también soporta
check boxes en menús, utilizando la clase JCHECKBOXMENUITEM (se ve en el apartado
de menús). Como JCHECKBOX y JCHECKBOXMENUITEM descienden de ABSTRACTBUTTON,
los check boxes tienen todas las características de un botón normal como se
explicó en apartado anterior. Por ejemplo, podemos especificar imágenes para ser
utilizadas en los checkboxes.
Además tienen un estado de selección que cambia cada vez que el usuario pulsa el
botón. Mediante programación se puede seleccionar con SETSELECTED y consultar
cuando se desee con ISSELECTED.
Los check boxes son similares a los botones de radio (se ve en apartado siguiente),
pero su modelo de selección es diferente, por convención. Cualquier número de
check boxes en un grupo (BUTTONGROUP) permite que ninguno, alguno o todos
puedan ser seleccionados. Por otro lado, en un grupo de botones de radio, sólo
puede haber uno seleccionado.
La Figura 17.20 muestra una aplicación que utiliza JCHECKBOX para simular un
pequeño carrito de compra. Ilustra cómo se pueden capturar el evento de selección
de los check boxes, además de acceder al estado de los mismos para informar al
usuario de qué ítems tiene seleccionados.
Figura 17.20. Ejemplo de JCheckBox
El siguiente fragmento de código muestra cómo se puede obtener la aplicación de
Figura 17.20.
patatasCheck = new JCheckBox("Patatas"); patatasCheck.setMnemonic('P'); patatasCheck.setSelected(true); lechugaCheck = new JCheckBox("Lechuga"); lechugaCheck.setMnemonic('L'); lechugaCheck.setSelected(false); tomatesCheck = new JCheckBox("Tomates"); tomatesCheck.setMnemonic('T'); tomatesCheck.setSelected(false); MiListener listener = new MiListener(); patatasCheck.addItemListener(listener); lechugaCheck.addItemListener(listener); tomatesCheck.addItemListener(listener); ... class MiListener implements ItemListener { public void itemStateChanged(ItemEvent e){ String mensaje = "";
JJCChheecckkBBooxxDDeemmoo..jjaavvaa
if (e.getStateChange() == e.SELECTED){ mensaje = "Has añadido: " + ((JCheckBox)e.getItemSelectable()).getText(); }else{ mensaje = "Has eliminado: " + ((JCheckBox)e.getItemSelectable()).getText(); } mensaje +="\n\nCesta actual: "; if (patatasCheck.isSelected()){ mensaje+="\n"+patatasCheck.getText(); } if (lechugaCheck.isSelected()){ mensaje+="\n"+lechugaCheck.getText(); } if (tomatesCheck.isSelected()){ mensaje+="\n"+tomatesCheck.getText(); } JOptionPane.showMessageDialog(JCheckBoxDemo.this, mensaje); //JCheckBoxDemo.this es una referencia al JFrame } };
17.4.3.4 JRadioButton
Los botones de radio suelen aparecer en un grupo que, por convención, sólo uno de
ellos puede estar seleccionado. Swing soporta botones de radio con las clases
JRADIOBUTTON y BUTTONGROUP. Para poner un botón de radio en un menú, se utiliza la
clase JRADIOBUTTONMENUITEM. Otras formas de presentar una entre varias opciones
son los combo boxes y las listas. Los botones de radio tienen un aspecto similar a
los check boxes, pero, por convención, los check boxes no tienen límites sobre
cuantos ítems pueden estar seleccionados a la vez.
BUTTONGROUP es quien se encarga de que cuando un botón de radio se selecciona,
automáticamente se deseleccionen los restantes del mismo grupo.
Como JRADIOBUTTON desciende de ABSTRACTBUTTON, los botones de radio Swing
tienen todas las características de los botones normales vistos anteriormente. Por
ejemplo, se puede especificar la imagen mostrada por un botón de radio.
La Figura 17.21 muestra una aplicación que utiliza JRADIOBUTTON dentro de un
BUTTONGROUP.
Figura 17.21. Ejemplo de JRadioButton
El siguiente fragmento de código muestra cómo se puede obtener la aplicación de
la Figura 17.21. En el caso de los botones de radio el oyente más utilizado es el
ACTIONLISTENER, igual que para los botones normales (JBUTTON).
patatasRadio = new JRadioButton("Patatas"); patatasRadio.setMnemonic('P'); patatasRadio.setSelected(true); lechugaRadio = new JRadioButton("Lechuga"); lechugaRadio.setMnemonic('L'); lechugaRadio.setSelected(true); tomatesRadio = new JRadioButton("Tomates"); tomatesRadio.setMnemonic('T'); tomatesRadio.setSelected(true); //Meterlos en un grupo que permite la seleccion unica ButtonGroup grupo = new ButtonGroup(); grupo.add(patatasRadio); grupo.add(lechugaRadio); grupo.add(tomatesRadio); MiListener listener = new MiListener(); patatasRadio.addActionListener(listener); lechugaRadio.addActionListener(listener); tomatesRadio.addActionListener(listener); ... class MiListener implements ActionListener { public void actionPerformed(ActionEvent e){ JOptionPane.showMessageDialog(JRadioButtonDemo.this, "Has seleccionado "+((JRadioButton)e.getSource()).getText()); //JRadioButtonDemo.this es una referencia al JFrame } };
JJRRaaddiiooBBuuttttoonnDDeemmoo..jjaavvaa
17.4.3.5 JComboBox
Un JCOMBOBOX permite crear una lista desplegable y un campo de texto. El usuario
puede teclear un valor o elegirlo desde una lista. Un JCOMBOBOX editable ahorra
tiempo de entrada proporcionando atajos para los valores más comúnmente
introducidos. Un JCOMBOBOX no editable desactiva el tecleo pero aún así permite al
usuario seleccionar un valor desde una lista. Esto proporciona un espacio
alternativo a un grupo de botones de radio o una lista.
A un JCOMBOBOX se le puede añadir cualquier objeto, mediante ADDITEM, que será
mostrado en forma de cadena de texto obtenida a partir de su método TOSTRING. Esto
es muy útil para tener referenciados en el propio combo objetos más complejos del
dominio de la aplicación que se esté manejando, de forma transparente. El objeto
seleccionado en cada momento en el combo se obtiene con el método
GETSELECTEDITEM.
El modo de capturar el evento en un JCOMBOBOX suele ser con el oyente
ACTIONLISTENER de forma que cada vez que el usuario modifica el elemento
seleccionado del combo se produce el evento. También se podrían capturar con
ITEMLISTENER de forma que se lanza el evento cuando un elemento del combo se
selecciona o ha dejado de ser seleccionado, por lo que cuando el usuario cambia el
elemento seleccionado se producen dos eventos: uno por el que se selecciona y
otro por el que deja de estar seleccionado.
La Figura 17.22 muestra una aplicación que utiliza JCOMBOBOX. Cada vez que se
modifica la selección del combo, se muestra un mensaje con el texto del elemento
que se acaba de seleccionar.
Figura 17.22. Ejemplo de JComboBox
El siguiente fragmento de código muestra cómo se podría obtener la aplicación de
la Figura 17.22.
JComboBox box = new JComboBox(); box.addItem("Patatas"); box.addItem("Lechuga"); box.addItem("Tomates"); box.setEditable(true); box.addActionListener(new ActionListener(){ public void actionPerformed(ActionEvent e){ JComboBox box = (JComboBox) e.getSource(); JOptionPane.showMessageDialog(JComboBoxDemo.this, "Has seleccionado "+box.getSelectedItem()); } });
17.4.3.6 JList
Un JLIST le presenta al usuario un grupo de ítems para elegir. Los ítems pueden ser
cualquier OBJECT. Una lista puede tener muchos ítems, o podría crecer hasta
tenerlos, por eso suelen situarse dentro de paneles desplazables (JSCROLLPANE).
Para crear un JLIST se puede utilizar uno de sus constructores que reciben como
parámetro los elementos de la lista, bien mediante un VECTOR, un array de OBJECT o
un LISTMODEL. Sea cual sea el constructor que se use, siempre se crea un LISTMODEL
para la lista, que siempre podrá ser accedido mediante el método GETMODEL. Para
obtener un elemento concreto de la lista se usa LISTMODEL. Si además se desea
modificar la lista una vez creada, se puede usar como modelo DEFAULTLISTMODEL
para construir la lista y luego añadir o eliminar elementos sobre el modelo.
Lo habitual es acceder a los elementos seleccionados de la lista. Para ello se
proporcionan los métodos GETSELECTEDINDEX o GETSELECTEDINDICES para obtener la o
las posiciones de los elementos seleccionados actualmente; y GETSELECTEDVALUE y
GETSELECTEDVALUES para obtener el o los elementos seleccionados.
El usuario puede seleccionar elementos de la lista. Para ello existen varios modos
de selección configurables mediante el método SETSELECTIONMODE. Las opciones
son:
Modo Descripción SINGLE_SELECTION Sólo un ítem de la lista puede ser seleccionado. Cuando el
usuario selecciona un ítem, cualquier ítem anteriormente seleccionado se deselecciona primero.
JJCCoommbbooBBooxxDDeemmoo..jjaavvaa
SINGLE_INTERVAL_SELECTION Se puede seleccionar varios ítems contiguos. Cuando el usuario empieza una nueva selección, cualquier ítem anteriormente seleccionado se deselecciona primero.
MULTIPLE_INTERVAL_SELECTION El valor por defecto. Se pueden seleccionar cualquier combinación de ítems. El usuario debe deseleccionar explícitamente los ítems.
Es posible capturar el evento del cambio de selección mediante la implementación
de un oyente LISTSELECTIONLISTENER.
La Figura 17.23 muestra una aplicación que utiliza una JLIST modificable.
Figura 17.23. Ejemplo de JList
El siguiente fragmento de código muestra cómo se puede obtener la aplicación de
la Figura 17.23.
// Se utiliza DefaultListModel porque se quiere modificar despues la lista DefaultListModel model = new DefaultListModel(); model.addElement("Item 1"); model.addElement("Item 2"); model.addElement("Item 3"); // Creacion de la lista final JList lista = new JList(model); lista.setSelectionMode(ListSelectionModel.SINGLE_INTERVAL_SELECTION); // Boton de añadir JButton botonAñadir = new JButton("Añadir uno mas"); botonAñadir.addActionListener(new ActionListener(){ public void actionPerformed(ActionEvent e){ DefaultListModel model = (DefaultListModel) lista.getModel(); model.addElement("Otro mas"); } }); //Etiquetas para mostrar datos
JJLLiissttDDeemmoo..jjaavvaa
final JLabel cantidadSeleccionada = new JLabel("Tienes seleccionados 0 elementos"); final JLabel unElemento = new JLabel(" "); // Capturar eventos cuando se modifica la seleccion en la lista lista.addListSelectionListener(new ListSelectionListener(){ public void valueChanged(ListSelectionEvent e){ if (e.getValueIsAdjusting()){ return; }else{ // Actualizar las etiquetas cantidadSeleccionada.setText("Tienes seleccionados "+lista.getSelectedIndices().length+" elementos"); if (lista.getSelectedIndices().length==1){ unElemento.setText("El elemento seleccionado es: "+lista.getSelectedValue().toString()); }else{ unElemento.setText(" "); } } } });
17.4.3.7 Construcción de Menús
Un menú proporciona una forma de ahorrar espacio y permitir al usuario elegir una
entre varias opciones. Otros componentes con los que el usuario puede hacer una
elección incluyen combo boxes, listas, botones de radio y barras de herramientas.
Si alguno de los ítems de un menú realiza una acción que está duplicada en otro
ítem de menú o en un botón de una barra de herramientas, sería interesante utilizar
la API Action que se trata más adelante.
Los menús son únicos en que, por convención, no se sitúan con los otros
componentes en el UI. En su lugar, aparecen en una barra de menú o en un menú
contextual. Una barra de menú contiene uno o más menús, y tiene una posición
dependiente de la plataforma (normalmente debajo de la parte superior de la
ventana). Un menú contextual es un menú que es invisible hasta que el usuario
realiza una acción del ratón específica de la plataforma, como pulsar el botón
derecho del ratón sobre un componente. Entonces el menú contextual aparece bajo
el cursor.
Las clases que permiten la creación de menús son:
• JMENUBAR. Se utiliza para que los menús aparezcan en una barra de menús
en un JFRAME.
• JMENU. Agrupa a un conjunto de ítems de menú. Se utiliza tanto para los
botones directos en la barra de menús, como para los menús internos que
contengan submenús.
• JMENUITEM, JCHECKBOXMENUITEM, JRADIOBUTTONMENUITEM. Son los elementos
hoja de los menús.
• JPOPUPMENU. Se utiliza para crear menús contextuales. Al igual que JMENU
se le añaden recursivamente JMENUITEM y JMENU, sin embargo proporciona
el método SHOW que lo muestra en una posición cualquiera del JFRAME.
La Figura 17.24 muestra los componentes Swing que implementan cada parte de
los menús de una aplicación.
Figura 17.24. Ejemplo de menús
El siguiente fragmento de código muestra cómo se puede obtener la aplicación de
la Figura 17.24.
JMenuBar menuBar = new JMenuBar(); JMenu menu = new JMenu("Un Menu"); menuBar.add(menu); JMenuItem menuItem = new JMenuItem("Un Item con Solo Texto"); menu.add(menuItem); menuItem = new JMenuItem("Both text and icon", new ImageIcon(JFrame.class.getResource("/icons/load.png"))); menu.add(menuItem); menuItem = new JMenuItem(new ImageIcon(JFrame.class.getResource("/icons/load.png"))); menu.add(menuItem); //un grupo de radio buttons
JJMMeennuuDDeemmoo..jjaavvaa
menu.addSeparator(); ButtonGroup group = new ButtonGroup(); JMenuItem rbMenuItem = new JRadioButtonMenuItem("Un Item con radio button"); rbMenuItem.setSelected(true); group.add(rbMenuItem); menu.add(rbMenuItem); rbMenuItem = new JRadioButtonMenuItem("Otro"); group.add(rbMenuItem); menu.add(rbMenuItem); //un grupo de check boxes menu.addSeparator(); JMenuItem cbMenuItem = new JCheckBoxMenuItem("Un Item con check box"); menu.add(cbMenuItem); cbMenuItem = new JCheckBoxMenuItem("Otro"); menu.add(cbMenuItem); //submenu menu.addSeparator(); JMenu submenu = new JMenu("Un Submenu"); menuItem = new JMenuItem("Un Item en un submenu"); submenu.add(menuItem); menuItem = new JMenuItem("Otro Item"); submenu.add(menuItem); menu.add(submenu); //Build second menu in the menu bar. menu = new JMenu("Otro Menu"); menuBar.add(menu); ... frame.setJMenuBar(menuBar);
Como se puede ver en el código anterior, para configurar una barra de menús para
un JFRAME, se utiliza el método SETJMENUBAR. Para añadir un JMENU a un JMENUBAR,
se utiliza el método ADD(JMENU). Para añadir ítems de menú y submenús a un JMENU,
se utiliza el método ADD(JMENUITEM).
17.4.3.7.1 Manejar Eventos desde los menús
El manejo de eventos en menús es muy similar al de los distintos tipos de botones
y check boxes. Para detectar cuando el usuario selecciona un JMENUITEM, se puede
escuchar por eventos Action (ACTIONEVENT) (igual que se haría para un JBUTTON).
Para detectar cuando el usuario selecciona un JRADIOBUTTONMENUITEM, se puede
escuchar tanto por eventos Action, como por eventos item (ITEMEVENT). Para
JCHECKBOXMENUITEMS, generalmente se escuchan también eventos item.
17.4.3.7.2 Crear un menú contextual
Para traer un menú contextual se usa JPOPUPMENU. El siguiente fragmento de código
muestra cómo se construye un menú contextual.
JPopupMenu popup = new JPopupMenu(); JMenuItem menuItem = new JMenuItem("Un elemento de menu contextual"); popup.add(menuItem); menuItem = new JMenuItem("Otro elemento de menu contextual”); menuItem.addActionListener(this);
popup.add(menuItem); MouseListener popupListener = new PopupListener(); componente.addMouseListener(popupListener); //componente es cualquier componente
Como se puede ver en el código anterior, se debe registrar un oyente de ratón
(MOUSELISTENER) para cada componente al que debería estar asociado el menú
contextual.
El oyente debe detectar las peticiones del usuario para que aparezca el menú
contextual, es decir, detectar si el botón del ratón pulsado es el que, por convenio
en la plataforma es que el que debe desplegar un menú contextual. Por ejemplo en
Windows se usa el botón derecho, sin embargo en Mac OSX se utiliza una
combinación de teclas y el botón del ratón. Es por ello por lo que existe el método
de MOUSEEVENT denominado ISPOPUPTRIGGER. Para visualizar un JPOPUPMENU se utiliza
su método SHOW.
El siguiente fragmento de código muestra cómo se podría codificar el oyente de
ratón desde donde se visualiza el menú contextual.
class PopupListener extends MouseAdapter { public void mousePressed(MouseEvent e) { maybeShowPopup(e); } public void mouseReleased(MouseEvent e) { maybeShowPopup(e); } private void maybeShowPopup(MouseEvent e) { if (e.isPopupTrigger()) { popup.show(e.getComponent(), e.getX(), e.getY()); } } }
17.4.3.8 JTable
JTABLE permite mostrar tablas de datos, y opcionalmente permitir que el usuario los
edite. JTABLE no contiene ni almacena datos; simplemente es una vista de los datos.
La Figura 17.25 muestra una tabla típica mostrada en un JSCROLLPANE.
Figura 17.25. Ejemplo de JTable
El siguiente fragmento de código es suficiente para mostrar la aplicación de la
Figura 17.25:
String[][] data = { {"Maria", "Garcia", "Snowboarding", new Integer(5), new Boolean(false)}, {"Alicia", "Gomez", "Tenis", new Integer(3), new Boolean(true)}, {"Catalina", "Perez", "Atletismo", new Integer(2), new Boolean(false)}, {"Marcos", "Fernandez", "Futbol", new Integer(20), new Boolean(true)}, {"Angela", "Gonzalez", "Natacion", new Integer(4), new Boolean(false)} }; String[] columnNames = {"Nombre", "Primer Apellido", "Deporte", "# de años", "Vegetariano"}; JTable tabla = new JTable(data, columnNames); this.add(new JScrollPane(tabla));
Para obtener fácilmente el valor de una celda se utiliza el método GETVALUEAT de
JTABLE que recibe como parámetros la fila y la columna.
El ejemplo utiliza uno de los constructores de JTABLE que aceptan datos
directamente.
• JTABLE(OBJECT[][] ROWDATA, OBJECT[] COLUMNNAMES).
• JTABLE(VECTOR ROWDATA, VECTOR COLUMNNAMES).
La ventaja de utilizar uno de estos constructores es que es sencillo. Sin embargo,
estos constructores también tienen desventajas.
• Automáticamente hacen todas las celdas editables.
• Tratan igual a todos los tipos de datos. Por ejemplo, si una columna tiene
datos BOOLEAN, los datos podrían mostrarse mejor con un CHECKBOX en la
tabla. Sin embargo, si especificamos los datos como un argumento array o
vector del constructor de JTABLE, nuestro dato BOOLEAN se mostrará como
un STRING. Se pueden ver estas diferencias en las dos últimas columnas de
los ejemplos anteriores.
• Requieren colocar todos los datos de la tabla en un array o vector, lo que
es inapropiado para algunos datos. Por ejemplo, si estamos instanciando
un conjunto de objetos desde una base de datos, se podrían pedir los
objetos bajo demanda, en vez de copiar todos los valores en un array o un
vector.
Para solucionar esos problemas se debe utilizar la interfaz TABLEMODEL. Un JTABLE
obtiene sus datos siempre de un objeto TABLEMODEL. JTABLE pregunta a este objeto
todo lo que necesita saber para dibujarse: número de filas y columnas, cabecera de
columnas, valor en una posición, tipo de objeto que tiene una columna (por defecto
OBJECT), además de delegar la asignación de nuevos valores a las filas y columnas.
Si no se especifica el TABLEMODEL a usar, JTABLE mantiene uno por defecto. Para
facilitar la implementación de esta interfaz, existe la clase ABSTRACTTABLEMODEL que
es una implementación por defecto de dicha interfaz.
El siguiente fragmento de código muestra la implementación de un TABLEMODEL
propio.
class MyTableModel extends AbstractTableModel { Object[][] data = { {"Maria", "Garcia", "Snowboarding", new Integer(5), new Boolean(false)}, {"Alicia", "Gomez", "Tenis", new Integer(3), new Boolean(true)}, {"Catalina", "Perez", "Atletismo", new Integer(2), new Boolean(false)}, {"Marcos", "Fernandez", "Futbol", new Integer(20), new Boolean(true)}, {"Angela", "Gonzalez", "Natacion", new Integer(4), new Boolean(false)} }; String[] columnNames = {"Nombre", "Primer Apellido", "Deporte", "# de años", ”Vegetariano"}; public int getColumnCount() { return columnNames.length; } public int getRowCount() { return data.length; } public String getColumnName(int col) { return columnNames[col]; } public Object getValueAt(int row, int col) { return data[row][col]; } public Class getColumnClass(int c) {
JJTTaabblleeDDeemmoo..jjaavvaa
return getValueAt(0, c).getClass(); } public boolean isCellEditable(int row, int col) { if (col < 2) { return false; } else { return true; } } public void setValueAt(Object value, int row, int col) { data[row][col]=value; } }
El anterior fragmento de código daría como resultado la tabla que se muestra en la
Figura 17.26.
Figura 17.26. Ejemplo de JTable con JTableModel
Como se puede observar en la Figura 17.26, gracias a la información más detallada
que proporciona el TABLEMODEL, sobre todo por el método GETCOLUMNCLASS, el JTABLE
es capaz de mostrar la información más ajustada: los valores enteros se muestran
alineados a la derecha, y los booleanos se muestran con un check box.
La Tabla 17.6 muestra el mapeo que existe entre la clase que devuelve
GETCOLUMNCLASS y una renderización concreta en función de la misma.
Tabla 17.6. Renderizaciones en función de los valores devueltos por getColumnClass
Clase devuelta por getColumnClass
Cómo se renderiza
BOOLEAN Mediante un check box. NUMBER Mediante una etiqueta de texto alineada a la derecha. DOUBLE, FLOAT Igual que con NUMBER, pero la transformación del objeto
numérico a una cadena de texto se hace mediante NUMBERFORMAT que respeta además el locale.
DATE Se renderiza por una etiqueta de texto con la transformación del objeto fecha a una cadena de texto mediate DATEFORMAT.
IMAGEICON, ICON Mediante una etiqueta con icono centrada. OBJECT Mediante una etiqueta de texto que muestra el TOSTRING del
objeto.
17.4.3.9 Componentes de texto
Swing proporciona un conjunto de componentes para el manejo de texto. Van
desde componentes sencillos, para introducir pequeñas cantidades de información,
hasta componentes complejos capaces de dar estilo al texto, visualizar imágenes,
componentes o incluso HTML. Por orden de complejidad, los componentes de
texto principales en Swing son: JTEXTFIELD, JTEXTAREA y JTEXTPANE.
17.4.3.9.1 JTextField
Un JTEXTFIELD es un componente básico que permite al usuario teclear una pequeña
cantidad de texto y dispara un evento ACTION cuando el usuario indique que la
entrada de texto se ha completado (normalmente pulsando Return). Si se necesita
proporcionar un campo para una contraseña, es decir, que no muestre los caracteres
tecleados por el usuario se puede utilizar la clase JPASSWORDFIELD.
El siguiente fragmento de código muestra un ejemplo de uso de este componente
que además responde a la pulsación de la tecla Return.
final JTextField textField = new JTextField(20); //maximo 20 caracteres textField.addActionListener(new ActionListener(){ public void actionPerformed(ActionEvent evt) { String text = textField.getText(); JOptionPane.showMessageDialog(this, “Has escrito ”+text); } }); this.add(textField);
La Figura 17.27 muestra el aspecto que tendría la aplicación que utiliza JTEXTFIELD
en el momento de pulsar Return.
JJTTeexxttFFiieellddDDeemmoo..jjaavvaa
Figura 17.27. Ejemplo de JTextField
17.4.3.9.2 JTextArea
Un JTEXTAREA es un componente que muestra un área de texto muestra múltiples
líneas de texto y permite que el usuario edite el texto con el teclado y el ratón. El
texto sólo puede tener una fuente.
La Figura 17.28 muestra el aspecto que tendría un JTEXTAREA (utilizando un
JSCROLLPANE para desplazarla en caso de que haya más texto del que se puede
visualizar).
Figura 17.28. Ejemplo de JTextArea
El siguiente fragmento de código muestra cómo se ha obtenido la aplicación de la
Figura 17.28.
JTextArea textArea = new JTextArea(5, 10); // 5 filas y 10 columnas textArea.setFont(new Font("Serif", Font.ITALIC, 16)); textArea.setText( "Esto es un JTextArea editable " + "iniciada con el método setText. " + "Un area de texto es un componente de texto \"plano\", " + "lo que significa que aunque puede mostrar texto " + "en cualquier fuente, todo el texto debe estar en la misma fuente." ); textArea.setLineWrap(true); textArea.setWrapStyleWord(true); JScrollPane areaScrollPane = new JScrollPane(textArea); areaScrollPane.setVerticalScrollBarPolicy(JScrollPane.VERTICAL_SCROLLBAR_ALWAYS); areaScrollPane.setPreferredSize(new Dimension(250, 250));
En el ejemplo anterior, se establece la fuente del área de texto y, tal y como dice el
ejemplo, únicamente se puede usar una fuente para todo el área.
Las dos siguientes sentencias tratan de la ruptura de líneas. Primero llama a
SETLINEWRAP, que activa la ruptura de líneas. Por defecto, un área de texto no rompe
las líneas. En su lugar muestra el texto en una sola línea y si el área de texto está
dentro de un panel de desplazamiento, se permite a sí mismo desplazarse
horizontalmente. La segunda es una llamada a SETWRAPSTYLEWORD, que indica que
se rompan las líneas entre palabras.
17.4.3.9.3 JTextPane
JTEXTPANE permite la creación de áreas de texto muy personalizables que permiten
visualizar texto con formato avanzado. La propia explicación de este componente
sería muy amplia, por lo que se ha decidido mostrar únicamente un ejemplo
ilustrativo de algunas de sus capacidades.
La Figura 17.29 muestra un ejemplo de una aplicación que usa un JTEXTPANE para
mostrar un texto con diferentes estilos.
JJTTeexxttAArreeaaDDeemmoo..jjaavvaa
Figura 17.29. Ejemplo de JTextPane
El siguiente fragmento de código muestra cómo se puede obtener la aplicación de
la Figura 17.29. Básicamente, se introduce el texto inicial en un array y se crean y
codifican varios estilos (objetos que representan diferentes formatos de párrafos y
caracteres) en otro array. Posteriormente, el código ejecuta un bucle por el array,
inserta el texto en el JTEXTPANE, y especifica el estilo a utilizar para el texto
insertado.
// Crear el Text Pane JTextPane textPane = createTextPane(); JScrollPane paneScrollPane = new JScrollPane(textPane); ... private JTextPane createTextPane() { JTextPane textPane = new JTextPane(); String[] initString = { "This is an editable JTextPane, ", "another ", "styled ", "text ", "component, ", "which supports embedded icons..." + newline, " " + newline, "...and embedded components..." + newline, " " + newline + newline, "JTextPane is a subclass of JEditorPane that ", "uses a StyledEditorKit and StyledDocument, and provides ", "cover methods for interacting with those objects." }; String[] initStyles = { "regular", "italic", "bold", "small", "large", "regular", "icon", "regular", "button", "regular", "regular", "regular" }; initStylesForTextPane(textPane); Document doc = textPane.getDocument(); try {
JJTTeexxttPPaanneeDDeemmoo..jjaavvaa
for (int i=0; i < initString.length; i++) { textPane.setCaretPosition(doc.getLength()); doc.insertString(doc.getLength(), initString[i], textPane.getStyle(initStyles[i])); textPane.setLogicalStyle(textPane.getStyle(initStyles[i])); } } catch (BadLocationException ble) { System.err.println("Couldn't insert initial text."); } return textPane; } protected void initStylesForTextPane(JTextPane textPane) { //Initialize some styles Style def = StyleContext.getDefaultStyleContext(). getStyle(StyleContext.DEFAULT_STYLE); Style regular = textPane.addStyle("regular", def); StyleConstants.setFontSize(def, 14); StyleConstants.setFontFamily(def, "SansSerif"); Style s = textPane.addStyle("italic", regular); StyleConstants.setItalic(s, true); s = textPane.addStyle("bold", regular); StyleConstants.setBold(s, true); s = textPane.addStyle("small", regular); StyleConstants.setFontSize(s, 10); s = textPane.addStyle("large", regular); StyleConstants.setFontSize(s, 16); s = textPane.addStyle("icon", regular); StyleConstants.setAlignment(s, StyleConstants.ALIGN_CENTER); StyleConstants.setIcon(s, new ImageIcon("images/Pig.gif")); s = textPane.addStyle("button", regular); StyleConstants.setAlignment(s, StyleConstants.ALIGN_CENTER); JButton button = new JButton(new ImageIcon("images/sound.gif")); button.setMargin(new Insets(0,0,0,0)); button.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent e) { Toolkit.getDefaultToolkit().beep(); } }); StyleConstants.setComponent(s, button); }
17.4.3.10 JTree
Con la clase JTREE, se puede mostrar un árbol de datos. JTREE realmente no contiene
datos, simplemente es un vista de ellos. La Figura 17.30 muestra un ejemplo de un
árbol JTREE.
Figura 17.30. Ejemplo de JTree
Como muestra la figura anterior, JTREE muestra los datos verticalmente. Cada fila
contiene exactamente un ítem de datos o nodo. Cada árbol tiene un nodo raíz
(llamado “Root” en la Figura 17.30, del que descienden todos los nodos). Los
nodos que no pueden tener hijos se llaman nodos hoja (leaf). En el caso de la
Figura 17.17, el look and feel marca los nodos hojas con un círculo y a los nodos
intermedios con una carpeta. Dichos nodos intermedios pueden tener cualquier
número de hijos, o incluso no tenerlos. Normalmente el usuario puede expandir y
contraer los nodos que no son hojas. Por defecto, los nodos que no son hojas
empiezan contraídos.
Cuando se inicializa un árbol, se crea un ejemplar de TREENODE para cada nodo del
árbol, incluyendo el nodo raíz. Cada nodo que no tenga hijos es una hoja. Para
hacer que un nodo sin hijos no sea una hoja, se llama al método
SETALLOWSCHILDREN(TRUE) sobre él.
Normalmente los JTREE suelen incluirse dentro de paneles de desplazamiento
(JSCROLLPANE) tal y como se ha hecho en la aplicación que muestra la Figura 17.31,
la cual implementa un pequeño catálogo de libros.
Figura 17.31. Aplicación que integra JTree para mostrar un catálogo de libros
El código para obtener la aplicación de la Figura 17.31 se muestra en los siguientes
tres fragmentos de código. El primer fragmento, mostrado a continuación, ilustra
cómo se inició el árbol insertando sus elementos.
DefaultMutableTreeNode raiz = new DefaultMutableTreeNode(“The Java Series”); DefaultMutableTreeNode category = null; DefaultMutableTreeNode book = null; category = new DefaultMutableTreeNode("Books for Java Programmers"); raiz.add(category); //Tutorial book = new DefaultMutableTreeNode(new BookInfo("The Java Tutorial: Object-Oriented Programming for the Internet", "tutorial.html")); category.add(book); ... category = new DefaultMutableTreeNode("Books for Java Implementers"); raiz.add(category); //VM book = new DefaultMutableTreeNode(new BookInfo("The Java Virtual Machine Specification", "vm.html")); category.add(book); //Language Spec book = new DefaultMutableTreeNode(new BookInfo("The Java Language Specification", "jls.html")); category.add(book); // Crear el arbol JTree tree = new JTree(raiz);
Como se puede ver en el ejemplo, todos los nodos, incluyendo la raíz son
instancias de DEFAULTMUTABLETREENODE, la cual implementa la interfaz TREENODE y
suele ser suficiente para cualquier árbol. A dicha clase se le pueden ir añadiendo
hijos recursivamente (instancias de MUTABLETREENODE).
Cuando se crea un DEFAULTMUTABLETREENODE, en el constructor se le puede pasar
cualquier objeto que es el que realmente utiliza el programador para guardar la
información del árbol. En el ejemplo se han guardado tanto cadenas de texto (para
nodos intermedios) como un objeto propio de usuario, denominado BOOKINFO y que
guarda información relativa a un libro. El árbol utilizará el método TOSTRING para
obtener la etiqueta que se mostrará en el árbol, por lo cual suele ser muy útil
redefinirlo.
private class BookInfo { public String bookName; public URL bookURL; public BookInfo(String book, String filename) { bookName = book; } //Esto es lo que se mostrará en el árbol. public String toString() {
JJTTrreeeeDDeemmoo..jjaavvaa
return bookName; } }
Finalmente, se ha añadido un oyente de eventos de selección sobre el árbol
(TREESELECTIONLISTENER) para actuar cuando se pincha en un nodo y mostrar así
información relativa al libro que se ha seleccionado. Para obtener dicho
comportamiento bastaría con escribir el siguiente fragmento de código.
tree.addTreeSelectionListener(new TreeSelectionListener() { public void valueChanged(TreeSelectionEvent e) { // Obtener el nodo de nuevo DefaultMutableTreeNode node = (DefaultMutableTreeNode) (e.getPath().getLastPathComponent()); Object nodeInfo = node.getUserObject(); if (node.isLeaf()) { BookInfo book = (BookInfo)nodeInfo; displayURL(book.bookURL); } else { displayURL(helpURL); } } });
Dicho fragmento obtiene el nodo seleccionado a partir del evento, para finalmente
acceder al objeto de usuario (BOOKINFO) y obtener la URL de donde descargar
información adicional del libro.
17.4.3.10.1 Personalización de la presentación
Un árbol usa un único objeto responsable de renderizar todos los nodos el cual
implementa la interfaz TREECELLRENDERER. Por defecto todos los JTREE tienen como
renderizador un objeto de la clase DEFAULTTREECELLRENDERER. Si se quiere hacer una
personalización simple, como cambiar el icono utilizado para las hojas y para los
nodos intermedios, el color de la fuente, el color de selección, se puede configurar
este propio renderizador, tal y como muestra el siguiente fragmento de código.
DefaultTreeCellRenderer renderer = new DefaultTreeCellRenderer(); renderer.setLeafIcon(new ImageIcon("icons/center.png")); tree.setCellRenderer(renderer);
El resultado sería el mostrado en la Figura 17.32.
Figura 17.32. Personalización de los iconos de un JTree
Para un control total sobre el renderizado habría que implementar directamente la
clase TREECELLRENDERER.
17.4.3.10.2 Modificación dinámica de un JTree
Para insertar o eliminar nodos de un árbol se debe utilizar el objeto
DEFAULTTREEMODEL que representa el modelo de datos del árbol. El siguiente
fragmento de código muestra cómo se puede insertar un nuevo nodo colgando de
otro ya existente en el árbol mediante el método INSERTNODEINTO del modelo.
DefaultMutableTreeNode parentNode = null; TreePath parentPath = tree.getSelectionPath(); //el nodo padre sera el que esta seleccionado // obtener el nodo padre if (parentPath == null) { parentNode = rootNode; } else { parentNode = (DefaultMutableTreeNode) (parentPath.getLastPathComponent()); } DefaultMutableTreeNode childNode = new DefaultMutableTreeNode(“nodo hijo”); DefaultTreeModel model = (DefaultTreeModel) tree.getModel(); treeModel.insertNodeInto(childNode, parentNode, parentNode.getChildCount()); // visualizar el nuevo nodo insertado tree.expandPath(new TreePath(parentNode.getPath())); tree.scrollPathToVisible(new TreePath(childNode.getPath()));
De forma similar, para eliminar un nodo se utilizaría el método
REMOVENODEFROMPARENT también perteneciente a DEFAULTTREEMODEL.
17.5 Control de la distribución con Layouts
Un controlador de distribución (Layout Manager) es un objeto que implementa la
interfaz LAYOUTMANAGER y su objetivo es determinar el tamaño y posición de los
componentes Swing dentro de un contenedor. Aunque los componentes pueden
indicar el tamaño y orientación, el manejador de distribución es el que tiene la
última palabra.
El uso de controladores de distribución es muy útil cuando se usa con ventanas
cuyo tamaño puede ser modificado por el usuario o si la aplicación va a ser
visualizada en diferentes sistemas operativos donde el aspecto puede variar. El uso
de controladores de distribución es un capítulo esencial para que el acabado de la
aplicación tenga un mínimo de seriedad.
Swing proporciona cinco controladores de distribución comúnmente utilizados:
FLOWLAYOUT, BORDERLAYOUT, BOXLAYOUT, GRIDBAGLAYOUT y GRIDLAYOUT.
17.5.1 FlowLayout
FLOWLAYOUT es el controlador de distribución por defecto para todos los paneles.
Simplemente coloca los componentes de izquierda a derecha, empezando una
nueva línea si es necesario. La Figura 17.33 muestra una aplicación que utiliza este
controlador de distribución para colocar cinco botones.
Figura 17.33. Aplicación que usa FlowLayout
El siguiente fragmento de código muestra cómo se ha utilizado FLOWLAYOUT para
obtener la aplicación de la Figura 17.33.
// Constructor del JFrame de la aplicacion ... Container contentPane = getContentPane(); contentPane.setLayout(new FlowLayout()); contentPane.add(new JButton("Button 1")); contentPane.add(new JButton("2")); contentPane.add(new JButton("Button 3"));
FFlloowwLLaayyoouuttDDeemmoo..jjaavvaa
contentPane.add(new JButton("Long-Named Button 4")); contentPane.add(new JButton("Button 5"));
17.5.2 BorderLayout
BORDERLAYOUT es el controlador de distribución por defecto para las ventanas, como
JFRAME y JDIALOG. Utiliza cinco áreas para contener los componentes: north, south,
east, west y center (norte, sur, este, oeste y centro). Cuando se redimensiona la
ventana todo el espacio extra se sitúa en el área central. La Figura 17.34 muestra
una aplicación que utiliza este controlador de distribución para colocar cinco
botones.
Figura 17.34. Aplicación que usa BorderLayout
El siguiente fragmento de código muestra cómo se ha utilizado BORDERLAYOUT para
obtener la aplicación de la Figura 17.34.
//Constructor del JFrame de la aplicacion Container contentPane = getContentPane(); // Por defecto el panel tiene BorderLayout // contentPane.setLayout(new BorderLayout()); //innecesario contentPane.add(new JButton("Button 1 (NORTH)"), BorderLayout.NORTH); contentPane.add(new JButton("2 (CENTER)"), BorderLayout.CENTER); contentPane.add(new JButton("Button 3 (WEST)"), BorderLayout.WEST); contentPane.add(new JButton("Long-Named Button 4 (SOUTH)"), BorderLayout.SOUTH); contentPane.add(new JButton("Button 5 (EAST)"), BorderLayout.EAST);
Se debe tener en cuenta que cuando se está utilizando BORDERLAYOUT, el método ADD
del contenedor necesita un parámetro adicional que indica una de las cinco
localizaciones posibles.
BORDERLAYOUT permite definir el espacio entre las cinco zonas en su constructor:
BORDERLAYOUT(INT HORIZONTALGAP, INT VERTICALGAP).
BBoorrddeerrLLaayyoouuttDDeemmoo..jjaavvaa
17.5.3 BoxLayout
BOXLAYOUT coloca los componentes e una única fila o columna. Respeta las
peticiones de máximo tamaño de los componentes y también permite alinearlos. Se
puede ver a BOXLAYOUT como una versión más potente de FLOWLAYOUT.
La Figura 17.35 muestra una aplicación que utiliza este controlador de
distribución. En la parte superior de la ventana un BOXLAYOUT de arriba-a-abajo
sitúa una etiqueta sobre un panel de desplazable. En la parte inferior, un BOXLAYOUT
de izquierda-a-derecha sitúa dos botones uno junto al otro. Un BORDERLAYOUT
combina las dos partes de la ventana y se asegura que cualquier exceso de espacio
será entregado al panel de scroll.
Figura 17.35. Aplicación que usa dos BoxLayout dentro de un BorderLayout
El código necesario para obtener la ventana de la Figura 17.35 se muestra a
continuación.
... // Etiqueta y scroll dispuestos de arriba-a-abajo. JPanel listPane = new JPanel(); listPane.setLayout(new BoxLayout(listPane, BoxLayout.Y_AXIS)); JLabel label = new JLabel("Area de texto: "); listPane.add(label); listPane.add(Box.createRigidArea(new Dimension(0,5))); JScrollPane listScroller = new JScrollPane(new JTextArea("hola", 20,50)); listPane.add(listScroller);
BBooxxLLaayyoouuttDDeemmoo..jjaavvaa
listPane.setBorder(BorderFactory.createEmptyBorder(10,10,10,10)); // Botones dispuestos de izquierda-a-derecha. JPanel buttonPane = new JPanel(); buttonPane.setLayout(new BoxLayout(buttonPane, BoxLayout.X_AXIS)); buttonPane.setBorder(BorderFactory.createEmptyBorder(0, 10, 10, 10)); buttonPane.add(Box.createHorizontalGlue()); buttonPane.add(new JButton("Cancelar")); buttonPane.add(Box.createRigidArea(new Dimension(10, 0))); buttonPane.add(new JButton("Set")); // Unir todo en el panel de contenido con su BorderLayout. Container contentPane = getContentPane(); contentPane.add(listPane, BorderLayout.CENTER); contentPane.add(buttonPane, BorderLayout.SOUTH);
El ejemplo incorpora el uso de la clase BOX que contiene métodos estáticos para la
creación de componentes invisibles para usar como relleno. Existen tres tipos de
componentes invisibles que se pueden generar y que se muestran en la Tabla 17.7.
Tabla 17.7. Componentes invisibles de relleno con Box
Nombre Descripción Cómo se crean Área Rígida (Rigid) Se usa cuando se quiere un espacio
fijo entre dos componentes. BOX.CREATERIGIDAREA(SIZE)
Pegamento (Glue) Se usa para especificar dónde debería ir el exceso de espacio en la distribución. Se puede ver como una goma semi-húmeda que se estira y encoge, que sólo toma el espacio que los demás componentes no utilizan. Por ejemplo, poniendo un pegamento horizontal entre dos componentes en un box de izquierda-a-derecha se logra que todo el espacio sobrante quede entre los dos componentes en vez de a la derecha.
BOX.CREATEHORIZONTALGLUE() BOX.CREATEVERTICALGLUE()
Relleno personalizado (Filler)
Se usa para especificar un componente invisible con los tamaños mínimo, preferido y máximo que se deseen. Por ejemplo, poniendo un relleno personalizado de al menos 5 píxeles entre dos componentes en un BoxLayout de izquierda-a-derecha se obtendría esa separación entre dichos componentes, pero si hay más tamaño extra lo cogería hasta llegar al máximo definido.
NEW BOX.FILLER(MINSIZE, PREFSIZE, MAXSIZE)
La Figura 17.36 muestra el comportamiento que tendrían los rellenos vistos
separando dos componentes frente a los cambios de tamaño del contenedor.
Figura 17.36. Comportamiento de los diferentes rellenos
17.5.4 GridLayout
GRIDLAYOUT coloca los componentes en una parrilla de celdas que tienen el mismo
tamaño, mostrándolos en una sucesión de filas y columnas. Si se redimensiona el
componente, cambia el tamaño de cada celda para que todas sean lo más grande
posible. La Figura 17.37 muestra una aplicación que utiliza este controlador de
distribución para colocar cinco botones.
Figura 17.37. Aplicación que usa GridLayout
El siguiente fragmento de código muestra cómo se ha utilizado GRIDLAYOUT para
obtener la aplicación de la Figura 17.37.
Container contentPane = getContentPane(); contentPane.setLayout(new GridLayout(0,2)); //dos columnas y las filas que sean necesarias contentPane.add(new JButton("Boton 1")); contentPane.add(new JButton("2")); contentPane.add(new JButton("Boton 3")); contentPane.add(new JButton("Boton muy largo 4")); contentPane.add(new JButton("Boton 5"));
El constructor de GRIDLAYOUT utilizado en el ejemplo toma dos parámetros que
indican el número de filas y de columnas que tendrá la parrilla. Uno de ellos (sólo
uno) puede valer cero, indicando que no se desea ninguna restricción. Si se han
indicado valores tanto para filas como para columnas distintos de cero, en caso de
que se añadan más componentes que los que en principio cabrían, prevalecerá el
valor dado para las filas sobre el de las columnas.
17.5.5 GridBagLayout
GRIDBAGLAYOUT es el controlador de distribución más sofisticado y flexible. Alinea
los componentes situándolos en una parrilla de celdas, permitiendo que algunos
componentes ocupen más de una celda. Las filas de la parrilla no tienen porque ser
de la misma altura; de la misma forma las columnas pueden tener diferentes
anchuras. La Figura 17.38 muestra una aplicación que utiliza este controlador de
distribución para colocar cinco botones.
Figura 17.38. Aplicación que usa GridBagLayout
Para indicar el tamaño y la posición característicos de los componentes se hace
individualmente para cada componente utilizando un objeto de la clase
GGrriiddLLaayyoouuttDDeemmoo..jjaavvaa
GRIDBAGCONSTRAINTS que contiene toda la información relativa a la posición y
tamaño de un componente en el contenedor que utiliza GRIDBAGLAYOUT. El proceso
típico de uso de este controlador de distribución se puede ver en el siguiente
fragmento de código.
GridBagLayout gridbag = new GridBagLayout(); GridBagConstraints c = new GridBagConstraints(); JPanel pane = new JPanel(); pane.setLayout(gridbag); // Para cada componente que se va a añadir al contenedor: //...Crear el componente... //...Establecer los atributos para el objeto GridBagContraints... //indicar que se quieren usar esas especificaciones sobre ese componente gridbag.setConstraints(theComponent, c); pane.add(theComponent); //añadir finalmente el componente
Como se puede observar en el fragmento de código anterior, se necesita crear una
instancia de la clase GRIDBAGCONTRAINTS y, cada vez que se añada un componente, se
establecen los detalles en esa instancia, se asocia al componente y, finalmente, se
añade el componente. Se puede reutilizar la misma instancia de GRIDBAGCONTRAINTS
para añadir más componentes ya que una vez que se añade un componente los
valores que tenga en ese momento el objeto GRIDBAGCONTRAINTS son copiados y no
se verán afectados por cambios posteriores.
En una instancia de GRIDBAGCONTRAINTS se pueden indicar los valores que se
muestran en la Tabla 17.8. Todos ellos son atributos públicos.
Tabla 17.8. Valores configurables en GridBagConstraints
Variable Tipo / valores válidos Descripción GRIDX, GRIDY Entero (número de celda)
GRIDBAGCONSTRAINTS.RELATIVE Indica la posición en la parrilla. La esquina superior izquierda es la GRIDX=0 y GRIDY=0. Si se utiliza RELATIVE, el componente se situará una celda a la derecha (para GRIDX) y una abajo (para GRIDY) del componente añadido justo antes.
GRIDWIDTH, GRIDHEIGHT
Entero (número de celdas) GRIDBAGCONSTRAINTS.REMAINDER GRIDBAGCONSTRAINTS.RELATIVE
Indica el número de columnas (GRIDWIDTH) y filas (GRIDHEIGHT) utilizadas por el componente. Si se usa REMAINDER, el componente será siempre el último de esa fila (para GRIDWIDTH) o de esa columna (para GRIDHEIGHT). Si se usa RELATIVE, el componente será el siguiente al último de la fila (para
GGrriiddBBaaggLLaayyoouuttDDeemmoo..jjaavvaa
GRIDWIDTH) o de la columna (para GRIDHEIGHT).
FILL GRIDBAGCONSTANS.NONE GRIDBAGCONSTRAINTS.HORIZONTAL GRIDBAGCONSTRAINTS.VERTICAL GRIDBAGCONSTRAINTS.BOTH
Indica si se debe aprovechar el espacio sobrante cuando la celda del componente dispone de más espacio que el que éste ocupa. Con NONE (por defecto) no se aprovecha el espacio. Con HORIZONTAL se estiraría para aprovechar todo el ancho disponible, con VERTICAL para aprovechar todo el alto disponible, y BOTH para ambos casos.
IPADX, IPADY Entero (pixels). Indica un tamaño adicional al mínimo del componente tanto para el ancho (IPADX) como para el alto (IPADY). Por tanto el tamaño final del componente será su tamaño mínimo más IPADX*2 (ancho) o IPADY*2 (alto). Por defecto es cero.
INSETS Objeto de tipo INSETS: NEW INSETS(ARRIBA, IZQUIERDA, ABAJO, DERECHA).
Indica el espaciado externo mínimo del componente. Es decir, el espacio que hay como mínimo entre el propio componente y el borde de su celda.
ANCHOR GRIDBAGCONSTRAINTS.CENTER GRIDBAGCONSTRAINTS.NORTH GRIDBAGCONSTRAINTS.NORTHEAST GRIDBAGCONSTRAINTS.EAST GRIDBAGCONSTRAINTS.SOUTHEAST GRIDBAGCONSTRAINTS.SOUTH GRIDBAGCONSTRAINTS.SOUTHWEST GRIDBAGCONSTRAINTS.WEST GRIDBAGCONSTRAINTS.NORTHWEST
Indica el anclaje del componente (donde ha de colocarse) cuando se dispone de más espacio en la celda del que ocupa. Por defecto se utiliza CENTER.
WEIGHTX, WEIGHTY
Double entre 0.0 y 1.0. Indica la proporción de tamaño adicional que le corresponde a la columna (weightx) o fila (weighty) del componente cuando se redimensiona el contendor. Por defecto es 0.0, con lo que cuando se aumenta el tamaño del contenedor las filas y columnas no reciben espacio, si no que se queda todo en los bordes externos del contenedor, quedando la rejilla en el centro.
El código necesario para obtener la ventana de la Figura 17.38 se muestra a
continuación.
JButton button; Container contentPane = getContentPane(); GridBagLayout gridbag = new GridBagLayout(); GridBagConstraints c = new GridBagConstraints(); contentPane.setLayout(gridbag); c.fill = GridBagConstraints.HORIZONTAL;
button = new JButton("Button 1"); c.weightx = 0.5; c.gridx = 0; c.gridy = 0; gridbag.setConstraints(button, c); contentPane.add(button); button = new JButton("2"); c.gridx = 1; c.gridy = 0; gridbag.setConstraints(button, c); contentPane.add(button); button = new JButton("Button 3"); c.gridx = 2; c.gridy = 0; gridbag.setConstraints(button, c); contentPane.add(new JButton("Button 3")); button = new JButton("Long-Named Button 4"); c.ipady = 40; // Hacer que el componente sea alto c.weightx = 0.0; c.gridwidth = 3; c.gridx = 0; c.gridy = 1; gridbag.setConstraints(button, c); contentPane.add(button); button = new JButton("Button 5"); c.ipady = 0; //restablecer al valor por defecto c.weighty = 1.0; //pedir todo el tamaño adicional vertical c.anchor = GridBagConstraints.SOUTH; //colocarse siempre abajo c.insets = new Insets(10,0,0,0); //espacio arriba de 10 pixeles c.gridx = 1; // alineado con button 2 c.gridwidth = 2; // ocupa 2 columnas c.gridy = 2; // tercera fila gridbag.setConstraints(button, c); contentPane.add(button);
17.6 Threads y Swing
Una vez iniciada y visible una aplicación visual Swing, el programa es dirigido por
eventos que provocan la ejecución de código de manejo de eventos. Este
comportamiento se consigue gracias a que existe un hilo denominado de despacho
de eventos, encargado de recoger los eventos y de ejecutar el código de manejo de
los mismos presente en los manejadores de eventos. El hilo de despacho de eventos
también se encarga de dibujar los componentes en la pantalla. Por lo tanto en
Swing el código de manejo de eventos y de pintado se ejecuta en un único hilo.
Este diseño asegura que cada manejador de eventos finaliza antes de que se ejecute
el siguiente manejador, además de que el código de pintado nunca se ve
interrumpido por un evento.
Sin embargo, para evitar posibles problemas como interbloqueos, hay que tener
mucha precaución y respetar la siguiente regla:
Los componentes Swing se deben crear, modificar y consultar únicamente en el
hilo de despacho de eventos.
Por norma general el código que accede a los componentes visuales se encuentra
en los manejadores de eventos y, por lo tanto, se ejecuta en el hilo de despacho de
eventos. Se podrá por tanto acceder a los componentes visuales sin ningún tipo de
problema dentro del código de un manejador de eventos. Este caso es el más
habitual y no va en contra de la regla anterior.
Sin embargo, en ocasiones resulta útil el uso de hilos en las aplicaciones visuales:
1. Para realizar una tarea que implica mucho consumo de tiempo y se desea que
la interfaz siga respondiendo al usuario. Si en un manejador de eventos se
incluye código que tarda mucho tiempo en terminar, el hilo de despacho de
eventos permanecerá ejecutando dicho código y, durante ese tiempo, la
interfaz no se actualizará ni recogerá nuevos eventos, permaneciendo
“congelada”. El uso de hilos con este fin satisface la regla: el código de los
manejadores de eventos debe ser lo más rápido posible de forma que la
aplicación responda inmediatamente a todos los eventos del usuario.
2. Para realizar una operación que se ejecute cada cierto intervalo de tiempo.
3. Para esperar por mensajes procedentes de otros programas. Por ejemplo, se
puede mantener un hilo secundario bloqueado en la lectura de un SOCKET a la
espera de algún mensaje desde la otra parte y que la aplicación siga
respondiendo.
Todos los casos anteriores obligan a la utilización de un hilo secundario. El
problema surge si desde ese hilo se trata de acceder a los componentes visuales (p.
ej: al finalizar un cálculo costoso o al recibir un mensaje procedente de una
conexión, se desea modificar el texto de una etiqueta, habilitar botones, mostrar
mensajes, etc.).
El siguiente fragmento de código muestra la creación de un hilo para realizar un
cálculo costoso. El cálculo se hará en un hilo distinto al de despacho de eventos:
public void actionPerformed(ActionEvent e) { // codigo ejecutado en el hilo de despacho de eventos . . .
new Thread(){ public void run(){ // codigo NO ejecutado en el hilo de despacho de eventos calculoCostoso(); // tiempo apreciable por el usuario miAreaDeTexto.setText(“El calculo ha terminado!”); //mal! } }.start(); }
Como se puede observar, existe una línea al final del código del hilo secundario
que accede a un componente visual, lo cual debe evitarse. Lo que se debe
conseguir es que dicha línea se ejecute en el hilo de despacho de eventos. Existen
utilidades para obtener dicho comportamiento:
• Métodos INVOKELATER y INVOKEANDWAIT. Son dos métodos estáticos
pertenecientes a la clase JAVAX.SWING.SWINGUTILITIES. Ambos reciben como
parámetro un objeto que implementa la interfaz RUNNABLE y, por lo tanto,
proporciona un método RUN. INVOKELATER solicita al hilo de despacho de
eventos que ejecute el código del objeto RUNNABLE (método RUN). Por otro
lado, INVOKEANDWAIT se comporta igual que INVOKELATER, con la diferencia
de que la llamada a INVOKELATER retorna inmediatamente, pero la llamada a
INVOKEANDWAIT no retorna hasta que el hilo de despacho de eventos haya
ejecutado el código del objeto RUNNABLE. Se recomienda el uso de
INVOKELATER para evitar posibles interbloqueos. El siguiente fragmento de
código muestra cómo se utilizaría el método INVOKELATER, solventando el
problema que se está tratando:
public void actionPerformed(ActionEvent e) { new Thread(){ public void run(){ calculoCostoso(); // tiempo apreciable por el usuario SwingUtilities.invokeLater(new Runnable(){ public void run(){ // codigo ejecutado en el hilo de despacho de eventos miAreaDeTexto.setText(“El calculo ha terminado!”); } }); } }.start(); }
• Clase JAVAX.SWING.TIMER. Permite ejecutar código en el hilo de despacho de
eventos cada cierto intervalo de tiempo. Para ello se le pasan dos
parámetros en el constructor: uno indica el período de repetición y el otro
es un objeto de tipo ACTIONLISTENER, al cual le será invocado el método
ACTIONPERFORMED cada vez que se cumpla el período. El código dicho
método se ejecutará en el hilo de despacho de eventos. El siguiente
fragmento de código muestra un ejemplo de uso de la clase TIMER:
Timer miTimer = new Timer(ONE_SECOND, new ActionListener(){ public void actionPerformed(ActionEvent evt){ // codigo ejecutado en el hilo de despacho de eventos miAreaDeTexto.setText(“Ha pasado un segundo mas!”); } });