Una anécdota sobre TScrollBox y última promoción de Rad Studio XE5

Cuando empecé a escribir esta entrada, hace ya varios días, mi pensamiento era básicamente resaltar la última promoción de Embarcadero, que a priori y solo por el valor añadido que aporta, creo que se puede valorar como muy interesante. De hecho, si fuera el caso que estuvierais en estos momentos valorando la adquisición del producto, supone la oferta de fin de año un bonus importante en descuentos y herramientas adicionales que siempre se agradecen en los tiempos actuales de crisis. Por otro lado, el partner de Embarcadero para vuestra área, casi con seguridad va a ofreceros  valor   añadido que se va a poder agregar a dicha compra (sé por lo leído en correos que por ejemplo Danysoft tiene preparada también ofertas que cierran el año y las señalaremos al final de esta entrada).

Decía que estaba metido de lleno en el tema de la preparación de la entrada, familiarmente lo referimos como metidos en harina :-D. Había añadido tanto la imagen que luce en la oferta de Embarcadero como el contador de tiempo atrás. El contador, bajo la imagen, debería indicar el tiempo que falta hasta el 31 de Diciembre de 2013, fecha en la que finaliza la oferta de Embarcadero. También tenía preparado parte del texto para buscar la traducción mas adecuada o cercana, y no recién había añadido esto último, la recepción de un correo de un compañero cambiaba o daba un giro a las expectativas de la propia entrada…

¡ULTIMA OFERTA DE EMBARCADERO!

ofertaxe5dic2013

[ujicountdown id=»SpecialOffer» expire=»2013/12/31 23:59″ hide = «true» url = «http://www.embarcadero.com/radoffer»]

Así que la oferta de Embarcadero tenía que esperar. Dejé el texto del detalle en la parte inferior de la entrada y abrí un hueco para incluir la anécdota de mi amigo.

 El punto de partida de Seguismundo…

El correo de este compañero y amigo, al que podemos llamar Seguismundo, (por aquello de que tenga un nombre), formaba parte de otros muchos correos que desde tiempo atrás he ido recibiendo, quizás porque la comunidad es cada vez mas grande y las relaciones entre los que la componen se van estrechando. No es rara la semana que no se reciba algún correo fruto de las entradas del blog o de la actividad del grupo de Facebook.

En este caso, Seguismundo andaba con la idea de crear un componente contenedor muy sencillo, que le permitiera definir filas y columnas sin ceñirse a los tradicionales que todos conocemos. Buscaba algo mas casero para experimentar.

Me planteaba así su problema de esta forma:

 […] 
Quiero hacer un componente en FMX que se asemeje a una grilla, es decir un título, un encabezado de columnas y las respectivas filas de datos para cada columna, todo iba muy bien hasta que llegué al punto de poner las filas (es decir una serie de columnas dentro de un TLayout) que llegan a un componente TVertScrollBox (o ScrollBox pasa lo mismo) y allí, no encuentro la manera de que las columnas se pongan en el orden correcto. 
[…]

y proseguía pidiéndome que compilara el código con XE5 por aquello de que pudiera ser un bug ya solucionado. Adjunto a su correo, Seguismundo incluía el conjunto de ficheros de una aplicación de pruebas, para poder mostrarme el problema, lo cual me alegró sobremanera ya que resulta en ocasiones complicado tras la lectura de un correo averiguar que problema realmente se padece.   😯

Me puse pues a analizar qué había recibido y observé con detenimiento las unidades que Seguismundo me había enviado, concretamente la unidad UPruebaGridFM.pas, que era la que contenía los objetos de la interfaz y la unidad UClassGrillaTest, que era donde había declarado una clase que representaba cada fila del contenedor, responsable de crear 5 etiquetas a piñón.

Lo siguiente fue abrir el entorno de XE5 y compilar el paquete recibido. No se había generado ningún error… ¡vamos bien! pensaba para mi. Y ¡voila! allí se estaba ejecutando el ejemplo de Seguismundo.

El interfaz no podia dar lugar a errores de interpretación. Tres botones que al pulsarlos, rellenaban sucesivamente filas de etiquetas agrupadas en un contenedor que representaba conceptualmente una fila, dentro una instancia de TLayout, otra de TRectangle y finalmente en una instancia de TScrollBox respectivamente.

 

PuntoPartidaTest

Respecto al problema que él me comentaba no veia nada raro… A ojo de buen cubero no acababa de ver cual era realmente el problema (al menos respecto a la ordenación de las etiquetas creadas), por lo que decidí enviar de vuelta una respuesta rápida a Seguismundo, para comentarle que no sabía exactamente a que se refería. Evidentemente sí habían otros puntos que no me parecían correctos formalmente y en ese mismo correo ya lo compartía con él.

Código de la Unidad UPruebaGridFMX de Seguismundo

unit UPruebaGridFMX;

interface

uses
  System.SysUtils, System.Types, System.UITypes, System.Rtti, System.Classes,
  System.Variants, FMX.Types, FMX.Controls, FMX.Forms, FMX.Dialogs,
  FMX.StdCtrls, FMX.Layouts, UClassGrillaTest, FMX.Objects;

type
  TForm10 = class(TForm)
    ScrollBox1: TScrollBox;
    btnTestLayout: TButton;
    btnTestRectangle: TButton;
    Layout1: TLayout;
    Rectangle1: TRectangle;
    btnTestScrollBox: TButton;
    procedure btnTestLayoutClick(Sender: TObject);
    procedure btnTestRectangleClick(Sender: TObject);
    procedure btnTestScrollBoxClick(Sender: TObject);
  private
    { Private declarations }
    Grilla: TComp1;

    procedure NuevoObjeto;
  public
    { Public declarations }
  end;

var
  Form10: TForm10;

implementation

{$R *.fmx}

procedure TForm10.btnTestLayoutClick(Sender: TObject);
begin
  Grilla := TComp1.Create(nil);
  Grilla.FLayout1.Parent := Layout1;
  NuevoObjeto;
end;

procedure TForm10.btnTestRectangleClick(Sender: TObject);
begin
  Grilla := TComp1.Create(nil);
  Grilla.FLayout1.Parent := Rectangle1;
  NuevoObjeto;
end;

procedure TForm10.btnTestScrollBoxClick(Sender: TObject);
begin
  Grilla := TComp1.Create(nil);
  Grilla.FLayout1.Parent := ScrollBox1;
  NuevoObjeto;
end;

procedure TForm10.NuevoObjeto;
begin
  with Grilla do
  begin
    FLayout1.Align := TAlignLayout.alBottom;

    FLabel1.Align := TAlignLayout.alRight;
    FLabel1.AutoSize := False;
    FLabel1.Text := IntToStr(X) + '1';
    FLabel1.Width := 50;
    FLabel1.Align := TAlignLayout.alMostLeft;

    FLabel2.Align := TAlignLayout.alRight;
    FLabel2.AutoSize := False;
    FLabel2.Text := IntToStr(X) + '2';
    FLabel2.Width := 50;
    FLabel2.Align := TAlignLayout.alMostLeft;

    FLabel3.Align := TAlignLayout.alRight;
    FLabel3.AutoSize := False;
    FLabel3.Text := IntToStr(X) + '3';
    FLabel3.Width := 50;
    FLabel3.Align := TAlignLayout.alMostLeft;

    FLabel4.Align := TAlignLayout.alRight;
    FLabel4.AutoSize := False;
    FLabel4.Text := IntToStr(X) + '4';
    FLabel4.Width := 50;
    FLabel4.Align := TAlignLayout.alMostLeft;

    FLabel5.Align := TAlignLayout.alRight;
    FLabel5.AutoSize := False;
    FLabel5.Text := IntToStr(X) + '5';
    FLabel5.Width := 50;
    FLabel5.Align := TAlignLayout.alMostLeft;

    FLayout1.Align := TAlignLayout.alMostTop;
  end;
end;

end.

 

 

Código de la Unidad UClassGrillaTest de Seguismundo

unit UClassGrillaTest;

interface

uses System.Classes, FMX.Layouts, FMX.StdCtrls, FMX.Types, FMX.Objects;

type
  TComp1 = class
  public
    FLayout1: TLayout;
    FLabel1: TLabel;
    FLabel2: TLabel;
    FLabel3: TLabel;
    FLabel4: TLabel;
    FLabel5: TLabel;

    class var X: integer;

    constructor Create(AReceptor: TFmxObject);
  end;

implementation

{ TComp1 }

constructor TComp1.Create(AReceptor: TFmxObject);
begin
  FLayout1 := TLayout.Create(nil);
  FLayout1.Parent := AReceptor;

  FLabel1 := TLabel.Create(nil);
  FLabel1.Parent := FLayout1;

  FLabel2 := TLabel.Create(nil);
  FLabel2.Parent := FLayout1;

  FLabel3 := TLabel.Create(nil);
  FLabel3.Parent := FLayout1;

  FLabel4 := TLabel.Create(nil);
  FLabel4.Parent := FLayout1;

  FLabel5 := TLabel.Create(nil);
  FLabel5.Parent := FLayout1;

  Inc(X);
end;

end.

 

Su correo no se hizo esperar y de vuelta su respuesta me indicaba el problema…

Hola Salvador, gracias.  El problema se ve en la primer fila del ScrollBox, que para el ejemplo que capturaste es: 31 35 34 33 32, como puedes ver las demás si se ordenan correctamente p.ej. 81 82 83 84 85.
Lo anterior significa que aún en XE5 sucede igual, lo que no sé es si eso se debe a alguna parametrización en el ScrollBox o a un manejo en especial …?
Seguismundo
 

 En fin… Volví a observar con detenimiento la captura recibida y asentía dandole la razón. En la imagen inferior se puede ver que en la instancia del TScrollBox, la primera fila de etiquetas se ordenan de una forma extraña y únicamente sucede en dicha fila. En los otros componentes similares el código funcionaba de una forma similar a como el autor, Seguismundo, esperaba: {31,35,34,33,32.. 81,82,83,84,85…}. Ok -pensé para mi-

 

Detalle

 Por qué las cosas pasan porque pasan…

Sobre este supuesto le envié un correo de cortesía a Seguismundo, para comentarle que iba un poco liado y que tan pronto como me fuera posible le comentaba algo. Y me puse a ver ese código con mas detenimiento, cosa que me ocupo un par de días mas.

Las cosas siempre pasan por alguna razón. En nuestra profesión no hay magia aunque a veces los usuarios con los que convivimos nos concedan poderes mágicos. 😀  No es raro que cuando me llaman tanto Yolanda como Trini, de Administración, porque pongamos por caso no les funcione la impresora, la simple presencia de un servidor haga que el chisme imprima correctamente, y que estas santas mujeres acaben pensando que soy capaz de alterar mágicamente un solo byte. También mis amigos en ocasiones descubren que puedo apagando y encendiendo un router solucionar un problema de la adsl. Nos llegamos a convertir en una especie de talismán.

😆

Pero en nuestro mundo, el de las personas que resolvemos problemas a través de estos lenguajes mágicos que hacen cosas triviales, descubrimos por experiencia que todo pasa por una razón también, aunque en ese momento no seamos capaces de verla.

El código de Seguismundo tenía algunos problemillas fuera de lo que era el propio error detectado, visible a simple vista.

Si os parece podemos comentarlas.

Por ejemplo. Seguismundo declaraba la clase TComp como descendiente de TObject, en su tipo

type
  TComp1 = class
  public
...

por lo que las instancias creadas no entraban en el flujo de destrucción automática relativas al propietario (el owner) y en un ambiente de escritorio deberían ser expresamente liberadas. El parámetro del constructor lo utilizaba para asignar una referencia válida al parent de los componentes que internamente manejaba TComp.

constructor TComp1.Create(AReceptor: TFmxObject);
begin
  FLayout1 := TLayout.Create(nil);
  FLayout1.Parent := AReceptor;
...

y quedaban finalmente, tanto los TLabel como los TLayout, emparentados pero huérfanos de padre, lo cual es una situación un tanto extraña. Es mas, podían llegar a existir potenciales problemas de memoria, instancias que fueron creadas y que quedaron en el limbo de aquellas referencias que no podrían ser alcanzadas jamas… Esa situación sucedía cuando repetías el click del botón sucesivamente, ya que FLabel1 y resto de presuntos implicados, siempre iban a apuntar a la ultima instancia creada, dejando en el olvido las anteriores.

Por otro lado, cada instancia de TComp, sufría un problema similar.

Grilla := TComp1.Create(nil);

Cierto que era código para andar por casa, y así lo entendía yo al recibirlo, pero en mis correos de vuelta, todos estos comentarios estaban entre las cosas que compartimos.

De hecho, aun suponiendo que estuviéramos en el ámbito de las aplicaciones móviles, en donde el sistema de ARC, automatiza la destrucción de las instancias en función de un contador interno, el código tampoco hubiera funcionado correctamente y establecía efectos indeseados que a simple vista no eran tan aparentes.

Por ejemplo, supongamos que llevamos el código a las plataformas móviles:

La ejecución del código que asignaba la referencia a la fila del contenedor

  Grilla := TComp1.Create(nil);
  Grilla.FLayout1.Parent := ScrollBox1;

provocaría en ese caso, que al salir de ámbito, a partir de la segunda vez que el botón fuera pulsado, fuera destruida la instancia de TComp creada en la pulsación anterior, puesto que el contador de referencias se decrementa y al llegar a cero, la instancia pasa a mejor vida. El resultado final, no aparente, habría dejado todas las etiquetas esparcidas en el contenedor desligadas de su contenedor fila (TComp en el ejemplo) salvo las de la ultima instancia creada, aspecto este que no era el inicialmente planeado por el autor del código supongo. Eso se puede ver fácilmente estableciendo un punto de parada en una linea de su destructor, si fuera añadido.

Si alguno de vosotros tiene duda os aconsejo dar un vistazo a algunos enlaces de Marco Cantú

http://blog.marcocantu.com/blog/delphi_arc_android.html y

http://edn.embarcadero.com/article/43073,  principalmente este último.

Pero si fuéramos a analizar con mas detalle la ejecución del código que me envió Seguismundo, en el caso concreto de que pulsáramos sobre el botón asociado al contenedor de ScrollBox repetidamente, observaríamos dos efectos anómalos:

1- El scrollbox nunca nos mostraria la barra de scroll horizontal (aun en el caso de existir condiciones de que fueran visibles)

2- Cuando la cantidad de filas desbordaran la altura de nuestro componente contenedor, descubriríamos que no visualizamos la ultima de ellas, que siempre sería inaccesible.

Todo eso, sin contar el problema inicial detectado, que provocaba que se ordenara de forma incorrecta la primera file de las etiquetas creadas.

Una estrategia equivocada…

Quizás fuera esta la razón, con mas peso, que finalmente motivó el incluir esta anécdota dentro de la entrada. Su razonamiento respecto al uso de la propiedad Align para reubicar los componentes dentro del contenedor de la clase TScrollBox, no funcionaba correctamente pero tenía una cierta lógica que en mi opinión le podía llevar a engaño. Y quizás por eso el compartir estas lineas para que Seguismundo pudiera ver ese matiz

Su razonamiento era mas o menos este:

Quiero colocar cada fila que vaya creando e insertando dentro del contenedor, una pegada a la otra, y que la ultima siempre se acople al final de las que ya existan. 

De igual forma razonaría según esa idea respecto a cualquier alineamiento, fuera hacia arriba, hacia abajo o hacia cualquiera de los laterales.

Puestos en esa tesitura, la propiedad Align es una tentación porque llegas a conseguir eso mismo de una forma sencilla… (la cual no siempre es correcta).

Intentaré demostrarlo.

Supongamos un formulario sencillo que contiene una instancia de TScrollBox de 400×400. Supongamos también que vamos a instanciar de forma repetida unos contenedores vacíos que representarían las sucesivas filas (mas que nada por simplificar) que tendrán unas medidas de 50×600, los cuales queremos colocar uno pegado a otro, de arriba a abajo.

Bajo esos supuestos, la creación del primer contenedor, forzaría a que nuestro padre, la instancia de TScrollBox hiciera visible el scroll horizontal. De igual forma, la repetición sucesiva a partir del quinto layout, forzaría a que fuera visible el scroll vertical, puesto que a partir de la quinta fila el height o altura total de la suma de todas las filas creadas sería mayor que el espacio hábil del contenedor, lo cual debería generar igualmente el conjunto de procesos que permitirá visualizar y manipular las barras de scroll.  

 Si revisamos las unidades del código fuente donde se declara la clase TScrollBox en Firemonkey, FMX.Layouts, veremos que las llamadas que fuerzan a recalcular el contenido del contenedor padre, se centran en la función RealingContent( ), que siempre que es invocada hace dos cosas básicas:

  1. Inicializar los cálculos, invocando a  InvalidateContentSize( ), que hace un reset o puesta a cero de la estructura que representa el área total contenida.
  2. Lanzar el proceso de calculo que produce que se ubiquen correctamente los distintos componentes (en nuestro ejemplo filas) y puedan ser explorados por las barras de scroll. La función Realign( ) es la responsable de todo ello.

Esto sucede durante la vida de nuestra instancia de la clase TScrollBox cuando cambian las medidas de altura o anchura. En esos momentos el componente necesita saber si para el nuevo tamaño qué debe o no debe mostrar. Frente a lo que yo esperaba, mientras estudiaba con puntos de parada el flujo del código, no sucede así cuando cambian el Top o el Bottom del componente. En su lugar, va a considerar los cambios en la propiedad Position que los representan, con los valores X e Y. Por todo ello, y sabiendo que también la asignación del Parent por lógica, lanzará o invocará las rutinas que comentábamos, debería ser está asignación la que hiciéramos en ultimo lugar.

Para verlo, los puntos de parada nos permiten descubrir el verdadero responsable de que todo funcione correctamente. Realign( ) invocará a DoRealign( ) y ésta a InternalAlign( ) que centralizará de alguna forma todo el conjunto de cálculos.

Dentro de esta función encontraremos una invocación a DoCalcContentBounds( ) cuyo valor de retorno va a ser el área total del conjunto de hijos contenidos en el contenedor. Esta es la función:

function TScrollBox.DoCalcContentBounds: TRectF;
var
  i: Integer;
  R, LocalR: TRectF;
begin
  Result := TRectF.Create(0, 0, 0, 0);
  if Assigned(FContent) and Assigned(ContentLayout) then
  begin
    R := ContentLayout.LocalRect;
    for i := 0 to FContent.ControlsCount - 1 do
    if FContent.Controls[i].Visible then
    begin
      {$IFDEF MSWINDOWS}
      if (csDesigning in ComponentState)
        and Supports(FContent.Controls[i], IDesignerControl) then Continue;
      {$ENDIF}
      LocalR := FContent.Controls[i].ParentedRect;
      if (FContent.Controls[i].Align in [TAlignLayout.alTop, TAlignLayout.alMostTop, TAlignLayout.alBottom, TAlignLayout.alMostBottom]) or
         (TAnchorKind.akRight in FContent.Controls[i].Anchors) then
        LocalR.Right := R.Right;
      if (FContent.Controls[i].Align in [TAlignLayout.alLeft, TAlignLayout.alMostLeft, TAlignLayout.alRight, TAlignLayout.alMostRight]) or
         (TAnchorKind.akBottom in FContent.Controls[i].Anchors) then
        LocalR.Bottom := R.Bottom;

      Result.Union(LocalR);
    end;
  end;
end;

Observándola con un poco de detenimiento se puede deducir lo siguiente:

  • Solo va a ejecutarse si el contenedor ha sido asignado. El contenido del contenedor padre es representado por la referencia a ContentLayout. Por esa razón comentaba la conveniencia de asignar la referencia al Parent en ultimo lugar, ya que todos los cálculos van a realizarse tan solo en el caso de que exista esta asignación al Parent. Evitaremos bucles y procesos innecesarios si existen llamadas previas a campos que puedan lanzar el recalculo.
  • Se obtiene un area que representa la posición y tamaño de dicho contenedor padre, que sirve como referencia inicial. Recordemos que se había inicializado previamente.
  • Se recorrerán la matriz que contiene los controles hijos para analizar de que forma afectan al tamaño de ContentLayout siguiendo unas pautas:
    • Si el componente hijo presenta un Align alTop o alBotton, dado que esta acoplado al padre, considerara su valor Rigth (el punto derecho).
    • Si el componente hijo presenta un Align alLeft o alRight, dado que también está acoplado al padre pero por un lateral, considerará pertinente el valor del padre en la parte inferior.
    • Otro valor de Allign va a considerar el área y posición del hijo contenido.
  • De cada transición de ese bucle que recorre los hijos visibles del contenedor va acumulando en el resultado el calculo de la unión de áreas, comparando el área del hijo actual en el bucle con el área previa.

Y nos falta, para darle un vistazo a como se genera esa área resultante, que las tenéis en las tres funciones que siguen a continuación.

class function TRectF.Union(const R1, R2: TRectF): TRectF;
begin
  UnionRectF(Result, R1, R2);
end;

procedure TRectF.Union(const R: TRectF);
begin
  Self := TRectF.Union(Self, R);
end;

class function TRectF.Union(const Points: Array of TPointF): TRectF;
var
  I: Integer;
  TLCorner, BRCorner: TPointF;
begin
  if Length(Points) > 0 then
  begin
    TLCorner := Points[Low(Points)];
    BRCorner := Points[Low(Points)];

    if Length(Points) > 1 then
    begin 
      for I := Low(Points) + 1 to High(Points) do
      begin
        if Points[I].X < TLCorner.X then TLCorner.X := Points[I].X;         
if Points[I].X > BRCorner.X then BRCorner.X := Points[I].X;
        if Points[I].Y < TLCorner.Y then TLCorner.Y := Points[I].Y;         
if Points[I].Y > BRCorner.Y then BRCorner.Y := Points[I].Y;
      end;
    end;

    Result := TRectF.Create(TLCorner, BRCorner);
  end
  else begin
    Result := TRectF.Empty;
  end;
end;

En este punto, podríamos entender el porque de los problemas iniciales.

Si en la hipótesis que lanzamos lineas mas arriba, con un contenedor de 400×400, creáramos nuestro primer Layout hijo de 50×600, y lo acoplaramos con un alineamiento alTop, esa primera llamada funcionaria en cualquier caso correctamente. Basicamente ha sucedido:

  • Creamos el layout.
  • Asignamos el parent.
  • Forzamos Align alBottom para que se sitúe el ultimo.
  • Forzamos Align alTop para que se sitúe en la parte superior.

Cuando se asigna el valor del Parent, el Align del hijo creado es el valor por defecto, alNone, y para ese valor, considerara el área del hijo (50×600), la cual, unida al área del contenido del padre que fue inicialmente respetada dará una área de 50×600.

Para el segundo de los hijos, el área que representa ContentLayout es 50×600. La ejecución resultante de este segundo bloque tras la asignación del Parent sigue siendo 50×600, porque el primer hijo cambió su Align a alTop mientras que el segundo vuelve a tomar el valor por defecto, por lo que accidentalmente nos vuelve a calcular un área de 50×600, lo cual era «erróneo» pero no nos dimos cuenta porque no ha necesitado hacer visibles la barra de scroll vertical, dado que la suma del Height de los hijos no supera la altura del contenedor.

Solo cuando se produzca esta situación, cuando es creado el quinto hijo empezaremos a detectar el problema, puesto que siempre va a quedar oculta de nuestro calculo la ultima de las filas.

Finalmente, respecto a la barra de scroll horizontal, la propia lógica nos hace intuir que las respectivas asignaciones alTop de los hijos previos, harán que la referencia a ContentLayout considere el Width del contenedor padre de forma que aunque su tamaño sea superior, cualquier invocación a Realing( ) ocultaría la barra horizontal de scroll.

La función GetParentedRect, es la que nos descubre que solo considera los valores del campo Position, frente a nuestra suposición inicial de que puede ser relevante el Top o el Bottom (al menos en Firemonkey) para el caso que tenemos en mente.

function TControl.GetParentedRect: TRectF;
begin
  Result := RectF(0, 0, Width, Height);
  OffsetRect(Result, Position.X, Position.Y);
end;

¿Podemos ayudar a  Seguismundo?

Lo intenté, siguiendo los supuestos que el me comentaba.

😎

A vuelta de correo le enviaba unas lineas de código, que básicamente hacían lo que el me había comentado pero en su lugar, dejaba el valor de Align del componente hijo en el valor por defecto, alNone, de forma que los cálculos resultantes siempre considerasen el área y posición de los hijos. La altura y desplazamiento adecuado, quedaba calculado en función del valor de ContentLayout actual, como podréis ver en el código, que adjunto en la parte inferior.

El código intenta corregir algunas de las cosas que comentábamos. Ahora el componente que representa la fila no va a descender de TObject sino que va a ser el propio layout. La variable de clase que identificaba la fila ahora había sido sustituida cediendo dicho control a un descendiente del contenedor padre, creado interponiendo la clase. Y sucesivos detalles.

Este es el código que envié a Seguismundo, que contiene un modulo de interfaz y una unidad con la declaración de los componentes:

 

unit UMainSampleScrollBoxFMX;

interface

uses
  System.SysUtils, System.Types, System.UITypes, System.Rtti, System.Classes,
  System.Variants, FMX.Types, FMX.Controls, FMX.Forms, FMX.Dialogs,
  FMX.StdCtrls, FMX.Layouts, FMX.Objects, UClassGrillaTest;

type
  TfmTestScrollBox = class(TForm)
    btnTestScrollBox: TButton;
    ScrollBox1: TScrollBox;
    panTest: TPanel;
    lbAuthor: TLabel;
    procedure btnTestScrollBoxClick(Sender: TObject);
    procedure FormCreate(Sender: TObject);
  private
    { Private declarations }
  public
    { Public declarations }
  end;

var
  fmTestScrollBox: TfmTestScrollBox;

implementation

{$R *.fmx}

procedure TfmTestScrollBox.btnTestScrollBoxClick(Sender: TObject);
begin
  ScrollBox1.AddRowLabels(Random(10)+1, Random(20)+30, Random(40)+20);
end;

procedure TfmTestScrollBox.FormCreate(Sender: TObject);
begin
  Randomize;
end;

end.

 

Esta es una imagen del formulario de la interfaz.

formulariotest

 

Y finalmente, la unidad que contiene las clases que representan a las filas, que yo he llamado TRowLabels.

 

unit UClassSampleScrollBoxTest;

interface

uses System.Classes, FMX.Layouts, FMX.StdCtrls, FMX.Types, FMX.Objects;

type
  TRowLabels = class;

  TScrollBox = class(FMX.Layouts.TScrollBox)
  private
    FRowLabelsArray: array of TRowLabels;
  public
    function AddRowLabels( AColumnCount: Integer; AWidth, AHeight: Single): TRowLabels;
    constructor Create(AOwner: TComponent); override;
    destructor Destroy; override;
  end;

  TRowLabels = class(TLayout)
  protected
    function CreaEtiqueta(ARowIndex, AColumnIndex: Integer; AWidth, AHeight: Single): TLabel; virtual;
    procedure Paint; override;
  public
    constructor Create(AOwner: TComponent; AIndexRow, AColumnCount: Integer; AWidth, AHeight: Single);
  end;

implementation

{  TRowLabels }

uses System.SysUtils, FMX.Dialogs, System.Types;

function TRowLabels.CreaEtiqueta(ARowIndex, AColumnIndex: Integer; AWidth, AHeight: Single): TLabel;
begin
   Result:= TLabel.Create(Self);
   with Result do
   begin
     Parent:= Self;
     Name:= 'F'+ IntToStr(ARowIndex)+'C'+IntToStr(AColumnIndex);
     Width := AWidth;
     Height:= AHeight;
     Align := TAlignLayout.alRight;
     AutoSize := False;
     Text := Name;
     Align := TAlignLayout.alMostLeft;
   end;
end;

constructor TRowLabels.Create(AOwner: TComponent; AIndexRow, AColumnCount: Integer; AWidth, AHeight: Single);
var
  FColumnCount: Integer;
begin
  inherited Create(AOwner);
  Name:= 'lyRow'+IntToStr(AIndexRow);
  Width:= AColumnCount * AWidth;
  Height:= AHeight;
  Position.X:= TScrollBox(AOwner).ContentBounds.Left;
  Position.Y:= TScrollBox(AOwner).ContentBounds.Height;
  FColumnCount:= AColumnCount;
  while (FColumnCount > 0) do
  begin
     CreaEtiqueta(AIndexRow, AColumnCount-FColumnCount+1, AWidth, AHeight);    
     Dec(FColumnCount);
  end;
   Parent:= AOwner as TFmxObject;
end;

procedure TRowLabels.Paint;
var
  R: TRectF;
begin
  inherited;
  if not (csDesigning in ComponentState) then
  begin
    R := LocalRect;
    InflateRect(R, -0.5, -0.5);
    Canvas.DrawDashRect(R, 0, 0, AllCorners, AbsoluteOpacity, $A9999999);
  end;
end;

{ TMyScrollBox }

function TScrollBox.AddRowLabels( AColumnCount: Integer; AWidth, AHeight: Single): TRowLabels;
var
  i: Integer;
begin
  i:= Length(FRowLabelsArray);
  Inc(i);
  SetLength(FRowLabelsArray, i);
  FRowLabelsArray[i-1]:= TRowLabels.Create(Self, i, AColumnCount, AWidth, AHeight);
  Result:= FRowLabelsArray[i-1];
end;

constructor TScrollBox.Create(AOwner: TComponent);
begin
  inherited;
  SetLength(FRowLabelsArray, 0);
end;

destructor TScrollBox.Destroy;
begin
  SetLength(FRowLabelsArray, 0);
  inherited;
end;

end.

La ejecución mostraba que los cálculos eran ahora correctos. Esta es una captura de la misma. Se la envié a mi amigo Seguismundo junto el proyecto por si hubiera podido ayudarle. Para las pruebas, había asignado valores aleatorios tanto a la cantidad de columnas a crear como al ancho o altura de cada una de ellas, representada en esa etiqueta que muestra la posición FilaXColumna en el text (F1C1 representa la fila 1 columna 1 por ejemplo).

 

TestFinal

 

Descarga el código fuente

 

 Vuelta a la realidad

Espero que estas anécdotas de verdad os puedan servir de acicate para que tengáis la misma o mayor curiosidad que yo mismo por seguir aprendiendo día a día. No importa quien os diga que no podéis llegar mas lejos.

Tenemos una buena herramienta en nuestras manos. Yo diría que la mejor, pero es verdad que siempre esperamos que lo mejor sea lo que pueda venir el día de mañana.

También tenemos una buena Comunidad, que se ha esforzado por no quedarse atrás.

Yo encuentro muchas razones para seguir con Delphi pero no deja de ser mi opinión personal.

He dejado para el final el comentar todo el tema de la promoción de final de año y mientras finalizaba el articulo, recibía el correo de Danysoft que concretaba en nuestro idioma el contenido de la oferta, haciendo que no necesite traducir el texto original en ingles de Embarcadero.

jover1000x125

La oferta finaliza el 31 de Diciembre de este año.

Entérate de las 5 razones para dar el salto a Rad Studio XE5
Razón 1 | Actualiza desde cualquier versión anteriorFour special offers on RAD Studio, Delphi and C++Builder XE5.
Razón 2 | Bonus Pack gratuito valorado en +1300€
Razón 3 | Bonus Pack exclusivo Danysoft
Razón 4 | Edición Ultimate al precio de Enteprise
Razón 5 | Descuento adicional en 2 o + licencias, o mantenimiento

En el enlace de la imagen, podéis encontrar detalles ampliados de esta oferta que como compartía con vosotros al incoo me parecía interesantes.

Me despido de vosotros y de mi amigo Seguismundo, buen camino.

Deja una respuesta

Introduce tus datos o haz clic en un icono para iniciar sesión:

Logo de WordPress.com

Estás comentando usando tu cuenta de WordPress.com. Salir /  Cambiar )

Foto de Facebook

Estás comentando usando tu cuenta de Facebook. Salir /  Cambiar )

Conectando a %s

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

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

A %d blogueros les gusta esto: