Tiempo de diseño y tiempo de ejecución

En Delphi existen dos conceptos que suelen surgir bastante a menudo, tiempo de diseño (design time) y tiempo de ejecución (run time).

El concepto de tiempo de diseño se refiere, de alguna forma, al sistema de diseño del IDE de Delphi, es decir, la parte en la que arrastramos forms, botones, campos de texto, etc … y los situamos en las posiciones que queremos, es decir, realizamos el diseño de nuestra aplicación.

El tiempo de ejecución se refiere al tiempo durante el cual el programa esta ejecutando, es decir, el tiempo de vida de la aplicación.

Tiempo de diseño vs Tiempo de ejecución

Cuando estamos creando nuestra aplicación, la forma más natural de definir el aspecto de esta ir creando los forms que necesitemos e ir añadiendo botones, campos de texto y otros y situandolos en pantalla hasta encontrar el diseño que buscamos.

Cada vez que creamos un form delphi automáticamente crea una nueva unidad (.pas) y un nuevo fichero de descripción de formulario (.dfm). El IDE de Delphi se encarga de ir modificando estos dos ficheros automáticamente para reflejar los cambios que vamos haciendo. Por ejemplo, vamos a echar un vistazo a un dfm de ejemplo que contiene un Form (Form1) que a su vez contiene un TMemo y TButton.

object Form1: TForm1
  Left = 192
  Top = 107
  Width = 696
  Height = 432
  Caption = 'Ejemplo'
  Color = clBtnFace
  Font.Charset = DEFAULT_CHARSET
  Font.Color = clWindowText
  Font.Height = -11
  Font.Name = 'MS Sans Serif'
  Font.Style = []
  OldCreateOrder = False
  PixelsPerInch = 96
  TextHeight = 13
  object memoText: TMemo
    Left = 16
    Top = 16
    Width = 665
    Height = 337
    Lines.Strings = (
      'Esto son algunas'
      'lineas del memo')
    TabOrder = 0
  end
  object btnAceptar: TButton
    Left = 600
    Top = 368
    Width = 75
    Height = 25
    Caption = 'Aceptar'
    TabOrder = 1
    OnClick = btnAceptarClick
  end
end

Como se puede ver, el dfm se limita a especificar, siguiendo un formato esquematizado bastante intuitivo, las propiedades que vamos definiendo en tiempo de diseño desde el Id de delphi, incluyendo parametros como la posición, color o los eventos asociados (el onclick del boton en este caso). En el .pas por otro lado podemos encontrar la linea que hace referencia a dicho dfm:

implementation

{$R *.dfm}

que le indíca al compilador de Delphi que debe leer la información del fichero con el mismo nombre que el .pas pero con extensión dfm.

El modo de diseño es el más común para diseñar el interface de nuestra aplicación, sin embargo existen determinados casos en los que su uso no es lo más adecuado o sencillamente no es viable (como por ejemplo si queremos crear un determinado numero de botones pero no sabremos cuantos hasta que el programa este ejecutando, lea de una base de datos o similar).

Por otro lado, he observado en ocasiones el uso indiscriminado del IDE para la creación de componentes no visuales (TQuerys, Sockets… etc). Pese a que en algunos casos puede resultar comodo (por ejemplo la creación de un Timer como componente no visual puede ser interesante) en la mayoría de los casos resulta una mala práctica de programación llenar nuestro formulario con componentes que realmente no necesitamos. Por ejemplo resulta absurdo arrastrar al form un TQuery por cada query a la base de datos que queramos definir, no sólo es una práctica burda de programación sino que además dificulta la realización del diseño en pantalla.

Pasando de tiempo de diseño a tiempo de ejecución

En general lo que vamos a hacer ahora (cojer un objeto del dfm y realizar su equivalente en código) no tiene caso hacerlo, el diseñador de Delphi es muy completo y no tiene sentido (como norma general) intentar ocupar su lugar generando el código que ya de por si el genera de forma correcta pero servirá como introducción y ejemplo a la creación de componentes en tiempo de ejución.

Concretamente vamos a eliminar del dfm que vimos antes el botón y lo vamos a hacer que dicho botón se cree al crearse el form, para ello eliminaremos las lineas correspondientes al botón asi como la declaración del componente en el .pas, es decir, del dfm eliminamos:

  object btnAceptar: TButton
    Left = 600
    Top = 368
    Width = 75
    Height = 25
    Caption = 'Aceptar'
    TabOrder = 1
    OnClick = btnAceptarClick
  end

y del .pas eliminariamos

type
  TForm1 = class(TForm)
    memoText: TMemo;
    btnAceptar: TButton; // ---- Eliminamos esta linea
    procedure btnAceptarClick(Sender: TObject);

Ahora lo que vamos a hacer es crear el botón cuando se cree el form por lo que necesitamos:

  • Crear una variable de tipo TButton privada
  • Realizar los pasos necesarios para, crear, situar y asignar las propiedades correspondientes a dicha variable en el evento Create del formulario.
procedure TForm1.btnAceptarClick(Sender: TObject);
begin
  ShowMessage('Wohoo');
end;

procedure TForm1.FormCreate(Sender: TObject);
begin
  { Creamos el boton, el encargado de liberarlo es el form (Self) }
  btnAceptar := TButton.Create(Self);
  with btnAceptar do
  begin
    { Asignamos la propiedad parent, necesario para que se muestre }
    Parent := Self;
    { Asignamos sus propiedades }
    Left := 600;
    Top := 368;
    Width := 75;
    Height := 25;
    Caption := 'Aceptar';
    TabOrder := 1;
    OnClick := btnAceptarClick;
  end;
end;

Caso práctico. Ejemplo I

Uno de los casos más comunes en los que no tiene sentido crear un componente es en el caso de las query a la base de datos, especialmente si son muchas puesto que arrastrar un TQuery a nuestro form por cada consulta que queramos hacer. Podríamos discutir si se podría crear uno o dos y reutilizarlos en el programa pero, en mi opinión dicha elección es francamente desaconsejable en la mayoría de los casos ya que el posible beneficio de rendimiento es practicamente despreciable y la perdidad de legibilidad en el código y en el diseño es considerable.

procedure MiAccesoABaseDeDatos;
var
  qry : TQuery; // El objeto que dará acceso a la base de datos
begin
  // Creamos el objeto sin owner
  qry := TQuery.Create(nil);
  // Asignar el alias de la base de datos
  // sobre la que se ejecutará la query
  qry.DatabaseName := 'DatabaseAlias';
  // Definir la query SQL
  qry.Sql.Add('SELECT * FROM Clientes');
  // Ejecutar la query
  qry.Open;
  // Obtener la información que queramos
  txtName.Text := qry.FieldByName('Name').AsString;
  txtApellidos.Text := qry.FieldByName('Apellidos').AsString;
  // Cerrar y liberar el query
  qry.Close;
  qry.Free;
end;

El parametro que pasamos en el Create (owner) indica el objeto responsable de liberar el objeto. En el ejemplo en que pasamos el botón del .dfm al .pas el owner era el propio form ya que , al destruirse el form este llamará automáticamente al destructor de todos los objetos de los que es dueño (owner). En este caso sin embargo no queremos que nadie libere el objeto ya que nos vamos a encargar nosotros de ello por lo que definimos el owner como nil.

Caso práctico. Ejemplo II

Otra situación que puede resultar de interes es la creación de componentes visuales, como pueden ser botones o campos de texto, por ejemplo, en una ocasión se me presento el siguiente problema. Quería generar un pequeño programa que creará la secuencia SQL de creación de una tabla, para ello el cliente debería especificar cada uno de los campos de los que constará la tabla pero, ¿cuantos campos se mostrarán en el formulario de entrada? ¿cuatro? ¿cinco? ¿veinte?. Es evidente que no hay una solución factible estática de forma que tendremos que ir creando los campos necesarios de forma dinámica, a ser posible conforme el usuario los vaya necesitando.

Para ello había que ir creando dinámicamente los campos, que cuando el usuario hubiera rellenado uno se crearan un nuevo textbox (para introducir el nombre del campo) y un nuevo combobox (con los tipos de campo a elegir) (había también otros campos pero quedan fuera de este ejemplo).

Para hacer este programa decidí finalmente crear un objeto de tipo TScrollBox (un componente que ofrece la posibilidad de hacer scroll para mostrar lo que hay dentro de él) en tiempo de diseño y añadir en tiempo de diseño los campos mediante un pequeño boton ‘+’.

Antes de poner el código que consigue el ‘milagro’ aclararemos otro concepto, el de parent. Igual que vimos que existia un parametro owner en el Create, cada componente visual tiene una propiedad llamada parent que define el padre del objeto, es decir el componente que contiene el objeto (y que debe ser un descendiente de la clase TWinControl), posibles valores pueden ser un form, un group box, un scrollbox, un panel, etc … Es necesario asignar esta propiedad para que determinados componentes se muestren ya que de lo contrario no tendrían sentido (un botón tiene que estar ubicado en un form (o algo que esté dentro de un form) para poder mostrarse, no existe el concepto de un boton «flotando» sobre el escritorio).

Partimos de un formulario creado que contiene un TScrollBox (scbFields) y un botón (btnAddField) que cada vez que se pulse añadirá un nuevo campo de texto y un combobox.

procedure TForm1.btnAddFieldClick(Sender: TObject);
var
  edt : TEdit;
  cmb : TComboBox;
  ctrl : TControl;
  desplazamiento : integer;
begin
  desplazamiento := 0;
  { Obtener los datos del último control creado.
    Puesto que creamos primero el edit box y luego
    el combo box será el combo }
  if scbFields.ControlCount > 0 then
  begin
    ctrl := scbFields.Controls[scbFields.ControlCount - 1];
    desplazamiento := ctrl.Top + ctrl.Height;
  end;
  // Crear y posicionar el editbox
  edt := TEdit.Create(scbFields);
  edt.Parent := scbFields;
  edt.Left := 10;
  edt.Width := 100;
  // Calcular la posición (posición top, más alto más 3)
  edt.Top := desplazamiento + 3;
  // Crear y posicionar el combobox
  cmb := TComboBox.Create(scbFields);
  cmb.Parent := scbFields;
  cmb.Left := edt.Left + edt.Width + 10;
  cmb.Top := edt.Top;
  // Añadir valores al combo box
  cmb.Items.Add('INTEGER');
  cmb.Items.Add('BOOLEAN');
  cmb.Items.Add('MEMO');
end;

Actualización: A raiz de una pregunta sobre este tema en los foros de trucomanía he decidido actualizar el artículo para reflejar como realizar la asignación de eventos sobre los nuevos objetos creados. Para ello vamos a suponer que queremos que todo lo que se escriba en el campo apareza en mayusculas. Para ello necesitamos asignar el evento OnChange de los editbox que vamos creando de forma que hagan automáticamente dicha comprobación.

Un evento en Delphi (y con evento me refiero al procedimiento que responde a una acción de un componente como por ejemplo un click) no es más que un puntero a un metodo, dicho metodo estará tipado, es decir, tendrá definida una serie de parametros que indican como es (en C# a esto se le llama delegado (delegate)). Si examinamos la ayuda del evento OnChange de un TEdit obtenemos que es de tipo TNotifyEvent:

property OnChange: TNotifyEvent
`

`
type TNotifyEvent = procedure(

  Sender: TObject

) of object;

De forma que un TNotifyEvent es un metodo de tipo procedure que recibe un parametro de tipo TObject (el of object final nos indica precisamente que es un procedimiento de un objeto, es decir, un metodo).

De esta forma sabemos que a la propiedad OnChange de nuestro TEdit debe asignarsele un metodo de tipo TNotifyEvent de forma que nuestro nuevo código quedaría …

procedure TForm1.EditChange(Sender: TObject);
var
  edt : TEdit;
begin
  edt := Sender as TEdit;
  if Assigned(edt) then
  begin
    // Convertir a mayusculas
    edt.Text := UpperCase(edt.Text);
    // Situar el cursor al final
    edt.SelStart := Length(edt.Text);
  end;
end;

procedure TForm1.btnAddFieldClick(Sender: TObject);
var
  edt : TEdit;
  cmb : TComboBox;
  ctrl : TControl;
  desplazamiento : integer;
begin
  desplazamiento := 0;
  { Obtener los datos del último control creado.
    Puesto que creamos primero el edit box y luego
    el combo box será el combo }
  if scbFields.ControlCount > 0 then
  begin
    ctrl := scbFields.Controls[scbFields.ControlCount - 1];
    desplazamiento := ctrl.Top + ctrl.Height;
  end;
  // Crear y posicionar el editbox
  edt := TEdit.Create(scbFields);
  edt.Parent := scbFields;
  edt.Left := 10;
  edt.Width := 100;
  // Calcular la posición (posición top, más alto más 3)
  edt.Top := desplazamiento + 3;
  // Asignar el evento
  edt.OnChange := Form1.EditChange;
  // Crear y posicionar el combobox
  cmb := TComboBox.Create(scbFields);
  cmb.Parent := scbFields;
  cmb.Left := edt.Left + edt.Width + 10;
  cmb.Top := edt.Top;
  // Añadir valores al combo box
  cmb.Items.Add('INTEGER');
  cmb.Items.Add('BOOLEAN');
  cmb.Items.Add('MEMO');
end;

Ahora cada vez que se vaya cambiando el texto en cualquiera de los TEdit se llamará al evento EditChange (el parametro sender podría servirnos si quisiéramos para realizar acciones diferentes dependiendo de quien genere el evento).