FMX Custom Header for TStringGrid - delphi

I am using this code to set the column Headers for my TStringGrid (FMX - 10.4.1)
procedure TForm1.StringGrid1ApplyStyleLookup(Sender: TObject);
var
Header: THeader;
HeaderItem: THeaderItem;
I: Integer;
begin
Header:= THeader((Sender as TStringGrid).FindStyleResource('header'));
if Assigned(Header) then
begin
for I := 0 to pred(Header.Count) do
begin
HeaderItem:= Header.Items[I];
HeaderItem.StyledSettings := HeaderItem.StyledSettings - [TStyledSetting.Size, TStyledSetting.FontColor];
HeaderItem.Font.Size := 20;
HeaderItem.FontColor:= TAlphaColors.Blue;
HeaderItem.TextSettings.HorzAlign := TTextAlign.Center;
HeaderItem.TextSettings.VertAlign := TTextAlign.Center;
end;
Header.Height := 28;
end;
end;
I get this result as expected
However, if I'm updating the list with some new data, the header is back to default style
Why is it different now? Why ApplyStyleLookup is applied only once ?
How can I make sure the correct settings will be applied to my headers each and every time ?
Thanks
Here below a sample code
object Form1: TForm1
Left = 0
Top = 0
Caption = 'Form1'
ClientHeight = 400
ClientWidth = 600
Position = DesktopCenter
FormFactor.Width = 320
FormFactor.Height = 480
FormFactor.Devices = [Desktop]
DesignerMasterStyle = 0
object StringGrid1: TStringGrid
Align = Client
CanFocus = True
ClipChildren = True
Margins.Left = 5.000000000000000000
Margins.Top = 50.000000000000000000
Margins.Right = 5.000000000000000000
Margins.Bottom = 5.000000000000000000
Size.Width = 590.000000000000000000
Size.Height = 345.000000000000000000
Size.PlatformDefault = False
StyleLookup = 'gridstyle'
TabOrder = 0
RowCount = 0
Options = [ColumnResize, ColLines, RowLines, RowSelect, Tabs, Header]
OnApplyStyleLookup = StringGrid1ApplyStyleLookup
Viewport.Width = 586.000000000000000000
Viewport.Height = 320.000000000000000000
object StringColumn1: TStringColumn
Header = 'Test'
end
end
object Button1: TButton
Position.X = 8.000000000000000000
Position.Y = 8.000000000000000000
Size.Width = 177.000000000000000000
Size.Height = 33.000000000000000000
Size.PlatformDefault = False
TabOrder = 1
Text = 'Show Form Properties'
OnClick = Button1Click
end
object Text1: TText
Anchors = [akLeft, akTop, akRight]
Position.X = 192.000000000000000000
Position.Y = 8.000000000000000000
Size.Width = 401.000000000000000000
Size.Height = 33.000000000000000000
Size.PlatformDefault = False
Text = 'Unkown'
TextSettings.HorzAlign = Trailing
end
end
unit Unit1;
interface
uses
System.SysUtils, System.Types, System.UITypes, System.Classes, System.Variants,
System.Rtti, System.TypInfo,
FMX.Types, FMX.Controls, FMX.Forms, FMX.Graphics, FMX.Dialogs, FMX.Grid.Style,
FMX.StdCtrls, FMX.Controls.Presentation, FMX.ScrollBox, FMX.Grid, FMX.Header,
FMX.Objects;
type
TForm1 = class(TForm)
StringGrid1: TStringGrid;
Button1: TButton;
Text1: TText;
StringColumn1: TStringColumn;
procedure Button1Click(Sender: TObject);
procedure StringGrid1ApplyStyleLookup(Sender: TObject);
private
{ Private declarations }
FCount: cardinal;
public
{ Public declarations }
end;
var
Form1: TForm1;
implementation
{$R *.fmx}
procedure TForm1.Button1Click(Sender: TObject);
var
PropList: PPropList;
PropCount, PropIndex: Integer;
begin
StringGrid1.ClearColumns;
PropCount:= GetPropList(Form1, PropList);
StringGrid1.RowCount:= PropCount;
StringGrid1.RowHeight:= 20;
StringGrid1.AddObject(TStringColumn.Create(StringGrid1));
StringGrid1.Columns[0].Width:= (StringGrid1.Width - 24) / 2;
StringGrid1.Columns[0].HorzAlign:= TTextAlign.Leading;
StringGrid1.Columns[0].Header:= 'Property';
StringGrid1.AddObject(TStringColumn.Create(StringGrid1));
StringGrid1.Columns[1].Width:= (StringGrid1.Width - 24) / 2;
StringGrid1.Columns[1].HorzAlign:= TTextAlign.Leading;
StringGrid1.Columns[1].Header:= 'Value';
for PropIndex:= 0 to pred(PropCount) do
begin
StringGrid1.Cells[0, PropIndex]:= PropList[PropIndex].Name;
StringGrid1.Cells[1, PropIndex]:= GetPropValue(Form1, PropList[PropIndex].Name, true);
end;
end;
procedure TForm1.StringGrid1ApplyStyleLookup(Sender: TObject);
var
Header: THeader;
HeaderItem: THeaderItem;
I: Integer;
begin
inc(FCount);
Text1.Text:= Format('Executed [%.3d]', [FCount]);
Header:= THeader((Sender as TStringGrid).FindStyleResource('header'));
if Assigned(Header) then
begin
for I := 0 to pred(Header.Count) do
begin
HeaderItem:= Header.Items[I];
HeaderItem.StyledSettings := HeaderItem.StyledSettings - [TStyledSetting.Size, TStyledSetting.FontColor];
HeaderItem.Font.Size := 20;
HeaderItem.FontColor:= TAlphaColors.Blue;
HeaderItem.TextSettings.HorzAlign := TTextAlign.Center;
HeaderItem.TextSettings.VertAlign := TTextAlign.Center;
end;
Header.Height := 28;
end;
end;
end.

I can not answer "why" questions, otherwise than "by design".
But to solve your problem, call
StringGrid1.NeedStyleLookup;
after you have made your changes to the structure (number of columns / rows) of the grid.

To personnalise the header of a grid in a FireMonkey project, you can use onDrawColumnHeader event (if you prefer to draw on the Canvas) or use styles.
In the form editor, on your grid, use context menu to "change default style" or "change personalised style".
The header is in "grid style / background / header" for the text.
To personnalise the background, create a new "headeritemstyle" element (try TLayout with no HitTest), add a "background" to it (TButtonStyle, TRectangle or other) aligned as Contents with no HitTest and a TText component with StyleName "text", HitTest to False and aligned to client. You also can manage it's behaviour directly from this style element.

Related

FMX: How to draw a bitmap with reduced size and high quality?

I notice that quality of images is poor when drawn smaller than the source image. This is especially obvious when the image contains thin lines as the subsampling causes parts of the lines to disappear. I'm looking for a way to improve the quality without compromising too much on runtime. So ideally I'd like it to be done on the GPU. Below is an example with 3 different methods. The last method is my own code, which shows what I'm trying to achieve but is not done on the GPU and so is too slow. It also won't work if I want to include some rotation as well as scale. I have also tried changing the HighSpeed flag in the DrawBitmap function, but it doesn't seem to make a difference.
Form:
object Form1: TForm1
Left = 0
Top = 0
Caption = 'Form1'
ClientHeight = 240
ClientWidth = 438
FormFactor.Width = 320
FormFactor.Height = 480
FormFactor.Devices = [Desktop]
OnCreate = FormCreate
DesignerMasterStyle = 0
object Image1: TImage
MultiResBitmap = <
item
end>
Position.X = 8.000000000000000000
Position.Y = 8.000000000000000000
end
object Image2: TImage
MultiResBitmap = <
item
end>
Position.X = 216.000000000000000000
Position.Y = 8.000000000000000000
end
object Image3: TImage
MultiResBitmap = <
item
end>
Position.X = 216.000000000000000000
Position.Y = 80.000000000000000000
end
object Image4: TImage
MultiResBitmap = <
item
end>
Position.X = 216.000000000000000000
Position.Y = 152.000000000000000000
end
object Label1: TLabel
Position.X = 266.000000000000000000
Position.Y = 24.000000000000000000
Size.Width = 164.000000000000000000
Size.Height = 17.000000000000000000
Size.PlatformDefault = False
Text = 'Scale destination rectangle'
TabOrder = 6
end
object Label2: TLabel
Position.X = 266.000000000000000000
Position.Y = 96.000000000000000000
Size.Width = 164.000000000000000000
Size.Height = 17.000000000000000000
Size.PlatformDefault = False
Text = 'Create canvas scale transform'
TabOrder = 5
end
object Label3: TLabel
Position.X = 266.000000000000000000
Position.Y = 168.000000000000000000
Size.Width = 164.000000000000000000
Size.Height = 17.000000000000000000
Size.PlatformDefault = False
Text = 'Hand coded Shrink function'
TabOrder = 4
end
end
Unit:
unit Unit1;
interface
uses
System.SysUtils, System.Types, System.UITypes, System.Classes, System.Variants,
FMX.Types, FMX.Controls, FMX.Forms, FMX.Graphics, FMX.Dialogs, FMX.Objects,
FMX.Controls.Presentation, FMX.StdCtrls;
type
PAlphaColorArray = ^TAlphaColorArray;
TAlphaColorArray = array[0..32767] of TAlphaColor;
TForm1 = class(TForm)
Image1: TImage;
Image2: TImage;
Image3: TImage;
Image4: TImage;
Label1: TLabel;
Label2: TLabel;
Label3: TLabel;
procedure FormCreate(Sender: TObject);
private
{ Private declarations }
public
{ Public declarations }
end;
procedure Shrink(const ABitmap : TBitmap ; out ABitmapOut : TBitmap);
var
Form1: TForm1;
implementation
uses Math, Math.Vectors;
{$R *.fmx}
procedure TForm1.FormCreate(Sender: TObject);
const
r : Single = 0.2;
size : Integer = 200;
var
srcRect, dstRect : TRectF;
bmp : TBitmap;
begin
// Create source image - circle
Image1.Bitmap.BitmapScale := 1;
Image1.Bitmap.SetSize(size, size);
Image1.Width := Image1.Bitmap.Width;
Image1.Height := Image1.Bitmap.Height;
if Image1.Bitmap.Canvas.BeginScene then begin
Image1.Bitmap.Canvas.Clear(TAlphaColorRec.White);
Image1.Bitmap.Canvas.Stroke.Thickness := 1;
Image1.Bitmap.Canvas.DrawEllipse(TRectF.Create(size/10,size/10,9*size/10,9*size/10), 1);
Image1.Bitmap.Canvas.EndScene;
end;
srcRect := Image1.Bitmap.BoundsF;
dstRect := srcRect;
dstRect.BottomRight := PointF(dstRect.Width*r, dstRect.Height*r);
// Try reducing by scaling the destination rectangle
// Same method as TBitmap.CreateThumbnail
Image2.Bitmap.BitmapScale := 1;
Image2.Bitmap.SetSize(Ceil(dstRect.Width), Ceil(dstRect.Height));
Image2.Width := Image2.Bitmap.Width;
Image2.Height := Image2.Bitmap.Height;
if Image2.Bitmap.Canvas.BeginScene then begin
Image2.Bitmap.Canvas.DrawBitmap(Image1.Bitmap, srcRect, dstRect, 1, True);
Image2.Bitmap.Canvas.EndScene;
end;
// Try reducing using the canvas transform matrix
Image3.Bitmap.BitmapScale := 1;
Image3.Bitmap.SetSize(Ceil(dstRect.Width), Ceil(dstRect.Height));
Image3.Width := Image2.Bitmap.Width;
Image3.Height := Image2.Bitmap.Height;
if Image3.Bitmap.Canvas.BeginScene then begin
Image3.Bitmap.Canvas.SetMatrix(TMatrix.CreateScaling(r, r));
Image3.Bitmap.Canvas.DrawBitmap(Image1.Bitmap, srcRect, srcRect, 1, True);
Image3.Bitmap.Canvas.EndScene;
end;
// Reduce using Shrink function - Slow (not done on GPU)
bmp := TBitmap.Create(Ceil(dstRect.Width), Ceil(dstRect.Height));
bmp.BitmapScale := 1;
Image4.Width := bmp.Width;
Image4.Height := bmp.Height;
Shrink(Image1.Bitmap, bmp);
Image4.Bitmap.Assign(bmp);
bmp.Free;
end;
// Shrink a bitmap using all available data from source
// Could be sped up a little for specific cases such as scaling by 0.5
procedure Shrink(const ABitmap : TBitmap ; out ABitmapOut : TBitmap);
Var
Lx, Ly : integer;
LyBox, LxBox, LyBox1, LyBox2, LxBox1, LxBox2 : integer;
TR, TG, TB, TA : integer;
LRowIn, LRowOut, LRowInStart : PAlphaColorArray;
LBoxCount : integer;
LRowBytes, LRowBytesIn : integer;
LBoxRows : array of PAlphaColorArray;
LRatioW, LRatioH : Real;
bdata, bdatao : TBitmapData;
begin
if (ABitmapOut.Width = 0) or (ABitmapOut.Height = 0) then Exit;
LRatioH := ABitmap.Height / ABitmapOut.Height;
LRatioW := ABitmap.Width / ABitmapOut.Width;
ABitmap.Map(TMapAccess.Read, bdata);
ABitmapOut.Map(TMapAccess.Write, bdatao);
try
SetLength(LBoxRows, Trunc(LRatioH)+1);
LRowOut := PAlphaColorArray(bdatao.GetScanline(0));
LRowBytes := bdatao.Pitch;
LRowBytesIn := bdata.Pitch;
LRowInStart := PAlphaColorArray(bdata.GetScanline(0));
for Ly := 0 to ABitmapOut.Height-1 do begin
LyBox1 := Trunc(Ly*LRatioH);
LyBox2 := Trunc((Ly+1)*LRatioH) - 1;
for LyBox := LyBox1 to LyBox2 do
PByte(LBoxRows[LyBox-LyBox1]) := PByte(LRowInStart) + LyBox*LRowBytesIn;
for Lx := 0 to ABitmapOut.Width-1 do begin
LxBox1 := Trunc(Lx*LRatioW);
LxBox2 := Trunc((Lx+1)*LRatioW) - 1;
TR := 0; TG := 0; TB := 0; TA := 0;
LBoxCount := 0;
for LyBox := LyBox1 to LyBox2 do begin
LRowIn := LBoxRows[LyBox-LyBox1];
for LxBox := LxBox1 to LxBox2 do begin
Inc(TB, TAlphaColorRec(LRowIn[LxBox]).B);
Inc(TG, TAlphaColorRec(LRowIn[LxBox]).G);
Inc(TR, TAlphaColorRec(LRowIn[LxBox]).R);
Inc(TA, TAlphaColorRec(LRowIn[LxBox]).A);
Inc(LBoxCount);
end;
end;
TAlphaColorRec(LRowOut[Lx]).B := TB div LBoxCount;
TAlphaColorRec(LRowOut[Lx]).G := TG div LBoxCount;
TAlphaColorRec(LRowOut[Lx]).R := TR div LBoxCount;
TAlphaColorRec(LRowOut[Lx]).A := TA div LBoxCount;
end;
Inc(PByte(LRowOut), LRowBytes);
end;
finally
ABitmap.Unmap(bdata);
ABitmapOut.Unmap(bdatao);
end;
end;
end.

How to simulate ROWSELECT when selecting a ListItem in an OwnerDrawn TListView.OnDrawItem event handler?

In a 32-bit VCL Application in Windows 10 in Delphi 11 Alexandria, I select a ListItem in the OwnerDrawn TListView.OnDrawItem event handler and I want the ENTIRE UNINTERRUPTED row to be selected. Unfortunately, not the entire row gets selected, but only the caption-text portion of the row gets selected:
This is what I need to achieve:
This is the code of the form-unit:
unit Unit1;
interface
uses
Winapi.Windows, Winapi.Messages, System.SysUtils, System.Variants, System.Classes, Vcl.Graphics,
Vcl.Controls, Vcl.Forms, Vcl.Dialogs, Vcl.ComCtrls, Vcl.StdCtrls;
type
TForm1 = class(TForm)
ListView1: TListView;
Edit1: TEdit;
procedure ListView1DrawItem(Sender: TCustomListView; Item: TListItem; Rect: TRect; State: TOwnerDrawState);
private
{ Private declarations }
public
{ Public declarations }
end;
var
Form1: TForm1;
implementation
//uses
//CodeSiteLogging,
//Generics.Collections,
//System.StrUtils,
//Vcl.Themes;
{$R *.dfm}
procedure TForm1.ListView1DrawItem(Sender: TCustomListView; Item: TListItem; Rect: TRect; State: TOwnerDrawState);
const
Alignments: array[TAlignment] of TTextFormats = (tfLeft, tfRight, tfCenter);
procedure SetCanvasColors(const aBrushColor, aFontColor: TColor);
begin
(Sender as TListView).Canvas.Brush.Color := aBrushColor;
(Sender as TListView).Canvas.Font.Color := aFontColor;
end;
begin
if not Assigned(Item) then EXIT;
var SelectionColor := clYellow;
if Edit1.Text = '' then
begin
/// Draw normal Item Columns:
var LV := Sender as TListView;
LV.Canvas.Brush.Style := bsSolid;
LV.Canvas.FillRect(Rect);
var x1 := 0;
var x2 := 0;
var RR := Rect;
var SS: string;
LV.Canvas.Brush.Style := bsClear;
for var i := 0 to 1 do
begin
Inc(x2, LV.Columns[i].Width);
RR.Left := x1;
RR.Right := x2;
if i = 0 then
SS := Item.Caption
else
begin
SS := Item.SubItems[i - 1];
end;
SS := #32 + SS;
if ([odSelected, odHotLight] * State <> []) then
SetCanvasColors(SelectionColor, clWindowText)
else
SetCanvasColors(clWindow, clWindowText);
LV.Canvas.TextRect(RR, SS, [tfSingleLine, Alignments[LV.Columns[i].Alignment], tfVerticalCenter]);
x1 := x2;
end;
end;
// code removed that is not relevant for this question...
end;
end.
And this is the code of the form DFM file:
object Form1: TForm1
Left = 0
Top = 0
Caption = 'Form1'
ClientHeight = 191
ClientWidth = 545
Color = clBtnFace
Font.Charset = DEFAULT_CHARSET
Font.Color = clWindowText
Font.Height = -13
Font.Name = 'Segoe UI'
Font.Style = []
Position = poScreenCenter
PixelsPerInch = 96
TextHeight = 17
object ListView1: TListView
Tag = -1
Left = 0
Top = 25
Width = 545
Height = 166
Align = alClient
Columns = <
item
AutoSize = True
end
item
Width = 100
end>
Items.ItemData = {
05CA0100000400000000000000FFFFFFFFFFFFFFFF01000000FFFFFFFF000000
001654006F006D00200068006100720076006500730074006500640020003300
20006100700070006C00650073000566007200750069007400E09FD791000000
00FFFFFFFFFFFFFFFF01000000FFFFFFFF00000000194A006500720072007900
200069006E0068006500720069007400650064002000350020006F0072006100
6E006700650073000566007200750069007400D0BFD79100000000FFFFFFFFFF
FFFFFF01000000FFFFFFFF000000002454006800650020006200610062007900
2000680061007300200065006100740065006E00200073006F006D0065002000
7300740072006100770062006500720072006900650073000566007200750069
00740068D2D79100000000FFFFFFFFFFFFFFFF01000000FFFFFFFF000000003D
530061006C006C0079002000770061006E0074007300200074006F0020006200
61006B006500200061002000630061006B006500200077006900740068002000
660069007600650020006100700070006C0065007300200061006E0064002000
7400680072006500650020006F00720061006E0067006500730004630061006B
00650060F0D791FFFFFFFFFFFFFFFF}
OwnerDraw = True
ReadOnly = True
RowSelect = True
TabOrder = 0
ViewStyle = vsReport
OnDrawItem = ListView1DrawItem
end
object Edit1: TEdit
AlignWithMargins = True
Left = 33
Top = 0
Width = 479
Height = 25
Margins.Left = 33
Margins.Top = 0
Margins.Right = 33
Margins.Bottom = 0
Align = alTop
TabOrder = 1
Visible = False
end
end
The issue seems to be that you partly think about declarative programming, when in fact Delphi is entirely imperative.
If you want the background to be a single, blue rectangle, you have to write a code of line that draws a single, blue rectangle.
Since you want this to be the background, on top of which the text should be printed, you need to put this line before the text-drawing commands.
Here's a simple example:
Create a new VCL app and add a TListView to the main form. As always, set DoubleBuffered to True. In this case, I set Align = alClient, in which case you are aesthetically obliged to also set Border = bsNone.
Add columns and data.
Then, to make it owner drawn, set OwnerDraw = True.
Then add the following OnDrawItem handler:
procedure TForm1.ListView1DrawItem(Sender: TCustomListView; Item: TListItem;
Rect: TRect; State: TOwnerDrawState);
begin
if Sender <> ListView1 then
Exit;
// Draw the background
if odSelected in State then
begin
ListView1.Canvas.Brush.Color := clHighlight;
ListView1.Canvas.Font.Color := clHighlightText;
end
else
begin
ListView1.Canvas.Brush.Color := clWindow;
ListView1.Canvas.Font.Color := clWindowtext;
end;
ListView1.Canvas.FillRect(Rect);
// Draw each column
var x := 0;
for var i := 0 to ListView1.Columns.Count - 1 do
begin
var S := '';
if i = 0 then
S := Item.Caption
else
S := Item.SubItems[i - 1];
S := #32 + S; // padding happens to equal width of a single space
var W := ListView1.Columns[i].Width;
var R := TRect.Create(x, Rect.Top, x + W, Rect.Bottom);
ListView1.Canvas.TextRect(R, S, [tfSingleLine, tfVerticalCenter, tfEndEllipsis]);
Inc(x, W);
end;
end;
Result:
Please note that this simple example has a serious bug, since it doesn't support a non-zero position of the horizontal scroll bar. This can be fixed very easily, almost trivially. (How?)
In addition, in a real scenario, you would also implement the focus rectangle and the mouse hover effect.

Vertically drawn text on TCanvas not visible when drawn from initial FormResize

I'm using the DrawTextRotatedB function from Josef Ċ vejk's excellent answer to the question How to draw text in a canvas vertical + horizontal with Delphi 10.2
to draw text vertically on a TPanel (full code below, Win 32 program).
This is done in a message handler (message WM_DRAWTEXT). The PostMessage call is in the FormResize (which in the real program does a lot more).
Issue:
FormResize is called from FormShow, all relevant code is executed, but the vertical text does not show.
If I then resize the form, the same code gets executed again and it is visible.
How can this be, and how to fix it?
Structure view of form components (drawing on PnlLeftLeft):
Full test code below. Note that this logs to a text file specified by the cLogFileName constant at the top. This log contains (initial resize + one subsequent resize).
FormShow start
FormShow end
FormResize start
PostMessage left sent
FormResize end
RedrawMessage left: test text
(X,Y): (51,284)
RedrawMessage ends
FormResize start
PostMessage left sent
FormResize end
RedrawMessage left: test text
(X,Y): (51,285)
RedrawMessage ends
uFrmTest.Pas
unit uFrmTest;
interface
uses
Winapi.Windows, Winapi.Messages, System.SysUtils, System.Variants, System.Classes, Vcl.Graphics,
Vcl.Controls, Vcl.Forms, Vcl.Dialogs, Vcl.Imaging.pngimage, Vcl.ExtCtrls, System.UITypes;
const
WM_DRAWTEXT = WM_USER + 100;
cLogFileName = 'd:\temp\log.lst';
type
TFrmTest = class(TForm)
PnlClient: TPanel;
PnlLeft: TPanel;
PnlRight: TPanel;
PnlLeftLeft: TPanel;
procedure FormShow(Sender: TObject);
procedure FormDestroy(Sender: TObject);
procedure FormCreate(Sender: TObject);
procedure FormResize(Sender: TObject);
private
FTextFile: TextFile; // Debugging do cLogFileName
procedure RedrawMessage(var Msg: TMessage); message WM_DRAWTEXT;
public
end;
var
FrmTest: TFrmTest;
implementation
{$R *.dfm}
procedure DrawTextRotated(ACanvas: TCanvas; Angle, X, Y: Integer; AText: String);
// DrawTextRotatedB from https://stackoverflow.com/a/52923681/512728
var
Escapement: Integer;
LogFont: TLogFont;
NewFontHandle: HFONT;
OldFontHandle: HFONT;
begin
if not Assigned(ACanvas) then
Exit;
// Get handle of font and prepare escapement
GetObject(ACanvas.Font.Handle, SizeOf(LogFont), #LogFont);
while Angle > 360 do Angle := Angle - 360;
while Angle < -360 do Angle := Angle + 360;
Escapement := Angle * 10;
// We must initialise all fields of the record structure
LogFont.lfWidth := 0;
LogFont.lfHeight := ACanvas.Font.Height;
LogFont.lfEscapement := Escapement;
LogFont.lfOrientation := 0;
if fsBold in ACanvas.Font.Style then
LogFont.lfWeight := FW_BOLD
else
LogFont.lfWeight := FW_NORMAL;
LogFont.lfItalic := Byte(fsItalic in ACanvas.Font.Style);
LogFont.lfUnderline := Byte(fsUnderline in ACanvas.Font.Style);
LogFont.lfStrikeOut := Byte(fsStrikeOut in ACanvas.Font.Style);
LogFont.lfCharSet := ACanvas.Font.Charset;
LogFont.lfOutPrecision := OUT_DEFAULT_PRECIS;
LogFont.lfClipPrecision := CLIP_DEFAULT_PRECIS;
LogFont.lfQuality := DEFAULT_QUALITY;
LogFont.lfPitchAndFamily := DEFAULT_PITCH;
StrPCopy(LogFont.lfFaceName, ACanvas.Font.Name);
// Create new font with rotation
NewFontHandle := CreateFontIndirect(LogFont);
try
// Select the new font into the canvas
OldFontHandle := SelectObject(ACanvas.Handle, NewFontHandle);
try
// Output result
ACanvas.Brush.Style := VCL.Graphics.bsClear;
try
ACanvas.TextOut(X, Y, AText);
finally
ACanvas.Brush.Style := VCL.Graphics.bsSolid;
end;
finally
// Restore font handle
NewFontHandle := SelectObject(ACanvas.Handle, OldFontHandle);
end;
finally
// Delete the deselected font object
DeleteObject(NewFontHandle);
end;
end;
procedure TFrmTest.FormCreate(Sender: TObject);
begin
AssignFile(FTextFile,cLogFileName);
Rewrite(FTextFile);
end;
procedure TFrmTest.FormDestroy(Sender: TObject);
begin
CloseFile(FTextFile);
end;
procedure TFrmTest.FormResize(Sender: TObject);
begin
WriteLn(FTextFile,'FormResize start');
PostMessage(Handle,WM_DRAWTEXT,0,0);
WriteLn(FTextFile,'PostMessage left sent');
WriteLn(FTextFile,'FormResize end');
end;
procedure TFrmTest.FormShow(Sender: TObject);
begin
WriteLn(FTextFile,'FormShow start');
WriteLn(FTextFile,'FormShow end');
end;
type
THackPanel = class(TPanel);
procedure TFrmTest.RedrawMessage(var Msg: TMessage);
const cLeftVertText = 'test text';
var lHorDrawOffset, lVertDrawOffset: Integer;
begin
WriteLn(FTextFile,'RedrawMessage left: ' + cLeftVertText);
THackPanel(PnlLeftLeft).Canvas.Font := PnlLeftLeft.Font;
lVertDrawOffset := (PnlLeftLeft.Height - THackPanel(PnlLeftLeft).Canvas.TextHeight(cLeftVertText)) DIV 2;
lHorDrawOffset := (PnlLeftLeft.Width - THackPanel(PnlLeftLeft).Canvas.TextWidth(cLeftVertText)) DIV 2;
DrawTextRotated(THackPanel(PnlLeftLeft).Canvas , 90, lHorDrawOffset, lVertDrawOffset, cLeftVertText);
WriteLn(FTextFile,Format('(X,Y): (%d,%d)',[lHorDrawOffset,lVertDrawOffset]));
WriteLn(FTextFile,'RedrawMessage ends');
WriteLn(FTextFile,'');
end;
end.
uFrmTest.dfm
object FrmTest: TFrmTest
Left = 0
Top = 0
Caption = 'FrmTest'
ClientHeight = 592
ClientWidth = 905
Color = clWhite
Font.Charset = DEFAULT_CHARSET
Font.Color = clWindowText
Font.Height = -11
Font.Name = 'Tahoma'
Font.Style = []
OldCreateOrder = False
OnCreate = FormCreate
OnDestroy = FormDestroy
OnResize = FormResize
OnShow = FormShow
PixelsPerInch = 96
TextHeight = 13
object PnlClient: TPanel
Left = 0
Top = 0
Width = 905
Height = 592
Align = alClient
BevelOuter = bvNone
ParentColor = True
TabOrder = 0
ExplicitLeft = 88
ExplicitTop = 8
object PnlLeft: TPanel
AlignWithMargins = True
Left = 20
Top = 10
Width = 367
Height = 582
Margins.Left = 20
Margins.Top = 10
Margins.Right = 20
Margins.Bottom = 0
Align = alLeft
BevelOuter = bvNone
ParentColor = True
TabOrder = 0
object PnlLeftLeft: TPanel
Tag = -20
Left = 0
Top = 0
Width = 145
Height = 582
Align = alLeft
BevelOuter = bvNone
ParentColor = True
TabOrder = 0
end
end
object PnlRight: TPanel
AlignWithMargins = True
Left = 427
Top = 10
Width = 458
Height = 582
Margins.Left = 20
Margins.Top = 10
Margins.Right = 20
Margins.Bottom = 0
Align = alClient
BevelOuter = bvNone
TabOrder = 1
end
end
end

Delphi FMX: Saving and loading container children

Starting from this layout at design time.
(It contains several TLayout, TGridPanelLayout, TText elements as example)
At runtime, I am saving the complete objects structure to a file using ObjectBinaryToText
But when loading the file back from the file using ObjectTextToBinary, I get this result
Why the sub-controls are not taking the exqct same layout as saved before?
The file structure seems to be OK and containing all sub-controls as described when saving my form with the IDE
Here is a piece of code demonstrating the problem.
PAS File
unit Unit1;
interface
uses
System.SysUtils, System.Types, System.UITypes, System.Classes,
system.Variants, FMX.Types, FMX.Controls, FMX.Forms, FMX.Graphics,
FMX.Dialogs, FMX.Objects, FMX.Layouts, FMX.Controls.Presentation,
FMX.StdCtrls;
type
TForm1 = class(TForm)
RecTop: TRectangle;
ButtonSave: TButton;
ButtonClear: TButton;
ButtonLoad: TButton;
Layout1: TLayout;
GridPanelLayout1: TGridPanelLayout;
Text1: TText;
Text2: TText;
Text3: TText;
Text4: TText;
procedure ButtonSaveClick(Sender: TObject);
procedure ButtonClearClick(Sender: TObject);
procedure ButtonLoadClick(Sender: TObject);
procedure FormCreate(Sender: TObject);
private
{ Private declarations }
AppPath: string;
AppDatFile: String;
public
{ Public declarations }
end;
var
Form1: TForm1;
implementation
{$R *.fmx}
uses
System.IOUtils;
procedure TForm1.ButtonSaveClick(Sender: TObject);
var
FileStream : TFileStream;
MemStream : TMemoryStream;
begin
FileStream := TFileStream.Create(AppDatFile, fmCreate);
try
MemStream := TMemoryStream.Create;
MemStream.WriteComponent(Layout1);
MemStream.Position := 0;
ObjectBinaryToText(MemStream, FileStream);
finally
MemStream.Free;
FileStream.Free;
end;
end;
procedure TForm1.ButtonClearClick(Sender: TObject);
var
i: Integer;
begin
for i := pred(Layout1.ChildrenCount) downto 0 do
Layout1.Children[i].Free;
end;
procedure TForm1.ButtonLoadClick(Sender: TObject);
var
FileStream : TFileStream;
MemStream : TMemoryStream;
begin
if FileExists(AppDatFile) then
begin
FileStream := TFileStream.Create(AppDatFile, fmOpenRead);
try
MemStream := TMemoryStream.Create;
ObjectTextToBinary(FileStream, MemStream);
MemStream.Position := 0;
MemStream.ReadComponent(Layout1);
Layout1.Align:= TAlignLayout.Client;
finally
MemStream.Free;
FileStream.Free;
end;
end;
end;
procedure TForm1.FormCreate(Sender: TObject);
begin
AppPath:= TPath.GetLibraryPath;
AppDatFile:= TPath.Combine(AppPath, 'SaveLoadLayout.dat');
end;
end
FMX File
object Form1: TForm1
Left = 0
Top = 0
Caption = 'Form1'
ClientHeight = 480
ClientWidth = 640
FormFactor.Width = 320
FormFactor.Height = 480
FormFactor.Devices = [Desktop]
OnCreate = FormCreate
DesignerMasterStyle = 0
object RecTop: TRectangle
Align = Top
Size.Width = 640.000000000000000000
Size.Height = 41.000000000000000000
Size.PlatformDefault = False
end
object ButtonSave: TButton
Position.X = 8.000000000000000000
Position.Y = 8.000000000000000000
TabOrder = 3
Text = 'Save'
OnClick = ButtonSaveClick
end
object ButtonClear: TButton
Position.X = 96.000000000000000000
Position.Y = 8.000000000000000000
TabOrder = 2
Text = 'Clear'
OnClick = ButtonClearClick
end
object ButtonLoad: TButton
Position.X = 184.000000000000000000
Position.Y = 8.000000000000000000
TabOrder = 1
Text = 'Load'
OnClick = ButtonLoadClick
end
object Layout1: TLayout
Align = Client
Size.Width = 640.000000000000000000
Size.Height = 439.000000000000000000
Size.PlatformDefault = False
TabOrder = 4
object GridPanelLayout1: TGridPanelLayout
Align = Client
Size.Width = 640.000000000000000000
Size.Height = 439.000000000000000000
Size.PlatformDefault = False
TabOrder = 0
ColumnCollection = <
item
Value = 50.000000000000000000
end
item
Value = 50.000000000000000000
end>
ControlCollection = <
item
Column = 0
Control = Text1
Row = 0
end
item
Column = 1
Control = Text2
Row = 0
end
item
Column = 0
Control = Text3
Row = 1
end
item
Column = 1
Control = Text4
Row = 1
end>
RowCollection = <
item
Value = 50.000000000000000000
end
item
Value = 50.000000000000000000
end>
object Text1: TText
Align = Client
Size.Width = 320.000000000000000000
Size.Height = 219.500000000000000000
Size.PlatformDefault = False
Text = 'Text1'
end
object Text2: TText
Align = Client
Size.Width = 320.000000000000000000
Size.Height = 219.500000000000000000
Size.PlatformDefault = False
Text = 'Text2'
end
object Text3: TText
Align = Client
Size.Width = 320.000000000000000000
Size.Height = 219.500000000000000000
Size.PlatformDefault = False
Text = 'Text3'
end
object Text4: TText
Align = Client
Size.Width = 320.000000000000000000
Size.Height = 219.500000000000000000
Size.PlatformDefault = False
Text = 'Text4'
end
end
end
end
As I said in my comment, the problem is that WriteComponent wrongly write items with the format:
Control = Form1.Text1
This is not correct, it should be
Control = Text1
The behavior is maybe caused by the fact that serializing a component using other component, their owner is saved along.
The workaround is to correct what WriteComponent write. A simple implementation using a simple ReplaceString is like this:
procedure TForm1.ButtonSaveClick(Sender: TObject);
var
StringStream : TStringStream;
MemStream : TMemoryStream;
Buf : String;
begin
MemStream := nil;
StringStream := TStringStream.Create;
try
MemStream := TMemoryStream.Create;
MemStream.WriteComponent(Layout1);
MemStream.Position := 0;
ObjectBinaryToText(MemStream, StringStream);
Buf := StringReplace(StringStream.DataString,
' Control = ' + Self.Name + '.',
' Control = ', [rfReplaceAll]);
TFile.WriteAllText(AppDatFile, Buf);
finally
MemStream.Free;
StringStream.Free;
end;
end;
Be aware that this workaround implementation works for your example but could be confused because the search and replace do not use a real parser and could replace something else having the same form (A string property for example).

Get the Total ItemHeight of all the Items arbitrarily defined heights in the TListView at Runtime (Delphi Rio 10.3 FMX)

How to get the total ItemHeight (variably defined) of the items in the TListView at runtime.
The item height, by the way, is arbitrarily defined at runtime using the event UpdateObjects and I should be able to get it from there but I am not getting the correct height for some reasons. I feel it should be easier if I get those after it is defined.
I've tried this approach but no success. But my code ListView1.Items[i].Height looks not right here.
TotalItemHeight := 0;
for i = 0 to ListView1.Items.Count - 1 do
begin
TotalItemHeight := TotalItemHeight + ListView1.Items[i].Height; //I need help here..
end;
.FMX Procedure:
unit MainForm;
interface
uses
System.SysUtils, System.Types, System.UITypes, System.Classes, System.Variants,
System.Generics.Collections,
FMX.Types, FMX.Controls, FMX.Forms, FMX.Graphics, FMX.Dialogs,
FMX.Controls.Presentation, FMX.StdCtrls, FMX.ListView.Types,
FMX.ListView.Appearances, FMX.ListView.Adapters.Base, FMX.ListView,
Markov;
type
TVariableHeight = class(TForm)
Button1: TButton;
ListView1: TListView;
ToolBar1: TToolBar;
Label1: TLabel;
procedure Button1Click(Sender: TObject);
procedure ListView1UpdateObjects(const Sender: TObject;
const AItem: TListViewItem);
private
FChain: TChain;
FBitmaps: TDictionary<Integer, TBitmap>;
FText: TArray<string>;
procedure ReadText;
function GetDimensionBitmap(const Width, Height: Single): TBitmap;
public
function GetTextHeight(const D: TListItemText; const Width: Single; const Text: string): Integer;
destructor Destroy; override;
end;
var
VariableHeight: TVariableHeight;
implementation
uses
System.IOUtils, FMX.TextLayout;
{$R *.fmx}
// Create a new item with random text of random length
procedure TVariableHeight.Button1Click(Sender: TObject);
begin
ReadText;
ListView1.Items.Add.Data['txtMain'] := FText[Random(Length(FText))];
// with classic appearances, use Text property
//ListView1.Items.Add.Text := FChain.Generate(Random(100) + 5);
end;
function TVariableHeight.GetDimensionBitmap(const Width, Height: Single): TBitmap;
procedure Arrow(C: TCanvas; P: array of TPointF);
begin
C.DrawLine(P[0], P[1], 1.0);
C.DrawLine(P[0], P[2], 1.0);
C.DrawLine(P[0], P[3], 1.0);
end;
var
EndP1, EndP2: TPointF;
TextBitmap: TBitmap;
IntHeight: Integer;
begin
IntHeight := Trunc(Height);
if FBitmaps = nil then
FBitmaps := TDictionary<Integer, TBitmap>.Create;
if not FBitmaps.TryGetValue(IntHeight, Result) then
begin
Result := TBitmap.Create(Trunc(Width), IntHeight);
FBitmaps.Add(IntHeight, Result);
if Result.Canvas.BeginScene then
begin
Result.Canvas.Clear(TAlphaColorRec.Null);
Result.Canvas.Stroke.Color := TAlphaColorRec.Darkgray;
// Draw the arrows
EndP1 := TPointF.Create(Width/2, 0);
EndP2 := TPointF.Create(Width/2, Height);
Arrow(Result.Canvas,
[EndP1, TPointF.Create(Width/2, Height/2 - Width/2),
EndP1 + TPointF.Create(-2, 5), EndP1 + TPointF.Create(2, 5)]);
Arrow(Result.Canvas,
[EndP2, TPointF.Create(Width/2, Height/2 + Width/2),
EndP2 + TPointF.Create(-2, -5), EndP2 + TPointF.Create(2, -5)]);
// Draw the dimension text
TextBitmap := TBitmap.Create(Trunc(Width), Trunc(Width));
try
if TextBitmap.Canvas.BeginScene then
with TextBitmap.Canvas do
begin
Clear(TAlphaColorRec.Null);
Fill.Color := TAlphaColorRec.Darkgray;
FillText(TextBitmap.BoundsF, ''.Format('%d', [IntHeight]), False, 1,
[], TTextAlign.Center, TTextAlign.Center);
EndScene;
end;
TextBitmap.Rotate(90);
Result.Canvas.DrawBitmap(TextBitmap, TextBitmap.BoundsF,
TextBitmap.BoundsF.CenterAt(Result.BoundsF), 1);
finally
TextBitmap.Free;
end;
Result.Canvas.EndScene;
end;
end;
end;
destructor TVariableHeight.Destroy;
var
Key: Integer;
begin
FChain.Free;
if FBitmaps <> nil then
for Key in FBitmaps.Keys do
FBitmaps[Key].Free;
FBitmaps.Free;
inherited;
end;
// Calculate height for text drawable D
function TVariableHeight.GetTextHeight(const D: TListItemText; const Width: single; const Text: string): Integer;
var
Layout: TTextLayout;
begin
// Create a TTextLayout to measure text dimensions
Layout := TTextLayoutManager.DefaultTextLayout.Create;
try
Layout.BeginUpdate;
try
// Initialize layout parameters with those of the drawable
Layout.Font.Assign(D.Font);
Layout.VerticalAlign := D.TextVertAlign;
Layout.HorizontalAlign := D.TextAlign;
Layout.WordWrap := D.WordWrap;
Layout.Trimming := D.Trimming;
Layout.MaxSize := TPointF.Create(Width, TTextLayout.MaxLayoutSize.Y);
Layout.Text := Text;
finally
Layout.EndUpdate;
end;
// Get layout height
Result := Round(Layout.Height);
// Add one em to the height
Layout.Text := 'm';
Result := Result + Round(Layout.Height);
finally
Layout.Free;
end;
end;
procedure TVariableHeight.ListView1UpdateObjects(const Sender: TObject;
const AItem: TListViewItem);
var
Drawable: TListItemText;
SizeImg: TListItemImage;
Text: string;
AvailableWidth: Single;
begin
SizeImg := TListItemImage(AItem.View.FindDrawable('imgSize'));
AvailableWidth := TListView(Sender).Width - TListView(Sender).ItemSpaces.Left
- TListView(Sender).ItemSpaces.Right - SizeImg.Width;
ShowMessage(TListView(Sender).Height.ToString);
// Find the text drawable which is used to calcualte item size.
// For dynamic appearance, use item name.
// For classic appearances use TListViewItem.TObjectNames.Text
// Drawable := TListItemText(AItem.View.FindDrawable(TListViewItem.TObjectNames.Text));
Drawable := TListItemText(AItem.View.FindDrawable('txtMain'));
Text := Drawable.Text;
// Randomize the font when updating for the first time
if Drawable.TagFloat = 0 then
begin
Drawable.Font.Size := 1; // Ensure that default font sizes do not play against us
Drawable.Font.Size := 10 + Random(4) * 4;
Drawable.TagFloat := Drawable.Font.Size;
if Text.Length < 100 then
Drawable.Font.Style := [TFontStyle.fsBold];
end;
// Calculate item height based on text in the drawable
AItem.Height := GetTextHeight(Drawable, AvailableWidth, Text);
Drawable.Height := AItem.Height;
Drawable.Width := AvailableWidth;
SizeImg.OwnsBitmap := False;
SizeImg.Bitmap := GetDimensionBitmap(SizeImg.Width, AItem.Height);
end;
procedure TVariableHeight.ReadText;
const
Delimiters: array of char = [#10, #13];
var
Reader: TStreamReader;
Stream: TResourceStream;
begin
if Length(FText) = 0 then
begin
Stream := TResourceStream.Create(HInstance, 'Blabla', RT_RCDATA);
Reader := TStreamReader.Create(Stream);
try
FText := Reader.ReadToEnd.Split(Delimiters, TStringSplitOptions.ExcludeEmpty)
finally
Reader.Close;
Reader.Free;
Stream.Free;
end;
end;
end;
end.
.FMX File
object VariableHeight: TVariableHeight
Left = 0
Top = 0
Caption = 'VariableHeight'
ClientHeight = 480
ClientWidth = 510
FormFactor.Width = 320
FormFactor.Height = 480
FormFactor.Devices = [Desktop]
DesignerMasterStyle = 0
object ListView1: TListView
ItemAppearanceClassName = 'TDynamicAppearance'
ItemEditAppearanceClassName = 'TDynamicAppearance'
HeaderAppearanceClassName = 'TListHeaderObjects'
FooterAppearanceClassName = 'TListHeaderObjects'
OnUpdateObjects = ListView1UpdateObjects
Align = Client
Size.Width = 510.000000000000000000
Size.Height = 436.000000000000000000
Size.PlatformDefault = False
TabOrder = 0
ItemAppearanceObjects.ItemObjects.ObjectsCollection = <
item
AppearanceObjectName = 'txtMain'
AppearanceClassName = 'TTextObjectAppearance'
Appearance.TextAlign = Leading
Appearance.WordWrap = True
Appearance.Height = 44.000000000000000000
Appearance.VertAlign = Center
end
item
AppearanceObjectName = 'imgSize'
AppearanceClassName = 'TImageObjectAppearance'
Appearance.ScalingMode = Original
Appearance.Width = 30.000000000000000000
Appearance.Align = Trailing
end>
ItemAppearanceObjects.ItemEditObjects.ObjectsCollection = <
item
AppearanceObjectName = 'Text1'
AppearanceClassName = 'TTextObjectAppearance'
end>
end
object ToolBar1: TToolBar
Padding.Top = 4.000000000000000000
Padding.Bottom = 4.000000000000000000
Size.Width = 510.000000000000000000
Size.Height = 44.000000000000000000
Size.PlatformDefault = False
TabOrder = 1
object Label1: TLabel
Align = Contents
Size.Width = 510.000000000000000000
Size.Height = 44.000000000000000000
Size.PlatformDefault = False
StyleLookup = 'toollabel'
TextSettings.HorzAlign = Center
Text = 'Variable Row Height Demo'
end
object Button1: TButton
Align = Right
Margins.Right = 5.000000000000000000
Position.X = 448.000000000000000000
Position.Y = 4.000000000000000000
Size.Width = 57.000000000000000000
Size.Height = 36.000000000000000000
Size.PlatformDefault = False
StyleLookup = 'listitembutton'
TabOrder = 1
Text = 'Fill List'
OnClick = Button1Click
end
end
end

Resources