How can I save and load my svTree data structure? - delphi

I am trying to implement a simple contact manager using the VirtualStringTree component. I have it set up to look like a list-view component with only three columns that will all contain text:
For the data structure, I am using svTree by Linas, which was mentioned in another Stack Overflow question.
I have declared a record like this:
type
TMainData = record
Name, Email, Password: string;
end;
In the form's OnCreate I have this:
procedure TForm1.FormCreate(Sender: TObject);
begin
MyTree := TSVTree<TMainData>.Create(False);
MyTree.VirtualTree := vst1;
end;
I am adding the data from TEdits like this:
procedure TForm1.BuildStructure;
var
svNode: TSVTreeNode<TMainData>;
Data: TMainData;
begin
MyTree.BeginUpdate;
try
Data.Name := edtname.Text;
Data.Email := edtEmail.Text;
Data.Password := edtPassword.Text;
svNode := MyTree.AddChild(nil, Data);
finally
MyTree.EndUpdate;
end;
Label1.Caption := 'Count: '+IntToStr(MyTree.TotalCount);
end;
How can I save this into a stream or a file to be loaded back? I have tried using MyTree.SaveToFile('C:/Test.dat') and MyTree.LoadFromFile('C:/Test.dat'), but when it's loaded back the tree view contains no data, only a blank row.

You need to set OnLoadNode and OnSaveNode procedures for your TSVTree and implement your logic here. You can look at Project2 in the Demos folder. E.g.:
uses
uSvHelpers;
MyTree.OnSaveNode := DoSave;
MyTree.OnLoadNode := DoLoad;
procedure TForm1.DoLoad(Sender: TSVTree<TMainData>; Node: TSVTreeNode<TMainData>; Stream: TStream);
var
obj: TMainData;
begin
//
if Assigned(Node) then
begin
//read from stream
//read in correct order
obj.Name := Stream.AsString;
obj.Email := Stream.AsString;
obj.Password := Stream.AsString;
Node.FValue := obj;
end;
end;
procedure TForm1.DoSave(Sender: TSVTree<TMainData>; Node: TSVTreeNode<TMainData>; Stream: TStream);
begin
if Assigned(Node) then
begin
//read from stream
Stream.WriteString(Node.FValue.Name);
Stream.WriteString(Node.FValue.Email);
Stream.WriteString(Node.FValue.Password);
end;
end;
After that you can just call MyTree.SaveToFile('C:/Test.dat') or MyTree.LoadFromFile('C:/Test.dat'). In my demo and this example i've used another unit (uSvHelpers) which implements TStream helper for more OO stream support. You can of course use your own way to write your data information to stream.

Looks like you need to implement the OnSaveNode and OnLoadNode events:
procedure TForm.VTLoadNode(Sender: TBaseVirtualTree;
Node: PVirtualNode; Stream: TStream);
begin
// Load Node Data record from the stream
end;
procedure TForm.VTSaveNode(Sender: TBaseVirtualTree;
Node: PVirtualNode; Stream: TStream);
begin
// Save Node Data record to the stream
end;

Related

How to make copy of a treeview nodes to another treeview

How do I take the data from one populated treeview and put it into another existing treeview.
All the nodes and children need to be copied.
I think you can use a stream to save and load tree content. I don't have a running environment right now to test an example but, your code could looks like :
var
MS: TMemoryStream;
begin
MS := TMemoryStream.Create;
try
Tree1.SaveToStream(MS);
MS.Position := 0;
Tree2.LoadFromStream(MS);
finally
MS.Free;
end;
end;
TTreeNode implements the TPersistent.Assign() method, so one option would be to write a recursive function that iterates the source TreeView adding-and-assigning nodes to the target TreeView. For example:
procedure CopyNodes(SrcTree, DstTree: TTreeView);
var
DstNodes: TTreeNodes;
SrcRootNode: TTreeNode;
procedure DoCopyNodes(SrcNode, Relative: TTreeNode; AddMode: TNodeAttachMode);
var
DstNode: TTreeNode;
begin
while SrcNode <> nil do
begin
DstNode := DstNodes.AddNode(nil, Relative, '', nil, AddMode);
try
DstNode.Assign(SrcNode);
DoCopyNodes(SrcNode.GetFirstChild, DstNode, naAddChild);
except
DstNode.Delete;
raise;
end;
SrcNode := SrcNode.GetNextSibling;
end;
end;
begin
SrcRootNode := SrcTree.Items.GetFirstNode;
if SrcRootNode <> nil then
begin
DstNodes := DstTree.Items;
DstNodes.BeginUpdate;
try
DoCopyNodes(SrcRootNode, nil, naAdd);
finally
DstNodes.EndUpdate;
end;
end;
end;
CopyNodes(TreeView1, TreeView2);

Save/Load objects as blob in a database

Whats wrong with my code? I try to save and then load again an object to a blob field in a database, but get nothing back.
Records are saved, but I can't say if the data was written correctly because I cant read the data back.
Here is the object type:
TMyObject = class
Name: string;
end;
And here I try to save:
procedure TForm1.btnSaveObjectClick(Sender: TObject);
var
myObject: TmyObject;
aMemoryStream: TMemoryStream;
begin
myObject:= TMyObject.Create;
myObject.Name:=edtName.Text;
aMemoryStream:= TMemoryStream.Create;
aMemoryStream.Write(myObject, myObject.InstanceSize);
aMemoryStream.Position:=0;
with TSQLQuery.Create(nil) do
begin
DataBase:=Conn;
SQL.Text:='INSERT INTO testtable (data) VALUES (:data)';
try
ParamByName('data').LoadFromStream(aMemoryStream, ftBlob);
ExecSQL;
TX.CommitRetaining;
finally
aMemoryStream.Free;
myObject.Free;
Free;
end;
end;
end;
Trying to read the data back up again:
procedure TForm1.btnLoadObjectClick(Sender: TObject);
var
myObject: TMyObject;
BlobStream : TStream;
begin
with TSQLQuery.Create(nil) do
begin
DataBase:=Conn;
SQL.Text:='SELECT data FROM testtable';
myObject:= TmyObject.Create;
try
Open;
Last;
BlobStream:= CreateBlobStream(FieldByName('data'), bmread);
BlobStream.Position:=0;
BlobStream.Read(myObject, BlobStream.Size);
ShowMessage('Stored Name: ' +myObject.Name);
finally
myObject.Free;
Free;
end;
end;
end;
Also, should BlobStream be free'd?
The correct way for storing your objects into files, streams or blob fields is to first extend your object with additional methods for loading and saving data from your objects fields (objects internal variables) into single memory block.
You do this by saving one field after another.
If your objects are dynamically sized (containing dynamic arrays or strings) don't forget to store the size of these separately so you will know how much data belongs to them when loading your objects later on.
Also if your objects contain some other objects you also need them to have similar methods for storing and loading their data.
The implementation depends heavily on your object's class design. Here a code example for a string field:
type
TMyObject = class
public
Name: string;
procedure SaveToStream(AStream: TStream);
procedure LoadFromStream(AStream: TStream);
end;
procedure TMyObject.SaveToStream(AStream: TStream);
var
Len: Integer;
begin
Len := Length(Name);
AStream.Write(Len, SizeOf(Len));
AStream.Write(PChar(Name)^, Len);
end;
procedure TMyObject.LoadFromStream(AStream: TStream);
var
Len: Integer;
begin
AStream.Read(Len, SizeOf(Len));
SetString(Name, PChar(nil), Len);
AStream.Read(PChar(Name)^, Len);
end;
With this, it is possible to use the stream that CreateBlobStream returns and just save myObject to the blobfield:
BlobField := FieldByName('data') as TBlobField;
Stream := CreateBlobStream(BlobField, bmWrite);
myObject.SaveToStream(Stream);
..or load it from the stream:
Stream:= CreateBlobStream(FieldByName('data'), bmread);
myObject.LoadFromStream(Stream);

How to read data from xml file and display it over the text box in delphi language

I am new to the delphi language, and here I have a doubt, I have a xml file called vehicle.xml.
It looks like this
<data>
<vehicle>
<type>Car</type>
<model>2005</model>
<number>1568</number>
</vehicle>
<vehicle>
<type>Car</type>
<model>2009</model>
<number>1598</number>
</vehicle>
</data>
My Delphi form contains three text boxes:
txtType
txtModel
txtnumber
While loading the page I want to display the contents of the vehicle.xml over the text box like:
txtType=Car
txtModel=2005
txtNumber="1568"
Have a look at Delphi's own TXMLDocument component, for example:
uses
..., XMLIntf, XMLDoc;
procedure TForm1.FormCreate(Sender: TObject);
var
Vehicle: IXMLNode;
begin
XMLDocument1.FileName :='vehicle.xml';
XMLDocument1.Active := True;
try
Vehicle := XMLDocument.DocumentElement;
txtType.Text := Vehicle.ChildNodes['type'].Text;
txtModel.Text := Vehicle.ChildNodes['model'].Text;
txtnumber.Text := Vehicle.ChildNodes['number'].Text;
finally
XMLDocument1.Active := False;
end;
end;
Alternatively, use the IXMLDocument interface directly (which TXMLDocument implements):
uses
..., XMLIntf, XMLDoc;
procedure TForm1.FormCreate(Sender: TObject);
var
Doc: IXMLDocument;
Vehicle: IXMLNode;
begin
Doc := LoadXMLDocument('vehicle.xml');
Vehicle := Doc.DocumentElement;
txtType.Text := Vehicle.ChildNodes['type'].Text;
txtModel.Text := Vehicle.ChildNodes['model'].Text;
txtnumber.Text := Vehicle.ChildNodes['number'].Text;
end;
Update: the XML in the question has been altered to now wrap the vehicle element inside of a data element, and to have multiple vehicle elements. So the code above has to be adjusted accordingly, eg:
uses
..., XMLIntf, XMLDoc;
procedure TForm1.FormCreate(Sender: TObject);
var
Doc: IXMLDocument;
Data: IXMLNode;
Node: IXMLNode;
I: Integer;
begin
Doc := LoadXMLDocument('vehicle.xml');
Data := Doc.DocumentElement;
for I := 0 to Data.ChildNodes.Count-1 do
begin
Node := Data.ChildNodes[I];
// if all of the child nodes will always be 'vehicle' only
// then this check can be removed...
if Node.LocalName = 'vehicle' then
begin
// use Node.ChildNodes['type'], Node.ChildNodes['model'],
// and Node.ChildNodes['number'] as needed...
end;
end;
end;
You can read the XML file using the unit MSXML (or any other XML parser).
It gives you a tree structure representing the XML file. Where vehicle is the top node and the other three are the child nodes.
Each node has a text property that can be used to get the value. You can assign that to the text boxes on your form.
Code sample:
uses
ActiveX,
MSXML;
procedure TForm1.ReadFromXML(const AFilename: string);
var
doc : IXMLDOMDocument;
node : IXMLDomNode;
begin
CoInitialize; // Needs to be called once before using CoDOMDocument.Create;
if not FileExists(AFileName) then
Exit; // Add proper Error handling.
doc := CoDOMDocument.Create;
doc.load(AFileName);
if (doc.documentElement = nil) or (doc.documentElement.nodeName <> 'vehicle') then
Exit; // Add proper Error handling.
node := doc.documentElement.firstChild;
while node<>nil do begin
if node.nodeName = 'model' then
txtModel.Text := node.text;
if node.nodeName = 'number' then
txtNumber.Text := node.text;
if node.nodeName = 'type' then
txtType.Text := node.text;
node := node.nextSibling;
end;
end;
smses count="1" backup_set="b8ea1116-9614-41d0-ac5b-ef93c27089cd" backup_date="1605468953370" type="full">

Simple read/write record .dat file in Delphi

For some reason my OpenID account no longer exists even when I used it yesterday. But anyway.
I need to save record data into a .dat file. I tried a lot of searching, but it was all related to databases and BLOB things. I wasn't able to construct anything from it.
I have the following record
type
Scores = record
name: string[50];
score: integer;
end;
var rank: array[1..3] of scores;
I just need a simple way of saving and reading the record data from a .dat file. I had the book on how to do it, but that's at school.
You should also take a look at the file of-method.
This is kinda out-dated, but it's a nice way to learn how to work with files.
Since records with dynamic arrays (including ordinary strings) can't be stored to files with this method, unicode strings will not be supported. But string[50] is based on ShortStrings and your record is therefore already non-unicode...
Write to file
var
i: Integer;
myFile: File of TScores;
begin
AssignFile(myFile,'Rank.dat');
Rewrite(myFile);
try
for i := 1 to 3 do
Write(myFile, Rank[i]);
finally
CloseFile(myFile);
end;
end;
Read from file
var
i: Integer;
Scores: TScores;
myFile: File of TScores;
begin
AssignFile(myFile, 'Rank.dat');
Reset(myFile);
try
i := 1;
while not EOF(myFile) do
begin
Read(myFile, Scores);
Rank[i] := Scores; //You will get an error if i is out of the array bounds. I.e. more than 3
Inc(i);
end;
finally
CloseFile(myFile);
end;
end;
Use streams. Here is a simple demo (just demo - in practice there is no need to reopen file stream every time):
type
Scores = record
name: string[50];
score: integer;
end;
var rank: array[1..3] of scores;
procedure WriteScores(var Buf; Count: Integer);
var
Stream: TStream;
begin
Stream:= TFileStream.Create('test.dat', fmCreate);
try
Stream.WriteBuffer(Buf, SizeOf(Scores) * Count);
finally
Stream.Free;
end;
end;
procedure ReadScore(var Buf; Index: Integer);
var
Stream: TStream;
begin
Stream:= TFileStream.Create('test.dat', fmOpenRead or fmShareDenyWrite);
try
Stream.Position:= Index * SizeOf(Scores);
Stream.ReadBuffer(Buf, SizeOf(Scores));
finally
Stream.Free;
end;
end;
// write rank[1..3] to test.dat
procedure TForm1.Button1Click(Sender: TObject);
begin
rank[2].name:= '123';
WriteScores(rank, Length(Rank));
end;
// read rank[2] from test.dat
procedure TForm1.Button2Click(Sender: TObject);
begin
rank[2].name:= '';
ReadScore(rank[2], 2 - Low(rank));
ShowMessage(rank[2].name);
end;
Look in the help under "blockread" and or "blockwrite". There probably will be an example

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

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.

Resources