Returning a dynamic array from a function - arrays

I'm working with Delphi 7.
Here's my base class. The function load_for_edit() is supposed to return an array of strings which sounds like it does. The problem is not particularly here but further.
...
type
TStringArray = array of string;
...
function load_for_edit(): TStringArray;
...
numberOfFields: integer;
...
function TBaseForm.load_for_edit(): TStringArray;
var
query: TADOQuery;
i: integer;
begin
query := TADOQuery.Create(self);
query.Connection := DataModule1.ADOConnection;
query.SQL.Add('CALL get_' + self.table_name + '_id(' + self.id + ')');
query.Open;
numberOfFields := query.Fields.Count;
SetLength(result, query.Fields.Count);
for i := 0 to query.Fields.Count - 1 do
result[i] := query.Fields[i].Value.AsString;
end;
Next is the class that is a descendant of the base class and it's trying to receive an array from the base class's load_for_edit() function.
...
type
TStringArray = array of string;
...
procedure TPublisherEditForm.FormShow(Sender: TObject);
var
rec: TStringArray;
begin
inherited;
SetLength(rec, self.numberOfFields);
rec := load_for_edit(); // Compilation stops here
end;
But, the application won't compile. Delphi spits out this error message:
Incompatible types
So, what this means is that the function load_for_edit() returns a data type that is different from the variable rec's data type, though, as can be seen from their respective type declaration sections, their declarations are absolutely identical. I don't know what's going on here and what to do. Please, help me come up with a solution.

You've got two separate declarations of TStringArray (one in each unit), and they are not the same. (The fact that they're in two separate units makes them different. UnitA.TStringArray is not the same as UnitB.TStringArray, even if they're both declared as type TStringArray = array of string;.)
You need to use a single type declaration:
unit
BaseFormUnit;
interface
uses
...
type
TStringArray: array of string;
TBaseForm = class(...)
numberOfFields: Integer;
function load_for_edit: TStringArray;
end;
implementation
// Not sure here. It looks like you should use a try..finally to
// free the query after loading, but if you need to use it later
// to save the edits you should create it in the TBaseForm constructor
// and free it in the TBaseForm destructor instead, which means it
// should also be a field of the class declaration above.
function TBaseForm.load_for_edit(): TStringArray;
var
query: TADOQuery;
i: integer;
begin
query := TADOQuery.Create(self);
query.Connection := DataModule1.ADOConnection;
query.SQL.Add('CALL get_' + self.table_name + '_id(' + self.id + ')');
query.Open;
numberOfFields := query.Fields.Count;
SetLength(result, numberOfFields);
for i := 0 to numberOfFields - 1 do
Result[i] := query.Fields[i].Value.AsString;
end;
...
end.
Now your descendant class can access it:
unit
PublisherEditFormUnit;
interface
uses
... // Usual Forms, Windows, etc.
BaseFormUnit;
type
TPublisherEditForm = class(TBaseForm)
...
procedure FormShow(Sender: TObject);
end;
implementation
procedure TPublisherEditForm.FormShow(Sender: TObject);
var
rec: TStringArray;
begin
// No need to call SetLength - the base class does.
rec := load_for_edit(); // Compilation stops here
end;
...
end.

Related

Aggregate of multiple UI controls bound to one database column

I currently have a TDBRadioGroup bound to a CHAR column on the database using the Values property to specify the value stored in the database column for each radio button. Now we have a requirement to add 2 new radio groups along side the existing one that would modify the value. In other words I basically need 3 controls on the form to act as one in a data-aware configuration for the purposes of determining the value stored. A simple example might be if each radio group has a one-character value, and in the database you concatenate them into a 3-character string. Our mapping is more complex than that but that's the general idea. What would be a good way to do this? If someone tells me live bindings would be the way to go, that would be good to know, as it would be help me to convince management to upgrade Delphi. We're stuck on XE.
Interesting q!
I hope I've understood you correctly. Suppose there is a form with 3 radiogroups, one per line, like so
*A B C
D *E F
G H *I
where the asterisks indicates the positions of the selected buttons and suppose
that these settings translate into a field value AEI in a row of a dataset.
If the G entry of the 3rd RG is clicked, the dataset field value should become
GEI. That's what I've assumed you are looking for.
The code below is a "proof of concept" which implements the above functionality.
It does so using 3 standard (non-db-aware) TRadioGroups and an adaptor class
TDBRadioGroupAdaptor which makes these operate as db-aware RadioGroups. The
TDBRadioGroupAdaptor uses a descendant, TRGFieldDataLink, of the standard TFieldDataLink (see DBCtrls.Pas)
to interface with the dataset. I've used a TClientDataSet so the project can be completely
self-contained and I've avoided the use of generices as you didn't specify a Delphi version.
Most of the db-aware functionality is contained in the TRGFieldDataLink, which
serves to interface the dataset and the RadioGroups and differs from a standard
TFieldDataLink in that it treats the field value as composed of N (3 in the code example but it supports an arbitrary number)
sub-fields which each have their own RadioGroup. As ever with implementing db-aware
functionality, the code for the TDBRadioGroupAdaptor and TRGFieldDataLink is fairly
long-winded, as most of it is necessary but not very interesting. About the only
ones that are interesting, in that they make the wheels go around, are
function TRGFieldDataLink.GetGroupString : String;
// Returns a string from the ItemIndexes of the RadioGroups
procedure TRGFieldDataLink.GetGroupValues;
// Sets the DataSet field from the RadioGroup ItemIndexes
which I hope are self-explanatory from the embedded comments.
Because it's only supposed to be a proof-of-concept, the implementation is incomplete,
lacking f.i. keyboard support, but that could easily be added by mimicking the
source code of the standard TDBRadioGroup.
Obviously, something similar could be used to treat subfields of a dataset field as
a group of strings independently editable in a group of TEdits.
Also obviously, it would be possible to turn the TDBRadioGroupAdaptor into a full-blown compound component which includes its own radiogroups, but I've left that as an exercise for the reader.
Code (warning: long!)
type
TDBRadioGroupAdaptor = class;
TRGFieldDataLink = class(TFieldDataLink)
private
FAdaptor: TDBRadioGroupAdaptor;
FRecordChanging : Boolean;
procedure GetGroupValues;
function GetGroupString: String;
procedure SetGroupValues(AValue: String);
public
constructor Create(AAdaptor : TDBRadioGroupAdaptor);
destructor Destroy; override;
procedure RecordChanged(Field : TField); override;
procedure UpdateData; override;
property Adaptor : TDBRadioGroupAdaptor read FAdaptor write FAdaptor;
end;
TDBRadioGroupAdaptor = class
private
FDataLink : TRGFieldDataLink;
FRadioGroups : TList;
procedure SetDataSource(Value : TDataSource);
function GetDataSource : TDataSource;
function GetRadioGroup(Index: Integer): TRadioGroup;
procedure ItemClicked(Sender : TObject);
procedure SetFieldName(const Value: string);
function GetFieldName : String;
public
constructor Create;
destructor Destroy; override;
procedure Add(ARadioGroup : TRadioGroup);
property DataSource : TDataSource read GetDataSource write SetDataSource;
property FieldName : string read GetFieldName write SetFieldName;
property RadioGroup[Index : Integer] : TRadioGroup read GetRadioGroup;
end;
type
TForm1 = class(TForm)
DBGrid1: TDBGrid;
ClientDataSet1: TClientDataSet;
DataSource1: TDataSource;
DBNavigator1: TDBNavigator;
Button1: TButton;
RadioGroup1: TRadioGroup;
RadioGroup2: TRadioGroup;
RadioGroup3: TRadioGroup;
DBEdit1: TDBEdit;
DBRadioGroup1: TDBRadioGroup;
procedure FormCreate(Sender: TObject);
procedure FormDestroy(Sender: TObject);
private
protected
public
Adaptor : TDBRadioGroupAdaptor;
end;
[...]
{ TRGFieldDataLink }
constructor TRGFieldDataLink.Create(AAdaptor : TDBRadioGroupAdaptor);
begin
inherited Create;
Adaptor := AAdaptor;
end;
destructor TRGFieldDataLink.Destroy;
begin
inherited;
end;
procedure TRGFieldDataLink.SetGroupValues(AValue : String);
// Sets the ItemIndexes of the RadioGroups by matching each character of AValue
// to the contents of their Items
var
i,
Index : Integer;
S : String;
begin
if AValue = '' then Exit; // To avoid error when CreateDataSet is called
for i := 0 to Adaptor.FRadioGroups.Count - 1 do begin
S := AValue[i + 1];
Index := Adaptor.RadioGroup[i].Items.IndexOf(S);
Adaptor.RadioGroup[i].ItemIndex := Index;
end;
end;
procedure TRGFieldDataLink.RecordChanged(Field : TField);
// called when the dataset goes to a new record, e.g. during scrolling
// and sets the RadioGroups' ItemIndexes from the dataset data
var
FieldValue : String;
begin
Assert(DataSet <> Nil);
if FRecordChanging then exit; // just in case, avoid re-entrancy
try
FRecordChanging := True;
if Field = Nil then
Field := DataSet.FieldByName(FieldName); // Yukky way of setting Field
FieldValue := Field.AsString;
SetGroupValues(FieldValue);
finally
FRecordChanging := False;
end;
end;
function TRGFieldDataLink.GetGroupString : String;
// Returns a string from the ItemIndexes of the RadioGroups
var
i : Integer;
S : String;
begin
Result := '';
for i := 0 to Adaptor.FRadioGroups.Count - 1 do begin
S := Adaptor.RadioGroup[i].Items[Adaptor.RadioGroup[i].ItemIndex];
Result := Result + S [1];
end;
end;
procedure TRGFieldDataLink.GetGroupValues;
// Sets the DataSet field from the RadioGroup ItemIndexes
var
FieldValue,
S : String;
begin
Assert(DataSet <> Nil);
S := Field.AsString;
FieldValue := GetGroupString;
Field.AsString := FieldValue;
end;
procedure TRGFieldDataLink.UpdateData;
// Called by RTL to update the dataset record from the RadioGroups
begin
GetGroupValues;
end;
{ TDBRadioGroupAdaptor }
procedure TDBRadioGroupAdaptor.Add(ARadioGroup: TRadioGroup);
begin
FRadioGroups.Add(ARadioGroup);
ARadioGroup.OnClick := ItemClicked;
end;
constructor TDBRadioGroupAdaptor.Create;
begin
inherited;
FRadioGroups := TList.Create;
FDataLink := TRGFieldDataLink.Create(Self);
end;
destructor TDBRadioGroupAdaptor.Destroy;
begin
FDataLink.Free;
FRadioGroups.Free;
inherited Destroy;
end;
function TDBRadioGroupAdaptor.GetDataSource: TDataSource;
begin
Result := FDataLink.DataSource;
end;
procedure TDBRadioGroupAdaptor.SetDataSource(Value: TDataSource);
begin
FDataLink.DataSource := Value;
end;
function TDBRadioGroupAdaptor.GetRadioGroup(Index: Integer): TRadioGroup;
begin
Result := TRadioGroup(FRadioGroups[Index]);
end;
procedure TDBRadioGroupAdaptor.ItemClicked(Sender: TObject);
// Responds to one of the RadioGroups being clicked to put the DataSet into Edit state
// and updates the DataSet field from the ItemIndexes of the RadioGroups
var
S : String;
begin
if not FDataLink.FRecordChanging then begin
S := FDataLink.GetGroupString;
FDataLink.Edit;
FDataLink.SetGroupValues(S);
end;
end;
procedure TDBRadioGroupAdaptor.SetFieldName(const Value: string);
begin
FDataLink.FieldName := Value;
end;
function TDBRadioGroupAdaptor.GetFieldName: string;
begin
Result := FDataLink.FieldName;
end;
procedure TForm1.FormCreate(Sender: TObject);
var
Field : TField;
begin
// Create 2 fields in the CDS
Field := TIntegerField.Create(Self);
Field.FieldName := 'ID';
Field.FieldKind := fkData;
Field.DataSet := ClientDataSet1;
Field := TStringField.Create(Self);
Field.FieldName := 'Value';
Field.Size := 40;
Field.FieldKind := fkData;
Field.DataSet := ClientDataSet1;
RadioGroup1.Items.Add('A');
RadioGroup1.Items.Add('B');
RadioGroup1.Items.Add('C');
RadioGroup2.Items.Add('D');
RadioGroup2.Items.Add('E');
RadioGroup2.Items.Add('F');
RadioGroup3.Items.Add('G');
RadioGroup3.Items.Add('H');
RadioGroup3.Items.Add('I');
Adaptor := TDBRadioGroupAdaptor.Create;
Adaptor.Add(RadioGroup1);
Adaptor.Add(RadioGroup2);
Adaptor.Add(RadioGroup3);
Adaptor.DataSource := DataSource1;
Adaptor.FieldName := 'Value';
// Next, set up the CDS
ClientDataSet1.CreateDataSet;
ClientDataSet1.InsertRecord([1, 'AEI']);
ClientDataSet1.InsertRecord([2, 'BDG']);
ClientDataSet1.InsertRecord([3, 'ADG']);
end;
procedure TForm1.FormDestroy(Sender: TObject);
begin
Adaptor.Free;
end;

How to output an array that has nested arrays in Pascal

I'm creating a basic concept of a music player using Pascal, but I'm struggling to display the albums inside it. The error I got says "(134, 29) Error: Can't read or write variables of this type". I'm assuming it's saying that because I'm using an array within an array, and it's having a hard time displaying both at the same time (although I only want it to display the albums, not the tracks as well).
Here's what my code looks like:
function ReadAllTrack(prompt: String): Tracks;
var
i: Integer;
trackArray: Array of Track;
trackCount: Integer;
begin
WriteLn(prompt);
trackCount := ReadIntegerGreaterThan1('Please enter the number of tracks you would like to add: ');
Setlength(trackArray, trackCount);
for i := 0 to trackCount - 1 do
begin
WriteLn('Enter the details for your track:');
trackArray[i] := ReadTrack();
end;
result := trackArray;
end;
function ReadAlbum(): Album;
begin
result.albumName := ReadString('Album name: ');
result.artistName := ReadString('Artist name: ');
result.albumGenre := ReadGenre('Genre:');
result.trackCollection := ReadAllTrack('Track Collection:');
end;
function ReadAllAlbums(): Albums;
var
i: Integer;
albumArray: Array of Album;
albumCount: Integer;
begin
albumCount := ReadIntegerGreaterThan1('Please enter the number of albums you would like to add: ');
Setlength(albumArray, albumCount);
for i := 0 to albumCount - 1 do
begin
WriteLn('Enter the details for your album:');
albumArray[i] := ReadAlbum();
end;
result := albumArray;
end;
procedure DisplayAlbumOptions(listOfAllAlbums: Albums);
var
userInput: Integer;
begin
WriteLn('1. Display all albums');
WriteLn('2. Display all albums for a genre');
userInput := ReadIntegerRange('Please enter a number (1, 2) to select: ', 1, 2);
case userInput of
1: WriteLn(listOfAllAlbums); //Error: Can't read or write variables of this type
end;
end;
Basically what this does is it will ask the user showing 5 options:
1. Add albums
2. Display albums
etc
If the user selects 1, the program will ask the user to input the number of albums they want to input. Then for each album it'll ask them to enter the details, and then the tracks.
Then if the user selects 2, the program will ask the user to choose either display every single album there is, or display all albums for a single genre (I'll be working on this one after solving this problem). At first I thought it would be just as simple as WriteLn(TheAlbumArray); but turns out it was more complicated than I thought because I don't think it's possible for the program to display it this way. I tried separating the albums and tracks so that it would only display the albums when I use WriteLn(TheAlbumArray); but it wasn't possible because the tracks still have to be "inside" the album so that when I display the albums and select one of them, it would then display the tracks....
Any help or suggestion for this and/or the second will be much appreciated ^^
Your original question contained a lot of superfluous detail. After the edit, you removed the type declarations, but kept much of the superfluous detail.
However, it is possible to discern the problem you are passing an array of record to Writeln. The Writeln function can accept only certain simple types as arguments, e.g. strings, numerical types, boolean. You certainly cannot pass an array to Writeln. You must iterate over the array and process each member individually.
So you might try
for i := low(listOfAllAlbums) to high(listOfAllAlbums) do
WriteLn(listOfAllAlbums[i]);
But that does not work either, because listOfAllAlbums[i] is a record, and a record is a compound type which cannot be passed to Writeln. So you need to process the record separately. If you want to display just the title, then you write:
for i := low(listOfAllAlbums) to high(listOfAllAlbums) do
WriteLn(listOfAllAlbums[i].albumName);
If you want to print the track titles too then you need to iterate over the array contained in the record.
for i := low(listOfAllAlbums) to high(listOfAllAlbums) do
begin
WriteLn(listOfAllAlbums[i].albumName);
for j := low(trackCollection) to high(trackCollection) do
WriteLn(listOfAllAlbums[i].trackCollection[j]);
end;
It is impossible to use composite types (arrays, records, ...) in Read[ln] and Write[ln] procedures.
To make your code more transparent you could to create type helper for your array(s) and use well-known AsString property. Here is example for simple array of Integer:
program foo;
{$mode objfpc}{$H+}
{$modeswitch typehelpers}
uses
Classes, SysUtils;
type
TMyArray = array of Integer;
TMyArrayHelper = type helper for TMyArray
private
function GetAsString: string;
procedure SetAsString(const AValue: string);
public
property AsString: string read GetAsString write SetAsString;
end;
function TMyArrayHelper.GetAsString: string;
var
i: Integer;
begin
Result := '';
for i in Self do
begin
if Result <> '' then
Result := Result + ', ';
Result := Result + IntToStr(i);
end;
Result := '[' + Result + ']';
end;
// Relatively simple parser
// Fill free to implement ones for your array type
procedure TMyArrayHelper.SetAsString(const AValue: string);
var
tmp, s: string;
items: TStringArray;
i: Integer;
begin
tmp := Trim(AValue);
if not (tmp.StartsWith('[') and tmp.EndsWith(']')) then
raise Exception.CreateFmt('Invalid array literal format: "%s"', [tmp]);
tmp := tmp.Trim(['[', ']']);
items := tmp.Split([',']);
for s in items do
try
StrToInt(s);
except
on e: Exception do
raise Exception.CreateFmt('Invalid integer literal: "%s"', [s]);
end;
SetLength(Self, Length(items));
for i := 0 to Length(items) - 1 do
Self[i] := StrToInt(items[i]);
end;
var
a1, a2: TMyArray;
begin
a1.AsString := '[1,2,3,5]';
Writeln('a1 = ', a1.AsString);
a2.AsString := a1.AsString;
a2[1] := 999;
Writeln('a2 = ', a2.AsString);
end.
Helper types in FreePascal
TStringHelper in SysUtils unit

Adding Values to an Array in Pascal - iIllegal Qualifier"

I am trying to create a program that prints 11 buttons so I wanted to use an array. The only change with these buttons is the name.
When I try to compile, I get the error "illegal qualifier" at my first array assignment.
type
buttonName = array[0..11] of String;
procedure PopulateButton(const buttonName);
begin
buttonName[0] := 'Sequence';
buttonName[1] := 'Repetition';
buttonName[2]:= 'Modularisation';
buttonName[3]:= 'Function';
buttonName[4]:= 'Variable';
buttonName[5]:= 'Type';
buttonName[6]:= 'Program';
buttonName[7]:= 'If and case';
buttonName[8]:= 'Procedure';
buttonName[9]:= 'Constant';
buttonName[10]:= 'Array';
buttonName[11]:= 'For, while, repeat';
end;
and in main I am trying to use this for loop
for i:=0 to High(buttonName) do
begin
DrawButton(x, y, buttonName[i]);
y:= y+70;
end;
Please know, I am very new to this and am not too confident of my knowledge in arrays, parameters/calling by constant and such.
Thank you
The parameter definition of PopulateButton() is wrong.
Try this:
type
TButtonNames = array[0..11] of String;
procedure PopulateButtons(var AButtonNames: TButtonNames);
begin
AButtonNames[0] := 'Sequence';
...
end;
...
var lButtonNames: TButtonNames;
PopulateButtons(lButtonNames);
for i := Low(lButtonNames) to High(lButtonNames) do
begin
DrawButton(x, y, lButtonNames[i]);
y:= y+70;
end;
Also pay attention to the naming conventions. Types normally begin with a T and function parameters start with an A.

Cast static array to open array of different element type

(I already asked this at CodeReview where it got closed as off-topic. Hopefully it's on-topic here.)
I have a static arrays of a derived type (like LabelsA: array[0..3] of TLabel; in the following sample code) and a routine accepting an open array of the base type (like procedure DoSomethingWithControls(const AControls: array of TControl);), and I want to call DoSomethingWithControls with those static arrays. Please see my sample:
procedure DoSomethingWithControls(const AControls: array of TControl);
var
i: Integer;
begin
for i := Low(AControls) to High(AControls) do
Writeln(AControls[i].Name);
end;
procedure Test;
var
LabelsA: array[0..3] of TLabel;
LabelsB: array[0..1] of TLabel;
procedure Variant1;
type
TArray1 = array[Low(LabelsA)..High(LabelsA)] of TControl;
TArray2 = array[Low(LabelsB)..High(LabelsB)] of TControl;
begin
DoSomethingWithControls(TArray1(LabelsA));
DoSomethingWithControls(TArray2(LabelsB));
end;
procedure Variant2;
type
TControlArray = array[0..Pred(MaxInt div SizeOf(TControl))] of TControl;
PControlArray = ^TControlArray;
begin
DoSomethingWithControls(Slice(PControlArray(#LabelsA)^, Length(LabelsA)));
DoSomethingWithControls(Slice(PControlArray(#LabelsB)^, Length(LabelsB)));
end;
procedure Variant3;
var
ControlsA: array[Low(LabelsA)..High(LabelsA)] of TControl absolute LabelsA;
ControlsB: array[Low(LabelsB)..High(LabelsB)] of TControl absolute LabelsB;
begin
DoSomethingWithControls(ControlsA);
DoSomethingWithControls(ControlsB);
end;
begin
Variant1;
Variant2;
Variant3;
end;
There are some possible variants of calling DoSomethingWithControls:
Variant 1 is quite simple but needs an "adapter" types like TArray1
for every size of TLabel array. I would like it to be more flexible.
Variant 2 is more flexible and uniform but ugly and error prone.
Variant 3 (courtesy of
TOndrej) is similar to
Variant 1 - it doesn't need an explicit cast, but Variant 1 offers a
tiny bit more compiler security if you mess something up (e.g.
getting the array bounds wrong while copy-pasting).
Any ideas how i can formulate these calls without these disadvantages (without changing the element types of the arrays)? It should work with D2007 and XE6.
These casts are all rather ugly. They will all work, but using them makes you feel dirty. It's perfectly reasonable to use a helper function:
type
TControlArray = array of TControl;
function ControlArrayFromLabelArray(const Items: array of TLabel): TControlArray;
var
i: Integer;
begin
SetLength(Result, Length(Items));
for i := 0 to high(Items) do
Result[i] := Items[i];
end;
And then you call your function like this:
DoSomethingWithControls(ControlArrayFromLabelArray(...));
Of course, this would be so much cleaner if you could use generics.
Not extremely beautiful either but you could trick the compiler like this:
procedure Variant3;
var
ControlsA: array[Low(LabelsA)..High(LabelsA)] of TControl absolute LabelsA;
begin
DoSomethingWithControls(ControlsA);
end;
Declare an overloaded procedure:
procedure DoSomethingWithControls(const AControls: array of TControl); overload;
var
i: Integer;
begin
for i := 0 to High(AControls) do
if Assigned(AControls[i]) then
Writeln(AControls[i].Name)
else
WriteLn('Control item: ',i);
end;
procedure DoSomethingWithControls(const ALabels: array of TLabel); overload;
type
TControlArray = array[0..Pred(MaxInt div SizeOf(TControl))] of TControl;
PControlArray = ^TControlArray;
begin
DoSomethingWithControls(Slice(PControlArray(#ALabels)^, Length(ALabels)));
end;
This is a general solution to your variant2. One declaration for all cases, so less prone to errors.
Below example is based on how open array parameters are internally implemented. It won't work with "typed # operator" however.
procedure Variant4;
type
TCallProc = procedure (AControls: Pointer; HighBound: Integer);
var
CallProc: TCallProc;
begin
CallProc := #DoSomethingWithControls;
CallProc(#LabelsA, Length(LabelsA) - 1);
CallProc(#LabelsB, Length(LabelsB) - 1);
end;
Passing High(Labels) for HighBound is perhaps better as long as all static arrays are 0 based.
Since a dynamic array can be passed into method as an open array, and option would be to convert the static array to a dynamic array.
If you don't mind the overhead of copying the array, consider the following:
Write a function to convert an open array of labels into a dynamic TControlArray array.
type
TControlArray = array of TControl;
{$IFOPT R+} {$DEFINE R_ON} {$R-} {$ENDIF}
function MakeControlArray(const ALabels: array of TLabel): TControlArray;
begin
SetLength(Result, Length(ALabels));
Move(ALabels[0], Result[0], Length(ALabels) * SizeOf(TObject));
end;
{$IFDEF R_ON} {$R+} {$UNDEF R_ON} {$ENDIF}
Now Variant4 can be written as:
procedure Variant4;
begin
DoSomethingWithControls(MakeControlArray(LabelsA));
DoSomethingWithControls(MakeControlArray(LabelsB));
end;
Test cases:
procedure TAdHocTests.TestLabelsToControls;
const
LLabelsA: array[0..3] of TLabel = (Pointer(0),Pointer(1),Pointer(2),Pointer(3));
var
LLoopI: Integer;
LLabelsB: array[0..9] of TLabel;
LEmptyArray: TLabelArray;
begin
for LLoopI := Low(LLabelsB) to High(LLabelsB) do
begin
LLabelsB[LLoopI] := Pointer(LLoopI);
end;
DoSomethingWithControls(MakeControlArray(LLabelsA), Length(LLabelsA));
DoSomethingWithControls(MakeControlArray(LLabelsB), Length(LLabelsB));
DoSomethingWithControls(MakeControlArray([]), 0);
DoSomethingWithControls(MakeControlArray(LEmptyArray), 0);
end;
procedure TAdHocTests.DoSomethingWithControls(
const AControls: array of TControl;
AExpectedLength: Integer);
var
LLoopI: Integer;
begin
CheckEquals(AExpectedLength, Length(AControls), 'Length incorrect');
for LLoopI := Low(AControls) to High(AControls) do
begin
CheckEquals(LLoopI, Integer(AControls[LLoopI]));
end;
end;

Generic function to increase array of pointers

I'm working in a code that has a data structure like this:
type
TData1 = record
IntField: Integer;
StrField: string;
end;
TData2 = record
DateField: TDateTime;
StrField: string;
end;
var
AData1 = array of ^TData1;
AData2 = array of ^TData2;
Sometimes I have to add an element to one of that arrays, like this:
L := Length(AData1);
SetLength(AData1, L + 1);
New(AData1[L]);
How can I write a procedure that does that job of increasing the array size and allocating memory for a new item that works for any kind of pointer? And it must be done without changing the definitions of the records (TData1, TData2) and the arrays (AData1, AData2), so it won't break existing code.
Ps: I don't have much background with pointers and that kind of programming, I'd certainly use objects and dynamic linked lists, but in this case it's a legacy code and I can't change it, at least for now.
Update:
This is not a full answer. Using TDynArray as mentioned by David may solve your problem.
Using RTTI I made a another solution for you. It is a generic solution and you can easily add more functions/capabilities. It will preserve your type declarations. Example of adding/removing records is included.
The record TDynPtArray handles any dynamic array of pointers to records. It is initialized with an Init call:
DPA.Init(TypeInfo(TData1), AData1);
Data1 := DPA.Add; // Adds a record with default values and
// returns a pointer to the record
DPA.Remove; // Finalizes/deallocates the last record and
// shrinks the dynamic array
-
uses
Windows,System.SysUtils,System.TypInfo;
Type
TPtArray = array of Pointer;
PPtArray = ^TPtArray;
TDynPtArray = record
private
FDynArray: PPtArray;
FTypeInfo: PTypeInfo;
FTypeData: PTypeData;
public
constructor Init( T: Pointer; var dynArray);
function Add : Pointer;
procedure Remove;
procedure Clear;
end;
constructor TDynPtArray.Init(T: Pointer; var dynArray);
begin
FTypeInfo := T;
if (FTypeInfo^.Kind <> tkRecord) then
raise Exception.CreateFmt('%s is not a record',[FTypeInfo^.Name]);
FTypeData := GetTypeData( FTypeInfo);
FDynArray := #dynArray;
end;
function TDynPtArray.Add: Pointer;
var
L: integer;
begin
L := Length(FDynArray^);
SetLength(FDynArray^,L+1);
GetMem( FDynArray^[L], FTypeData^.elSize);
ZeroMemory( FDynArray^[L], FTypeData^.elSize);
Result := FDynArray^[L];
end;
procedure RecordClear(var Dest; TypeInfo: pointer);
asm
{$ifdef CPUX64}
.NOFRAME
{$endif}
jmp System.#FinalizeRecord
end;
procedure TDynPtArray.Remove;
var
L: integer;
begin
L := Length(FDynArray^);
if (L = 0) then
exit;
RecordClear( FDynArray^[L-1]^,FTypeInfo); // Finalize record
FreeMem( FDynArray^[L-1], FTypeData^.elSize);
SetLength(FDynArray^,L-1);
end;
procedure TDynPtArray.Clear;
begin
while (Length(FDynArray^) <> 0) do
Self.Remove;
end;
And a little test:
type
PData1 = ^TData1;
TData1 = record
IntField: Integer;
StrField: string;
end;
TData1Arr = array of PData1;
var
AData1: TData1Arr;
Data1: PData1;
DPA: TDynPtArray;
begin
DPA.Init(TypeInfo(TData1), AData1);
Data1:= DPA.Add;
Data1^.StrField := '111';
WriteLn(Data1^.IntField);
WriteLn(Data1^.StrField);
DPA.Clear;
ReadLn;
end.

Resources