Trace su camino hacia componentes gráficos personalizados

Nuestros componentes de gráficos personalizados requieren un dibujo manual, por lo que necesitaremos una subclase Canvas, que es el componente estándar proporcionado para la manipulación directa de gráficos. La técnica que usaremos será anular el paintmétodo Canvascon el dibujo personalizado que necesitamos. Usaremos el Graphicsobjeto, que se pasa automáticamente al paintmétodo de todos los componentes, para acceder a los colores y métodos de dibujo.

Crearemos dos componentes gráficos personalizados: un gráfico de barras y un gráfico de líneas. Comenzaremos construyendo una clase de marco general para los dos gráficos, que comparten algunos elementos base.

Construyendo un marco gráfico genérico

El gráfico de líneas y el gráfico de barras que vamos a crear son lo suficientemente similares como para que podamos crear un

Graph

clase para realizar parte del tedioso trabajo de diseño. Una vez hecho esto, podemos extender la clase para el tipo particular de gráfico que necesitamos.

Lo primero que debe hacer cuando diseña componentes gráficos personalizados es poner lápiz sobre papel y hacer un dibujo de lo que necesita. Debido a que contamos píxeles, es fácil confundirse con la ubicación de los elementos. Pensar un poco en el nombre y el posicionamiento de los elementos le ayudará a mantener el código más limpio y más fácil de leer más adelante.

El gráfico de líneas y el gráfico de barras usan el mismo diseño para el título y las líneas, por lo que comenzaremos creando un gráfico genérico que contenga estas dos características. El diseño que vamos a crear se muestra en la siguiente figura.

Para crear la Graphclase genérica , subclase Canvas. La región central es donde se mostrarán los datos reales del gráfico; dejaremos esto a una extensión de Graphpara implementar. Implementaremos los otros elementos (una barra de título, una línea vertical a la izquierda, una línea horizontal en la parte inferior y valores para el rango) en la clase base. Podríamos especificar una fuente y codificar las medidas de píxeles, pero el usuario no podría cambiar el tamaño del gráfico. Un mejor enfoque es medir los elementos con el tamaño actual del componente, de modo que el cambio de tamaño de la aplicación resulte en un cambio de tamaño correcto del gráfico.

Aquí está nuestro plan: tomaremos un Stringtítulo, un intvalor mínimo y un intvalor máximo en el constructor. Estos nos brindan toda la información que necesitamos para diseñar el marco. Vamos a mantener cuatro variables para su uso en las subclases - los top, bottom, left, y rightlos valores de las fronteras de la región de dibujo gráfico. Usaremos estas variables para calcular el posicionamiento de los elementos del gráfico más adelante. Comencemos con un vistazo rápido a la Graphdeclaración de clase.

importar java.awt. *; importar java.util. *; public class Graph extiende Canvas {// variables necesarias public int top; público int bottom; public int left; derecho int public; int titleHeight; int labelWidth; FontMetrics fm; int padding = 4; Título de la cadena; int min; int max; Elementos vectoriales;

Para calcular la ubicación correcta de los elementos del gráfico, primero necesitamos calcular las regiones en nuestro diseño de gráfico genérico que componen el marco. Para mejorar la apariencia del componente, agregamos un relleno de 4 píxeles a los bordes exteriores. Agregaremos el título centrado en la parte superior, teniendo en cuenta el área de relleno. Para asegurarnos de que el gráfico no se dibuje encima del texto, debemos restar la altura del texto de la región del título. Necesitamos hacer lo mismo con las etiquetas de rango de valores miny max. El ancho de este texto se almacena en la variable labelWidth. Necesitamos mantener una referencia a las métricas de fuentes para poder realizar las mediciones. lositemsEl vector se utiliza para realizar un seguimiento de todos los elementos individuales que se han agregado al componente Gráfico. Una clase utilizada para contener variables relacionadas con elementos de gráficos se incluye (y explica) después de la Graphclase, que se muestra a continuación.

Gráfico público (título de la cadena, int min, int max) {this.title = title; this.min = min; this.max = max; items = new Vector (); } // fin del constructor

El constructor toma el título del gráfico y el rango de valores, y creamos un vector vacío para los elementos individuales del gráfico.

remodelación del vacío público (int x, int y, int ancho, int alto) {super.reshape (x, y, ancho, alto); fm = getFontMetrics (getFont ()); titleHeight = fm.getHeight (); labelWidth = Math.max (fm.stringWidth (new Integer (min) .toString ()), fm.stringWidth (new Integer (max) .toString ())) + 2; top = padding + titleHeight; fondo = tamaño (). altura - relleno; izquierda = relleno + labelWidth; derecha = tamaño (). ancho - relleno; } // fin de remodelar

Nota: En JDK 1.1, el reshapemétodo se reemplaza por public void setBounds(Rectangle r). Consulte la documentación de la API para obtener más detalles.

Anulamos el reshapemétodo, que se hereda en la cadena de la Componentclase. El reshapemétodo se llama cuando se cambia el tamaño del componente y cuando se distribuye por primera vez. Utilizamos este método para recopilar medidas, de modo que siempre se actualizarán si se cambia el tamaño del componente. Obtenemos las métricas de fuente para la fuente actual y asignamos a la titleHeightvariable la altura máxima de esa fuente. Obtenemos el ancho máximo de las etiquetas, probamos para ver cuál es más grande y luego usamos esa. El top, bottom, left, y rightlas variables se calculan a partir de las otras variables y representan los límites de la región de dibujo gráfico central. Usaremos estas variables en las subclases de Graph. Tenga en cuenta que todas las mediciones tienen en cuenta una corrientetamaño del componente para que el redibujado sea correcto en cualquier tamaño o aspecto. Si usamos valores codificados de forma rígida, no se podría cambiar el tamaño del componente.

A continuación, dibujaremos el marco para el gráfico.

public void paint (Graphics g) {// dibuja el título fm = getFontMetrics (getFont ()); g.drawString (título, (tamaño (). ancho - fm.stringWidth (título)) / 2, arriba); // dibuja los valores máximo y mínimo g.drawString (new Integer (min) .toString (), padding, bottom); g.drawString (new Integer (max) .toString (), padding, top + titleHeight); // dibuja las líneas verticales y horizontales g.drawLine (izquierda, arriba, izquierda, abajo); g.drawLine (izquierda, abajo, derecha, abajo); } // terminar la pintura

El marco se dibuja en el paintmétodo. Dibujamos el título y las etiquetas en sus lugares apropiados. Dibujamos una línea vertical en el borde izquierdo de la región de dibujo del gráfico y una línea horizontal en el borde inferior.

En este siguiente fragmento, establecemos el tamaño preferido para el componente anulando el preferredSizemétodo. El preferredSizemétodo también se hereda de la Componentclase. Los componentes pueden especificar un tamaño preferido y un tamaño mínimo. He elegido un ancho preferido de 300 y una altura preferida de 200. El administrador de diseño llamará a este método cuando presente el componente.

public Dimension preferenciaSize () {return (nueva Dimensión (300, 200)); }} // fin del gráfico

Nota: En JDK 1.1, el preferredSizemétodo se reemplaza por public Dimension getPreferredSize().

A continuación, necesitamos una función para agregar y eliminar los elementos que se van a graficar.

public void addItem (String name, int value, Colour col) {items.addElement (nuevo GraphItem (nombre, valor, col)); } // finaliza addItem public void addItem (String name, int value) {items.addElement (new GraphItem (nombre, valor, Color.black)); } // finalizar 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); }} // fin removeItem} // fin del gráfico

He modelado los métodos addItemy removeItemdespués de métodos similares en la Choiceclase, por lo que el código tendrá una sensación familiar. Note que usamos dos addItemmétodos aquí; necesitamos una forma de agregar elementos con o sin color. Cuando se agrega un elemento, GraphItemse crea un nuevo objeto y se agrega al vector de elementos. Cuando se elimina un elemento, se eliminará el primero del vector con ese nombre. La GraphItemclase es muy sencilla; aquí está el código:

importar java.awt. *; class GraphItem {Título de la cadena; valor int; Color de color; GraphItem público (título de cadena, valor int, color de color) {this.title = título; this.value = valor; this.color = color; } // fin del constructor} // fin del GraphItem

La GraphItemclase actúa como contenedor de las variables relacionadas con los elementos del gráfico. Lo he incluido Coloraquí en caso de que se use en una subclase de Graph.

Con este marco en su lugar, podemos crear extensiones para manejar cada tipo de gráfico. Esta estrategia es bastante conveniente; no tenemos que tomarnos la molestia de volver a medir los píxeles del marco, y podemos crear subclases para centrarnos en rellenar la región de dibujo del gráfico.

Construyendo el gráfico de barras

Ahora que tenemos un marco de trabajo gráfico, podemos personalizarlo ampliando

Graph

e implementación de dibujos personalizados. Comenzaremos con un gráfico de barras simple, que podemos usar como cualquier otro componente. A continuación se ilustra un gráfico de barras típico. Completaremos la región de dibujo del gráfico anulando el

paint

método para llamar a la superclase

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.

Para garantizar que el trazado vertical sea siempre correcto, debemos asignar los valores de los elementos del gráfico con las ubicaciones de píxeles reales. Existe una complicación: los valores maxy mindeben ser significativos para la posición del valor del elemento del gráfico. En otras palabras, si el gráfico comienza en 150 y llega a 200, un elemento con un valor de 175 debería aparecer en la mitad del eje vertical. Para lograr esto, encontramos el porcentaje del rango del gráfico que representa el elemento y lo multiplicamos por el rango de píxeles real. Debido a que nuestro gráfico está al revés del sistema de coordenadas del contexto gráfico, restamos este número de bottompara encontrar el punto de trazado correcto. Recuerde, el origen (0,0) está en la esquina superior izquierda del código, pero en la esquina inferior izquierda para el estilo de gráfico que estamos creando.