Tutorial de Java

Look And Feel Flecha

Anterior | Siguiente

Y, ahora llega el momento de crear un Look&Feel propio. Para los lectores más inquietos y que dispongan de una gran dosis de paciencia y, realmente, necesiten hacerlo, a continuación se indica cómo se puede crear un nuevo Look&Feel para incorporarlo a Java. La mayoría de los usuarios y programadores, seguro que estarán más que satisfechos con los que proporciona Java por defecto, pero como Swing proporciona la posibilidad de controlar todo el interfaz, a los diseñadores les resultará de utilidad toda esta información.

Se va a crear un nuevo Look&Feel llamado flecha, del cual solamente se va a diseñar uno de los Componentes del interfaz, concretamente el Botón; los demás componentes se implementarían de forma similar, siempre teniendo en cuenta las características particulares que correspondan a cada uno de esos Componentes. El JButton que se va a crear, permitirá que se indique un texto y dispondrá de dos triángulos a cada lado del texto, en forma de flecha, que cambiarán de color en función del estado en que se encuentre el botón.

La imagen siguiente reproduce la ejecución del ejemplo java1426.java, que utiliza este nuevo Look&Feel, en el que se puede observar el nuevo JButton que se ha creado, y los demás Componentes del interfaz, que corresponden al Look&Feel Metal, que es el que se ha utilizado como partida para la implementación del Look&Feel Flecha.

También se puede utilizar como partida cualquier otra de las apariencias que proporciona Java, o crearse el nuevo Look&Feel desde cero. La opción más interesante es extender uno de los ya hechos, de forma que solamente haya que crear aquellos Componentes que de verdad vayan a tener una apariencia diferente o un comportamiento distinto al del Componente del Look de Java. La imagen siguiente reproduce la ejecución del mismo ejemplo pero utilizando el Look&Feel Motif como partida para la creación del nuevo Look&Feel y la implementación del JButton.

Para la implementación de este Look&Feel se utiliza el paquete flecha, que está generado a partir de los ficheros fuente FlechaLookAndFeel.java, FlechaButtonUI.java, FlechaButtonListener.java y FlechaButtonBorder.java. El comportamiento del botón no se altera, solamente se cambia la apariencia y la respuesta del botón a las acciones del ratón se adecúan a la nueva imagen del JButton.

El código del ejemplo java1426.java, como se puede observar, es sumamente sencillo, y la forma de cambiar al Look&Feel creado es igualmente simple.

import java.awt.*;
import java.awt.event.*;
import com.sun.java.swing.*;

public class java1426 extends JPanel {

  public java1426() {
    // Se crea el botón que vamos a cambiar de apariencia
    JButton boton = new JButton( "Boton Ej." );

    ActionListener miListener = new ActionListener() {
      public void actionPerformed( ActionEvent evt ) {
        String look = null;

        // Aquí se controla el Look&Feel presente
        if( evt.getActionCommand().equals( "Flecha" ) ) {
          look = "flecha.FlechaLookAndFeel";
        } 
        else {
          look = "com.sun.java.swing.plaf.metal.MetalLookAndFeel";
        }

        try {
          UIManager.setLookAndFeel( look );
          SwingUtilities.updateComponentTreeUI( java1426.this );
          java1426.this.validate();
        } catch( Exception e ) {
          System.err.println( "No se puede cambiar al LookAndFeel: "+look );
        }
      }
    };

    // Grupo de botones que va a permitir alternar entre un Look y
    // otro
    ButtonGroup grupo = new ButtonGroup();
    JRadioButton botonMetal = new JRadioButton( "Metal" );
    botonMetal.setSelected( true );
    botonMetal.addActionListener( miListener );
    grupo.add( botonMetal );

    JRadioButton botonFlecha = new JRadioButton( "Flecha" );
    botonFlecha.addActionListener( miListener );
    grupo.add( botonFlecha );

    add( boton );
    add( botonMetal );
    add( botonFlecha );
  }

  public static void main( String args[] ) {
    JFrame ventana = new JFrame( "Tutorial de Java, Swing" );

    ventana.addWindowListener( new WindowAdapter() {
      public void windowClosing( WindowEvent evt ) {
        System.exit( 0 );
      }
    });
    ventana.getContentPane().add( new java1426(),BorderLayout.CENTER );
    ventana.setSize( 300,100 );
    ventana.show();
  }
}

Si se repasan los ficheros que componen el paquete flecha, el lector podrá observar que las clases que se implementan no revisten excesiva complicación, ya que al extender clases ya implementadas en los paquetes de Java, solamente hay que modificar aquellos comportamientos que deban adecuarse a la nueva situación que se cree con el Componente nuevo. Por ejemplo, en la clase FlechaLookAndFeel, lo único necesario es mapear las cosas que se necesitan hacer con la clase nueva, para que los métodos correspondan con los nombres correctos siguiendo la nomenclatura de las clases UI. Estos nombres se pueden ver en el paquete com.sun.java.swing.basic, o preguntando sobre un componente determinado invocando al método getUIClassID().

package flecha;
import java.awt.Color;
import com.sun.java.swing.*;
import com.sun.java.swing.plaf.*;
import com.sun.java.swing.plaf.metal.*;

public class FlechaLookAndFeel extends MetalLookAndFeel {

  public String getID() {
    return( "flecha" );
    }

  public String getName() {
    return( "Flecha Look and Feel" );
    }

  public String getDescription() {
    return( "Look and Feel Flecha, Tutorial de Java" );
    }

  public boolean isNativeLookAndFeel() {
    return( false );
    }

  public boolean isSupportedLookAndFeel() {
    return( true );
    }

  protected void initClassDefaults( UIDefaults tabla ) {
    super.initClassDefaults( tabla );
    table.put( "ButtonUI","flecha.FlechaButtonUI" );
    }
}

Ahora llega el turno de crear las clases UI que se han definido. En general, las clases UI son idénticas, con excepción del método paint(); en el resto de los métodos comunes los cambios son mínimos. En el caso concreto del botón que se está creando, son necesarias dos clases de soporte: la clase FlechaButtonBorder que va a pintar los bordes del botón y la clase FlechaButtonListener, que va a deteminar la forma de respuesta que tendrá el botón ante las acciones del ratón y del teclado.

package flecha;
import java.awt.*;
import java.awt.event.*;
import com.sun.java.swing.*;
import com.sun.java.swing.border.*;
import com.sun.java.swing.plaf.*;
import com.sun.java.swing.plaf.basic.*;
import java.io.Serializable;
import com.sun.java.swing.plaf.ButtonUI;

public class FlechaButtonUI extends ButtonUI implements Serializable { protected final static Insets margenDefecto = new Insets( 2,5,2,5 ); protected final static Font fuenteDefecto = new Font( "Serif",Font.BOLD,10 ); private final static Border bordeDefecto = new CompoundBorder( FlechaButtonBorder.getButtonBorder(), FlechaButtonBorder.getButtonBorder() ); protected static final int sepTxtIcono = 3; protected FlechaButtonListener receptor; protected static ButtonUI botonUI; public static ComponentUI createUI( JComponent c ) { if( botonUI == null ) { botonUI = new FlechaButtonUI(); } return( botonUI ); } public void installUI( JComponent c ) { receptor = new FlechaButtonListener( c ); c.addMouseListener( receptor ); c.addMouseMotionListener( receptor ); c.setFont( fuenteDefecto ); if( c.getBorder() == null ) c.setBorder( bordeDefecto ); } public void uninstallUI( JComponent c ) { c.removeMouseListener( receptor ); c.removeMouseMotionListener( receptor ); if( c.getBorder() == bordeDefecto ) c.setBorder( null ); } public void paint( Graphics g,JComponent c ) { AbstractButton ab = (AbstractButton)c; ButtonModel bm = ab.getModel(); Dimension tamano = ab.getSize(); g.setFont( c.getFont() ); FontMetrics fm = g.getFontMetrics(); // Desplazamiento que se aplica al texto e iconos cuando se // pulsa el botón, para que simule el efecto de pulsado int offsetDesplaz = 0; Rectangle rectBoton = new Rectangle( tamano ); Rectangle rectIcono = new Rectangle(); Rectangle rectTexto = new Rectangle(); String texto = SwingUtilities.layoutCompoundLabel( fm, ab.getText(),ab.getIcon(), ab.getVerticalAlignment(), ab.getHorizontalAlignment(), ab.getVerticalTextPosition(), ab.getHorizontalTextPosition(), rectBoton,rectIcono,rectTexto,sepTxtIcono ); if( bm.isArmed() && bm.isPressed() ) { offsetDesplaz = 1; } // Se pinta el fondo del botón, que es en color plano, pero se // le podría poner cualquier imagen if( c.isOpaque() ) { g.setColor( ab.getBackground() ); g.fillRect( 0,0,tamano.width,tamano.height ); } // Pinta las flechas if( ab.getIcon() != null ) { Icon icono = null; if ( !bm.isEnabled() ) { icono = ab.getDisabledIcon(); } else if( bm.isPressed() && bm.isArmed() ) { icono = ab.getPressedIcon(); } else if( bm.isRollover() ) { icono = ab.getRolloverIcon(); } if( icono == null ) { icono = ab.getIcon(); } if( bm.isPressed() && bm.isArmed() ) { icono.paintIcon( c,g,rectIcono.x+offsetDesplaz, rectIcono.y+offsetDesplaz ); } else { icono.paintIcon( c,g,rectIcono.x,rectIcono.y ); } } // Pinta la cadena de texto del título del botón if( (texto != null) && (texto.length() != 0) ) { if( bm.isEnabled() ) { g.setColor (ab.getForeground()); BasicGraphicsUtils.drawString( g,texto,bm.getMnemonic(), rectTexto.x+offsetDesplaz, rectTexto.y+fm.getAscent()+offsetDesplaz ); } else { g.setColor( ab.getBackground().brighter() ); BasicGraphicsUtils.drawString( g,texto,bm.getMnemonic(), rectTexto.x,rectTexto.y+fm.getAscent() ); g.setColor( ab.getBackground().darker() ); BasicGraphicsUtils.drawString( g,texto,bm.getMnemonic(), rectTexto.x-1,rectTexto.y+fm.getAscent()-1 ); } } } public Dimension getMinimumSize( JComponent c ) { return( getPreferredSize( c ) ); } public Dimension getMaximumSize( JComponent c ) { return( getPreferredSize( c ) ); } public Dimension getPreferredSize( JComponent c ) { if( ( c.getComponentCount() > 0 ) || !( c instanceof AbstractButton ) ) { return( null ); } AbstractButton ab = (AbstractButton) c; Icon icono = ab.getIcon(); String texto = ab.getText(); Font font = ab.getFont(); FontMetrics fm = ab.getFontMetrics( font ); Rectangle rectBoton = new Rectangle( Short.MAX_VALUE,Short.MAX_VALUE ); Rectangle rectIcono = new Rectangle(); Rectangle rectTexto = new Rectangle(); SwingUtilities.layoutCompoundLabel( fm,texto,icono, ab.getVerticalAlignment(), ab.getHorizontalAlignment(), ab.getVerticalTextPosition(), ab.getHorizontalTextPosition(), rectBoton,rectIcono,rectTexto,sepTxtIcono ); // Se calcula la unión del rectángulo formado por el icono y el texto Rectangle rect = rectIcono.union( rectTexto ); Insets insets = getInsets( c ); rect.width += insets.left + insets.right; rect.height += insets.top + insets.bottom; return( rect.getSize() ); } public Insets getDefaultMargin( AbstractButton b ) { return( margenDefecto ); } public Insets getInsets( JComponent c ) { Border borde = c.getBorder(); Insets insets = ((borde != null) ? borde.getBorderInsets ( c ) : new Insets( 0,0,0,0 ) ); return( insets ); } }

En la clase anterior, los métodos installUI(), uninstalUI() y createUI(), deberían ser los únicos que sorprendiesen al lector, porque los demás ya se han visto en ejemplos anteriores hasta la saciedad. Estas rutinas de instalación tienen como cometido básico el instalar el nuevo borde y el nuevo receptor de eventos que se han creado específicamente para el botón que se implementa. Por supuesto, el método paint() pinta el objeto y el resto del trabajo se deja para la clase que controla el borde del botón.

El código del receptor de eventos, si no se están haciendo cosas específicas con el teclado o a la hora de recibir el foco, debería ser muy semejante, e incluso compartido, entre múltiples objetos. Básicamente, lo que se le dice es lo que debe hacer: la pulsación del ratón hará que el botón se arme y se hunda; que es el comportamiento normal de un JButton. Ahora bien, se puede indicar cualquier cosa; por ejemplo, si se quiere que cada vez que se pulse aparezca un muñeco saludando antes de que el botón se active, pues no hay problemas en hacerlo, se puede perfectamente. En el caso del objeto FlechaButtonUI se admiten solamente los eventos de ratón habituales.

package flecha;
import java.awt.*;
import java.awt.event.*;
import com.sun.java.swing.*;
import java.io.Serializable;

public class FlechaButtonListener implements MouseListener,
  MouseMotionListener,Serializable {
    AbstractButton ab;

  public FlechaButtonListener( JComponent c ) {
    ab = (AbstractButton)c;
  }

  public void mouseMoved( MouseEvent evt ) {
  }

  public void mouseClicked( MouseEvent evt ) {
  }

  public void mouseDragged( MouseEvent evt ) {
    ButtonModel bm = ab.getModel();
    if( bm.isPressed() ) {
      Graphics g = ab.getGraphics();
      if( g != null ) {
    Rectangle r = g.getClipBounds();
    if( r.contains( evt.getPoint() ) )
      bm.setArmed( true );
    else
      bm.setArmed( false );
      }
    }
  }

  public void mousePressed( MouseEvent evt ) {
    ButtonModel bm = ab.getModel();
    bm.setArmed( true );
    bm.setPressed( true );
  }

  public void mouseReleased( MouseEvent evt ) {
    ButtonModel bm = ab.getModel();
    bm.setPressed( false );
  }

  public void mouseEntered( MouseEvent evt ) {
    if( ab.getRolloverIcon() != null )
      ab.getModel().setRollover( true );
  }

  public void mouseExited( MouseEvent evt ) {
    if( ab.getRolloverIcon() != null )
      ab.getModel().setRollover( false );
  }
}

Finalmente, la clase FlechaButtonBorder es la clase que le da la apariencia nueva al botón. El borde habitual es sustituido por dos triángulos a los lados del texto, y también, en función del estado del botón, se cambiará el color de estos triángulos.

Navegador

Home | Anterior | Siguiente | Indice | Correo