Is it possible to display one object multiple times in a VirtualStringTree? - delphi

I realize that I really need to rewrite my programs data structure (not now, but soon, as the deadline is monday), as I am currently using VST (VirtualStringTree) to store my data.
What I would like to achieve, is a Contact List structure. The Rootnodes are the Categories, and the children are the Contacts. There is a total of 2 levels.
The thing is though, that I need a contact to display in more than 1 category, but they need to be synchronized. Particularly the Checkstate.
Currently, to maintain sync, I loop thru my whole tree to find nodes that have the same ID as the one that was just changed. But doing so is very slow when there is a huge ammount of nodes.
So, I thought: Would it be possible to display one instance of the Contact Object, in multiple Categories?
Note: Honestly I am not 100% familiar with the terminology - what I mean by Instance, is one Object (or Record), so I will not have to look thru my entire tree to find Contact Objects with the same ID.
Here is an example:
As you see, Todd Hirsch appears in Test Category, and in All Contacts. But behind the scenes, those are 2 PVirtualNodes, so when I change a property on one of the node's (Like CheckState), or something in the node's Data Record/Class, the 2 nodes are not synchronized. And currently the only way I can synchronize them, is to loop thru my tree, find all the nodes that house that same contact, and apply the changes to them and their data.
To summarize: What I am looking for, is a way to use one object/record, and display it in several Categories in my tree - and whenever one node gets checked, so will every other node that houses the same Contact object.
Do I make any sense here?

Of course you can. You need to separate nodes and data in your mind. Nodes in TVirtualStringTree do not need to hold the data, the can simply be used to point to an instance where the data can be found. And of course you can point two nodes to the same object instance.
Say you have a list of TPerson's and you haev a tree where you want to show each person in different nodes. Then you declare the record you use for your nodes simply as something like:
TNodeRecord = record
... // anything else you may need or want
DataObject: TObject;
...
end;
In the code where the nodes are initialized, you do something like:
PNodeRecord.DataObject := PersonList[SomeIndex];
That's the gist of it. If you want a general NodeRecord, like I showed above, then you would need to cast it back to the proper class in order to use it in the various Get... methods. You can of course also make a specific record per tree, where you declare DataObject to be of the specific type of class that you display in the tree. The only drawback is that you then limit the tree to showing information for that class of objects.
I should have a more elaborate example lying around somewhere. When I find it, I'll add it to this answer.
Example
Declare a record to be used by the tree:
RTreeData = record
CDO: TCustomDomainObject;
end;
PTreeData = ^RTreeData;
TCustomDomainObject is my base class for all domain information. It is declared as:
TCustomDomainObject = class(TObject)
private
FList: TObjectList;
protected
function GetDisplayString: string; virtual;
function GetCount: Cardinal;
function GetCDO(aIdx: Cardinal): TCustomDomainObject;
public
constructor Create; overload;
destructor Destroy; override;
function Add(aCDO: TCustomDomainObject): TCustomDomainObject;
property DisplayString: string read GetDisplayString;
property Count: Cardinal read GetCount;
property CDO[aIdx: Cardinal]: TCustomDomainObject read GetCDO;
end;
Please note that this class is set up to be able to hold a list of other TCustomDomainObject instances. On the form which shows your tree you add:
TForm1 = class(TForm)
...
private
FIsLoading: Boolean;
FCDO: TCustomDomainObject;
protected
procedure ShowColumnHeaders;
procedure ShowDomainObject(aCDO, aParent: TCustomDomainObject);
procedure ShowDomainObjects(aCDO, aParent: TCustomDomainObject);
procedure AddColumnHeaders(aColumns: TVirtualTreeColumns); virtual;
function GetColumnText(aCDO: TCustomDomainObject; aColumn: TColumnIndex;
var aCellText: string): Boolean;
protected
property CDO: TCustomDomainObject read FCDO write FCDO;
public
procedure Load(aCDO: TCustomDomainObject);
...
end;
The Load method is where it all starts:
procedure TForm1.Load(aCDO: TCustomDomainObject);
begin
FIsLoading := True;
VirtualStringTree1.BeginUpdate;
try
if Assigned(CDO) then begin
VirtualStringTree1.Header.Columns.Clear;
VirtualStringTree1.Clear;
end;
CDO := aCDO;
if Assigned(CDO) then begin
ShowColumnHeaders;
ShowDomainObjects(CDO, nil);
end;
finally
VirtualStringTree1.EndUpdate;
FIsLoading := False;
end;
end;
All it really does is clear the form and set it up for a new CustomDomainObject which in most cases would be a list containing other CustomDomainObjects.
The ShowColumnHeaders method sets up the column headers for the string tree and adjusts the header options according to the number of columns:
procedure TForm1.ShowColumnHeaders;
begin
AddColumnHeaders(VirtualStringTree1.Header.Columns);
if VirtualStringTree1.Header.Columns.Count > 0 then begin
VirtualStringTree1.Header.Options := VirtualStringTree1.Header.Options
+ [hoVisible];
end;
end;
procedure TForm1.AddColumnHeaders(aColumns: TVirtualTreeColumns);
var
Col: TVirtualTreeColumn;
begin
Col := aColumns.Add;
Col.Text := 'Breed(Group)';
Col.Width := 200;
Col := aColumns.Add;
Col.Text := 'Average Age';
Col.Width := 100;
Col.Alignment := taRightJustify;
Col := aColumns.Add;
Col.Text := 'CDO.Count';
Col.Width := 100;
Col.Alignment := taRightJustify;
end;
AddColumnHeaders was separated out to allow this form to be used as a base for other forms showing information in a tree.
The ShowDomainObjects looks like the method where the whole tree will be loaded. It isn't. We are dealing with a virtual tree after all. So all we need to do is tell the virtual tree how many nodes we have:
procedure TForm1.ShowDomainObjects(aCDO, aParent: TCustomDomainObject);
begin
if Assigned(aCDO) then begin
VirtualStringTree1.RootNodeCount := aCDO.Count;
end else begin
VirtualStringTree1.RootNodeCount := 0;
end;
end;
We are now mostly set up and only need to implement the various VirtualStringTree events to get everything going. The first event to implement is the OnGetText event:
procedure TForm1.VirtualStringTree1GetText(Sender: TBaseVirtualTree; Node:
PVirtualNode; Column: TColumnIndex; TextType: TVSTTextType; var CellText:
string);
var
NodeData: ^RTreeData;
begin
NodeData := Sender.GetNodeData(Node);
if GetColumnText(NodeData.CDO, Column, {var}CellText) then
else begin
if Assigned(NodeData.CDO) then begin
case Column of
-1, 0: CellText := NodeData.CDO.DisplayString;
end;
end;
end;
end;
It gets the NodeData from the VirtualStringTree and used the obtained CustomDomainObject instance to get its text. It uses the GetColumnText function for this and that was done, again, to allow for using this form as a base for other forms showing trees. When you go that route, you would declare this method virtual and override it in any descendant forms. In this example it is simply implemented as:
function TForm1.GetColumnText(aCDO: TCustomDomainObject; aColumn: TColumnIndex;
var aCellText: string): Boolean;
begin
if Assigned(aCDO) then begin
case aColumn of
-1, 0: begin
aCellText := aCDO.DisplayString;
end;
1: begin
if aCDO.InheritsFrom(TDogBreed) then begin
aCellText := IntToStr(TDogBreed(aCDO).AverageAge);
end;
end;
2: begin
aCellText := IntToStr(aCDO.Count);
end;
else
// aCellText := '';
end;
Result := True;
end else begin
Result := False;
end;
end;
Now that we have told the VirtualStringTree how to use the CustomDomainObject instance from its node record, we of course still need to link the instances in the main CDO to the nodes in the tree. That is done in the OnInitNode event:
procedure TForm1.VirtualStringTree1InitNode(Sender: TBaseVirtualTree;
ParentNode, Node: PVirtualNode; var InitialStates: TVirtualNodeInitStates);
var
ParentNodeData: ^RTreeData;
ParentNodeCDO: TCustomDomainObject;
NodeData: ^RTreeData;
begin
if Assigned(ParentNode) then begin
ParentNodeData := VirtualStringTree1.GetNodeData(ParentNode);
ParentNodeCDO := ParentNodeData.CDO;
end else begin
ParentNodeCDO := CDO;
end;
NodeData := VirtualStringTree1.GetNodeData(Node);
if Assigned(NodeData.CDO) then begin
// CDO was already set, for example when added through AddDomainObject.
end else begin
if Assigned(ParentNodeCDO) then begin
if ParentNodeCDO.Count > Node.Index then begin
NodeData.CDO := ParentNodeCDO.CDO[Node.Index];
if NodeData.CDO.Count > 0 then begin
InitialStates := InitialStates + [ivsHasChildren];
end;
end;
end;
end;
Sender.CheckState[Node] := csUncheckedNormal;
end;
As our CustomDomainObject can have a list of other CustomDomainObjects, we also set the InitialStates of the node to include HasChildren when the Count of the lsit is greater than zero. This means that we also need to implement the OnInitChildren event, which is called when the user clicks on a plus sign in the tree. Again, all we need to do there is tell the tree for how many nodes it needs to prepare:
procedure TForm1.VirtualStringTree1InitChildren(Sender: TBaseVirtualTree; Node:
PVirtualNode; var ChildCount: Cardinal);
var
NodeData: ^RTreeData;
begin
ChildCount := 0;
NodeData := Sender.GetNodeData(Node);
if Assigned(NodeData.CDO) then begin
ChildCount := NodeData.CDO.Count;
end;
end;
That's all folks!!!
As I have shown an example with a simple list, you still need to figure out which data instances you need to link to which nodes, but you should have a fair idea now of where you need to do that: the OnInitNode event where you set the CDO member of the node record to point to the CDO instance of your choice.

Related

how to retain connections between controls when copying?

i want to ask how to retain controlls when im making a copy of a control. for example i have an edit box that can be controlled with a slider for value change. when i make a copy using this code i achieve a copy of the items but the slider stops controlling editbox values. how can i fix that?
TypInfo;
procedure CloneProperties(const Source: TControl; const Dest: TControl);
var
ms: TMemoryStream;
OldName: string;
begin
OldName := Source.Name;
Source.Name := ''; // needed to avoid Name collision
try
ms := TMemoryStream.Create;
try
ms.WriteComponent(Source);
ms.Position := 0;
ms.ReadComponent(Dest);
finally
ms.Free;
end;
finally
Source.Name := OldName;
end;
end;
procedure CloneEvents(Source, Dest: TControl);
var
I: Integer;
PropList: TPropList;
begin
for I := 0 to GetPropList(Source.ClassInfo, [tkMethod], #PropList) - 1 do
SetMethodProp(Dest, PropList[I], GetMethodProp(Source, PropList[I]));
end;
procedure DuplicateChildren(const ParentSource: TWinControl;
const WithEvents: Boolean = True);
var
I: Integer;
CurrentControl, ClonedControl: TControl;
begin
for I := ParentSource.ControlCount - 1 downto 0 do
begin
CurrentControl := ParentSource.Controls[I];
ClonedControl := TControlClass(CurrentControl.ClassType).Create(CurrentControl.Owner);
ClonedControl.Parent := ParentSource;
CloneProperties(CurrentControl, ClonedControl);
ClonedControl.Name := CurrentControl.Name + '_';
if WithEvents then
CloneEvents(CurrentControl, ClonedControl);
end;
end;
procedure TForm1.Button1Click(Sender: TObject);
begin
DuplicateChildren(Panel1);
end;
Unless I'm misunderstanding you, your CloneProperties doesn't seem to have anything to do with the question you're asking. In your example of an edit control E1 and a slider S1, you can clone both of them to produce E2 and S2, but somewhere in your code there must be a statement that changes the value in E1 depending on the value of S1. However, in the way you've most likely written it, that statement doesn't apply to E2 and S2.
The simplest way around that is to write a method which takes the component instances and links the operation of the two together. e.g.
procedure TForm1.SetEditControlFromSlider(AnEdit : TEdit; ASlider : { TWhatever the slider actually is);
begin
// Set AnEdit's value from ASlider's properties
end;
Then, you can call this with Edit/Slider pairs like this
SetEditControlFromSlider(E1, S1);
[...]
SetEditControlFromSlider(E2, S2);
I can imagine you might not like having to do that.
IMO, the cleanest solution is to avoid attempting to clone components altogether and create a TFrame containing the Edit, Slider and the code that connects them, and then add to your form as many instances of the frame as you need. It's as easy as falling off a log.
type
TEditFrame = class(TFrame) // needs to be in its own unit, Used by your form
Edit1: TEdit;
TrackBar1: TTrackBar;
procedure TrackBar1Change(Sender: TObject);
private
public
end;
[...]
procedure TEditFrame.TrackBar1Change(Sender: TObject);
begin
Edit1.Text := IntToStr(TrackBar1.Position)
end;
Then, you can add clones of the frame to TForm1 by
procedure TForm1.Button1Click(Sender: TObject);
var
AFrame : TEditFrame;
begin
Inc(FrameCount); // Field of TForm1
AFrame := TEditFrame.Create(Self);
AFrame.Name := AFrame.Name + IntToStr(FrameCount);
AFrame.Parent := Self;
AFrame.Top := AFrame.Height * FrameCount;
end;
Note that because the code which links the two components, TrackBar1Change, it compiled into the frame's unit, it is automatically shared by every instance of the frame you create, without any need to "clone" the code.

Hiding items in TListBox while filtering by String

Short Version: Is there any way to control or modify LisBox items individually? for example set their Visible property to False separately.
I found a TListBoxItem class in Fire Monkey when I was searching, but I don't want to use Fire Monkey and want it in VCL.
Detailed Version:
I tried to filter my ListBox using two TStringList and an Edit, one StringList is global to keep the original list (list_files_global) and another StringList to help filtering procedure (list_files_filter) and my primary list of files is my ListBox (list_files).
I created my global StringList on onCreate event while program is starting to store my original list:
procedure Tfrm_main.FormCreate(Sender: TObject);
Begin
list_files_global := TStringList.Create;
list_files_global.Assign(list_files.Items);
End;
and used Edit's onChange event for filtering:
procedure Tfrm_main.edit_files_filterChange(Sender: TObject);
Var
list_files_filter: TStringList;
i: Integer;
Begin
list_files_filter := TStringList.Create;
list_files_filter.Assign(list_files.Items);
list_files.Clear;
for i := 0 to list_files_filter.Count - 1 do
if pos(edit_files_filter.text, list_files_filter[i]) > 0 then
list_files.Items.Add(list_files_filter[i]);
End;
and for switching off the filter, just recover the list from my global list that I created at first:
list_files.Items := list_files_global;
here so far, everything works just fine, but problem is when I'm trying to edit/rename/delete items from filtered list, for example I change an item:
list_files.Items[i] := '-- Changed Item --';
list will be edited, but when I switch off the filter, the original list will be back and all changes are lost.
so I want to know is there any proper way to solve this problem? Something like hiding items individually or change items visibility, etc... so I can change the filtering algorithm and get rid of all this making extra lists.
I searched the internet and looked into Delphi's help file for a whole day and nothing useful came up.
The items of a VCL listbox, List Box in the API, does not have any visibility property. The only option for not showing an item is to delete it.
You can use the control in virtual mode however, where there are no items at all. You decide what data to keep, what to display. That's LBS_NODATA window style in the API. In VCL, set the style property to lbVirtual.
Extremely simplified example follows.
Let's keep an array of records, one record per virtual item.
type
TListItem = record
FileName: string;
Visible: Boolean;
end;
TListItems = array of TListItem;
You can extend the fields as per your requirements. Visibility is one of the main concerns in the question, I added that. You'd probably add something that represents the original name so that you know what name have been changed, etc..
Have one array per listbox. This example contains one listbox.
var
ListItems: TListItems;
Better make it a field though, this is for demonstration only.
Required units.
uses
ioutils, types;
Some initialization at form creation. Empty the filter edit. Set listbox style accordingly. Fill up some file names. All items will be visible at startup.
procedure TForm1.FormCreate(Sender: TObject);
var
ListFiles: TStringDynArray;
i: Integer;
begin
ListFiles := ioutils.TDirectory.GetFiles(TDirectory.GetCurrentDirectory);
SetLength(ListItems, Length(ListFiles));
for i := 0 to High(ListItems) do begin
ListItems[i].FileName := ListFiles[i];
ListItems[i].Visible := True;
end;
ListBox1.Style := lbVirtual;
ListBox1.Count := Length(ListFiles);
Edit1.Text := '';
end;
In virtual mode the listbox is only interested in the Count property. That will arrange how many items will show, accordingly the scrollable area.
Here's the filter part, this is case sensitive.
procedure TForm1.Edit1Change(Sender: TObject);
var
Text: string;
Cnt: Integer;
i: Integer;
begin
Text := Edit1.Text;
if Text = '' then begin
for i := 0 to High(ListItems) do
ListItems[i].Visible := True;
Cnt := Length(ListItems);
end else begin
Cnt := 0;
for i := 0 to High(ListItems) do begin
ListItems[i].Visible := Pos(Text, ListItems[i].FileName) > 0;
if ListItems[i].Visible then
Inc(Cnt);
end;
end;
ListBox1.Count := Cnt;
end;
The special case in the edit's OnChange is that when the text is empty. Then all items will show. Otherwise code is from the question. Here we also keep the total number of visible items, so that we can update the listbox accordingly.
Now the only interesting part, listbox demands data.
procedure TForm1.ListBox1Data(Control: TWinControl; Index: Integer;
var Data: string);
var
VisibleIndex: Integer;
i: Integer;
begin
VisibleIndex := -1;
for i := 0 to High(ListItems) do begin
if ListItems[i].Visible then
Inc(VisibleIndex);
if VisibleIndex = Index then begin
Data := ListItems[i].FileName;
Break;
end;
end;
end;
What happens here is that the listbox requires an item to show providing its index. We loop through the master list counting visible items to find out which one matches that index, and supply its text.
This is something I often do, but with list views instead of list boxes. The basic principles are the same, though.
I tend to store the individual items as objects, which are reference types in Delphi. And I keep them all in one main unfiltered list, which owns the objects, while I maintain a filtered list (which does not own the objects) for display purposes. Like #Sertac, I combine this with a virtual list view.
To see how this works in practice, create a new VCL application and drop a list view (lvDisplay) and an edit control (eFilter) on the main form:
Notice I have added three columns to the list view control: "Name", "Age", and "Colour". I also make it virtual (OwnerData = True).
Now define the class for the individual data items:
type
TDogInfo = class
Name: string;
Age: Integer;
Color: string;
constructor Create(const AName: string; AAge: Integer; const AColor: string);
function Matches(const AText: string): Boolean;
end;
where
{ TDogInfo }
constructor TDogInfo.Create(const AName: string; AAge: Integer;
const AColor: string);
begin
Name := AName;
Age := AAge;
Color := AColor;
end;
function TDogInfo.Matches(const AText: string): Boolean;
begin
Result := ContainsText(Name, AText) or ContainsText(Age.ToString, AText) or
ContainsText(Color, AText);
end;
And let us create the unfiltered list of dogs:
TForm1 = class(TForm)
eFilter: TEdit;
lvDisplay: TListView;
procedure FormCreate(Sender: TObject);
procedure FormDestroy(Sender: TObject);
private
FList, FFilteredList: TObjectList<TDogInfo>;
public
end;
where
function GetRandomDogName: string;
const
DogNames: array[0..5] of string = ('Buster', 'Fido', 'Pluto', 'Spot', 'Bill', 'Rover');
begin
Result := DogNames[Random(Length(DogNames))];
end;
function GetRandomDogColor: string;
const
DogColors: array[0..2] of string = ('Brown', 'Grey', 'Black');
begin
Result := DogColors[Random(Length(DogColors))];
end;
procedure TForm1.FormCreate(Sender: TObject);
var
i: Integer;
begin
FList := TObjectList<TDogInfo>.Create(True); // Owns the objects
// Populate with sample data
for i := 1 to 1000 do
FList.Add(
TDogInfo.Create(GetRandomDogName, Random(15), GetRandomDogColor)
);
FFilteredList := FList;
lvDisplay.Items.Count := FFilteredList.Count;
lvDisplay.Invalidate;
end;
procedure TForm1.FormDestroy(Sender: TObject);
begin
if FFilteredList <> FList then
FreeAndNil(FFilteredList);
FreeAndNil(FList);
end;
The idea is that the list view control always displays the FFilteredList, which either points to the same object instance as FList, or points to a filtered (or sorted) version of it:
// The list view's OnData event handler
procedure TForm1.lvDisplayData(Sender: TObject; Item: TListItem);
begin
if FFilteredList = nil then
Exit;
if not InRange(Item.Index, 0, FFilteredList.Count - 1) then
Exit;
Item.Caption := FFilteredList[Item.Index].Name;
Item.SubItems.Add(FFilteredList[Item.Index].Age.ToString);
Item.SubItems.Add(FFilteredList[Item.Index].Color);
end;
// The edit control's OnChange handler
procedure TForm1.eFilterChange(Sender: TObject);
var
i: Integer;
begin
if string(eFilter.Text).IsEmpty then // no filter, display all items
begin
if FFilteredList <> FList then
begin
FreeAndNil(FFilteredList);
FFilteredList := FList;
end;
end
else
begin
if (FFilteredList = nil) or (FFilteredList = FList) then
FFilteredList := TObjectList<TDogInfo>.Create(False); // doesn't own the objects
FFilteredList.Clear;
for i := 0 to FList.Count - 1 do
if FList[i].Matches(eFilter.Text) then
FFilteredList.Add(FList[i]);
end;
lvDisplay.Items.Count := FFilteredList.Count;
lvDisplay.Invalidate;
end;
The result:
Notice that there always is only one in-memory object for each dog, so if you rename a dog, the changes will reflect in the list view, filtered or not. (But don't forget to invalidate it!)

Recursive procedure for getting child records in hierarchical dataset

I have a data table (MDTasks) that has records represented in a tree view. Each record has a unique field, 'ID', and a field 'Parent_ID', which refers to the ID of the parent. I am trying to make a list of all the children at any level of a record with a given ID. I have the following, which gets the first child at each level, but doesn't go back up to get the siblings at any level. I would be grateful for any help. Thank you.
procedure TfmList.GetChildren(TaskID: integer);
var
iChildID: integer;
begin
with MDTasks do
begin
first;
while not EOF do
begin
if FieldByName('Parent_ID').AsInteger = TaskID then
begin
iChildID := FieldByName('ID').AsInteger;
Memo1.Lines.Add(IntToStr(iChildID));
GetChildren(iChildID);
end;
next;
end;
end;
end;
The code below should do what you want, if I understand you correctly. It uses a ClientDataSet so that my answer is self contained and so that the test data can easily be set up.
It uses a call to CloneCursor to do the recursive search for the children of the specified parent node ID.
procedure TForm1.FormCreate(Sender: TObject);
begin
CDS1.CreateDataSet;
CDS1.InsertRecord([1, -1]);
CDS1.InsertRecord([2, -1]);
CDS1.InsertRecord([3, -1]);
CDS1.InsertRecord([4, 2]); // This and the following rows are all children of ID = 2
CDS1.InsertRecord([5, 2]);
CDS1.InsertRecord([6, 4]);
CDS1.InsertRecord([7, 4]);
CDS1.InsertRecord([8, 7]);
FindChildren(2);
end;
procedure TForm1.FindChildren(ParentID : Integer);
procedure FindChildrenInner(ParentID : Integer);
var
TempCDS : TClientDataSet;
ID : Integer;
begin
TempCDS := TClientDataSet.Create(Nil);
try
TempCDS.CloneCursor(CDS1, False, True);
TempCDS.First;
while not TempCDS.Eof do begin
if TempCDS.FieldByName('Parent_ID').AsInteger = ParentID then begin
ID := TempCDS.FieldByName('ID').AsInteger;
Memo1.Lines.Add(Format('ID: %d, Parent: %d', [ID, ParentID]));
FindChildrenInner(ID);
end;
TempCDS.Next;
end;
finally
TempCDS.Free;
end;
end;
begin
Memo1.Lines.BeginUpdate;
try
Memo1.Lines.Clear;
Assert(CDS1.Locate('ID', ParentID, []));
FindChildrenInner(ParentID);
finally
Memo1.Lines.EndUpdate;
end;
end;
Looking at the code, I would say that it gets all the childs at the last level (not only first one). The problem is that you "share" the dataset (MDTasks) between levels - so when the last level iterates to the end of the table and returns to the caller the while not EOF do evaluates to false (on the parent level now) and loop ends, thus only the first child is gotten on levels above the last one.
One solution would be to iterate each level fully, logging Parent_IDs into local array, then use that array to get the next level below it.
Another solution is probably to use bookmarks in the table MDTasks. Let your procedure set a bookmark at the beginning and return to this bookmark at the end so the position/cursor in MDTasks is not changed from GetChildren.
your problem is that you are calling recursive function against the same data. you can use filtering, for example:
procedure GoGoGo (Id: integer);
var
OldFilter: string;
begin
OldFilter := Table.Filter;
Table.Filter := 'ParentId = ' + IntToStr (Id);
try
Table.First;
while not Table.Eof do begin
Memo.Lines.Add (Format('ID: %d, Parent: %d',
[Table.FieldByName ('I'd).AsInteger,
Table.FieldByName ('ParentID').AsInteger]));
GoGoGo (Table.FieldByName ('Id').AsInteger);
if Table.Locate ('Id', Id, []) then
Table.Next;
end;
finally
Table.Filter := OldFilter;
end;
end;
<...>
Memo.Lines.Clear;
Table.Filtered := TRUE;
Table.First;
GoGoGo (Table.FieldByName ('Id').AsInteger);
haven't tested this code, it may contain errors, but hopefully, idea is quite clear. with the same idea you can use another approaches, like create new TQuery with dynamically generated where clause, etc. I'm not a fan of such things just because in a really big tree, creating new objects in every iteration of recursive function may result in a stack overflow.
gl there ;)

VirtualStringTree - Can't get parent-child nodes to work correctly

I need to render a Main-Menu in a TVirtualStringTree - each menu item has a Category. The Categories will make up the root nodes of the tree, and under each Category root node, will be the menu items.
The datasets fields for the Categories and MenuItems look like this:
My code in OnInitNode scrolls through the Category dataset's records and loads the menu items for each Category as child nodes. However I've got something wrong (see image) and the Category nodes are all the same text - which means that the dataset is not scrolling to the next record.
It seems that this line of code in the InitNode event is causing it to exit the loop and seems to be the cause of problem:
Sender.ChildCount[Node] := x;
But then what is the proper way to render the child nodes?
This is my code:
type
TTreeCategoryData = record
ID: Integer;
DispText: String;
end;
PTreeCategoryData = ^TTreeCategoryData;
TTreeMenuItemData = record
ID: Integer;
CategoryID: Integer;
DispText: String;
ClassName: String;
end;
PTreeMenuItemData = ^TTreeMenuItemData;
Tvstmainmenu_CategoryNodeData = record
TreeCategoryData: PTreeCategoryData;
end;
Pvstmainmenu_CategoryNodeData = ^Tvstmainmenu_CategoryNodeData;
Tvstmainmenu_MenuItemNodeNodeData = record
TreeMenuItemData: PTreeMenuItemData;
end;
Pvstmainmenu_MenuItemNodeNodeData = ^Tvstmainmenu_MenuItemNodeNodeData;
procedure TfmMain.FormShow(Sender: TObject);
var
x: Integer;
begin
datamod.uspspmenucatgy_S.PrepareSQL(True);
datamod.uspspmenucatgy_S.Open;
x := datamod.uspspmenucatgy_S.RecordCount;
vstmainmenu.RootNodeCount := x;
end;
procedure TfmMain.vstmainmenuFreeNode(Sender: TBaseVirtualTree; Node: PVirtualNode);
var
CategoryNodeData: Pvstmainmenu_CategoryNodeData;
MenuItemNodeNodeData: Pvstmainmenu_MenuItemNodeNodeData;
begin
if (Sender.GetNodeLevel(Node) = 0) then
begin
CategoryNodeData := Sender.GetNodeData(Node);
if Assigned(CategoryNodeData) and Assigned(CategoryNodeData.TreeCategoryData) then
begin
Dispose(CategoryNodeData.TreeCategoryData);
end;
end
else if (Sender.GetNodeLevel(Node) = 1) then
begin
MenuItemNodeNodeData := Sender.GetNodeData(Node);
if Assigned(MenuItemNodeNodeData) and Assigned(MenuItemNodeNodeData.TreeMenuItemData) then
begin
Dispose(MenuItemNodeNodeData.TreeMenuItemData);
end;
end;
end;
procedure TfmMain.vstmainmenuGetText(Sender: TBaseVirtualTree; Node: PVirtualNode; Column: TColumnIndex;
TextType: TVSTTextType; var CellText: string);
var
CategoryNodeData: Pvstmainmenu_CategoryNodeData;
MenuItemNodeNodeData: Pvstmainmenu_MenuItemNodeNodeData;
TreeCategoryData: PTreeCategoryData;
TreeMenuItemData: PTreeMenuItemData;
begin
if (Sender.GetNodeLevel(Node) = 0) then
begin
CategoryNodeData := Sender.GetNodeData(Node);
if Assigned(CategoryNodeData) and Assigned(CategoryNodeData.TreeCategoryData) then
begin
TreeCategoryData := CategoryNodeData.TreeCategoryData;
CellText := TreeCategoryData^.DispText;
end;
end
else if (Sender.GetNodeLevel(Node) = 1) then
begin
MenuItemNodeNodeData := Sender.GetNodeData(Node);
if Assigned(MenuItemNodeNodeData) and Assigned(MenuItemNodeNodeData.TreeMenuItemData) then
begin
TreeMenuItemData := MenuItemNodeNodeData.TreeMenuItemData;
CellText := TreeMenuItemData^.DispText;
end;
end;
end;
procedure TfmMain.vstmainmenuInitNode(Sender: TBaseVirtualTree; ParentNode, Node: PVirtualNode;
var InitialStates: TVirtualNodeInitStates);
var
CategoryNodeData: Pvstmainmenu_CategoryNodeData;
MenuItemNodeNodeData: Pvstmainmenu_MenuItemNodeNodeData;
x: Integer;
begin
if (Sender.GetNodeLevel(Node) = 0) then
begin
CategoryNodeData := Sender.GetNodeData(Node);
CategoryNodeData.TreeCategoryData := New(PTreeCategoryData);
with CategoryNodeData.TreeCategoryData^ do
begin
ID := datamod.uspspmenucatgy_Srow_id.AsInteger;
DispText := datamod.uspspmenucatgy_Scategory.AsString;
end;
// :Pcategory_id
datamod.uspspmenu_S.ParamByName('Pcategory_id').AsInteger := datamod.uspspmenucatgy_Srow_id.AsInteger;
datamod.uspspmenu_S.PrepareSQL(True);
if (datamod.uspspmenu_S.State = dsBrowse) then
datamod.uspspmenu_S.Refresh
else
datamod.uspspmenu_S.Open;
x := datamod.uspspmenu_S.RecordCount;
Sender.ChildCount[Node] := x;
datamod.uspspmenucatgy_S.Next;
end
else if (Sender.GetNodeLevel(Node) = 1) then
begin
MenuItemNodeNodeData := Sender.GetNodeData(Node);
MenuItemNodeNodeData.TreeMenuItemData := New(PTreeMenuItemData);
with MenuItemNodeNodeData.TreeMenuItemData^ do
begin
ID := datamod.uspspmenu_Srow_id.AsInteger;
CategoryID := datamod.uspspmenucatgy_Srow_id.AsInteger;
DispText := datamod.uspspmenu_Smenuitem.AsString;
ClassName := datamod.uspspmenu_Stframeclass.AsString;
end;
datamod.uspspmenu_S.Next;
end;
end;
Here is what is happening. Each root node (parent) should be different but it isn't. Further the child nodes for the 2nd root node should be different, but it seems to be stuck on the last child node of the 1st root node:
Thanks in advance!
Try some alternative approach like creating nodes in a separate procedure, e.g.:
procedure TfrmMain.LoadTree;
var
LTreeCategoryData: PTreeCategoryData;
LCategoryNode: PVirtualNode;
begin
datamod.uspspmenucatgy_S.PrepareSQL(True);
datamod.uspspmenucatgy_S.Open;
while not datamod.uspspmenucatgy_S.Eof do
begin
// 1. create parent node itself
LTreeCategoryData := New(PTreeCategoryData);
with LTreeCategoryData^ do
begin
ID := datamod.uspspmenucatgy_Srow_id.AsInteger;
DispText := datamod.uspspmenucatgy_Scategory.AsString;
end;
LCategoryNode := vstmainmenu.AddChild(vstmainmenu.RootNode, LTreeCategoryData);
// 2. create child nodes
datamod.uspspmenu_S.ParamByName('Pcategory_id').AsInteger := datamod.uspspmenucatgy_Srow_id.AsInteger;
datamod.uspspmenu_S.PrepareSQL(True);
datamod.uspspmenu_S.Open;
while not datamod.uspspmenu_S.Eof do
begin
LTreeMenuItemData := New(PTreeMenuItemData);
with LTreeMenuItemData^ do
begin
ID := datamod.uspspmenu_Srow_id.AsInteger;
CategoryID := datamod.uspspmenucatgy_Srow_id.AsInteger;
DispText := datamod.uspspmenu_Smenuitem.AsString;
ClassName := datamod.uspspmenu_Stframeclass.AsString;
end;
vstmainmenu.AddChild(LCategoryNode, LTreeMenuItemData);
datamod.uspspmenu_S.Next;
end;
datamod.uspspmenu_S.Close;
datamod.uspspmenucatgy_S.Next;
end;
datamod.uspspmenucatgy_S.Close;
end;
Just call this new procedure, whenever you want to load the whole tree.
When you initialize a parent (category) node, you set the SQL, open the query, and assign the number of child nodes. You're assuming that those nodes will immediately be initialized, so the query will be traversed right away.
That's not how this tree control works. Children are initialized on demand, not in any defined order. It's possible that all the parent nodes get initialized before any children, which explains why many of the children have the same text — you discarded two of the queries with the third, so the remaining children kept reusing the last valid result from the most recent query.
Years ago, there was a data-aware version of the tree control; there might still be some such controls. They might be better suited to your purpose.
Otherwise, what you should do is use AddNode to add the nodes to the tree. Iterate over the query results and add a node for each record as you encounter it.
The category nodes probably all have the same titles for a similar reason. Let your query-processing code drive the adding of nodes, not vice versa.

"descending" records in delphi?

I know you can't actually descend anything from a record, but I'm not sure how to summarize my problem in one sentence. Edit the title if you do.
What I want to do here is make an array of some generic type, which can be one of X number of types, the array would be filled with those custom types (they have different fields and that's what is important). The easy way is to make just an array of variant records, each variant has it's own type, but obviously can't redeclare identifiers like so:
GenericRec = Record
case SubTypeName: TSubTypeName of
type1name: (SubRec: Type1);
type2name: (SubRec: Type2);
...
typeNname: (SubRec: TypeN);
end;
Changing SubRec to SubRec1, SubRec2... SubRecN makes referencing painful, but not impossible.
And since I started looking for alternative solutions to the above problem, classes came to mind.
The obvious example to demonstrate what I am trying to achieve is TObject, an array of those can be assigned to many different things. That's what I want, but with records (and that's impossible to do), because I want to be able to save the records to file as well as read them back (also because it's something I'm already familiar with). Making my own simple class is not a problem, making a descendant class from that to represent my subtype - I can do that. But what about writing that to file and reading it back? This boils down to serialization, which I have no idea how to do. From what I gather it's not as easy and the class must be descended from TComponent.
TMyClass = Class
Does it make any difference if I make the class like above? It's nothing fancy and has at most 10 fields, including a few custom types.
Setting serialization aside (just because I have a lot of reading to do on that topic), use of classes here also might be out of the question.
At this point, what are my options? Should I abandon records and try this with classes? Or would it be a lot less complicated just to stick to records and deal with the variant "limitation"? I'm all about learning and if exploding the class approach might make me smarter, I'll do it. I've also just looked into TList too (never used it), but it seems that it doesn't mix too well with records, well maybe it can be done, but that might be out of my league at the moment. I'm open to any kind of suggestions. What do i do?
You're conflating serialization with "writing everything to disk with a single BlockWrite call." You can serialize anything you want, regardless of whether it descends from TComponent or TPersistent.
Although writing everything with a single BlockWrite call looks convenient at first, you'll quickly find it's not really what you want if your desired record types are going to store anything particularly interesting (like strings, dynamic arrays, interfaces, objects, or other reference- or pointer-based types).
You'll probably also find variant records unsatisfying since you'll be coding to the lowest common denominator. You won't be able to access anything in the record without checking the actual contained type, and the size of even the smallest amount of data will occupy the same amount of space as the largest data type.
The question seems to describe polymorphism, so you may as well embrace what the language already provides for that. Use an array (or list, or any other container) of objects. Then you can use virtual methods to treat them all uniformly. You can implement dynamic dispatch for records if you want (e.g., give each record a function pointer that refers to a function that knows how to deal with that record's contained data type), but in the end you'll probably just find yourself reinventing classes.
The "natural" way of handling such data is to use a class, and not a record. It will be much easier to work with, both at definition time and when dealing with implementation: in particular, virtual methods are very powerful to customize a process for a particular kind of class. Then use a TList/TObjectList or a TCollection, or a generic-based array in newer versions of Delphi to store the list.
About serialization, there are several ways to do it. See Delphi: Store data in somekind of structure
In your particular case, the difficulty comes from the "variant" kind of record you are using. IMHO the main drawback is that the compiler will refuse to set any reference-counted kind of variable (e.g. a string) within the "variant" part. So you'll be able to write only "plain" variables (like integer) within this "variant" part. A big limitation IMHO, which reduces the interest of this solution.
Another possibility could be to store the kind of record at the beginning of its definition, e.g. with a RecType: integer or even better with a RecType: TEnumerationType which will be more explicit than a number. But you'll have to write a lot of code by hand, and works with pointers, which is a bit error-prone if you are not very fluent with pointer coding.
So you can also store the type information of the record, accessible via TypeInfo(aRecordVariable). Then you can use FillChar to initialize the record content to zero, just after allocation, then use the following function to finalize the record content, just after disallocation (this is what Dispose() does internally, and you shall call it, otherwise you'll leak memory):
procedure RecordClear(var Dest; TypeInfo: pointer);
asm
jmp System.#FinalizeRecord
end;
But such an implementation pattern will just reinvent the wheel! It is in fact how class is implemented: the first element of any TObject instance is a pointer to its ClassType:
function TObject.ClassType: TClass;
begin
Pointer(Result) := PPointer(Self)^;
end;
There is also another structure in Delphi, which is called object. It is some kind of record, but it supports inheritance - see this article. It is the old style of OOP programming in Turbo Pascal 5.5 days, deprecated, but still available. Note that I discovered a weird compilation issue on newer versions of Delphi: sometimes, an object allocated on the stack is not always initialized.
Take a look at our TDynArray wrapper and its associated functions, who is able to serialize any record content, into binary or JSON. See Delphi (win32) serialization libraries question. It will work with variant records, even if they include a string in their unvariant part, whereas a plain "Write/BlockWrite" won't work with reference counted fields.
To do this with records, you would create different record types that have a common field(s) in front, and then put those same field(s) in the generic record. Then you can simply type-cast a pointer to a generic record to a pointer to a specific record when needed. For example:
type
PGenericRec = ^GenericRec;
GenericRec = Record
RecType: Integer;
end;
PType1Rec = ^Type1Rec;
Type1Rec = Record
RecType: Integer;
// Type1Rec specific fields...
end;
PType2Rec = ^Type2Rec;
Type2Rec = Record
RecType: Integer;
// Type2Rec specific fields...
end;
PTypeNRec = ^TypeNRec;
TypeNRec = Record
RecType: Integer;
// TypeNRec specific fields...
end;
var
Recs: array of PGenericRec;
Rec1: PType1Rec;
Rec2: PType2Rec;
RecN: PTypeNRec;
I: Integer;
begin
SetLength(Recs, 3);
New(Rec1);
Rec1^.RecType := RecTypeForType1Rec;
// fill Rec1 fields ...
Recs[0] := PGenericRec(Rec1);
New(Rec2);
Rec2^.RecType := RecTypeForType2Rec;
// fill Rec2 fields ...
Recs[1] := PGenericRec(Rec2);
New(RecN);
Rec3^.RecType := RecTypeForTypeNRec;
// fill RecN fields ...
Recs[2] := PGenericRec(RecN);
for I := 0 to 2 do
begin
case Recs[I]^.RecType of
RecTypeForType1Rec: begin
Rec1 := PType1Rec(Recs[I]);
// use Rec1 as needed...
end;
RecTypeForType1Re2: begin
Rec2 := PType2Rec(Recs[I]);
// use Rec2 as needed...
end;
RecTypeForTypeNRec: begin
RecN := PTypeNRec(Recs[I]);
// use RecN as needed...
end;
end;
end;
for I := 0 to 2 do
begin
case Recs[I]^.RecType of
RecTypeForType1Rec: Dispose(PType1Rec(Recs[I]));
RecTypeForType2Rec: Dispose(PType2Rec(Recs[I]));
RecTypeForTypeNRec: Dispose(PTypeNRec(Recs[I]));
end;
end;
end;
As for serialization, you do not need TComponent for that. You can serialize records, you just have to do it manually. For writing, write out the RecType value first, then write out the record-specific values next. For reading, read the RecType value first, then create the appropriate record type for that value, then read the record-specific values into it.:
interface
type
PGenericRec = ^GenericRec;
GenericRec = Record
RecType: Integer;
end;
NewRecProc = procedure(var Rec: PGenericRec);
DisposeRecProc = procedure(Rec: PGenericRec);
ReadRecProc = procedure(Rec: PGenericRec);
WriteRecProc = procedure(const Rec: PGenericRec);
function NewRec(ARecType: Integer): PGenericRec;
procedure DisposeRec(var Rec: PGenericRec);
procedure ReadRec(Rec: PGenericRec);
procedure WriteRec(const Rec: PGenericRec);
procedure RegisterRecType(ARecType: Integer; ANewProc: NewRecProc; ADisposeProc: DisposeRecProc; AReadproc: ReadRecFunc; AWriteProc: WriteRecProc);
implementation
type
TRecTypeReg = record
RecType: Integer;
NewProc: NewRecProc;
DisposeProc: DisposeRecProc;
ReadProc: ReadRecProc;
WriteProc: WriteRecProc;
end;
var
RecTypes: array of TRecTypeReg;
function NewRec(ARecType: Integer): PGenericRec;
var
I: Integer;
begin
Result := nil;
for I = Low(RecTypes) to High(RecTypes) do
begin
with RecTypes[I] do
begin
if RecType = ARecType then
begin
NewProc(Result);
Exit;
end;
end;
end;
raise Exception.Create('RecType not registered');
end;
procedure DisposeRec(var Rec: PGenericRec);
var
I: Integer;
begin
for I = Low(RecTypes) to High(RecTypes) do
begin
with RecTypes[I] do
begin
if RecType = Rec^.RecType then
begin
DisposeProc(Rec);
Rec := nil;
Exit;
end;
end;
end;
raise Exception.Create('RecType not registered');
end;
procedure ReadRec(var Rec: PGenericRec);
var
LRecType: Integer;
I: Integer;
begin
Rec := nil;
LRecType := ReadInteger;
for I = Low(RecTypes) to High(RecTypes) do
begin
with RecTypes[I] do
begin
if RecType = LRecType then
begin
NewProc(Rec);
try
ReadProc(Rec);
except
DisposeProc(Rec);
raise;
end;
Exit;
end;
end;
end;
raise Exception.Create('RecType not registered');
end;
procedure WriteRec(const Rec: PGenericRec);
var
I: Integer;
begin
for I = Low(RecTypes) to High(RecTypes) do
begin
with RecTypes[I] do
begin
if RecType = Rec^.RecType then
begin
WriteInteger(Rec^.RecType);
WriteProc(Rec);
Exit;
end;
end;
end;
raise Exception.Create('RecType not registered');
end;
procedure RegisterRecType(ARecType: Integer; ANewProc: NewRecProc; ADisposeProc: DisposeRecProc; AReadproc: ReadRecFunc; AWriteProc: WriteRecProc);
begin
SetLength(RecTypes, Length(RecTypes)+1);
with RecTypes[High(RecTypes)] do
begin
RecType := ARecType;
NewProc := ANewProc;
DisposeProc := ADisposeProc;
ReadProc := AReadProc;
WriteProc := AWriteProc;
end;
end;
end.
.
type
PType1Rec = ^Type1Rec;
Type1Rec = Record
RecType: Integer;
Value: Integer;
end;
procedure NewRec1(var Rec: PGenericRec);
var
Rec1: PType1Rec;
begin
New(Rec1);
Rec1^.RecType := RecTypeForType1Rec;
Rec := PGenericRec(Rec1);
end;
procedure DisposeRec1(Rec: PGenericRec);
begin
Dispose(PType1Rec(Rec));
end;
procedure ReadRec1(Rec: PGenericRec);
begin
PType1Rec(Rec)^.Value := ReadInteger;
end;
procedure WriteRec1(const Rec: PGenericRec);
begin
WriteInteger(PType1Rec(Rec)^.Value);
end;
initialization
RegisterRecType(RecTypeForType1Rec, #NewRec1, #DisposeRec1, #ReadRec1, #WriteRec1);
.
type
PType2Rec = ^Type2Rec;
Type2Rec = Record
RecType: Integer;
Value: Boolean;
end;
procedure NewRec2(var Rec: PGenericRec);
var
Rec2: PType2Rec;
begin
New(Rec2);
Rec2^.RecType := RecTypeForType2Rec;
Rec := PGenericRec(Rec2);
end;
procedure DisposeRec2(Rec: PGenericRec);
begin
Dispose(PType2Rec(Rec));
end;
procedure ReadRec2(Rec: PGenericRec);
begin
PType2Rec(Rec)^.Value := ReadBoolean;
end;
procedure WriteRec2(const Rec: PGenericRec);
begin
WriteBoolean(PType2Rec(Rec)^.Value);
end;
initialization
RegisterRecType(RecTypeForType2Rec, #NewRec2, #DisposeRec2, #ReadRec2, #WriteRec2);
.
type
PTypeNRec = ^Type2Rec;
TypeNRec = Record
RecType: Integer;
Value: String;
end;
procedure NewRecN(var Rec: PGenericRec);
var
RecN: PTypeNRec;
begin
New(RecN);
RecN^.RecType := RecTypeForTypeNRec;
Rec := PGenericRec(RecN);
end;
procedure DisposeRecN(Rec: PGenericRec);
begin
Dispose(PTypeNRec(Rec));
end;
procedure ReadRecN(Rec: PGenericRec);
begin
PTypeNRec(Rec)^.Value := ReadString;
end;
procedure WriteRecN(const Rec: PGenericRec);
begin
WriteString(PTypeNRec(Rec)^.Value);
end;
initialization
RegisterRecType(RecTypeForTypeNRec, #NewRecN, #DisposeRecN, #ReadRecN, #WriteRecN);
.
var
Recs: array of PGenericRec;
procedure CreateRecs;
begin
SetLength(Recs, 3);
NewRec1(Recs[0]);
PRecType1(Recs[0])^.Value : ...;
NewRec2(Recs[1]);
PRecType2(Recs[1])^.Value : ...;
NewRecN(Recs[2]);
PRecTypeN(Recs[2])^.Value : ...;
end;
procedure DisposeRecs;
begin
for I := 0 to High(Recs) do
DisposeRec(Recs[I]);
SetLength(Recs, 0);
end;
procedure SaveRecs;
var
I: Integer;
begin
WriteInteger(Length(Recs));
for I := 0 to High(Recs) do
WriteRec(Recs[I]);
end;
procedure LoadRecs;
var
I: Integer;
begin
DisposeRecs;
SetLength(Recs, ReadInteger);
for I := 0 to High(Recs) do
ReadRec(Recs[I]);
end;

Resources