Documente Academic
Documente Profesional
Documente Cultură
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.
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.)
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.
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.
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.
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.
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.
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).
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>>
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.
8
Figura 1.6 Model-delegate architecture
<<fichero figure1-6.gif>>
Métodos de 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.
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.
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");
}
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.
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
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: 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);
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.
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.
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.
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.
int h = miComponente.getHeight();
int w = miComponente.getWidth();
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.
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.
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:
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():
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í.
}
}
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
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:
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);
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.)
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) {};
}
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);
}
En Swing, esta funcionalidad está implementada como tres métodos estáticos de SwingUtilities
(vea el código fuente de SwingUtilities.java):
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.
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).
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.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:
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.)
26
Las clases que quieren tener un control total sobre su serialización y deserialización deberían
implementar el interface Externalizable.
El código: BakedBean.java
ver \Chapter1\1
import javax.swing.*;
import javax.swing.event.*;
import java.beans.*;
import java.awt.*;
import java.io.*;
// 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
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 #";
}
m_beanColor = newColor;
m_supporter.firePropertyChange(BEAN_COLOR, oldColor, newColor);
}
28
public Dimension getPreferredSize() {
return m_beanDimension;
}
29
out.writeObject(m_beanString);
}
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.
30
Figura 2.2 BakedBean en nuestro editor de propiedades de JavaBeans personal
<<fichero figure2-2.gif>>
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:
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)
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:
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.
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).
El Código: TestFrame.java
ver \Chapter1\2
import java.awt.*;
import javax.swing.*;
34
super( "Graphics demo" );
getContentPane().add(new JCanvas());
}
public JCanvas() {
setDoubleBuffered(true);
setOpaque(true);
}
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));
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.
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.*;
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.
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
// contador
int c = 0;
39
int cliph = r.height;
w = m_flight.getIconWidth();
h = m_flight.getIconHeight();
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++;
}
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):
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.
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)
RepaintManager.currentManager(null).
setDoubleBufferingEnabled(false);
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 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:
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().
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();
El Código: TestFrame.java
ver \Chapter1\4
import java.awt.*;
import javax.swing.*;
import javax.swing.plaf.*;
import java.io.*;
public JCanvas() {
super.setUI(EmptyUI.createUI(this));
}
44
public void paintComponent(Graphics g) {
super.paintComponent(g);
ps = DebugGraphics.logStream();
ps.println("\n===> paintComponent ENTERED <===");
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):
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]
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.
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);
¿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).
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?
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().
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).
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:
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.
¡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):
51
B. El método paintBorder() pinta simplemente el borde del componente si lo tiene.
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.
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
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.
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.
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).
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.
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.
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.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).
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.
59
continúa.
boolean isEventDispatchThread(): devuelve true si el hilo actual es el hilo de despacho de
eventos.
60