Módulo C.P.2017 (III): Reloj Analógico.

Buen camino a todos:

La tercera parte de la serie, cuarta si incluimos la entrada introductoria, debería finalizar con un reloj que nos muestre, además de las manecillas, los dos diales, que representan como imagináis unas marcas sobre la esfera, que ayudan visualmente a fijar la hora. Lo cual, nos da una excusa para hablar del componente TPath, que representa un componente que es capaz de gestionar un grupo de curvas y lineas, conectadas entre si, formando, como imagino intuís, un camino. No hay que confundir con otro TPath, registro (record), que, por alguna extraña casualidad o infortunio, ha coincidido en el nombre, pero ubicado en System.IOUtils y vinculado con las rutas de los ficheros; y el que vamos a compartir, definido en la unidad FMX.Objects. Y de TPath, si vamos al detalle, lo que nos interesa es una de sus partes, un objeto instanciado a través de la clase TPathData, especializada en hacer el trabajo sucio con esos puntos, lineas o figuras.

Pero antes de entrar en esas cuitas, tenemos que abordar varios puntos: el primero de ellos, relacionado con un consejo que os puede ser práctico. Posteriormente, comentaremos unos cambios que introducimos, a raíz del comentario de nuestro compañero Germán Estévez, añadiremos también un temporizador a nuestro reloj, que era algo que ya comentamos al despedir la entrada anterior, y finalmente, abordaremos los diales. Esa es la ruta que os propongo para hoy.

¿Estais listos…?

A prueba de fugas…

Me parecía necesario hacer una pequeña parada técnica para recordar un tema importante y que -en ocasiones- puede pasar inadvertido, al menos al principio. Nos viene bien, hacer ese pequeño descanso antes de seguir. Por el título, «fugas», «a prueba de fugas» estaréis pensando que hablo de la seguridad. 😀   No. No… A ver: levantad un poco la vista, sobre la imagen de la cabecera de la entrada, que me ha costado un buen rato componer: ¿veis la palabra ReportMemoryLeaksOnShutdown escondida?

1896994_10205986662721341_5397199377122110989_n
Juan Antonio Castillo – El blog de Jachguate

Hablaba de la correcta gestión de la memoria, o por lo menos de tener la certeza de que ésta se libera correctamente. El término pertenece a una variable global que nos permitirá comprobarlo, pero antes de ampliar ese punto, tengo otro enlace para vosotros.

Y vuelvo a citar nuevamente a mi buen amigo Juan Antonio Castillo, un grande MVP de Embarcadero, del que siempre acabas aprendiendo tanto en persona, desde la redes o los grupos de trabajo, como desde su blog (El blog de Jachguate) que por supuesto recomiendo. De su blog recojo hoy un enlace del 2014, concretamente de su entrada Fastmm, fulldebugmode y fugas de memoria. que quería citar para que le deis una lectura detenida por que viene ni pintado para abrir la reseña. Es obvio que las fugas de memoria o una incorrecta gestión puede traer al traste un programa y hacer que nuestro esfuerzo quede un poco en entredicho. Juan apunta:

Fuga de memoria
(memory leak en inglés), es la condición por la cual un programa no libera bloques de memoria que ha reservado pero en realidad ya no está utilizando, y por ello si se ejecuta durante el tiempo suficiente, puede llegar a agotar la memoria disponible para la aplicación o incluso en todo el sistema.

El primer concepto que aprendí al llegar a Delphi, hace casi 20 años, era que uno debía ser muy respetuoso y responsable con todo cuanto creaba. Era como jugar a ser Dios.  Todo lo que tu ponías sobre la mesa, eras el responsable de limpiarlo. Y eso estaba grabado a fuego, porque se era consciente de la importancia que tenía, junto con otros aspectos que alertaban en el uso eficiente de la misma.

Su entrada, describía con bastante detalle los pasos para activar el control de memoria de FastMM. Hoy, en Berlin 10.1, tanto en W32 como en W64 se mantiene el mismo gestor de memoria, (Berlín Starter es la versión que estamos utilizando para abordar la serie), pero bastaría asignar a true, la variable global comentada unas lineas mas arriba y al cerrarse la aplicación podemos obtener un reporte, si existe memoria que no se ha podido liberar al cerrar nuestra aplicación.

Podeis incluir la linea en la sección de inicialización de la unidad, para que se cargue con el módulo, al inicio:

initialization
 ReportMemoryLeaksOnShutdown:= True;

report_memory_leaks

Y comentarla cuando ya no la necesitéis.

Un típico caso donde es útil es ante el olvido de una llamada a Free, que tampoco debería ser lo mas habitual, al destruir nuestro formulario o componente. El reporte tiene la forma de una ventana modal y en dicha ventana, detalla los bloques de memoria con problema, al detener su ejecución.

Aquí lo veis en acción. He eliminado la linea FInternalTimer.Free, con un comentario (las dos barras //), y al cerrar el proyecto emerge la notificación.

buena_idea
Se pierde el acceso a la memoria que la instancia ocupaba pero podemos detectar esta circunstancia.

Como bien dice Juan Antonio es un tema extenso que excede su entrada, y lamentablemente también estas lineas de descanso. Me quedo con algunos puntos a recalcar: hablamos solo de windows y no de otras plataformas. Y según la documentación, solo es aplicable a Delphi y no a C++Builder. Es sencillo de aplicar como veis, pero os daría un consejo: no esperéis al final del desarrollo. Si desde el principio evaluáis que no hay fugas de memoria y lo seguís repitiendo gradualmente, será mas fácil reconocer los puntos que la generan, si en algun momento aparecen. Tal vez no sean culpa vuestra. Quizás sea un componente de terceros que inadvertidamente los cuela. En cualquier caso, vosotros sois los responsables y quienes al final vais a tener que lidiar con ello. Aquí no vale decir: ¡es que yo esperaba que el recuento de referencias liberara la instancia al final…!  =:-O

Escuchando a los mayores…

En la entrada anterior, tuvimos el privilegio de que German nos acompañase en los comentarios. Apostaba sobre la conveniencia de mover uno de los métodos.

Yo hubiera ubicado el procedimiento AjustarManecilla dentro de la clase TManecilla, al igual que has hecho, por ejemplo, con el SetAnguloRotacion.

Decir antes que nada que este «tipo», Germán, en un tío sano, grande e inteligente. Y hasta donde se, ciclista redomado, disfrutando en los alrededores de Barcelona.

82fd8f194d9593ce79d2ebaeb8cd7812

Y no solo es uno de los MVPs que mas brilla, por merecimiento propio, sino que además es una persona sencilla, y de esas que uno agradece conocer y mantener buena amistad. Con él, junto a otros compañeros, se sostiene uno de los sitios referencia para la Comunidad, como Club Delphi. Y su blog, al igual que el de Juan Antonio, mantiene información de calidad y de gran ayuda para quienes llegan a Delphi, sean noveles o veteranos.

Vamos a ver que podemos hacer por contentarle…

Lo primero de todo, vamos a añadir una propiedad a la clase TManecilla:

property PorcentajeSobreRadio: Single read FPorcentajeSobreRadio write SetPorcentajeSobreRadio;

Cuando estaba evaluando la posibilidad de mover el método, tal y como comentaba German, me dí cuenta de que existía una opción mejor, y que nos permitía incluso eliminarlo. Y para poder hacer esto, me hacia falta conocer desde la aguja el porcentaje a aplicar y mover, parte de las rutinas que contiene a otro punto de ejecución.

La idea era trasladar el posicionamiento de ajuste, también al método SetBounds() sobrescrito en el reloj. De esa forma, al hacer: (en azul las lineas añadidas)

procedure TMiReloj.SetBounds(X,Y, AWidth, AHeight: Single);
begin
 inherited SetBounds(X,Y,AWidth,AHeight);
 FRadioEsfera:= Min(ShapeRect.Width / 2, ShapeRect.Height / 2)- MargenEsfera;

 with FMSegundos do
 begin
   AjustarPosicion(AWidth, AHeight);
   Width:= FRadioEsfera * PorcentajeSobreRadio / 100;
   Height:= FRadioEsfera * PorcentajeSobreRadio / 100;
 end;

 with FMMinutos do
 begin
   AjustarPosicion(AWidth, AHeight);
   Width:= FRadioEsfera * PorcentajeSobreRadio / 100;
   Height:= FRadioEsfera * PorcentajeSobreRadio / 100;
 end;

 with FMHoras do
 begin
   AjustarPosicion(AWidth, AHeight);
   Width:= FRadioEsfera * PorcentajeSobreRadio / 100;
   Height:= FRadioEsfera * PorcentajeSobreRadio / 100;
 end;
end;

no era necesario invocar ningún método adicional durante la ejecución de Paint() para corregir el tamaño. Cada vez que el reloj se redimensiona, cambian las posiciones de las manecillas, así como el radio correcto al tamaño y su talla respecto a la esfera. Y eso funcionaría tanto en tiempo de diseño como de ejecución.

Esto implica, lógicamente, hacer un pequeño cambio durante la construcción de cada aguja. Hemos añadido un método privado, que además de los parámetros existentes anteriores, introduce el nuevo valor, y de paso, resuelve el tema planteado, que os dejé para meditar. Mi propuesta sería tal que así:

function TMiReloj.CreaManecilla(const ATipoManecilla: TTipo;const ANombre: String; APorcentajeRadio: Single): TManecilla;
begin
  Result:= TManecilla.CreateManecilla(self, ATipoManecilla);
  with Result do
  begin
    Parent:= Self;
    Name:= ANombre;
    PorcentajeSobreRadio:= APorcentajeRadio;
    Stored:= False;
  end;
end;

Y ahora, nuestro constructor se ve favorecido por ser mas legible, como apuntaba en aquella entrada Juan Antonio.

constructor TMiReloj.Create(AOwner: TComponent);
begin
  inherited;
...
 FMHoras:= CreaManecilla(maHoras, 'manecilla_horas', 40.0);
 FMMinutos:= CreaManecilla(maMinutos, 'manecilla_minutos', 55.0);
 FMSegundos:= CreaManecilla(maSegundos, 'manecilla_segundos', 70.0);
...

end;

Por lo tanto, dicho esto, no es que se haya movido el método… es que nos lo hemos cargado, caput. Ea!!!  😉

Es decir, la clase TManecilla, ha quedado un poco mas eficiente y sencilla, añadiendo una propiedad para guardar el porcentaje sobre el radio, y eliminando un método en el reloj, que no se necesitaba expresamente.

También, aprovechamos para hacer un cambio adicional en la declaración de TMiReloj, moviendo de la zona privada a la zona protegida estricta las variables FMHoras, FMMinutos y FMSegundos:

...
strict protected
 FMHoras: TManecilla;
 FMMinutos: TManecilla;
 FMSegundos: TManecilla;
protected
...

Eso nos permitirá acceder a las instancias desde un descendiente de TMiReloj, de forma que seamos capaces de modificar en un futuro, algún valor que ahora no estamos considerando, como por ejemplo el color o el grosor. Yo en este caso, me he decantado en usar un seccion Strict Protected, porque deseo expresamente que solo sean accesibles desde un nuevo componente.

Dicho esto, podemos darle un poco de vida. Vamos a añadir un temporizador, representado en una instancia de TTimer que se dispara cada segundo.

Todo el tiempo del mundo…

El componente TTimer, pertenece a esa categoría de componentes No Visuales, mas bizarros y entrañables de la paleta de componentes. Veréis, es por su sencillez, un tesoro para los que se inician porque, aunque no tenga componente_ttimerrepresentación visual en tiempo de ejecución, es sencillo, útil y práctico de usar y el candidato ideal para acompañarte en los primeros pasos. No es de extrañar, encontrar algún neófito ajustando el estado de un botón mediante una atrevida composición de eventos temporizados para que el usuario sepa que no va poder pulsarlo. 😀 Posiblemente, esta cándida alma todavía no ha descubierto las acciones (TActions) pero es un poco lo de menos, porque todos nos hemos reconocido en algún momento aprendiendo y evolucionando continuamente, a veces tras la lectura de un libro o simplemente, tras conversar con un compañero con el que hemos compartido unas lineas de código. Y eso, es sin duda lo positivo. Decía acertadamente un compañero y amigo, durante un curso que recibí al tiempo de dar mis primeros pasos, que uno siempre estaba aprendiendo, continuamente, y que, a medida que ibas escalando esa montaña de conocimientos, descubrías que estabas en el fondo del valle, y que aun quedaban picos mas altos que escalar. Esa es la humildad y el espíritu con el que debéis vivir y no solo es aplicable a la programación sino que llega a ser una lección de vida.

Nuestro temporizador, como el mismo nombre indica, va a repetir la ejecución de un método que tiene el tipo TNotifyEvent. Por defecto, el valor de intervalo es de 1 segundo (1000). Y la propiedad Enabled, activa o desactiva la ejecución. Y poco mas…

Vamos a añadir, una referencia a nuestra declaración de clase. Podemos de momento situarla en la sección privada de la misma.

 TMiReloj = class(TCircle)
 private
 { Private declarations }
  FDateTime: TDateTime;
  FRadioEsfera: Single;
  FInternalTimer: TTimer;
  FEnableTimer: Boolean;
  FOnInternalTimer: TNotifyEvent;
...

Nos hacer falta, un par de propiedades, que añadiremos a la sección published o publicada del componente.

published
...
  property EnableTimer: Boolean read FEnableTimer write SetEnableTimer default False; 
  property OnInternalTimer: TNotifyEvent read FOnInternalTimer write FOnInternalTimer;
...
end;

La primera representa el interruptor, con dos valores (encendido o apagado) y la segunda, como hemos indicado el método a ejecutar para cada activación. En el método de escritura de la propiedad EnableTimer, se nos presenta la oportunidad para enlazar este valor, que va ser persistente, con el estado del temporizador.

Podemos hacer:

procedure TMiReloj.SetEnableTimer(const Value: Boolean);
begin
 if FEnableTimer <> Value then
 begin
  FEnableTimer := Value;
  FInternalTimer.Enabled:= FEnableTimer;
  Repaint;
 end;
end;

De esa forma, cambiamos el estado del reloj interno en función de este valor.

El siguiente paso, es añadirlo al constructor para instanciar la clase, y en el destructor para liberar la memoria. Igualmente a lo hecho para nuestras manecillas, he añadido un método privado que encapsula la creación del componente.

function TMiReloj.CreaTemporizador(FuncTemporiza: TNotifyEvent): TTimer;
begin
 Result:= TTimer.Create(nil);
 with Result do
 begin
  Enabled:= False;
  OnTimer:= FuncTemporiza;
 end;
end;

De esa forma, es mas legible el código:

constructor TMiReloj.Create(AOwner: TComponent);
begin
 inherited;
...
 FEnableTimer:= False;
 FInternalTimer:= CreaTemporizador(TimerOnInternalTimer);

Y el metodo destructor, se limita a liberar la memoria asociada a dicha instancia.

destructor TMiReloj.Destroy;
begin
 FInternalTimer.Free;
 ...
 inherited;
end;

Y finalmente, el «qué hará»: el método TimerOnInternalTimer, que se ocupa de actualizar la hora interna de nuestro reloj.

procedure TMiReloj.TimerOnInternalTimer(Sender: TObject);
begin
 SetNewTime(Now);
 if Assigned(FOnInternalTimer) then FOnInternalTimer(Self);
end;

Si recordáis la entrada anterior, la ejecución de SetNewTime, ajustaba el ángulo de rotación de las manecillas, para sincronizar la hora visualmente.

Y la última línea, dispara el evento citado anteriormente, en el caso de que haya sido asignado, para dar la oportunidad a quienes usan nuestro reloj -desde la interfaz- a enlazar el evento y responder también si lo desean, toda vez que nosotros ya hemos asignado el propio del componente TTimer (*).

(*) Notad que no estamos ante una suscripción representada en un array de notificaciones sino de un puntero a un método. En otros entornos, sí pueden existir por diseño un proceso de registro donde la filosofía sería similar a: yo también quiero participar, o yo también deseo que me lo notifiques. Y el gestor, recorre una lista de tareas por hacer o por notificar. En nuestro caso: la filosofía es «yo me ocupo, no molestes…».  🙂

Sintonizando el dial…

Abordamos la parte que dibuja cada dial o marca horaria.

Os situo…

La documentación de Embarcadero trae en la parte que denomina Guía de creación de Componentes para Firemonkey, un detalle de los pasos a seguir que aunque, escueto, ¡porque siempre deseamos que sea lo mas exhaustivo posible!, es suficiente para poder dar esos primeros pasos. Mencioné este punto en algún momento anterior.

El paso tercero, Define un camino para dibujar un polígono, es un de los ejemplo mas acertados que he podido encontrar, para explicar como se construye una colección de puntos, lineas y movimientos hasta generar una ruta, que se transformará finalmente en una imagen, algo visual que podemos ver. Desde las figuras geométricas mas sencillas hasta complejas imágenes, todo tiene cabida en un TPathData.

Permitidme que os muestre la imagen que ilustra aquel ejemplo, porque nos vamos a servir de parte de estas rutinas para generar las nuestras:

polgyongdrawing
Imagen que ilustra el ejemplo de Embarcadero

La documentación de ayuda, se apoya en el método CreatePath, método diseñado para construir un polígono regular en función del numero de puntos menos uno, ya que como veis en la imagen, el punto 1 y 4 coinciden (son el mismo). Para cuatro puntos obtenemos un triángulo, para cinco un cuadrado, para seis un pentágono.

¿Os dais cuenta?…

No va a ahorrar mucho trabajo. De hecho, y para que se vea que expresamente me he basado en dicha construcción, he mantenido el nombre original al método GoToVertex( ), ejecutado dentro de CreatePath. Los adaptaremos para que nos ayuden a dibujar cada dial y en mi caso, yo lo voy a llamar:

procedure CreateDial(const ATipoDial: TTipo); virtual;

Suponemos el primero de ellos, permite adaptarse tanto para los minutos como para los segundos y cuenta con 60 marcas no unidas entre si. Por el contrario, el segundo dial, solo cuenta con 12 marcas. Parametrizar la invocación del método nos ayudará a dibujar uno u otro según el caso.

Otro detalle a tener en cuenta es la posición del punto 1 y 4, situado a las 3 (con un ángulo de cero grados sobre el eje X). Lo lógico, y siendo un reloj tan chulo como el que queremos construir es situarnos a 90 º, a las 12 en punto. Para ello, nos basta intercambiar el seno por el coseno en ambas coordenadas X,Y y ¡voila!, nuestro dibujo iniciará su trazo desde la parte superior. Este es el cambio que proponemos: (en azul)

 NewLocation.X := (Width / 2) + (Sin(n * Angle) * CircumRadius);
 NewLocation.Y := (Height / 2) - (Cos(n * Angle) * CircumRadius);

Así que basándonos en esto, vamos a darle forma:

procedure TMiReloj.CreateDial(const ATipoDial: TTipo);

 procedure GoToAVertex(n: Integer; p: Integer; Angle, CircumRadius: Double;
 const ATipoDial: TTipo; IsLineTo: Boolean = True);
 var
  NewLocation: TPointF;
  FLongDial: Integer;
  begin //punto centro
   NewLocation.X := (Width / 2) + (Sin(n * Angle) * CircumRadius);
   NewLocation.Y := (Height / 2) - (Cos(n * Angle) * CircumRadius);

   case ATipoDial of
     TTipo.maSegundos: FLongDial:= 5;
     TTipo.maMinutos: FLongDial:= 7;
     TTipo.maHoras: FLongDial:= 10;
   else
     FLongDial:= 0;
   end;

   if IsLineTo then
   begin
     FPath.MoveTo(NewLocation);
     NewLocation.X:= NewLocation.X + (Sin(n * Angle) * FLongDial);
     NewLocation.Y:= NewLocation.Y - (Cos(n * Angle) * FLongDial);
     if n <= p then FPath.LineTo(NewLocation);
   end
   else
     NewLocation.X:= NewLocation.X + (Sin(n * Angle) * FLongDial);
 end;

var
 i: Integer;
 Angle: Double;
 FNumberOfSides: Integer;
begin
 case ATipoDial of
   TTipo.maSegundos: FNumberOfSides:= 60;
   TTipo.maMinutos: FNumberOfSides:= 60;
   TTipo.maHoras: FNumberOfSides:= 12;
 else
   FNumberOfSides:= 1;
 end;
 Angle:= 2 * PI / FNumberOfSides;

 FPath.Clear;

 GoToAVertex(0, FNumberOfSides, Angle, FRadioEsfera, ATipoDial, False);
 for i := 1 to FNumberOfSides + 1 do
   GoToAVertex(i, FNumberOfSides, Angle, FRadioEsfera, ATipoDial);

 FPath.ClosePath;
end;

En azul, os remarco, donde se produce el «movimiento» o ruta de dibujo.

El ejemplo de Embarcadero se limitaba a unir cada punto por lo que podían escribir tal que:

if IsLineTo then
 FPath.LineTo(NewLocation)
 else
 FPath.MoveTo(NewLocation);

Ahora, nosotros queremos ejecutar un movimiento adicional, para pintar un fragmento del radio, longitud establecida por FLongDial.

if IsLineTo then
 begin
  FPath.MoveTo(NewLocation);
  NewLocation.X:= NewLocation.X + (Sin(n * Angle) * FLongDial);
  NewLocation.Y:= NewLocation.Y - (Cos(n * Angle) * FLongDial);
  if n <= p then FPath.LineTo(NewLocation);
 end
 else
  NewLocation.X:= NewLocation.X + (Sin(n * Angle) * FLongDial);

Elemental querido Watson…

Ahora estamos en condiciones de entender la naturaleza de TPath y TPathData porque la clave es pensar que no hemos dibujado nada todavía.

¿Como es eso…?

¿Seguro…?

Lo que nuestro método hace es crear una ruta y almacenarla en la propiedad FPathData que contiene un array de registros del tipo TPathPoint. Y cada TPathPoint, viene caracterizado por un punto espacial (en este caso en el plano de las 2 dimensiones) y un tipo TPathPointKind, que identifica si es movimiento, linea, curva o punto de cierre. No necesita mas. Cada orden que ejecutamos se transforma en un nuevo TPathPoint de la matriz y la lectura de la matriz, permite generar una cadena textual que identifica secuencialmente la lista de operaciones que contiene. Por lo tanto existe una correspondencia entre la codificación de la cadena y las operaciones. Y esto os va a llevar al estándar W3C para la definición de Gráficos Vectoriales. Existe un lenguaje, que podemos interpretar como Comandos, que identifican distintas letras ‘M’, ‘L’, ‘H’, etc… para relacionar las operaciones y se estructura una sintaxis que incluye la letra y el punto asociado. Podéis verlo con detalle en el mismo código fuente de Delphi, en la unidad FMX.Graphics, método SetPathString de TPathData. La cadena textual, es una representación de esa matriz y la clase, proporciona datos para su gestión y almacenamiento. La naturaleza del tratamiento gráfico vectorial, es una de las ventajas de Firemonkey respecto a la VCL.

Conocido esto, nos basta un método que saque partido de esa ruta para pintarla, cada vez que es necesario actualizar el estado del componente, fruto de un cambio en el contexto espacial (sus coordenadas), bien por un cambio de la hora actual (contexto temporal).

procedure TMiReloj.PintarDial(const ATipoDial: TTipo; APintar: Boolean);
begin
 if not APintar then Exit;

 case ATipoDial of
  maSegundos: begin
               CreateDial(maSegundos);
               Canvas.Stroke.Thickness:= 1;
               Canvas.FillPath(FPath, AbsoluteOpacity);
               Canvas.DrawPath(FPath, AbsoluteOpacity);
              end;
  maMinutos :;
  maHoras : begin
               CreateDial(maHoras);
               Canvas.Stroke.Thickness:= 4;
               Canvas.Stroke.Color:= claGrey;
               Canvas.FillPath(FPath, AbsoluteOpacity);
               Canvas.DrawPath(FPath, AbsoluteOpacity);
             end;
 end;
end;

Por lo tanto, el método Paint, hasta el momento quedaría reducido a:

procedure TMiReloj.Paint;
begin
 inherited;
 PintarDial(maSegundos, paDialSegundos in FAdornos);
 PintarDial(maHoras, paDialHoras in FAdornos);
end;

Sencillo, ¿no os parece?. Fácil de leer y entender.

Para darle al usuario del componente, la posibilidad de hacerlo visible o no, según la necesidad, introducimos una propiedad de tipo conjunto, declarada en la parte pública previa. Los conjuntos nos ayudan a que el código sea mas legible. Os invito a recodar aquella entrada del blog donde abordábamos el tema: El mundo en un conjunto (2009) . Hay algunas entradas que por la naturaleza de su contenido son propias de una versión o versiones del entorno. En este caso, que hablamos del lenguaje, tienen una carácter mas atemporal.

type
...
TParteAdorno = (paExtra,
 paLogo,
 paDialSegundos,
 paDialMinutos,
 paDialHoras,
 paEsfera,
 paBoton,
 paAutor,
 paMarca);

 TAdornos = Set of TParteAdorno;
...

y la declaración de la propiedad publicada, para que persista al tiempo de diseño:

property Adornos: TAdornos read FAdornos write SetAdornos default [paDialSegundos, paDialMinutos, paDialHoras] ;

Salvo las manecillas, que no las hemos incluido porque suponemos que siempre van a ser visibles, el resto de adornos sí que se han representado. Aquí se incluyen 3 diales pero solo son implementados 2.

¡Bye, bye parte tercera!, ¡hello parte cuarta!

¡Qué bien que hemos avanzado un buen trecho!. La parte cuarta, la próxima entrada, rematará los últimos retoques del bloque del reloj analógico y nos prepararía para enfrentar el reloj de fichar.

Tendremos algunas distracciones en esa cuarta entrada: conversaremos sobre un tema de interés «galgos o podencos», desde la perspectiva de la herencia o de la composición mediante interfaces, que es un debate que os podéis plantear dentro del contexto de la evolución de vuestra aplicación. Y añadiremos algunos detalles al reloj, como el logo central y algun texto interior, como el nombre del autor o el del blog. Finalmente, en esa cuarta parte, compartiremos unas lineas de código que prueben que nuestro reloj funciona. En la entrada introductoria, se presentaba ya algún video mostrando estos detalles. Y esa parte cuarta cerrará el primer bloque de la serie.

Lo que tenemos al momento actual.

Así queda nuestro componente. En la imagen inmediatamente inferior, podéis ver el aspecto desde el formulario, en tiempo de diseño y con el temporizador desactivado.

vista_reloj

Y a continuación os incluyo como queda el código en nuestra unidad MiReloj:

unit MiReloj;

interface

uses
  System.SysUtils, System.Classes, FMX.Types, FMX.Controls, FMX.Objects,
FMX.Graphics;

type
  TTipo = (maSegundos, maMinutos, maHoras);

  TParteAdorno = (paExtra,
                  paLogo,
                  paDialSegundos,
                  paDialMinutos,
                  paDialHoras,
                  paEsfera,
                  paBoton,
                  paAutor,
                  paMarca);

  TAdornos = Set of TParteAdorno;

  TManecilla = class(TLine)
  private
    FTipoManecilla: TTipo;
    FPorcentajeSobreRadio: Single;
    procedure SetPorcentajeSobreRadio(const Value: Single);
  protected
   procedure ParentChanged; override;
  public
   constructor CreateManecilla(AOwner: TComponent; ATipoManecilla: TTipo); virtual;
   procedure SetAnguloRotacion(const AHoraActual: TDateTime);
   procedure AjustarPosicion(ANewWidth, ANewHeight: Single);
  published
   property Tipo: TTipo read FTipoManecilla;
   property PorcentajeSobreRadio: Single read FPorcentajeSobreRadio write SetPorcentajeSobreRadio;
  end;

  TMiReloj = class(TCircle)
  private
    { Private declarations }
    FDateTime: TDateTime;
    FRadioEsfera: Single;
    FInternalTimer: TTimer;
    FEnableTimer: Boolean;
    FOnInternalTimer: TNotifyEvent;
    FPath: TPathData;
    FAdornos: TAdornos;
    FMargenEsfera: SmallInt;
    function CreaManecilla(const ATipoManecilla: TTipo;const ANombre: String; APorcentajeRadio: Single): TManecilla;
    function CreaTemporizador(FuncTemporiza: TNotifyEvent): TTimer;
    procedure SetEnableTimer(const Value: Boolean);
    procedure TimerOnInternalTimer(Sender: TObject);
    procedure SetAdornos(const Value: TAdornos);
    procedure SetMargenEsfera(const Value: SmallInt);
  strict protected
    FMHoras: TManecilla;
    FMMinutos: TManecilla;
    FMSegundos: TManecilla;
  protected
    { Protected declarations }
    procedure CreateDial(const ATipoDial: TTipo); virtual;
    procedure Paint; override;
    procedure PintarDial(const ATipoDial: TTipo; APintar: Boolean); virtual;
  public
    { Public declarations }
    constructor Create(AOwner: TComponent); override;
    destructor Destroy; override;
    procedure SetBounds(X,Y, AWidth, AHeight: Single); override;
    procedure SetNewTime(const ANow: TDateTime);
  published
    { Published declarations }
    property Fill;
    property Stroke;
    property OnKeyDown;
    property Adornos: TAdornos read FAdornos write SetAdornos default [paDialSegundos, paDialMinutos, paDialHoras] ;
    property EnableTimer: Boolean read FEnableTimer write SetEnableTimer default False;
    property MargenEsfera: SmallInt read FMargenEsfera write SetMargenEsfera default 20;
    property OnInternalTimer: TNotifyEvent read FOnInternalTimer write FOnInternalTimer;
  end;

procedure Register;

implementation

uses System.UITypes, System.UIConsts, System.Types,
System.Math, DateUtils;

procedure Register;
begin
  RegisterComponents('Samples', [TMiReloj]);
end;

function TMiReloj.CreaManecilla(const ATipoManecilla: TTipo;const ANombre: String; APorcentajeRadio: Single): TManecilla;
begin
  Result:= TManecilla.CreateManecilla(self, ATipoManecilla);
  with Result do
  begin
    Parent:= Self;
    Name:= ANombre;
    PorcentajeSobreRadio:= APorcentajeRadio;
    Stored:= False;
  end;
end;

constructor TMiReloj.Create(AOwner: TComponent);
begin
  inherited;

  FMargenEsfera:= 20;

  Width:= 400;
  Height:= 400;

  FMHoras:= CreaManecilla(maHoras, 'manecilla_horas', 40.0);
  FMMinutos:= CreaManecilla(maMinutos, 'manecilla_minutos', 55.0);
  FMSegundos:= CreaManecilla(maSegundos, 'manecilla_segundos', 70.0);

  FAdornos:= [paDialSegundos, paDialMinutos, paDialHoras];

  FPath := TPathData.Create;

  FEnableTimer:= False;
  FInternalTimer:= CreaTemporizador(TimerOnInternalTimer);
end;

procedure TMiReloj.CreateDial(const ATipoDial: TTipo);

  procedure GoToAVertex(n: Integer; p: Integer; Angle, CircumRadius: Double;
    const ATipoDial: TTipo; IsLineTo: Boolean = True);
  var
    NewLocation: TPointF;
    FLongDial: Integer;
  begin            //punto centro
    NewLocation.X := (Width  / 2) + (Sin(n * Angle) * CircumRadius);
    NewLocation.Y := (Height / 2) - (Cos(n * Angle) * CircumRadius);

    case ATipoDial of
      TTipo.maSegundos: FLongDial:= 5;
      TTipo.maMinutos: FLongDial:= 7;
      TTipo.maHoras: FLongDial:= 10;
    else
      FLongDial:= 0;
    end;

    if IsLineTo then
    begin
      FPath.MoveTo(NewLocation);
      NewLocation.X:= NewLocation.X +  (Sin(n * Angle) * FLongDial);
      NewLocation.Y:= NewLocation.Y -  (Cos(n * Angle) * FLongDial);
      if n <= p then FPath.LineTo(NewLocation);
    end
    else
      NewLocation.X:= NewLocation.X +  (Sin(n * Angle) * FLongDial);
  end;

var
  i: Integer;
  Angle: Double;
  FNumberOfSides: Integer;
begin
  case ATipoDial of
    TTipo.maSegundos: FNumberOfSides:= 60;
    TTipo.maMinutos: FNumberOfSides:= 60;
    TTipo.maHoras: FNumberOfSides:= 12;
  else
    FNumberOfSides:= 1;
  end;
  Angle:=  2 * PI / FNumberOfSides;

  FPath.Clear;

  GoToAVertex(0, FNumberOfSides, Angle, FRadioEsfera, ATipoDial, False);
  for i := 1 to FNumberOfSides + 1 do
    GoToAVertex(i, FNumberOfSides, Angle, FRadioEsfera, ATipoDial);

  FPath.ClosePath;
end;

function TMiReloj.CreaTemporizador(FuncTemporiza: TNotifyEvent): TTimer;
begin
  Result:= TTimer.Create(nil);
  with Result do
  begin
    Enabled:= False;
    OnTimer:= FuncTemporiza;
  end;
end;

destructor TMiReloj.Destroy;
begin
  FInternalTimer.Free;
  FPath.Free;
  inherited;
end;

procedure TMiReloj.SetNewTime(const ANow: TDateTime);
begin
  //actualizamos la hora
  FDateTime:= ANow;
  //comunicamos a cada manecilla para que corrijan al angulo adecuado
  FMSegundos.SetAnguloRotacion(FDateTime);
  FMMinutos.SetAnguloRotacion(FDateTime);
  FMHoras.SetAnguloRotacion(FDateTime);

  Repaint;
end;

procedure TMiReloj.TimerOnInternalTimer(Sender: TObject);
begin
  SetNewTime(Now);
  if Assigned(FOnInternalTimer) then FOnInternalTimer(Self);
end;

procedure TMiReloj.SetAdornos(const Value: TAdornos);
begin
  if FAdornos <> Value then
  begin
    FAdornos := Value;
    Repaint;
  end;
end;

procedure TMiReloj.SetBounds(X,Y, AWidth, AHeight: Single);
begin
  inherited SetBounds(X,Y,AWidth,AHeight);

  FRadioEsfera:= Min(ShapeRect.Width / 2, ShapeRect.Height / 2)- MargenEsfera;

  with FMSegundos do
  begin
    AjustarPosicion(AWidth, AHeight);
    Width:= FRadioEsfera * PorcentajeSobreRadio / 100;
    Height:= FRadioEsfera * PorcentajeSobreRadio / 100;
  end;

  with FMMinutos do
  begin
    AjustarPosicion(AWidth, AHeight);
    Width:= FRadioEsfera * PorcentajeSobreRadio / 100;
    Height:= FRadioEsfera * PorcentajeSobreRadio / 100;
  end;

  with FMHoras do
  begin
    AjustarPosicion(AWidth, AHeight);
    Width:= FRadioEsfera * PorcentajeSobreRadio / 100;
    Height:= FRadioEsfera * PorcentajeSobreRadio / 100;
  end;
end;

procedure TMiReloj.SetEnableTimer(const Value: Boolean);
begin
  if FEnableTimer <> Value then
  begin
    FEnableTimer := Value;
    FInternalTimer.Enabled:= FEnableTimer;
    Repaint;
  end;
end;

procedure TMiReloj.SetMargenEsfera(const Value: SmallInt);
begin
  if FMargenEsfera <> Value then
  begin
    FMargenEsfera := Value;
    Repaint;
  end;
end;

procedure TMiReloj.Paint;
begin
  inherited;
  PintarDial(maSegundos, paDialSegundos in FAdornos);
  PintarDial(maHoras, paDialHoras in FAdornos);
end;

procedure TMiReloj.PintarDial(const ATipoDial: TTipo; APintar: Boolean);
begin
  if not APintar then Exit;

  case ATipoDial of
    maSegundos: begin
                  CreateDial(maSegundos);
                  Canvas.Stroke.Thickness:= 1;
                  Canvas.FillPath(FPath, AbsoluteOpacity);
                  Canvas.DrawPath(FPath, AbsoluteOpacity);
                end;
    maMinutos :;
    maHoras   : begin
                  CreateDial(maHoras);
                  Canvas.Stroke.Thickness:= 4;
                  Canvas.Stroke.Color:= claGrey;
                  Canvas.FillPath(FPath, AbsoluteOpacity);
                  Canvas.DrawPath(FPath, AbsoluteOpacity);
                end;
  end;
end;

{ TManecilla }

constructor TManecilla.CreateManecilla(AOwner: TComponent; ATipoManecilla: TTipo);
begin
  inherited Create(AOwner);
  FTipoManecilla:= ATipoManecilla;
  with RotationCenter do
  begin
    X:= 0;
    Y:= 0;
  end;
  Stroke.Kind:= TBrushKind.Gradient;
    case FTipoManecilla of
      maSegundos: begin
                    Stroke.Color:= claGreen;
                    Stroke.Thickness:= 2;
                  end;
      maMinutos : begin
                    Stroke.Color:= claBlue;
                    Stroke.Thickness:= 5;
                  end;
      maHoras   : begin
                    Stroke.Color:= claBlue;
                    Stroke.Thickness:= 5;
                  end;
    end;
end;

procedure TManecilla.ParentChanged;
begin
  inherited;
  if (Parent <> nil) and (Parent is TMiReloj) then
  begin
    Position.x := (Parent as TMiReloj).ShapeRect.Width/2;
    Position.y:= (Parent as TMiReloj).ShapeRect.Height/2;
  end;
end;

procedure TManecilla.AjustarPosicion(ANewWidth, ANewHeight: Single);
begin
  Position.x := ANewWidth/2;
  Position.y:= ANewHeight/2;
end;

procedure TManecilla.SetAnguloRotacion(const AHoraActual: TDateTime);
var
  FRotacionSegundos: Real;
  FRotacionMinutos: Real;
  FRotacionHoras: Real;
  FTemp: Word;
begin
  case FTipoManecilla of
    maSegundos: begin
                  FTemp:= SecondOfTheMinute(AHoraActual);
                  FRotacionSegundos:= 225 + (FTemp) * 6;
                  RotationAngle:= FRotacionSegundos;
                end;
    maMinutos : begin
                  FTemp:= MinuteOfTheHour(AHoraActual);
                  FRotacionMinutos:= 225 +  (FTemp) * 6;
                  RotationAngle:= FRotacionMinutos;
                end;
    maHoras   : begin
                  FTemp:= HourOfTheDay(AHoraActual);
                  FRotacionHoras:= 225 + ((FTemp) * 30);
                  FTemp:= MinuteOfTheHour(AHoraActual);
                  FRotacionHoras:= FRotacionHoras + (FTemp*0.5);
                  RotationAngle:= FRotacionHoras;
                end;
  end;
end;

procedure TManecilla.SetPorcentajeSobreRadio(const Value: Single);
begin
  FPorcentajeSobreRadio := Value;
end;

initialization
 ReportMemoryLeaksOnShutdown:= True;



end.

Sed felices y hasta la próxima semana.

Documentación bibliografica:

Para la preparación de esta entrada concreta, me he apoyado en la lectura de varias fuentes que os pueden ayudar a ampliar ideas, pues tratan con mayor detalle algunos puntos:

  • Guia de Componentes Firemonkey
  • DockWiki de Embarcadero
  • Delphi Cook Book por Daniele Teti (Cap. 4)  (ISBN: 978-1-78528-742-8)
  • La Guia de Delphi por Francisco Charte (Cap. 11)  (ISBN: 978-84-939810-1-2)
  • Desarrollo de aplicaciones iOS/Android con Delphi (Cap. 9 y 10)  (ISBN: 978-84-939910-7-4)

Otra documentación bibliografica que os puede ayudar:

  • Object Pascal Handbook por Marco Cantú (ISBN-13: 978-1514349946) disponible desde la web de Embarcadero.

4 respuestas a “Módulo C.P.2017 (III): Reloj Analógico.

Add yours

    1. Hola:
      Visita estos dos enlaces:
      http://delphiaccess.com/foros/ (Comunidad DelphiAccess)
      https://www.clubdelphi.com (Club Delphi)
      Son dos de nuestros mejores sitios a nivel de comunidad Hispana, para resolver los problemas que te surgirán a medida que avances en Delphi. Cualquiera de los dos son excelentes apoyos cuando te inicias, ya que los componen grandes profesionales y llevan muchos años apoyando a la Comunidad.
      Puedes también visitar nuestro grupo de facebook, delphi solidario https://www.facebook.com/groups/delphisolidario/

      Un saludo
      Salvador

      Me gusta

Deja un comentario

Blog de WordPress.com.

Subir ↑

Marina Casado

Escritora y Doctora en Literatura Española. Periodista cultural. Madrid, España

Sigo aqui

Mi rincon del cuadrilatero, ahi donde al creer que me he rendido, aun sigo peleando.

Recetas y consejos nutricionales

Indicadas para personas con diabetes, recomendadas para todos.

¡Buen camino!

ANÉCDOTAS Y REFLEXIONES SOBRE UN VIAJE A SANTIAGO…

https://lfgonzalez.visiblogs.com/

Algunas reflexiones y comentarios sobre Delphi

It's All About Code!

A blog about Delphi, C++ Builder and related technologies...

The Podcast at Delphi.org

The Podcast about the Delphi programming language, tools, news and community.

Blog de Carlos G

Algunas reflexiones y comentarios sobre Delphi

The Road to Delphi

Delphi - Free Pascal - Oxygene

La web de Seoane

Algunas reflexiones y comentarios sobre Delphi

El blog de cadetill

Cosas de programación....... y de la vida

Delphi-losophy

A Lover of Delphic Wisdom

Delphi en Movimiento

Algunas reflexiones y comentarios sobre Delphi

marcocantu.blog

Algunas reflexiones y comentarios sobre Delphi

Press F9

Algunas reflexiones y comentarios sobre Delphi

El blog de jachguate

Un blog sobre tecnología y la vida en general