Vous êtes sur la page 1sur 60

Parte I - Bases

La Parte I se compone de dos capítulos que sientan las bases para un aprendizaje productivo y exitoso de
la librería de clases JFC/Swing. El primero empieza con un breve vistazo de lo qué es Swing y una
introducción a su arquitectura. El segundo profundiza un poco más en una discusión detallada de los
principales mecanismos subyacentes de Swing, y como interactuar con ellos. Hay varias secciones sobre
temas que son bastante avanzados, como la multitarea y el dibujo en pantalla. Este material es común a
varias áreas de Swing e introduciéndolo en el capítulo 2, su comprensión de lo que vendrá
posteriormente mejorará notablemente. Contamos con que tendrá que volver a él a menudo, y en algún
lugar le instaremos explícitamente a que lo haga. Como mínimo, le recomendamos que conozca los
contenidos del capítulo 2 antes de seguir adelante.

Capítulo 1. Un vistazo a Swing


En este capítulo:
• AWT
• Swing
• MVC
• Delegados UI y PLAF

1.1 AWT
AWT (Abstract Window Toolkit) es la parte de Java diseñada para crear interfaces de usuario y para
dibujar gráficos e imágenes. Es un conjunto de clases que intentan ofrecer al desarrollador todo lo que
necesita para crear una interfaz de usuario para cualquier applet o aplicación Java. La mayoría de los
componentes AWT descienden de la clase java.awt.Component como podemos ver en la figura 1.1.
(Obsérvese que las barras de menú de AWT y sus ítems no encajan dentro de la jerarquía de
Component.)

Figura 1.1 Jerarquía parcial de Components


<<fichero figure1-1.gif>>

1
JFC está compuesto de cinco partes fundamentales: AWT, Swing, Accesibilidad, Java 2D, y Arrastrar y
Soltar. Java 2D se ha convertido en una parte más de AWT, Swing está construido sobre AWT, el
soporte de accesibilidad se ha construido dentro de Swing. Las cinco partes de JFC no son en absoluto
mutuamente exclusivas, y se espera que Swing se fusione más profundamente con AWT en futuras
versiones de Java. El API de Arrastrar y Soltar no estaba totalmente desarrollado durante la escritura de
este libro pero esperamos que esta tecnología se integre más con Swing y AWT en un futuro próximo.
De este modo, AWT está en el corazón de JFC, lo que la convierte en una de las librerías más
importantes de Java 2.

1.2 Swing
Swing es un extenso conjunto de componentes que van desde los más simples, como etiquetas, hasta los
más complejos, como tablas, árboles, y documentos de texto con estilo. Casi todos los componentes
Swing descienden de un mismo padre llamado JComponent que desciende de la clase de AWT
Container. Es por ello que Swing es más una capa encima de AWT que una sustitución del mismo. La
figura 1.2 muestra una parte de la jerarquía de JComponent. Si la compara con la jerarquía de
Component notará que para cada componente AWT hay otro equivalente en Swing que empieza con
"J". La única excepción es la clase de AWT Canvas, que se puede reemplazar con JComponent,
JLabel, o JPanel (en la sección 2.8 abordaremos esto en detalle). Asimismo se percatará de que
existen algunas clases Swing sin su correspondiente homólogo.

La figura 1.2 representa sólo una pequeña fracción de la librería Swing, pero esta fracción son las clases
con las que se enfrentará más a menudo. El resto de Swing existe para suministrar un amplio soporte y la
posibilidad de personalización a los componentes estas clases definen.

Figura 1.2 Parte de la jerarquía de JComponent


<<fichero figure1-2.gif>>

2
1.2.1 Orden Z

A los componentes Swing se les denomina ligeros mientras que a los componentes AWT se les
denominados pesados. La diferencia entre componentes ligeros y pesados es su orden: la noción de
profundidad. Cada componente pesado ocupa su propia capa de orden Z. Todos los componentes ligeros
se encuentran dentro de componentes pesados y mantienen su propio esquema de capas definido por
Swing. Cuando colocamos un componente pesado dentro de un contenedor que también lo es, se
superpondrá por definición a todos los componentes ligeros del contenedor.

Lo que esto significa es que debemos intentar evitar el uso de componentes ligeros y pesados en un
mismo contenedor siempre que sea posible. Esto no significa que no podamos mezclar nunca con éxito
componentes AWT y Swing, sólo que tenemos que tener cuidado y saber qué situaciones son seguras y
cuáles no. Puesto que probablemente no seremos capaces de prescindir completamente del uso de
componentes pesados en un breve espacio de tiempo, debemos encontrar formas de que las dos
tecnologías trabajen juntas de manera aceptable.

La regla más importante a seguir es que no deberíamos colocar componentes pesados dentro de
contenedores ligeros, que comúnmente soportan hijos que se superponen. Algunos ejemplos de este tipo
de contenedores son JInternalFrame, JScrollPane, JLayeredPane, y JDesktopPane. En
segundo lugar, si usamos un menú emergente en un contenedor que posee un componente pesado,
tenemos que forzar a dicho menú a ser pesado. Para controlar esto en una instancia específica de
JPopupMenu podemos usar su método setLightWeightPopupEnabled().

Nota: Para JMenus (que usan JPopupMenus para mostrar sus contenidos) tenemos que usar primero el
método getPopupMenu() para recuperar su menú emergente asociado. Una vez recuperado podemos
llamar entonces a setLightWeightPopupEnabled(false) en él para imponer funcionalidad
pesada. Esto tiene que hacerse con cada JMenu de nuestra aplicación, incluyendo menús dentro de
menús, etc.

Alternativamente podemos llamar al método estático setDefaultLightWeightPopupEnabled()


de JPopupMenu y pasarle un valor false para forzar a todos los menús emergentes de una sesión de
Java a ser pesados. Tenga en cuenta que sólo afectará a los menús emergentes creados a partir de que se
ha hecho la llamada. Es por eso una buena idea llamar a este método durante la inicialización.

1.2.2 Independencia de la plataforma


La característica más notable de los componentes Swing es que están escritos al 100% en Java y no
dependen de componentes nativos, como sucede con casi todos los componentes AWT. Esto significa
que un botón Swing y un área de texto se verán y funcionarán idénticamente en las plataformas
Macintosh, Solaris, Linux y Windows. Este diseño elimina la necesidad de comprobar y depurar las
aplicaciones en cada plataforma destino.

Nota: Las únicas excepciones a esto son los cuatro componentes pesados de Swing que son subclases directas
de clases de AWT, que dependen de componentes nativos: JApplet, JDialog, JFrame, y
JWindow. Ver capítulo 3.

1.2.3 Vistazo al paquete Swing


javax.swing
Contiene la mayor parte de los componentes básicos de Swing, modelos de componente por
defecto, e interfaces. (La mayoría de las clases mostradas en la Figura 1.2 se encuentran en este

3
paquete.)
javax.swing.border
Clases e interfaces que se usan para definir estilos de bordes específicos. Observe que los bordes
pueden ser compartidos por cualquier número de componentes Swing, ya que no son
componentes por si mismos.
javax.swing.colorchooser
Clases e interfaces que dan soporte al componente JColorChooser, usado para selección de
colores. (Este paquete también contiene alguna clase privada interesante sin documentar.)
javax.swing.event
El paquete contiene todos los oyentes y eventos específicos de Swing. Los componentes Swing
también soportan eventos y oyentes definidos en java.awt.event y java.beans.
javax.swing.filechooser
Clases e interfaces que dan soporte al componente JFileChooser, usado para selección de
ficheros.
javax.swing.plaf
Contiene el API del comportamiento y aspecto conectable usado para definir componentes de
interfaz de usuario personalizados. La mayoría de las clases de este paquete son abstractas. Las
implementaciones de look-and-feel, como metal, motif y basic, crean subclases e implementan
las clases de este paquete. Éstas están orientadas a desarrolladores que, por una razón u otra, no
pueden usar uno de los look-and-feel existentes.
javax.swing.plaf.basic
Consiste en la implementación del Basic look-and-feel, encima del cual se construyen los look-
and-feels que provee Swing. Normalmente deberemos usar las clases de este paquete si
queremos crear nuestro look-and-feel personal.
javax.swing.plaf.metal
Metal es el look-and-feel por defecto de los componentes Swing. Es el único look-and-feel que
viene con Swing y que no está diseñado para ser consistente con una plataforma específica.
javax.swing.plaf.multi
Este es el Multiplexing look-and-feel. No se trata de una implementación normal de look-and-
feel ya que no define ni el aspecto ni el comportamiento de ningún componente. Más bien
ofrece la capacidad de combinar varios look-and-feels para usarlos simultáneamente. Un
ejemplo típico podría ser un look-and-feel de audio combinado con metal o motif. Actualmente
Java 2 no viene con ninguna implementación de multiplexing look-and-feel (de todos modos, se
rumorea que el equipo de Swing esta trabajando en un audio look-and-feel mientras escribimos
estas líneas).
javax.swing.table
Clases e interfaces para dar soporte al control de JTable. Este componente se usa para manejar
datos en forma de hoja de cálculo. Soporta un alto grado de personalización sin requerir mejoras
de look-and-feel.
javax.swing.text
Clases e interfaces usadas por los componentes de texto, incluyendo soporte para documentos
con o sin estilo, las vistas de estos documentos, resaltado, acciones de editor y personalización
del teclado.
javax.swing.text.html
Esta extensión del paquete text contiene soporte para componentes de texto HTML. (El soporte
de HTML está siendo ampliado y reescrito completamente mientras escribimos este libro. Es
por ello que la cobertura que le damos es muy limitada.)
javax.swing.text.html.parser
Soporte para analizar gramaticalmente HTML.
javax.swing.text.rtf
Contiene soporte para documents RTF.
javax.swing.tree
Clases e interfaces que dan soporte al componente JTree. Este componente se usa para mostrar
y manejar datos que guardan alguna jerarquía. Soporta un alto grado de personalización sin
requerir mejoras de look-and-feel.
javax.swing.undo

4
El paquete undo contiene soporte para implementar y manejar la funcionalidad
deshacer/rehacer.

1.3 Arquitectura MVC


MVC es una descomposición orientada a objeto del diseño de interfaces de usuario bien conocida que
data de finales de los 70. Los componentes se descomponen en tres partes: un modelo, una vista, y un
controlador. Los componentes Swing están basados en una versión más moderna de este diseño. Antes
de que abordemos como trabaja MVC en Swing, necesitamos comprender como se diseñó originalmente
su funcionamiento.

Nota: La separación en tres partes descrita aquí se usa en la actualidad solamente en un pequeño número de
conjuntos de componentes de interfaz de usuario, entre los que destaca VisualWorks.

Figura 1.3 La arquitectura Modelo-Vista-Controlador


<<fichero figure1-3.gif>>

1.3.1 Modelo
El modelo es el responsable de conservar todos los aspectos del estado del componente. Esto incluye,
por ejemplo, aquellos valores como el estado pulsado/no pulsado de un botón, los datos de un carácter de
un componente de texto y como esta estructurado, etc. Un modelo puede ser responsable de
comunicación indirecta con la vista y el controlador. Por indirecta queremos decir que el modelo no
‘conoce’ su vista y controlador--no mantiene referencias hacia ellos. En su lugar el modelo enviará
notificaciones o broadcasts (lo que conocemos como eventos). En la figura 1.3 esta comunicación
indirecta se representa con líneas de puntos.
1.3.2 Vista
La vista determina la representación visual del modelo del componente. Esto es el “aspecto(look)” del
componente. Por ejemplo, la vista muestra el color correcto de un componente, tanto si el componente
sobresale como si está hundido (en el caso de un botón), y el renderizado de la fuente deseada. La vista
es responsable de mantener actualizada la representación en pantalla y debe hacerlo recibiendo mensajes
indirectos del modelo o mensajes directos del controlador.
1.3.3 Controlador
El controlador es responsable de determinar si el componente debería reaccionar a algún evento
proveniente de dispositivos de entrada, tales como el teclado o el ratón. El controlador es el
“comportamiento(feel)” del componente, y determina que acciones se ejecutan cuando se usa el

5
componente. El controlador puede recibir mensajes directos desde la vista, e indirectos desde el modelo.

Por ejemplo, supongamos que tenemos un checkbox seleccionado en nuestro interfaz. Si el controlador
determina que el usuario ha pulsado el ratón debe enviar un mensaje a la vista. Si la vista determina que
la pulsación ha sido en el checkbox envía un mensaje al modelo. El modelo se actualiza y lo notifica
mediante un mensaje, que será recibido por la(s) vista(s), para decirle que debería actualizarse basándose
en el nuevo estado del modelo. De está manera, el modelo no está ligado a una vista o un controlador
específico, permitiéndonos tener varias vistas y controladores manipulando un mismo modelo.
1.3.4 Controlador y vista personalizados
Una de las principales ventajas de la arquitectura MVC es la posibilidad de personalizar el
“aspecto(look)” y el “comportamiento(feel)” de un componente sin modificar el modelo. La Figura 1.4
muestra un grupo de componentes que usan dos interfaces de usuario diferentes. Lo más importante de
esta figura es que los componentes mostrados son los mismos, pero que se están usando dos
implementaciones diferentes de look-and-feel (diferentes vistas y controladores -- como veremos más
adelante).

Figura 1.4 Malachite y Windows look-and-feels de los mismos componentes


<<fichero figure1-4.gif>>

Algunos componentes Swing ofrecen también la posibilidad de personalizar partes específicas del
componente sin afectar al modelo. Más específicamente, estos componentes permiten definir nuestros
propios editor y visualizador de celdas, que se usan para aceptar y mostrar datos específicos
respectivamente. La figura 1.5 muestra las columnas de una tabla que contiene datos del mercado de
valores, que se visualizan con iconos y colores personalizados. Veremos como sacar provecho de esta
funcionalidad en nuestro estudio de las listas, tablas, árboles y listas despegables (JComboBox).

6
Figura 1.5 Visualización personalizada
<<fichero figure1-5.gif>>

1.3.5 Modelos personalizados


Otra gran ventaja de la arquitectura MCV de Swing es la posibilidad de personalizar y reemplazar el
modelo de datos de un componente. Por ejemplo, podemos construir nuestro propio modelo de
documento de texto que preste especial atención a la escritura de una fecha o un número de teléfono de
una manera determinada. Podemos también asociar el mismo modelo de datos con más de un
componente (como ya comprobamos viendo MVC). Por ejemplo, dos JTextAreas pueden guardar su
texto en el mismo modelo de documento, mientras que están usando dos vistas diferentes de esa
información.

Diseñaremos e implementaremos nuestros propios modelos de datos para JComboBox, JList, JTree,
JTable, y más ampliamente a lo largo de nuestro repaso a los componentes de texto. Abajo hemos
listado algunas definiciones de interfaces de modelos Swing, con una breve descripción de los datos para
cuyo almacenamiento están diseñados, y con que componentes se usan:

BoundedRangeModel
Usado por: JProgressBar, JScrollBar, JSlider.
Guarda: 4 enteros: value, extent, min, max.
Value y extent tienen que estar entre los valores de min y max. Extent es siempre <= max y >=
value.
ButtonModel
Usado por: Todas las subclases de AbstractButton.
Guarda: Un booleano que determina si el botón está seleccionado (armado) o no (desarmado).
ListModel
Usado por: JList.
Guarda: Una colección de objetos.
ComboBoxModel
Usado por: JComboBox.
Guarda: Una colección de objetos y un objeto seleccionado.
MutableComboBoxModel
Usado por: JComboBox.
Guarda: Un vector (u otra colección alterable) de objetos y un objeto seleccionado.
ListSelectionModel
Usado por: JList, TableColumnModel.
Guarda: Uno o más índices de selecciones de la lista o de ítems de la tabla. Permite seleccionar
sólo uno, un intervalo simple, o un intervalo múltiple (discontinuo).
SingleSelectionModel
Usado por: JMenuBar, JPopupMenu, JMenuItem, JTabbedPane.

7
Guarda: El índice del elemento seleccionado en una colección de objetos perteneciente al
implementador.
ColorSelectionModel
Usado por: JColorChooser.
Guarda: Un Color.
TableModel
Usado por: JTable.
Guarda: Una matriz de objetos.
TableColumnModel
Usado por: JTable.
Guarda: Una colección de objetos TableColumn, un conjunto de oyentes para eventos de
modelos de una columna, la anchura entre cada columna, la anchura total de todas las columnas,
un modelo de selección, y un indicador de selección de columna.
TreeModel
Usado por: JTree.
Guarda: Objetos que se pueden mostrar en un árbol. Las implementaciones tienen que ser
capaces de distinguir entre las hojas y el resto de nodos, y los objetos deben estar organizados
jerárquicamente.
TreeSelectionModel
Usado por: JTree.
Guarda: Las filas seleccionadas. Permite selección simple, continua y discontinua.
Document
Usado por: Todos los componentes de texto.
Guarda: Contenido. Normalmente es texto (caracteres). Implementaciones más complejas
soportan texto con estilo, imágenes, y otros tipos de contenido. (p.e. componentes embebidos).

No todos los componentes Swing tienen modelos, aquellos que se usan como contenedores, como
JApplet, JFrame, JLayeredPane, JDesktopPane, JInternalFrame, etc. no los tienen. Sin
embargo, los componentes interactivos como JButton, JTextField, JTable, etc. tienen que tener
modelos. De hecho, algunos componentes Swing tienen más de un modelo (p.e. JList usa un modelo
para mantener información sobre la selección, y otro para guardar los datos). Esto quiere decir que MVC
no es totalmente rígido en Swing. Componentes simples o complejos, que no guardan grandes
cantidades de información (como JDesktopPane), no necesitan separar los modelos. La vista y el
controlador de cada componente están casi siempre separadas en todos los componentes Swing, como
veremos en la siguiente sección.

Entonces, ¿cómo encaja el componente por si mismo dentro del definición de MVC?. El componente se
comporta como un mediador entre el/los modelo(s), la vista y el controlador. No es ni la M, ni la V, ni la
C, aunque puede ocupar el lugar de una o incluso todas estas partes si lo diseñamos para ello. Esto se
verá más claro cuando progresemos en este capítulo y a lo largo del resto del libro.

1.4 Delegados UI y PLAF


Casi todos los conjuntos de componentes modernos combinan la vista y el controlador, tanto si se basan
en SmallTalk, como C++, o ahora Java. Ejemplos de ello son MacApp, Smalltalk/V, Interviews, y los
widgets X/Motif que se usan en IBM Smalltalk. JFC Swing ha sido el último en añadirse a este grupo.
Swing empaqueta todos los controladores y vistas de un componente dentro de un objeto denominado
delegado UI. Por esta razón, la arquitectura subyacente de Swing se denomina más acertadamente como
modelo-delegado que como modelo-vista-controlador. Idealmente, la comunicación entre el modelo y el
delegado UI es indirecta, permitiendo así tener asociado más de un modelo a un delegado UI, y
viceversa. Podemos verlo en la Figura 1.6.

8
Figura 1.6 Model-delegate architecture
<<fichero figure1-6.gif>>

1.4.1 La clase ComponentUI


Todos los delegados UI descienden de una clase abstracta que se llama ComponentUI. Los métodos de
ComponentUI describen los fundamentos de la comunicación entre un delegado UI y un componente.
Observe que a cada método se le pasa como parámetro un JComponent.

Métodos de ComponentUI:

static ComponentUI CreateUI(JComponent c)


Este se implementa normalmente para que devuelva una instancia compartida del delegado UI
que define la subclase apropiada de ComponentUI. Esta instancia se usa para ser compartida
entre componentes del mismo tipo (p.e. Todos los JButtons que usan el Metal look-and-feel
comparten la misma instancia estática del delegado UI definido en
javax.swing.plaf.metal.MetalButtonUI por defecto.)
installUI(JComponent c)
Instala el ComponentUI en el componente especificado. Esto añade normalmente oyentes al
componente y/o a su(s) modelo(s), para avisar al delegado UI cuando ocurran cambios en el
estado que requieran que se actualice la vista.
uninstallUI(JComponent c)
Borra este ComponentUI y cualquier oyente añadido por installUI() del componente
especificado y/o de su(s) modelo(s).
update(Graphics g, JComponent c)
Si el componente es opaco debería pintar su fondo y entonces llamar a paint(Graphics g,
JComponent c).
paint(Graphics g, JComponent c)
Coge toda la información necesaria del componente y posiblemente de su(s) modelo(s) para
dibujarlo correctamente.
getPreferredSize(JComponent c)
Devuelve el tamaño preferido del componente especificado por el ComponentUI.
getMinimumSize(JComponent c)
Devuelve el tamaño mínimo del componente especificado por el ComponentUI.
getMaximumSize(JComponent c)
Devuelve el tamaño máximo del componente especificado por el ComponentUI.

Para obligar a usar un delegado UI específico podemos usar el método setUI() del componente
(observe que setUI() está declarado como protected en JComponent porque sólo tiene sentido en
subclases de JComponent):

9
JButton m_button = new JButton();
m_button.setUI((MalachiteButtonUI)
MalachiteButtonUI.createUI(m_button));

La mayor parte de los delegados UI se construyen de manera que conocen un componente y su(s)
modelo(s) sólo mientras llevan a cabo tareas de dibujo o de vista-controlador. Swing evita normalmente
asociar delegados UI a un componente determinado (a causa de la instancia estática). De todos modos,
nada nos impide asignar el nuestro propio como demuestra el código anterior.

Nota: La clase JComponent define métodos para asignar delegados UI porque las declaraciones de métodos
no implican código específico de un componente. Esto no es posible con modelos de datos porque no
hay un interface de modelo del que todos ellos desciendan (p.e. no hay una clase base como
ComponentUI para los modelos Swing). Por esta razón los métodos para asignar modelos se definen
en las subclases de JComponent que sea necesario.

1.4.2 Pluggable look-and-feel


Swing incluye varios conjuntos de delegados UI. Cada conjunto contiene implementaciones de
ComponentUI para casi todos los componentes Swing y podemos llamar a estos conjuntos una
implementación de look-and-feel o pluggable look-and-feel (PLAF). El paquete javax.swing.plaf
se componen de clases abstractas que derivan de ComponentUI, y las clases del paquete
javax.swing.plaf.basic descienden de ellas para implementar el Basic look-and-feel. Éste es un
conjunto de delegados UI que se usan como base para construir el resto de clases de look-and-feel.
(Observe que el Basic look-and-feel no se puede usar directamente ya que BasicLookAndFeel es una
clase abstracta.) Hay tres implementaciones de pluggable look-and-feel que descienden de Basic look-
and-feel:

Windows: com.sun.java.swing.plaf.windows.WindowsLookAndFeel
CDE\Motif: com.sun.java.swing.plaf.motif.MotifLookAndFeel
Metal (por defecto): javax.swing.plaf.metal.MetalLookAndFeel

Hay también un MacLookAndFeel que simula las interfaces de usuario de Macintosh, pero no viene
con Java 2 y se debe descargar separadamente. Las librerías de los Windows y Macintosh pluggable
look-and-feel sólo se soportan en la plataforma correspondiente.

El multiplexing look-and-feel, javax.swing.plaf.multi.MultiLookAndFeel, extiende todas


las clases abstractas de javax.swing.plaf. Está diseñado para permitir que combinaciones de look-
and-feels se usen simultáneamente, y está enfocado pero no limitado, al uso con look-and-feels de
Accesibilidad. El trabajo de cada delegado UI multiplexado es manejar cada uno de sus delegados UI
hijos.

Todos los paquetes look-and-feel contienen una clase que desciende la clase abstracta
javax.swing.LookAndFeel: BasicLookAndFeel, MetalLookAndFeel,
WindowsLookAndFeel, etc. Esas son los puntos centrales de acceso a cada paquete de look-and-feel.
Las usamos cuando cambiamos el look-and-feel actual, y la clase UIManager (que maneja los look-
and-feels instalados) los usa para acceder a la tabla UIDefaults del look-and-feel actual (que entre
otras cosas contiene los nombres de las clases de los delegados UI del look-and-feel correspondientes a
cada componente Swing). Para cambiar el look-and-feel actual de una aplicación, tenemos simplemente
que llamar al método setLookAndFeel() de UIManager, pasándole el nombre completo del
LookAndFeel que vamos a usar. El código siguiente se puede usar para llevar esto a cabo en tiempo de
ejecución:

10
try {
UIManager.setLookAndFeel(
"com.sun.java.swing.plaf.motif.MotifLookAndFeel");
SwingUtilities.updateComponentTreeUI(myJFrame);
}
catch (Exception e) {
System.err.println("Could not load LookAndFeel");
}

SwingUtilities.updateComponentTreeUI() informa a todos los hijos del componente


especificado que el look-and-feel ha cambiado y que necesitan reemplazar sus delegados UI por los del
tipo especificado.
1.4.3 ¿Dónde están los delegados UI?
Hemos hablado de ComponentUI, y de los paquetes LookAndFeel donde se encuentran las
implementaciones, pero no hemos mencionado nada acerca de las clases específicas de los delegados UI
que derivan de ComponentUI. Todas las clases abstractas del paquete javax.swing.plaf
descienden de ComponentUI y se corresponden con un componente Swing determinado. El nombre de
cada clase sigue con el esquema general (sin la “J”) añadiéndole el sufijo “UI”. Por ejemplo, LabelUI
desciende de ComponentUI y es el delegado base usado por JLabel.

Estas clases son extendidas por implementaciones concretas como los paquetes basic y multi. Los
nombres de estas subclases siguen el esquema general de añadir un prefijo con el nombre del look-and-
feel al nombre de la superclase. Por ejemplo, BasicLabelUI y MultiLabelUI descienden ambas de
LabelUI y se encuentran en los paquetes basic y multi respectivamente. La figura 1.7 muestra la
jerarquía de LabelUI.

Figura 1.7 Jerarquía de LabelUI


<<fichero figure1-7.gif>>

Se espera que la mayoría de las implementaciones de look-and-feel extiendan las clases definidas en el
paquete basic, o las usen directamente. Los delegados UI de Metal, Motif, y Windows están
construidos encima de las versiones de Basic. Sin embargo, el Multi look-and-feel, es la única de las
implementaciones que no desciende de Basic, y es simplemente un medio para permitir instalar un
número arbitrario de delegados UI en un componente determinado.

La figura 1.7 debería enfatizar el hecho de que Swing suministra un gran numero clases de delegados UI.
Si quisiéramos crear una implementación completa de pluggable look-and-feel, queda claro que
supondría un gran esfuerzo y llevaría bastante tiempo. En el capítulo 21 aprenderemos cosas sobre este
proceso, así como a modificar y trabajar con los look-and-feels existentes.

11
Capítulo 2. Mecánicas de Swing
En este capítulo:
• Cambiando el tamaño y la posición de JComponent, y sus propiedades
• Manejo y lanzamiento de eventos
• Multitarea
• Temporizadores
• Los servicios de AppContext
• Interior de los temporizadores y TimerQueue
• JavaBeans
• Fuentes, Colores, Gráficos y texto
• Usando el área de recorte de Graphics
• Depuración de Gráficos
• Pintado y validación
• Manejo del foco
• Entrada de teclado, KeyStrokes, y Actions
• SwingUtilities

2.1 Cambiando el tamaño y la posición de JComponent y sus


propiedades
2.1.1 Propiedades
Todos los componentes Swing cumplen la especificación de los JavaBeans. En la sección 2.7 veremos
esto en detalle. Entre las cinco características que debe soportar un JavaBean se encuentra un conjunto
de propiedades y sus métodos de acceso asociados. Una propiedad es una variable global, y sus métodos
de acceso, si tiene alguno, son normalmente de la forma setPropertyname(),
getPropertyname() o isPropertyname().

Una propiedad que no tienen ningún evento asociado a un cambio en su valor se llama una propiedad
simple. Una propiedad ligada (bound property) es aquella para la que se lanzan
PropertyChangeEvents después de un cambio en su estado. Podemos registrar nuestros
PropertyChangeListeners para escuchar PropertyChangeEvents a través del método
addPropertyChangeListener() de JComponent. Una propiedad restringida (constrained
property) es aquella para la que se lanzan PropertyChangeEvents justo antes de que ocurra un
cambio en su estado. Podemos resgistrar VetoableChangeListeners que escuchen a
PropertyChangeEvents por medio del método addVetoableChangeListener() de
JComponent. Se puede vetar un cambio en el código de manejo de eventos de un
VetoableChangeListener lanzando una PropertyVetoException. (Sólo hay una clase en

12
Swing con propiedades restringidas: JInternalFrame).

Nota: Todos estos oyentes y eventos están definidos en el paquete java.awt.beans.

Los PropertyChangeEvent’s llevan consigo tres segmentos de información: nombre de la


propiedad, el valor antiguo, y el nuevo. Los Beans pueden usar instancias de
PropertyChangeSupport para manejar el lanzamiento de PropertyChangeEvents
correspondientes a cada propiedad ligada, a todos los oyentes registrados. De manera similar, una
instancia de VetoableChangeSupport se puede usar para manejar el envío de todos los
PropertyChangeEvents correspondientes a cada propiedad restringida.

Swing introduce una nueva clase llamada SwingPropertyChangeSupport (definida en


javax.swing.event) que es una subclase casi idéntica de PropertyChangeSupport. La
diferencia es que SwingPropertyChangeSupport se ha construido para que sea más eficiente. Lo
consigue sacrificando la seguridad entre los hilos, que, como veremos más tarde en este capítulo, no es
asunto de Swing si se siguen consistentemente las reglas generales de la multitarea (porque todo el
procesamiento de eventos debería llevarse a cabo en un solo hilo-el hilo de despacho de eventos). Por lo
tanto, si confiamos en que nuestro código ha sido construido de manera segura respecto a los hilos,
deberíamos usar esta versión más eficiente, en lugar de PropertyChangeSupport.

Nota: No hay equivalente en Swing para VetoableChangeSupport porque sólo hay cuatro propiedades
restringidas en Swing--todas definidas en JInternalFrame.

Swing introduce un nuevo tipo de propiedad que podemos llamar de cambio (change property), a falta
de un nombre dado. Usamos ChangeListeners para escuchar ChangeEvents que se lanzan cuando
cambia el estado de estas propiedades. Un ChangeEvent sólo lleva consigo un segmento de
información: la fuente del evento. Por esta razón, las propiedades de cambio son menos poderosas que
las propiedades ligadas y que las restringidas, pero están más extendidas. Un JButton, por ejemplo,
envía eventos de cambios todas las veces que se arma (se pulsa por primera vez), se presiona, o se suelta
(ver capítulo 5).

Otro nuevo aspecto en el estilo de las propiedades que introduce Swing es la noción de propiedades
cliente (client properties). Estas son básicamente pares clave/valor que se guardan en una Hashtable
facilitada por todos los componentes Swing. Esto permite añadir y borrar propiedades en tiempo de
ejecución, y se usa a menudo como un sitio donde guardar datos sin tener que construir una nueva
subclase.

Peligro: Las propiedades cliente pueden parecer una forma fantástica de añadir soporte al cambio de
propiedades para componentes personalizados, pero se nos recomienda explícitamente no hacerlo: “El
diccionario clientProperty no está pensado para soportar un alto grado de extensiones de
JComponent y no se debería considerar como una alternativa a la creación de subclases cuando se
diseña un nuevo componente.”API

Las propiedades cliente son ligadas: cuando una de ellas cambia, se envía un PropertyChangeEvent
a todos los PropertyChangeListeners registrados. Para añadir una propiedad a la Hashtable de
propiedades cliente de un componente, tenemos que hacer lo siguiente:
miComponente.putClientProperty("minombre", miValor);

Para recuperar una propiedad cliente:


miObjeto = miComponente.getClientProperty("minombre");

13
Para borrar una propiedad cliente le asignamos un valor null:

miComponente.putClientProperty("minombre", null);

Por ejemplo, JDesktopPane usa una propiedad cliente para controlar la visualización del contorno
mientras arrastramos JInternalFrames (esto funcionará sin importar el L&F que se esté usando):

miDesktop.putClientProperty("JDesktopPane.dragMode", "outline");

Nota: Puede localizar que propiedades tienen tienen eventos de cambio asociados con ellas, así como
cualquier otro tipo de evento, inspeccionando el código fuente de Swing. A no ser que esté usando Swing
para interfaces simples, le recomendamos que se acostumbre a esto.

Cinco componentes Swing tienen propiedades cliente especiales a las que solo el Metal L&F presta
atención. Concretamente son estas:

JTree.lineStyle
Un String que se usa para especificar si las relaciones entro los nodos se muestran como
líneas angulosas (“Angled”), líneas horizontales que definen los límites de las celdas
(“Horizontal” -- por defecto), o no se muestran líneas (“None”).
JScrollBar.isFreeStanding
Un Boolean que se usa para especificar si JScrollbar tendrá un borde (Boolean.FALSE -
- por defecto) o sólo las partes superior e izquierda (Boolean.TRUE).
JSlider.isFilled
Un Boolean que especifica si la parte más baja de un deslizador (JSlider) debe estar rellena
(Boolean.TRUE) o no (Boolean.FALSE -- por defecto).
JToolBar.isRollover
Un Boolean que sirve para determinar si un botón de la barra de herramientas muestra un
borde grabado sólo cuando el puntero del ratón se encuentra entre sus límites y ningún borde
cuando no (Boolean.TRUE), o se usa siempre un borde grabado (Boolean.FALSE -- por
defecto).
JInternalFrame.isPalette
Un Boolean que especifica si se usa un borde muy fino (Boolean.TRUE) o el borde normal
(Boolean.FALSE -- por defecto). En Java 2 FCS no se usa esta propiedad.

2.1.2 Cambiando el tamaño y la posición


Como JComponent desciende de java.awt.Container, hereda todas las funcionalidades de
posición y tamaño a las que estamos acostumbrados. Para manejar el tamaño preferido, máximo y
mínimo de un componente disponemos de los siguientes métodos:

setPreferredSize(), getPreferredSize()
El tamaño deseable de un componente. Lo usan la mayoría de los administradores de
disposición (Layout Managers) para dimensionar los componentes.
setMinimumSize(), getMinimumSize()
Usados durante el posicionamiento para especificar los límites inferiores de las dimensiones del
componente.
setMaximumSize(), getMaximumSize()
Usados durante el posicionamiento para especificar los límites superiores de las dimensiones del
componente.

Cada uno de los métodos setXX()/getXX() acepta/devuelve una instancia de Dimension.


Aprenderemos más de lo que significan estos tamaños dependiendo de cada administrador de
disposición en el capítulo 4. Si un administrador de disposición presta atención o no a estos tamaños

14
depende solamente de la implementación de dicho administrador. Es perfectamente factible construir un
administrador que simplemente los ignore todos, o que sólo preste atención a uno. El dimensionado de
los componentes en un contenedor es específico de cada administrador de disposición.

El método setBounds() de JComponent se puede usar para asignar a un componente el tamaño y


la posición dentro de su contenedor padre. Este método está sobrecargado, y puede tomar tanto
parámetros de tipo Rectangle (java.awt.Rectangle) como cuatro parámetros de tipo int que
represente la altura, la anchura y las coordenadas x e y. Por ejemplo, estas dos formas son equivalentes:
miComponent.setBounds(120,120,300,300);
Rectangle rec = new Rectangle(120,120,300,300);
miComponent.setBounds(rec);

Verá que setBounds() no pasará por encima de ninguna de las políticas de posicionamiento activas a
causa de un administrador de disposición de un contenedor padre. Por esta razón una llamada a
setBounds() puede parecer ignorada en determinadas situaciones porque intentó hacer su trabajo,
pero el componente fue obligado a volver a su tamaño original por el administrador de disposición (los
administradores de disposición siempre tienen la última palabra determinando el tamaño de un
componente).

setBounds() se usa normalmente para manejar componentes hijos en contenedores sin administrador
de disposición (como JLayeredPane, JDesktopPane, y JComponent). Por ejemplo, usamos
normalmente setBounds() cuando añadimos un JInternalFrame a un JDesktopPane.

El tamaño de un componente se puede obtener al estilo de AWT:

int h = miComponente.getHeight();
int w = miComponente.getWidth();

El tamaño se puede recuperar también como una instancia de Rectangle o de Dimension:

Rectangle rec2 = miComponente.getBounds();


Dimension dim = miComponente.getSize();

Rectangle contiene cuatro propiedades accesibles públicamente que describen su posición y su


tamaño:

int recX = rec2.x;


int recY = rec2.y;
int recAnchura = rec2.width;
int recAltura = rec2.height;

Dimension contiene dos propiedades accesibles públicamente que describen su tamaño:

int dimAnchura = dim.width;


int dimAltura = dim.height;

Las coordenadas que una instancia de Rectangle devuelve usando su método getBounds()
representan la situación de un componente dentro de su padre. Estas coordenadas se pueden obtener
también usando los métodos getX() y getY(). Adicionalmente, podemos determinar la posición de
un componente dentro de su contenedor mediante el método setLocation(int x, int y).

JComponent también mantiene una alineación. La alineación horizontal o vertical se puede especificar
con valores reales (float) entre 0.0 y 1.0: 0.5 significa el centro, valores más cercanos a 0.0 significan
izquierda o arriba, y más cercanos a 1.0 significan derecha o abajo. Los correspondientes métodos de

15
JComponent son:

setAlignmentX(float f);
setAlignmentY(float f);

Estos valores se usan sólo en contenedores que se manejan mediante BoxLayout o OverlayLayout.

2.2 Manejo y lanzamiento de eventos


Los eventos ocurren en cualquier momento que se pulsa una tecla o un botón del ratón. La forma en la
que los componentes reciben y procesan los eventos no ha cambiado desde el JDK1.1. Los componentes
Swing pueden generar diferentes tipos de eventos, incluyendo los de java.awt.event y por supuesto,
los de javax.swing.event. Algunos de estos nuevos tipos de eventos de Swing son específicos del
componente. Todos los tipos de eventos se representan por un objeto, que como mínimo, identifica la
fuente del evento, y a menudo lleva información adicional acerca de la clase específica de evento que se
trata, e información acerca del estado de la fuente del evento antes y después de que éste se generase.
Las fuentes de eventos son normalmente componentes o modelos, pero hay también clases de objetos
diferentes que pueden generar eventos.

Como vimos en el último capítulo, para recibir la notificación de eventos, debemos registrar oyentes en
el objeto destino. Un oyente es una implementación de alguna de las clases XXListener (donde XX es
un tipo de evento) definidas en los paquetes java.awt.event, java.beans, y
javax.swing.event. Como mínimo, siempre hay un método definido en cada interface al que se le
pasa el XXEvent correspondiente como parámetro. Las clases que soportan la notificación de
XXEvents implementan generalmente el interface XXListener, y tienen soporte para registrar y
cancelar el registro de estos oyentes a través del uso de los métodos addXXListener() y
removeXXListener() respectivamente. La mayoría de los destinos de eventos permiten tener
registrados cualquier número de oyentes. Igualmente, cualquier instancia de un oyente se puede registrar
para recibir eventos de cualquier número de fuentes de éstos. Normalmente, las clases que soportan
XXEvents ofrecen métodos fireXX() protegidos (protected) que se usan para construir objetos de
eventos y para enviarlos a los manejadores de eventos para su proceso.
2.2.1 La clase javax.swing.event.EventListenerList
EventListenerList es un vector de pares XXEvent/XXListener. JComponent y cada uno de
sus descendientes usa una EventListenerList para mantener sus oyentes. Todos los modelos por
defecto mantienen también oyentes y una EventListenerList. Cuando se añade un oyente a un
componente Swing o a un modelo, la instancia de Class asociada al evento (usada para identificar el
tipo de evento) se añade a un vector EventListenerList, seguida del oyente. Como estos pares se
guardan en un vector en lugar de en una colección modificable (por eficiencia), se crea un nuevo vector
usando el método System.arrayCopy() en cada adición o borrado. Cuando se reciben eventos, se
recorre la lista y se envían eventos a todos los oyentes de un tipo adecuado. Como el vector está
ordenado de la forma XXEvent, XXListener, YYEvent, YYListener, etc., un oyente
correspondiente a un determinador tipo de evento está siempre el siguiente en el vector. Esta estrategia
permite unas rutinas de manejo de eventos muy eficientes (ver sección 2.7.7). Para seguridad entre
procesos, los métodos para añadir y borrar oyentes de una EventListenerList sincronizan el acceso
al vector cuando lo manipulamos.

JComponent define sus EventListenerList como un campo protegido llamado listenerList,


así que todas sus subclases lo heredan. Los componentes Swing manejan la mayoría de sus oyentes
directamente a través de listenerList.
2.2.2 Hilo de despacho de eventos (Event-dispatching thread)
Todos los eventos se procesan por los oyentes que los reciben dentro del hilo de despacho de eventos

16
(una instancia de java.awt.EventDispatchThread). Todo el dibujo y posicionamiento de
componentes debería llevarse a cabo en este hilo. El hilo de despacho de eventos es de vital importancia
en Swing y AWT, y juega un papel principal manteniendo actualizado el estado y la visualización de un
componente en una aplicación bajo control.

Asociada con este hilo hay una cola FIFO (primero que entró - primero que sale) de eventos -- la cola de
eventos del sistema (una instancia de java.awt.EventQueue). Esta cola se rellena, como cualquier
cola FIFO, en serie. Cada petición toma su turno para ejecutar el código de manejo de eventos, que
puede ser para actualizar las propiedades, el posicionamiento o el repintado de un componente. Todos
los eventos se procesan en serie para evitar situaciones tales como modificar el estado de un componente
en mitad de un repintado. Sabiendo esto, tenemos que ser cuidadosos de no despachar eventos fuera del
hilo de despacho de eventos. Por ejemplo, llamar directamente a un método fireXX() desde un hilo de
ejecución separado es inseguro. Tenemos que estar seguros también de que el código de manejo de
eventos se puede ejecutar rápidamente. En otro caso toda la cola de eventos del sistema se bloquearía
esperando a que terminase el proceso de un evento, el repintado, o el posicionamiento, y nuestra
aplicación parecería bloqueada o congelada.

2.3 Multitarea
Para ayudar a asegurarnos que todo nuestro código de manejo de eventos se ejecuta sólo dentro del hilo
de despacho de eventos, Swing provee una clase de mucha utilidad, que entre otras cosas, nos permite
añadir objetos Runnable a la cola de eventos del sistema. Esta clase se llama SwingUtilities y
contiene dos métodos en los que estamos interesados aquí: invokeLater() e invokeAndWait(). El
primer método añade un Runnable a la cola de eventos del sistema y vuelve inmediatamente. El
segundo método añade un Runnable y espera a que sea despachado, entonces vuelve una vez que
termina. La sintaxis básica de cada una es la siguiente:

Runnable trivialRunnable = new Runnable() {


public void run() {
hazTrabajo(); // hace algún trabajo
}
};
SwingUtilities.invokeLater(trivialRunnable);

try {
Runnable trivialRunnable2 = new Runnable() {
public void run() {
hazTrabajo(); // hace algún trabajo
}
};
SwingUtilities.invokeAndWait(trivialRunnable2);
}
catch (InterruptedException ie) {
System.out.println("...Espera del hilo interrumpida!");
}
catch (InvocationTargetException ite) {
System.out.println(
"...excepción no capturada dentro de run() en Runnable");
}

Como estos Runnables se colocan en la cola de eventos del sistema para ejecutarse dentro del hilo de
despacho de eventos, tenemos que tener cuidado de que se ejecuten tan rápidamente como cualquier otro
código de manejo de eventos. En los dos ejemplo de arriba, si el método hacerTrabajo() hiciese
alguna cosa que le llevase un largo tiempo (como cargar un fichero grande) veríamos que la aplicación
se congelaría hasta que la carga finalizase. En los casos que conlleven mucho tiempo como este,
deberíamos usar nuestro propio hilo separado para mantener la sensibilidad de la aplicación.

17
El código siguiente muestra la forma típica de construir nuestro propio hilo que haga un trabajo costoso
en tiempo. Para actualizar de manera segura el estado de algún componente dentro de este hilo, tenemos
que usar invokeLater() o invokeAndWait():

Thread trabajoDuro = new Thread() {


public void run() {
hacerTrabajoPesado(); // hace algún trabajo costoso en tiempo
SwingUtilities.invokeLater( new Runnable () {
public void run() {
actualizaComponentes(); // actualiza el estado de lo(s)
componente(s)
}
});
}
};
trabajoDuro.start();

Nota: se debería usar invokeLater() en lugar de invokeAndWait() siempre que sea posible. Si
tenemos que usar invokeAndWait(), debemos estar seguros de que no hay zonas sensibles a
bloqueos (p.e. bloques sincronizados) mantenidas por el hilo que llama, que otro hilo podría necesitar
durante la operación.

Esto soluciona el problema de la sensibilidad, y añade código relativo al componente al hilo de despacho
de eventos, pero no se puede considerar aún amigable al usuario. Normalmente el usuario debería ser
capaz de interrumpir una tarea costosa en tiempo. Si estamos esperando una conexión a una red, no
queremos esperar indefinidamente si el destino no existe. En casi todas las circunstancias el usuario
debería tener la opción de interrumpir nuestro hilo. El pseudocódigo siguiente nos muestra una manera
típica de llevar esto a cabo, donde stopButton hace que el hilo sea interrumpido, actualizando el
estado del componente adecuadamente:
Thread trabajoDuro = new Thread() {
public void run() {
hacerTrabajoPesado();
SwingUtilities.invokeLater( new Runnable () {
public void run() {
actualizaComponentes(); // actualiza el estado de lo(s)
componente(s)
}
});
}
};
trabajoDuro.start();
public void hacerTrabajoPesado() {
try {
// [alguna clase de bucle]
// ...si, en algún punto, esto supone cambiar
// el estado del componente tenemos que usar
// invokeLater aquí porque este es un hilo
// separado.
//
// Tenemos como mínimo que hacer una cosa de
// las siguientes:
// 1. Chequear periódicamente Thread.interrupted()
// 2. Dormir o esperar periódicamente
if (Thread.interrupted()) {
throw new InterruptedException();
}
Thread.wait(1000);
}

18
catch (InterruptedException e) {
// hacer que alguien sepa que hemos sido interrumpidos
// ...si esto supone cambiar el estado del componente
// tenemos que usar invokeLater aquí.
}
}

JButton stopButton = new JButton("Stop");


ActionListener stopListener = new ActionListener() {
public void actionPerformed(ActionEvent event) {
// interrumpir el hilo y hacer que el usuario sepa que
// el hilo ha sido interrumpido deshabilitando el botón
// de stop.
// ...esto se hará dentro del hilo de despacho de eventos
workHarder.interrupt();
stopButton.setEnabled(false);
}
};
stopButton.addActionListener(stopListener);

Nuestro stopButton interrumpe el hilo workHarder cuando se pulsa. Hay dos formas de que
hacerTrabajoPesado() sepa si workHarder, el hilo en el que se ejecuta, ha sido interrumpido. Si
está durmiendo o esperando, una InterruptedException será lanzada, que podremos capturar y
procesar adecuadamente. La otra manera de detectar la interrupción es chequear periódicamente el
estado llamando a Thread.interrupted().

Esto se usa para construir y mostrar diálogos complejos, en procesos de E/S que conllevan cambios en el
estado del componente (como cargar un documento en un componente de texto), carga de clases o
cálculo intensivo, para esperar algún mensaje o el establecimiento de una conexión de red, etc.

Referencia: Los miembros del equipo de Swing han escrito algún artículo sobre como utilizar hilos con
Swing, y han construido una clase llamada SwingWorker que hace el manejo del tipo de multitarea
descrito aquí más conveniente. Ver
http://java.sun.com/products/jfc/tsc/archive/tech_topics_arch/threads/threads.html

2.3.1 Casos especiales


Hay algunos casos especiales en los cuales no necesitamos que código que afecte al estado de
componentes se ejecute en el hilo de despacho de eventos:

1. Algunos métodos en Swing, aunque pocos y distantes entre sí, están marcados como seguros respecto
a los hilos y no necesitan consideración especial. Algunos métodos que son seguros respecto a los hilos
pero que no están marcados son: repaint(), revalidate(), e invalidate().

2. Un componente se puede construir y manipular de la forma que queramos, sin tener cuidado con los
hilos, siempre que no se haya tenido en cuenta (realized) (lo que quiere decir que no se haya mostrado o
que no haya encolado una petición de repintado). Los contenedore de más alto nivel (JFrame,
JDialog, JApplet) se tienen en cuenta una vez que se ha llamado a setVisible(true), show(),
o pack() en ellos. Observe también que se considera que un componente se tiene en cuenta tan pronto
como se añade a un contenedor que se tiene en cuenta.

3. Cuando trabajamos con applets Swing (JApplets) todos los componentes se pueden construir y
manipular sin prestar atención a los hilos hasta que se llama al método start(), lo que sucede después
del método init().

19
2.3.2 ¿Cómo construimos nuestros métodos para que sean seguros respecto a los hilos?
Esto es realmente fácil. Aquí tenemos una plantilla de método seguro respecto a los hilos, que podemos
usar para garantizar que el código de este método se ejecuta sólo en el hilo de despacho de eventos:

public void hacerTrabajoSeguro() {


if (SwingUtilities.isEventDispatchThread()) {
//
// hacer todo el trabajo aquí...
//
}
else {
Runnable llamaAhacerTrabajoSeguro = new Runnable() {
public void run() {
hacerTrabajoSeguro();
}
};
SwingUtilities.invokeLater(llamaAhacerTrabajoSeguro);
}
}

2.3.3 ¿Cómo funcionan invokeLater() e invokeAndWait()?

clase javax.swing.SystemEventQueueUtilities [privada de paquete]


Cuando SwingUtilities recibe un objeto Runnable a través de invokeLater(), lo pasa
inmediatamente al método postRunnable() de una clase llamada
SystemEventQueueUtilities. Si el Runnable se recibe a través de invokeAndWait(), se
comprueba primero que el hilo actual no sea el hilo de despacho de eventos. (Sería fatal permitir invocar
a invokeAndWait() desde el hilo de despacho de eventos) Si este es el caso se lanza un error. En otro
caso, construimos un Object que usaremos como bloqueo en una sección crítica (p.e. un bloque
sincronizado). Este bloque contiene dos instrucciones. La primera envía el Runnable al método
postRunnable() de SystemEventQueueUtilities, junto con una referencia al objeto de
bloqueo. La segunda espera al objeto de bloqueo, de manera que el hilo no avanzará hasta que el objeto
sea notificado--por ello “invoke and wait (invocar y esperar).”

Lo primero que hace el método postRunnable() es comunicarse con la cola privada


SystemEventQueue, una clase interna de SystemEventQueueUtilities, para obtener un
referencia a la cola de eventos del sistema. Se envuelve entonces el Runnable en una instancia de
RunnableEvent, otra clase interna privada. Al constructor de RunnableEvent se le pasan un
Runnable y un Object, que representa al objeto de bloqueo (null si se llamó a invokeLater())
como parámetros.

La clase RunnableEvent es una subclase de AWTEvent, y define su propio ID del evento como un
int estático -- EVENT_ID. (Vea que siempre que definimos nuestros propios eventos debemos usar un
ID mayor que el valor de AWTEvent.RESERVED_ID_MAX.) El EVENT_ID de RunnableEvent es
AWTEvent.RESERVED_ID_MAX + 1000. RunnableEvent contiene también una instancia estática
de un RunnableTarget, otra clase interna privada más. RunnableTarget es una subclase de
Component y su único propósito es actuar como fuente y destino de RunnableEvents.

¿Cómo hace esto RunnableTarget? Su constructor habilita los eventos con un ID que concuerde con
el ID del RunnableEvent:

enableEvents(RunnableEvent.EVENT_ID);

También sobrecarga el método protegido de Component processEvent() para recibir


RunnableEvents. Dentro de este método lo primero que se hace es ver si en efecto el evento pasado es

20
una instancia de RunnableEvent. Si lo es, es pasado al método processRunnableEvent() de
SystemEventQueueUtilities (esto ocurre una vez que el RunnableEvent ha sido despachado
de la cola de eventos del sistema.)

Volvamos de nuevo a RunnableEvent. El contructor RunnableEvent llama al constructor de su


superclase (AWTEvent) pasándole una instancia de RunnableTarget como la fuente del evento, y
EVENT_ID como el ID del evento. Mantiene también referencias al Runnable y al objeto de bloqueo.

Resumiendo: cuando llamamos a invokeLater() o a invokeAndWait(), el Runnable que les


pasamos es pasado al método SystemEventQueueUtilities.postRunnable() junto al objeto
de bloqueo al que está esperando el hilo que les invoca (si fue un invokeAndWait()). Este método
intenta entonces obtener acceso a la cola de eventos del sistema y luego envuelve el Runnable y el
objeto de bloqueo en una instancia de RunnableEvent.

Una vez que se ha creado la instancia de RunnableEvent, el método postRunnable() (en el que
hemos estado todo este tiempo) comprueba si ha obtenido el acceso a la cola de eventos del sistema.
Esto ocurrirá sólo si no estamos ejecutándonos como un applet, ya que los applets no tienen acceso
directo a la cola de eventos del sistema. En este punto, tenemos dos posibles caminos dependiendo de si
nos estamos ejecutando como un applet o como una aplicación:
Aplicaciones:
Como tenemos acceso directo a la cola de eventos del sistema de AWT simplemente ponemos el
RunnableEvent y volvemos. Entonces el evento es despachado en algún punto del hilo de despacho
de eventos enviándolo al método processEvent() de RunnableTarget, el cual lo envía entonces
al método processRunnableEvent(). Si no se ha usado bloqueo (se llamó a invokeLater()) el
Runnable se ejecuta y hemos terminado. Si se ha usado un bloqueo (se llamó a invokeAndWait()),
entramos en un bloque sincronizado en el objeto de bloqueo de forma que nadie más puede acceder al
objeto mientras ejecutamos el Runnable. Recuerde que este es el mismo objeto de bloqueo al que está
esperando el hilo que invocó a SwingUtilities.invokeAndWait(). Una vez que el Runnable
termina, lo notificamos a ese objeto, que despierta al hilo invocante y hemos acabado.
Applets:
SystemEventQueueUtilities hace algunas cosas muy interesantes para rodear el hecho de que los
applets no tengan acceso directo a la cola de eventos del sistema. Para abreviar una tarea muy
complicada, un RunnableCanvas (una clase interna privada que desciende de java.awt.Canvas)
invisible se mantiene para cada applet y se guarda en una Hashtable estática usando el hilo invocante
como clave. Un Vector de RunnableEvents se mantiene también y, en lugar de añadir manualmente
un evento a la cola de eventos del sistema, un RunnableCanvas añade una petición de repaint().
Entonces, cuando se despacha la petición de repintado en el hilo de despacho de eventos. El método
paint() apropiado de RunnableCanvas es llamado como se esperaba. Este método ha sido
construido para que localice cualquier RunnableEvent (guardado en el Vector) asociado con un
determinado RunnableCanvas, y lo ejecute (algo rebuscado, pero funciona).

2.4 Temporizadores
clase javax.swing.Timer
Puede pensar en Timer como un hilo único que Swing provee convenientemente para lanzar
ActionEvents a intervalos especificados (aunque no es así como exactamente funciona un Timer
internamente, como veremos en la sección 2.6). Los ActionListeners se pueden registrar para que
reciban estos eventos tal y como los registramos en botones, o en otros componentes. Para crear un
Timer simple que lance ActionEvents cada segundo podemos hacer algo como lo siguiente:

21
import java.awt.event.*;
import javax.swing.*;

class TimerTest
{
public TimerTest() {
ActionListener act = new ActionListener() {
public void actionPerformed(ActionEvent e) {
System.out.println("Swing is powerful!!");
}
};
Timer tim = new Timer(1000, act);
tim.start();

while(true) {};
}

public static void main( String args[] ) {


new TimerTest();
}
}

En primer lugar configuramos un ActionListener para que reciba ActionEvents. Entonces


construimos un nuevo Timer pasando el tiempo entre eventos en milisegundos, el retraso (delay), y un
ActionListener al que enviárselos. Finalmente llamamos al método start() de Timer para
activarlo. Como no hay ejecutándose una GUI el programa saldrá inmediatamente, por lo tanto ponemos
un bucle para permitir al Timer que siga con su trabajo indefinidamente (explicaremos por qué es
necesario esto en la sección 2.6).

Cuando ejecute este código verá que se muestra “Swing is powerful!!” en la salida estándar cada
segundo. Observe que Timer no lanza un evento justo cuando se inicia. Esto es a causa de su retraso
inicial (initial delay) que por defecto equivale al tiempo que se le pasa al constructor. Si queremos que el
Timer lance un evento justo cuando se inicia debemos poner el retraso inicial a 0 usando su método
setInitialDelay().

En cualquier momento podemos llamar a stop() para detener el Timer y start() para reiniciarlo
(start() no hace nada si ya se está ejecutando). Podemos llamar a restart() en un Timer para que
empiece de nuevo todo el proceso. El método restart() es sólo una abreviatura para llamar a
stop() y start() secuencialmente.

Podemos poner el retraso de un Timer usando su método setDelay() y decirle si debe repetirse o no
usando el método setRepeats(). Una vez que hemos hecho que un Timer no se repita, sólo lanzará
una acción cuando se inicie (o si ya se está ejecutando), y entonces se detendrá.

El método setCoalesce() permite que varios Timer de lanzamiento de eventos se combinen en uno.
Esto puede ser útil durante sobrecargas, cuando el hilo de la TimerQueue (que veremos más adelante)
no tiene suficiente tiempo de proceso para manejar todos sus Timers.

Los Timers son fáciles de usar y a menudo se pueden utilizar como una herramienta conveniente para
construir nuestros propios hilos. Sin embargo, hay mucho más por detrás que merece un poco de
atención. Antes de que veamos a fondo como trabajan los Timers, echaremos un vistazo al servicio de
mapeo de clases de Swing (SecurityContext-to-AppContext) para applets, así como a la forma en
la que las aplicaciones manejan sus clases de servicio (también usando AppContext). Si no siente
curiosidad por como comparte Swing las clases de servicio, puede saltarse la siguiente sección. aunque
nos referiremos de vez en cuando a AppContext, no significa que sea necesario para entender los
detalles.

22
2.5 Los servicios de AppContext
clase sun.awt.AppContext [específica de la plataforma]
Peligro: AppContext no está pensada para ser usada por cualquier desarrollador, ya que no es parte del API
de Java 2. La abordamos aquí sólo para facilitar una mejor comprensión de como las clases de servicio
de Swing trabajan entre bastidores.

AppContext es una tabla de servicio de una aplicación o de un applet (diremos “app” para abreviar)
que es única para cada sesión de Java (applet o aplicación). Para los applets, existe un AppContext
separado para cada SecurityContext que corresponde a la base del código del applet. Por ejemplo,
si tenemos dos applets en la misma página, cada uno usando código de un directorio diferente, los dos
deberán tener asociado con ellos un SecurityContext distinto. Si al contrario, se han cargado desde
la misma base de código, tendrán que compartir necesariamente un SecurityContext. Las
aplicaciones Java no tienen SecurityContexts. En su lugar, se ejecutan en espacios de nombres que
son diferenciados por los ClassLoaders. No profundizaremos en los detalles de SecurityContexts
o ClassLoaders aquí, pero es suficiente decir que se pueden usar por los SecurityManagers para
indicar dominios de seguridad, y la clase AppContext está diseñada para aprovecharse de esto
permitiendo que haya tan sólo una instancia de ella misma por cada dominio de seguridad. De esta
forma, applets de diferentes bases de código no pueden acceder al AppContext del otro. ¿Pero, por qué
es esto importante? Vamos allá...

Una instancia compartida (shared instance) es una instancia de una clase que se puede obtener
normalmente usando un método estático definido en esa clase. Cada AppContext mantiene una
Hashtable de instancias compartidas disponibles para el dominio de seguridad asociado, y a cada
instancia se le denomina como un servicio. Cuando se pide un servicio por primera vez, éste registra su
instancia compartida con su AppContext asociado. Esto consiste en crear una nueva instancia de si
mismo y añadirla al mapeo clave/valor del AppContext.

Una razón por la cual estas instancias compartidas se registran con un AppContext en lugar de ser
implementadas como instancias estáticas normales, directamente recuperables por la clase de servicio, es
por propósitos de seguridad. Los servicios registrados con un AppContext se pueden acceder sólo
desde apps seguras (trusted apps), mientras que las clases que proveen directamente instancias estáticas
de si mismas permiten que éstas se usen de manera global (requiriendo que implementemos nuestro
propio mecanismo de seguridad si queremos limitar el acceso a ellas). Otra razón para ello es la
robustez. Cuantos menos applets interactúen con otros de manera indocumentada, más robustos podrán
ser.

Por ejemplo, imagine que una app intenta acceder a todos los eventos importantes en la EventQueue
del sistema (donde se encolan todos los eventos para que sean procesados en el hilo de despacho de
eventos) para intentar lograr contraseñas. Usando distintas EventQueues en cada AppContext, a los
únicos eventos principales que la app tendría acceso sería a los suyos. (Por esto hay sólo una
EventQueue por cada AppContext)

Entonces, ¿cómo accedemos a nuestro AppContext para añadir, borrar o recuperar servicios?
AppContext no está pensado para que sea accedido por desarrolladores, pero podemos si realmente lo
necesitamos, y esto garantizaría que nuestro código no sería certificado como 100% puro nunca, ya que
AppContext no forma parte del núcleo del API. No obstante, así es como se hace: El método estático
AppContext.getAppContext() determina el AppContext correcto a usar dependiendo de si se
está ejecutando una aplicación o una applet. Podemos usar entonces los métodos put(), get() y
remove() del AppContext devuelto para manejar las instancias compartidas. Para lograr esto,
tenemos que implementar nuestros propios métodos como sigue:

23
private static Object appContextGet(Object key) {
return sun.awt.AppContext.getAppContext().get(key);
}

private static void appContextPut(Object key, Object value) {


sun.awt.AppContext.getAppContext().put(key, value);
}

private static void appContextRemove(Object key) {


sun.awt.AppContext.getAppContext().remove(key);
}

En Swing, esta funcionalidad está implementada como tres métodos estáticos de SwingUtilities
(vea el código fuente de SwingUtilities.java):

static void appContextPut(Object key, Object value)


static void appContextRemove(Object key, Object value)
static Object appContextGet(Object key)

De todas formas, no podemos acceder a ellos porque son privados del paquete. Estos son los métodos
usados por las clases de servicio de Swing. Alguna de las clases de servicio de Swing que registran
instancias compartidas con AppContext son: EventQueue, TimerQueue, ToolTipManager,
RepaintManager, FocusManager y UIManager.LAFState (todas serán abordadas en algún
punto de este libro). Es también interesante que SwingUtilities provee secretamente una instancia
invisible de Frame registrado con AppContext para actuar como el padre de todos los JDialogs y
JWindows con propietarios a null.

2.6 Interior de los temporizadores y TimerQueue


class javax.swing.TimerQueue [privada de paquete]
Un Timer es un objeto que contiene un pequeño Runnable capaz de despachar ActionEvents a una
lista de ActionListeners (guardados en una EventListenerList). Todas las instancias de
Timer se manejan por la instancia compartida de TimerQueue (registrada con AppContext).

Un TimerQueue es una clase de servicio cuyo trabajo es manejar todas las instancias de Timer en una
sesión de Java. La clase TimerQueue provee el método estático sharedInstance() para recuperar
el servicio TimerQueue de AppContext. Siempre que un nuevo Timer se crea y se inicia, es añadido
a la TimerQueue compartida, que mantiene una lista de Timers ordenados por el tiempo en el que
expiran (p.e. el tiempo que queda para lanzar el próximo evento).

La TimerQueue es un demonio (daemon) que se inicia inmediatamente en la instanciación. Esto sucede


cuando se llama a TimerQueue.sharedInstance() por primera vez (cuando se inicia el primer
Timer de una sesión de Java). Espera continuamente a que expire el Timer con el tiempo más cercano.
Una vez que esto ocurre envía una señal al Timer para que envíe ActionEvents a todos sus oyentes,
entonces asigna un nuevo Timer como cabeza de la lista, y finalmente borra el que ha expirado. Si el
Timer que ha expirado está en modo de repetición, se añade de nuevo a la lista el lugar apropiado
basándose en su retraso.

Nota: La razón real por la que el ejemplo de Timer de la sección 2.4 saldría inmediatamente si no ponemos
un bucle, es que la TimerQueue es un demonio. Los demonios son hilos de servicio y cuando la JVM
sólo tiene ejecutándose demonios terminará ya que asume que no se está haciendo trabajo real.
Normalmente este comportamiento es el deseable.

Los eventos de un Timer se envían siempre al hilo de despacho de eventos de manera segura respecto a
los hilos enviando su objeto Runnable a SwingUtilities.invokeLater().

24
2.7 La arquitectura JavaBeans
Como en este libro estamos interesados en crear aplicaciones Swing, necesitamos comprender y apreciar
el hecho de que cada componente Swing sea un JavaBean.

Nota: Si es familiar con el modelo de componentes JavaBeans puede que quiera saltar a la siguiente sección.

2.7.1 El modelo de componentes JavaBeans


La especificación de JavaBeans identifica cinco rasgos que todos los beans deben ofrecer. Revisaremos
estos rasgos aquí, junto con las clases y mecanismos que los hacen posibles. La primera cosa que hay
que hacer es pensar en un componente simple, como un botón, y aplicar lo que aquí veamos a este
componente. Segundo, estamos asumiendo un conocimiento básico del API Java Reflection:
“Instancias de Class representan clases e interfaces en una aplicación Java ejecutándose.”API
“Un Method provee información de un único método de una clase o interface y acceso al mismo.”API
“Una Field provee información de un único campo de una clase o interface y acceso dinámico al
mismo.”API

2.7.2 Introspección
La introspección es la facultad de descubrir los métodos, las propiedades, y la información de los
eventos, de un bean. Esto se consigue usando la clase java.beans.Introspector.
Introspector provee métodos estáticos para generar un objeto BeanInfo que contenga toda la
información que se pueda descubrir de un bean determinado. Esto incluye información sobre todas las
superclases del bean, a no ser que especifiquemos en que superclase debe detenerse la introspección
(podemos especificar la profundidad de una introspección). El código siguiente recupera toda la
información que se puede descubrir de un bean:

BeanInfo myJavaBeanInfo =
Introspector.getBeanInfo(myJavaBean);

Un objeto BeanInfo divide toda la información del bean en varios grupos, algunos de los cuales son:

• Un BeanDescriptor: provee información general descriptiva, tal como un nombre para que se
visualice.
• Un vector de EventSetDescriptors: provee información sobre el conjunto de eventos que un
bean lanza. Estos se pueden usar, entre otras cosas, para recuperar los métodos asociados a oyentes
de eventos del bean como instancias de Method.
• Un vector de MethodDescriptors: provee información sobre los métodos accesibles
externamente de un bean (incluiría por ejemplo a todos los métodos públicos). Esta información se
usa para construir una instancia de Method para cada método.
• Un vector de PropertyDescriptors: provee información sobre las propiedades que un bean
mantiene, y que pueden accederse mediante los métodos get, set, y/o is. Estos objetos se pueden
usar para construir instancias de Method y Class correspondientes a los métodos de acceso y a los
tipos de las clases respectivamente de la propiedad.

2.7.3 Propiedades
Como vimos en la sección 2.1.1, los beans soportan diferentes tipos de propiedades. Propiedades simples
son variables que cuando se modifican, el bean no hará nada. Las propiedades ligadas y restringidas son

25
variables que cuando se modifican, el bean mandará eventos de notificación a todos los oyentes. Esta
notificación tiene la forma de un objeto de evento, que contiene el nombre de la propiedad, el valor
anterior de la propiedad, y el valor nuevo. En el momento que una propiedad ligada cambia, debería
enviar un PropertyChangeEvent. Cuando va a cambiar una propiedad restringida, el bean debería
lanzar un PropertyChangeEvent antes de que ocurra el cambio, permitiendo que éste sea vetado.
Otros objetos pueden escuchar estos eventos para procesarlos como corresponda (lo que guia la
comunicación).

Asociados con las propiedades están los métodos setXX()/getXX() e isXX() de los beans. Si un
método setXX() está disponible se dice que su propiedad asociada es escribible. Si un método
getXX() o isXX() está disponible se dice que la propiedad asociada es legible. Un método isXX()
corresponde normalmente a la obtención de un propiedad booleana (ocasionalmente los métodos
getXX() se usan para esto también).

2.7.4 Personalización
Las propiedades de un bean están expuestas a través de sus métodos setXX()/getXX() e isXX(), y
se pueden modificar en tiempo de ejecución (o en tiempo de diseño). Los JavaBeans se usan
comúnmente en entornos de desarrollo (IDE's) donde las hojas de propiedades se pueden mostrar
permitiendo que las propiedades de los beans se lean o se escriban (dependiendo de los métodos de
acceso).
2.7.5 Comunicación
Los Beans están diseñados para enviar eventos que notifican a todos los oyentes registrados con él
cuando cambia de valor una propiedad ligada o una restringida. Las apps se construyen registrando
oyentes de bean a bean. Como podemos usar la introspección para recoger información sobre el envío y
el recibo de eventos de cualquier bean, las herramientas de diseño pueden aprovechar este conocimiento
para permitir una personalización más poderosa en la etapa de diseño. La comunicación es la unión
básica que mantiene unido a un GUI interactivo.
2.7.6 Persistencia
Todos los JavaBeans tienen que implementar el interface Serializable (directa o indirectamente)
para permitir la serialización de su estado en un almacenamiento persistente (almacenamiento que existe
después de que termine el programa). Todos los objetos se guardan salvo los que se declaran como
transient. (Observe que JComponent implementa directamente este interface.)

Las clases que necesiten un procesamiento especial durante la serialización tienen que implementar los
siguientes métodos privados:

private void writeObject(java.io.ObjectOutputStream out) y


private void readObject(java.io.ObjectInputStream in)

Estos métodos se llaman para escribir o leer una instancia de esta clase de un stream. Observe que el
mecanismo de serialización por defecto será invocado para serializar todas las subclases porque se trata
de métodos privados. (Vea la documentación del API o el tutorial de Java para más información sobre la
serialización.)

Nota: Como en la primera versión de Java 2, JComponent implementa readObject() y


writeObject() como privados, todas las subclases tienen que implementar estos métodos si
requieren un procesamiento especial. Actualmente la persistencia a largo plazo no se recomienda, y es
posible que cambie en futuras versiones. Sin embargo, no hay ningún problema en implementar
persistencia a corto plazo (p.e. para RMI, transferencia de datos, etc.).

26
Las clases que quieren tener un control total sobre su serialización y deserialización deberían
implementar el interface Externalizable.

Este interface define dos métodos:

public void writeExternal(ObjectOutput out)


public void readExternal(ObjectInput in)

Estos métodos se invocarán cuando writeObject() y readObject() (vistos anteriormente) sean


invocados para llevar a cabo alguna serialización/deserialización.
2.7.7 Un JavaBean simple basado en Swing
El siguiente código nos muestra como construir un JavaBean basado en Swing con propiedades simples,
ligadas, restringidas y de cambio.

El código: BakedBean.java
ver \Chapter1\1
import javax.swing.*;
import javax.swing.event.*;
import java.beans.*;
import java.awt.*;
import java.io.*;

public class BakedBean extends JComponent implements Externalizable


{
// Nombres de la propiedad (sólo para propiedades ligadas o
restringidas)
public static final String BEAN_VALUE = "Value";
public static final String BEAN_COLOR = "Color";

// Propiedades
private Font m_beanFont; // simple
private Dimension m_beanDimension; // simple
private int m_beanValue; // ligada
private Color m_beanColor; // restringida
private String m_beanString; // de cambio

// Maneja todos los PropertyChangeListeners


protected SwingPropertyChangeSupport m_supporter =
new SwingPropertyChangeSupport(this);

// Maneja todos los VetoableChangeListeners


protected VetoableChangeSupport m_vetoer =
new VetoableChangeSupport(this);

// Sólo se necesita un ChangeEvent ya que el único estado del evento


// es la propiedad fuente. La fuente de los eventos generados
// es siempre "this". Verá esto en montones de código Swing.
protected transient ChangeEvent m_changeEvent = null;

// Esto puede manejar todos los tipos de oyentes, siempre que


configuremos
// los métodos fireXX para que miren correctamente en esta lista.
// Esto hará que aprecie las clases XXSupport.
protected EventListenerList m_listenerList =
new EventListenerList();

public BakedBean() {
m_beanFont = new Font("SanSerif", Font.BOLD | Font.ITALIC, 12);
m_beanDimension = new Dimension(150,100);

27
m_beanValue = 0;
m_beanColor = Color.black;
m_beanString = "BakedBean #";
}

public void paintComponent(Graphics g) {


super.paintComponent(g);
g.setColor(m_beanColor);
g.setFont(m_beanFont);
g.drawString(m_beanString + m_beanValue,30,30);
}

public void setBeanFont(Font font) {


m_beanFont = font;
}

public Font getBeanFont() {


return m_beanFont;
}

public void setBeanValue(int newValue) {


int oldValue = m_beanValue;
m_beanValue = newValue;

// Avisar a todos los PropertyChangeListeners


m_supporter.firePropertyChange(BEAN_VALUE,
new Integer(oldValue), new Integer(newValue));
}

public int getBeanValue() {


return m_beanValue;
}

public void setBeanColor(Color newColor)


throws PropertyVetoException {
Color oldColor = m_beanColor;

// Avisar a todos los VetoableChangeListeners antes de hacer el


cambio
// ...se lanzará una excepción si hay un veto
// ...si no continuaremos y haremos el cambio
m_vetoer.fireVetoableChange(BEAN_COLOR, oldColor, newColor);

m_beanColor = newColor;
m_supporter.firePropertyChange(BEAN_COLOR, oldColor, newColor);
}

public Color getBeanColor() {


return m_beanColor;
}

public void setBeanString(String newString) {


m_beanString = newString;
// Avisar a todos los ChangeListeners
fireStateChanged();
}

public String getBeanString() {


return m_beanString;
}

public void setPreferredSize(Dimension dim) {


m_beanDimension = dim;
}

28
public Dimension getPreferredSize() {
return m_beanDimension;
}

public void setMinimumSize(Dimension dim) {


m_beanDimension = dim;
}
public Dimension getMinimumSize() {
return m_beanDimension;
}

public void addPropertyChangeListener(


PropertyChangeListener l) {
m_supporter.addPropertyChangeListener(l);
}
public void removePropertyChangeListener(
PropertyChangeListener l) {
m_supporter.removePropertyChangeListener(l);
}

public void addVetoableChangeListener(


VetoableChangeListener l) {
m_vetoer.addVetoableChangeListener(l);
}

public void removeVetoableChangeListener(


VetoableChangeListener l) {
m_vetoer.removeVetoableChangeListener(l);
}

// Recuerde que EventListenerList es un array de


// parejas clave/valor:
// key = XXListener referencia a la clase
// value = XXListener instancia
public void addChangeListener(ChangeListener l) {
m_listenerList.add(ChangeListener.class, l);
}

public void removeChangeListener(ChangeListener l) {


m_listenerList.remove(ChangeListener.class, l);
}
// Este es el código típico de despacho de EventListenerList.
// Verá esto a menudo en código Swing.
protected void fireStateChanged() {
Object[] listeners = m_listenerList.getListenerList();
// Procesa los oyentes del último al primero, avisando
// a los que estén interesados en este evento
for (int i = listeners.length-2; i>=0; i-=2) {
if (listeners[i]==ChangeListener.class) {
if (m_changeEvent == null)
m_changeEvent = new ChangeEvent(this);
((ChangeListener)listeners[i+1]).stateChanged(m_changeEvent);
}
}
}

public void writeExternal(ObjectOutput out) throws IOException {


out.writeObject(m_beanFont);
out.writeObject(m_beanDimension);
out.writeInt(m_beanValue);
out.writeObject(m_beanColor);

29
out.writeObject(m_beanString);
}

public void readExternal(ObjectInput in)


throws IOException, ClassNotFoundException {
setBeanFont((Font)in.readObject());
setPreferredSize((Dimension)in.readObject());
// Usar el tamaño preferido para el mínimo..
setMinimumSize(getPreferredSize());
setBeanValue(in.readInt());
try {
setBeanColor((Color)in.readObject());
}
catch (PropertyVetoException pve) {
System.out.println("Color change vetoed..");
}
setBeanString((String)in.readObject());
}

public static void main(String[] args) {


JFrame frame = new JFrame("BakedBean");
frame.getContentPane().add(new BakedBean());
frame.setVisible(true);
frame.pack();
}
}

BakedBean tiene representación visual (no es obligatorio para un bean). Tiene las propiedades:
m_beanValue, m_beanColor, m_beanFont, m_beanDimension, y m_beanString. Soporta
persistencia implementando el interface Externalizable y los métodos writeExternal() y
readExternal() para controlar su propia serialización (observe que el orden en el que se escriben y
se leen los datos coincide). BakedBean soporta personalización mediante sus métodos setXX() y
getXX(), y soporta comunicación permitiendo el registro de PropertyChangeListeners,
VetoableChangeListeners, y ChangeListeners. Y, sin tener que hacer nada especial, soporta
introspección.

Utilizar un método para mostrar BakedBean en un frame no está dentro de la funcionalidad de


JavaBeans. La Figura 2.1 muestra BakedBean siendo ejecutado como una aplicación.

Figura 2.1 BakedBean en nuestro editor personal de JavaBeans


<<fichero figure2-1.gif>>

En el capítulo 18 (sección 18.9) construiremos en entorno completo de edición de propiedades de


JavaBeans. La Figura 2.2 muestra una instancia de BakedBean en este entorno. Al BakedBean
mostrado se le han modificado las propiedades m_beanDimension, m_beanColor, y m_beanValue
con nuestro editor de propiedades y se ha serializado al disco. Lo que realmente muestra la Figura 2.2 es
una instancia de ese BakedBean después de que ha sido deserializado (cargado desde el disco). Observe
que cualquier componente Swing puede ser creado, modificado, serializado y deserializado usando este
entorno ya que todos ellos cumplen las especificaciones de los JavaBeans

30
Figura 2.2 BakedBean en nuestro editor de propiedades de JavaBeans personal
<<fichero figure2-2.gif>>

2.8 Fuentes, Colores, Gráficos y texto


2.8.1 Fuentes

clase java.awt.Font, abstract class java.awt.GraphicsEnvironment


Como vimos anteriormente en BakedBean, las fuentes son muy fáciles de crear:

m_beanFont = new Font("SanSerif", Font.BOLD | Font.ITALIC, 12);

En este código "SanSerif" es el nombre de la fuente, Font.Bold | Font.PLAIN es el estilo (que


en este caso es negrita (bold) y cursiva (italic)), y 12 es el tamaño. La clase Font define tres contantes
estáticas de tipo int para indicar el estilo: Font.BOLD, Font.ITALIC, FONT.PLAIN. Podemos
especificar el tamaño de la fuente con un int en el constructor de Font. Usando Java 2, para obtener
una lista de los nombres de fuentes disponibles en tiempo de ejecución, podemos preguntar al
GraphicsEnvironment local:

GraphicsEnvironment ge = GraphicsEnvironment.
getLocalGraphicsEnvironment();
String[] fontNames = ge.getAvailableFontFamilyNames();

Nota: Java 2 introduce un nuevo, poderoso y completo mecanismo para comunicarse con dispositivos que
pueden dibujar gráficos, como pantallas o impresoras. Estos dispositivos se representan como instancias
de la clase GraphicsDevice. Es interesante, que un GraphicsDevice puede estar en la máquina
local o en una remota. Cada GraphicsDevice tiene un conjunto de objetos
GraphicsConfiguration asociados con él. Una GraphicsConfiguration describe
características específicas del dispositivo asociado. Normalmente cada GraphicsConfiguration
de un GraphicsDevice representa un modo de operación diferente (por ejemplo resolución y número
de colores).

Nota: En código para el JDK1.1, para obtener la lista de nombres de fuentes había que usar el código
siguiente:
String[] fontnames = Toolkit.getDefaultToolkit().getFontList();
El método Toolkit.getFontList() se ha desaconsejado en Java 2 y este código debería actualizarse.

31
GraphicsEnvironment es una clase abstracta que describe una colección de GraphicsDevices.
Las subclases de GraphicsEnvironment deben tener tres métodos para obtener arrays de Fonts e
información de Font:

Font[] getAllFonts(): obtiene todas las Fonts disponibles en tamaño de un punto.


String[] getAvailableFontFamilyNames(): obtiene los nombres de todas las familias de
fuentes disponibles.
String[] getAvailableFontFamilyNames(Locale l): obtiene los nombres de todas las
familias de fuentes disponibles usando el Locale (soporte a la internationalización) especificado.

GraphicsEnvironment también tiene métodos estáticos para recuperar GraphicsDevices y la


instancia local de GraphicsEnvironment. Para encontrar que Fonts están disponibles para el
sistema en el que se está ejecutando nuestro programa, debemos usar esta instancia local de
GraphicsEnvironment como vimos antes. Es mucho más eficiente y conveniente obtener los
nombres de fuentes disponibles y usarlos para construir Fonts que obtener el array de objetos Font.

Podríamos pensar que, dado un objeto Font, podemos usar los métodos típicos de acceso
getXX()/setXX() para cambiar su nombre, estilo y tamaño. Bueno, sólo habríamos acertado a
medias. Podemos usar los métodos getXX() para obtener esta información de una Font:

String getName()
int getSize()
float getSize2D()
int getStyle

Sin embargo, no podemos usar los métodos setXX(). En su lugar debemos usar uno de los siguientes
métodos de instancia de Font para conseguir una nueva Font:

deriveFont(float size)
deriveFont(int style)
deriveFont(int style, float size)
deriveFont(Map attributes)
deriveFont(AffineTransform trans)
deriveFont(int style, AffineTransform trans)

Normalmente estaremos interesados solamente en los tres primeros métodos.

Nota: AffineTransforms se usan en el mundo de Java 2D para llevar a cabo cosas como translaciones,
escalado, rotaciones, reflejado y recortes. Un Map es un objeto que mapea claves a valores (no contiene
los objetos involucrados) y los atributos a los que nos referimos aquí son parejas clave/valor como se
describe en los documentos del API de java.text.TextAttribute (esta clase está definida en el
paquete java.awt.font que es nuevo en Java 2, y que se considera parte de Java 2D -- ver capítulo
23).

2.8.2 Colores
La clase Color tiene varias instancias estáticas de Color para ser usadas por comodidad (p.e.
Color.blue, Color.yellow, etc.). Podemos construir también un Color usando, entre otros, los
siguientes constructores:

Color(float r, float g, float b)


Color(int r, int g, int b)

32
Color(float r, float g, float b, float a)
Color(int r, int g, int b, int a)

Normalmente usamos los dos primeros métodos, y aquellos familiarizados con el JDK1.1 los
reconocerán. El primero permite especificar los valores de rojo, azul y verde como floats de 0.0 a 1.0.
El segundo toma estos valores como ints de 0 a 255.

Los segundos dos métodos son nuevos en Java 2. Ambos tienen un cuarto parámetro que representa el
valor alpha del Color. El valor alpha controla directamente la transparencia. Por defecto es 1.0 o 255
que corresponde a completamente opaco. 0.0 o 0 significa totalmente transparente.

Observe que, como con las Fonts, hay un montón de métodos de acceso getXX() pero no de
setXX(). En lugar de modificar un objeto Color es más normal que creemos uno nuevo.

Nota: La clase Color tiene los métodos estáticos brighter() y darker() que devuelven un Color
más claro (brighter) o más oscuro (darker) que el Color especificado, pero su comportamiento es
impredecible a causa de errores internos de redondeo y sugerimos no usarlos.

Especificando un valor alpha podemos usar el Color resultante como fondo de un componente para
hacerlo transparente. Esto funcionará para cualquier componente ligero que Swing provea como
etiquetas, componentes de texto, frames internos, etc. Por supuesto habrá cuestiones específicas de cada
componente involucradas (como hacer transparentes el borde y la barra de título de un frame interno
transparentes). La siguiente sección muestra un ejemplo simple de canvas, que muestra como usar el
valor alpha para mostrar alguna superficie transparente.

Nota: La propiedad de opacidad de un componente Swing, controlada mediante setOpaque(), no está


relacionada directamente con la transparencia de Color. Por ejemplo, si tenemos una JLabel opaca,
cuyo fondo se ha puesto a un verde transparente (p.e. Color(0,255,0,150)) Los límites de la
etiqueta se pintarán sólo porque es opaca. Seremos capaces de ver a través de ella sólo porque el color es
transparente. Si quitamos la opacidad, el fondo de la etiqueta no se dibujará. Ambas cosas se tienen que
usar conjuntamente para crear componentes transparentes, pero no están directamente relacionadas.

2.8.3 Gráficos y texto

clase abstracta java.awt.Graphics, clase abstracta java.awt.FontMetrics


Dibujar en Swing es muy distinto a hacerlo en AWT. En AWT sobrescribimos normalmente el método
paint() de Component para dibujar y el método update() para otras cosas como implementar
nuestro propio doble buffer o rellenar el fondo antes de llamar a paint().

Con Swing, el dibujo de componentes es mucho más complejo. Como JComponent es una subclase de
Component, usa los métodos update() y paint() por diferentes razones. De hecho, no se invoca
nunca al método update() para nada. Hay cinco pasos adicionales en el pintado que normalmente se
desarrollan dentro del método paint(). Veremos este proceso en la sección 2.11, pero basta con decir
aquí que cualquier subclase de JComponent que quiera tener el control de su propio dibujado debería
sobrescribir el método paintComponent() y no el método paint(). Adicionalmente, debería
empezar siempre su método paintComponent() con una llamada a super.paintComponent().

Sabiendo esto, es bastante fácil construir un JComponent que actúe como nuestro propio canvas ligero.
Todo lo que tenemos que hacer es escribir una subclase y sobreescribir el método
paintComponent(). Dentro de ese método podemos hacer todo nuestro pintado. Así es como
tomamos control del dibujado de nuestros simples componentes personalizados. De todos modos, esto

33
no se debería intentar con los componentes Swing normales porque los delegados UI están a cargo de su
dibujado (veremos como personalizar el dibujado en el delegado UI al final del capítulo 6, y durante el
capítulo 21).

Nota: La clase de awt Canvas se puede reemplazar por una versión simplificada de la clase JCanvas que
definiremos en el siguiente ejemplo.

Dentro del método paintComponent() tenemos acceso al objeto Graphics de ese componente (a
menudo denominado el contexto gráfico del componente) que podemos utilizar para pintar superficies y
dibujar líneas y texto. La clase Graphics define una gran cantidad de métodos que se usan para estos
propósitos, y es conveniente que mire los documentos del API. El código siguiente muestra como
construir una subclase de Component que pinta un ImageIcon y algunas superficies y texto usando
diferentes Fonts y Colors, algunas completamente opacos y otros parcialmente transparentes (vimos
una funcionalidad parecida pero menos interesante en BakedBean).

Figura 2.3 Demostración de Graphics en un canvas ligero.


<<fichero figure2-3.gif>>

El Código: TestFrame.java
ver \Chapter1\2

import java.awt.*;
import javax.swing.*;

class TestFrame extends JFrame


{
public TestFrame() {

34
super( "Graphics demo" );
getContentPane().add(new JCanvas());
}

public static void main( String args[] ) {


TestFrame mainFrame = new TestFrame();
mainFrame.pack();
mainFrame.setVisible( true );
}
}

class JCanvas extends JComponent {


private static Color m_tRed = new Color(255,0,0,150);
private static Color m_tGreen = new Color(0,255,0,150);
private static Color m_tBlue = new Color(0,0,255,150);

private static Font m_biFont =


new Font("Monospaced", Font.BOLD | Font.ITALIC, 36);
private static Font m_pFont =
new Font("SanSerif", Font.PLAIN, 12);
private static Font m_bFont = new Font("Serif", Font.BOLD, 24);

private static ImageIcon m_flight = new ImageIcon("flight.gif");

public JCanvas() {
setDoubleBuffered(true);
setOpaque(true);
}

public void paintComponent(Graphics g) {


super.paintComponent(g);

// pinta todo el componente de blanco


g.setColor(Color.white);
g.fillRect(0,0,getWidth(),getHeight());
// pinta un círculo amarillo
g.setColor(Color.yellow);
g.fillOval(0,0,240,240);

// pinta un círculo magenta


g.setColor(Color.magenta);
g.fillOval(160,160,240,240);

// pinta el icono de debajo del cuadrado azul


int w = m_flight.getIconWidth();
int h = m_flight.getIconHeight();
m_flight.paintIcon(this,g,280-(w/2),120-(h/2));

// pinta el icono de debajo del cuadrado rojo


m_flight.paintIcon(this,g,120-(w/2),280-(h/2));

// pinta un cuadrado rojo transparente


g.setColor(m_tRed);
g.fillRect(60,220,120,120);

// pinta un círculo verde transparente


g.setColor(m_tGreen);
g.fillOval(140,140,120,120);

// pinta un cuadrado azul transparente


g.setColor(m_tBlue);
g.fillRect(220,60,120,120);

g.setColor(Color.black);

35
// Negrita, Cursiva, 36-puntos "Swing"
g.setFont(m_biFont);
FontMetrics fm = g.getFontMetrics();
w = fm.stringWidth("Swing");
h = fm.getAscent();
g.drawString("Swing",120-(w/2),120+(h/4));

// Normal, 12-puntos "is"


g.setFont(m_pFont);
fm = g.getFontMetrics();
w = fm.stringWidth("is");
h = fm.getAscent();
g.drawString("is",200-(w/2),200+(h/4));

// Negrita 24-puntos "powerful!!"


g.setFont(m_bFont);
fm = g.getFontMetrics();
w = fm.stringWidth("powerful!!");
h = fm.getAscent();
g.drawString("powerful!!",280-(w/2),280+(h/4));
}

// Algunos administradores de disposición necesitan esta información


public Dimension getPreferredSize() {
return new Dimension(400,400);
}

public Dimension getMinimumSize() {


return getPreferredSize();
}

public Dimension getMaximumSize() {


return getPreferredSize();
}
}

Observe que sobrescribimos los métodos de JComponent getPreferredSize(),


getMinimumSize(), y getMaximumSize(), para que algunos administradores de disposición
puedan dimensionar este componente (de otra manera alguno pondría su tamaño a 0x0). Es siempre una
buena práctica la sobreescritura de estos métodos cuando se implementan componentes personalizados.

La clase Graphics usa lo que se llama el área de recorte (clipping area). Dentro del método paint()
de un componente, esta es la región de la vista del componente que se está repintando. Sólo el dibujo
hecho dentro de los límites del área de recorte será dibujado en el momento. Podemos obtener el tamaño
y la posición de estos límites llamando a getClipBounds() que nos devuelve una instancia de
Rectangle describiéndola. La razón por la que se usa el área de recorte es por eficiencia: no hay
motivo para pintar regiones invisibles cuando no tenemos que hacerlo. (Mostraremos como extender este
ejemplo para trabajar con el área de recorte para una mayor eficiencia en la próxima sección).

Nota: Todos los componentes Swing tienen doble buffer por defecto. Si estamos construyendo nuestro propio
canvas ligero no tenemos que preocuparnos por el doble buffer. Este no es el caso con un Canvas de
AWT.

Como mencionamos antes, la manipulación de Fonts y Font es muy compleja. Estamos viendo su
estructura, pero una cosa que deberíamos saber es como obtener información útil sobre las fuentes y el
texto dibujado al usarlas. Esto implica el uso de la clase FontMetrics. En el ejemplo anterior,
FontMetrics nos permitió determinar la anchura y la altura de tres Strings, dibujados en la Font
actual asociada con el objeto Graphics, de forma que pudimos dibujarlos centrados en los círculos.

36
La Figura 2.4 ilustra algunas de las informaciones más comunes que podemos obtener de un objeto
FontMetrics. El significado de base (baseline), subida (ascent), bajada (descent), y altura (height)
debería quedar claro con el diagrama. La subida es la distancia de la base hasta lo más alto de la mayoría
de las letras de la fuente. Observe que cuando usamos g.drawString() para dibujar texto, las
coordenadas especificadas representan la posición de la base del primer carácter.

FontMetrics ofrece varios métodos para obtener esta información y otras más detalladas, como la
anchura de un String dibujado en la Font asociada.

Figura 2.4 Usando FontMetrics


<<fichero figure2-4.gif>>

Para obtener una instancia de FontMetrics llamamos primero a nuestro objeto Graphics para que
use la Font que queremos examinar usando el método setFont(). Creamos entonces la instancia de
FontMetrics llamando a getFontMetrics() en nuestro objeto Graphics:

g.setFont(m_biFont);
FontMetrics fm = g.getFontMetrics();

Una operación típica cuando dibujamos texto es centrarlo en un punto determinado. Suponga que
queremos centrar el texto “Swing” en 200,200. Aquí está el código que deberíamos usar (asumiendo que
hemos recuperado el objeto FontMetrics, fm, como se mostró anteriormente):

int w = fm.stringWidth("Swing");
int h = fm.getAscent();
g.drawString("Swing",200-(w/2),200+(h/4));

Obtenemos la anchura de “Swing” en la fuente actual, la dividimos para dos, y se la restamos a 200 para
centrar el texto horizontalmente. Para centrarlo verticalmente obtenemos la subida de la fuente actual, la
dividimos para cuatro y se la añadimos a 200. La razón por la que dividimos la subida para cuatro NO
está probablemente muy clara.

Ahora es el momento de acometer un error común que ha llegado con Java 2. La figura 2.4 no es una
forma exacta de documentar FontMetrics. Estas es la forma de la que hemos visto documentadas
estas cosas en el tutorial Java y en casi todos los demás sitios. De todas formas, parece que hay unos
pocos problemas con FontMetrics en Java 2 FCS. Aquí escribiremos un programa simple que
demuestra estos problemas. Nuestro programa dibujará el texto “Swing” con una fuente de 36 puntos,
negrita y monospaced. Dibujamos líneas en su subida, subida/2, subida/4, base, y bajada. La Figura 2.5
muestra el resultado.

37
Figura 2.5 La realidad cuando se trabaja con FontMetrics en Java 2
<<fichero figure2-5.gif>>

El Código: TestFrame.java
Ver \Chapter1\2\fontmetrics

import java.awt.*;
import javax.swing.*;

class TestFrame extends JFrame


{
public TestFrame() {
super( "Lets get it straight!" );
getContentPane().add(new JCanvas());
}

public static void main( String args[] ) {


TestFrame mainFrame = new TestFrame();
mainFrame.pack();
mainFrame.setVisible( true );
}
}

class JCanvas extends JComponent


{
private static Font m_biFont = new Font("Monospaced", Font.BOLD, 36);

public void paintComponent(Graphics g) {


g.setColor(Color.black);

// Negrita 36-puntos "Swing"


g.setFont(m_biFont);
FontMetrics fm = g.getFontMetrics();
int h = fm.getAscent();

g.drawString("Swing",50,50); // Prueba también: Ñ Ö Ü ^


// dibujar la línea de Subida
g.drawLine(10,50-h,190,50-h);

// dibujar la línea de Subida/2


g.drawLine(10,50-(h/2),190,50-(h/2));

// dibujar la línea de Subida/4


g.drawLine(10,50-(h/4),190,50-(h/4));
// dibujar la línea de Base
g.drawLine(10,50,190,50);

// dibujar la línea de Bajada


g.drawLine(10,50+fm.getDescent(),190,50+fm.getDescent());
}

38
public Dimension getPreferredSize() {
return new Dimension(200,100);
}
}

Le aconsejamos que pruebe este programa con diferentes tipos de fuente, tamaños, y con caracteres con
marcas diacríticas como Ñ, Ö, o Ü. Observará que la subida es siempre mucho mayor de lo que
normalmente está documentado que sería, y que la bajada es siempre menor. La forma más fiable de
centrar el texto verticalmente que hemos encontrado es utilizar base + subida/4. Aún así, se puede usar
también base + bajada y dependiendo de la fuente que se use puede ser más ajustado.

La realidad es que no hay una forma de llevar esto a cabo correctamente a causa del estado actual de
FontMetrics en Java 2.Puede experimentar resultados muy diferentes si no está usando la primera
versión de Java 2. Es una buena idea ejecutar este programa y verificar si los resultados en su sistema
son similares o no a los de la figura 2.5. Si no, será mejor que use un mecanismo de centrado diferente
para su texto que debería ser simple de determinar mediante la experimentación con esta aplicación.

Nota: En el JDK1.1, para obtener una instancia de FontMetrics había que hacer lo siguiente:
FontMetrics fm = Toolkit.getDefaultToolkit().getFontMetrics(myfont);
El método Toolkit.getFontMetrics está desaconsejado en Java 2 y este código debería ser
actualizado.

2.9 Usando el área de recorte de Graphics


Podemos usar el área de recorte para optimizar el dibujo de componentes. Esto puede que no mejore
notablemente la velocidad de dibujado de componentes simples como nuestro JCanvas anterior, pero
es importante comprender como implementar esta funcionalidad, ya que todo el sistema de dibujo de
Swing está basado en este concepto (veremos más sobre esto en la siguiente sección).

Modificamos ahora JCanvas para que cada una de nuestras superficies, strings e imágenes se pinte
solamente si el área de recorte intersecciona con el rectángulo que lo limita. (Estas intersecciones son
bastante fáciles de calcular, y podría ser útil que trabajase con ellas y las verificase una a una.)
Adicionalmente, mantenemos un contador local que se incrementa cada vez que se pinta una de nuestros
ítems. Al finalizar el método paintComponent() mostramos el número total de ítems que se pintaron.
A continuación está nuestro método optimizado paintComponent() de JCanvas (con contador):

El Código: JCanvas.java
ver \Chapter1\3

public void paintComponent(Graphics g) {


super.paintComponent(g);

// contador
int c = 0;

// para usarse a continuación


int w = 0;
int h = 0;
int d = 0;

// obtener área de recorte


Rectangle r = g.getClipBounds();
int clipx = r.x;
int clipy = r.y;
int clipw = r.width;

39
int cliph = r.height;

// dibujar sólo el área de recorte


g.setColor(Color.white);
g.fillRect(clipx,clipy,clipw,cliph);

// dibujar el círculo amarillo si está dentro del área de recorte


if (clipx <= 240 && clipy <= 240) {
g.setColor(Color.yellow);
g.fillOval(0,0,240,240); c++;
}

// dibujar el círculo magenta si está dentro del área de recorte


if (clipx + clipw >= 160 && clipx <= 400
&& clipy + cliph >= 160 && clipy <= 400) {
g.setColor(Color.magenta);
g.fillOval(160,160,240,240); c++;
}

w = m_flight.getIconWidth();
h = m_flight.getIconHeight();

// pintar el icono de debajo del cuadrado azul si está dentro del


// área de recorte
if (clipx + clipw >= 280-(w/2) && clipx <= (280+(w/2))
&& clipy + cliph >= 120-(h/2) && clipy <= (120+(h/2))) {
m_flight.paintIcon(this,g,280-(w/2),120-(h/2)); c++;
}

// pintar el icono de debajo del cuadrado rojo si está dentro del


// área de recorte
if (clipx + clipw >= 120-(w/2) && clipx <= (120+(w/2))
&& clipy + cliph >= 280-(h/2) && clipy <= (280+(h/2))) {
m_flight.paintIcon(this,g,120-(w/2),280-(h/2)); c++;
}
// dibujar el cuadrado rojo transparente si está dentro del área de
// recorte
if (clipx + clipw >= 60 && clipx <= 180
&& clipy + cliph >= 220 && clipy <= 340) {
g.setColor(m_tRed);
g.fillRect(60,220,120,120); c++;
}

// dibujar el círculo verde transparente si está dentro del área de


// recorte
if (clipx + clipw > 140 && clipx < 260
&& clipy + cliph > 140 && clipy < 260) {
g.setColor(m_tGreen);
g.fillOval(140,140,120,120); c++;
}

// dibujar el cuadrado azul transparente si está dentro del área de


// recorte
if (clipx + clipw > 220 && clipx < 380
&& clipy + cliph > 60 && clipy < 180) {
g.setColor(m_tBlue);
g.fillRect(220,60,120,120); c++;
}

g.setColor(Color.black);

g.setFont(m_biFont);
FontMetrics fm = g.getFontMetrics();
w = fm.stringWidth("Swing");

40
h = fm.getAscent();
d = fm.getDescent();
// Negrita, Cursiva, 36-puntos "Swing" si está dentro del área de
// recorte
if (clipx + clipw > 120-(w/2) && clipx < (120+(w/2))
&& clipy + cliph > (120+(h/4))-h && clipy < (120+(h/4))+d)
{
g.drawString("Swing",120-(w/2),120+(h/4)); c++;
}

g.setFont(m_pFont);
fm = g.getFontMetrics();
w = fm.stringWidth("is");
h = fm.getAscent();
d = fm.getDescent();
// Normal, 12-puntos "is" si está dentro del área de recorte
if (clipx + clipw > 200-(w/2) && clipx < (200+(w/2))
&& clipy + cliph > (200+(h/4))-h && clipy < (200+(h/4))+d)
{
g.drawString("is",200-(w/2),200+(h/4)); c++;
}

g.setFont(m_bFont);
fm = g.getFontMetrics();
w = fm.stringWidth("powerful!!");
h = fm.getAscent();
d = fm.getDescent();
// Negrita 24-puntos "powerful!!" si está dentro del área de recorte
if (clipx + clipw > 280-(w/2) && clipx < (280+(w/2))
&& clipy + cliph > (280+(h/4))-h && clipy < (280+(h/4))+d)
{
g.drawString("powerful!!",280-(w/2),280+(h/4)); c++;
}

System.out.println("# items repainted = " + c + "/10");


}

Pruebe a ejecutar este ejemplo desplazando otra ventana de su escritorio sobre partes del JCanvas.
Mantenga la consola a la vista de forma que pueda monitorizar cuantos ítems se dibujan en cada
repintado. Su salida debería mostrar algo como lo siguiente (por supuesto, probablemente verá otros
números diferentes):

# items repainted = 4/10


# items repainted = 0/10
# items repainted = 2/10
# items repainted = 2/10
# items repainted = 1/10
# items repainted = 2/10
# items repainted = 10/10
# items repainted = 10/10
# items repainted = 8/10
# items repainted = 4/10

Optimizar este canvas no fue difícil, pero imagine como sería optimizar un contenedor con un número
variable de hijos, que probablemente se superponen, con doble buffer y trasparencia. Esto es lo que hace
JComponent, y lo hace bastante eficientemente. Aprenderemos un poco más sobre como se hace esto
en la sección 2.11. Pero primero terminaremos con nuestro vistazo de alto nivel a los gráficos
introduciendo una funcionalidad nueva de Swing muy poderosa: la depuración de gráficos.

2.10 Depuración de gráficos


La depuración de gráficos nos ofrece la posibilidad de observar todas las operaciones de pintado durante

41
el dibujo de un componente y de todos sus hijos. Esto se consigue con un cambio lento, usando distintos
destellos para indicar la región que se está pintando. Se intenta ayudar a encontrar problemas con el
dibujo, la disposición, y las jerarquías de componentes -- y con cualquier cosa relacionada. Si está
habilitada la depuración de gráficos, el objeto Graphics que se usa cuando se pinta es una instancia de
DebugGraphics (una subclase de Graphics). JComponent, y por tanto todos los componente
Swing, soporta la depuración de gráficos, que se puede activar/desactivar con el método
setDebugGraphicsOptions() de JComponent. Este método recibe un int como parámetro que
corresponde normalmente a uno de (o una combinación de bits -- usando el operador binario | ) los
cuatro valores estáticos definidos en DebugGraphics.
2.10.1 Opciones de la depuración de gráficos
1. DebugGraphics.FLASH_OPTION: Cada operación de pintado produce un número determinado de
destellos, de un determinado color y con un intervalo especificado. Los valores por defecto son: 250ms
como intervalo, 4 destellos, y color rojo. Estos valores se pueden modificar con los siguientes métodos
estáticos de DebugGraphics:

setFlashTime(int flashTime)
setFlashCount(int flashCount)
setFlashColor(Color flashColor)

Si no deshabilitamos el doble buffer mediante el RepaintManager (que veremos en la siguiente


sección) no veremos el pintado en el momento en que ocurre:

RepaintManager.currentManager(null).
setDoubleBufferingEnabled(false);

Nota: Desactivar el doble buffer en el RepaintManager tiene el efecto de ignorar la propiedad


doubleBuffered de todos los componentes.

2. DebugGraphics.LOG_OPTION: Envía mensajes describiendo todas las operaciones de pintado


cuando ocurren. Por defecto estos mensajes se envían a la salida estándar (la consola -- System.out).
Pero podemos cambiar el destino con el método estático setLogStream() de DebugGraphics. Este
método recibe un PrintStream como parámetro. Para enviar la salida a un fichero haríamos algo
como lo siguiente:

PrintStream debugStream = null;


try {
debugStream = new PrintStream(
new FileOutputStream("JCDebug.txt"));
}
catch (Exception e) {
System.out.println("can't open JCDebug.txt..");
}
DebugGraphics.setLogStream(debugStream);

Si en algún punto tenemos que redirigir la salida de nuevo hacia la salida estándar:

DebugGraphics.setLogStream(System.out);

Podemos insertar cualquier cadena obteniendo el stream de salida con el método estático logStream()
de DebugGraphics, e imprimiendo en él:

PrintStream ps = DebugGraphics.logStream();
ps.println("\n===> paintComponent ENTERED <===");

42
Peligro: Escribir un registro a un fichero sobrescribirá el mismo cada vez que se reinicialice el stream.

Todas las operaciones se imprimen con la siguiente sintaxis:

"Graphics" + (isDrawingBuffer() ? "<B>" : "") +


"(" + graphicsID + "-" + debugOptions + ")"

Todas las líneas empiezan con “Graphics”. El método isDrawingBuffer() nos dice si está habilitado
el buffer. Si lo está, se añade “<B>”. Los valores de graphicsID y de debugOptions se ponen entre
paréntesis y separados con un “-”. El valor de graphicsID representa el número de instancias de
DebugGraphics que se han creado durante la vida de la aplicación (p.e. es un contador de tipo int
estático). El valor de debugOptions representa el modo de depurado actual:

LOG_OPTION = 1
LOG_OPTION y FLASH_OPTION = 3
LOG_OPTION y BUFFERED_OPTION = 5
LOG_OPTION, FLASH_OPTION, y BUFFERED_OPTION = 7

Por ejemplo, con el registro y los destellos habilitados, vemos una salida parecida a esta para todas las
operaciones:

Graphics(1-3) Setting color: java.awt.Color[r=0,g=255,b=0]

Las llamadas a los métodos de Graphics se añadirán al registro cuando está opción esté habilitada. La
línea anterior se generó al hacerse una llamada a setColor().

3. DebugGraphics.BUFFERED_OPTION: Se supone que desplegará un frame mostrando el dibujado


tal como ocurre en el buffer invisible si está habilitado el doble-buffer. En 2 FCS esta opción no es
funcional.

4. DebugGraphics.NONE_OPTION: Apaga la depuración de gráficos.


2.10.2 Advertencias sobre la depuración de gráficos
Hay varios asuntos con los que hay que tener cuidado cuando usamos la depuración de gráficos:

1. La depuración de gráficos no funcionará para cualquier componente cuyo UI sea null. Por tanto, si
ha creado una subclase directa de JComponent sin un delegado UI, como hicimos anteriormente con
JCanvas, la depuración de gráficos no hará nada. La forma más simple de evitar esto es definir un
delegado UI trivial (vacío). Veremos como hacer esto con un ejemplo más tarde.

2. DebugGraphics no limpia cuando termina. Por defecto, se usa un color rojo para los destellos.
Cuando se marca una región, ésta se rellena con ese color rojo del destello y no se borra (simplemente se
pinta encima). Esto supone un problema porque el dibujo transparente no se mostrará transparente. En
cambio, se fusionará con el rojo de abajo (o con cualquiera que sea el color del destello). Esto no supone
necesariamente un defecto de diseño ya que nada nos impide usar un color completamente transparente
para los destellos. Con un valor alpha de 0 el color del destello no se verá nunca. El único problema de
esto es que no veremos ningún destello. De todos modos, en la mayoría de los casos es fácil de seguir lo
que se está dibujando si ponemos flashTime y flashCount de forma que haya bastante tiempo entre
operaciones.
2.10.3 Usando la depuración de gráficos
Ahora habilitaremos la depuración de gráficos en nuestro ejemplo de JCanvas de las dos últimas
secciones. Como tenemos que tener un delegado UI, definimos una subclase trivial de ComponentUI e
implementamos su método createUI() para que devuelva una instancia estática de si mismo:

43
class EmptyUI extends ComponentUI
{
private static final EmptyUI sharedInstance = new EmptyUI();

public static ComponentUI createUI(JComponent c) {


return sharedInstance;
}
}

Para asociar adecuadamente este delegado UI con JCanvas, simplemente llamamos a


super.setUI(EmptyUI.createUI(this)) desde el constructor de JCanvas. Configuramos
también una variable de tipo PrintStream en JCanvas y la usamos para añadir unas pocas líneas
propias al stream de registro durante el método paintComponent (para guardar cuando empieza y
termina el método). No se ha hecho ningún otro cambio en el código paintComponent() de
JCanvas.

En nuestra aplicación de prueba, TestFrame, creamos una instancia de JCanvas y habilitamos la


depuración de gráficos con las opciones LOG_OPTION y FLASH_OPTION. Deshabilitamos el doble
buffer con RepaintManager, ponemos el intervalo de los destellos a 100ms, el número de éstos a 2, y
usamos un color totalmente transparente.

El Código: TestFrame.java
ver \Chapter1\4

import java.awt.*;
import javax.swing.*;
import javax.swing.plaf.*;
import java.io.*;

class TestFrame extends JFrame


{
public TestFrame() {
super( "Graphics demo" );
JCanvas jc = new JCanvas();
RepaintManager.currentManager(jc).
setDoubleBufferingEnabled(false);
jc.setDebugGraphicsOptions(DebugGraphics.LOG_OPTION |
DebugGraphics.FLASH_OPTION);
DebugGraphics.setFlashTime( 100 );
DebugGraphics.setFlashCount( 2 );
DebugGraphics.setFlashColor(new Color(0,0,0,0));
getContentPane().add(jc);
}

public static void main( String args[] ) {


TestFrame mainFrame = new TestFrame();
mainFrame.pack();
mainFrame.setVisible( true );
}
}
class JCanvas extends JComponent
{
// Código de la sección 2.9 intacto

private PrintStream ps;

public JCanvas() {
super.setUI(EmptyUI.createUI(this));
}

44
public void paintComponent(Graphics g) {
super.paintComponent(g);

ps = DebugGraphics.logStream();
ps.println("\n===> paintComponent ENTERED <===");

// Todo el código de pintado intacto

ps.println("\n# items repainted = " + c + "/10");


ps.println("===> paintComponent FINISHED <===\n");
}

// Código de la sección 2.9 intacto


}

class EmptyUI extends ComponentUI


{
private static final EmptyUI sharedInstance = new EmptyUI();

public static ComponentUI createUI(JComponent c) {


return sharedInstance;
}
}

Poniendo LOG_OPTION, la depuración de gráficos nos ofrece una mejor información para verificar
correctamente como funciona nuestra optimización del área de recorte (de la última sección). Cuando se
ejecuta este ejemplo se verá la siguiente salida en su consola (suponiendo que no tape la región visible
de JCanvas cuando se pinta por primera vez):

Graphics(0-3) Enabling debug


Graphics(0-3) Setting color:
javax.swing.plaf.ColorUIResource[r=0,g=0,b=0]
Graphics(0-3) Setting font:
javax.swing.plaf.FontUIResource[family=dialog,name=Dialog,
style=plain,size=12]

===> paintComponent ENTERED <===


Graphics(1-3) Setting color: java.awt.Color[r=255,g=255,b=255]
Graphics(1-3) Filling rect: java.awt.Rectangle[x=0,y=0,
width=400,height=400]
Graphics(1-3) Setting color: java.awt.Color[r=255,g=255,b=0]
Graphics(1-3) Filling oval: java.awt.Rectangle[x=0,y=0,
width=240,height=240]
Graphics(1-3) Setting color: java.awt.Color[r=255,g=0,b=255]
Graphics(1-3) Filling oval:
java.awt.Rectangle[x=160,y=160,width=240,height=240]
Graphics(1-3) Drawing image: sun.awt.windows.WImage@32a5625a at:
java.awt.Point[x=258,y=97]
Graphics(1-3) Drawing image: sun.awt.windows.WImage@32a5625a at:
java.awt.Point[x=98,y=257]
Graphics(1-3) Setting color: java.awt.Color[r=255,g=0,b=0]
Graphics(1-3) Filling rect:
java.awt.Rectangle[x=60,y=220,width=120,height=120]
Graphics(1-3) Setting color: java.awt.Color[r=0,g=255,b=0]
Graphics(1-3) Filling oval:
java.awt.Rectangle[x=140,y=140,width=120,height=120]
Graphics(1-3) Setting color: java.awt.Color[r=0,g=0,b=255]
Graphics(1-3) Filling rect:
java.awt.Rectangle[x=220,y=60,width=120,height=120]
Graphics(1-3) Setting color: java.awt.Color[r=0,g=0,b=0]
Graphics(1-3) Setting font:
java.awt.Font[family=monospaced.bolditalic,name=Mono

45
spaced,style=bolditalic,size=36]
Graphics(1-3) Drawing string: "Swing" at:
java.awt.Point[x=65,y=129]
Graphics(1-3) Setting font:
java.awt.Font[family=Arial,name=SanSerif,style=plain,size=12]
Graphics(1-3) Drawing string: "is" at:
java.awt.Point[x=195,y=203]
Graphics(1-3) Setting font:
java.awt.Font[family=serif.bold,name=Serif,style=bold,size=24]
Graphics(1-3) Drawing string: "powerful!!" at:
java.awt.Point[x=228,y=286]

# items repainted = 10/10


===> paintComponent FINISHED <===

2.11 Pintado y validación


En el corazón del mecanismo de pintado y validación de JComponent está RepaintManager. Es
RepaintManager el que es responsable de enviar peticiones de pintado y validación a la cola de
eventos del sistema de AWT para su despacho. Para resumir, lo hace interceptando las peticiones de
repaint() y revalidate(), combinando cualquier petición cuando sea posible, envolviéndolas en
objetos Runnable, y enviándolas a invokeLater(). Hay unos pocos temas que merecen más
atención aquí antes de que veamos los detalles de los procesos de pintado y validación.

Nota: Esta sección contiene una explicación relativamente exhaustiva de los más complejos mecanismos
subyacentes de Swing. Si es relativamente nuevo en Java o Swing le recomendamos que se la salte . Si
está buscando información sobre como sobreescribir y usar sus propios métodos de pintado, vaya a la
sección 2.8. Para personalizar el dibujo de los delegados UI vea el capítulo 21.

2.11.1 Doble buffer


Hemos mencionado el doble buffer, como deshabilitarlo en el RepaintManager, y como especificar el
doble buffer de componentes individuales con el método setDoubleBuffered() de JComponent.
Pero, ¿cómo trabaja?

El doble buffer es la técnica de pintar en una pantalla invisible en lugar de hacerlo directamente en un
componente visible. Al final, la imagen resultante se pinta en la pantalla (lo que sucede relativamente
deprisa). Cuando se usan componentes AWT, los desarrolladores debían implementar su propio doble-
buffer para reducir el parpadeo. Estaba claro que el doble buffer debía ser una funcionalidad interna a
causa de su extendido uso. Por tanto, no sorprende mucho encontrar esta funcionalidad en todos los
componentes Swing

Internamente, el doble buffer consiste en crear una Image y obtener su objeto Graphics para usarlo en
todos los métodos de pintado. Si el componente que vamos a pintar tiene hijos, este objeto Graphics se
pasará a ellos para usarlo para pintar, y así sucesivamente. Por tanto, si estamos usando doble-buffer en
un componente, todos sus hijos lo estarán haciendo también (lo tengan habilitado o no) porque dibujaran
en el mismo objeto Graphics. Vea que sólo hay una pantalla invisible para cada RepaintManager, y
sólo hay normalmente una instancia de RepaintManager para cada applet o aplicación
(RepaintManager es una clase de servicio que registra una instancia compartida de si misma con
AppContext--ver sección 2.5).

Como veremos en el capítulo 3, JRootPane es el componente Swing de más alto nivel en cualquier
ventana (lo que incluye a JInternalFrame -- aunque no sea realmente una ventana). Habilitando el
doble buffer en JRootPane, todos sus hijos se pintarán también usando doble buffer. Como vimos en la
última sección, RepaintManager también ofrece un control global sobre el doble buffer de todos los

46
componentes. Por tanto, otra forma de garantizar que todos los componentes usen doble buffer es llamar
a:

RepaintManager.currentManager(null).setDoubleBufferingEnabled(true);

2.11.2 Dibujo optimizado


No hemos visto el hecho de que los componentes puedan superponer a cualquier otro en Swing todavía,
pero pueden hacerlo. JLayeredPane, por ejemplo, es un contenedor que permite que cualquier número
de componentes se superponga a cualquier otro. Repintar este tipo de contedor es mucho más
complicado que repintar otro contenedor que sepamos que no permite la superposición, principalmente a
causa de la posibilidad de que los componentes sean transparentes.

¿Qué significa para un componente el ser transparente? Técnicamente significa que su método
isOpaque() devolverá false. Podemos modificar esta propiedad llamando al método
setOpaque(). Lo que la opacidad significa en este contexto, es que un componente pintará todos los
pixels dentro de sus límites. Si está a false, no se garantiza que pase esto. Generalmente está puesto a
true, pero veremos que cuando está puesta a false se incrementa la carga de trabajo de todo el
mecanismo de pintado. A no ser que estemos construyendo un componente que no tiene que rellenar
toda su región rectangular (como haremos en el capítulo 5 con los botones poligonales), deberíamos
dejar siempre esta propiedad a true, como está por defecto para la mayoría de los componentes (Este
valor lo pone normalmente un delegado UI).

El método isOptimizedDrawingEnabled() de JComponent está sobreescrito para que devuelva


true para casi todas las subclases de JComponent excepto: JLayeredPane, JViewport, y
JDesktopPane (una subclase de JLayeredpane). Básicamente, llamar a este método es equivalente
a preguntarle a un componente: ¿es posible que uno de tus componentes hijos se superponga a los otros?
Si lo es, entonces hay un montón de trabajo de repintado más que hacer para tener en cuenta el hecho de
que cualquier número de componentes, de cualquier parte de nuestra jerarquía de componentes, se pueda
superponer a los demás. Adicionalmente, como los componentes pueden ser transparentes, los
componentes situados completamente debajo de otros se pueden ver aún a través de éstos. Este tipo de
componentes no son necesariamente hermanos (están en el mismo contenedor) porque podemos tener
varios contenedores no opacos puestos uno sobre otro. En situaciones como esta, tenemos que hacer un
recorrido completo del árbol para determinar que componentes tienen que ser refrescados. Si se ha
sobreescrito isOptimizedDrawingEnabled() para que devuelva true, asumimos que no tenemos
que considerar una situación como esta. Es por ello, que el dibujo es más eficiente, o 'optimizado'.
2.11.3 El interior de la validación
Una petición de revalidate() se genera cuando un componente tiene que ser situado de nuevo,
Cuando se recibe una petición de un determinado componente, tiene que haber alguna manera de
determinar si posicionar ese componente afectará a algún otro. El método isValidateRoot() de
JComponent devuelve false para la mayoría de los componentes. Básicamente, llamar a este método
es equivalente a preguntar: si posiciono tu contenido de nuevo, ¿puedes garantizarme que ninguno de tus
padres o hermanos se verá afectado desfavorablemente (tendrá que ser posicionado de nuevo)? Por
defecto, sólo JRootPane, JScrollPane, y JTextField devuelven true. Esto puede parecer
sorprendente al principio, pero aparentemente es cierto que estos componentes son los únicos
componentes Swing cuyo contenido puede ser posicionado de forma exitosa, en cualquier situación
(suponiendo que no hay componentes pesados), sin afectar a los padres o hermanos. No importa lo
grande que hagamos algo dentro de un JRootPane, JScrollPane, o JTextField, ellos no
cambiarán el tamaño o la posición si no es a causa de alguna influencia exterior (p.e. un hermano o un
padre). Para ayudarle a que se convenza, intente añadir un componente de texto multi-línea en un
contenedor sin ponerlo dentro de un panel de scroll. Puede observar que la creación de nuevas líneas
cambiará su tamaño (dependiendo del posicionamiento). Lo importante no es que pase raramente o que

47
se pueda prevenir, sino que puede pasar. Este es el único tipo de situación sobre la que
isValidateRoot() nos avisará. Por tanto, ¿dónde se usa este método?

Un componente o su padre se revalida normalmente cuando el valor de una propiedad cambia y el


tamaño, la posición o el posicionamiento interno del componente se ven afectados. Llamando
recusivamente a isValidateRoot() en el padre de un componente Swing hasta que obtengamos
true, terminaremos con el ascendiente más cercano de ese componente que nos garantice que su
validación no afectará a sus padres o hermanos. Veremos que RepaintManager depende de este
método para despachar peticiones de validación.

Nota: Por hermanos queremos decir componentes del mismo contenedor. Por padres queremos decir
contenedores padre.

2.11.4 RepaintManager
Como sabemos, normalmente hay sólo una instancia en uso de una clase de servicio para cada applet o
aplicación. Por tanto, a no ser que creemos específicamente nuestra propia instancia de
RepaintManager, lo que no necesitaremos hacer casi nunca, todo el repintado es manejado por la
instancia compartida que está registrada con AppContext. Normalmente la obtenemos llamando al
método estático currentManager() de RepaintManager:

myRepaintManager = RepaintManager.currentManager(null);

Este método recibe un Component como parámetro. De todos modos, no importa lo que le pasemos. De
hecho el componente que se pasa a este método no se usa en ningún sitio del método (vea el código
fuente de RepaintManager.java), por lo que se puede usar un valor null de forma segura. (Esta
definición existe para que la usen subclases que quieran trabajar con más de un RepaintManager,
posiblemente usando uno para cada componente.)

RepaintManager existe para dos propósitos: para proveer revalidación y repintado eficientes.
Intercepta todas las peticiones repaint() y revalidate(). Esta clase maneja también todo el doble
buffer en Swing y mantiene una Image sencilla para este propósito. El tamaño máximo de esta Image
es por defecto el tamaño de la pantalla. Aún así, podemos modificar el mismo manualmente usando el
método setDoubleBufferMaximumSize() de RepaintManager. (El resto de funcionalidades de
RepaintManager se verá a lo largo de esta sección donde sean aplicables.)

Nota: Los cell renderers que se usan en componentes como JList, JTree, y JTable son especiales ya que
están envueltos en instancias de CellRendererPane y todas las peticiones de validación y repintado
no se propagan por la jerarquía. Vea el capítulo 17 para saber más CellRendererPane y la causa de
este comportamiento. Es suficiente que sepa que los cell renderers no siguen el esquema de pintado y
validación que hemos visto en esta sección.

2.11.5 Revalidación
RepaintManager mantiene un Vector de componentes que tienen que ser validados. Cuando quiera
que una petición revalidate es interceptada, se envía el componente fuente al método
addInvalidComponent() y se chequea su propiedad “validateRoot” usando isValidateRoot().
Esto sucede recursivamente en los padres del componente hasta que isValidateRoot() devuelve
true. Se chequea entonces la visibilidad del componente resultante, si es que hay alguno. Si alguno de los
contenedores padre no es visible no hay razón para revalidarlo. En otro caso RepaintManager recorre
su árbol hasta que alcanza el componente raíz, un Window o Applet. RepaintManager chequea
entonces el Vector de componentes invalidos y si en él no está aún el componente lo añade. Después

48
de que se añada con éxito, RepaintManager pasa entonces la raíz al método
queueComponentWorkRequest() de SystemEventQueueUtilities (vimos esta clase en la
sección 2.3). Este método comprueba si ya hay un ComponentWorkRequest (esta es una clase
privada estática en SystemEventQueueUtilities que implementa Runnable) que corresponda a
esta raíz guardada en la tabla de peticiones de trabajos. Si no hay ninguna, se crea una nueva. Si ya hay
una, simplemente tomamos una referencia a ella. Entonces sincronizamos el acceso a esa
ComponentWorkRequest, la ponemos en la tabla de peticiones de trabajos si es nueva, y
comprobamos si está pendiente (p.e. si ha sido añadida a la cola de eventos del sistema de AWT). Si no
está pendiente, la enviamos a SwingUtilities.invokeLater(). Se marca entonces como
pendiente y dejamos el bloque sincronizado. Cuando se ejecuta finalmente en el hilo de despacho de
eventos notifica a RepaintManager que ejecute validateInvalidComponents(), seguido de
paintDirtyRegions().

El método validateInvalidComponents() básicamente comprueba el Vector de


RepaintManager que contiene los componentes que necesitan validación, y llama a validate() en
cada uno de ellos. (Este método es en la actualidad un poco más cuidadoso de lo que describimos aquí,
ya que sincroniza el acceso para evitar la adición de componentes invalidos durante la ejecución).

Note: Recuerde que se debería llamar a validateInvalidComponents() sólo dentro del hilo de
despacho de eventos. Nunca llame a este método desde cualquier otro hilo. La misma regla se aplica para
paintDirtyRegions().

El método paintDirtyRegions() es mucho más complicado, y veremos alguno de esos detalles más
adelante. Por ahora, es suficiente con saber que este método pinta todas las regiones que lo precisen de
todos los componentes que se mantengan en RepaintManager.
2.11.6 Repintado
JComponent define dos métodos repaint(), y se hereda la versión sin argumentos de repaint()
que existe en java.awt.Container:

public void repaint(long tm, int x, int y, int width, int height)
public void repaint(Rectangle r)
public repaint() // heredado de java.awt.Container

Si llama a la versión sin argumentos se repinta todo el componente. Para componentes pequeños y
simples esto está bien, pero para los más grandes y complejos esto no es eficiente. Los otros dos
métodos reciben los límites de la región que debe repintarse (la región sucia) como parámetro. Los
parámetros de tipo int del primer método corresponden a las coordenadas x e y, la anchura y la altura
de esa región. El segundo recibe la misma información encapsulada en una instancia de Rectangle. El
segundo método repaint() mostrado anteriormente llama directamente al primero. El primer método
envía los parametros de la región sucia al método addDirtyRegion() de RepaintManager.

Nota: El parámetro long del primer método repaint() no representa absolutamente nada y no se usa. No
importa el valor que use para él. La única razón por la que está ahí es para sobreescribir el método
repaint() correcto de java.awt.Component.

RepaintManager contiene una Hashtable de regiones sucias. Cada componente tendrá como
máximo una región sucia en esta tabla en un momento determinado. Cuando se añade una región sucia,
usando addDirtyRegion(), se comprueba el tamaño de la región y del componente. En el caso de
que tenga una anchura o una altura <= 0 el método vuelve y no pasa nada. Si es mayor que 0x0, se
comprueba la visibilidad del componente fuente y sus ancestros, y, si son todos visibles, su componente
raíz, una Window o un Applet, se encuentra navegando por el árbol (de forma parecida a como sucede

49
en addInvalidateComponent()). Se pregunta a la Hashtable de regiones sucias si ya tiene
guardada una región sucia de nuestro componente. De ser así, devuelve su valor, un Rectangle, y el
método SwingUtilities.computeUnion() conveniente se usa para combinar la nueva región
sucia con la anterior. Finalmente, RepaintManager pasa la raíz al método
queueComponentWorkRequest() de SystemEventQueueUtilities. Lo que sucede a partir de
aquí es idéntico a lo que vimos para la revalidación (ver más arriba).

Ahora podemos hablar un poco sobre el método paintDirtyRegions() que resumimos


anteriormente. (Recuerde que debería llamarse sólo dentro del hilo de despacho de eventos.) Este
método empieza creando una referencia local a la Hashtable de regiones sucias de
RepaintManger, y redirigiendo la Hashtable de regiones sucias de RepaintManager hacia una
nueva vacía. Esto se hace en una sección crítica de forma que no se pueden añadir regiones sucias
mientras sucede el intercambio.El resto de este método es bastante largo y complicado por lo que
concluiremos con un resumen del código más significativo (ver RepaintManager.java para más detalles).

El método paintDirtyRegions() continúa iterando a través de una Enumeration de componentes


sucios. Llamando al método collectDirtyComponents() de RepaintManager para cada uno.
Este método mira en todos los ancestros del componente sucio especificado y comprueba cualquier
superposición con su región sucia usando el método SwingUtilities.computeIntersection().
De esta forma todos los límites de la región sucia se minimizan de forma que sólo se mantiene la región
visible. (Observe que collectDirtyComponents() tiene en cuenta la transparencia.) Una vez que
se ha hecho esto para cada componente sucio, el método paintDirtyRegions() entra en un bucle.
Este bucle calcula la intersección final de cada componente sucio con su región sucia. Al final de cada
iteración se llama a paintImmediately() en el componente sucio asociado, que pinta en ese
momento todas las regiones sucias minimizadas en su posición correcta (veremos esto más adelante).
Esto completa el método paintDirtyRegions(), pero todavía tenemos que ver el aspecto más
importante de todo el proceso: el pintado.
2.11.7 Pintar
JComponent incluye un método update() que simplemente llama a paint(). El método
update() no se usa actualmente en ningún componente Swing, pero se provee por compatibilidad. El
método paint() de JComponent, al contrario que las implementaciones AWT de paint(), no
maneja todo el pintado de un componente. De hecho, es bastante raro que maneja algo de él
directamente. El único trabajo del método paint() de JComponent es el manejo de las áreas de
recorte, translaciones, y pintar piezas de Image que se usan por RepaintManager para doble buffer.
El resto del trabajo se delega en otros métodos.Veremos en breve cada uno de estos métodos y el orden
en que las operaciones de pintado suceden, pero primero tenemos que saber como se invoca a paint().

Como sabemos por nuestro repaso al proceso de repintado, RepaintManager es responsable de llamar
a un método llamado paintImmediately() en todos los componentes para pintar su región sucia
(recuerde que hay siempre una sola región sucia para cada componente porque RepaintManager las
combina inteligentemente). Este método, y el privado al que llama, hacen un repintado artístico incluso
más espectacular. Primero comprueba si el componente destino es visible, por si ha sido movido,
ocultado o eliminado desde que se hizo la petición. Entonces recorre los padres no opacos del
componente (usando isOpaque()) y aumenta los límites de la región a repintar adecuadamente hasta
que alcanza un padre opaco:

1. Si el padre alcanzado es una subclase de JComponent, se llama al método privado


_paintImmediately() y se le pasa la nueva región calculada. Este método interroga al método
isOptimizedDrawing(), comprueba si está habilitado el doble buffer (en cuyo caso utiliza la
pantalla invisible del objeto Graphics asociado a la Image del RepaintManager), y continúa
trabajando con isOpaque() para determinar el componente padre final y sus límites para invocar a
paint().

50
A. Si está habilitado el doble buffer, llama a paintWithBuffer() (otro método privado).
Este método trabaja con el objeto Graphics de la pantalla invisible y su área de recorte para
generar llamadas al método paint() del padre (pasándole el objeto Graphics usando un área
de recorte distinta cada vez). Después de cada llamada a paint(), usa el objeto Graphics
resultante para dibujar directamente al componente visible. (En este caso específico, el método
paint() no usará ningún buffer internamente ya que sabe, porque comprueba algunos flags
que no explicaremos, que se está teniendo cuidado con el proceso de buffer en algún otro sitio.)

B. Si no está habilitado el doble buffer, se hace simplemente una llamada al paint() del
padre.

2. Si el padre no es un JComponent, se envian los límites de la región al método repaint() de


ese padre, que normalmente llamará al método paint() de java.awt.Component. Este método
reenviará entonces el tráfico a todos los métodos paint() de sus hijos ligeros. De todas formas,
antes de hacer esto se asegura de que todos los hijos ligeros a los que notifica no están
completamente cubiertos por el área de recorte actual del objeto Graphics que se pasó.

¡En todos los casos hemos alcanzado finalmente el método paint() de JComponent!

Dentro del método paint() de JComponent, si está habilitada la depuración de gráficos se usará una
instancia de DebugGraphics para todo el pintado. Una mirada rápida al código de pintado de
JComponent muestra un gran uso de una clase llamada SwingGraphics. Ésta no está en los
documentos del API porque es privada de paquete. Parece ser una clase muy útil para manejar
translaciones personalizadas, manejo del área de recorte, y una Stack (pila) de objetos Graphics que
se usa para caché, reciclaje, y operaciones de tipo deshacer. SwingGraphics funciona actualmente
como un envoltorio para todas las instancias de Graphics usadas durante el proceso de pintado. Sólo
se puede instanciar pasándole un objeto Graphics existente. Esta funcionalidad se ha hecho incluso
más explícita, por el hecho de que implementa un interface llamado GraphicsWrapper, que es
también privado de paquete.

El método paint() comprueba si el doble buffer está habilitado y si se llamó a este método desde
paintWithBuffer() (ver más arriba):

1. Si se llamó a paint() desde paintWithBuffer() o si no está habilitado el doble buffer,


paint() comprueba si el área de recorte del objeto Graphics actual está totalmente oscurecida
por algún componente hijo. Si no lo está, se llama a paintComponent(), paintBorder(), y
paintChildren() en ese orden. Si está completamente oscurecida, sólo hace falta llamar a
paintChildren(). (Veremos lo que hacen estos tres métodos dentro de poco.)

2. Si está habilitado el doble buffer y no se llamó desde paintWithBuffer(), usará el objeto


Graphics de pantalla invisible de la Image asociada con RepaintManager durante el resto de
este método. Comprobará entonces si el área de recorte del objeto Graphics actual está
completamente oscurecida por los componentes hijo. Si no lo está, se llama a
paintComponent(), paintBorder(), y paintChildren() en ese orden. Si lo está sólo es
necesario llamar a paintChildren().

A. El método paintComponent() comprueba si el componente tiene un delegado UI


instalado. Si no lo tiene simplemente sale. Si lo tiene, llama a update() en ese delegado UI y
sale. El método update() de un delegado UI es normalmente responsable del pintado del
fondo de un componente, si es opaco, y entonces llama a paint(). El método paint() de un
delegado UI es el que pinta el contenido del componente correspondiente. (Veremos como
personalizar los delegados UI extensamente a lo largo de este texto.)

51
B. El método paintBorder() pinta simplemente el borde del componente si lo tiene.

C. El método paintChildren() está un poco más implicado en el proceso. Para resumir,


busca por todos los componentes hijo y determina si se debería invocar a paint() en éstos
usando el área de recorte de Graphics actual, el método isOpaque() y el método
isOptimizedDrawingEnabled(). El método paint() llamado en cada hijo iniciará
esencialmente el proceso de pintado del hijo desde la parte 2 de arriba, y este proceso se repetirá
hasta que no existan más hijos o no necesiten ser pintados.

Cuando construimos o creamos subclases de componentes Swing ligeros se espera normalmente que si
queremos pintar algo dentro del mismo componente (en lugar de en el delegado UI que es donde lo
haremos habitualmente) sobreescribamos el método paintComponent() y llamemos inmediatamente
a super.paintComponent(). De esta forma daremos al delegado UI la oportunidad de que dibuje el
componente primero. Sobreescribir el método paint(), o cualquier otro de los métodos mencionado
anteriormente será rara vez necesario, y siempre es una buena práctica evitar hacerlo.

2.12 Manejo del foco


Cuando se sitúan componentes Swing dentro de un contenedor Swing, el camino del foco del teclado es,
por defecto, de izquierda a derecha y de arriba a abajo. Nos referimos normalmente a este camino como
el ciclo del foco, y cambiar el foco de un componente al siguiente del ciclo se logra usando la tecla TAB
o CTRL-TAB. Para moverse en la dirección inversa a través del ciclo usamos SHIFT-TAB o CTRL-
SHIFT-TAB. El ciclo se controla por una instancia de la clase abstracta FocusManager.

FocusManager utiliza cinco propiedades de JComponent para tratar cuando el foco alcanza a éste o
le abandona:
focusCycleRoot: esta especifica si el componente contiene un ciclo de foco propio. Si contiene un
ciclo de foco, el foco entrará en este componente y se moverá a través de su ciclo de foco hasta que
se envíe fuera de ese componente manualmente o mediante código. Por defecto está propiedad es
false (para la mayoría de los componentes), y no se puede cambiar con un método típico de
acceso setXX(). Sólo se puede cambiar sobreescribiendo el método isFocusCycleRoot() y
devolviendo el valor booleano apropiado.
managingFocus: esta especifica si los KeyEvents correspondientes a un cambio de foco serán
enviados al componente o interceptados y consumidos por el FocusManager. Por defecto esta
propiedad es false (para la mayoría de los componentes), y no se puede cambiar con un método
típico de acceso setXX(). Sólo se puede cambiar sobreescribiendo el método
isManagingFocus() y devolviendo el valor booleano apropiado.
focusTraversable: esta especifica si el foco se puede transferir al componente por el
FocusManager a causa de un desplazamiento del foco en el ciclo. Por defecto esta propiedad es
true (para la mayoría de los componentes), y no se puede cambiar con un método típico de acceso
setXX(). Sólo se puede cambiar sobreescribiendo el método isFocusTraversable() y
devolviendo el valor booleano apropiado. (Observe que cuando el foco alcanza a un componente a
través de una pulsación de ratón se llama a su método requestFocus(). Sobreescribiendo
requestFocus() podemos responder a peticiones de foco de manera específica para cada
componente.)
requestFocusEnabled: especifica si una pulsación de ratón dará el foco a ese componente. Esto
no afecta al trabajo del FocusManager , que continuará transfiriendo el foco al componente como
parte del ciclo del foco. Por defector esta propiedad es true (para la mayoría de los componentes),
y se puede cambiar con el método setRequestFocusEnabled() de JComponent .
nextFocusableComponent: esta especifica el componente al que se transfiere el foco cuando se
pulsa la tecla TAB. Por defecto está puesto a null, ya que el camino del foco se maneja para

52
nosotros por el servicio FocusManager. Asignando un componente como el
nextFocusableComponent potenciará el mecanismo de foco de FocusManager. Esto se
consigue pasando el componente al método setNextFocusableComponent() de
JComponent.

2.12.1 FocusManager

clase abstracta javax.swing.FocusManager


Esta clase define la responsabilidad de determinar como se mueve el foco de un componente a otro.
FocusManager es una clase de servicio cuya instancia compartida se guarda en la tabla de servicio
AppContext (ver 2.5). Para acceder a FocusManager usamos su método estático
getCurrentManager(). Para asignar un nuevo FocusManager usamos el método estático
setCurrentManager(). Podemos deshabilitar el servicio actual FocusManager usando el método
estático disableFocusManager(), y podemos comprobar si está habilitado o no en un momento
determinado usando el método estático isFocusManagerEnabled().

Los siguientes tres métodos abstractos se tienen que definir en las subclases:
focusNextComponent(Component aComponent): se debería llamar para desplazar el foco al
siguiente componente en el ciclo del foco cuya propiedad focusTraversable sea true.
focusPreviousComponent(Component aComponent): se debería llamar para desplazar el
foco al anterior componente en el ciclo del foco cuya propiedad focusTraversable sea true.
processKeyEvent(Component focusedComponent, KeyEvent anEvent): se debería
llamar para, o bien consumir un KeyEvent enviado al componente, o bien para permitirle ser
procesado por el componente. Este método se usa normalmente para determinar si una pulsación de
teclado corresponde a un desplazamiento en el foco. Si este es el caso, el KeyEvent se consume
normalmente y se mueve el foco hacia delante o hacia atrás usando los métodos
focusNextComponent() o focusPreviousComponent() respectivamente.

Nota: “FocusManager recibirá los eventos de teclado KEY_PRESSED, KEY_RELEASED y KEY_TYPED. Si


se consume un evento, todos los demás eventos se deberían consumir también.”API

2.12.2 DefaultFocusManager

clase javax.swing.DefaultFocusManager
DefaultFocusManager desciende de FocusManager y define los tres métodos requiridos, así como
varios métodos adicionales. El método más importante en esta clase es compareTabOrder(), que
recibe dos Components como parámetros y determina en primer lugar cual de ellos está situado más
cerca de la parte de arriba del contenedor para que sea la raíz del ciclo del foco. Si ambos está situados a
la misma altura este método determinará cual de ellos está más a la izquierda. Se devolverá un valor de
true si el primer componente pasado debe obtener el foco antes que el segundo. En otro caso devolverá
false.

Los métodos focusNextComponent() y focusPreviousComponent() desplazan el foco como


se esperaba, y los métodos getComponentBefore() y getComponentAfter() se definen para
devolver el componente anterior y posterior respectivamente, que recibirán el foco después de un
determinado componente en el ciclo del foco. Los métodos getFirstComponent() y
getLastComponent() devuelven el primer componente y el último que recibirán el foco en el ciclo
del foco de un determinado contenedor.

53
El método processKeyEvent() intercepta KeyEvents enviados al componente que posee
actualmente el foco. Si estos eventos corresponden a un desplazamiento del foco (p.e. TAB, CTRL-
TAB, SHIFT-TAB, y SHIFT-CTRL-TAB) se consumen y se cambia el foco adecuadamente. En caso
contrario, estos eventos se envían al componente para ser procesados (ver sección 2.13). Observe que el
FocusManager siempre intercepta los eventos de teclado.

Nota: Por defecto, CTRL-TAB y SHIFT-CTRL-TAB se pueden usar para desplazar el foco fuera de
componentes de texto. TAB y SHIFT-TAB moverán el cursor en su lugar (ver capítulos 11 y 19).

2.12.3 Escuchando cambios del foco


Como con los componentes AWT, podemos escuchar cambios del foco en un componente adjuntando
una instancia del interface java.awt.FocusListener. FocusListener define dos métodos, cada
uno de los cuales recibe una instancia de java.awt.FocusEvent como parámetro:
focusGained(FocusEvent e): este método recibe un FocusEvent cuando se da el foco a un
componente al que se le ha añadido este oyente.
focusLost(FocusEvent e): este método recibe un FocusEvent cuando se pierde el foco en un
componente al que se le ha añadido este oyente.
FocusEvent desciende de java.awt.ComponentEvent y define, entre otros, los identificadores
FOCUS_LOST y FOCUS_GAINED para distinguir entre sus dos tipos de eventos. Un evento
FOCUS_LOST ocurrirá correspondiendo a una pérdida de foco temporal o permanente. Las pérdidas
ocurren cuando otra aplicación u otra ventana recibe el foco. Cuando el foco vuelve a esta ventana, el
componente que perdió el foco lo obtendrá de nuevo, y un evento FOCUS_GAINED se despachará en ese
momento. Las pérdidas permanentes de foco ocurren cuando el foco se mueve a causa de una pulsación
en otro componente de la misma ventana, o mediante código al invocar a requestFocus() en otro
componente, o despachando algún KeyEvent que cause un cambio de foco en el método
processKeyEvent() de FocusManager. Como es lógico, podemos añadir o borrar
implementaciones de FocusListener a cualquier componente Swing usando los métodos
addFocusListener() y removeFocusListener() de Component respectivamente.

2.13 Entrada de teclado, KeyStrokes, y Actions


2.13.1 Escuchando la entrad de teclado
Se lanzan KeyEvents por un componente siempre que el componente tiene el foco y el usuario pulsa
una tecla. Para escuchar estos eventos en un componente particular podemos añadir KeyListeners
usando el métodos addKeyListener(). KeyEvent desciende de InputEvent y, al contrario que la
mayoría de los eventos, los KeyEvents se despachan antes de que la operación correspondiente tome
parte (p.e. en un cuadro de texto la operación podría ser añadir un carácter específico al contenido del
documento). Podemos consumir estos eventos usando el método consume() antes de que se manejen
más adelante por asociaciones de teclas u otros oyentes. (más adelante veremos exactamente como tener
notificación de la entrada de teclado, y en que orden ocurre ésta).

Hay tres tipos de eventos KeyEvent, cada uno de los cuales ocurre por lo menos una vez cada
activación de teclado (p.e. pulsar y soltar una tecla del teclado):
KEY_PRESSED: este tipo de evento de tecla se genera cuando una tecla del teclado se pulsa. La tecla
que se ha pulsado queda especificada por la propiedad keyCode y un código virtual de la tecla se
puede obtener con el método getKeyCode() de KeyEvent. Un código virtual de tecla se usa
para informar de la tecla exacta del teclado que ha causado el evento, tal como
KeyEvent.VK_ENTER. KeyEvent define numerosas constantes estáticas de tipo int, que

54
empiezan con el prefijo “VK,” que significa Virtual Key (tecla virtual) (ver los documentos del API
de KeyEvent para una lista completa). Por ejemplo, si se pulsa CTRL-C, se lanzarán dos eventos
KEY_PRESSED. El int devuelto por getKeyCode() correspondiente a pulsar CTRL será un
KeyEvent.VK_CTRL. Igualmente, el int devuelto por getKeyCode() correspondiente a pulsar
la tecla “C” será un KeyEvent.VK_C. (Observe que el orden en el que se lanzan depende del
orden en el que se pulsan.) KeyEvent también tiene un propiedad keyChar que especifica la
representación Unicode del carácter pulsado (si no hay representación Unicode se usa
KeyEvent.CHAR_UNDEFINED--p.e. las teclas de función de un teclado normal de PC). Podemos
obtener el carácter keyChar correspondiente a un KeyEvent usando el método getKeyChar().
Por ejemplo, el carácter devuelto por getKeyChar() correspondiente a pulsar la tecla “C” será
‘c’. Si estaba pulsado SHIFT cuando se pulsó la tecla “C”, el carácter devuelto por getKeyChar()
correspondiente a la tecla “C” será ‘C’. (Observe que se devuelven distintos keyChars para
mayúsculas y minúsculas, a pesar de que se usa el mismo keyCode en ambas situaciones--p.e. el
valor VK_C se será devuelto por getKeyCode() esé pulsada o no la tecla SHIFT cuando se pulsa
la tecla “C”. Observe también que no hay keyChar asociado con teclas como CTRL, y
getKeyChar() devolverá simplemente ‘’ en este caso.)
KEY_RELEASED: este tipo de evento de teclado se genera cuando se suelta una tecla. Salvo por esta
diferencia, los eventos KEY_RELEASED son idénticos a los eventos KEY_PRESSED (aunque, como
veremos más adelante, ocurren mucho menos a menudo).
KEY_TYPED: este tipo de eventos se lanzan en algún momento entre un evento KEY_PRESSED y un
evento KEY_RELEASED. Nunca contiene una propiedad keyCode correspondiente a la tecla
pulsada, y se devolverá 0 siempre que se llame a getKeyCode() en un evento de este tipo.
Observe que para teclas sin representación Unicode (como RE PAG, PRINT SCREEN, etc.), no se
lanzará el evento KEY_TYPED.

La mayoría de las teclas con representación Unicode, cuando se mantienen pulsadas durante un rato,
generarán repetidos KEY_PRESSED y KEY_TYPED (en este orden). El conjunto de teclas que muestran
este comportamiento, y el porcentaje en que lo hacen, no se puede controlar y depende de la plataforma.

Cada KeyEvent mantiene un conjunto de modificadores que especifica el estado de las teclas SHIFT,
CTRL, ALT, y META. Este es un valor de tipo int que es el resultado de un or binario entre
InputEvent.SHIFT_MASK, InputEvent.CTRL_MASK, InputEvent.ALT_MASK, y
InputEvent.META_MASK (dependiendo de que teclas están pulsadas en el momento del evento).
Podemos obtener este valor con getModifiers(), y podemos comprobar específicamente cual de
estas teclas estaba pulsada en el momento en que se lanzó evento usando isShiftDown(),
isControlDown(), isAltDown(), y isMetaDown().

KeyEvent también contiene la propiedad booleana actionKey que especifica si la tecla que lo ha
lanzado corresponde a una acción que debería ejecutar la aplicación (true) o si son datos que se usan
normalmente para cosas como la adición de contenido a un componente de texto (false). Podemos usar
el método isActionKey() de KeyEvent para obtener el valor de esta propiedad.

2.13.2 KeyStrokes
El uso de KeyListeners para manejar la entrada de teclado componente por componente era necesario
antes de Java 2. A causa de esto, una significativa, y a menudo tediosa, cantidad de tiempo se gastaba
planificando y depurando operaciones de teclado. El equipo de Swing se percató de esto, e incluyó la
funcionalidad de interceptar eventos de teclado sin tener en cuenta el componente que tenga el foco. Esta
funcionalidad está implementada usando asociaciones de instancias de la clase
javax.swing.KeyStroke con ActionListeners (normalmente instancias de
javax.swing.Action).

55
Nota: A las acciones de teclado registradas se les conoce normalmente como aceleradores de teclado.

Cada instancia de KeyStroke encapsula un keyCode de KeyEvent (ver anteriormente), un valor


modifiers (idéntico al de KeyEvent -- ver anteriormente), y una propiedad booleana que especifica
si se debería activar en una pulsación de tecla (false -- por defecto) o cuando se suelta la tecla
(true). La clase KeyStroke ofrece cinco métodos estáticos para crear objetos KeyStroke (observe
que todos los objetos KeyStrokes están escondidos, y no es necesario que estos métodos develvan
siempre una instancia completamente nueva):
getKeyStroke(char keyChar)
getKeyStroke(int keyCode, int modifiers)
getKeyStroke(int keyCode, int modifiers, boolean onKeyRelease)
getKeyStroke(String representation)
getKeyStroke(KeyEvent anEvent)

El último método devolverá un KeyStroke con las propiedades correspondientes a los atributos del
KeyEvent. Las propiedades keyCode, keyChar, y modifiers se toman del KeyEvent y la
propiedad onKeyRelease se pone a true si el tipo del evento es KEY_RELEASED y a false en caso
contrario.

Para registrar una combinación KeyStroke/ActionListener con un JComponent podemos usar su


método registerKeyBoardAction(ActionListener action, KeyStroke stroke, int
condition). El parámetro ActionListener tiene que estar definido de forma que su método
actionPerformed() haga las operaciones necesarias cuando se intercepte entrada de teclado
correspondiente al parámetro KeyStroke. El parámetro int especifica bajo que condiciones se
considera valido el KeyStroke:
JComponent.WHEN_FOCUSED: sólo se llamará al correspondiente ActionListener si el
componente con el que está registrado este KeyStroke tiene el foco.
JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT: sólo se llamará al correspondiente
ActionListener si el componente con el que está registrado este KeyStroke es ancestro
(contiene) del componente que tiene el foco.
JComponent.WHEN_IN_FOCUSED_WINDOW: sólo se llamará al correspondiente
ActionListener si el componente con el que está registrado este KeyStroke está en algún
lugar de la ventana de más alto nivel (p.e. JFrame, JDialog, JWindow, JApplet, o algún otro
componente pesado) que tiene el foco. Observe que las acciones de teclado registradas con esta
condición se manejan en una instancia de la clase privada de servicio KeyBoardManager (ver
2.13.4) en lugar de en el componente.

Por ejemplo, para asociar la invocación de un ActionListener a la pulsación de ALT-H sin importar
el componente que tenga el foco en un JFrame determinado, podemos hacer lo siguiente:
KeyStroke myKeyStroke =
KeyStroke.getKeyStroke(KeyEvent.VK_H,
InputEvent.ALT_MASK, false);

myJFrame.getRootPane().registerKeyBoardAction(
myActionListener, myKeyStroke,
JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT);

Cada JComponent mantiene una propiedad cliente de tipo Hashtable que contiene todos los
KeyStrokes asociados. Cuando se registra un KeyStroke usando el método
registerKeyboardAction(), se añade a esta estructura. Sólo se puede registrar un

56
ActionListener para cada KeyStroke, y si ya hay un ActionListener para un determinado
KeyStroke, el nuevo seobreescribirá al anterior. Podemos obtener un array de KeyStrokes
correspondientes a las asociaciones guardadas en esta Hashtable usando el método
getRegisteredKeyStrokes() de JComponent, y podemos anular todas las asociaciones con el
método resetKeyboardActions(). Dado un objeto KeyStroke podemos obtener su
correspondiente ActionListener con el método getActionForKeyStroke() de JComponent,
y podemos obtener su correspondiente propiedad de condición con el método
getConditionForKeyStroke().

2.13.3 Actions
Una instancia de Action es básicamente una implementación conveniente de ActionListener que
encapsula una Hashtable de propiedades ligadas semejante a la de las propiedades cliente de
JComponent (ver capítulo 12 para más detalles sobre el trabajo con implementaciones de Action y
sus propiedades). A menudo usamos instancias de Action cuando registramos acciones de teclado.

Nota: Los componentes de texto son especiales porque usan una resolución jerárquica mediante KeyMaps.
Un KeyMap es una lista de asociaciones Action/KeyStroke y JTextComponent soporta
múltiples niveles de este tipo de mapeo. Ver capítulos 11 y 19.

2.13.4 El flujo de la entrada de teclado


Cada KeyEvent se despacha primero al componente con el foco. El FocusManager tiene la primera
oportunidad de procesarlo. Si el FocusManager no lo quiere, entonces se hace que el JComponent
llame a super.processKeyEvent() que da la oportunidad a muchos KeyListeners de procesar el
evento. Si los oyentes no lo consumen y el componente con el foco es un JTextComponent, se recorre
la jerarquía de KeyMap (ver capítulos 11 y 19 para más detalles sobre KeyMaps). Si no se consume el
evento, en este momento las asociaciones de teclado registradas con el componente que tiene el foco
tienen una oportunidad. Primero, los KeyStrokes definidos con la condición WHEN_FOCUSED tienen
esa oportunidad. Si ninguno de éstos maneja el evento, el componente navega por sus contenedores
padre (hasta que alcanza un JRootPane) buscando KeyStrokes que estén definidas con la condición
WHEN_ANCESTOR_OF_FOCUSED_COMPONENT. Si el evento no se ha manejado después de que se haya
alcanzado el contenedor de máximo niveI, se envía a KeyboardManager, una clase de servicio que es
privada de paquete (observe que al contrario de la mayoría de las clases de servicio de Swing,
KeyboardManager no registra su instancia compartida con AppContext -- ver sección 2.5).
KeyboardManager busca componentes con KeyStrokes registrados con la condición
WHEN_IN_FOCUSED_WINDOW y les envía el evento. Si no se encuentra ninguno de éstos, entonces
KeyboardManager pasa el evento a todas las JMenuBars de la ventana actual y les da a sus
aceleradores la oportunidad de procesar el evento. Si el evento aún no es manejado, comprobamos si el
foco reside en un JInternalFrame (porque es el único RootPaneContainer que puede estar
dentro de otro componente Swing). Si es el caso, nos trasladamos al padre del JInternalFrame. Este
proceso continúa hasta que se procesa el evento o se alcanza la ventana de máximo nivel.

2.14 SwingUtilities
clasa javax.swing.SwingUtilities
En la sección 2.3 vimos dos métodos de la clase SwingUtilities que se usaban para ejecutar código
en el hilo de despacho de eventos. Estos son sólo 2 de los 36 métodos de utilidad genérica definidos en
SwingUtilities, que se dividen en siete grupos: métodos de cálculo, métodos de conversión,
métodos de accesibilidad, métodos de recuperación, métodos relacionados con la multitarea y los
eventos, métodos para los botones del ratón, y métodos de disposición/dibujo/UI. Todos estos métodos
son estáticos y se describen muy brevemente en esta sección (para una comprensión más avanzada vea el

57
código fuente de SwingUtilities.java).

2.14.1 Métodos de cálculo


Rectangle[] computeDifference(Rectangle rectA, Rectangle rectB): devuelve
las regiones rectangulares que representan la porción de rectA que no intersecciona con rectB.
Rectangle computeIntersection(int x, int y, int width, int height,
Rectangle dest): devuelve la intersección de dos áreas rectangulares. La primera región se
define con los parámetros de tipo int y la segunda por con el parámetro de tipo Rectangle. El
parámetro de tipo Rectangle se modifica y se devuelve como resultado del cálculo de forma que
no se tiene que instanciar un nuevo Rectangle.
Rectangle computeUnion(int x, inty, int width, int height, Rectangle
dest): devuelve la unión de dos áreas rectangulares. La primera región se define con los
parámetros de tipo int y la segunda por con el parámetro de tipo Rectangle. El parámetro de
tipo Rectangle se modifica y se devuelve como resultado del cálculo de forma que no se tiene
que instanciar un nuevo Rectangle.
isRectangleContainingRectangle(Rectangle a, Rectangle b): devuelve true si el
Rectangle a contiene completamente al Rectangle b.
computeStringWidth(FontMetrics fm, String str): devuelve la achura del String de
acuerdo al objeto FontMetrics (ver 2.8.3).

2.14.2 Métodos de conversión


MouseEvent convertMouseEvent(Component source, MouseEvent sourceEvent,
Component destination): devuelve un MouseEvent nuevo con destination como fuente
y las coordenadas x e y convertidas al sistema de coordenadas de destination (asumiendo en
ambos casos que destination no sea null). Si destination es null las coordenadas se
convierten al sistema de coordenadas de source, y se pone source como fuente del evento. Si
ambos son null el MouseEvent devuelto es idéntico al evento que se pasa.
Point convertPoint(Component source, Point aPoint, Component
destination): devuelve un Point que representa aPoint convertido al sistema de
coordenadas del componente destination como si se hubiese generado en el componente
source. Si uno de los componentes es null se usa el sistema de coordenadas del otro, y si ambos
son null el Point devuelto es idéntico al Point pasado.
Point convertPoint(Component source, int x, int y, Component
destination): este método funciona igual que el anterior método convertPoint() excepto
que recibe parámetros de tipo int que representan las coordenadas del Point a convertir en lugar
de una instancia de Point.
Rectangle convertRectangle(Component source, Rectangle aRectangle,
Component destination): devuelve un Rectangle convertido del sistema de coordenadas
del componente source al sistema de coordenadas del componente destination. Este método
se comporta de forma similar a convertPoint().
void convertPointFromScreen(Point p, Component c): convierte el Point dado en
coordenadas de la pantalla al sistema de coordenadas del Component dado.
void convertPointToScreen(Point p, Component c): convierte el Point dado en el
sistema de coordenadas del Component dado al sistema de coordenadas de la pantalla.

2.14.3 Métodos de accesibilidad


Accessible getAccessibleAt(Component c, Point p): devuelve el componente

58
Accessible en el determinado Point del sistema de coordenadas del Component dado (se
devolverá null si no se encuentra ninguno). Observe que un componente Accessible es aquel
que implementa el interface javax.accessibility.Accessible.
Accessible getAccessibleChild(Component c, int i): devuelve el i-ésimo hijo
Accessible del Component dado.
int getAccessibleChildrenCount(Component c): devuelve el número de hijos
Accessible que contiene el Component dado.
int getAccessibleIndexInParent(Component c): devuelve el índice en su padre del
Component dado descartando todos los componentes contenidos que no implementen el interface
Accessible. Se devolverá -1 si el padre es null o no implementa Accessible, o si el
Component dado no implementa Accessible.
AccessibleStateSet getAccessibleStateSet(Component c): devuelve el conjunto de
AccessibleStates que no están activos para el Component dado.

2.14.4 Métodos de recuperación

Component findFocusOwner(Component c): devuelve el componente contenido dentro del


Component dado (o el Component dado) que tiene el foco. Si no hay tal componente se devuelve
null.
Container getAncestorNamed(String name, Component comp): devuelve el ancestro
más cercano del Component dado con el nombre que le pasamos. En otro caso se devuelve null.
(Observe que cada Component tiene una propiedad name que se puede asignar y recuperar usando
los métodos setName() y getName() respectivamente.)
Container getAncestorOfClass(Class c, Component comp): devuelve el ancestro más
cercano del Component dado que es una instancia de c. En otro caso se devuelve null.
Component getDeepestComponentAt(Component parent, int x, int y): devuelve el
hijo más profundo del Component dado que contiene el punto (x,y) en términos del sistema de
coordenadas del Component dado. Si el Component no es un Container este método termina
inmediatamente.
Rectangle getLocalBounds(Component c): devuelve un Rectagle que representa los
límites de un Component determinado en su propio sistema de coordenadas (de este modo siempre
empieza en 0,0).
Component getRoot(Component c): devuelve el primer ancestro de c que es una Window. En
otro caso este método devuelve el último ancestro que es un Applet.
JRootPane getRootPane(Component c): devuelve el primer JRootPane que es padre de c, o
c si es un JRootPane.
Window windowForComponent(Component c): devuelve el primer ancestro de c que es una
Window. En otro caso devuelve null.
boolean isDescendingFrom(Component allegedDescendent, Component
allegedAncestor): devulve true si allegedAncestor contiene a allegedDescendent.

2.14.5 Métodos relacionados con la multitarea y los eventos


Ver sección 2.3 para más información sobre estos métodos.
void invokeAndWait(Runnable obj): envía el Runnable a la cola de despacho de eventos y
bloquea el hilo actual.
void invokeLater(Runnable obj): envía el Runnable a la cola de despacho de eventos y

59
continúa.
boolean isEventDispatchThread(): devuelve true si el hilo actual es el hilo de despacho de
eventos.

2.14.6 Métodos para los botones del ratón


boolean isLeftMouseButton(MouseEvent): devuelve true si el MouseEvent corresponde
a una pulsación del botón izquierdo del ratón.
boolean isMiddleMouseButton(MouseEvent): devuelve true si el MouseEvent
corresponde a una pulsación del botón de en medio del ratón.
boolean isRightMouseButton(MouseEvent): devuelve true si el MouseEvent
corresponde a una pulsación del botón derecho del ratón.

2.14.7 Métodos de disposición/dibujo/UI


String layoutCompoundLabel(FontMetrics fm, String text, icon icon, int
verticalAlignment, int horizontalAlignment, int verticalTextPosition,
int horizontalTextPosition, Rectangle viewR, Rectangle iconR,
Rectangle textR, int textIconGap): Este método se usa normalmente por el delegado
UI de JLabel para posicionar texto y/o un icono usando el FontMetrics, las condiciones de
alineamiento y las posiciones del texto dentro del Rectangle viewR . Si se determina que el texto
de la etiqueta no cabrá dentro de este Rectangle, se usan puntos suspensivos (“...”) en lugar del
texto que no cabría. Los Rectangles textR e iconR se modifican para reflejar la nueva
disposición, y se devuelve el String resultante de esta disposición.
String layoutCompoundLabel(JComponent c, FontMetrics fm, String text,
icon icon, int verticalAlignment, int horizontalAlignment, int
verticalTextPosition, int horizontalTextPosition, Rectangle viewR,
Rectangle iconR, Rectangle textR, int textIconGap): este método es idéntico al
anterior, pero recibe el componente destino para comprobar si el la orientación del texto se debe
tener en cuenta (ver el artículo “Component Orientation in Swing: How JFC Components support
BIDI text” en the Swing Connection para más información sobre orientación:
http://java.sun.com/products/jfc/tsc/tech_topics/bidi/bidi.html).
void paintComponent(Graphics g, Component c, Container p, int x, int y,
int w, int h): pinta el Component dado en el contexto gráfico dado, usando el rectángulo
definido por los cuatro parámetros de tipo int como área de recorte. El Container se usa para
que actúe como el padre del Component de forma que cualquier petición de validación o repintado
que sucedan en ese componente no se propaguen por el árbol de ancestros del componente al que
pertenece el contexto gráfico dado. Esta es la misma metodología que usan los pintores de
componentes de JList, JTree, y JTable para mostrar correctamente el comportamiento de
"sello de goma" (rubber stamp). Este comportamiento se logra mediante el uso de un
CellRendererPane (ver capítulo 17 para más información sobre esta clase y por qué se usa para
envolver los pintores).
void paintComponent(Graphics g, Component c, Container p, Rectangle r):
funciona de forma idéntica al método anterior, pero recibe un Rectangle como parámetro en
lugar de cuatro ints.
void updateComponentTreeUI(Component c): notifica a todos los componentes que
contiene c, y a c, que actualicen su delegado UI para que correspondan a los actuales UIManager
y UIDefaults (ver capítulo 21).

60