Boutons OpenLook

Nous allons analyser un exemple fourni avec le JDK qui montre comme créer des boutons au look particulier (OpenLook) en partant de la classe Component. Vous pouvez vous faire une idée de l'apparence de ces boutons en ouvrant la page OpenlookTest.html.

Vue d'ensemble

Lors de la création d'un composant à partir de la classe Component nous avons à gérer plusieurs problèmes :

Pas à pas

Déclaration et constructeurs

La classe OpenLookButon que nous créons sera dérivée de la classe Component. Elle utilisera :
 - un champ label qui contiendra le texte affiché sur le bouton,
 - un champ privé pressed qui indiquera si le bouton est pressé ou non,
 - un champ actionListener qui contiendra l'écouteur associé au bouton.
D'autre part une constante capWidth sera utilisée comme rayon des arcs utilisés pour le dessin du bouton.

import java.util.*;
import java.awt.*;
import java.awt.event.*;

public class OpenlookButton extends Component {

  static int capWidth = 20;          // The width of the Button's endcap
  String label;                      // The Button's text
  protected boolean pressed = false; // true if the button is detented.
  ActionListener actionListener;     // Post action events to listeners
  
  
  /**
   * Constructs an OpenlookButton with no label.
   */
  public OpenlookButton() {
      this("");
  }

  /**
   * Constructs an OpenlookButton with the specified label.
   * @param label the label of the button
   */
  public OpenlookButton(String label) {
      this.label = label;
      enableEvents(AWTEvent.MOUSE_EVENT_MASK);
  }
  
} 

Deux constructeurs sont définis. Il faudra utiliser le second qui permet d'initialiser le champ label. Ce constructeur définit le type d'évènements à gérer en utilisant la méthode enableEvents.

Accès au champ label

L'accès au champ label se fera par l'intermédiaire des méthodes getLabel et setLabel.

  /**
   * gets the label
   * @see setLabel
   */
  public String getLabel() {
      return label;
  }
  
  /**
   * sets the label
   * @see getLabel
   */
  public void setLabel(String label) {
      this.label = label;
      invalidate();
      repaint();
  }

Notons que la méthode setLabel fait appel aux méthodes invalidate et repaint. En effet un changement du libellé du bouton doit provoquer un redimensionnement (d'où invalidate) et un réaffichage du bouton (d'où repaint).

Méthode de dessin

La méthode paint va utiliser 3 couleurs nommées interior, highlight1 et highlight2 qui serviront à dessiner le fond et la bordure. La couleur interior est la couleur de fond du composant qui pourra être définie par setBackground. Les deux autres couleurs sont dérivées de interior en utilisant les méthodes darker et brighter de la classe Color. Le choix de ces couleurs va dépendre de l'état du bouton (pressé ou non) pour donner l'impression d'ombrage correspondante.

  /**
   * paints the button
   */
  public void paint(Graphics g) {
      int width = getSize().width - 1;
      int height = getSize().height - 1;

      Color interior;
      Color highlight1;
      Color highlight2;
      
      interior = getBackground();

      // ***** determine what colors to use
      if(pressed) {
	  highlight1 = interior.darker();
	  highlight2 = interior.brighter();
      } else {
	  highlight1 = interior.brighter();
	  highlight2 = interior.darker();
      }

      // ***** paint the interior of the button
      g.setColor(interior);
      // left cap
      g.fillArc(0, 0, 			    // start
		capWidth, height,    	    // size
		90, 180);		    // angle

      // right cap
      g.fillArc(width - capWidth, 0,        // start
		capWidth, height,           // size
		270, 180);		    // angle

      // inner rectangle
      g.fillRect(capWidth/2, 0, width - capWidth, height);
      

      // ***** highlight the perimeter of the button
      // draw upper and lower highlight lines
      g.setColor(highlight1);
      g.drawLine(capWidth/2, 0, width - capWidth/2, 0);
      g.setColor(highlight2);
      g.drawLine(capWidth/2, height, width - capWidth/2, height);

      // upper arc left cap
      g.setColor(highlight1);
      g.drawArc(0, 0,                       // start
                capWidth, height,           // size
                90, 180-40                  // angle
      );
 
      // lower arc left cap
      g.setColor(highlight2);
      g.drawArc(0, 0,                       // start
                capWidth, height,           // size
                270-40, 40                  // angle
      );

      // upper arc right cap
      g.setColor(highlight1);
      g.drawArc(width - capWidth, 0,        // start
                capWidth, height,           // size
                90-40, 40                   // angle
      );
 
      // lower arc right cap
      g.setColor(highlight2);
      g.drawArc(width - capWidth, 0,        // start
                capWidth, height,           // size
                270, 180-40                 // angle
      );

      // ***** draw the label centered in the button
      Font f = getFont();
      if(f != null) {
	  FontMetrics fm = getFontMetrics(getFont());
	  g.setColor(getForeground());
	  g.drawString(label,
		       width/2 - fm.stringWidth(label)/2,
		       height/2 + fm.getHeight()/2 - fm.getMaxDescent()
	  );
      }
  }

Nous pouvons remarquer l'utilisation de la classe FontMetrics pour calculer les dimensions du texte et obtenir un affichage centré.

Les dimensions

Deux méthodes donnent les dimensions minimales et optimales.

  /**
   * The preferred size of the button. 
   */
  public Dimension getPreferredSize() {
      Font f = getFont();
      if(f != null) {
	  FontMetrics fm = getFontMetrics(getFont());
	  return new Dimension(fm.stringWidth(label) + capWidth*2,
			       fm.getHeight() + 10);
      } else {
	  return new Dimension(100, 50);
      }
  }
  
  /**
   * The minimum size of the button. 
   */
  public Dimension getMinimumSize() {
      return new Dimension(100, 50);
  }

Les dimensions minimales sont fixées à 100 sur 50. Les dimensions optimales sont calculées en fonction de la dimension du texte en tenant compte des demi-cercles et d'une marge.

Gestion d'un écouteur

Nous devons écrire deux méthodes qui permettent de déclarer ou de supprimer l'écouteur vers lequel les évènements ActionEvent générés par le bouton devront être envoyés. Cela se fait en surchargeant les méthodes addActionListener et removeActionListener.

  /**
   * Adds the specified action listener to receive action events
   * from this button.
   * @param listener the action listener
   */
   public void addActionListener(ActionListener listener) {
       actionListener = AWTEventMulticaster.add(actionListener, listener);
       enableEvents(AWTEvent.MOUSE_EVENT_MASK);
   }
 
   /**
    * Removes the specified action listener so it no longer receives
    * action events from this button.
    * @param listener the action listener
    */
   public void removeActionListener(ActionListener listener) {
       actionListener = AWTEventMulticaster.remove(actionListener, listener);
   }

Ces méthodes mettent à jour le champ actionListener de notre bouton et font état des choix au système de transmission des évènements en utilisant les méthodes AWTEventMulticaster.add et AWTEventMulticaster.remove.

Gestion des évènements

La dernière méthode à écrire concerne la gestion des évènements souris en surchargeant la méthode processMouseEvent. Il faudra effectuer les opérations suivantes :
 - au moment où on appuie sur le bouton (évènement MOUSE_PRESSED) on met le champ pressed à true et on redessine le bouton
 - au moment où on relache le bouton (évènement MOUSE_RELEASED) on appelle la méthode actionPerformed de l'écouteur s'il est défini, puis on met le champ pressed à false et on redessine.

   /**
    * Paints the button and sends an action event to all listeners.
    */
   public void processMouseEvent(MouseEvent e) {
       Graphics g;
       switch(e.getID()) {
          case MouseEvent.MOUSE_PRESSED:
            // render myself inverted....
            pressed = true;
	
	    // Repaint might flicker a bit. To avoid this, you can use
	    // double buffering (see the Gauge example).
	    repaint();
            break;
          case MouseEvent.MOUSE_RELEASED:
            if(actionListener != null) {
               actionListener.actionPerformed(new ActionEvent(
                   this, ActionEvent.ACTION_PERFORMED, label));
            }
            // render myself normal again
            if(pressed == true) {
                pressed = false;

	        // Repaint might flicker a bit. To avoid this, you can use
	        // double buffering (see the Gauge example).
		repaint();
            }
            break;
          case MouseEvent.MOUSE_ENTERED:
 
            break;
          case MouseEvent.MOUSE_EXITED:
            if(pressed == true) {
                // Cancel! Don't send action event.
                pressed = false;

	        // Repaint might flicker a bit. To avoid this, you can use
	        // double buffering (see the Gauge example).
		repaint();

                // Note: for a more complete button implementation,
                // you wouldn't want to cancel at this point, but
                // rather detect when the mouse re-entered, and
                // re-highlight the button. There are a few state
                // issues that that you need to handle, which we leave
                // this an an excercise for the reader (I always
                // wanted to say that!)
            }
            break;
       }
       super.processMouseEvent(e);
   }

On termine cette méthode par un appel à la méthode définie dans la classe parente Component en utilisant le mot-clé super.

Exemple d'utilisation

Ecrivons une applet simple qui utilise des boutons OpenLook.

//test des boutons OpenLook

import java.awt.*;
import java.awt.event.*;

public class OpenlookTest extends java.applet.Applet
 implements ActionListener {
 
 TextArea zoneTexte;
 OpenlookButton bouton;
 
 public void init() {
  setLayout(new BorderLayout());
  zoneTexte=new TextArea();
  add(zoneTexte,"Center");
  Panel p=new Panel();
  p.setBackground(Color.lightGray);
  add(p,"South");
  bouton=new OpenlookButton("VERT");
  bouton.setBackground(Color.green);
  bouton.setForeground(Color.black);
  bouton.addActionListener(this);
  p.add(bouton);
  bouton=new OpenlookButton("BLEU");
  bouton.setBackground(Color.cyan);
  bouton.setForeground(Color.black);
  bouton.addActionListener(this);
  p.add(bouton);
 }
 
 public void actionPerformed(ActionEvent e) {
  zoneTexte.append("Bouton pressé: " + e.getActionCommand()+'\n');
 }
}

La couleur des boutons est définie avec la méthode héritée setBackground. On pourrait de même utiliser la méthode setFont pour choisir la fonte dans laquelle le libellé du bouton doit s'afficher.

Observons une nouvelle fois le résultat obtenu en ouvrant la page OpenlookTest.html.



Retour au menu