Recent

Author Topic: [SOLVED] Virtual String Tree  (Read 690 times)

Pe3s

  • Hero Member
  • *****
  • Posts: 533
[SOLVED] Virtual String Tree
« on: April 26, 2022, 10:08:55 pm »
Hello, when reading a VST, Acces Violation pops up?
What can it be caused by ?
Code: Pascal  [Select][+][-]
  1. unit Unit1;
  2.  
  3. {$mode objfpc}{$H+}
  4.  
  5. interface
  6.  
  7. uses
  8.   Classes, SysUtils, Forms, Controls, Graphics, Dialogs, StdCtrls, ExtCtrls,
  9.   VirtualTrees;
  10.  
  11. type
  12.   PData = ^TData;
  13.   TData = packed record
  14.     Imie: String;
  15.     Nazwisko: String;
  16.   end;
  17.  
  18.   { TForm1 }
  19.  
  20.   TForm1 = class(TForm)
  21.     Button1: TButton;
  22.     Button2: TButton;
  23.     Button3: TButton;
  24.     Button4: TButton;
  25.     Edit1: TEdit;
  26.     Edit2: TEdit;
  27.     Edit3: TEdit;
  28.     Label1: TLabel;
  29.     Label2: TLabel;
  30.     Label3: TLabel;
  31.     Panel1: TPanel;
  32.     VST: TVirtualStringTree;
  33.     procedure Button1Click(Sender: TObject);
  34.     procedure Button2Click(Sender: TObject);
  35.     procedure Button3Click(Sender: TObject);
  36.     procedure Button4Click(Sender: TObject);
  37.     procedure FormCreate(Sender: TObject);
  38.     procedure VSTGetText(Sender: TBaseVirtualTree; Node: PVirtualNode;
  39.       Column: TColumnIndex; TextType: TVSTTextType; var CellText: String);
  40.   private
  41.     procedure SaveData;
  42.     procedure LoadData;
  43.  
  44.   public
  45.  
  46.   end;
  47.  
  48. var
  49.   Form1: TForm1;
  50.   Data: PData;
  51.   Node: PVirtualNode;
  52.   Cnt, I: Integer;
  53.   FS: TFileStream;
  54.  
  55. implementation
  56.  
  57. {$R *.lfm}
  58.  
  59. { TForm1 }
  60.  
  61. procedure TForm1.Button1Click(Sender: TObject);
  62. begin
  63.   VST.NodeDataSize := SizeOf(TData);
  64.  
  65.   Node:= VST.AddChild(nil);
  66.   Data:= VST.GetNodeData(Node);
  67.  
  68.   Data^.Imie:= Edit1.Text;
  69.   Data^.Nazwisko:= Edit2.Text;
  70. end;
  71.  
  72. procedure TForm1.Button2Click(Sender: TObject);
  73. begin
  74.   if not Assigned(VST.FocusedNode) then Exit;
  75.     VST.DeleteNode(VST.FocusedNode);
  76. end;
  77.  
  78. procedure TForm1.Button3Click(Sender: TObject);
  79. begin
  80.   LoadData;
  81. end;
  82.  
  83. procedure TForm1.Button4Click(Sender: TObject);
  84. begin
  85.   SaveData;
  86. end;
  87.  
  88. procedure TForm1.FormCreate(Sender: TObject);
  89. begin
  90.   //LoadData;
  91. end;
  92.  
  93. procedure TForm1.VSTGetText(Sender: TBaseVirtualTree; Node: PVirtualNode;
  94.   Column: TColumnIndex; TextType: TVSTTextType; var CellText: String);
  95. begin
  96.   Data:= VST.GetNodeData(Node);
  97.  
  98.   case Column of
  99.   0: CellText:= Data^.Imie;
  100.   1: CellText:= Data^.Nazwisko;
  101.   end;
  102. end;
  103.  
  104. procedure TForm1.SaveData;
  105. begin
  106.   FS:= TFileStream.Create(ExtractFilePath(Application.ExeName)+'baza.vst', fmCreate or fmShareDenyWrite);
  107.   try
  108.     FS.WriteWord(VST.RootNodeCount);
  109.     Node:= VST.GetFirst;
  110.     while Assigned(Node) do
  111.     begin
  112.       Data := VST.GetNodeData(Node);
  113.       FS.Write(Data^, SizeOf(TData));
  114.       Node:= VST.GetNext(Node);
  115.     end;
  116.   finally
  117.     FS.Free;
  118.   end;
  119. end;
  120.  
  121. procedure TForm1.LoadData;
  122. begin
  123.  
  124.   VST.Clear;
  125.   VST.NodeDataSize:= SizeOf(TData);
  126.  
  127.   FS:= TFileStream.Create(ExtractFilePath(Application.ExeName)+'baza.vst', fmOpenRead or fmShareDenyNone);
  128.   try
  129.     Cnt:= FS.ReadWord;
  130.     if Cnt > 0 then
  131.     begin
  132.       I:= 1;
  133.       while I <= Cnt do
  134.       begin
  135.         Node:= VST.AddChild(nil);
  136.         Data:= VST.GetNodeData(Node);
  137.         FS.Read(Data^, SizeOf(TData));
  138.         Inc(I);
  139.       end;
  140.     end;
  141.   finally
  142.     FS.Free;
  143.   end;
  144. end;
« Last Edit: April 27, 2022, 07:01:28 pm by Pe3s »

jamie

  • Hero Member
  • *****
  • Posts: 6077
Re: Virtual String Tree
« Reply #1 on: April 26, 2022, 11:26:23 pm »
Strings are managed and nothing more than a pointer.

you are expecting to read pointers from file to be exactly correct for the current instance of your code, it will not be if the file was created with a different time of your app.

instead, you should be using something different to write strings out to file and reading them back.

 Like a ShortString type instead.

 Short strings you can specify the max length and your data record will have space for it and it will not be managed related.

 Don't use managed types in a record that is going to be stored later to be used in a different session of your code.

etc, etc and etc.
The only true wisdom is knowing you know nothing

wp

  • Hero Member
  • *****
  • Posts: 11830
Re: Virtual String Tree
« Reply #2 on: April 26, 2022, 11:43:35 pm »
PLEASE: This is such a short program, it is much more helpful for us if you post a complete, compilable project. Put .lfm, .pas, .lpi, and lpr files into a common zip and upload it via "Attachment and other options".

The issue is that you write/read the TData record directly (FS.Write(Data^, Sizeof(TData)). This is wrong here because the TData record contains dynamic strings which are pointers. So, you are writing only the address of the string, but not the string content itself, and after reading the address points to anywhere...

There are two ways to solve this:
/1/ Declare the strings in TData as ShortStrings, i.e. with their max length in square brackets. This way the string are stored inside the TData record, and your code works:
Code: Pascal  [Select][+][-]
  1. type
  2.   TData = packed record
  3.     Imie: String[32];
  4.     Nazwisko: String[16];
  5.   end;
/2/ If you have strings longer than 255 characters (this is the max length of ShortStrings) or want smaller files you still can use your original TData record, but you must read/write the strings with Stream.ReadAnsiString/Stream.WriteAnsiString. Be careful with the layout of the record elements if you have non-string data as well: put the strings at the end of the record declaration and write/read the non-string data first en bloc and the strings separately. Here's an example:
Code: Pascal  [Select][+][-]
  1. type
  2.   TData = record
  3.     IntNumber: integer;
  4.     FloatValue: Double;
  5.     Imie: String;           // declare the strings at the end of the record
  6.     Nazwisko: String;
  7.   end;
  8.  
  9. procedure  TForm1.SaveData;
  10. begin
  11.   ...
  12.   FS.Write(Data^, SizeOf(TData - 2*SizeOf(string));  // Write everything en bloc, but skip the 2 strings at the end
  13.   FS.WriteAnsiString(Data^.Imie);
  14.   FS.WriteAnsiString(Data^.Nazwisko);
  15.   ...
  16. end;
  17.  
  18. procedure TForm1.LoadData;
  19. begin
  20.   ...
  21.   FS.Read(Data^, SizeOf(TData - 2*SizeOf(String));  // Read everything except for the strings
  22.   Data^.Imie := FS.ReadAnsiString;
  23.   Data^.Nazwisko := FS.ReadAnsiString;
  24. ...

I found some other issues in your code:

/1/ "Abuse" of global variables with should be local variables instead. Because your Data variable is global it exists only once, but Data is used all over again and there is a high risk that one operation may overwrite the Data set up by another operation. Much better if you declare Node, Data, Cnt, I, and FS only locally within the procedures that need them. Here is the OnGetText handler as an example:
Code: Pascal  [Select][+][-]
  1. procedure TForm1.VSTGetText(Sender: TBaseVirtualTree; Node: PVirtualNode;
  2.   Column: TColumnIndex; TextType: TVSTTextType; var CellText: String);
  3. var
  4.   Data: PData;  // declare locally!
  5. begin
  6.   Data:= VST.GetNodeData(Node);
  7.   case Column of
  8.     0: CellText:= Data^.Imie;
  9.     1: CellText:= Data^.Nazwisko;
  10.   end;
  11. end;

/2/ There is no OnFreeNode handler to release the memory occupied by the strings in the TData record. Otherwise you have a huge memory leak:
Code: Pascal  [Select][+][-]
  1. procedure TForm1.VSTFreeNode(Sender: TBaseVirtualTree; Node: PVirtualNode);
  2. var
  3.   Data: PData;
  4. begin
  5.   Data := VST.GetNodeData(Node);
  6.   Data^.Imie := '';
  7.   Data^.Nazwisko := '';
  8. end;

/3/ I would not set the node's NodeDatasize multiple times at different locations. If you happen to assign different values you will destroy the tree which requires a constant NodeDataSize for all nodes. Better to do this in the OnGetNodeDataSize event, or to set it in the form's OnCreate.
Code: Pascal  [Select][+][-]
  1. procedure TForm1.VSTGetNodeDataSize(Sender: TBaseVirtualTree;
  2.   var NodeDataSize: Integer);
  3. begin
  4.   NodeDataSize := SizeOf(TData);
  5. end;
« Last Edit: April 26, 2022, 11:48:09 pm by wp »

Pe3s

  • Hero Member
  • *****
  • Posts: 533
Re: Virtual String Tree
« Reply #3 on: April 27, 2022, 07:01:11 pm »
Thank you for your very valuable advice   :)

 

TinyPortal © 2005-2018