如何保存含所有节点及关联记录对象的TreeView并快速加载?
解决Delphi TreeView节点关联记录的高效保存与加载问题
核心问题分析
直接将TreeView保存到Blob时,仅能存储节点的UI属性(文本、层级、图像索引等),但节点Data属性关联的TMyRec指针是进程内的内存地址,无法跨运行时复用;逐项加载4000+节点时,频繁的内存分配和文件IO操作会导致加载速度极慢。
可行实现思路与示例
思路1:序列化TreeView结构+所有记录数据到流
将节点的结构信息(父节点索引、文本、图像索引等)和TMyRec的实际数据一起写入流,加载时先为所有记录分配内存,再重建TreeView节点并关联对应记录指针。
步骤与代码示例
- 定义自定义记录及序列化辅助方法
type TMyRec = record ID: Integer; Name: string; Value: Double; // 其他自定义字段 end; // 将TMyRec序列化到流 procedure WriteMyRecToStream(Stream: TStream; const Rec: TMyRec); begin Stream.WriteBuffer(Rec.ID, SizeOf(Rec.ID)); // 单独序列化字符串:先写长度再写内容 var StrLen := Length(Rec.Name); Stream.WriteBuffer(StrLen, SizeOf(StrLen)); if StrLen > 0 then Stream.WriteBuffer(Rec.Name[1], StrLen * SizeOf(Char)); Stream.WriteBuffer(Rec.Value, SizeOf(Rec.Value)); end; // 从流反序列化TMyRec procedure ReadMyRecFromStream(Stream: TStream; out Rec: TMyRec); var StrLen: Integer; begin Stream.ReadBuffer(Rec.ID, SizeOf(Rec.ID)); Stream.ReadBuffer(StrLen, SizeOf(StrLen)); SetLength(Rec.Name, StrLen); if StrLen > 0 then Stream.ReadBuffer(Rec.Name[1], StrLen * SizeOf(Char)); Stream.ReadBuffer(Rec.Value, SizeOf(Rec.Value)); end;
- 保存TreeView及关联记录到流
procedure SaveTreeViewWithRecs(TreeView: TTreeView; Stream: TStream); var I: Integer; Node: TTreeNode; Rec: TMyRec; HasRec: Boolean; begin // 先写入节点总数 Stream.WriteBuffer(TreeView.Items.Count, SizeOf(Integer)); for I := 0 to TreeView.Items.Count - 1 do begin Node := TreeView.Items[I]; // 写入节点基础信息:父节点索引(无父节点则为-1)、文本、图像索引、选中图像索引 Stream.WriteBuffer(Integer(Node.Parent.Index), SizeOf(Integer)); var StrLen := Length(Node.Text); Stream.WriteBuffer(StrLen, SizeOf(StrLen)); if StrLen > 0 then Stream.WriteBuffer(Node.Text[1], StrLen * SizeOf(Char)); Stream.WriteBuffer(Node.ImageIndex, SizeOf(Integer)); Stream.WriteBuffer(Node.SelectedIndex, SizeOf(Integer)); // 写入关联的TMyRec数据 HasRec := Assigned(Node.Data); Stream.WriteBuffer(HasRec, SizeOf(Boolean)); if HasRec then begin Rec := TMyRec(Node.Data^); WriteMyRecToStream(Stream, Rec); end; end; end;
- 从流加载TreeView及关联记录
procedure LoadTreeViewWithRecs(TreeView: TTreeView; Stream: TStream); var NodeCount, I, ParentIdx, ImgIdx, SelIdx, StrLen: Integer; NodeText: string; ParentNode: TTreeNode; PRec: ^TMyRec; Rec: TMyRec; HasRec: Boolean; begin TreeView.Items.BeginUpdate; try TreeView.Items.Clear; // 读取节点总数 Stream.ReadBuffer(NodeCount, SizeOf(Integer)); for I := 0 to NodeCount - 1 do begin // 读取节点基础信息 Stream.ReadBuffer(ParentIdx, SizeOf(Integer)); Stream.ReadBuffer(StrLen, SizeOf(StrLen)); SetLength(NodeText, StrLen); if StrLen > 0 then Stream.ReadBuffer(NodeText[1], StrLen * SizeOf(Char)); Stream.ReadBuffer(ImgIdx, SizeOf(Integer)); Stream.ReadBuffer(SelIdx, SizeOf(Integer)); // 创建节点 if ParentIdx = -1 then ParentNode := nil else ParentNode := TreeView.Items[ParentIdx]; ParentNode := TreeView.Items.AddChild(ParentNode, NodeText); ParentNode.ImageIndex := ImgIdx; ParentNode.SelectedIndex := SelIdx; // 读取并关联TMyRec Stream.ReadBuffer(HasRec, SizeOf(Boolean)); if HasRec then begin GetMem(PRec, SizeOf(TMyRec)); ReadMyRecFromStream(Stream, PRec^); ParentNode.Data := PRec; end; end; finally TreeView.Items.EndUpdate; end; end;
思路2:用索引替代指针关联记录
将所有TMyRec存储在TList<TMyRec>中,节点Data属性存储该记录在列表中的索引(转为Pointer类型)。加载时先恢复列表,点击节点时通过索引从列表中获取记录。
代码示例
- 保存逻辑
var RecList: TList<TMyRec>; // 初始化记录列表,将节点记录转存为索引关联 procedure InitRecList(TreeView: TTreeView); var Node: TTreeNode; begin RecList := TList<TMyRec>.Create; for Node in TreeView.Items do begin if Assigned(Node.Data) then begin RecList.Add(TMyRec(Node.Data^)); Node.Data := Pointer(RecList.Count - 1); // 存储索引 end; end; end; // 保存记录列表和TreeView到流 procedure SaveTreeViewAndRecList(TreeView: TTreeView; Stream: TStream); begin // 先保存记录总数 Stream.WriteBuffer(RecList.Count, SizeOf(Integer)); for var Rec in RecList do WriteMyRecToStream(Stream, Rec); // 保存TreeView结构(此时节点Data是索引,可直接序列化) TreeView.SaveToStream(Stream); end;
- 加载逻辑
procedure LoadTreeViewAndRecList(TreeView: TTreeView; Stream: TStream); var RecCount, I: Integer; Rec: TMyRec; Node: TTreeNode; begin TreeView.Items.BeginUpdate; try // 加载记录列表 Stream.ReadBuffer(RecCount, SizeOf(Integer)); RecList := TList<TMyRec>.Create; for I := 0 to RecCount - 1 do begin ReadMyRecFromStream(Stream, Rec); RecList.Add(Rec); end; // 加载TreeView结构 TreeView.LoadFromStream(Stream); // 重新关联节点与记录 for Node in TreeView.Items do begin if Node.Data <> nil then begin var Idx := Integer(Node.Data); // 直接关联列表中记录的地址(若需修改记录,可分配内存拷贝) Node.Data := @RecList[Idx]; end; end; finally TreeView.Items.EndUpdate; end; end; // 节点点击事件处理 procedure TForm1.TreeView1Click(Sender: TObject); var Node: TTreeNode; Idx: Integer; Rec: TMyRec; begin Node := TreeView1.Selected; if Assigned(Node) and Assigned(Node.Data) then begin Idx := Integer(Node.Data); Rec := RecList[Idx]; // 使用记录数据 ShowMessageFmt('ID: %d, Name: %s', [Rec.ID, Rec.Name]); end; end;
关键注意事项
- 加载TreeView时必须用
BeginUpdate和EndUpdate包裹操作,避免频繁刷新UI导致卡顿。 - 若
TMyRec包含动态内容(如字符串、动态数组),不能直接用WriteBuffer序列化整个记录,需单独处理动态字段。 - 若使用指针关联记录,需在TreeView节点销毁时释放内存(如在
TreeView1Deletion事件中调用FreeMem(Node.Data)),避免内存泄漏。
内容的提问来源于stack exchange,提问作者Zink
相关产品推荐
相关产品推荐

