Buen camino
Está finalizando este segundo día de nuestro #DelphiWeek especial, y espero que te hayas acercado a ver si todavía podemos compartir una lineas.
Hoy daremos un pequeño paso mas.
Decía en uno de los párrafos finales de la entrada anterior, refiriéndome de forma amplia a los dos primeros ejemplos que compartimos:
Eso sí… considerad que sigue existiendo el mismo problema que en la primera: seguimos necesitando modificar y compilar nuevamente nuestro código cada vez que deseamos ampliar el numero de operaciones disponibles. Además, todavía es poco reutilizable, dado que debemos arrastrar necesariamente todo nuestro interfaz.
Crea tu propia calculadora con Delphi (Parte I)
Aludía a dos aspectos que, en mi humilde opinión, se podían destacar. Uno de ellos el mas evidente, que era que el interfaz fuera íntima parte de la calculadora. Cualquier intento de reutilizar el código, te hubiera obligado a planteártelo. Otro menos evidente a primera vista, es que aunque puedes añadir cuantas operaciones quieras, siempre te vas a ver obligado a compilar nuevamente. ¿Recuerdas la estructura case que discriminaba la operación en función del parámetro que recibía?
Se veía tal que así:
procedure TfrmCalculadoraTrad.ProcesarOperacion(AOperador: Char);
begin
case Operador of
' ' : Operando:= StrToFloat(lbDisplay.Caption);
'+' : Operando:= Operando + StrToFloat(lbDisplay.Caption);
'-' : Operando:= Operando - StrToFloat(lbDisplay.Caption);
'x' : Operando:= Operando * StrToFloat(lbDisplay.Caption);
'/' : Operando:= Operando / StrToFloat(lbDisplay.Caption);
end;
ActualizaDisplay(FloatToStr(Operando));
FlagNumero:= False;
Operador:= AOperador;
end;
Hoy vamos a dar uno de esos pasos y mañana, miércoles, mas o menos a esta hora, compartiremos otro avance, respecto al segundo punto.
Seguimos en el ejemplo demo 2 que habíamos compartido. Pero si te parece, dado que nuestro propósito es compilar en todas las plataformas disponibles por nuestra herramienta, vamos a crear un nuevo proyecto multiplataforma. Pulsamos sobre el menú principal, File, New, Multi Device Application Delphi. Podemos elegir la aplicación vacía (Blank Application) entre las distintas opciones, que es la que mas nos puede interesar en este momento. Una vez hecho esto, sobre el formulario principal, podemos volver a poner los distintos bótones existentes en la demo 2, tal que así:
De momento, seguimos centrados en la plataforma Win32, que por defecto se activará al crear tu proyecto, simplemente por simplicidad, por evitar tener que lidiar con otros problemas que puedan surgir, no relacionados con el mismo código: es posible que intentes hacer el deploy para las plataformas móviles y existan impedimentos que puedan ser «normales» (no importaste los sdks adecuados, no has ejecutado las aplicaron PasServer para comunicar el entorno con la maquina que hace de anfitrión y sobre la que se ejecutará la aplicación, etc…). Es decir, que pueden haber muchos motivos por los que en este punto, y como consejo, puede ser adecuado avanzar en el marco de win32, aun cuando ya hayamos elegido la plataforma de FireMonkey. En cualquier momento podremos activarlas.
El paso de hoy es muy sencillo. Veámoslo:
Tras crear el nuevo proyecto, que habrá añadido el formulario que te sirve de interfaz, y sobre el que has añadido los distintos componentes, añadimos una nueva unidad, que podemos llamar UCalculadora.pas. Para añadirla, otra vez sobre el menú principal, File, New, Unit-Delphi.
Sobre esta nueva unidad, crearemos una clase, con la sana intención de desligar la calculadora propiamente del interfaz de usuario (¿recuerdas al final de la primera parte, el código que te comentaba respecto a la demo3?). Ahora estamos dando un paso sobre la demo 2 para acercarnos a esta idea.
Esta es la clase TCalculadoraBasica, que vamos a especificar e implementar.
type
TCalculadoraBasica = class(TComponent)
private
FDisplay: String;
FlagNumero: Boolean;
Operando: Double;
Operador: Char;
procedure DesplazarDigitoALaIzquierda(AOperando: Char);
procedure IntroduceDigito(AOperando: Char);
protected
procedure DoReset; virtual;
procedure ActualizaDisplay(const AOperando: String); virtual;
public
constructor Create(AOwner: TComponent); override;
function ExisteError: Boolean;
function LeeDisplay: String;
function ProcesarDigito(const ALexema: Char): String;
function ProcesarOperacion(const ALexema: Char): String;
function Reset: String;
end;
Nuestra calculadora ahora arrastra algunos de los métodos y variables que existían en el interfaz de usuario anteriormente. Los remarco en color rojo para que los distingas mejor. Si te fijas, si anteriormente pertenecían al ámbito privado del formulario ahora algunos de ellos deberán ser visibles y por lo tanto, los moveremos a la parte publica de la clase. Es el caso de los métodos ProcesarDigito() y ProcesarOperacion().
Añadimos algunas funciones nuevas, fruto de la nueva naturaleza de la calculadora. La sustitución del componente label por un simple string (FDisplay), nos permite anticipar que necesitaremos un método de lectura, por lo que añadimos LeeDisplay. La acción del Reset, sería el equivalente a nuestro anterior evento al pulsar AC.
De momento, no vamos a evaluar los posibles errores, pero la existencia de esta función nos va a servir en futuras partes de la serie.
Veamos la implementación, que es prácticamente similar a la existente en la demo 2.
implementation
{ TCalculadoraBasica }
procedure TCalculadoraBasica.ActualizaDisplay(const AOperando: String);
begin
FDisplay:= AOperando;
end;
constructor TCalculadoraBasica.Create(AOwner: TComponent);
begin
inherited;
Operando:= 0;
Operador:= ' ';
FlagNumero:= False;
end;
procedure TCalculadoraBasica.DesplazarDigitoALaIzquierda(AOperando: Char);
begin
FDisplay:= FDisplay + AOperando;
end;
function TCalculadoraBasica.ExisteError: Boolean;
begin
Result:= False;
end;
procedure TCalculadoraBasica.IntroduceDigito(AOperando: Char);
begin
if AOperando = ',' then
FDisplay:= '0' + AOperando
else
FDisplay:= AOperando;
if FDisplay <> '0' then FlagNumero:= True;
end;
function TCalculadoraBasica.LeeDisplay: String;
begin
Result:= FDisplay
end;
function TCalculadoraBasica.ProcesarDigito(const ALexema: Char): String;
begin
if FlagNumero then
begin
if Length(FDisplay) < MaxDigitos then
begin
if (FDisplay = '0') and (ALexema = '0') then
Exit(LeeDisplay)
else
begin
//evaluamos si ya exite el punto decimal
if (ALexema = ',') and (StrScan(Pchar(FDisplay), ',') <> nil) then
Exit(LeeDisplay);
DesplazarDigitoALaIzquierda(ALexema);
end;
end;
end
else
IntroduceDigito(ALexema);
Result:= LeeDisplay;
end;
function TCalculadoraBasica.ProcesarOperacion(const ALexema: Char): String;
begin
case Operador of
' ','=' : Operando:= StrToFloat(FDisplay);
'+' : Operando:= Operando + StrToFloat(FDisplay);
'-' : Operando:= Operando - StrToFloat(FDisplay);
'x' : Operando:= Operando * StrToFloat(FDisplay);
'/' : Operando:= Operando / StrToFloat(FDisplay);
end;
ActualizaDisplay(FloatToStr(Operando));
FlagNumero:= False;
Operador:= ALexema;
Result:= LeeDisplay;
end;
function TCalculadoraBasica.Reset: String;
begin
DoReset;
Result:= LeeDisplay;
end;
procedure TCalculadoraBasica.DoReset;
begin
if FDisplay = '0' then
begin
Operando:= 0;
Operador:= ' ';
end
else
FDisplay:= '0';
FlagNumero:= False;
end;
end.
Hemos tenido que modificar ligeramente los métodos principales que se van a comunicar con el interfaz, respectivamente ProcesarOperación y ProcesarDigito.
En el primero, la linea
' ' : Operando:= StrToFloat(lbDisplay.Caption);
ahora va a evaluar el dígito ‘=’,
' ','=' : Operando:= StrToFloat(FDisplay);
de forma que se refunde el evento de click inicial para esta pulsación.
Algo similar nos ha sucedido con el evento correspondiente al punto decimal, que fue refundido también en el código nuevo, correspondiente a la función ProcesarDigito()
Antes:
procedure TfrmCalculadoraTrad.ProcesarDigito(AOperando: Char);
begin
if FlagNumero then
begin
if Length(lbDisplay.Caption) < 12 then
begin
if (lbDisplay.Caption = '0') and (AOperando = '0') then
Exit
else
DesplazarDigitoALaIzquierda(AOperando);
end;
end
else
IntroduceDigito(AOperando);
end;
Ahora:
function TCalculadoraBasica.ProcesarDigito(const ALexema: Char): String;
begin
if FlagNumero then
begin
if Length(FDisplay) < MaxDigitos then
begin
if (FDisplay = '0') and (ALexema = '0') then
Exit(LeeDisplay)
else
begin
//evaluamos si ya exite el punto decimal
if (ALexema = ',') and (StrScan(Pchar(FDisplay), ',') <> nil) then
Exit(LeeDisplay);
DesplazarDigitoALaIzquierda(ALexema);
end;
end;
end
else
IntroduceDigito(ALexema);
Result:= LeeDisplay;
end;
Hemos cambiado la naturaleza de ambos métodos, pasando de ser procedimientos a convertirse en funciones que van a devolver una cadena con el resultado final del Display, de forma que el Interfaz de usuario, podrá actualizar el label adecuado.
Pero… ahora habrás dado un pequeño paso para desligar ambos. Si lo piensas bien: ¿Qué mas te da que sea un label, un botón, un panel, un item de un StringList, quien te devuelva el valor que muestra la pantalla de nuestra calculadora? Quiero resaltar al decir esto, que esta unidad, que representa tu calculadora, es susceptible de ser reutilizada en el ámbito de otra aplicación que la pueda necesitar y eso te sitúa en una posición mejor que en la entrada anterior.
Por lo tanto, nuestro interfaz de usuario, la unidad que yo he llamado UMain.pas, se reducirá sensiblemente, quedando disponible la misma funcionalidad.
Júzgalo tu mismo:
unit UMain;
interface
uses
System.SysUtils, System.Types, System.UITypes, System.Classes, System.Variants,
FMX.Types, FMX.Controls, FMX.Forms, FMX.Graphics, FMX.Dialogs, FMX.StdCtrls,
UCalculadora;
type
TfrmCalculadoraTrad = class(TForm)
bnSiete: TButton;
bnOcho: TButton;
bnNueve: TButton;
bnCuatro: TButton;
bnCinco: TButton;
bnSeis: TButton;
bnUno: TButton;
bnDos: TButton;
bnTres: TButton;
bnCero: TButton;
bnDobleCero: TButton;
bnPuntoDecimal: TButton;
bnResultado: TButton;
bnDivision: TButton;
bnMultiplicacion: TButton;
bnResta: TButton;
bnSuma: TButton;
lbDisplay: TLabel;
btnAC: TButton;
procedure bnDobleCeroClick(Sender: TObject);
procedure btnACClick(Sender: TObject);
procedure btnClick(Sender: TObject);
procedure bnOperarClick(Sender: TObject);
procedure FormCreate(Sender: TObject);
procedure bnPuntoDecimalClick(Sender: TObject);
private
{ Private declarations }
FCalculadora: TCalculadoraBasica;
public
{ Public declarations }
end;
var
frmCalculadoraTrad: TfrmCalculadoraTrad;
implementation
{$R *.fmx}
procedure TfrmCalculadoraTrad.btnClick(Sender: TObject);
begin
lbDisplay.Text:= FCalculadora.ProcesarDigito((Sender as TButton).Text.Chars[0]);
end;
procedure TfrmCalculadoraTrad.FormCreate(Sender: TObject);
begin
FCalculadora:= TCalculadoraBasica.Create(Self);
end;
procedure TfrmCalculadoraTrad.bnOperarClick(Sender: TObject);
begin
lbDisplay.Text:= FCalculadora.ProcesarOperacion((Sender as TButton).Text.Chars[0]);
end;
procedure TfrmCalculadoraTrad.bnPuntoDecimalClick(Sender: TObject);
begin
lbDisplay.Text:= FCalculadora.ProcesarDigito(',');
end;
procedure TfrmCalculadoraTrad.bnDobleCeroClick(Sender: TObject);
begin
FCalculadora.ProcesarDigito('0');
lbDisplay.Text:= FCalculadora.ProcesarDigito('0');
end;
procedure TfrmCalculadoraTrad.btnACClick(Sender: TObject);
begin
FCalculadora.Reset;
lbDisplay.Text:= '0';
end;
end.
Estas son las unidades que hemos visto en este segundo día: Código fuente.
Y mañana que…
No te olvides de acercarte a ultima hora del día. Viviremos la tercera jornada de esta semana especial.
Daremos el siguiente paso sobre el mismo escenario para dar los primeros avances respecto al segundo aspecto citado al iniciar esta segunda parte, y que hacia referencia a la necesidad de compilar sucesivamente la aplicación, a medida que se nos plantean nuevas operaciones. Nos apoyaremos en la herencia y crearemos un catalogo de operaciones disponibles, abordando esta tarea desde la perspectiva de una colección genérica, como puede ser la clase:
TObjectDictionary<String, TDigitoOperador>, parametrizada por un string y una clase base de las distintas operaciones.