类对象的流式转化
TVirtualStringTree的SaveToStream与LoadFromStream
我们已经知道,使用对象的SaveToStream方法,能把对象的内容写到stream中,而使用LoadFromStream方法,能把对象的内容从stream中恢复。不过,这里有些地方,我们可能会忽略。
有一点需要注意,如果想把对象的内容直接保存到stream中,这里的对象应该是那种文本类型的对象,例如TStrings,你使用其SaveToStream方法,其实际上是取出文本,然后把文本写到stream中,以下是Tstrings的SaveToStream源代码:
procedure TStrings.SaveToStream(Stream: TStream);
var
S: string;
begin
S := GetTextStr;
Stream.WriteBuffer(Pointer(S)^, Length(S));
end;
从源代码中我们可以看到,程序首先是获得指向文本的指针,然后通过Stream的WriteBuffer方法,把文本写到流当中。这样,就完成了对文本对象转化成stream的操作。
同样的道理,TStrings的LoadFromStream方法,应该是从stream中读出一定长度的内容,然后还原到Tstrings当中,以下是Tstrings的LoadFromStream源代码:
procedure TStrings.LoadFromStream(Stream: TStream);
var
Size: Integer;
S: string;
begin
BeginUpdate;
try
Size := Stream.Size - Stream.Position;
SetString(S, nil, Size);
Stream.Read(Pointer(S)^, Size);
SetTextStr(S);
finally
EndUpdate;
end;
end;
源代码也印证了我们的估计,程序首先获得要读取的内容的长度,然后按照这个大小对s设置了长度,把stream的内容读到s中,最后就可以还原原来的文本内容了。
似乎一切都在掌握之中,不过我们似乎观察到,对流的操作是非常依赖于特定的数据类型的。这里无论是Stream.WriteBuffer还是Stream.Read等,都需要预先知道需要读写的stream的长度,而这些长度是因数据类型的不同而不同。例如Integer是16位的,而Cardinal是32位的。于是,我们可以猜测得到,如果需要把一个类转化成Stream时,就不是那么直接简单了。
我们在一次项目的预研工作中,需要把TVirtualStringTree的内容转化为stream,从而在适当的时候从stream中恢复这棵树。TVirtualStringTree中的每一节点(node),都是一个我们自己建立的一个数据结构。此时问题来了,我们直接使用VT(TvirutalStringTree的实例)的SaveToStream后形成的stream,通过VT的LoadFromStream还原,程序却在VT的VTSetLevelGetText中报错。我们从Stream中截取了一部分的内容,Stream是以二进制格式存在的,打开后,出现许多我们看不明的内容,在下文我们会尝试解释一下。
我们排除了在还原之前,对stream进行过修改的可能性,在追查原因的时候,我们发现了以下一段写在TvirutalStringTree的SaveToStream方法中的一段文字:
Saves Node and all its children to Stream. If Node is nil then all top level nodes will be stored.
Note: You should be careful about assuming what is actually saved. The problem here is that we are dealing with virtual data. The tree can so not know what it has to save. The only fact we reliably know is the tree's structure. To be flexible for future enhancements as well as unknown content (unknown to the tree class which is saving/loading the stream) a chunk based approach is used here. Every tree class handles only those chunks which are not handled by an anchestor class and are known by the class.
The base tree class saves only the structure of the tree along with application provided data. descendants may optionally add their own chunks to store additional information. See: WriteChunks.
(翻译:(本人水平有限,仅供参考)
把节点与该节点的孩子保存到流当中,如果节点是空,那么所有的顶层节点都会被存储起来。
注意:你必须注意你所假定要真正存储的内容,这里的问题是,我们是在处理虚数据。这棵树不会知道其要存储什么内容,唯一可靠的事实是树的结构。为了方便以后的扩展,又能实现内容无关性(不知道要保存/装载的树的数据类型),这里使用了许多的基方法。每一颗树仅仅处理那些没有被祖先类处理和只有本类才拥有的方法。
父类的树仅仅保存树的结构以及由程序提供数据,继承类可以随意增加他们的方法来存储更多的信息,请见WriteChunks方法。)
于是,我们就找到WriteChunks方法,有发现了以下文字:
Adds another sibling chunk for Node storing the label if the node is initialized.
Note: If the application stores a node's caption in the node's data member (which will be quite common) and needs to store more node specific data then it should use the OnSaveNode event rather than the caption autosave function (take out soSaveCaption from StringOptions). Otherwise the caption is unnecessarily stored twice.
(翻译:(本人水平有限,仅供参考)
增加其他子类的方法用于存储被初始化了的节点的caption信息。
注意:如果程序存储了节点的数据成员中的caption信息(这一点非常常用),并且需要存储节点的更多数据信息,那就必须使用OnSaveNode事件而不是使用自动存储caption信息的功能了(使用自动存储节点的caption只需在StringOpions中开启soSaveCaption就可以了)。另外,caition信息不需要存储2次)
总结以上文字,这里说,用SaveToStream方法保存节点(node)信息时,只能保证把结构保存到Stream中,一般来说,保存节点(node)的caption是非常常用的,因此已经在TVirtualStringTree实现了,但对于其他的信息,那就无能为力了。不过TVirtualStringTree也知道这种情况,预留了OnSaveNode方法,用户可以通过这个方法来保存他想保存的信息。
为什么会出现无能为力的情况呢?问题就在于刚才我们提到的Stream.WriteBuffer和Stream.Read等需要知道要读写的Stream的长度,而长度是因数据类型不同而不同上。他们无法预先知道用户自定义的类的结构,加上SaveToStream一直都是处理虚数据的,因此无法把类的信息写到Stream中。
既然找到了原因,那么解决起来就比较简单了。以下是我们写的一段代码,可以作为参考,以后用户自己定义新的数据类型时,也可以模仿这样的格式来实现。其实现的思路是将自定义的类的属性一个一个写进Stream中,读取的时候,按照存储的顺序一个一个读出来。
PVTNode = ^TVTNode;
TVTNode = record
AObject:TObject;
end;
TReportItem = class
public
FReportItemId, //报表项目内码
FReportItemParent,//父内码
FReport_id,//报表编号
FReportItemName, //报表项名称
FReportItemOperate,//聚合操作+、-、~
FIsDetail,//是否明细
FSelfType, //分类
FisVisible:String;//是否可见
FIsSaveOK:Boolean;
End;
procedure TfrmNewReportFromExcel.VTSetLevelSaveNode(Sender: TBaseVirtualTree;
Node: PVirtualNode; Stream: TStream);
var
Data : PVTNode;
NodeData : TReportItem;
len : Integer;
begin
with Sender do
begin
Data:=GetNodeData(Node);
NodeData:=TReportItem(Data.AObject);
len := length(NodeData.FReportItemId) ;
Stream.WriteBuffer(len,sizeof(Integer));
Stream.WriteBuffer(NodeData.FReportItemId[1],len);
len := length( NodeData.FReportItemParent) ;
Stream.WriteBuffer(len,sizeof(Integer));
Stream.WriteBuffer(NodeData.FReportItemParent[1],len);
len := length(NodeData.FReport_id);
Stream.WriteBuffer(len,sizeof(Integer));
Stream.WriteBuffer(NodeData.FReport_id[1],len);
len := length(NodeData.FReportItemName);
Stream.WriteBuffer(len,sizeof(Integer));
Stream.WriteBuffer(NodeData.FReportItemName[1],len);
len := length(NodeData.FReportItemOperate) ;
Stream.WriteBuffer(len,sizeof(Integer));
Stream.WriteBuffer(NodeData.FReportItemOperate[1],len);
len := length(NodeData.FIsDetail);
Stream.WriteBuffer(len,sizeof(Integer));
Stream.WriteBuffer(NodeData.FIsDetail[1],len);
len := length(NodeData.FSelfType) ;
Stream.WriteBuffer(len,sizeof(Integer));
Stream.WriteBuffer(NodeData.FSelfType[1],len);
len := length(NodeData.FisVisible);
Stream.WriteBuffer(len,sizeof(Integer));
Stream.WriteBuffer(NodeData.FisVisible[1],len);
Stream.WriteBuffer(NodeData.FIsSaveOK,sizeof(Boolean));
end;
end;
procedure TfrmNewReportFromExcel.VTSetLevelLoadNode(Sender: TBaseVirtualTree;
Node: PVirtualNode; Stream: TStream);
var
Data : PVTNode;
NodeData : TReportItem;
len : Integer;
tempStr : String;
begin
with Sender do
begin
NodeData := TReportItem.Create ;
Stream.ReadBuffer(len,Sizeof(Integer));
SetLength(NodeData.FReportItemId,len);
Stream.ReadBuffer(NodeData.FReportItemId[1],len);
Stream.ReadBuffer(len,Sizeof(Integer));
SetLength(NodeData.FReportItemParent,len);
Stream.ReadBuffer(NodeData.FReportItemParent[1],len);
Stream.ReadBuffer(len,Sizeof(Integer));
SetLength(NodeData.FReport_id,len);
Stream.ReadBuffer(NodeData.FReport_id[1],len);
Stream.ReadBuffer(len,Sizeof(Integer));
SetLength(NodeData.FReportItemName,len);
Stream.ReadBuffer(NodeData.FReportItemName[1],len);
Stream.ReadBuffer(len,Sizeof(Integer));
SetLength(NodeData.FReportItemOperate,len);
Stream.ReadBuffer(NodeData.FReportItemOperate[1],len);
Stream.ReadBuffer(len,Sizeof(Integer));
SetLength(NodeData.FIsDetail,len);
Stream.ReadBuffer(NodeData.FIsDetail[1],len);
Stream.ReadBuffer(len,Sizeof(Integer));
SetLength(NodeData.FSelfType,len);
Stream.ReadBuffer(NodeData.FSelfType[1],len);
Stream.ReadBuffer(len,Sizeof(Integer));
SetLength(NodeData.FisVisible,len);
Stream.ReadBuffer(NodeData.FisVisible[1],len);
Stream.ReadBuffer(NodeData.FIsSaveOK,Sizeof(Boolean));
Data:=GetNodeData(Node);
Data.AObject:=NodeData;
end;
end;
实现了OnSaveNode后,我们还是截取了Stream中一部分二进制内容,此时,我们意外地发现,有一些我们能看得明白信息,例如“Z01.01,Z01,001,货币资金,Account,1”,这正是我们写进Stream中的类的内容,于是我们估计,那些我们看不明白的内容,应该就是保存树的结构等的信息了。
到这里,我们已经可以成功地保存与还原stream中的信息到TVirtualStringTree中了。在dephi中,对象转化成Stream是如此,在其他的编程语言上,也是同样的处理方式,这种技术,就叫做对象的串行化了。