Tracez votre chemin vers les composants graphiques personnalisés

Nos composants graphiques personnalisés nécessitent un dessin manuel, nous devrons donc créer une sous Canvas- classe , qui est le composant standard fourni pour la manipulation graphique directe. La technique que nous allons utiliser sera de remplacer la paintméthode de Canvasavec le dessin personnalisé dont nous avons besoin. Nous utiliserons l' Graphicsobjet, qui est automatiquement passé dans la paintméthode de tous les composants, pour accéder aux couleurs et aux méthodes de dessin.

Nous allons créer deux composants graphiques personnalisés: un graphique à barres et un graphique linéaire. Nous allons commencer par construire une classe de cadre général pour les deux graphes, qui partagent certains éléments de base.

Construire un cadre de graphe générique

Le graphique linéaire et le graphique à barres que nous allons créer sont suffisamment similaires pour que nous puissions créer un

Graph

classe pour effectuer certains des travaux de mise en page fastidieux. Une fois cela fait, nous pouvons étendre la classe pour le type particulier de graphique dont nous avons besoin.

La première chose à faire lorsque vous concevez des composants graphiques personnalisés est de mettre le crayon sur papier et de dessiner une image de ce dont vous avez besoin. Parce que nous comptons les pixels, il est facile de se tromper sur le placement des éléments. Penser à la dénomination et au positionnement des éléments vous aidera à garder le code plus propre et plus facile à lire plus tard.

Le graphique linéaire et le graphique à barres utilisent la même disposition pour le titre et les lignes, nous allons donc commencer par créer un graphique générique contenant ces deux caractéristiques. La mise en page que nous allons créer est illustrée dans la figure ci-dessous.

Pour créer la Graphclasse générique , nous allons sous-classe Canvas. La région centrale est l'endroit où les données graphiques réelles seront affichées; nous laisserons cela à une extension de Graphà implémenter. Nous allons implémenter les autres éléments - une barre de titre, une ligne verticale à gauche, une ligne horizontale en bas et des valeurs pour la plage - dans la classe de base. Nous pourrions spécifier une police et coder en dur les mesures de pixels, mais l'utilisateur ne pourrait pas redimensionner le graphique. Une meilleure approche consiste à mesurer les éléments par rapport à la taille actuelle du composant, de sorte que le redimensionnement de l'application se traduira par un redimensionnement correct du graphique.

Voici notre plan: nous prendrons un Stringtitre, une intvaleur minimale et une intvaleur maximale dans le constructeur. Ceux-ci nous donnent toutes les informations dont nous avons besoin pour définir le cadre. Nous allons continuer à quatre variables pour une utilisation dans les sous - classes - les top, bottom, leftet les rightvaleurs des frontières de la région de dessin graphique. Nous utiliserons ces variables pour calculer le positionnement des éléments du graphique plus tard. Commençons par un rapide coup d'œil à la Graphdéclaration de classe.

import java.awt. *; import java.util. *; public class Graph étend Canvas {// variables nécessaires public int top; public int bas; public int left; public int right; int titleHeight; int labelWidth; FontMetrics fm; remplissage int = 4; Titre de la chaîne; int min; int max; Éléments vectoriels;

Pour calculer le placement correct des éléments du graphique, nous devons d'abord calculer les régions de notre présentation graphique générique qui composent le cadre. Pour améliorer l'apparence du composant, nous ajoutons un remplissage de 4 pixels sur les bords extérieurs. Nous ajouterons le titre centré en haut, en tenant compte de la zone de rembourrage. Pour nous assurer que le graphique n'est pas dessiné au-dessus du texte, nous devons soustraire la hauteur du texte de la zone de titre. Nous devons faire la même chose pour les étiquettes de plage de valeurs minet max. La largeur de ce texte est stockée dans la variable labelWidth. Nous devons garder une référence aux métriques de police afin de faire les mesures. leitemsvector est utilisé pour garder une trace de tous les éléments individuels qui ont été ajoutés au composant Graph. Une classe utilisée pour contenir des variables liées aux éléments de graphique est incluse (et expliquée) après la Graphclasse, qui est illustrée ci-après.

Public Graph (String title, int min, int max) {this.title = title; this.min = min; this.max = max; items = nouveau vecteur (); } // constructeur de fin

Le constructeur prend le titre du graphique et la plage de valeurs, et nous créons un vecteur vide pour les éléments de graphique individuels.

public void reshape (int x, int y, int width, int height) {super.reshape (x, y, width, height); fm = getFontMetrics (getFont ()); titleHeight = fm.getHeight (); labelWidth = Math.max (fm.stringWidth (nouvel Integer (min) .toString ()), fm.stringWidth (nouvel Integer (max) .toString ())) + 2; top = padding + titleHeight; bas = taille (). hauteur - remplissage; gauche = padding + labelWidth; droite = taille (). largeur - remplissage; } // fin du remodelage

Remarque: dans JDK 1.1, la reshapeméthode est remplacée par public void setBounds(Rectangle r). Consultez la documentation de l'API pour plus de détails.

Nous remplaçons la reshapeméthode, qui est héritée dans la chaîne de la Componentclasse. La reshapeméthode est appelée lorsque le composant est redimensionné et lorsqu'il est présenté pour la première fois. Nous utilisons cette méthode pour collecter les mesures, afin qu'elles soient toujours mises à jour si le composant est redimensionné. Nous obtenons les métriques de police pour la police actuelle et attribuons à la titleHeightvariable la hauteur maximale de cette police. Nous obtenons la largeur maximale des étiquettes, en testant pour voir laquelle est la plus grande, puis en utilisant celle-ci. Le top, bottom, left, et les rightvariables sont calculées à partir des autres variables et représentent les frontières de la région de dessin de graphique central. Nous utiliserons ces variables dans les sous-classes de Graph. Notez que toutes les mesures prennent en compte un couranttaille du composant afin que le redessiner soit correct à n'importe quelle taille ou aspect. Si nous utilisions des valeurs codées en dur, le composant ne pouvait pas être redimensionné.

Ensuite, nous allons dessiner le cadre du graphique.

public void paint (Graphics g) {// dessine le titre fm = getFontMetrics (getFont ()); g.drawString (titre, (taille (). largeur - fm.stringWidth (titre)) / 2, haut); // dessine les valeurs max et min g.drawString (new Integer (min) .toString (), padding, bottom); g.drawString (new Integer (max) .toString (), padding, top + titleHeight); // dessine les lignes verticales et horizontales g.drawLine (gauche, haut, gauche, bas); g.drawLine (gauche, bas, droite, bas); } // fin de la peinture

Le cadre est dessiné dans la paintméthode. Nous dessinons le titre et les étiquettes aux endroits appropriés. Nous dessinons une ligne verticale à la bordure gauche de la zone de dessin du graphe et une ligne horizontale à la bordure inférieure.

Dans cet extrait de code suivant, nous définissons la taille préférée du composant en remplaçant la preferredSizeméthode. La preferredSizeméthode est également héritée de la Componentclasse. Les composants peuvent spécifier une taille préférée et une taille minimale. J'ai choisi une largeur préférée de 300 et une hauteur préférée de 200. Le gestionnaire de mise en page appellera cette méthode lorsqu'il disposera le composant.

public Dimension favoriteSize () {return (new Dimension (300, 200)); }} // fin du graphe

Remarque: dans JDK 1.1, la preferredSizeméthode est remplacée par public Dimension getPreferredSize().

Ensuite, nous avons besoin d'une fonction pour ajouter et supprimer les éléments à représenter graphiquement.

 public void addItem(String name, int value, Color col) { items.addElement(new GraphItem(name, value, col)); } // end addItem public void addItem(String name, int value) { items.addElement(new GraphItem(name, value, Color.black)); } // end addItem public void removeItem(String name) { for (int i = 0; i < items.size(); i++) { if (((GraphItem)items.elementAt(i)).title.equals(name)) items.removeElementAt(i); } } // end removeItem } // end Graph 

I've modeled the addItem and removeItem methods after similar methods in the Choice class, so the code will have a familiar feel. Notice that we use two addItem methods here; we need a way to add items with or without a color. When an item is added, a new GraphItem object is created and added to the items vector. When an item is removed, the first one in the vector with that name will be removed. The GraphItem class is very simple; here is the code:

import java.awt.*; class GraphItem { String title; int value; Color color; public GraphItem(String title, int value, Color color) { this.title = title; this.value = value; this.color = color; } // end constructor } // end GraphItem 

The GraphItem class acts as a holder for the variables relating to graph items. I've included Color here in case it will be used in a subclass of Graph.

With this framework in place, we can create extensions to handle each type of graph. This strategy is quite convenient; we don't have to go to the trouble of measuring the pixels for the framework again, and we can create subclasses to focus on filling in the graph drawing region.

Building the bar chart

Now that we have a graphing framework, we can customize it by extending

Graph

and implementing custom drawing. We'll begin with a simple bar chart, which we can use just like any other component. A typical bar chart is illustrated below. We'll fill in the graph drawing region by overriding the

paint

method to call the superclass

paint

method (to draw the framework), then we'll perform the custom drawing needed for this type of graph.

import java.awt.*; public class BarChart extends Graph { int position; int increment; public BarChart(String title, int min, int max) { super(title, min, max); } // end constructor 

To space the items evenly, we keep an increment variable to indicate the amount we will shift to the right for each item. The position variable is the current position, and the increment value is added to it each time. The constructor simply takes in values for the super constructor (Graph), which we call explicitly.

Now we can get down to some actual drawing.

 public void paint(Graphics g) { super.paint(g); increment = (right - left)/(items.size()); position = left; Color temp = g.getColor(); for (int i = 0; i < items.size(); i++) { GraphItem item = (GraphItem)items.elementAt(i); int adjustedValue = bottom - (((item.value - min)*(bottom - top)) /(max - min)); g.drawString(item.title, position + (increment - fm.stringWidth(item.title))/2, adjustedValue - 2); g.setColor(item.color); g.fillRect(position, adjustedValue, increment, bottom - adjustedValue); position+=increment; g.setColor(temp); } } // end paint } // end BarChart 

Let's take a close look at what's happening here. In the paint method, we call the superclass paint method to draw the graph framework. We then find the increment by subtracting the right edge from the left edge, and then dividing the result by the number of items. This value is the distance between the left edges of the graph items. Because we want the graph to be resizable, we base these values on the current value of the left and right variables inherited from Graph. Recall that the left, right, top, and bottom values are the current actual pixel measurements of the graph drawing region taken in the reshape method of Graph, and therefore available for our use. If we did not base our measurements on these values, the graph would not be resizable.

We'll draw the rectangles in the color specified by the GraphItem. To allow us to go back to the original color, we set a temporary color variable to hold the current value before we change it. We cycle through the vector of graph items, calculating an adjusted vertical value for each one, drawing the title of the item and a filled rectangle representing its value. The increment is added to the x position variable each time through the loop.

The adjusted vertical value ensures that if the component is stretched vertically, the graph will still remain true to its plotted values. To do this properly, we need to take the percentage of the range the item represents and multiply that value by the actual pixel range of the graph drawing region. We then subtract the result from the bottom value to correctly plot the point.

As you can see from the following diagram, the total horizontal pixel size is represented by right - left and the total vertical size is represented by bottom - top.

We take care of the horizontal stretching by initializing the position variable to the left edge and increasing it by the increment variable for each item. Because the position and increment variables are dependent on the actual current pixel values, the component is always resized correctly in the horizontal direction.

Pour garantir que le tracé vertical est toujours correct, nous devons mapper les valeurs des éléments du graphique avec les emplacements réels des pixels. Il y a une complication: les valeurs maxet mindoivent être significatives pour la position de la valeur de l'élément graphique. En d'autres termes, si le graphique commence à 150 et va à 200, un élément avec une valeur de 175 doit apparaître à mi-hauteur de l'axe vertical. Pour ce faire, nous trouvons le pourcentage de la plage graphique que l'élément représente et le multiplions par la plage de pixels réelle. Comme notre graphique est à l'envers du système de coordonnées du contexte graphique, nous soustrayons ce nombre bottompour trouver le point de tracé correct. N'oubliez pas que l'origine (0,0) se trouve dans le coin supérieur gauche du code, mais dans le coin inférieur gauche du style de graphique que nous créons.