AprendiendoBORLAND Delphi. Marta Sananes. Universidad de Los Andes, Venezuela. Mayo 2002.

Ejemplo de Dibujo con TCanvas, uso de Botones, Check boxes y Diálogo de Color
 
Este ejemplo muestra el uso de varios tipos de controles que facilitan la interacción de los usuarios con la aplicación. También se muestra en forma muy elemental las posibilidades de graficación usando la propiedad de clase TCanvas. 
 
Después de haber estudiado este ejemplo, el lector podría como ejercicio desarrollar una aplicación de dibujo más útil, agregando o rediseñando completamente este ejemplo para que ofrezca al menos las siguientes capacidades: 
  • Dibujar líneas por trozos continuos a partir de un punto inicial hasta un punto final marcado con el segundo botón del ratón.
  • Diferenciar color de trazos (pen) de color de relleno (brush)
  • Colocar letreros
  • Guardar dibujo en formato bmp

El contenido de los archivos que confroman este ejemplo de aplicación se muestra en forma de tablas, en la columna izquierda el código en ObjectPascal y en la derecha explicaciones y comentarios. 

En los archivos de programa (.dpr y .pas), las líneas que son generadas automáticamente por Delphi están en color amarillo; en blanco sólo las que el programador tuvo que agregar. Aparecen resaltadas en ambos casos las palabras reservadas de ObjectPascal, tal como lo hace el Editor de Delphi.
Son tres los archivos que conforman la aplicación: Dibujando.dpr: es el archivo raíz o índice del proyecto, de extensión .dpr (Delphi PRoject); DibujoF.pas: es la Unit que contiene la programación correspondiente a la forma de interfaz de usuario; DibujoF.dfm: es parte de la visualización en modo texto de la Forma diseñada por el programador como interfaz de usuario. Este archivo de extensión .dfm (Delphi ForM) contiene las declaraciones correspondientes al diseño de la Forma; es generado por Delphi transcribiendo la construcción que hace el usuario en forma visual. No hace falta que el programador vea este archivo ni debe modificar manualmente el texto, Delphi se encarga de su actualización cada vez que visualmente se modifique la apariencia o el contenido de la Forma asociada, ya sea por acciones directas sobre la Forma o modificando propiedades con el Object Inspector .

Diseņo de la Forma de Interfaz de Usuario

Dibujando.dpr
 
program Dibujando;

uses
  Forms,
  DibujoF in 'DibujoF.pas' {Form1};

{$R *.RES}

begin
  Application.Initialize;
  Application.CreateForm(TForm1, Form1);
  Application.Run;
end.

Para proyectos diseñados con formas de interfaz gráfica para usuarios, Delphi genera el archivo  de extensión .dpr. En la sección uses coloca los nombres y las ubicaciones de todas las Units incorporadas al proyecto, además de la referencia a la Delphi-Unit Forms, que contiene la Clase TForm, clase primitiva de todas las formas.

Unit DibujoF.pas
 
unit DibujoF;
interface
uses
  Windows, Messages, SysUtils, Classes, Graphics, 
  Controls, Forms, Dialogs, StdCtrls, ExtCtrls;
type
La palabra interface señala el comienzo de bloque de declaraciones, donde se colocan todas las declaraciones o definiciones (tipos, variables, constantes, funciones y procedimientos, labels).

En la instrucción uses Delphi genera la lista de Units utilizadas en la aplicación, a la cual se pueden agregar las que el usuario desee, preferiblemente usando el botón de Agregar Unit en la barra de herramientas del menú principal.
Comienzo de declaraciones de tipos (tipos de datos y clases)


  TForm1 = class(TForm)
    PaintBox1: TPaintBox;
 

    ColorDialog1: TColorDialog; 
 

 

Delphi genera las declaraciones de los controles y componentes colocados por el programador en la forma y registra en el archivo de la forma (DibujoF.dfm en este caso) las propiedades asignadas con el ObjectInspector.

TPaintBox es una clase para definir áres rectangulares dentro de una forma. Posee la propiedad Canvas, de clase TCanvas: área de dibujo. 
 

 

    GroupBox1: TGroupBox; //Agrupa tipos de figuras
    Shape11: TShape;    // Línea
    Shape12: TShape;    // Cuadrado
    Shape13: TShape;    // Círculo
    RadioButton1: TRadioButton; // Línea
    RadioButton2: TRadioButton; // Cuadrado
    RadioButton3: TRadioButton; // Círculo
Los controles de tipo TGroupBox sirven para agrupar controles relacionados en la forma, con letrero descriptivo (propiedad Caption).

En GroupBox1 se agrupan controles para selección de tipo de figura que se puede colocar en el área de dibujo: línea, cuadrado y círculo, represeantados visualmente por los controles de tipo TShape

La selección se hace con controles de tipo TRadioButton, que se comportan de manera excluyente: uno y sólo uno del grupo puede estar seleccionado. Se podría haber usado el tipo de control TRadioGroup, que es específico para este uso, en vez del control genérico TGroupBox.

    GroupBox2: TGroupBox; //Agrupa tipos de línea
    CheckBox1: TCheckBox; // Ancho 1 pixel
    CheckBox2: TCheckBox; // ...
    CheckBox3: TCheckBox;
    CheckBox4: TCheckBox;
    Shape21: TShape;
    Shape24: TShape;
    Shape23: TShape;
    Shape22: TShape;
En GroupBox2 se agrupan controles para selección, según su grosor, del tipo de línea que se puede colocar en el área de dibujo: 1, 2, 3 y 4 pixeles, represeantados visualmente por los controles de tipo TShape.

La selección se hace con controles de tipo TCheckBox, que se  comportan de manera independiente: ninguno, uno, varios o todos pueden estar seleccionados, lo cual es inapropiado en este caso. ¿Por qué ?

    GroupBox3: TGroupBox; //Agrupa mensaje y botón de fin
    Label1: TLabel;      //Mensaje
    BTerminar: TButton;  //Boton para terminar
En GroupBox3 se agrupan controles para indicar el modo de uso de la aplicación, incluido un botón para finalizar.
    GroupBox4: TGroupBox; //Agrupa selección de color
    ShapeColor: TShape; //Cuadrito de selección de color
    Label2: TLabel;     //Mensaje 
En GroupBox4 se agrupan controles para selección de color de dibujo para cualquiera de las selecciones de dibujo (contorno de figuras con relleno y líneas). El control de clase TShape se rellena con el color que esté seleccionado. Se lo hace funcionar como un botón, ya que el método de respuesta al evento OnMouseUp sobre él hace que se active el Diálogo para seleccionar color.
    procedure PaintBox1MouseUp(Sender: TObject; 
              Button: TMouseButton;
              Shift: TShiftState; X, Y: Integer);
    procedure SelLinea(Sender: TObject);
    procedure SelFigura(Sender: TObject);
    procedure Inicializar(Sender: TObject);
    procedure Terminar(Sender: TObject);
    procedure PaintBox1Paint(Sender: TObject);
    procedure ShapeColorMouseUp(Sender: TObject; 
              Button: TMouseButton;
              Shift: TShiftState; X, Y: Integer);
  private
    { Private declarations }
  public
    { Public declarations }
  end;

Métodos de respuesta a los eventos considerados y fin de la declaración de la forma. Delphi coloca los encabezados private y public por si el usuario necesita  definiciones adicionales.
El evento OnMouseUp es equivalente al OnClick, pero con la diferencia de que entrega al método de respuesta las coordenadas en pixeles de la posición del puntero del ratón sobre el control. A este tipo de evento responde el PaintBox1 y el ShapeColor, de clases TPaintBox y TShape respectivamente.
Los demás controles activos responden al evento OnClick.
  TFigura = (linea, cuadrado, circulo);
var
  Form1: TForm1;
  elColor: TColor;
  laFigura: TFigura;
  laLinea: integer;
Se define un tipo enumerado para distinguir los tipos de figuras considerados.
Delphi coloca la declaración de la variable de referencia a la forma definida como objeto de la clase TForm1.

Luego se definen las variables que registran las selecciones consideradas: elColor (referencia a la clase del color activo), laFigura (tipo de figura)  y laLinea (grosor). 

implementation
{$R *.DFM}
Comienzo de bloque de implementación de métodos de las clases definidas y de procedimientos y funciones no asociados a ninguna clase.
procedure TForm1.PaintBox1MouseUp(Sender: TObject; 
          Button: TMouseButton;
          Shift: TShiftState; X, Y: Integer);
begin 
  with PaintBox1.Canvas do begin
    Brush.Color:=elColor; 
    Pen.Color:=elColor; Pen.Width:=laLinea;
    case laFigura of
    linea:  begin MoveTo(X,Y);
                  LineTo(X+60,+60); end;
    cuadrado: Rectangle(X,Y,X+60,Y+60);
    circulo : Ellipse(X,Y,X+60,Y+60);
  end;
  end;
end;
En respuesta al evento OnMouseUp sobre PaintBox1 se dibuja a partir de las coordenadas de la posición del puntero del ratón según el tipo de figura, color y línea que estén selccionados.

Observe el uso de la instrucción Pascalwith que permite abreviar las referencias a propiedades, atributos y métodos del objeto considerado (o los campos si se trata de un record). En este caso el objeto es la propiedad Canvas de clase TCanvas del objeto PaintBox1 de clase TPaintBox. Ver propiedades y métodos de TCanvas en el Help.

La propiedad Color de la propiedad Brush del PaintBox1.Canvas es el color de relleno de figuras. La propiedad Color de la propiedad Pen del PaintBox1.Canvas es el Color de dibujo de líneas y contornos y la propiedad Width es el ancho de línea.

Si la figura seleccionda es linea, se dibuja una línea  partiendo del punto donde estaba el puntero del ratón (método MoveTo..) al hacer Click hasta (método LineTo..) un punto fijo en  la diagonal abajo y a la derecha. (Como ejercicio se propone que la aplicación pueda dibujar línea continuas, para lo cual necesitaría guardar la posición actual del ratón y proporcionar algún mecanismo para indicar fin de línea).

Si la figura seleccionda es cuadrado, se dibuja con el método Rectangle... un rectángulo de lados iguales.

Si la figura seleccionda es circulo, se dibuja con el método
Ellipse... una elipse de ejes de igual longitud.

procedure TForm1.SelLinea(Sender: TObject);
begin 
  with TCheckBox(Sender) do
   if Checked then laLinea:=TabOrder + 1;
end;
En respuesta al evento OnClick en cualquiera de los Check Boxes se coloca como ancho de línea su número de orden dentro de su TGroupBox, ya que la propiedad TabOrder da el orden de colocación de los controles en el grupo, empezando desde 0. El orden de colocación se hizo en el orden del ancho de línea, dado por la propiedad Height, del TShape adyacente. También se hubiera podido definir la propiedad Tag directamente con el valor del ancho para el mismo propósito.

Observe la necesidad de usar typecasting (nombre de un tipo de clase [TCheckBox] aplicado a un objeto) ya que el parámetro que representa al objeto en el que ocurrió el evento (Sender) es del tipo más general en Delphi, TObject.

Observe también la inconveniencia de usar TCheckBox en vez de TRadioButton. Sin embargo, por programación se podría imitar el comportamiento de los botones de radio, haciendo que al seleccionar uno (propiedad Checked en true) se coloque la propiedad en false en todos los demás.
 

procedure TForm1.SelFigura(Sender: TObject);
begin 
  with TRadioButton(Sender) do
    if Checked then laFigura:=TFigura(TabOrder);
end;
En respuesta al evento OnClick en cualquiera de los Radio Buttons se coloca como tipo de figura el tipo correspondiente a su número de orden dentro de su TGroupBox, ya que la propiedad TabOrder da el orden de colocación de estos controles en el grupo, empezando desde 0. El orden de colocación se hizo en el orden del tipo de figura, dado por la propiedad Shape, del TShapeadyacente. También se hubiera podido definir la propiedad Tag con el valor del orden del tipo de figura para el mismo propósito.

Observe la necesidad de usar typecasting ya que la declaración del parámetro que representa al objeto en el que ocurrió el evento (Sender) es de la clase más general en Delphi, TObject. También es necesario usar typecasting para reinterpretar el número entero dado por TabOrder como el tipo de figura definido en  la posición correspondiente a ese orden en el tipo TFigura
 

procedure TForm1.Inicializar(Sender: TObject);
begin
  elColor:=clBlack; 
  laLinea:=1;
  laFigura:=linea;
end;
En respuesta al evento OnActivate de la forma, generado por Windows, se inicializan los valores de las selecciones consideradas: color, ancho de línea y tipo de figura inciales.
procedure TForm1.Terminar(Sender: TObject);
begin
  Close; 
end;
En respuesta al evento OnClick en el Boton que indica Terminar, se cierra la forma con lo cual también termina la aplicación.
procedure TForm1.PaintBox1Paint(Sender: TObject);
begin
  with PaintBox1 do 
       Canvas.Rectangle(0,0,Width,Height); 
end;
En respuesta al evento OnPaint en la forma o en un TPaintBox, el programador puede programar la restauración parcial o total del dibujo contenido en su Canvas, esto es, refrescar el contenido de la propiedad Canvas de clase TCanvas. El evento OnPaint es generado por Windows en la primera aparición al crear la forma o cuando la forma se descubre después de haber estado minimizada o cubierta por otra forma, lo cual a su vez desecandena la ocurrencia de eventos OnPaint en los TPaintBox que estén colocados en la forma.

En este caso sólo se refresca el marco del área del TPaintBox, lo que haya estado dibujado se pierde.

Se presenta más adelante una versión de esta Unit en la que se refresca el área de dibujo.

procedure TForm1.ShapeColorMouseUp(Sender: TObject; 
          Button: TMouseButton;
          Shift: TShiftState; X, Y: Integer);
begin
  with ColorDialog1 do
  if Execute then
  begin elColor:=Color; ShapeColor.Brush.Color:=elColor; end;
end;
En respuesta al evento OnMouseUp sobre el ShapeColor de clase TShape, se despliega el diálogo standard de selección de color. Si el usuario escoge un color, se actualiza la variable global que registra el color activo y se cambia el color de relleno del control ShapeColor.
end. Fin de la Unit.

Forma DibujoF.dfm vista como texto
 

object Form1: TForm1
  Left = 192
  Top = 107
  BorderStyle = bsToolWindow
  Caption = 'Dibujando...'
  ClientHeight = 453
  ClientWidth = 503
  Color = clBtnFace
  Font.Charset = DEFAULT_CHARSET
  Font.Color = clWindowText
  Font.Height = -11
  Font.Name = 'MS Sans Serif'
  Font.Style = []
  OldCreateOrder = False
  OnActivate = Inicializar
  PixelsPerInch = 96
  TextHeight = 13
  object PaintBox1: TPaintBox
    Left = 8
    Top = 14
    Width = 329
    Height = 419
    OnMouseUp = PaintBox1MouseUp
    OnPaint = PaintBox1Paint
  end
  object GroupBox2: TGroupBox
    Left = 352
    Top = 144
    Width = 145
    Height = 129
    Caption = 'Ancho línea'
    TabOrder = 0
    object Shape21: TShape
      Left = 56
      Top = 32
      Width = 65
      Height = 1
    end
    object Shape24: TShape
      Left = 56
      Top = 104
      Width = 65
      Height = 5
      Brush.Color = clBlack
    end
    object Shape23: TShape
      Left = 56
      Top = 80
      Width = 65
      Height = 4
      Brush.Color = clBlack
    end
    object Shape22: TShape
      Left = 56
      Top = 56
      Width = 65
      Height = 2
      Brush.Color = clBlack
    end
    object CheckBox1: TCheckBox
      Left = 16
      Top = 24
      Width = 33
      Height = 17
      Caption = '1'
      Checked = True
      State = cbChecked
      TabOrder = 0
      OnClick = SelLinea
    end
    object CheckBox2: TCheckBox
      Left = 16
      Top = 48
      Width = 33
      Height = 17
      Caption = '2'
      TabOrder = 1
      OnClick = SelLinea
    end
    object CheckBox3: TCheckBox
      Left = 16
      Top = 72
      Width = 33
      Height = 17
      Caption = '3'
      TabOrder = 2
      OnClick = SelLinea
    end
    object CheckBox4: TCheckBox
      Left = 16
      Top = 96
      Width = 33
      Height = 17
      Caption = '4'
      TabOrder = 3
      OnClick = SelLinea
    end
  end
  object GroupBox1: TGroupBox
    Left = 352
    Top = 8
    Width = 145
    Height = 129
    Caption = 'Figura'
    TabOrder = 1
    object Shape11: TShape
     Left = 96
      Top = 24
      Width = 33
      Height = 1
      Brush.Color = clBlack
    end
    object Shape12: TShape
      Left = 96
      Top = 40
      Width = 33
      Height = 33
    end
    object Shape13: TShape
      Left = 96
      Top = 80
      Width = 33
      Height = 41
      Shape = stCircle
    end
    object RadioButton1: TRadioButton
      Left = 16
      Top = 16
      Width = 57
      Height = 17
      Caption = 'Línea'
      Checked = True
      TabOrder = 0
      TabStop = True
      OnClick = SelFigura
    end
    object RadioButton2: TRadioButton
      Left = 16
      Top = 48
      Width = 73
      Height = 17
      Caption = 'Cuadrado'
      TabOrder = 1
      OnClick = SelFigura
    end
    object RadioButton3: TRadioButton
      Left = 16
      Top = 88
      Width = 73
      Height = 17
      Caption = 'Círculo'
      TabOrder = 2
      OnClick = SelFigura
    end
  end
  object GroupBox3: TGroupBox
    Left = 352
    Top = 336
    Width = 145
    Height = 97
    Caption = 'Dibujo'
    TabOrder = 2
    object Label1: TLabel
      Left = 8
      Top = 24
      Width = 134
      Height = 13
      Caption = 'Marque punto en el cuadro'
      Font.Charset = DEFAULT_CHARSET
      Font.Color = clWindowText
      Font.Height = -11
      Font.Name = 'MS Sans Serif'
      Font.Style = [fsItalic]
      ParentFont = False
    end
    object BTerminar: TButton
      Left = 8
      Top = 56
      Width = 129
      Height = 25
      Caption = 'Terminar'
      TabOrder = 0
      OnClick = Terminar
    end
  end
  object GroupBox4: TGroupBox
    Left = 352
    Top = 280
    Width = 145
    Height = 49
    Caption = 'Color'
    TabOrder = 3
    object ShapeColor: TShape
      Left = 8
      Top = 16
      Width = 25
      Height = 25
      Brush.Color = clBlack
      Pen.Color = clWhite
      Pen.Width = 2
      Shape = stSquare
      OnMouseUp = ShapeColorMouseUp
    end
    object Label2: TLabel
      Left = 40
      Top = 16
      Width = 90
      Height = 13
      Caption = ' Click para cambiar'
    end
  end
  object ColorDialog1: TColorDialog
    Ctl3D = True
    Top = 424
  end
end

Unit DibujoF.pas (versión resfrescando el PaintBox1.Canvas, se muestran sólo los cambios)
 

La manera de redibujar o refrescar el contenido del Canvas que se muestra a continuación es efectiva pero, si el área del Canvas es grande, tiene el inconveniente de que consume memoria porque hay que mantener una copia guardada de todos los puntos del área. Esto hace que el proceso de refresacado pueda hacerse lento y que toda la aplicación pueda funcionar también más lentamente si la memoria requerida tiene que ser administrada en modo virtual por no haber disponible suficiente memoria  real. Una forma alternativa que no consuma tanta memoria sería manteniendo una Lista o registro de todos los elementos colocados en el Canvas, de manera que al refrescar se reproduzca el contenido reponiendo todos los elementos. 

Este tipo de solución debe ser más eficiente en uso de memoria y tiempo pero requiere más trabajo de programación, aunque facilitado por Delphi que dispone también de clases predefinidas para manejo de listas.


 
var
  Form1: TForm1;
  elColor: TColor;
  laFigura: TFigura;
  laLinea: integer;

  losPixeles: variant; 
  implementation
{$R *.DFM}

Se agrega la declaración de losPixeles, como variable de tipo predefinido variant, que es un tipo de referencia a arreglos que se crean asignando dinámicamente el espacio o tamaño de memoria requerido, esto es, en tiempo de ejecución. (Ver Help: variant arrays)

El arreglo se usa para guardar el contenido en pixeles de la propiedad Canvas del PaintBox1: valores de color de cada uno de los puntos del área rectangular. 

procedure TForm1.PaintBox1MouseUp(Sender: TObject; 
          Button: TMouseButton;
          Shift: TShiftState; X, Y: Integer);
var i,j: integer;
begin 
  with PaintBox1.Canvas do begin
    Brush.Color:=elColor; 
    Pen.Color:=elColor; Pen.Width:=laLinea;
    case laFigura of
    linea:  begin MoveTo(X,Y); LineTo(X+60,Y+60); end;
    cuadrado: Rectangle(X,Y,X+60,Y+60);
    circulo : Ellipse(X,Y,X+60,Y+60);
  end;
  end;
  // Guardado de todos los puntos del área del Canvas 
  for i:=0 to PaintBox1.Width do 
   for j:=0 to PaintBox1.Height do
    losPixeles[i,j]:=PaintBox1.Canvas.Pixels[i,j];
end;
Se agrega la declaración de variables enteras para el recorrido del área rectangular.

Se guarda el contenido en pixeles de toda el área del Canvas del PaintBox1, porque no se sabe cual pedazo pudiera quedar cubierto que necesite ser refrescado al descubrirse.

Para mayor eficiencia pudiera guardarse sólo los pixeles que se hayan modificado por el dibujo de nuevas figuras.

procedure TForm1.Inicializar(Sender: TObject);
var i, j: integer;
begin 
  elColor:=clBlack; 
  laLinea:=1; 
  laFigura:=linea; 
  losPixeles :=
  VarArrayCreate([0,PaintBox1.Width,
                  0,PaintBox1.Height],varInteger);
  with PaintBox1 do 
  begin
       Canvas.Rectangle(0,0,Width,Height);
       for i:=0 to Width do
        for j:=0 to Height do
           losPixeles[i,j]:=Canvas.Pixels[i,j];
  end;
end;
Se agrega la declaración de variables enteras para el recorrido de todos los puntos del área rectangular.

Se aprovecha también el evento OnActivate de la forma para hacer la creación del arreglo-matriz con capacidad para almacenar los valores de todos los pixeles del área de dibujo Canvas del PaintBox1 (Ver Help: variant arrays), para
colocar un marco al área de dibujo y finalmente guardar todos los puntos (pixeles) en el arreglo dinámico creado.
 

procedure TForm1.PaintBox1Paint(Sender: TObject);
var i, j: integer;
begin 
  with PaintBox1.Canvas.ClipRect do
  for i:=Left to Right do
   for j:=Top to Bottom do
    PaintBox1.Canvas.Pixels[i,j]:=losPixeles[i,j];
end;
Se agrega la declaración de variables enteras para el recorrido de todos los puntos del área rectangular.

Cuando una forma que había quedado cubierta total o parcialmente se descubre, queda definida en la propiedad ClipRect del Canvas, de tipo TRect, la porción rectangular ahora descubierta y que necesita ser redibujada (refrescada), tal como Windows lo detecta. El área queda definida por los valores de los campos del registro (record) tipo TRect: Left, Top, Right y Bottom, en coordenadas relativas al punto esquina izquierda arriba del rectángulo del Canvas. Sólo es necesario entonces refrescar esa porción, lo cual contribuye a la eficiencia de la aplicación ya que redibujar un área grande consume cierto tiempo. El refrescado se hace restaurando en la propiedad Pixeles del Canvas los valores guardados en el arreglo dinámico losPixeles.