I'm editing the 2nd column of a displayed node in the TVirtualStringTree. However after the edit is complete it I'm unable to retrieve the text using Sender.GetNodeData(Node) - it contains no text.
How can I get the text in the OnEdited event? Is there some other way to get the edited text? I've read the first few FAQ pages of the Virtual Treeview CHM help documentation and also refered to the answer in this SO question but could not find the answer.
Here is the present code:
TTherapData = record
TherapID: Integer;
TherapName: String[120];
TherapInstr: String[120];
Selected: Byte;
end;
PTherapData = ^TTherapData;
procedure TfmPatient_Conslt.vstRxList_AsgEdited(Sender: TBaseVirtualTree; Node: PVirtualNode; Column: TColumnIndex);
var
TherapData: PTherapData;
begin
TherapData := Sender.GetNodeData(Node);
if Assigned(TherapData) then
begin
TherapData^.TherapInstr := vstRxList_Asg.Text[Node, 1];
showmessage(TherapData^.TherapInstr);
end;
FTherapDataListAsg_Iter := 0;
vstRxList_Asg.NodeDataSize := SizeOf(TTherapData);
vstRxList_Asg.RootNodeCount := 0;
vstRxList_Asg.RootNodeCount := TherapDataList_CountSelectedItems;
end;
Thanks to hint from TLama, the answer is to handle the OnNewText event:
procedure TfmPatient_Conslt.vstRxList_AsgNewText(Sender: TBaseVirtualTree; Node: PVirtualNode; Column: TColumnIndex;
NewText: string);
var
TherapData: PTherapData;
begin
if (Column = 1) then
begin
TherapData := Sender.GetNodeData(Node);
if Assigned(TherapData) then
TherapData^.TherapInstr := NewText;
end;
end;
Related
I followed the conventional practice of displaying data on TVirtualStringTree. But it displays only the string "node" in each cell. Can someone tell what I am missing here ?
Thanks in advance.
My Code:
type
TRecFileDirectory = record
FileDirectory: WideString;
FileDirectoryLock: wordbool;
end;
TPRecFileDirectory = ^TRecFileDirectory;
implementation
procedure TForm2.btn4Click(Sender: TObject);
var
I: Integer;
NewNode: PVirtualNode;
ptrFileDir: TPRecFileDirectory;
begin
vsTree1.BeginUpdate;
for I := 0 to Length(arrFileDirectory)-1 do
begin
NewNode := vsTree1.AddChild(nil);
ptrFileDir := vsTree1.GetNodeData(NewNode);
ptrFileDir^.FileDirectory := arrFileDirectory[I].FileDirectory;
ptrFileDir^.FileDirectoryLock := arrFileDirectory[I].FileDirectoryLock;
end;
vsTree1.EndUpdate;
btn4.caption := btn4.caption+' DONE';
end;
You need to implement an event handler for the OnGetText event, that extracts the string to be displayed from your data, dependant on the column and node that is supplied. For example:
procedure TForm1.VSTGetText(Sender: TBaseVirtualTree; Node: PVirtualNode; Column: TColumnIndex; TextType: TVSTTextType; var CellText: string);
var
MyData: TPRecFileDirectory;
begin
MyData := Node.GetData();
if Column = 0 then
CellText := MyData.FileDirectory;
end;
Set DefaultText property to empty string, e.g. in code or in Inspector
I am using Delphi XE3 with Virtual Tree View.
My codes are below:
type
TMyData = record
Caption: string;
end;
procedure TForm1.Button1Click(Sender: TObject);
var
RootNode: PVirtualNode;
PData: ^TMyData;
begin
RootNode := tvItems.AddChild(nil);
PData := tvItems.GetNodeData(RootNode);
PData^.Caption := 'This is a test node';
end;
procedure TForm1.FormCreate(Sender: TObject);
begin
tvItems.NodeDataSize := SizeOf(TMyData);
end;
procedure TForm1.tvItemsGetText(Sender: TBaseVirtualTree; Node: PVirtualNode;
Column: TColumnIndex; TextType: TVSTTextType; var CellText: string);
var
PData: ^TMyData;
begin
if Assigned(Node) then
begin
PData := tvItems.GetNodeData(Node);
if Assigned(PData) then
Celltext := PData^.Caption;
end;
end;
When I click the "Button1", the root node will be created. However, when my mouse clicks the node text, it will not be selected.
Some of my findings:
One must clicks to the beginning of the node text to select the node. If clicking in middle or in the end of the node text, then the node will not be selected.
If I change tvItemsGetText to below, then the problem disappears:
procedure TForm1.tvItemsGetText(Sender: TBaseVirtualTree; Node: PVirtualNode;
Column: TColumnIndex; TextType: TVSTTextType; var CellText: string);
var
PData: ^TMyData;
begin
CellText := 'This is a test node';
end;
I set a breakpoint in tvItemsGetText and find it will be invoked several times. At the first several times, the PData will be nil, which makes the CellText empty. At the final invokation, the PData will become valid and the CellText will be set to 'This is a test node'.
It seems that the range that allow mouse click and select the node is determined by the initial texts of the node. If the initial text is empty string, then one must click at the very beginning of the node to select it.
Is this a bug of Virtual Tree View?
There are several ways to init new node by user data.
1. Using OnInitNode event:
procedure TForm5.Button1Click(Sender: TObject);
begin
vt1.InsertNode(nil, amAddChildLast); // internal calls vt1InitNode
end;
procedure TForm5.vt1InitNode(Sender: TBaseVirtualTree; ParentNode, Node: PVirtualNode;
var InitialStates: TVirtualNodeInitStates);
var
PData: ^TMyData;
begin
PData := Sender.GetNodeData(Node);
PData^.Caption := 'This is a test node';
end;
2 Using UserData param
Variant 1. Dynamic data
Do not forget to remove InitNode event and dont set NodeDataSize property
type
TMyData = record
Caption: string;
end;
PMyData = ^TMyData;
procedure TForm5.Button1Click(Sender: TObject);
var
p: PMyData;
begin
New(p);
p.Caption:='This is a test node';
vt1.InsertNode(nil, amAddChildLast, p); // create node with initialized user data
// by default VirtualTree use NodeDataSize = SizeOf(pointer),
// so there is no reason to use GetNodeDataSize event
end;
procedure TForm5.vt1GetText(Sender: TBaseVirtualTree; Node: PVirtualNode;
Column: TColumnIndex; TextType: TVSTTextType;
var CellText: string);
var
PData: PMyData;
begin
if Assigned(Node) then
begin
PData := PMyData(Sender.GetNodeData(Node)^); // little modification
// for correct access to dynamic node data
if Assigned(PData) then
CellText := PData.Caption;
end;
end;
procedure TForm5.vt1FreeNode(Sender: TBaseVirtualTree; Node: PVirtualNode);
var
p: PMyData;
begin
p:=PMyData(Sender.GetNodeData(Node)^);
Dispose(p); // as you allocate memory for user data - you should free it to avoid memory leaks
end;
Variant 2. Objects
Add new private function to your form:
private
{ Private declarations }
function GetObjectByNode<T: class>(Node: PVirtualNode): T;
// do not forget to include System.Generics.Collections to `uses`
realization:
function TForm5.GetObjectByNode<T>(Node: PVirtualNode): T;
var
NodeData: Pointer;
tmpObject: TObject;
begin
Result := nil;
if not Assigned(Node) then
exit;
NodeData := vt1.GetNodeData(Node);
if Assigned(NodeData) then
tmpObject := TObject(NodeData^);
if tmpObject is T then
Result := T(tmpObject)
else
Result := nil;
end;
And the main code (almost identical to variant 1):
procedure TForm5.Button1Click(Sender: TObject);
var
d: TMyData;
begin
d := TMyData.Create;
d.Caption := 'This is a test node';
vt1.InsertNode(nil, amAddChildLast, d);
end;
procedure TForm5.vt1FreeNode(Sender: TBaseVirtualTree; Node: PVirtualNode);
var
d: TMyData;
begin
d := GetObjectByNode<TMyData>(Node);
d.Free;
end;
procedure TForm5.vt1GetText(Sender: TBaseVirtualTree; Node: PVirtualNode;
Column: TColumnIndex; TextType: TVSTTextType;
var CellText: string);
var
d: TMyData;
begin
d := GetObjectByNode<TMyData>(Node);
if Assigned(d) then
CellText := d.Caption;
end;
I use Delphi XE5 and created a VirtualTree with several parent and child nodes. When I select a child node and execute the following code:
vstSource:= vstTree.FocusedNode;
vstTarget:= vstTree.GetNextSibling(vstSource);
vstTree.moveto(vstSource,vstTarget,amInsertAfter,false);
The cursor jumps to the next node and selects it but the selected node does not move down.
I use VirtualTreeView v.5.3.0.
Can anybody tell me what's wrong?
Edit:
I've made an update to v.6.0 but nothing changes. Here's the full code (it's only a sample to test the VirtualTree):
procedure TForm1.Button1Click(Sender: TObject);
begin
myTree.RootNodeCount := 2;
end;
procedure TForm1.Button2Click(Sender: TObject);
var
SNode, TNode: PVirtualNode;
begin
SNode:= myTree.FocusedNode;
TNode:= myTree.GetNextSibling(SNode);
myTree.MoveTo(SNode, TNode, amInsertAfter, False);
end;
procedure TForm1.myTreeGetText(Sender: TBaseVirtualTree; Node: PVirtualNode;
Column: TColumnIndex; TextType: TVSTTextType; var CellText: string);
begin
if Column > 0 then
Exit;
case Sender.GetNodeLevel(Node) of
0: CellText := 'Main';
1:
case Node.Index of
0: CellText := 'Val1';
1: CellText := 'Val2';
2: CellText := 'Val3';
3: CellText := 'Val4';
4: CellText := 'Val5';
end;
end;
end;
procedure TForm1.myTreeInitNode(Sender: TBaseVirtualTree; ParentNode,
Node: PVirtualNode; var InitialStates: TVirtualNodeInitStates);
begin
if Sender.GetNodeLevel(Node) = 0 then
Sender.ChildCount[Node] := 5;
end;
I'm using a TVirtualStringTree to store pointers to records.
Originally there is a TList that contains the list of records.
I'm using the OnInitNode event to iterate through the TList and assign each records' data to the tree's nodes.
However, when retrieving the data associated with a node in the OnNewText event handler, the pointer returned has a different address than the one originally stored in the tree.
Further, I can see through debugging that the pointer (to the record data) retrieved from the node is not the same as the one originally stored in the node. I need to save changed data to a database and need to reference the record with the changed data. It should be simple as referencing the pointer, but the problem is that the pointer is not the same.
I'm not sure what I'm doing wrong and hope someone can help me fix this.
Thanks in advance.
Here is my code:
Data structure and declarations:
TTherapData = record
TherapID: Integer;
TherapName: String[120];
TherapInstr: String[120];
Selected_DB: Byte;
Selected: Byte;
end;
PTherapData = ^TTherapData;
FTherapDataList: TList<PTherapData>;
FTherapDataListAsg_Iter: Integer;
vstRxList_Asg: TVirtualStringTree;
Loading the data into the TList, and then into the tree:
procedure TfmPatient_Conslt.LoadTherapList(const ADBLoad: Boolean = False);
var
TherapData: PTherapData;
d, x: Integer;
begin
datamod.uspLKTHERAP_S.First;
while not datamod.uspLKTHERAP_S.Eof do
begin
New(TherapData);
TherapData^.TherapID := datamod.uspLKTHERAP_SROW_ID.AsInteger;
TherapData^.TherapName := datamod.uspLKTHERAP_SIMPRTHERAP.AsString;
TherapData^.TherapInstr := EmptyStr;
TherapData^.Selected := 0;
TherapData^.Selected_DB := 0;
FTherapDataList.Add(TherapData);
datamod.uspLKTHERAP_S.Next;
end;
datamod.uspCONSLT_RX_S.First;
while not datamod.uspCONSLT_RX_S.Eof do
begin
d := datamod.uspCONSLT_RX_SRX_ID.AsInteger;
TherapData := FTherapDataList[TherapDataList_GetIndexOfID(d)];
TherapData^.TherapInstr := datamod.uspCONSLT_RX_SRX_INSTRUCTION.AsString;
TherapData^.Selected := 1;
TherapData^.Selected_DB := 1;
datamod.uspCONSLT_RX_S.Next;
end;
x := TherapDataList_CountSelectedItems;
FTherapDataListAsg_Iter := 0;
vstRxList_Asg.NodeDataSize := SizeOf(TTherapData);
vstRxList_Asg.RootNodeCount := 0;
vstRxList_Asg.RootNodeCount := x;
end;
procedure TfmPatient_Conslt.vstRxList_AsgInitNode(Sender: TBaseVirtualTree; ParentNode, Node: PVirtualNode;
var InitialStates: TVirtualNodeInitStates);
var
TherapData: PTherapData;
begin
TherapData := Sender.GetNodeData(Node);
while (FTherapDataList[FTherapDataListAsg_Iter]^.Selected <> 1) do
Inc(FTherapDataListAsg_Iter);
TherapData^.TherapID := FTherapDataList[FTherapDataListAsg_Iter]^.TherapID;
TherapData^.TherapName := FTherapDataList[FTherapDataListAsg_Iter]^.TherapName;
TherapData^.TherapInstr := FTherapDataList[FTherapDataListAsg_Iter]^.TherapInstr;
{ TherapData := FTherapDataList[FTherapDataListAsg_Iter]; } //
{ TherapData^ := FTherapDataList[FTherapDataListAsg_Iter]^; } //
Inc(FTherapDataListAsg_Iter);
end;
procedure TfmPatient_Conslt.vstRxList_AsgGetText(Sender: TBaseVirtualTree; Node: PVirtualNode; Column: TColumnIndex;
TextType: TVSTTextType; var CellText: string);
var
TherapData: PTherapData;
begin
TherapData := Sender.GetNodeData(Node);
if Assigned(TherapData) then
if (Column = 0) then
CellText := TherapData^.TherapName
else if (Column = 1) then
CellText := TherapData^.TherapInstr;
end;
procedure TfmPatient_Conslt.vstRxList_AsgEditing(Sender: TBaseVirtualTree; Node: PVirtualNode; Column: TColumnIndex;
var Allowed: Boolean);
begin
Allowed := (Column = 1);
end;
Retrieving the data. I noticed the problem here:
procedure TfmPatient_Conslt.vstRxList_AsgNewText(Sender: TBaseVirtualTree; Node: PVirtualNode; Column: TColumnIndex;
NewText: string);
var
TherapData: PTherapData;
begin
if (Column = 1) then
begin
TherapData := Sender.GetNodeData(Node);
if Assigned(TherapData) then // <---- There is a debug breakpoint here
// and the watch window screen-shot
// is taken here
TherapData^.TherapInstr := NewText;
// Showmessage(Format('%p', [TherapData])); // <---- The pointer value is not the same
// as that originally stored !
end;
end;
This is where I save the list data to database, and the reason that I need the tree to change the original data and not a copy:
procedure TfmPatient_Conslt.SaveRxListToDB;
var
TherapData: PTherapData;
begin
for TherapData in FTherapDataList do
begin
if (TherapData^.Selected = 1) and (TherapData^.Selected_DB = 0) then
begin
// Add new entries to DB
// :ROW_ID, :CONSLT_ID, :RX_ID, :RX_INSTRUCTION
datamod.uspCONSLT_RX_I.ParamByName('ROW_ID').AsInteger := 0;
datamod.uspCONSLT_RX_I.ParamByName('CONSLT_ID').AsInteger := FConsultationID;
datamod.uspCONSLT_RX_I.ParamByName('RX_ID').AsInteger := TherapData^.TherapID;
datamod.uspCONSLT_RX_I.ParamByName('RX_INSTRUCTION').AsString := TherapData^.TherapInstr;
datamod.uspCONSLT_RX_I.PrepareSQL(False);
datamod.uspCONSLT_RX_I.ExecProc;
TherapData^.Selected_DB := 1;
end
else if (TherapData^.Selected = 1) and (TherapData^.Selected_DB = 1) then
begin
// Update existing DB entries
// :CONSLT_ID, :RX_ID, :RX_INSTRUCTION
datamod.uspCONSLT_RX_U.ParamByName('CONSLT_ID').AsInteger := FConsultationID;
datamod.uspCONSLT_RX_U.ParamByName('RX_ID').AsInteger := TherapData^.TherapID;
datamod.uspCONSLT_RX_U.ParamByName('RX_INSTRUCTION').AsString := TherapData^.TherapInstr;
datamod.uspCONSLT_RX_U.PrepareSQL(False);
datamod.uspCONSLT_RX_U.ExecProc;
end
else if (TherapData^.Selected = 0) and (TherapData^.Selected_DB = 1) then
begin
// Delete removed entries from DB
// :CONSLT_ID, :RX_ID
datamod.uspCONSLT_RX_D.ParamByName('CONSLT_ID').AsInteger := FConsultationID;
datamod.uspCONSLT_RX_D.ParamByName('RX_ID').AsInteger := TherapData^.TherapID;
datamod.uspCONSLT_RX_D.PrepareSQL(False);
datamod.uspCONSLT_RX_D.ExecProc;
TherapData^.Selected_DB := 0;
end;
end;
end;
Here's a screenshot of the from the Debug->Watch List window:
To answer to the question in the title, I would say yes, your coding approach is wrong.
The mistake is that you don't save a pointer to your data record to the VT's node, you're allocating whole (separate) TTherapData record for each node! So the "mistake" is the line
vstRxList_Asg.NodeDataSize := SizeOf(TTherapData);
in the TfmPatient_Conslt.LoadTherapList method. What you probably want is an additional record which holds pointer to the data record:
type
TVTNodeData = record
TherapData: PTherapData;
end;
PVTNodeData = ^TVTNodeData;
and use that record as node data record:
vstRxList_Asg.NodeDataSize := SizeOf(TVTNodeData);
and node init becomes something like
procedure TfmPatient_Conslt.vstRxList_AsgInitNode(Sender: TBaseVirtualTree; ParentNode, Node: PVirtualNode; var InitialStates: TVirtualNodeInitStates);
var
NodeData: PVTNodeData;
begin
NodeData := Sender.GetNodeData(Node);
while (FTherapDataList[FTherapDataListAsg_Iter]^.Selected <> 1) do
Inc(FTherapDataListAsg_Iter);
NodeData^.TherapData := FTherapDataList[FTherapDataListAsg_Iter];
Inc(FTherapDataListAsg_Iter);
end;
and using the data in other tree events like
procedure TfmPatient_Conslt.vstRxList_AsgGetText(Sender: TBaseVirtualTree; Node:PVirtualNode; Column: TColumnIndex; TextType: TVSTTextType; var CellText: string);
var
NodeData: PVTNodeData;
TherapData: PTherapData;
begin
NodeData := Sender.GetNodeData(Node);
if Assigned(NodeData) and Assigned(NodeData.TherapData) then begin
TherapData := NodeData.TherapData;
if (Column = 0) then
CellText := TherapData^.TherapName
else if (Column = 1) then
CellText := TherapData^.TherapInstr;
end;
end;
Here is what I am "trying" to achieve
I have a function to generate passwords which I then add into a TStringList after this I should populate the VirtualTreeView with the items but I am having no luck in getting anywhere fast with doing so. How should it be done the correct way? I am still learning and am not a professional.
My function for generating the passwords:
function Generate(AllowUpper,AllowLower,AllowNumbers,AllowSymbols:Boolean; PassLen:Integer):String;
const
UpperList = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ';
LowerList = 'abcdefghijklmnopqrstuvwxyz';
NumberList = '0123456789';
SymbolList = '!#$%&/()=?#<>|{[]}\*~+#;:.-_';
var
MyList : String;
Index : Integer;
i : Integer;
begin
Result:='';
MyList:='';
//here if the flag is set the elements are added to the main array (string) to process
if AllowUpper then MyList := MyList + UpperList;
if AllowLower then MyList := MyList + LowerList;
if AllowNumbers then MyList := MyList + NumberList;
if AllowSymbols then MyList := MyList + SymbolList;
Randomize;
if Length(MyList)>0 then
for i := 1 to PassLen do
begin
Index := Random(Length(MyList))+1;
Result := Result+MyList[Index];
end;
end;
Here is how I am calling it
procedure TMain.Button3Click(Sender: TObject);
var
i: integer;
StrLst: TStringList;
// Timing vars...
Freq, StartCount, StopCount: Int64;
TimingSeconds: real;
begin
vst1.Clear;
Panel2.Caption := 'Generating Passwords...';
Application.ProcessMessages;
// Start Performance Timer...
QueryPerformanceFrequency(Freq);
QueryPerformanceCounter(StartCount);
StrLst := TStringList.Create;
try
for i := 1 to PassLenEd.Value do
StrLst.Add(Generate(ChkGrpCharSelect.Checked[0],ChkGrpCharSelect.Checked[1],
ChkGrpCharSelect.Checked[2],ChkGrpCharSelect.Checked[3],20));
// Stop Performance Timer...
QueryPerformanceCounter(StopCount);
TimingSeconds := (StopCount - StartCount) / Freq;
// Display Timing... How long it took to generate
Panel2.Caption := 'Generated '+IntToStr(PassLenEd.Value)+' passwords in '+
FloatToStrF(TimingSeconds,ffnumber,1,3)+' seconds';
// Add to VirtualTreeList - here???
finally
StrLst.Free;
end;
end;
I expect that I am doing this completely the wrong way, I have been trying for 2 days now, it would be great if someone could put me straight with how I should go about it.
Chris
I'd probably stick with TListView but turn it into a virtual list view. Like this:
procedure TMyForm.FormCreate;
begin
ListView.OwnerData := True;
ListView.OnData = ListViewData;
ListView.Items.Count := StringList.Count;
end;
procedure TMyForm.ListViewData(Sender: TObject; ListItem: TListItem);
begin
ListItem.Caption := StringList[ListItem.Index];
end;
You can put millions of items in there in an instant.
You better store your stringlist somewhere else in your code to use it "virtually", e.g. in the form's private section. When after populating it, just set:
vst1.Clear;
vst1.RootNodeCount := StrLst.Count;
And on tree's get text event:
procedure TForm1.vst1GetText(Sender: TBaseVirtualTree; Node: PVirtualNode; Column: TColumnIndex;
TextType: TVSTTextType; var CellText: string);
begin
CellText := StrLst[Node.Index];
end;
Or if you really want VirtualTreeView, you can use something like this ...
I'm not sure if this is absolutely clear solution, I'm familiar with records, not only one single variables.
procedure TMain.Button3Click(Sender: TObject);
var i: integer;
p: PString;
Freq, StartCount, StopCount: Int64;
TimingSeconds: real;
begin
Panel2.Caption := 'Generating Passwords...';
Application.ProcessMessages;
QueryPerformanceFrequency(Freq);
QueryPerformanceCounter(StartCount);
vst1.BeginUpdate;
vst1.Clear;
for i := 1 to PassLenEd.Value do
begin
p := VirtualStringTree1.GetNodeData(VirtualStringTree1.AddChild(nil));
p^ := Generate(ChkGrpCharSelect.Checked[0],ChkGrpCharSelect.Checked[1], ChkGrpCharSelect.Checked[2],ChkGrpCharSelect.Checked[3],20);
end;
vst1.EndUpdate;
QueryPerformanceCounter(StopCount);
TimingSeconds := (StopCount - StartCount) / Freq;
Panel2.Caption := 'Generated '+IntToStr(PassLenEd.Value)+' passwords in '+
FloatToStrF(TimingSeconds,ffnumber,1,3)+' seconds';
end;
And you need to implement OnGetNodeDataSize and OnGetText events to initialize node data size and to display the text.
procedure TMain.vst1GetNodeDataSize(
Sender: TBaseVirtualTree; var NodeDataSize: Integer);
begin
NodeDataSize := SizeOf(string);
end;
procedure TMain.vst1GetText(Sender: TBaseVirtualTree;
Node: PVirtualNode; Column: TColumnIndex; TextType: TVSTTextType;
var CellText: string);
begin
CellText := PString(VirtualStringTree1.GetNodeData(Node))^;
end;
Edit 1: I've corrected data types UnicodeString -> String