Parte I - Bases

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

Capítulo 1. Un vistazo a Swing
En este capítulo: • • • • AWT Swing MVC Delegados UI y PLAF

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

Figura 1.1 Jerarquía parcial de Components

<<fichero figure1-1.gif>>

1

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

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

Figura 1.2 Parte de la jerarquía de JComponent

<<fichero figure1-2.gif>>

2

1.2.1

Orden Z

A los componentes Swing se les denomina ligeros mientras que a los componentes AWT se les denominados pesados. La diferencia entre componentes ligeros y pesados es su orden: la noción de profundidad. Cada componente pesado ocupa su propia capa de orden Z. Todos los componentes ligeros se encuentran dentro de componentes pesados y mantienen su propio esquema de capas definido por Swing. Cuando colocamos un componente pesado dentro de un contenedor que también lo es, se superpondrá por definición a todos los componentes ligeros del contenedor. Lo que esto significa es que debemos intentar evitar el uso de componentes ligeros y pesados en un mismo contenedor siempre que sea posible. Esto no significa que no podamos mezclar nunca con éxito componentes AWT y Swing, sólo que tenemos que tener cuidado y saber qué situaciones son seguras y cuáles no. Puesto que probablemente no seremos capaces de prescindir completamente del uso de componentes pesados en un breve espacio de tiempo, debemos encontrar formas de que las dos tecnologías trabajen juntas de manera aceptable. La regla más importante a seguir es que no deberíamos colocar componentes pesados dentro de contenedores ligeros, que comúnmente soportan hijos que se superponen. Algunos ejemplos de este tipo de contenedores son JInternalFrame, JScrollPane, JLayeredPane, y JDesktopPane. En segundo lugar, si usamos un menú emergente en un contenedor que posee un componente pesado, tenemos que forzar a dicho menú a ser pesado. Para controlar esto en una instancia específica de JPopupMenu podemos usar su método setLightWeightPopupEnabled().
Nota: Para JMenus (que usan JPopupMenus para mostrar sus contenidos) tenemos que usar primero el método getPopupMenu() para recuperar su menú emergente asociado. Una vez recuperado podemos llamar entonces a setLightWeightPopupEnabled(false) en él para imponer funcionalidad pesada. Esto tiene que hacerse con cada JMenu de nuestra aplicación, incluyendo menús dentro de menús, etc.

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

1.2.2 Independencia de la plataforma
La característica más notable de los componentes Swing es que están escritos al 100% en Java y no dependen de componentes nativos, como sucede con casi todos los componentes AWT. Esto significa que un botón Swing y un área de texto se verán y funcionarán idénticamente en las plataformas Macintosh, Solaris, Linux y Windows. Este diseño elimina la necesidad de comprobar y depurar las aplicaciones en cada plataforma destino.
Nota: Las únicas excepciones a esto son los cuatro componentes pesados de Swing que son subclases directas de clases de AWT, que dependen de componentes nativos: JApplet, JDialog, JFrame, y JWindow. Ver capítulo 3.

1.2.3 Vistazo al paquete Swing
javax.swing

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

3

paquete.)
javax.swing.border

Clases e interfaces que se usan para definir estilos de bordes específicos. Observe que los bordes pueden ser compartidos por cualquier número de componentes Swing, ya que no son componentes por si mismos. javax.swing.colorchooser Clases e interfaces que dan soporte al componente JColorChooser, usado para selección de colores. (Este paquete también contiene alguna clase privada interesante sin documentar.)
javax.swing.event

El paquete contiene todos los oyentes y eventos específicos de Swing. Los componentes Swing también soportan eventos y oyentes definidos en java.awt.event y java.beans.
javax.swing.filechooser

Clases e interfaces que dan soporte al componente JFileChooser, usado para selección de ficheros.
javax.swing.plaf

Contiene el API del comportamiento y aspecto conectable usado para definir componentes de interfaz de usuario personalizados. La mayoría de las clases de este paquete son abstractas. Las implementaciones de look-and-feel, como metal, motif y basic, crean subclases e implementan las clases de este paquete. Éstas están orientadas a desarrolladores que, por una razón u otra, no pueden usar uno de los look-and-feel existentes.
javax.swing.plaf.basic

Consiste en la implementación del Basic look-and-feel, encima del cual se construyen los lookand-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-andfeel ya que no define ni el aspecto ni el comportamiento de ningún componente. Más bien ofrece la capacidad de combinar varios look-and-feels para usarlos simultáneamente. Un ejemplo típico podría ser un look-and-feel de audio combinado con metal o motif. Actualmente Java 2 no viene con ninguna implementación de multiplexing look-and-feel (de todos modos, se rumorea que el equipo de Swing esta trabajando en un audio look-and-feel mientras escribimos estas líneas).
javax.swing.table

Clases e interfaces para dar soporte al control de JTable. Este componente se usa para manejar datos en forma de hoja de cálculo. Soporta un alto grado de personalización sin requerir mejoras de look-and-feel.
javax.swing.text

Clases e interfaces usadas por los componentes de texto, incluyendo soporte para documentos con o sin estilo, las vistas de estos documentos, resaltado, acciones de editor y personalización del teclado.
javax.swing.text.html

Esta extensión del paquete text contiene soporte para componentes de texto HTML. (El soporte de HTML está siendo ampliado y reescrito completamente mientras escribimos este libro. Es por ello que la cobertura que le damos es muy limitada.)
javax.swing.text.html.parser

Soporte para analizar gramaticalmente HTML.
javax.swing.text.rtf

Contiene soporte para documents RTF.
javax.swing.tree

Clases e interfaces que dan soporte al componente JTree. Este componente se usa para mostrar y manejar datos que guardan alguna jerarquía. Soporta un alto grado de personalización sin requerir mejoras de look-and-feel.
javax.swing.undo

4

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

1.3 Arquitectura MVC
MVC es una descomposición orientada a objeto del diseño de interfaces de usuario bien conocida que data de finales de los 70. Los componentes se descomponen en tres partes: un modelo, una vista, y un controlador. Los componentes Swing están basados en una versión más moderna de este diseño. Antes de que abordemos como trabaja MVC en Swing, necesitamos comprender como se diseñó originalmente su funcionamiento.
Nota: La separación en tres partes descrita aquí se usa en la actualidad solamente en un pequeño número de conjuntos de componentes de interfaz de usuario, entre los que destaca VisualWorks.

Figura 1.3 La arquitectura Modelo-Vista-Controlador

<<fichero figure1-3.gif>>

1.3.1 Modelo
El modelo es el responsable de conservar todos los aspectos del estado del componente. Esto incluye, por ejemplo, aquellos valores como el estado pulsado/no pulsado de un botón, los datos de un carácter de un componente de texto y como esta estructurado, etc. Un modelo puede ser responsable de comunicación indirecta con la vista y el controlador. Por indirecta queremos decir que el modelo no ‘conoce’ su vista y controlador--no mantiene referencias hacia ellos. En su lugar el modelo enviará notificaciones o broadcasts (lo que conocemos como eventos). En la figura 1.3 esta comunicación indirecta se representa con líneas de puntos.

1.3.2 Vista
La vista determina la representación visual del modelo del componente. Esto es el “aspecto(look)” del componente. Por ejemplo, la vista muestra el color correcto de un componente, tanto si el componente sobresale como si está hundido (en el caso de un botón), y el renderizado de la fuente deseada. La vista es responsable de mantener actualizada la representación en pantalla y debe hacerlo recibiendo mensajes indirectos del modelo o mensajes directos del controlador.

1.3.3 Controlador
El controlador es responsable de determinar si el componente debería reaccionar a algún evento proveniente de dispositivos de entrada, tales como el teclado o el ratón. El controlador es el “comportamiento(feel)” del componente, y determina que acciones se ejecutan cuando se usa el

5

componente. El controlador puede recibir mensajes directos desde la vista, e indirectos desde el modelo. Por ejemplo, supongamos que tenemos un checkbox seleccionado en nuestro interfaz. Si el controlador determina que el usuario ha pulsado el ratón debe enviar un mensaje a la vista. Si la vista determina que la pulsación ha sido en el checkbox envía un mensaje al modelo. El modelo se actualiza y lo notifica mediante un mensaje, que será recibido por la(s) vista(s), para decirle que debería actualizarse basándose en el nuevo estado del modelo. De está manera, el modelo no está ligado a una vista o un controlador específico, permitiéndonos tener varias vistas y controladores manipulando un mismo modelo.

1.3.4 Controlador y vista personalizados
Una de las principales ventajas de la arquitectura MVC es la posibilidad de personalizar el “aspecto(look)” y el “comportamiento(feel)” de un componente sin modificar el modelo. La Figura 1.4 muestra un grupo de componentes que usan dos interfaces de usuario diferentes. Lo más importante de esta figura es que los componentes mostrados son los mismos, pero que se están usando dos implementaciones diferentes de look-and-feel (diferentes vistas y controladores -- como veremos más adelante).

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

<<fichero figure1-4.gif>>

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

6

Figura 1.5 Visualización personalizada

<<fichero figure1-5.gif>>

1.3.5 Modelos personalizados
Otra gran ventaja de la arquitectura MCV de Swing es la posibilidad de personalizar y reemplazar el modelo de datos de un componente. Por ejemplo, podemos construir nuestro propio modelo de documento de texto que preste especial atención a la escritura de una fecha o un número de teléfono de una manera determinada. Podemos también asociar el mismo modelo de datos con más de un componente (como ya comprobamos viendo MVC). Por ejemplo, dos JTextAreas pueden guardar su texto en el mismo modelo de documento, mientras que están usando dos vistas diferentes de esa información. Diseñaremos e implementaremos nuestros propios modelos de datos para JComboBox, JList, JTree, JTable, y más ampliamente a lo largo de nuestro repaso a los componentes de texto. Abajo hemos listado algunas definiciones de interfaces de modelos Swing, con una breve descripción de los datos para cuyo almacenamiento están diseñados, y con que componentes se usan:
BoundedRangeModel Usado por: JProgressBar, JScrollBar, JSlider.

Guarda: 4 enteros: value, extent, min, max. Value y extent tienen que estar entre los valores de min y max. Extent es siempre <= max y >= value.
ButtonModel

Usado por: Todas las subclases de AbstractButton. Guarda: Un booleano que determina si el botón está seleccionado (armado) o no (desarmado).
ListModel

Usado por: JList. Guarda: Una colección de objetos.
ComboBoxModel Usado por: JComboBox.

Guarda: Una colección de objetos y un objeto seleccionado.
MutableComboBoxModel Usado por: JComboBox.

Guarda: Un vector (u otra colección alterable) de objetos y un objeto seleccionado.
ListSelectionModel Usado por: JList, TableColumnModel.

Guarda: Uno o más índices de selecciones de la lista o de ítems de la tabla. Permite seleccionar sólo uno, un intervalo simple, o un intervalo múltiple (discontinuo).
SingleSelectionModel Usado por: JMenuBar, JPopupMenu, JMenuItem, JTabbedPane.

7

Guarda: El índice del elemento seleccionado en una colección de objetos perteneciente al implementador.
ColorSelectionModel Usado por: JColorChooser. Guarda: Un Color. TableModel Usado por: JTable.

Guarda: Una matriz de objetos.
TableColumnModel Usado por: JTable.

Guarda: Una colección de objetos TableColumn, un conjunto de oyentes para eventos de modelos de una columna, la anchura entre cada columna, la anchura total de todas las columnas, un modelo de selección, y un indicador de selección de columna.
TreeModel

Usado por: JTree. Guarda: Objetos que se pueden mostrar en un árbol. Las implementaciones tienen que ser capaces de distinguir entre las hojas y el resto de nodos, y los objetos deben estar organizados jerárquicamente.
TreeSelectionModel Usado por: JTree.

Guarda: Las filas seleccionadas. Permite selección simple, continua y discontinua.
Document

Usado por: Todos los componentes de texto. Guarda: Contenido. Normalmente es texto (caracteres). Implementaciones más complejas soportan texto con estilo, imágenes, y otros tipos de contenido. (p.e. componentes embebidos). No todos los componentes Swing tienen modelos, aquellos que se usan como contenedores, como
JApplet, JFrame, JLayeredPane, JDesktopPane, JInternalFrame, etc. no los tienen. Sin embargo, los componentes interactivos como JButton, JTextField, JTable, etc. tienen que tener modelos. De hecho, algunos componentes Swing tienen más de un modelo (p.e. JList usa un modelo

para mantener información sobre la selección, y otro para guardar los datos). Esto quiere decir que MVC no es totalmente rígido en Swing. Componentes simples o complejos, que no guardan grandes cantidades de información (como JDesktopPane), no necesitan separar los modelos. La vista y el controlador de cada componente están casi siempre separadas en todos los componentes Swing, como veremos en la siguiente sección. Entonces, ¿cómo encaja el componente por si mismo dentro del definición de MVC?. El componente se comporta como un mediador entre el/los modelo(s), la vista y el controlador. No es ni la M, ni la V, ni la C, aunque puede ocupar el lugar de una o incluso todas estas partes si lo diseñamos para ello. Esto se verá más claro cuando progresemos en este capítulo y a lo largo del resto del libro.

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

8

Figura 1.6 Model-delegate architecture

<<fichero figure1-6.gif>>

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

Este se implementa normalmente para que devuelva una instancia compartida del delegado UI que define la subclase apropiada de ComponentUI. Esta instancia se usa para ser compartida entre componentes del mismo tipo (p.e. Todos los JButtons que usan el Metal look-and-feel comparten la misma instancia estática del delegado UI definido en javax.swing.plaf.metal.MetalButtonUI por defecto.)
installUI(JComponent c) Instala el ComponentUI en el componente especificado. Esto añade normalmente oyentes al

componente y/o a su(s) modelo(s), para avisar al delegado UI cuando ocurran cambios en el estado que requieran que se actualice la vista.
uninstallUI(JComponent c) Borra este ComponentUI y cualquier oyente añadido por installUI() del componente

especificado y/o de su(s) modelo(s).
update(Graphics g, JComponent c)

Si el componente es opaco debería pintar su fondo y entonces llamar a paint(Graphics g,
JComponent c). paint(Graphics g, JComponent c)

Coge toda la información necesaria del componente y posiblemente de su(s) modelo(s) para dibujarlo correctamente.
getPreferredSize(JComponent c)

Devuelve el tamaño preferido del componente especificado por el ComponentUI.
getMinimumSize(JComponent c)

Devuelve el tamaño mínimo del componente especificado por el ComponentUI.
getMaximumSize(JComponent c)

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

9

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

La mayor parte de los delegados UI se construyen de manera que conocen un componente y su(s) modelo(s) sólo mientras llevan a cabo tareas de dibujo o de vista-controlador. Swing evita normalmente asociar delegados UI a un componente determinado (a causa de la instancia estática). De todos modos, nada nos impide asignar el nuestro propio como demuestra el código anterior.
Nota: La clase JComponent define métodos para asignar delegados UI porque las declaraciones de métodos no implican código específico de un componente. Esto no es posible con modelos de datos porque no hay un interface de modelo del que todos ellos desciendan (p.e. no hay una clase base como ComponentUI para los modelos Swing). Por esta razón los métodos para asignar modelos se definen en las subclases de JComponent que sea necesario.

1.4.2 Pluggable look-and-feel
Swing incluye varios conjuntos de delegados UI. Cada conjunto contiene implementaciones de ComponentUI para casi todos los componentes Swing y podemos llamar a estos conjuntos una implementación de look-and-feel o pluggable look-and-feel (PLAF). El paquete javax.swing.plaf se componen de clases abstractas que derivan de ComponentUI, y las clases del paquete javax.swing.plaf.basic descienden de ellas para implementar el Basic look-and-feel. Éste es un conjunto de delegados UI que se usan como base para construir el resto de clases de look-and-feel. (Observe que el Basic look-and-feel no se puede usar directamente ya que BasicLookAndFeel es una clase abstracta.) Hay tres implementaciones de pluggable look-and-feel que descienden de Basic lookand-feel: Windows: com.sun.java.swing.plaf.windows.WindowsLookAndFeel CDE\Motif: com.sun.java.swing.plaf.motif.MotifLookAndFeel Metal (por defecto): javax.swing.plaf.metal.MetalLookAndFeel Hay también un MacLookAndFeel que simula las interfaces de usuario de Macintosh, pero no viene con Java 2 y se debe descargar separadamente. Las librerías de los Windows y Macintosh pluggable look-and-feel sólo se soportan en la plataforma correspondiente. El multiplexing look-and-feel, javax.swing.plaf.multi.MultiLookAndFeel, extiende todas las clases abstractas de javax.swing.plaf. Está diseñado para permitir que combinaciones de lookand-feels se usen simultáneamente, y está enfocado pero no limitado, al uso con look-and-feels de Accesibilidad. El trabajo de cada delegado UI multiplexado es manejar cada uno de sus delegados UI hijos. Todos los paquetes look-and-feel contienen una clase que desciende la clase abstracta
javax.swing.LookAndFeel: BasicLookAndFeel, MetalLookAndFeel, WindowsLookAndFeel, etc. Esas son los puntos centrales de acceso a cada paquete de look-and-feel. Las usamos cuando cambiamos el look-and-feel actual, y la clase UIManager (que maneja los lookand-feels instalados) los usa para acceder a la tabla UIDefaults del look-and-feel actual (que entre

otras cosas contiene los nombres de las clases de los delegados UI del look-and-feel correspondientes a cada componente Swing). Para cambiar el look-and-feel actual de una aplicación, tenemos simplemente que llamar al método setLookAndFeel() de UIManager, pasándole el nombre completo del LookAndFeel que vamos a usar. El código siguiente se puede usar para llevar esto a cabo en tiempo de ejecución:

10

try { UIManager.setLookAndFeel( "com.sun.java.swing.plaf.motif.MotifLookAndFeel"); SwingUtilities.updateComponentTreeUI(myJFrame); } catch (Exception e) { System.err.println("Could not load LookAndFeel"); } SwingUtilities.updateComponentTreeUI() informa a todos los hijos del componente

especificado que el look-and-feel ha cambiado y que necesitan reemplazar sus delegados UI por los del tipo especificado.

1.4.3 ¿Dónde están los delegados UI?
Hemos hablado de ComponentUI, y de los paquetes LookAndFeel donde se encuentran las implementaciones, pero no hemos mencionado nada acerca de las clases específicas de los delegados UI que derivan de ComponentUI. Todas las clases abstractas del paquete javax.swing.plaf descienden de ComponentUI y se corresponden con un componente Swing determinado. El nombre de cada clase sigue con el esquema general (sin la “J”) añadiéndole el sufijo “UI”. Por ejemplo, LabelUI desciende de ComponentUI y es el delegado base usado por JLabel. Estas clases son extendidas por implementaciones concretas como los paquetes basic y multi. Los nombres de estas subclases siguen el esquema general de añadir un prefijo con el nombre del look-andfeel al nombre de la superclase. Por ejemplo, BasicLabelUI y MultiLabelUI descienden ambas de LabelUI y se encuentran en los paquetes basic y multi respectivamente. La figura 1.7 muestra la jerarquía de LabelUI.

Figura 1.7 Jerarquía de LabelUI

<<fichero figure1-7.gif>>

Se espera que la mayoría de las implementaciones de look-and-feel extiendan las clases definidas en el paquete basic, o las usen directamente. Los delegados UI de Metal, Motif, y Windows están construidos encima de las versiones de Basic. Sin embargo, el Multi look-and-feel, es la única de las implementaciones que no desciende de Basic, y es simplemente un medio para permitir instalar un número arbitrario de delegados UI en un componente determinado. La figura 1.7 debería enfatizar el hecho de que Swing suministra un gran numero clases de delegados UI. Si quisiéramos crear una implementación completa de pluggable look-and-feel, queda claro que supondría un gran esfuerzo y llevaría bastante tiempo. En el capítulo 21 aprenderemos cosas sobre este proceso, así como a modificar y trabajar con los look-and-feels existentes.

11

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

2.1 Cambiando el tamaño y la posición de JComponent y sus propiedades
2.1.1 Propiedades
Todos los componentes Swing cumplen la especificación de los JavaBeans. En la sección 2.7 veremos esto en detalle. Entre las cinco características que debe soportar un JavaBean se encuentra un conjunto de propiedades y sus métodos de acceso asociados. Una propiedad es una variable global, y sus métodos de acceso, si tiene alguno, son normalmente de la forma setPropertyname(), getPropertyname() o isPropertyname(). Una propiedad que no tienen ningún evento asociado a un cambio en su valor se llama una propiedad simple. Una propiedad ligada (bound property) es aquella para la que se lanzan PropertyChangeEvents después de un cambio en su estado. Podemos registrar nuestros PropertyChangeListeners para escuchar PropertyChangeEvents a través del método addPropertyChangeListener() de JComponent. Una propiedad restringida (constrained property) es aquella para la que se lanzan PropertyChangeEvents justo antes de que ocurra un cambio en su estado. Podemos resgistrar VetoableChangeListeners que escuchen a PropertyChangeEvents por medio del método addVetoableChangeListener() de JComponent. Se puede vetar un cambio en el código de manejo de eventos de un VetoableChangeListener lanzando una PropertyVetoException. (Sólo hay una clase en

12

Swing con propiedades restringidas: JInternalFrame).
Nota: Todos estos oyentes y eventos están definidos en el paquete java.awt.beans. Los PropertyChangeEvent’s llevan consigo tres segmentos de información: nombre de la

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

propiedad,

el

valor

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

Swing introduce un nuevo tipo de propiedad que podemos llamar de cambio (change property), a falta de un nombre dado. Usamos ChangeListeners para escuchar ChangeEvents que se lanzan cuando cambia el estado de estas propiedades. Un ChangeEvent sólo lleva consigo un segmento de información: la fuente del evento. Por esta razón, las propiedades de cambio son menos poderosas que las propiedades ligadas y que las restringidas, pero están más extendidas. Un JButton, por ejemplo, envía eventos de cambios todas las veces que se arma (se pulsa por primera vez), se presiona, o se suelta (ver capítulo 5). Otro nuevo aspecto en el estilo de las propiedades que introduce Swing es la noción de propiedades cliente (client properties). Estas son básicamente pares clave/valor que se guardan en una Hashtable facilitada por todos los componentes Swing. Esto permite añadir y borrar propiedades en tiempo de ejecución, y se usa a menudo como un sitio donde guardar datos sin tener que construir una nueva subclase.
Peligro: Las propiedades cliente pueden parecer una forma fantástica de añadir soporte al cambio de propiedades para componentes personalizados, pero se nos recomienda explícitamente no hacerlo: “El diccionario clientProperty no está pensado para soportar un alto grado de extensiones de JComponent y no se debería considerar como una alternativa a la creación de subclases cuando se diseña un nuevo componente.”API

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

Para recuperar una propiedad cliente:
miObjeto = miComponente.getClientProperty("minombre");

13

Para borrar una propiedad cliente le asignamos un valor null:
miComponente.putClientProperty("minombre", null);

Por ejemplo, JDesktopPane usa una propiedad cliente para controlar la visualización del contorno mientras arrastramos JInternalFrames (esto funcionará sin importar el L&F que se esté usando):
miDesktop.putClientProperty("JDesktopPane.dragMode", "outline"); Nota: Puede localizar que propiedades tienen tienen eventos de cambio asociados con ellas, así como cualquier otro tipo de evento, inspeccionando el código fuente de Swing. A no ser que esté usando Swing para interfaces simples, le recomendamos que se acostumbre a esto.

Cinco componentes Swing tienen propiedades cliente especiales a las que solo el Metal L&F presta atención. Concretamente son estas:
JTree.lineStyle Un String que se usa para especificar si las relaciones entro los nodos se muestran como

líneas angulosas (“Angled”), líneas horizontales que definen los límites de las celdas (“Horizontal” -- por defecto), o no se muestran líneas (“None”).
JScrollBar.isFreeStanding Un Boolean que se usa para especificar si JScrollbar tendrá un borde (Boolean.FALSE - por defecto) o sólo las partes superior e izquierda (Boolean.TRUE). JSlider.isFilled Un Boolean que especifica si la parte más baja de un deslizador (JSlider) debe estar rellena (Boolean.TRUE) o no (Boolean.FALSE -- por defecto). JToolBar.isRollover Un Boolean que sirve para determinar si un botón de la barra de herramientas muestra un

borde grabado sólo cuando el puntero del ratón se encuentra entre sus límites y ningún borde cuando no (Boolean.TRUE), o se usa siempre un borde grabado (Boolean.FALSE -- por defecto).
JInternalFrame.isPalette Un Boolean que especifica si se usa un borde muy fino (Boolean.TRUE) o el borde normal (Boolean.FALSE -- por defecto). En Java 2 FCS no se usa esta propiedad.

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

El tamaño deseable de un componente. Lo usan la mayoría de los administradores de disposición (Layout Managers) para dimensionar los componentes. setMinimumSize(), getMinimumSize() Usados durante el posicionamiento para especificar los límites inferiores de las dimensiones del componente. setMaximumSize(), getMaximumSize() Usados durante el posicionamiento para especificar los límites superiores de las dimensiones del componente. Cada uno de los métodos setXX()/getXX() acepta/devuelve una instancia de Dimension. Aprenderemos más de lo que significan estos tamaños dependiendo de cada administrador de disposición en el capítulo 4. Si un administrador de disposición presta atención o no a estos tamaños

14

depende solamente de la implementación de dicho administrador. Es perfectamente factible construir un administrador que simplemente los ignore todos, o que sólo preste atención a uno. El dimensionado de los componentes en un contenedor es específico de cada administrador de disposición. El método setBounds() de JComponent se puede usar para asignar a un componente el tamaño y la posición dentro de su contenedor padre. Este método está sobrecargado, y puede tomar tanto parámetros de tipo Rectangle (java.awt.Rectangle) como cuatro parámetros de tipo int que represente la altura, la anchura y las coordenadas x e y. Por ejemplo, estas dos formas son equivalentes:
miComponent.setBounds(120,120,300,300); Rectangle rec = new Rectangle(120,120,300,300); miComponent.setBounds(rec);

Verá que setBounds() no pasará por encima de ninguna de las políticas de posicionamiento activas a causa de un administrador de disposición de un contenedor padre. Por esta razón una llamada a setBounds() puede parecer ignorada en determinadas situaciones porque intentó hacer su trabajo, pero el componente fue obligado a volver a su tamaño original por el administrador de disposición (los administradores de disposición siempre tienen la última palabra determinando el tamaño de un componente).
setBounds() se usa normalmente para manejar componentes hijos en contenedores sin administrador de disposición (como JLayeredPane, JDesktopPane, y JComponent). Por ejemplo, usamos normalmente setBounds() cuando añadimos un JInternalFrame a un JDesktopPane.

El tamaño de un componente se puede obtener al estilo de AWT:
int h = miComponente.getHeight(); int w = miComponente.getWidth();

El tamaño se puede recuperar también como una instancia de Rectangle o de Dimension:
Rectangle rec2 = miComponente.getBounds(); Dimension dim = miComponente.getSize(); Rectangle contiene cuatro propiedades accesibles públicamente que describen su posición y su

tamaño:
int int int int recX = rec2.x; recY = rec2.y; recAnchura = rec2.width; recAltura = rec2.height;

Dimension contiene dos propiedades accesibles públicamente que describen su tamaño: int dimAnchura = dim.width; int dimAltura = dim.height;

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

con valores reales (float) entre 0.0 y 1.0: 0.5 significa el centro, valores más cercanos a 0.0 significan izquierda o arriba, y más cercanos a 1.0 significan derecha o abajo. Los correspondientes métodos de

15

JComponent son: setAlignmentX(float f); setAlignmentY(float f);

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

2.2 Manejo y lanzamiento de eventos
Los eventos ocurren en cualquier momento que se pulsa una tecla o un botón del ratón. La forma en la que los componentes reciben y procesan los eventos no ha cambiado desde el JDK1.1. Los componentes Swing pueden generar diferentes tipos de eventos, incluyendo los de java.awt.event y por supuesto, los de javax.swing.event. Algunos de estos nuevos tipos de eventos de Swing son específicos del componente. Todos los tipos de eventos se representan por un objeto, que como mínimo, identifica la fuente del evento, y a menudo lleva información adicional acerca de la clase específica de evento que se trata, e información acerca del estado de la fuente del evento antes y después de que éste se generase. Las fuentes de eventos son normalmente componentes o modelos, pero hay también clases de objetos diferentes que pueden generar eventos. Como vimos en el último capítulo, para recibir la notificación de eventos, debemos registrar oyentes en el objeto destino. Un oyente es una implementación de alguna de las clases XXListener (donde XX es un tipo de evento) definidas en los paquetes java.awt.event, java.beans, y javax.swing.event. Como mínimo, siempre hay un método definido en cada interface al que se le pasa el XXEvent correspondiente como parámetro. Las clases que soportan la notificación de XXEvents implementan generalmente el interface XXListener, y tienen soporte para registrar y cancelar el registro de estos oyentes a través del uso de los métodos addXXListener() y removeXXListener() respectivamente. La mayoría de los destinos de eventos permiten tener registrados cualquier número de oyentes. Igualmente, cualquier instancia de un oyente se puede registrar para recibir eventos de cualquier número de fuentes de éstos. Normalmente, las clases que soportan XXEvents ofrecen métodos fireXX() protegidos (protected) que se usan para construir objetos de eventos y para enviarlos a los manejadores de eventos para su proceso.

2.2.1

La clase javax.swing.event.EventListenerList

EventListenerList es un vector de pares XXEvent/XXListener. JComponent y cada uno de sus descendientes usa una EventListenerList para mantener sus oyentes. Todos los modelos por defecto mantienen también oyentes y una EventListenerList. Cuando se añade un oyente a un componente Swing o a un modelo, la instancia de Class asociada al evento (usada para identificar el tipo de evento) se añade a un vector EventListenerList, seguida del oyente. Como estos pares se

guardan en un vector en lugar de en una colección modificable (por eficiencia), se crea un nuevo vector usando el método System.arrayCopy() en cada adición o borrado. Cuando se reciben eventos, se recorre la lista y se envían eventos a todos los oyentes de un tipo adecuado. Como el vector está ordenado de la forma XXEvent, XXListener, YYEvent, YYListener, etc., un oyente correspondiente a un determinador tipo de evento está siempre el siguiente en el vector. Esta estrategia permite unas rutinas de manejo de eventos muy eficientes (ver sección 2.7.7). Para seguridad entre procesos, los métodos para añadir y borrar oyentes de una EventListenerList sincronizan el acceso al vector cuando lo manipulamos.
JComponent define sus EventListenerList como un campo protegido llamado listenerList,

así que todas sus subclases lo heredan. Los componentes Swing manejan la mayoría de sus oyentes directamente a través de listenerList.

2.2.2

Hilo de despacho de eventos (Event-dispatching thread)

Todos los eventos se procesan por los oyentes que los reciben dentro del hilo de despacho de eventos

16

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

2.3 Multitarea
Para ayudar a asegurarnos que todo nuestro código de manejo de eventos se ejecuta sólo dentro del hilo de despacho de eventos, Swing provee una clase de mucha utilidad, que entre otras cosas, nos permite añadir objetos Runnable a la cola de eventos del sistema. Esta clase se llama SwingUtilities y contiene dos métodos en los que estamos interesados aquí: invokeLater() e invokeAndWait(). El primer método añade un Runnable a la cola de eventos del sistema y vuelve inmediatamente. El segundo método añade un Runnable y espera a que sea despachado, entonces vuelve una vez que termina. La sintaxis básica de cada una es la siguiente:
Runnable trivialRunnable = new Runnable() { public void run() { hazTrabajo(); // hace algún trabajo } }; SwingUtilities.invokeLater(trivialRunnable); try { Runnable trivialRunnable2 = new Runnable() { public void run() { hazTrabajo(); // hace algún trabajo } }; SwingUtilities.invokeAndWait(trivialRunnable2); } catch (InterruptedException ie) { System.out.println("...Espera del hilo interrumpida!"); } catch (InvocationTargetException ite) { System.out.println( "...excepción no capturada dentro de run() en Runnable"); }

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

17

El código siguiente muestra la forma típica de construir nuestro propio hilo que haga un trabajo costoso en tiempo. Para actualizar de manera segura el estado de algún componente dentro de este hilo, tenemos que usar invokeLater() o invokeAndWait():
Thread trabajoDuro = new Thread() { public void run() { hacerTrabajoPesado(); // hace algún trabajo costoso en tiempo SwingUtilities.invokeLater( new Runnable () { public void run() { actualizaComponentes(); // actualiza el estado de lo(s) componente(s) } }); } }; trabajoDuro.start(); Nota: se debería usar invokeLater() en lugar de invokeAndWait() siempre que sea posible. Si tenemos que usar invokeAndWait(), debemos estar seguros de que no hay zonas sensibles a bloqueos (p.e. bloques sincronizados) mantenidas por el hilo que llama, que otro hilo podría necesitar durante la operación.

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

18

catch (InterruptedException e) { // hacer que alguien sepa que hemos sido interrumpidos // ...si esto supone cambiar el estado del componente // tenemos que usar invokeLater aquí. } } JButton stopButton = new JButton("Stop"); ActionListener stopListener = new ActionListener() { public void actionPerformed(ActionEvent event) { // interrumpir el hilo y hacer que el usuario sepa que // el hilo ha sido interrumpido deshabilitando el botón // de stop. // ...esto se hará dentro del hilo de despacho de eventos workHarder.interrupt(); stopButton.setEnabled(false); } }; stopButton.addActionListener(stopListener);

Nuestro stopButton interrumpe el hilo workHarder cuando se pulsa. Hay dos formas de que hacerTrabajoPesado() sepa si workHarder, el hilo en el que se ejecuta, ha sido interrumpido. Si está durmiendo o esperando, una InterruptedException será lanzada, que podremos capturar y procesar adecuadamente. La otra manera de detectar la interrupción es chequear periódicamente el estado llamando a Thread.interrupted(). Esto se usa para construir y mostrar diálogos complejos, en procesos de E/S que conllevan cambios en el estado del componente (como cargar un documento en un componente de texto), carga de clases o cálculo intensivo, para esperar algún mensaje o el establecimiento de una conexión de red, etc.
Referencia: Los miembros del equipo de Swing han escrito algún artículo sobre como utilizar hilos con Swing, y han construido una clase llamada SwingWorker que hace el manejo del tipo de multitarea descrito aquí más conveniente. Ver http://java.sun.com/products/jfc/tsc/archive/tech_topics_arch/threads/threads.html

2.3.1 Casos especiales
Hay algunos casos especiales en los cuales no necesitamos que código que afecte al estado de componentes se ejecute en el hilo de despacho de eventos: 1. Algunos métodos en Swing, aunque pocos y distantes entre sí, están marcados como seguros respecto a los hilos y no necesitan consideración especial. Algunos métodos que son seguros respecto a los hilos pero que no están marcados son: repaint(), revalidate(), e invalidate(). 2. Un componente se puede construir y manipular de la forma que queramos, sin tener cuidado con los hilos, siempre que no se haya tenido en cuenta (realized) (lo que quiere decir que no se haya mostrado o que no haya encolado una petición de repintado). Los contenedore de más alto nivel (JFrame, JDialog, JApplet) se tienen en cuenta una vez que se ha llamado a setVisible(true), show(), o pack() en ellos. Observe también que se considera que un componente se tiene en cuenta tan pronto como se añade a un contenedor que se tiene en cuenta. 3. Cuando trabajamos con applets Swing (JApplets) todos los componentes se pueden construir y manipular sin prestar atención a los hilos hasta que se llama al método start(), lo que sucede después del método init().

19

2.3.2 ¿Cómo construimos nuestros métodos para que sean seguros respecto a los hilos?
Esto es realmente fácil. Aquí tenemos una plantilla de método seguro respecto a los hilos, que podemos usar para garantizar que el código de este método se ejecuta sólo en el hilo de despacho de eventos:
public void hacerTrabajoSeguro() { if (SwingUtilities.isEventDispatchThread()) { // // hacer todo el trabajo aquí... // } else { Runnable llamaAhacerTrabajoSeguro = new Runnable() { public void run() { hacerTrabajoSeguro(); } }; SwingUtilities.invokeLater(llamaAhacerTrabajoSeguro); } }

2.3.3 ¿Cómo funcionan invokeLater() e invokeAndWait()? clase javax.swing.SystemEventQueueUtilities [privada de paquete]
Cuando SwingUtilities recibe un objeto Runnable a través de invokeLater(), lo pasa postRunnable() de una clase llamada inmediatamente al método SystemEventQueueUtilities. Si el Runnable se recibe a través de invokeAndWait(), se comprueba primero que el hilo actual no sea el hilo de despacho de eventos. (Sería fatal permitir invocar a invokeAndWait() desde el hilo de despacho de eventos) Si este es el caso se lanza un error. En otro caso, construimos un Object que usaremos como bloqueo en una sección crítica (p.e. un bloque sincronizado). Este bloque contiene dos instrucciones. La primera envía el Runnable al método postRunnable() de SystemEventQueueUtilities, junto con una referencia al objeto de bloqueo. La segunda espera al objeto de bloqueo, de manera que el hilo no avanzará hasta que el objeto sea notificado--por ello “invoke and wait (invocar y esperar).” Lo primero que hace el método postRunnable() es comunicarse con la cola privada SystemEventQueue, una clase interna de SystemEventQueueUtilities, para obtener un referencia a la cola de eventos del sistema. Se envuelve entonces el Runnable en una instancia de RunnableEvent, otra clase interna privada. Al constructor de RunnableEvent se le pasan un Runnable y un Object, que representa al objeto de bloqueo (null si se llamó a invokeLater()) como parámetros. La clase RunnableEvent es una subclase de AWTEvent, y define su propio ID del evento como un int estático -- EVENT_ID. (Vea que siempre que definimos nuestros propios eventos debemos usar un ID mayor que el valor de AWTEvent.RESERVED_ID_MAX.) El EVENT_ID de RunnableEvent es AWTEvent.RESERVED_ID_MAX + 1000. RunnableEvent contiene también una instancia estática de un RunnableTarget, otra clase interna privada más. RunnableTarget es una subclase de Component y su único propósito es actuar como fuente y destino de RunnableEvents. ¿Cómo hace esto RunnableTarget? Su constructor habilita los eventos con un ID que concuerde con el ID del RunnableEvent:
enableEvents(RunnableEvent.EVENT_ID);

También sobrecarga el método protegido de Component processEvent() para recibir RunnableEvents. Dentro de este método lo primero que se hace es ver si en efecto el evento pasado es

20

una instancia de RunnableEvent. Si lo es, es pasado al método processRunnableEvent() de SystemEventQueueUtilities (esto ocurre una vez que el RunnableEvent ha sido despachado de la cola de eventos del sistema.) Volvamos de nuevo a RunnableEvent. El contructor RunnableEvent llama al constructor de su superclase (AWTEvent) pasándole una instancia de RunnableTarget como la fuente del evento, y EVENT_ID como el ID del evento. Mantiene también referencias al Runnable y al objeto de bloqueo. Resumiendo: cuando llamamos a invokeLater() o a invokeAndWait(), el Runnable que les pasamos es pasado al método SystemEventQueueUtilities.postRunnable() junto al objeto de bloqueo al que está esperando el hilo que les invoca (si fue un invokeAndWait()). Este método intenta entonces obtener acceso a la cola de eventos del sistema y luego envuelve el Runnable y el objeto de bloqueo en una instancia de RunnableEvent. Una vez que se ha creado la instancia de RunnableEvent, el método postRunnable() (en el que hemos estado todo este tiempo) comprueba si ha obtenido el acceso a la cola de eventos del sistema. Esto ocurrirá sólo si no estamos ejecutándonos como un applet, ya que los applets no tienen acceso directo a la cola de eventos del sistema. En este punto, tenemos dos posibles caminos dependiendo de si nos estamos ejecutando como un applet o como una aplicación:

Aplicaciones:
Como tenemos acceso directo a la cola de eventos del sistema de AWT simplemente ponemos el RunnableEvent y volvemos. Entonces el evento es despachado en algún punto del hilo de despacho de eventos enviándolo al método processEvent() de RunnableTarget, el cual lo envía entonces al método processRunnableEvent(). Si no se ha usado bloqueo (se llamó a invokeLater()) el Runnable se ejecuta y hemos terminado. Si se ha usado un bloqueo (se llamó a invokeAndWait()), entramos en un bloque sincronizado en el objeto de bloqueo de forma que nadie más puede acceder al objeto mientras ejecutamos el Runnable. Recuerde que este es el mismo objeto de bloqueo al que está esperando el hilo que invocó a SwingUtilities.invokeAndWait(). Una vez que el Runnable termina, lo notificamos a ese objeto, que despierta al hilo invocante y hemos acabado.

Applets:
SystemEventQueueUtilities hace algunas cosas muy interesantes para rodear el hecho de que los

applets no tengan acceso directo a la cola de eventos del sistema. Para abreviar una tarea muy complicada, un RunnableCanvas (una clase interna privada que desciende de java.awt.Canvas) invisible se mantiene para cada applet y se guarda en una Hashtable estática usando el hilo invocante como clave. Un Vector de RunnableEvents se mantiene también y, en lugar de añadir manualmente un evento a la cola de eventos del sistema, un RunnableCanvas añade una petición de repaint(). Entonces, cuando se despacha la petición de repintado en el hilo de despacho de eventos. El método paint() apropiado de RunnableCanvas es llamado como se esperaba. Este método ha sido construido para que localice cualquier RunnableEvent (guardado en el Vector) asociado con un determinado RunnableCanvas, y lo ejecute (algo rebuscado, pero funciona).

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

21

import java.awt.event.*; import javax.swing.*; class TimerTest { public TimerTest() { ActionListener act = new ActionListener() { public void actionPerformed(ActionEvent e) { System.out.println("Swing is powerful!!"); } }; Timer tim = new Timer(1000, act); tim.start(); while(true) {}; } public static void main( String args[] ) { new TimerTest(); } }

En primer lugar configuramos un ActionListener para que reciba ActionEvents. Entonces construimos un nuevo Timer pasando el tiempo entre eventos en milisegundos, el retraso (delay), y un ActionListener al que enviárselos. Finalmente llamamos al método start() de Timer para activarlo. Como no hay ejecutándose una GUI el programa saldrá inmediatamente, por lo tanto ponemos un bucle para permitir al Timer que siga con su trabajo indefinidamente (explicaremos por qué es necesario esto en la sección 2.6). Cuando ejecute este código verá que se muestra “Swing is powerful!!” en la salida estándar cada segundo. Observe que Timer no lanza un evento justo cuando se inicia. Esto es a causa de su retraso inicial (initial delay) que por defecto equivale al tiempo que se le pasa al constructor. Si queremos que el Timer lance un evento justo cuando se inicia debemos poner el retraso inicial a 0 usando su método setInitialDelay(). En cualquier momento podemos llamar a stop() para detener el Timer y start() para reiniciarlo (start() no hace nada si ya se está ejecutando). Podemos llamar a restart() en un Timer para que empiece de nuevo todo el proceso. El método restart() es sólo una abreviatura para llamar a stop() y start() secuencialmente. Podemos poner el retraso de un Timer usando su método setDelay() y decirle si debe repetirse o no usando el método setRepeats(). Una vez que hemos hecho que un Timer no se repita, sólo lanzará una acción cuando se inicie (o si ya se está ejecutando), y entonces se detendrá. El método setCoalesce() permite que varios Timer de lanzamiento de eventos se combinen en uno. Esto puede ser útil durante sobrecargas, cuando el hilo de la TimerQueue (que veremos más adelante) no tiene suficiente tiempo de proceso para manejar todos sus Timers. Los Timers son fáciles de usar y a menudo se pueden utilizar como una herramienta conveniente para construir nuestros propios hilos. Sin embargo, hay mucho más por detrás que merece un poco de atención. Antes de que veamos a fondo como trabajan los Timers, echaremos un vistazo al servicio de mapeo de clases de Swing (SecurityContext-to-AppContext) para applets, así como a la forma en la que las aplicaciones manejan sus clases de servicio (también usando AppContext). Si no siente curiosidad por como comparte Swing las clases de servicio, puede saltarse la siguiente sección. aunque nos referiremos de vez en cuando a AppContext, no significa que sea necesario para entender los detalles.

22

2.5 Los servicios de AppContext
clase sun.awt.AppContext [específica de la plataforma]
Peligro: AppContext no está pensada para ser usada por cualquier desarrollador, ya que no es parte del API de Java 2. La abordamos aquí sólo para facilitar una mejor comprensión de como las clases de servicio de Swing trabajan entre bastidores. AppContext es una tabla de servicio de una aplicación o de un applet (diremos “app” para abreviar) que es única para cada sesión de Java (applet o aplicación). Para los applets, existe un AppContext separado para cada SecurityContext que corresponde a la base del código del applet. Por ejemplo,

si tenemos dos applets en la misma página, cada uno usando código de un directorio diferente, los dos deberán tener asociado con ellos un SecurityContext distinto. Si al contrario, se han cargado desde la misma base de código, tendrán que compartir necesariamente un SecurityContext. Las aplicaciones Java no tienen SecurityContexts. En su lugar, se ejecutan en espacios de nombres que son diferenciados por los ClassLoaders. No profundizaremos en los detalles de SecurityContexts o ClassLoaders aquí, pero es suficiente decir que se pueden usar por los SecurityManagers para indicar dominios de seguridad, y la clase AppContext está diseñada para aprovecharse de esto permitiendo que haya tan sólo una instancia de ella misma por cada dominio de seguridad. De esta forma, applets de diferentes bases de código no pueden acceder al AppContext del otro. ¿Pero, por qué es esto importante? Vamos allá... Una instancia compartida (shared instance) es una instancia de una clase que se puede obtener normalmente usando un método estático definido en esa clase. Cada AppContext mantiene una Hashtable de instancias compartidas disponibles para el dominio de seguridad asociado, y a cada instancia se le denomina como un servicio. Cuando se pide un servicio por primera vez, éste registra su instancia compartida con su AppContext asociado. Esto consiste en crear una nueva instancia de si mismo y añadirla al mapeo clave/valor del AppContext. Una razón por la cual estas instancias compartidas se registran con un AppContext en lugar de ser implementadas como instancias estáticas normales, directamente recuperables por la clase de servicio, es por propósitos de seguridad. Los servicios registrados con un AppContext se pueden acceder sólo desde apps seguras (trusted apps), mientras que las clases que proveen directamente instancias estáticas de si mismas permiten que éstas se usen de manera global (requiriendo que implementemos nuestro propio mecanismo de seguridad si queremos limitar el acceso a ellas). Otra razón para ello es la robustez. Cuantos menos applets interactúen con otros de manera indocumentada, más robustos podrán ser. Por ejemplo, imagine que una app intenta acceder a todos los eventos importantes en la EventQueue del sistema (donde se encolan todos los eventos para que sean procesados en el hilo de despacho de eventos) para intentar lograr contraseñas. Usando distintas EventQueues en cada AppContext, a los únicos eventos principales que la app tendría acceso sería a los suyos. (Por esto hay sólo una EventQueue por cada AppContext) Entonces, ¿cómo accedemos a nuestro AppContext para añadir, borrar o recuperar servicios? AppContext no está pensado para que sea accedido por desarrolladores, pero podemos si realmente lo necesitamos, y esto garantizaría que nuestro código no sería certificado como 100% puro nunca, ya que
AppContext no forma parte del núcleo del API. No obstante, así es como se hace: El método estático AppContext.getAppContext() determina el AppContext correcto a usar dependiendo de si se está ejecutando una aplicación o una applet. Podemos usar entonces los métodos put(), get() y remove() del AppContext devuelto para manejar las instancias compartidas. Para lograr esto,

tenemos que implementar nuestros propios métodos como sigue:

23

private static Object appContextGet(Object key) { return sun.awt.AppContext.getAppContext().get(key); } private static void appContextPut(Object key, Object value) { sun.awt.AppContext.getAppContext().put(key, value); } private static void appContextRemove(Object key) { sun.awt.AppContext.getAppContext().remove(key); }

En Swing, esta funcionalidad está implementada como tres métodos estáticos de SwingUtilities (vea el código fuente de SwingUtilities.java):
static void appContextPut(Object key, Object value) static void appContextRemove(Object key, Object value) static Object appContextGet(Object key)

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

2.6 Interior de los temporizadores y TimerQueue
class javax.swing.TimerQueue [privada de paquete]
Un Timer es un objeto que contiene un pequeño Runnable capaz de despachar ActionEvents a una lista de ActionListeners (guardados en una EventListenerList). Todas las instancias de Timer se manejan por la instancia compartida de TimerQueue (registrada con AppContext). Un TimerQueue es una clase de servicio cuyo trabajo es manejar todas las instancias de Timer en una sesión de Java. La clase TimerQueue provee el método estático sharedInstance() para recuperar el servicio TimerQueue de AppContext. Siempre que un nuevo Timer se crea y se inicia, es añadido a la TimerQueue compartida, que mantiene una lista de Timers ordenados por el tiempo en el que expiran (p.e. el tiempo que queda para lanzar el próximo evento). La TimerQueue es un demonio (daemon) que se inicia inmediatamente en la instanciación. Esto sucede cuando se llama a TimerQueue.sharedInstance() por primera vez (cuando se inicia el primer Timer de una sesión de Java). Espera continuamente a que expire el Timer con el tiempo más cercano. Una vez que esto ocurre envía una señal al Timer para que envíe ActionEvents a todos sus oyentes, entonces asigna un nuevo Timer como cabeza de la lista, y finalmente borra el que ha expirado. Si el Timer que ha expirado está en modo de repetición, se añade de nuevo a la lista el lugar apropiado basándose en su retraso.
Nota: La razón real por la que el ejemplo de Timer de la sección 2.4 saldría inmediatamente si no ponemos un bucle, es que la TimerQueue es un demonio. Los demonios son hilos de servicio y cuando la JVM sólo tiene ejecutándose demonios terminará ya que asume que no se está haciendo trabajo real. Normalmente este comportamiento es el deseable.

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

24

2.7 La arquitectura JavaBeans
Como en este libro estamos interesados en crear aplicaciones Swing, necesitamos comprender y apreciar el hecho de que cada componente Swing sea un JavaBean.
Nota: Si es familiar con el modelo de componentes JavaBeans puede que quiera saltar a la siguiente sección.

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

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

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

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

25

variables que cuando se modifican, el bean mandará eventos de notificación a todos los oyentes. Esta notificación tiene la forma de un objeto de evento, que contiene el nombre de la propiedad, el valor anterior de la propiedad, y el valor nuevo. En el momento que una propiedad ligada cambia, debería enviar un PropertyChangeEvent. Cuando va a cambiar una propiedad restringida, el bean debería lanzar un PropertyChangeEvent antes de que ocurra el cambio, permitiendo que éste sea vetado. Otros objetos pueden escuchar estos eventos para procesarlos como corresponda (lo que guia la comunicación). Asociados con las propiedades están los métodos setXX()/getXX() e isXX() de los beans. Si un método setXX() está disponible se dice que su propiedad asociada es escribible. Si un método getXX() o isXX() está disponible se dice que la propiedad asociada es legible. Un método isXX() corresponde normalmente a la obtención de un propiedad booleana (ocasionalmente los métodos getXX() se usan para esto también).

2.7.4 Personalización
Las propiedades de un bean están expuestas a través de sus métodos setXX()/getXX() e isXX(), y se pueden modificar en tiempo de ejecución (o en tiempo de diseño). Los JavaBeans se usan comúnmente en entornos de desarrollo (IDE's) donde las hojas de propiedades se pueden mostrar permitiendo que las propiedades de los beans se lean o se escriban (dependiendo de los métodos de acceso).

2.7.5 Comunicación
Los Beans están diseñados para enviar eventos que notifican a todos los oyentes registrados con él cuando cambia de valor una propiedad ligada o una restringida. Las apps se construyen registrando oyentes de bean a bean. Como podemos usar la introspección para recoger información sobre el envío y el recibo de eventos de cualquier bean, las herramientas de diseño pueden aprovechar este conocimiento para permitir una personalización más poderosa en la etapa de diseño. La comunicación es la unión básica que mantiene unido a un GUI interactivo.

2.7.6 Persistencia
Todos los JavaBeans tienen que implementar el interface Serializable (directa o indirectamente) para permitir la serialización de su estado en un almacenamiento persistente (almacenamiento que existe después de que termine el programa). Todos los objetos se guardan salvo los que se declaran como transient. (Observe que JComponent implementa directamente este interface.) Las clases que necesiten un procesamiento especial durante la serialización tienen que implementar los siguientes métodos privados:
private void writeObject(java.io.ObjectOutputStream out) y private void readObject(java.io.ObjectInputStream in)

Estos métodos se llaman para escribir o leer una instancia de esta clase de un stream. Observe que el mecanismo de serialización por defecto será invocado para serializar todas las subclases porque se trata de métodos privados. (Vea la documentación del API o el tutorial de Java para más información sobre la serialización.)
Nota: Como en la primera versión de Java 2, JComponent implementa readObject() y writeObject() como privados, todas las subclases tienen que implementar estos métodos si requieren un procesamiento especial. Actualmente la persistencia a largo plazo no se recomienda, y es posible que cambie en futuras versiones. Sin embargo, no hay ningún problema en implementar persistencia a corto plazo (p.e. para RMI, transferencia de datos, etc.).

26

Las clases que quieren tener un control total sobre su serialización y deserialización deberían implementar el interface Externalizable. Este interface define dos métodos:
public void writeExternal(ObjectOutput out) public void readExternal(ObjectInput in)

Estos métodos se invocarán cuando writeObject() y readObject() (vistos anteriormente) sean invocados para llevar a cabo alguna serialización/deserialización.

2.7.7 Un JavaBean simple basado en Swing
El siguiente código nos muestra como construir un JavaBean basado en Swing con propiedades simples, ligadas, restringidas y de cambio. El código: BakedBean.java ver \Chapter1\1
import import import import import javax.swing.*; javax.swing.event.*; java.beans.*; java.awt.*; java.io.*;

public class BakedBean extends JComponent implements Externalizable { // Nombres de la propiedad (sólo para propiedades ligadas o restringidas) public static final String BEAN_VALUE = "Value"; public static final String BEAN_COLOR = "Color"; // Propiedades private Font m_beanFont; private Dimension m_beanDimension; private int m_beanValue; private Color m_beanColor; private String m_beanString; // // // // // simple simple ligada restringida de cambio

// Maneja todos los PropertyChangeListeners protected SwingPropertyChangeSupport m_supporter = new SwingPropertyChangeSupport(this); // Maneja todos los VetoableChangeListeners protected VetoableChangeSupport m_vetoer = new VetoableChangeSupport(this); // Sólo se necesita un ChangeEvent ya que el único estado del evento // es la propiedad fuente. La fuente de los eventos generados // es siempre "this". Verá esto en montones de código Swing. protected transient ChangeEvent m_changeEvent = null; // Esto puede manejar todos los tipos de oyentes, siempre que configuremos // los métodos fireXX para que miren correctamente en esta lista. // Esto hará que aprecie las clases XXSupport. protected EventListenerList m_listenerList = new EventListenerList(); public BakedBean() { m_beanFont = new Font("SanSerif", Font.BOLD | Font.ITALIC, 12); m_beanDimension = new Dimension(150,100);

27

m_beanValue = 0; m_beanColor = Color.black; m_beanString = "BakedBean #"; } public void paintComponent(Graphics g) { super.paintComponent(g); g.setColor(m_beanColor); g.setFont(m_beanFont); g.drawString(m_beanString + m_beanValue,30,30); } public void setBeanFont(Font font) { m_beanFont = font; } public Font getBeanFont() { return m_beanFont; } public void setBeanValue(int newValue) { int oldValue = m_beanValue; m_beanValue = newValue; // Avisar a todos los PropertyChangeListeners m_supporter.firePropertyChange(BEAN_VALUE, new Integer(oldValue), new Integer(newValue)); } public int getBeanValue() { return m_beanValue; } public void setBeanColor(Color newColor) throws PropertyVetoException { Color oldColor = m_beanColor; // Avisar a todos los VetoableChangeListeners antes de hacer el cambio // ...se lanzará una excepción si hay un veto // ...si no continuaremos y haremos el cambio m_vetoer.fireVetoableChange(BEAN_COLOR, oldColor, newColor); m_beanColor = newColor; m_supporter.firePropertyChange(BEAN_COLOR, oldColor, newColor); } public Color getBeanColor() { return m_beanColor; } public void setBeanString(String newString) { m_beanString = newString; // Avisar a todos los ChangeListeners fireStateChanged(); } public String getBeanString() { return m_beanString; } public void setPreferredSize(Dimension dim) { m_beanDimension = dim; }

28

public Dimension getPreferredSize() { return m_beanDimension; } public void setMinimumSize(Dimension dim) { m_beanDimension = dim; } public Dimension getMinimumSize() { return m_beanDimension; } public void addPropertyChangeListener( PropertyChangeListener l) { m_supporter.addPropertyChangeListener(l); } public void removePropertyChangeListener( PropertyChangeListener l) { m_supporter.removePropertyChangeListener(l); } public void addVetoableChangeListener( VetoableChangeListener l) { m_vetoer.addVetoableChangeListener(l); } public void removeVetoableChangeListener( VetoableChangeListener l) { m_vetoer.removeVetoableChangeListener(l); } // Recuerde que EventListenerList es un array de // parejas clave/valor: // key = XXListener referencia a la clase // value = XXListener instancia public void addChangeListener(ChangeListener l) { m_listenerList.add(ChangeListener.class, l); } public void removeChangeListener(ChangeListener l) { m_listenerList.remove(ChangeListener.class, l); } // Este es el código típico de despacho de EventListenerList. // Verá esto a menudo en código Swing. protected void fireStateChanged() { Object[] listeners = m_listenerList.getListenerList(); // Procesa los oyentes del último al primero, avisando // a los que estén interesados en este evento for (int i = listeners.length-2; i>=0; i-=2) { if (listeners[i]==ChangeListener.class) { if (m_changeEvent == null) m_changeEvent = new ChangeEvent(this); ((ChangeListener)listeners[i+1]).stateChanged(m_changeEvent); } } } public void writeExternal(ObjectOutput out) throws IOException { out.writeObject(m_beanFont); out.writeObject(m_beanDimension); out.writeInt(m_beanValue); out.writeObject(m_beanColor);

29

out.writeObject(m_beanString); } public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException { setBeanFont((Font)in.readObject()); setPreferredSize((Dimension)in.readObject()); // Usar el tamaño preferido para el mínimo.. setMinimumSize(getPreferredSize()); setBeanValue(in.readInt()); try { setBeanColor((Color)in.readObject()); } catch (PropertyVetoException pve) { System.out.println("Color change vetoed.."); } setBeanString((String)in.readObject()); } public static void main(String[] args) { JFrame frame = new JFrame("BakedBean"); frame.getContentPane().add(new BakedBean()); frame.setVisible(true); frame.pack(); } } BakedBean tiene representación visual (no es obligatorio para un bean). Tiene las propiedades: m_beanValue, m_beanColor, m_beanFont, m_beanDimension, y m_beanString. Soporta persistencia implementando el interface Externalizable y los métodos writeExternal() y readExternal() para controlar su propia serialización (observe que el orden en el que se escriben y se leen los datos coincide). BakedBean soporta personalización mediante sus métodos setXX() y getXX(), y soporta comunicación permitiendo el registro de PropertyChangeListeners, VetoableChangeListeners, y ChangeListeners. Y, sin tener que hacer nada especial, soporta

introspección. Utilizar un método para mostrar BakedBean en un frame no está dentro de la funcionalidad de JavaBeans. La Figura 2.1 muestra BakedBean siendo ejecutado como una aplicación.

Figura 2.1 BakedBean en nuestro editor personal de JavaBeans

<<fichero figure2-1.gif>>

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

30

Figura 2.2 BakedBean en nuestro editor de propiedades de JavaBeans personal

<<fichero figure2-2.gif>>

2.8 Fuentes, Colores, Gráficos y texto
2.8.1 Fuentes clase java.awt.Font, abstract class java.awt.GraphicsEnvironment
Como vimos anteriormente en BakedBean, las fuentes son muy fáciles de crear:
m_beanFont = new Font("SanSerif", Font.BOLD | Font.ITALIC, 12);

En este código "SanSerif" es el nombre de la fuente, Font.Bold | Font.PLAIN es el estilo (que en este caso es negrita (bold) y cursiva (italic)), y 12 es el tamaño. La clase Font define tres contantes estáticas de tipo int para indicar el estilo: Font.BOLD, Font.ITALIC, FONT.PLAIN. Podemos especificar el tamaño de la fuente con un int en el constructor de Font. Usando Java 2, para obtener una lista de los nombres de fuentes disponibles en tiempo de ejecución, podemos preguntar al GraphicsEnvironment local:
GraphicsEnvironment ge = GraphicsEnvironment. getLocalGraphicsEnvironment(); String[] fontNames = ge.getAvailableFontFamilyNames(); Nota: Java 2 introduce un nuevo, poderoso y completo mecanismo para comunicarse con dispositivos que pueden dibujar gráficos, como pantallas o impresoras. Estos dispositivos se representan como instancias de la clase GraphicsDevice. Es interesante, que un GraphicsDevice puede estar en la máquina local o en una remota. Cada GraphicsDevice tiene un conjunto de objetos GraphicsConfiguration asociados con él. Una GraphicsConfiguration describe características específicas del dispositivo asociado. Normalmente cada GraphicsConfiguration de un GraphicsDevice representa un modo de operación diferente (por ejemplo resolución y número de colores). Nota: En código para el JDK1.1, para obtener la lista de nombres de fuentes había que usar el código siguiente: String[] fontnames = Toolkit.getDefaultToolkit().getFontList(); El método Toolkit.getFontList() se ha desaconsejado en Java 2 y este código debería actualizarse.

31

GraphicsEnvironment es una clase abstracta que describe una colección de GraphicsDevices. Las subclases de GraphicsEnvironment deben tener tres métodos para obtener arrays de Fonts e información de Font: Font[] getAllFonts(): obtiene todas las Fonts disponibles en tamaño de un punto. String[] getAvailableFontFamilyNames(): obtiene los nombres de todas las familias de

fuentes disponibles.
String[] getAvailableFontFamilyNames(Locale l): obtiene los nombres de todas las familias de fuentes disponibles usando el Locale (soporte a la internationalización) especificado. GraphicsEnvironment también tiene métodos estáticos para recuperar GraphicsDevices y la instancia local de GraphicsEnvironment. Para encontrar que Fonts están disponibles para el

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

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

Sin embargo, no podemos usar los métodos setXX(). En su lugar debemos usar uno de los siguientes métodos de instancia de Font para conseguir una nueva Font:
deriveFont(float size) deriveFont(int style) deriveFont(int style, float size) deriveFont(Map attributes) deriveFont(AffineTransform trans) deriveFont(int style, AffineTransform trans)

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

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

32

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

Normalmente usamos los dos primeros métodos, y aquellos familiarizados con el JDK1.1 los reconocerán. El primero permite especificar los valores de rojo, azul y verde como floats de 0.0 a 1.0. El segundo toma estos valores como ints de 0 a 255. Los segundos dos métodos son nuevos en Java 2. Ambos tienen un cuarto parámetro que representa el valor alpha del Color. El valor alpha controla directamente la transparencia. Por defecto es 1.0 o 255 que corresponde a completamente opaco. 0.0 o 0 significa totalmente transparente. Observe que, como con las Fonts, hay un montón de métodos de acceso getXX() pero no de setXX(). En lugar de modificar un objeto Color es más normal que creemos uno nuevo.
Nota: La clase Color tiene los métodos estáticos brighter() y darker() que devuelven un Color más claro (brighter) o más oscuro (darker) que el Color especificado, pero su comportamiento es impredecible a causa de errores internos de redondeo y sugerimos no usarlos.

Especificando un valor alpha podemos usar el Color resultante como fondo de un componente para hacerlo transparente. Esto funcionará para cualquier componente ligero que Swing provea como etiquetas, componentes de texto, frames internos, etc. Por supuesto habrá cuestiones específicas de cada componente involucradas (como hacer transparentes el borde y la barra de título de un frame interno transparentes). La siguiente sección muestra un ejemplo simple de canvas, que muestra como usar el valor alpha para mostrar alguna superficie transparente.
Nota: La propiedad de opacidad de un componente Swing, controlada mediante setOpaque(), no está relacionada directamente con la transparencia de Color. Por ejemplo, si tenemos una JLabel opaca, cuyo fondo se ha puesto a un verde transparente (p.e. Color(0,255,0,150)) Los límites de la etiqueta se pintarán sólo porque es opaca. Seremos capaces de ver a través de ella sólo porque el color es transparente. Si quitamos la opacidad, el fondo de la etiqueta no se dibujará. Ambas cosas se tienen que usar conjuntamente para crear componentes transparentes, pero no están directamente relacionadas.

2.8.3 Gráficos y texto clase abstracta java.awt.Graphics, clase abstracta java.awt.FontMetrics
Dibujar en Swing es muy distinto a hacerlo en AWT. En AWT sobrescribimos normalmente el método paint() de Component para dibujar y el método update() para otras cosas como implementar nuestro propio doble buffer o rellenar el fondo antes de llamar a paint(). Con Swing, el dibujo de componentes es mucho más complejo. Como JComponent es una subclase de Component, usa los métodos update() y paint() por diferentes razones. De hecho, no se invoca nunca al método update() para nada. Hay cinco pasos adicionales en el pintado que normalmente se desarrollan dentro del método paint(). Veremos este proceso en la sección 2.11, pero basta con decir aquí que cualquier subclase de JComponent que quiera tener el control de su propio dibujado debería sobrescribir el método paintComponent() y no el método paint(). Adicionalmente, debería empezar siempre su método paintComponent() con una llamada a super.paintComponent(). Sabiendo esto, es bastante fácil construir un JComponent que actúe como nuestro propio canvas ligero. Todo lo que tenemos que hacer es escribir una subclase y sobreescribir el método paintComponent(). Dentro de ese método podemos hacer todo nuestro pintado. Así es como tomamos control del dibujado de nuestros simples componentes personalizados. De todos modos, esto

33

no se debería intentar con los componentes Swing normales porque los delegados UI están a cargo de su dibujado (veremos como personalizar el dibujado en el delegado UI al final del capítulo 6, y durante el capítulo 21).
Nota: La clase de awt Canvas se puede reemplazar por una versión simplificada de la clase JCanvas que definiremos en el siguiente ejemplo.

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

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

<<fichero figure2-3.gif>> El Código: TestFrame.java ver \Chapter1\2
import java.awt.*; import javax.swing.*; class TestFrame extends JFrame { public TestFrame() {

34

super( "Graphics demo" ); getContentPane().add(new JCanvas()); } public static void main( String args[] ) { TestFrame mainFrame = new TestFrame(); mainFrame.pack(); mainFrame.setVisible( true ); } } class JCanvas extends JComponent { private static Color m_tRed = new Color(255,0,0,150); private static Color m_tGreen = new Color(0,255,0,150); private static Color m_tBlue = new Color(0,0,255,150); private static Font m_biFont = new Font("Monospaced", Font.BOLD | Font.ITALIC, 36); private static Font m_pFont = new Font("SanSerif", Font.PLAIN, 12); private static Font m_bFont = new Font("Serif", Font.BOLD, 24); private static ImageIcon m_flight = new ImageIcon("flight.gif"); public JCanvas() { setDoubleBuffered(true); setOpaque(true); } public void paintComponent(Graphics g) { super.paintComponent(g); // pinta todo el componente de blanco g.setColor(Color.white); g.fillRect(0,0,getWidth(),getHeight()); // pinta un círculo amarillo g.setColor(Color.yellow); g.fillOval(0,0,240,240); // pinta un círculo magenta g.setColor(Color.magenta); g.fillOval(160,160,240,240); // pinta el icono de debajo del cuadrado azul int w = m_flight.getIconWidth(); int h = m_flight.getIconHeight(); m_flight.paintIcon(this,g,280-(w/2),120-(h/2)); // pinta el icono de debajo del cuadrado rojo m_flight.paintIcon(this,g,120-(w/2),280-(h/2)); // pinta un cuadrado rojo transparente g.setColor(m_tRed); g.fillRect(60,220,120,120); // pinta un círculo verde transparente g.setColor(m_tGreen); g.fillOval(140,140,120,120); // pinta un cuadrado azul transparente g.setColor(m_tBlue); g.fillRect(220,60,120,120); g.setColor(Color.black);

35

// Negrita, Cursiva, 36-puntos "Swing" g.setFont(m_biFont); FontMetrics fm = g.getFontMetrics(); w = fm.stringWidth("Swing"); h = fm.getAscent(); g.drawString("Swing",120-(w/2),120+(h/4)); // Normal, 12-puntos "is" g.setFont(m_pFont); fm = g.getFontMetrics(); w = fm.stringWidth("is"); h = fm.getAscent(); g.drawString("is",200-(w/2),200+(h/4)); // Negrita 24-puntos "powerful!!" g.setFont(m_bFont); fm = g.getFontMetrics(); w = fm.stringWidth("powerful!!"); h = fm.getAscent(); g.drawString("powerful!!",280-(w/2),280+(h/4)); } // Algunos administradores de disposición necesitan esta información public Dimension getPreferredSize() { return new Dimension(400,400); } public Dimension getMinimumSize() { return getPreferredSize(); } public Dimension getMaximumSize() { return getPreferredSize(); } }

Observe

que sobrescribimos los métodos de JComponent getPreferredSize(), getMinimumSize(), y getMaximumSize(), para que algunos administradores de disposición puedan dimensionar este componente (de otra manera alguno pondría su tamaño a 0x0). Es siempre una buena práctica la sobreescritura de estos métodos cuando se implementan componentes personalizados. La clase Graphics usa lo que se llama el área de recorte (clipping area). Dentro del método paint() de un componente, esta es la región de la vista del componente que se está repintando. Sólo el dibujo hecho dentro de los límites del área de recorte será dibujado en el momento. Podemos obtener el tamaño y la posición de estos límites llamando a getClipBounds() que nos devuelve una instancia de Rectangle describiéndola. La razón por la que se usa el área de recorte es por eficiencia: no hay motivo para pintar regiones invisibles cuando no tenemos que hacerlo. (Mostraremos como extender este ejemplo para trabajar con el área de recorte para una mayor eficiencia en la próxima sección).
Nota: Todos los componentes Swing tienen doble buffer por defecto. Si estamos construyendo nuestro propio canvas ligero no tenemos que preocuparnos por el doble buffer. Este no es el caso con un Canvas de AWT.

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

36

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

Figura 2.4 Usando FontMetrics

<<fichero figure2-4.gif>>

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

Una operación típica cuando dibujamos texto es centrarlo en un punto determinado. Suponga que queremos centrar el texto “Swing” en 200,200. Aquí está el código que deberíamos usar (asumiendo que hemos recuperado el objeto FontMetrics, fm, como se mostró anteriormente):
int w = fm.stringWidth("Swing"); int h = fm.getAscent(); g.drawString("Swing",200-(w/2),200+(h/4));

Obtenemos la anchura de “Swing” en la fuente actual, la dividimos para dos, y se la restamos a 200 para centrar el texto horizontalmente. Para centrarlo verticalmente obtenemos la subida de la fuente actual, la dividimos para cuatro y se la añadimos a 200. La razón por la que dividimos la subida para cuatro NO está probablemente muy clara. Ahora es el momento de acometer un error común que ha llegado con Java 2. La figura 2.4 no es una forma exacta de documentar FontMetrics. Estas es la forma de la que hemos visto documentadas estas cosas en el tutorial Java y en casi todos los demás sitios. De todas formas, parece que hay unos pocos problemas con FontMetrics en Java 2 FCS. Aquí escribiremos un programa simple que demuestra estos problemas. Nuestro programa dibujará el texto “Swing” con una fuente de 36 puntos, negrita y monospaced. Dibujamos líneas en su subida, subida/2, subida/4, base, y bajada. La Figura 2.5 muestra el resultado.

37

Figura 2.5 La realidad cuando se trabaja con FontMetrics en Java 2

<<fichero figure2-5.gif>> El Código: TestFrame.java Ver \Chapter1\2\fontmetrics
import java.awt.*; import javax.swing.*; class TestFrame extends JFrame { public TestFrame() { super( "Lets get it straight!" ); getContentPane().add(new JCanvas()); } public static void main( String args[] ) { TestFrame mainFrame = new TestFrame(); mainFrame.pack(); mainFrame.setVisible( true ); } } class JCanvas extends JComponent { private static Font m_biFont = new Font("Monospaced", Font.BOLD, 36); public void paintComponent(Graphics g) { g.setColor(Color.black); // Negrita 36-puntos "Swing" g.setFont(m_biFont); FontMetrics fm = g.getFontMetrics(); int h = fm.getAscent(); g.drawString("Swing",50,50); // Prueba también: Ñ Ö Ü ^ // dibujar la línea de Subida g.drawLine(10,50-h,190,50-h); // dibujar la línea de Subida/2 g.drawLine(10,50-(h/2),190,50-(h/2)); // dibujar la línea de Subida/4 g.drawLine(10,50-(h/4),190,50-(h/4)); // dibujar la línea de Base g.drawLine(10,50,190,50); // dibujar la línea de Bajada g.drawLine(10,50+fm.getDescent(),190,50+fm.getDescent()); }

38

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

Le aconsejamos que pruebe este programa con diferentes tipos de fuente, tamaños, y con caracteres con marcas diacríticas como Ñ, Ö, o Ü. Observará que la subida es siempre mucho mayor de lo que normalmente está documentado que sería, y que la bajada es siempre menor. La forma más fiable de centrar el texto verticalmente que hemos encontrado es utilizar base + subida/4. Aún así, se puede usar también base + bajada y dependiendo de la fuente que se use puede ser más ajustado. La realidad es que no hay una forma de llevar esto a cabo correctamente a causa del estado actual de FontMetrics en Java 2.Puede experimentar resultados muy diferentes si no está usando la primera versión de Java 2. Es una buena idea ejecutar este programa y verificar si los resultados en su sistema son similares o no a los de la figura 2.5. Si no, será mejor que use un mecanismo de centrado diferente para su texto que debería ser simple de determinar mediante la experimentación con esta aplicación.
Nota: En el JDK1.1, para obtener una instancia de FontMetrics había que hacer lo siguiente: FontMetrics fm = Toolkit.getDefaultToolkit().getFontMetrics(myfont); El método Toolkit.getFontMetrics está desaconsejado en Java 2 y este código debería ser actualizado.

2.9 Usando el área de recorte de Graphics
Podemos usar el área de recorte para optimizar el dibujo de componentes. Esto puede que no mejore notablemente la velocidad de dibujado de componentes simples como nuestro JCanvas anterior, pero es importante comprender como implementar esta funcionalidad, ya que todo el sistema de dibujo de Swing está basado en este concepto (veremos más sobre esto en la siguiente sección). Modificamos ahora JCanvas para que cada una de nuestras superficies, strings e imágenes se pinte solamente si el área de recorte intersecciona con el rectángulo que lo limita. (Estas intersecciones son bastante fáciles de calcular, y podría ser útil que trabajase con ellas y las verificase una a una.) Adicionalmente, mantenemos un contador local que se incrementa cada vez que se pinta una de nuestros ítems. Al finalizar el método paintComponent() mostramos el número total de ítems que se pintaron. A continuación está nuestro método optimizado paintComponent() de JCanvas (con contador): El Código: JCanvas.java ver \Chapter1\3
public void paintComponent(Graphics g) { super.paintComponent(g); // contador int c = 0; // para int w = int h = int d = usarse a continuación 0; 0; 0;

// obtener área de recorte Rectangle r = g.getClipBounds(); int clipx = r.x; int clipy = r.y; int clipw = r.width;

39

int cliph = r.height; // dibujar sólo el área de recorte g.setColor(Color.white); g.fillRect(clipx,clipy,clipw,cliph); // dibujar el círculo amarillo si está dentro del área de recorte if (clipx <= 240 && clipy <= 240) { g.setColor(Color.yellow); g.fillOval(0,0,240,240); c++; } // dibujar el círculo magenta si está dentro del área de recorte if (clipx + clipw >= 160 && clipx <= 400 && clipy + cliph >= 160 && clipy <= 400) { g.setColor(Color.magenta); g.fillOval(160,160,240,240); c++; } w = m_flight.getIconWidth(); h = m_flight.getIconHeight(); // pintar el icono de debajo del cuadrado azul si está dentro del // área de recorte if (clipx + clipw >= 280-(w/2) && clipx <= (280+(w/2)) && clipy + cliph >= 120-(h/2) && clipy <= (120+(h/2))) { m_flight.paintIcon(this,g,280-(w/2),120-(h/2)); c++; } // pintar el icono de debajo del cuadrado rojo si está dentro del // área de recorte if (clipx + clipw >= 120-(w/2) && clipx <= (120+(w/2)) && clipy + cliph >= 280-(h/2) && clipy <= (280+(h/2))) { m_flight.paintIcon(this,g,120-(w/2),280-(h/2)); c++; } // dibujar el cuadrado rojo transparente si está dentro del área de // recorte if (clipx + clipw >= 60 && clipx <= 180 && clipy + cliph >= 220 && clipy <= 340) { g.setColor(m_tRed); g.fillRect(60,220,120,120); c++; } // dibujar el círculo verde transparente si está dentro del área de // recorte if (clipx + clipw > 140 && clipx < 260 && clipy + cliph > 140 && clipy < 260) { g.setColor(m_tGreen); g.fillOval(140,140,120,120); c++; } // dibujar el cuadrado azul transparente si está dentro del área de // recorte if (clipx + clipw > 220 && clipx < 380 && clipy + cliph > 60 && clipy < 180) { g.setColor(m_tBlue); g.fillRect(220,60,120,120); c++; } g.setColor(Color.black); g.setFont(m_biFont); FontMetrics fm = g.getFontMetrics(); w = fm.stringWidth("Swing");

40

h = fm.getAscent(); d = fm.getDescent(); // Negrita, Cursiva, 36-puntos "Swing" si está dentro del área de // recorte if (clipx + clipw > 120-(w/2) && clipx < (120+(w/2)) && clipy + cliph > (120+(h/4))-h && clipy < (120+(h/4))+d) { g.drawString("Swing",120-(w/2),120+(h/4)); c++; } g.setFont(m_pFont); fm = g.getFontMetrics(); w = fm.stringWidth("is"); h = fm.getAscent(); d = fm.getDescent(); // Normal, 12-puntos "is" si está dentro del área de recorte if (clipx + clipw > 200-(w/2) && clipx < (200+(w/2)) && clipy + cliph > (200+(h/4))-h && clipy < (200+(h/4))+d) { g.drawString("is",200-(w/2),200+(h/4)); c++; } g.setFont(m_bFont); fm = g.getFontMetrics(); w = fm.stringWidth("powerful!!"); h = fm.getAscent(); d = fm.getDescent(); // Negrita 24-puntos "powerful!!" si está dentro del área de recorte if (clipx + clipw > 280-(w/2) && clipx < (280+(w/2)) && clipy + cliph > (280+(h/4))-h && clipy < (280+(h/4))+d) { g.drawString("powerful!!",280-(w/2),280+(h/4)); c++; } System.out.println("# items repainted = " + c + "/10"); }

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

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

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

41

el dibujo de un componente y de todos sus hijos. Esto se consigue con un cambio lento, usando distintos destellos para indicar la región que se está pintando. Se intenta ayudar a encontrar problemas con el dibujo, la disposición, y las jerarquías de componentes -- y con cualquier cosa relacionada. Si está habilitada la depuración de gráficos, el objeto Graphics que se usa cuando se pinta es una instancia de DebugGraphics (una subclase de Graphics). JComponent, y por tanto todos los componente Swing, soporta la depuración de gráficos, que se puede activar/desactivar con el método setDebugGraphicsOptions() de JComponent. Este método recibe un int como parámetro que corresponde normalmente a uno de (o una combinación de bits -- usando el operador binario | ) los cuatro valores estáticos definidos en DebugGraphics.

2.10.1 Opciones de la depuración de gráficos
1. DebugGraphics.FLASH_OPTION: Cada operación de pintado produce un número determinado de destellos, de un determinado color y con un intervalo especificado. Los valores por defecto son: 250ms como intervalo, 4 destellos, y color rojo. Estos valores se pueden modificar con los siguientes métodos estáticos de DebugGraphics:
setFlashTime(int flashTime) setFlashCount(int flashCount) setFlashColor(Color flashColor)

Si no deshabilitamos el doble buffer mediante el RepaintManager (que veremos en la siguiente sección) no veremos el pintado en el momento en que ocurre:
RepaintManager.currentManager(null). setDoubleBufferingEnabled(false); Nota: Desactivar el doble buffer en el RepaintManager tiene el efecto de ignorar la propiedad doubleBuffered de todos los componentes.

2. DebugGraphics.LOG_OPTION: Envía mensajes describiendo todas las operaciones de pintado cuando ocurren. Por defecto estos mensajes se envían a la salida estándar (la consola -- System.out). Pero podemos cambiar el destino con el método estático setLogStream() de DebugGraphics. Este método recibe un PrintStream como parámetro. Para enviar la salida a un fichero haríamos algo como lo siguiente:
PrintStream debugStream = null; try { debugStream = new PrintStream( new FileOutputStream("JCDebug.txt")); } catch (Exception e) { System.out.println("can't open JCDebug.txt.."); } DebugGraphics.setLogStream(debugStream);

Si en algún punto tenemos que redirigir la salida de nuevo hacia la salida estándar:
DebugGraphics.setLogStream(System.out);

Podemos insertar cualquier cadena obteniendo el stream de salida con el método estático logStream() de DebugGraphics, e imprimiendo en él:
PrintStream ps = DebugGraphics.logStream(); ps.println("\n===> paintComponent ENTERED <===");

42

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

Todas las operaciones se imprimen con la siguiente sintaxis:
"Graphics" + (isDrawingBuffer() ? "<B>" : "") + "(" + graphicsID + "-" + debugOptions + ")"

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

Por ejemplo, con el registro y los destellos habilitados, vemos una salida parecida a esta para todas las operaciones:
Graphics(1-3) Setting color: java.awt.Color[r=0,g=255,b=0]

Las llamadas a los métodos de Graphics se añadirán al registro cuando está opción esté habilitada. La línea anterior se generó al hacerse una llamada a setColor(). 3. DebugGraphics.BUFFERED_OPTION: Se supone que desplegará un frame mostrando el dibujado tal como ocurre en el buffer invisible si está habilitado el doble-buffer. En 2 FCS esta opción no es funcional. 4. DebugGraphics.NONE_OPTION: Apaga la depuración de gráficos.

2.10.2 Advertencias sobre la depuración de gráficos
Hay varios asuntos con los que hay que tener cuidado cuando usamos la depuración de gráficos: 1. La depuración de gráficos no funcionará para cualquier componente cuyo UI sea null. Por tanto, si ha creado una subclase directa de JComponent sin un delegado UI, como hicimos anteriormente con JCanvas, la depuración de gráficos no hará nada. La forma más simple de evitar esto es definir un delegado UI trivial (vacío). Veremos como hacer esto con un ejemplo más tarde. 2. DebugGraphics no limpia cuando termina. Por defecto, se usa un color rojo para los destellos. Cuando se marca una región, ésta se rellena con ese color rojo del destello y no se borra (simplemente se pinta encima). Esto supone un problema porque el dibujo transparente no se mostrará transparente. En cambio, se fusionará con el rojo de abajo (o con cualquiera que sea el color del destello). Esto no supone necesariamente un defecto de diseño ya que nada nos impide usar un color completamente transparente para los destellos. Con un valor alpha de 0 el color del destello no se verá nunca. El único problema de esto es que no veremos ningún destello. De todos modos, en la mayoría de los casos es fácil de seguir lo que se está dibujando si ponemos flashTime y flashCount de forma que haya bastante tiempo entre operaciones.

2.10.3 Usando la depuración de gráficos
Ahora habilitaremos la depuración de gráficos en nuestro ejemplo de JCanvas de las dos últimas secciones. Como tenemos que tener un delegado UI, definimos una subclase trivial de ComponentUI e implementamos su método createUI() para que devuelva una instancia estática de si mismo:

43

class EmptyUI extends ComponentUI { private static final EmptyUI sharedInstance = new EmptyUI(); public static ComponentUI createUI(JComponent c) { return sharedInstance; } }

Para asociar adecuadamente este delegado UI con JCanvas, simplemente llamamos a super.setUI(EmptyUI.createUI(this)) desde el constructor de JCanvas. Configuramos también una variable de tipo PrintStream en JCanvas y la usamos para añadir unas pocas líneas propias al stream de registro durante el método paintComponent (para guardar cuando empieza y termina el método). No se ha hecho ningún otro cambio en el código paintComponent() de JCanvas. En nuestra aplicación de prueba, TestFrame, creamos una instancia de JCanvas y habilitamos la depuración de gráficos con las opciones LOG_OPTION y FLASH_OPTION. Deshabilitamos el doble buffer con RepaintManager, ponemos el intervalo de los destellos a 100ms, el número de éstos a 2, y usamos un color totalmente transparente. El Código: TestFrame.java ver \Chapter1\4
import import import import java.awt.*; javax.swing.*; javax.swing.plaf.*; java.io.*;

class TestFrame extends JFrame { public TestFrame() { super( "Graphics demo" ); JCanvas jc = new JCanvas(); RepaintManager.currentManager(jc). setDoubleBufferingEnabled(false); jc.setDebugGraphicsOptions(DebugGraphics.LOG_OPTION | DebugGraphics.FLASH_OPTION); DebugGraphics.setFlashTime( 100 ); DebugGraphics.setFlashCount( 2 ); DebugGraphics.setFlashColor(new Color(0,0,0,0)); getContentPane().add(jc); } public static void main( String args[] ) { TestFrame mainFrame = new TestFrame(); mainFrame.pack(); mainFrame.setVisible( true ); } } class JCanvas extends JComponent { // Código de la sección 2.9 intacto private PrintStream ps; public JCanvas() { super.setUI(EmptyUI.createUI(this)); }

44

public void paintComponent(Graphics g) { super.paintComponent(g); ps = DebugGraphics.logStream(); ps.println("\n===> paintComponent ENTERED <==="); // Todo el código de pintado intacto ps.println("\n# items repainted = " + c + "/10"); ps.println("===> paintComponent FINISHED <===\n"); } // Código de la sección 2.9 intacto } class EmptyUI extends ComponentUI { private static final EmptyUI sharedInstance = new EmptyUI(); public static ComponentUI createUI(JComponent c) { return sharedInstance; } }

Poniendo LOG_OPTION, la depuración de gráficos nos ofrece una mejor información para verificar correctamente como funciona nuestra optimización del área de recorte (de la última sección). Cuando se ejecuta este ejemplo se verá la siguiente salida en su consola (suponiendo que no tape la región visible de JCanvas cuando se pinta por primera vez):
Graphics(0-3) Enabling debug Graphics(0-3) Setting color: javax.swing.plaf.ColorUIResource[r=0,g=0,b=0] Graphics(0-3) Setting font: javax.swing.plaf.FontUIResource[family=dialog,name=Dialog, style=plain,size=12] ===> paintComponent ENTERED <=== Graphics(1-3) Setting color: java.awt.Color[r=255,g=255,b=255] Graphics(1-3) Filling rect: java.awt.Rectangle[x=0,y=0, width=400,height=400] Graphics(1-3) Setting color: java.awt.Color[r=255,g=255,b=0] Graphics(1-3) Filling oval: java.awt.Rectangle[x=0,y=0, width=240,height=240] Graphics(1-3) Setting color: java.awt.Color[r=255,g=0,b=255] Graphics(1-3) Filling oval: java.awt.Rectangle[x=160,y=160,width=240,height=240] Graphics(1-3) Drawing image: sun.awt.windows.WImage@32a5625a at: java.awt.Point[x=258,y=97] Graphics(1-3) Drawing image: sun.awt.windows.WImage@32a5625a at: java.awt.Point[x=98,y=257] Graphics(1-3) Setting color: java.awt.Color[r=255,g=0,b=0] Graphics(1-3) Filling rect: java.awt.Rectangle[x=60,y=220,width=120,height=120] Graphics(1-3) Setting color: java.awt.Color[r=0,g=255,b=0] Graphics(1-3) Filling oval: java.awt.Rectangle[x=140,y=140,width=120,height=120] Graphics(1-3) Setting color: java.awt.Color[r=0,g=0,b=255] Graphics(1-3) Filling rect: java.awt.Rectangle[x=220,y=60,width=120,height=120] Graphics(1-3) Setting color: java.awt.Color[r=0,g=0,b=0] Graphics(1-3) Setting font: java.awt.Font[family=monospaced.bolditalic,name=Mono

45

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

2.11 Pintado y validación
En el corazón del mecanismo de pintado y validación de JComponent está RepaintManager. Es RepaintManager el que es responsable de enviar peticiones de pintado y validación a la cola de eventos del sistema de AWT para su despacho. Para resumir, lo hace interceptando las peticiones de repaint() y revalidate(), combinando cualquier petición cuando sea posible, envolviéndolas en objetos Runnable, y enviándolas a invokeLater(). Hay unos pocos temas que merecen más atención aquí antes de que veamos los detalles de los procesos de pintado y validación.
Nota: Esta sección contiene una explicación relativamente exhaustiva de los más complejos mecanismos subyacentes de Swing. Si es relativamente nuevo en Java o Swing le recomendamos que se la salte . Si está buscando información sobre como sobreescribir y usar sus propios métodos de pintado, vaya a la sección 2.8. Para personalizar el dibujo de los delegados UI vea el capítulo 21.

2.11.1 Doble buffer
Hemos mencionado el doble buffer, como deshabilitarlo en el RepaintManager, y como especificar el doble buffer de componentes individuales con el método setDoubleBuffered() de JComponent. Pero, ¿cómo trabaja? El doble buffer es la técnica de pintar en una pantalla invisible en lugar de hacerlo directamente en un componente visible. Al final, la imagen resultante se pinta en la pantalla (lo que sucede relativamente deprisa). Cuando se usan componentes AWT, los desarrolladores debían implementar su propio doblebuffer para reducir el parpadeo. Estaba claro que el doble buffer debía ser una funcionalidad interna a causa de su extendido uso. Por tanto, no sorprende mucho encontrar esta funcionalidad en todos los componentes Swing Internamente, el doble buffer consiste en crear una Image y obtener su objeto Graphics para usarlo en todos los métodos de pintado. Si el componente que vamos a pintar tiene hijos, este objeto Graphics se pasará a ellos para usarlo para pintar, y así sucesivamente. Por tanto, si estamos usando doble-buffer en un componente, todos sus hijos lo estarán haciendo también (lo tengan habilitado o no) porque dibujaran en el mismo objeto Graphics. Vea que sólo hay una pantalla invisible para cada RepaintManager, y sólo hay normalmente una instancia de RepaintManager para cada applet o aplicación (RepaintManager es una clase de servicio que registra una instancia compartida de si misma con AppContext--ver sección 2.5). Como veremos en el capítulo 3, JRootPane es el componente Swing de más alto nivel en cualquier ventana (lo que incluye a JInternalFrame -- aunque no sea realmente una ventana). Habilitando el doble buffer en JRootPane, todos sus hijos se pintarán también usando doble buffer. Como vimos en la última sección, RepaintManager también ofrece un control global sobre el doble buffer de todos los

46

componentes. Por tanto, otra forma de garantizar que todos los componentes usen doble buffer es llamar a:
RepaintManager.currentManager(null).setDoubleBufferingEnabled(true);

2.11.2 Dibujo optimizado
No hemos visto el hecho de que los componentes puedan superponer a cualquier otro en Swing todavía, pero pueden hacerlo. JLayeredPane, por ejemplo, es un contenedor que permite que cualquier número de componentes se superponga a cualquier otro. Repintar este tipo de contedor es mucho más complicado que repintar otro contenedor que sepamos que no permite la superposición, principalmente a causa de la posibilidad de que los componentes sean transparentes. ¿Qué significa para un componente el ser transparente? Técnicamente significa que su método
isOpaque() devolverá false. Podemos modificar esta propiedad llamando al método setOpaque(). Lo que la opacidad significa en este contexto, es que un componente pintará todos los

pixels dentro de sus límites. Si está a false, no se garantiza que pase esto. Generalmente está puesto a true, pero veremos que cuando está puesta a false se incrementa la carga de trabajo de todo el mecanismo de pintado. A no ser que estemos construyendo un componente que no tiene que rellenar toda su región rectangular (como haremos en el capítulo 5 con los botones poligonales), deberíamos dejar siempre esta propiedad a true, como está por defecto para la mayoría de los componentes (Este valor lo pone normalmente un delegado UI). El método isOptimizedDrawingEnabled() de JComponent está sobreescrito para que devuelva true para casi todas las subclases de JComponent excepto: JLayeredPane, JViewport, y JDesktopPane (una subclase de JLayeredpane). Básicamente, llamar a este método es equivalente a preguntarle a un componente: ¿es posible que uno de tus componentes hijos se superponga a los otros? Si lo es, entonces hay un montón de trabajo de repintado más que hacer para tener en cuenta el hecho de que cualquier número de componentes, de cualquier parte de nuestra jerarquía de componentes, se pueda superponer a los demás. Adicionalmente, como los componentes pueden ser transparentes, los componentes situados completamente debajo de otros se pueden ver aún a través de éstos. Este tipo de componentes no son necesariamente hermanos (están en el mismo contenedor) porque podemos tener varios contenedores no opacos puestos uno sobre otro. En situaciones como esta, tenemos que hacer un recorrido completo del árbol para determinar que componentes tienen que ser refrescados. Si se ha sobreescrito isOptimizedDrawingEnabled() para que devuelva true, asumimos que no tenemos que considerar una situación como esta. Es por ello, que el dibujo es más eficiente, o 'optimizado'.

2.11.3 El interior de la validación
Una petición de revalidate() se genera cuando un componente tiene que ser situado de nuevo, Cuando se recibe una petición de un determinado componente, tiene que haber alguna manera de determinar si posicionar ese componente afectará a algún otro. El método isValidateRoot() de JComponent devuelve false para la mayoría de los componentes. Básicamente, llamar a este método es equivalente a preguntar: si posiciono tu contenido de nuevo, ¿puedes garantizarme que ninguno de tus padres o hermanos se verá afectado desfavorablemente (tendrá que ser posicionado de nuevo)? Por defecto, sólo JRootPane, JScrollPane, y JTextField devuelven true. Esto puede parecer sorprendente al principio, pero aparentemente es cierto que estos componentes son los únicos componentes Swing cuyo contenido puede ser posicionado de forma exitosa, en cualquier situación (suponiendo que no hay componentes pesados), sin afectar a los padres o hermanos. No importa lo grande que hagamos algo dentro de un JRootPane, JScrollPane, o JTextField, ellos no cambiarán el tamaño o la posición si no es a causa de alguna influencia exterior (p.e. un hermano o un padre). Para ayudarle a que se convenza, intente añadir un componente de texto multi-línea en un contenedor sin ponerlo dentro de un panel de scroll. Puede observar que la creación de nuevas líneas cambiará su tamaño (dependiendo del posicionamiento). Lo importante no es que pase raramente o que

47

se pueda prevenir, sino que puede pasar. Este es el único tipo de situación sobre la que isValidateRoot() nos avisará. Por tanto, ¿dónde se usa este método? Un componente o su padre se revalida normalmente cuando el valor de una propiedad cambia y el tamaño, la posición o el posicionamiento interno del componente se ven afectados. Llamando recusivamente a isValidateRoot() en el padre de un componente Swing hasta que obtengamos true, terminaremos con el ascendiente más cercano de ese componente que nos garantice que su validación no afectará a sus padres o hermanos. Veremos que RepaintManager depende de este método para despachar peticiones de validación.
Nota: Por hermanos queremos decir componentes del mismo contenedor. Por padres queremos decir contenedores padre.

2.11.4 RepaintManager
Como sabemos, normalmente hay sólo una instancia en uso de una clase de servicio para cada applet o aplicación. Por tanto, a no ser que creemos específicamente nuestra propia instancia de RepaintManager, lo que no necesitaremos hacer casi nunca, todo el repintado es manejado por la instancia compartida que está registrada con AppContext. Normalmente la obtenemos llamando al método estático currentManager() de RepaintManager: myRepaintManager = RepaintManager.currentManager(null); Este método recibe un Component como parámetro. De todos modos, no importa lo que le pasemos. De hecho el componente que se pasa a este método no se usa en ningún sitio del método (vea el código fuente de RepaintManager.java), por lo que se puede usar un valor null de forma segura. (Esta definición existe para que la usen subclases que quieran trabajar con más de un RepaintManager, posiblemente usando uno para cada componente.)
RepaintManager existe para dos propósitos: para proveer revalidación y repintado eficientes. Intercepta todas las peticiones repaint() y revalidate(). Esta clase maneja también todo el doble buffer en Swing y mantiene una Image sencilla para este propósito. El tamaño máximo de esta Image es por defecto el tamaño de la pantalla. Aún así, podemos modificar el mismo manualmente usando el método setDoubleBufferMaximumSize() de RepaintManager. (El resto de funcionalidades de RepaintManager se verá a lo largo de esta sección donde sean aplicables.) Nota: Los cell renderers que se usan en componentes como JList, JTree, y JTable son especiales ya que están envueltos en instancias de CellRendererPane y todas las peticiones de validación y repintado no se propagan por la jerarquía. Vea el capítulo 17 para saber más CellRendererPane y la causa de este comportamiento. Es suficiente que sepa que los cell renderers no siguen el esquema de pintado y validación que hemos visto en esta sección.

2.11.5 Revalidación
RepaintManager mantiene un Vector de componentes que tienen que ser validados. Cuando quiera que una petición revalidate es interceptada, se envía el componente fuente al método addInvalidComponent() y se chequea su propiedad “validateRoot” usando isValidateRoot(). Esto sucede recursivamente en los padres del componente hasta que isValidateRoot() devuelve

true. Se chequea entonces la visibilidad del componente resultante, si es que hay alguno. Si alguno de los contenedores padre no es visible no hay razón para revalidarlo. En otro caso RepaintManager recorre su árbol hasta que alcanza el componente raíz, un Window o Applet. RepaintManager chequea entonces el Vector de componentes invalidos y si en él no está aún el componente lo añade. Después

48

de

que se añada con éxito, RepaintManager pasa entonces la raíz al método queueComponentWorkRequest() de SystemEventQueueUtilities (vimos esta clase en la sección 2.3). Este método comprueba si ya hay un ComponentWorkRequest (esta es una clase privada estática en SystemEventQueueUtilities que implementa Runnable) que corresponda a esta raíz guardada en la tabla de peticiones de trabajos. Si no hay ninguna, se crea una nueva. Si ya hay una, simplemente tomamos una referencia a ella. Entonces sincronizamos el acceso a esa ComponentWorkRequest, la ponemos en la tabla de peticiones de trabajos si es nueva, y comprobamos si está pendiente (p.e. si ha sido añadida a la cola de eventos del sistema de AWT). Si no está pendiente, la enviamos a SwingUtilities.invokeLater(). Se marca entonces como pendiente y dejamos el bloque sincronizado. Cuando se ejecuta finalmente en el hilo de despacho de eventos notifica a RepaintManager que ejecute validateInvalidComponents(), seguido de paintDirtyRegions(). El método validateInvalidComponents() básicamente comprueba el Vector de RepaintManager que contiene los componentes que necesitan validación, y llama a validate() en cada uno de ellos. (Este método es en la actualidad un poco más cuidadoso de lo que describimos aquí, ya que sincroniza el acceso para evitar la adición de componentes invalidos durante la ejecución).
Note: Recuerde que se debería llamar a validateInvalidComponents() sólo dentro del hilo de despacho de eventos. Nunca llame a este método desde cualquier otro hilo. La misma regla se aplica para paintDirtyRegions().

El método paintDirtyRegions() es mucho más complicado, y veremos alguno de esos detalles más adelante. Por ahora, es suficiente con saber que este método pinta todas las regiones que lo precisen de todos los componentes que se mantengan en RepaintManager.

2.11.6 Repintado
JComponent define dos métodos repaint(), y se hereda la versión sin argumentos de repaint() que existe en java.awt.Container: public void repaint(long tm, int x, int y, int width, int height) public void repaint(Rectangle r) public repaint() // heredado de java.awt.Container

Si llama a la versión sin argumentos se repinta todo el componente. Para componentes pequeños y simples esto está bien, pero para los más grandes y complejos esto no es eficiente. Los otros dos métodos reciben los límites de la región que debe repintarse (la región sucia) como parámetro. Los parámetros de tipo int del primer método corresponden a las coordenadas x e y, la anchura y la altura de esa región. El segundo recibe la misma información encapsulada en una instancia de Rectangle. El segundo método repaint() mostrado anteriormente llama directamente al primero. El primer método envía los parametros de la región sucia al método addDirtyRegion() de RepaintManager.
Nota: El parámetro long del primer método repaint() no representa absolutamente nada y no se usa. No importa el valor que use para él. La única razón por la que está ahí es para sobreescribir el método repaint() correcto de java.awt.Component. RepaintManager contiene una Hashtable de regiones sucias. Cada componente tendrá como

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

49

en addInvalidateComponent()). Se pregunta a la Hashtable de regiones sucias si ya tiene guardada una región sucia de nuestro componente. De ser así, devuelve su valor, un Rectangle, y el método SwingUtilities.computeUnion() conveniente se usa para combinar la nueva región sucia con la anterior. Finalmente, RepaintManager pasa la raíz al método queueComponentWorkRequest() de SystemEventQueueUtilities. Lo que sucede a partir de aquí es idéntico a lo que vimos para la revalidación (ver más arriba). Ahora podemos hablar un poco sobre el método paintDirtyRegions() que resumimos anteriormente. (Recuerde que debería llamarse sólo dentro del hilo de despacho de eventos.) Este método empieza creando una referencia local a la Hashtable de regiones sucias de RepaintManger, y redirigiendo la Hashtable de regiones sucias de RepaintManager hacia una nueva vacía. Esto se hace en una sección crítica de forma que no se pueden añadir regiones sucias mientras sucede el intercambio.El resto de este método es bastante largo y complicado por lo que concluiremos con un resumen del código más significativo (ver RepaintManager.java para más detalles). El método paintDirtyRegions() continúa iterando a través de una Enumeration de componentes sucios. Llamando al método collectDirtyComponents() de RepaintManager para cada uno. Este método mira en todos los ancestros del componente sucio especificado y comprueba cualquier superposición con su región sucia usando el método SwingUtilities.computeIntersection(). De esta forma todos los límites de la región sucia se minimizan de forma que sólo se mantiene la región visible. (Observe que collectDirtyComponents() tiene en cuenta la transparencia.) Una vez que se ha hecho esto para cada componente sucio, el método paintDirtyRegions() entra en un bucle. Este bucle calcula la intersección final de cada componente sucio con su región sucia. Al final de cada iteración se llama a paintImmediately() en el componente sucio asociado, que pinta en ese momento todas las regiones sucias minimizadas en su posición correcta (veremos esto más adelante). Esto completa el método paintDirtyRegions(), pero todavía tenemos que ver el aspecto más importante de todo el proceso: el pintado.

2.11.7 Pintar
JComponent incluye un método update() que simplemente llama a paint(). El método update() no se usa actualmente en ningún componente Swing, pero se provee por compatibilidad. El método paint() de JComponent, al contrario que las implementaciones AWT de paint(), no

maneja todo el pintado de un componente. De hecho, es bastante raro que maneja algo de él directamente. El único trabajo del método paint() de JComponent es el manejo de las áreas de recorte, translaciones, y pintar piezas de Image que se usan por RepaintManager para doble buffer. El resto del trabajo se delega en otros métodos.Veremos en breve cada uno de estos métodos y el orden en que las operaciones de pintado suceden, pero primero tenemos que saber como se invoca a paint(). Como sabemos por nuestro repaso al proceso de repintado, RepaintManager es responsable de llamar a un método llamado paintImmediately() en todos los componentes para pintar su región sucia (recuerde que hay siempre una sola región sucia para cada componente porque RepaintManager las combina inteligentemente). Este método, y el privado al que llama, hacen un repintado artístico incluso más espectacular. Primero comprueba si el componente destino es visible, por si ha sido movido, ocultado o eliminado desde que se hizo la petición. Entonces recorre los padres no opacos del componente (usando isOpaque()) y aumenta los límites de la región a repintar adecuadamente hasta que alcanza un padre opaco: 1. Si el padre alcanzado es una subclase de JComponent, se llama al método privado _paintImmediately() y se le pasa la nueva región calculada. Este método interroga al método isOptimizedDrawing(), comprueba si está habilitado el doble buffer (en cuyo caso utiliza la pantalla invisible del objeto Graphics asociado a la Image del RepaintManager), y continúa trabajando con isOpaque() para determinar el componente padre final y sus límites para invocar a paint().

50

A. Si está habilitado el doble buffer, llama a paintWithBuffer() (otro método privado). Este método trabaja con el objeto Graphics de la pantalla invisible y su área de recorte para generar llamadas al método paint() del padre (pasándole el objeto Graphics usando un área de recorte distinta cada vez). Después de cada llamada a paint(), usa el objeto Graphics resultante para dibujar directamente al componente visible. (En este caso específico, el método paint() no usará ningún buffer internamente ya que sabe, porque comprueba algunos flags que no explicaremos, que se está teniendo cuidado con el proceso de buffer en algún otro sitio.) B. Si no está habilitado el doble buffer, se hace simplemente una llamada al paint() del padre. 2. Si el padre no es un JComponent, se envian los límites de la región al método repaint() de ese padre, que normalmente llamará al método paint() de java.awt.Component. Este método reenviará entonces el tráfico a todos los métodos paint() de sus hijos ligeros. De todas formas, antes de hacer esto se asegura de que todos los hijos ligeros a los que notifica no están completamente cubiertos por el área de recorte actual del objeto Graphics que se pasó. ¡En todos los casos hemos alcanzado finalmente el método paint() de JComponent! Dentro del método paint() de JComponent, si está habilitada la depuración de gráficos se usará una instancia de DebugGraphics para todo el pintado. Una mirada rápida al código de pintado de JComponent muestra un gran uso de una clase llamada SwingGraphics. Ésta no está en los documentos del API porque es privada de paquete. Parece ser una clase muy útil para manejar translaciones personalizadas, manejo del área de recorte, y una Stack (pila) de objetos Graphics que se usa para caché, reciclaje, y operaciones de tipo deshacer. SwingGraphics funciona actualmente como un envoltorio para todas las instancias de Graphics usadas durante el proceso de pintado. Sólo se puede instanciar pasándole un objeto Graphics existente. Esta funcionalidad se ha hecho incluso más explícita, por el hecho de que implementa un interface llamado GraphicsWrapper, que es también privado de paquete. El método paint() comprueba si el doble buffer está habilitado y si se llamó a este método desde paintWithBuffer() (ver más arriba): 1. Si se llamó a paint() desde paintWithBuffer() o si no está habilitado el doble buffer, paint() comprueba si el área de recorte del objeto Graphics actual está totalmente oscurecida por algún componente hijo. Si no lo está, se llama a paintComponent(), paintBorder(), y paintChildren() en ese orden. Si está completamente oscurecida, sólo hace falta llamar a paintChildren(). (Veremos lo que hacen estos tres métodos dentro de poco.) 2. Si está habilitado el doble buffer y no se llamó desde paintWithBuffer(), usará el objeto Graphics de pantalla invisible de la Image asociada con RepaintManager durante el resto de este método. Comprobará entonces si el área de recorte del objeto Graphics actual está completamente oscurecida por los componentes hijo. Si no lo está, se llama a paintComponent(), paintBorder(), y paintChildren() en ese orden. Si lo está sólo es necesario llamar a paintChildren(). A. El método paintComponent() comprueba si el componente tiene un delegado UI instalado. Si no lo tiene simplemente sale. Si lo tiene, llama a update() en ese delegado UI y sale. El método update() de un delegado UI es normalmente responsable del pintado del fondo de un componente, si es opaco, y entonces llama a paint(). El método paint() de un delegado UI es el que pinta el contenido del componente correspondiente. (Veremos como personalizar los delegados UI extensamente a lo largo de este texto.)

51

B. El método paintBorder() pinta simplemente el borde del componente si lo tiene. C. El método paintChildren() está un poco más implicado en el proceso. Para resumir, busca por todos los componentes hijo y determina si se debería invocar a paint() en éstos usando el área de recorte de Graphics actual, el método isOpaque() y el método isOptimizedDrawingEnabled(). El método paint() llamado en cada hijo iniciará esencialmente el proceso de pintado del hijo desde la parte 2 de arriba, y este proceso se repetirá hasta que no existan más hijos o no necesiten ser pintados. Cuando construimos o creamos subclases de componentes Swing ligeros se espera normalmente que si queremos pintar algo dentro del mismo componente (en lugar de en el delegado UI que es donde lo haremos habitualmente) sobreescribamos el método paintComponent() y llamemos inmediatamente a super.paintComponent(). De esta forma daremos al delegado UI la oportunidad de que dibuje el componente primero. Sobreescribir el método paint(), o cualquier otro de los métodos mencionado anteriormente será rara vez necesario, y siempre es una buena práctica evitar hacerlo.

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

le abandona:
focusCycleRoot: esta especifica si el componente contiene un ciclo de foco propio. Si contiene un

ciclo de foco, el foco entrará en este componente y se moverá a través de su ciclo de foco hasta que se envíe fuera de ese componente manualmente o mediante código. Por defecto está propiedad es false (para la mayoría de los componentes), y no se puede cambiar con un método típico de acceso setXX(). Sólo se puede cambiar sobreescribiendo el método isFocusCycleRoot() y devolviendo el valor booleano apropiado.
managingFocus: esta especifica si los KeyEvents correspondientes a un cambio de foco serán enviados al componente o interceptados y consumidos por el FocusManager. Por defecto esta propiedad es false (para la mayoría de los componentes), y no se puede cambiar con un método típico de acceso setXX(). Sólo se puede cambiar sobreescribiendo el método isManagingFocus() y devolviendo el valor booleano apropiado. focusTraversable: esta especifica si el foco se puede transferir al componente por el FocusManager a causa de un desplazamiento del foco en el ciclo. Por defecto esta propiedad es true (para la mayoría de los componentes), y no se puede cambiar con un método típico de acceso setXX(). Sólo se puede cambiar sobreescribiendo el método isFocusTraversable() y

devolviendo el valor booleano apropiado. (Observe que cuando el foco alcanza a un componente a través de una pulsación de ratón se llama a su método requestFocus(). Sobreescribiendo requestFocus() podemos responder a peticiones de foco de manera específica para cada componente.)
requestFocusEnabled: especifica si una pulsación de ratón dará el foco a ese componente. Esto no afecta al trabajo del FocusManager , que continuará transfiriendo el foco al componente como parte del ciclo del foco. Por defector esta propiedad es true (para la mayoría de los componentes), y se puede cambiar con el método setRequestFocusEnabled() de JComponent . nextFocusableComponent: esta especifica el componente al que se transfiere el foco cuando se

pulsa la tecla TAB. Por defecto está puesto a null, ya que el camino del foco se maneja para

52

nosotros

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

2.12.1 FocusManager clase abstracta javax.swing.FocusManager
Esta clase define la responsabilidad de determinar como se mueve el foco de un componente a otro. FocusManager es una clase de servicio cuya instancia compartida se guarda en la tabla de servicio AppContext (ver 2.5). Para acceder a FocusManager usamos su método estático getCurrentManager(). Para asignar un nuevo FocusManager usamos el método estático setCurrentManager(). Podemos deshabilitar el servicio actual FocusManager usando el método estático disableFocusManager(), y podemos comprobar si está habilitado o no en un momento determinado usando el método estático isFocusManagerEnabled(). Los siguientes tres métodos abstractos se tienen que definir en las subclases: focusNextComponent(Component aComponent): se debería llamar para desplazar el foco al siguiente componente en el ciclo del foco cuya propiedad focusTraversable sea true.
focusPreviousComponent(Component aComponent): se debería llamar para desplazar el foco al anterior componente en el ciclo del foco cuya propiedad focusTraversable sea true. processKeyEvent(Component focusedComponent, KeyEvent anEvent): se debería llamar para, o bien consumir un KeyEvent enviado al componente, o bien para permitirle ser

procesado por el componente. Este método se usa normalmente para determinar si una pulsación de teclado corresponde a un desplazamiento en el foco. Si este es el caso, el KeyEvent se consume normalmente y se mueve el foco hacia delante o hacia atrás usando los métodos focusNextComponent() o focusPreviousComponent() respectivamente.

Nota: “FocusManager recibirá los eventos de teclado KEY_PRESSED, KEY_RELEASED y KEY_TYPED. Si se consume un evento, todos los demás eventos se deberían consumir también.”API

2.12.2 DefaultFocusManager clase javax.swing.DefaultFocusManager
DefaultFocusManager desciende de FocusManager y define los tres métodos requiridos, así como varios métodos adicionales. El método más importante en esta clase es compareTabOrder(), que recibe dos Components como parámetros y determina en primer lugar cual de ellos está situado más

cerca de la parte de arriba del contenedor para que sea la raíz del ciclo del foco. Si ambos está situados a la misma altura este método determinará cual de ellos está más a la izquierda. Se devolverá un valor de true si el primer componente pasado debe obtener el foco antes que el segundo. En otro caso devolverá false. Los métodos focusNextComponent() y focusPreviousComponent() desplazan el foco como se esperaba, y los métodos getComponentBefore() y getComponentAfter() se definen para devolver el componente anterior y posterior respectivamente, que recibirán el foco después de un determinado componente en el ciclo del foco. Los métodos getFirstComponent() y getLastComponent() devuelven el primer componente y el último que recibirán el foco en el ciclo del foco de un determinado contenedor.

53

El método processKeyEvent() intercepta KeyEvents enviados al componente que posee actualmente el foco. Si estos eventos corresponden a un desplazamiento del foco (p.e. TAB, CTRLTAB, SHIFT-TAB, y SHIFT-CTRL-TAB) se consumen y se cambia el foco adecuadamente. En caso contrario, estos eventos se envían al componente para ser procesados (ver sección 2.13). Observe que el FocusManager siempre intercepta los eventos de teclado.
Nota: Por defecto, CTRL-TAB y SHIFT-CTRL-TAB se pueden usar para desplazar el foco fuera de componentes de texto. TAB y SHIFT-TAB moverán el cursor en su lugar (ver capítulos 11 y 19).

2.12.3 Escuchando cambios del foco
Como con los componentes AWT, podemos escuchar cambios del foco en un componente adjuntando una instancia del interface java.awt.FocusListener. FocusListener define dos métodos, cada uno de los cuales recibe una instancia de java.awt.FocusEvent como parámetro: focusGained(FocusEvent e): este método recibe un FocusEvent cuando se da el foco a un componente al que se le ha añadido este oyente.
focusLost(FocusEvent e): este método recibe un FocusEvent cuando se pierde el foco en un

componente al que se le ha añadido este oyente.
FocusEvent desciende de java.awt.ComponentEvent y define, entre otros, los identificadores FOCUS_LOST y FOCUS_GAINED para distinguir entre sus dos tipos de eventos. Un evento FOCUS_LOST ocurrirá correspondiendo a una pérdida de foco temporal o permanente. Las pérdidas

ocurren cuando otra aplicación u otra ventana recibe el foco. Cuando el foco vuelve a esta ventana, el componente que perdió el foco lo obtendrá de nuevo, y un evento FOCUS_GAINED se despachará en ese momento. Las pérdidas permanentes de foco ocurren cuando el foco se mueve a causa de una pulsación en otro componente de la misma ventana, o mediante código al invocar a requestFocus() en otro componente, o despachando algún KeyEvent que cause un cambio de foco en el método processKeyEvent() de FocusManager. Como es lógico, podemos añadir o borrar implementaciones de FocusListener a cualquier componente Swing usando los métodos addFocusListener() y removeFocusListener() de Component respectivamente.

2.13 Entrada de teclado, KeyStrokes, y Actions
2.13.1 Escuchando la entrad de teclado
Se lanzan KeyEvents por un componente siempre que el componente tiene el foco y el usuario pulsa una tecla. Para escuchar estos eventos en un componente particular podemos añadir KeyListeners usando el métodos addKeyListener(). KeyEvent desciende de InputEvent y, al contrario que la mayoría de los eventos, los KeyEvents se despachan antes de que la operación correspondiente tome parte (p.e. en un cuadro de texto la operación podría ser añadir un carácter específico al contenido del documento). Podemos consumir estos eventos usando el método consume() antes de que se manejen más adelante por asociaciones de teclas u otros oyentes. (más adelante veremos exactamente como tener notificación de la entrada de teclado, y en que orden ocurre ésta). Hay tres tipos de eventos KeyEvent, cada uno de los cuales ocurre por lo menos una vez cada activación de teclado (p.e. pulsar y soltar una tecla del teclado): KEY_PRESSED: este tipo de evento de tecla se genera cuando una tecla del teclado se pulsa. La tecla que se ha pulsado queda especificada por la propiedad keyCode y un código virtual de la tecla se puede obtener con el método getKeyCode() de KeyEvent. Un código virtual de tecla se usa para informar de la tecla exacta del teclado que ha causado el evento, tal como KeyEvent.VK_ENTER. KeyEvent define numerosas constantes estáticas de tipo int, que

54

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

veremos más adelante, ocurren mucho menos a menudo).
KEY_TYPED: este tipo de eventos se lanzan en algún momento entre un evento KEY_PRESSED y un evento KEY_RELEASED. Nunca contiene una propiedad keyCode correspondiente a la tecla pulsada, y se devolverá 0 siempre que se llame a getKeyCode() en un evento de este tipo.

Observe que para teclas sin representación Unicode (como RE PAG, PRINT SCREEN, etc.), no se lanzará el evento KEY_TYPED. La mayoría de las teclas con representación Unicode, cuando se mantienen pulsadas durante un rato, generarán repetidos KEY_PRESSED y KEY_TYPED (en este orden). El conjunto de teclas que muestran este comportamiento, y el porcentaje en que lo hacen, no se puede controlar y depende de la plataforma. Cada KeyEvent mantiene un conjunto de modificadores que especifica el estado de las teclas SHIFT, CTRL, ALT, y META. Este es un valor de tipo int que es el resultado de un or binario entre InputEvent.SHIFT_MASK, InputEvent.CTRL_MASK, InputEvent.ALT_MASK, y InputEvent.META_MASK (dependiendo de que teclas están pulsadas en el momento del evento). Podemos obtener este valor con getModifiers(), y podemos comprobar específicamente cual de estas teclas estaba pulsada en el momento en que se lanzó evento usando isShiftDown(), isControlDown(), isAltDown(), y isMetaDown().
KeyEvent también contiene la propiedad booleana actionKey que especifica si la tecla que lo ha lanzado corresponde a una acción que debería ejecutar la aplicación (true) o si son datos que se usan normalmente para cosas como la adición de contenido a un componente de texto (false). Podemos usar el método isActionKey() de KeyEvent para obtener el valor de esta propiedad.

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

55

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

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

El último método devolverá un KeyStroke con las propiedades correspondientes a los atributos del KeyEvent. Las propiedades keyCode, keyChar, y modifiers se toman del KeyEvent y la propiedad onKeyRelease se pone a true si el tipo del evento es KEY_RELEASED y a false en caso contrario. Para registrar una combinación KeyStroke/ActionListener con un JComponent podemos usar su método registerKeyBoardAction(ActionListener action, KeyStroke stroke, int condition). El parámetro ActionListener tiene que estar definido de forma que su método actionPerformed() haga las operaciones necesarias cuando se intercepte entrada de teclado correspondiente al parámetro KeyStroke. El parámetro int especifica bajo que condiciones se considera valido el KeyStroke: JComponent.WHEN_FOCUSED: sólo se llamará al correspondiente ActionListener si el componente con el que está registrado este KeyStroke tiene el foco.
JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT: sólo se llamará al correspondiente ActionListener si el componente con el que está registrado este KeyStroke es ancestro

(contiene) del componente que tiene el foco.
JComponent.WHEN_IN_FOCUSED_WINDOW: sólo se llamará al correspondiente ActionListener si el componente con el que está registrado este KeyStroke está en algún lugar de la ventana de más alto nivel (p.e. JFrame, JDialog, JWindow, JApplet, o algún otro

componente pesado) que tiene el foco. Observe que las acciones de teclado registradas con esta condición se manejan en una instancia de la clase privada de servicio KeyBoardManager (ver 2.13.4) en lugar de en el componente. Por ejemplo, para asociar la invocación de un ActionListener a la pulsación de ALT-H sin importar el componente que tenga el foco en un JFrame determinado, podemos hacer lo siguiente:
KeyStroke myKeyStroke = KeyStroke.getKeyStroke(KeyEvent.VK_H, InputEvent.ALT_MASK, false); myJFrame.getRootPane().registerKeyBoardAction( myActionListener, myKeyStroke, JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT);

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

56

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

y

podemos

obtener

su

correspondiente

propiedad

de

condición

con

el

método

getConditionForKeyStroke().

2.13.3 Actions
Una instancia de Action es básicamente una implementación conveniente de ActionListener que encapsula una Hashtable de propiedades ligadas semejante a la de las propiedades cliente de JComponent (ver capítulo 12 para más detalles sobre el trabajo con implementaciones de Action y sus propiedades). A menudo usamos instancias de Action cuando registramos acciones de teclado.
Nota: Los componentes de texto son especiales porque usan una resolución jerárquica mediante KeyMaps. Un KeyMap es una lista de asociaciones Action/KeyStroke y JTextComponent soporta múltiples niveles de este tipo de mapeo. Ver capítulos 11 y 19.

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

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

57

código fuente de SwingUtilities.java).

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

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

2.14.3 Métodos de accesibilidad
Accessible getAccessibleAt(Component c, Point p): devuelve el componente

58

Accessible en el determinado Point del sistema de coordenadas del Component dado (se devolverá null si no se encuentra ninguno). Observe que un componente Accessible es aquel que implementa el interface javax.accessibility.Accessible. Accessible getAccessibleChild(Component c, int i): devuelve el i-ésimo hijo Accessible del Component dado. int getAccessibleChildrenCount(Component Accessible que contiene el Component dado. c): devuelve el número de hijos

int getAccessibleIndexInParent(Component c): devuelve el índice en su padre del Component dado descartando todos los componentes contenidos que no implementen el interface Accessible. Se devolverá -1 si el padre es null o no implementa Accessible, o si el Component dado no implementa Accessible. AccessibleStateSet getAccessibleStateSet(Component c): devuelve el conjunto de AccessibleStates que no están activos para el Component dado.

2.14.4 Métodos de recuperación
Component findFocusOwner(Component c): devuelve el componente contenido dentro del Component dado (o el Component dado) que tiene el foco. Si no hay tal componente se devuelve null. Container getAncestorNamed(String name, Component comp): devuelve el ancestro más cercano del Component dado con el nombre que le pasamos. En otro caso se devuelve null. (Observe que cada Component tiene una propiedad name que se puede asignar y recuperar usando los métodos setName() y getName() respectivamente.) Container getAncestorOfClass(Class c, Component comp): devuelve el ancestro más cercano del Component dado que es una instancia de c. En otro caso se devuelve null. Component getDeepestComponentAt(Component parent, int x, int y): devuelve el hijo más profundo del Component dado que contiene el punto (x,y) en términos del sistema de coordenadas del Component dado. Si el Component no es un Container este método termina

inmediatamente.
Rectangle getLocalBounds(Component c): devuelve un Rectagle que representa los límites de un Component determinado en su propio sistema de coordenadas (de este modo siempre

empieza en 0,0).
Component getRoot(Component c): devuelve el primer ancestro de c que es una Window. En otro caso este método devuelve el último ancestro que es un Applet. JRootPane getRootPane(Component c): devuelve el primer JRootPane que es padre de c, o c si es un JRootPane. Window windowForComponent(Component c): devuelve el primer ancestro de c que es una Window. En otro caso devuelve null. boolean isDescendingFrom(Component allegedDescendent, Component allegedAncestor): devulve true si allegedAncestor contiene a allegedDescendent.

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

59

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

eventos.

2.14.6 Métodos para los botones del ratón
boolean isLeftMouseButton(MouseEvent): devuelve true si el MouseEvent corresponde

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

si

el

MouseEvent

2.14.7 Métodos de disposición/dibujo/UI
String layoutCompoundLabel(FontMetrics fm, String text, icon icon, int verticalAlignment, int horizontalAlignment, int verticalTextPosition, int horizontalTextPosition, Rectangle viewR, Rectangle iconR, Rectangle textR, int textIconGap): Este método se usa normalmente por el delegado UI de JLabel para posicionar texto y/o un icono usando el FontMetrics, las condiciones de alineamiento y las posiciones del texto dentro del Rectangle viewR . Si se determina que el texto de la etiqueta no cabrá dentro de este Rectangle, se usan puntos suspensivos (“...”) en lugar del texto que no cabría. Los Rectangles textR e iconR se modifican para reflejar la nueva disposición, y se devuelve el String resultante de esta disposición. String layoutCompoundLabel(JComponent c, FontMetrics fm, String text, icon icon, int verticalAlignment, int horizontalAlignment, int verticalTextPosition, int horizontalTextPosition, Rectangle viewR, Rectangle iconR, Rectangle textR, int textIconGap): este método es idéntico al

anterior, pero recibe el componente destino para comprobar si el la orientación del texto se debe tener en cuenta (ver el artículo “Component Orientation in Swing: How JFC Components support BIDI text” en the Swing Connection para más información sobre orientación: http://java.sun.com/products/jfc/tsc/tech_topics/bidi/bidi.html).
void paintComponent(Graphics g, Component c, Container p, int x, int y, int w, int h): pinta el Component dado en el contexto gráfico dado, usando el rectángulo definido por los cuatro parámetros de tipo int como área de recorte. El Container se usa para que actúe como el padre del Component de forma que cualquier petición de validación o repintado

que sucedan en ese componente no se propaguen por el árbol de ancestros del componente al que pertenece el contexto gráfico dado. Esta es la misma metodología que usan los pintores de componentes de JList, JTree, y JTable para mostrar correctamente el comportamiento de "sello de goma" (rubber stamp). Este comportamiento se logra mediante el uso de un CellRendererPane (ver capítulo 17 para más información sobre esta clase y por qué se usa para envolver los pintores).
void paintComponent(Graphics g, Component c, Container p, Rectangle r): funciona de forma idéntica al método anterior, pero recibe un Rectangle como parámetro en lugar de cuatro ints. void updateComponentTreeUI(Component c): notifica a todos los componentes que contiene c, y a c, que actualicen su delegado UI para que correspondan a los actuales UIManager y UIDefaults (ver capítulo 21).

60

Sign up to vote on this title
UsefulNot useful