Delphi's component read and write mechanism (I)
1. Introduction to streaming objects (Streams) and read-write objects (Filers)
In object-oriented programming, object-based data management occupies a very important position. In Delphi, the support method of object-based data management is one of its major features.
Delphi is an integrated development environment that combines object-oriented visual design with object-oriented languages. The core of Delphi is the components. Components are a type of object. Delphi applications are entirely constructed by components, so developing high-performance Delphi applications will inevitably involve object-based data management technology.
Object-based data management includes two aspects:
● Use objects to manage data
● Management of various data objects (including objects and components)
Delphi attributes object-based data management classes to Stream objects (Stream) and Filer objects (Filers), and applies them to all aspects of the Visual Component Class Library (VCL). They provide rich functions to manage objects in memory, external memory and Windows resources.
Stream object, also known as streaming object, is a general term for TStream, THandleStream, TFileStream, TMemoryStream, TResourceStream and TBlobStream. They represent the ability to store data on various media, abstracting management operations of various data types (including objects and components) in memory, out-of-memory, and database fields into object methods, and make full use of object-oriented technology Advantages of this, applications can copy data in various Stream objects fairly easily.
Read and write objects (Filers) include TFiler objects, TReader objects and TWriter objects. The TFiler object is the basic object for file reading and writing, and the main uses of TReader and TWriter in applications. Both TReader and TWriter objects are inherited directly from TFiler objects. The TFiler object defines the basic properties and methods of the Filer object.
Filer objects mainly perform two major functions:
● Access form files and components in form files
●Provide data buffering to speed up data reading and writing operations
In order to have a perceptual understanding of streaming objects and read and write objects, let’s first look at an example.
a) Write a file
PRocedure TFomr1.WriteData (Sender: TObject); r;
Var
FileStream:TFilestream;
Mywriter:TWriter;
i: integer
Begin
FileStream:=TFilestream.create('c:/Test.txt',fmopenwrite);//Create a file stream object
Mywriter:=TWriter.create(FileStream,1024); //Contact Mywriter and FileStream
Mywriter.writelistbegin; //Write list start flag
For i:=0 to Memo1.lines.count-1 do
Mywriter.writestring(memo1.lines[i]); //Save text information from Memo component into a file
Mywriter.writelistend; //Write list end flag
FileStream.seek(0,sofrombeginning); //Move the file stream object pointer to the stream start position
Mywriter.free; //Release Mywriter object
FileStream.free; //Release FileStream object
End;
b) Read the file
procedure TForm1.ReadData(Sender: TObject);
Var
FileStream:TFilestream;
Myreader:TReader;
Begin
FileStream:=TFilestream.create('c:/Test.txt',fmopenread);
Myreader:=TRreader.create(FileStream,1024); //Contact Myreader and FileStream
Myreader.readlistbegin; //Read out the written list start flag
Memo1.lines.clear; //Clear the text content of the Memo1 component
While not myreader.endoflist do //Note a method of TReader:endoflist
Begin
Memo1.lines.add(myreader.readstring); //Add the read string to the Memo1 component
End;
Myreader.readlistend; //Read out the end flag of the written list
Myreader.free; //Release Myreader object
FileStream.free; //Release FileStream object
End;
The above two processes are one for the writing process and the other for the reading process. The writing process uses TWriter to save the content (text information) in a Memo as a binary file saved on disk using TFilestream. The reading process is just the opposite of the writing process. Through TReader, TFilestream is used to convert the contents in the binary file into text information and display it in the Memo. Running the program can see that the reading process faithfully restores the information saved in the writing process.
The following figure describes the relationship between data objects (including objects and components), streaming objects, and read and write objects.
Figure (1)
It is worth noting that read and write objects such as TFiler objects, TReader objects and TWriter objects are rarely called directly by application writers. They are usually used to read and write the state of components, and they play a very important role in the read and write component mechanism. Important role.
For streaming object Stream, many reference materials are introduced in detail, while reference materials for TFiler objects, TReader objects and TWriter objects, especially component read and write mechanisms are rare. This article will track the original VCL code Analysis of component reading and writing mechanisms.
2. Reading and writing objects (Filer) and component reading and writing mechanism
The Filer object is mainly used to access Delphi's form files and components in the form files. Therefore, to clearly understand the Filer object, you must be clear about the structure of the Delphi form files (DFM files).
DFM files are used for Delphi storage forms. Forms are the core of Delphi visual programming. The form corresponds to the window in the Delphi application, the visual components in the form correspond to the interface elements in the window, and non-visual components such as TTimer and TOpenDialog, corresponding to a certain function of the Delphi application. The design of the Delphi application is actually centered on the design of the form. Therefore, DFM files also occupy a very important position in Delphi application design. All elements in the form, including the form's own properties, are included in the DFM file.
In the Delphi application window, interface elements are interconnected by ownership relationships, so the tree structure is the most natural expression; accordingly, the components in the form are also organized by the tree structure; correspondingly in the DFM file, To express this relationship. DFM files are physically stored in text (previously stored as binary files in Delphi2.0), and logically, they arrange the relationships of each component in a tree structure. From this text, you can see the tree structure of the form. Here is the content of the DFM file:
object Form1: TForm1
Left = 197
Top = 124
...
PixelsPerInch = 96
TextHeight = 13
object Button1: TButton
Left = 272
...
Caption = 'Button1'
TabOrder = 0
end
object Panel1: TPanel
Left = 120
...
Caption = 'Panel1'
TabOrder = 1
object CheckBox1: TCheckBox
Left = 104
...
Caption = 'CheckBox1'
TabOrder = 0
end
end
end
This DFM file is generated by TWriter through the streaming object Stream. Of course, there is also a conversion process from binary files to text information files. This conversion process is not the object to be studied in this article, so such a process is ignored.
When the program starts running, TReader reads the form and components through the stream object Stream, because when Delphi compiles the program, it uses the compilation instruction {$R *.dfm} to compile the DFM file information into the executable file using the compilation instruction {$R *.dfm}. , so what TReader reads is actually information about the form and components that are compiled into the executable.
TReader and TWriter can not only read and write most standard data types in Object Pascal, but also read and write advanced types such as List and Variant, and even read and write Perperties and Components. However, TReader and TWriter themselves actually provide very limited functions, and most of the actual work is done by TStream, a very powerful class. In other words, TReader and TWriter are actually just tools, which are only responsible for how to read and write components. As for the specific read and write operations, TStream is done.
Since TFiler is a public ancestor class of TReader and TWriter, to understand TReader and TWriter, start with TFiler.
TFiler
Let’s first look at the definition of the TFiler class:
TFiler = class(TObject)
Private
FStream: TStream;
FBuffer: Pointer;
FBufSize: Integer;
FBufPos: Integer;
FBufEnd: Integer;
FRoot: TComponent;
FLookupRoot: TComponent;
FAncestor: TPersistent;
FIgnoreChildren: Boolean;
protected
procedure SetRoot(Value: TComponent); virtual;
public
constructor Create(Stream: TStream; BufSize: Integer);
destructor Destroy; override;
procedure DefineProperty(const Name: string;
ReadData: TReaderProc; WriteData: TWriterProc;
HasData: Boolean); virtual; abstract;
procedure DefineBinaryProperty(const Name: string;
ReadData, WriteData: TStreamProc;
HasData: Boolean); virtual; abstract;
procedure FlushBuffer; virtual; abstract;
property Root: TComponent read FRoot write SetRoot;
property LookupRoot: TComponent read FLookupRoot;
property Ancestor: TPersistent read FAncestor write FAncestor;
property IgnoreChildren: Boolean read FIgnoreChildren write FIgnoreChildren;
end;
The TFiler object is an abstract class of TReader and TWriter, which defines the basic properties and methods used for component storage. It defines the Root attribute. Root specifies the root object of the component read or written. Its Create method uses the Stream object as an incoming parameter to establish a connection with the Stream object. The specific read and write operations of the Filer object are all made by the Stream object. Finish. Therefore, as long as the media that is accessible to the Stream object, the component can be accessed by the Filer object.
The TFiler object also provides two public methods that define properties: DefineProperty and DefineBinaryProperty, which enable the object to read and write properties that are not defined in the published part of the component. Let’s focus on these two methods below.
The Defineproperty ( ) method is used to persist standard data types such as strings, integers, booleans, characters, floating points, and enums.
In the Defineproperty method. The Name parameter specifies the name of the attribute that should be written to the DFM file, which is not defined in the published part of the class.
The ReadData and WriteData parameters specify the method to read and write the required data when accessing an object. The types of ReadData parameters and WriteData parameters are TReaderProc and TWriterProc respectively. These two types are declared like this:
TReaderProc = procedure(Reader: TReader) of object;
TWriterProc = procedure(Writer: TWriter) of object;
The HasData parameter determines whether the property has data to be stored at runtime.
The DefineBinaryProperty method has many similarities with Defineproperty. It is used to store binary data, such as sound and images.
Let’s explain the uses of these two methods below.
We put a non-visual component such as TTimer on the form. When we reopen the form, we found that the TTimer is still in its original place, but the TTimer does not have Left and Top attributes, so where is its location information stored?
Open the DFM file of this form and you can see several lines similar to the following:
object Timer1: TTimer
Left = 184
Top = 149
end
Delphi's streaming system can only save published data, but TTimer does not have published Left and Top attributes, so how are these data saved?
TTimer is a derived class of TComponent. In the TComponent class, we found that there is such a function:
procedure TComponent.DefineProperties(Filer: TFiler);
var
Ancestor: TComponent;
Info: Longint;
Begin
Info := 0;
Ancestor := TComponent(Filer.Ancestor);
if Ancestor <> nil then Info := Ancestor.FDesignInfo;
Filer.DefineProperty('Left', ReadLeft, WriteLeft,
LongRec(FDesignInfo).Lo <> LongRec(Info).Lo);
Filer.DefineProperty('Top', ReadTop, WriteTop,
LongRec(FDesignInfo).Hi <> LongRec(Info).Hi);
end;
TComponent's DefineProperties is a virtual method that overwrites its ancestor class TPersistent, and in the TPersistent class this method is an empty virtual method.
In the DefineProperties method, we can see that there is a Filer object as its parameter. When defining a property, it references the Ancestor property. If the property is not empty, the object should only read and write different properties inherited from the Ancestor. value. It calls the DefineProperty method of TFiler and defines ReadLeft, WriteLeft, ReadTop, WriteTop methods to read and write Left and Top properties.
Therefore, any component derived from TComponent, even if it does not have Left and Top attributes, will have two such properties when streaming to a DFM file.
In the process of searching for data, it was found that few data involve component reading and writing mechanisms. Since the component writing process is completed by Delphi's IDE during the design stage, it cannot be tracked for its running process. Therefore, the author understands the component's reading mechanism by tracking the original VCL code during the program operation, and analyzes the component's writing mechanism through the reading mechanism and TWriter. Therefore, the following will explain the component reading and writing mechanism according to this thinking process, first talking about TReader, and then TWriter.
TReader
First look at Delphi's project files and you will find a few lines of code like this:
Begin
application.Initialize;
Application.CreateForm(TForm1, Form1);
Application.Run;
end.
This is the entrance to the Delphi program. Simply put the meaning of these lines of code: Application.Initialize performs some necessary initialization work on the start of running applications. Application.CreateForm(TForm1, Form1) creates the necessary form. The Application.Run program starts running and enters the message cycle.
What we care most about now is the sentence of creating a form. How are forms and components on the forms created? As mentioned earlier: all components in the form, including the properties of the form itself, are included in the DFM file. When Delphi compiles the program, he uses the compilation command {$R *.dfm} to compile the DFM file information. into the executable file. Therefore, it can be concluded that when creating a form, you need to read DFM information. What should you use to read it? Of course it is a TReader!
By following the program step by step, you can find that the program calls the ReadRootComponent method of TReader during the creation of the form. The purpose of this method is to read out the root component and all the components it has. Let’s take a look at the implementation of this method:
function TReader.ReadRootComponent(Root: TComponent): TComponent;
...
Begin
ReadSignature;
Result := nil;
GlobalNameSpace.BeginWrite; // Loading from stream adds to name space
try
try
ReadPrefix(Flags, I);
if Root = nil then
Begin
Result := TComponentClass(FindClass(ReadStr)).Create(nil);
Result.Name := ReadStr;
end else
Begin
Results := Root;
ReadStr; { Ignore class name }
if csDesigning in Result.ComponentState then
ReadStr else
Begin
Include(Result.FComponentState, csLoading);
Include(Result.FComponentState, csReading);
Result.Name := FindUniqueName(ReadStr);
end;
end;
FRoot := Results;
FFinder := TClassFinder.Create(TPersistentClass(Result.ClassType), True);
try
FLookupRoot := Results;
G := GlobalLoaded;
if G <> nil then
FLoaded := G else
FLoaded := TList.Create;
try
if FLoaded.IndexOf(FRoot) < 0 then
FLoaded.Add(FRoot);
FOwner := FRoot;
Include(FRoot.FComponentState, csLoading);
Include(FRoot.FComponentState, csReading);
FRoot.ReadState(Self);
Exclude(FRoot.FComponentState, csReading);
if G = nil then
for I := 0 to FLoaded.Count - 1 do TComponent(FLoaded[I]).Loaded;
Finally
if G = nil then FLoaded.Free;
FLoaded := nil;
end;
Finally
FFinder.Free;
end;
...
Finally
GlobalNameSpace.EndWrite;
end;
end;
ReadRootComponent first calls ReadSignature to read the Filer object tag ('TPF0'). Detecting tags before loading objects can prevent negligence and ineffective or outdated data reading.
Let’s take a look at the ReadPrefix(Flags, I). The function of the ReadPrefix method is very similar to that of ReadSignature, except that it is the logo (PreFix) in front of the component in the read stream. When a Write object writes a component to a stream, it prewrites two values in front of the component. The first value indicates whether the component is a form inherited from the ancestor form and whether it is in the form. Important flag; the second value indicates the order it was created in the ancestor form.
Then, if the Root parameter is nil, a new component is created with the class name read out by ReadStr, and the component's Name property is read from the stream; otherwise, the class name is ignored and the uniqueness of the Name property is judged.
FRoot.ReadState(Self);
This is a very critical sentence. The ReadState method reads the properties of the root component and the components it owns. Although this ReadState method is a TComponent method, further tracking can be found that it actually finally located the ReadDataInner method of TReader. The implementation of this method is as follows:
procedure TReader.ReadDataInner(Instance: TComponent);
var
OldParent, OldOwner: TComponent;
Begin
while not EndOfList do ReadProperty(Instance);
ReadListEnd;
OldParent := Parent;
OldOwner := Owner;
Parent := Instance.GetChildParent;
try
Owner := Instance.GetChildOwner;
if not Assigned(Owner) then Owner := Root;
while not EndOfList do ReadComponent(nil);
ReadListEnd;
Finally
Parent := OldParent;
Owner := OldOwner;
end;
end;
There is this line of code:
while not EndOfList do ReadProperty(Instance);
This is used to read the properties of the root component. As mentioned earlier, there are both published properties of the component itself and non-published properties, such as Left and Top of TTimer. For these two different properties, there should be two different reading methods. In order to verify this idea, let's take a look at the implementation of the ReadProperty method.
procedure TReader.ReadProperty(AInstance: TPersistent);
...
Begin
...
PropInfo := GetPropInfo(Instance.ClassInfo, FPropName);
if PropInfo <> nil then ReadPropValue(Instance, PropInfo) else
Begin
{ Cannot reliably recover from an error in a defined property }
FCanHandleExcepts := False;
Instance.DefineProperties(Self);
FCanHandleExcepts := True;
if FPropName <> '' then
PropertyError(FPropName);
end;
...
end;
In order to save space, some code has been omitted. Here is an explanation: FPropName is the attribute name read from the file.
PropInfo := GetPropInfo(Instance.ClassInfo, FPropName);
This code is to obtain the information of the published property FPropName. From the following code, we can see that if the attribute information is not empty, the attribute value is read through the ReadPropValue method, and the ReadPropValue method reads the attribute value through the RTTI function, which will not be introduced in detail here. If the attribute information is empty, it means that the attribute FPropName is not published, and it must be read through another mechanism. This is the DefineProperties method mentioned above, as follows:
Instance.DefineProperties(Self);
This method actually calls the DefineProperty method of TReader:
procedure TReader.DefineProperty(const Name: string;
ReadData: TReaderProc; WriteData: TWriterProc; HasData: Boolean);
Begin
if SameText(Name, FPropName) and Assigned(ReadData) then
Begin
ReadData(Self);
FPropName := '';
end;
end;
It first compares whether the read attribute name is the same as the preset attribute name. If it is the same and the reading method ReadData is not empty, call the ReadData method to read the attribute value.
OK, the root component has been read, and the next step should be to read the components owned by the root component. Let’s look at the method again:
procedure TReader.ReadDataInner(Instance: TComponent);
There is a code following this method:
while not EndOfList do ReadComponent(nil);
This is exactly what is used to read the child components. The reading mechanism of the child component is the same as the reading of the root component introduced above, which is a deep traversal of a tree.
So far, the component reading mechanism has been introduced.
Let’s look at the component writing mechanism. When we add a component to the form, its related properties will be saved in the DFM file, and this process is done by TWriter.
Ø TWriter
A TWriter object is an instantiable Filer object that writes data into a stream. The TWriter object is inherited directly from TFiler. In addition to overwriting the methods inherited from TFiler, it also adds a large number of methods for writing various data types (such as Integer, String, Component, etc.).
The TWriter object provides many methods to write various types of data into the stream. The TWrite object writes data into the stream in different formats according to different data. Therefore, to master the implementation and application methods of TWriter objects, you must understand the format of the Writer objects storing data.
The first thing to note is that each Filer object stream contains Filer object tags. This tag takes up four bytes and its value is "TPF0". The Filer object accesses the tag for WriteSignature and ReadSignature methods. This tag is mainly used to guide reading operations when Reader objects read data (components, etc.).
Secondly, the Writer object must leave a byte flag bit before storing data to indicate what type of data is stored later. This byte is a value of type TValueType. TValueType is an enum type that occupies one byte space, and its definition is as follows:
TValueType = (VaNull, VaList, VaInt8, VaInt16, VaInt32, VaEntended, VaString, VaIdent,
VaFalse, VaTrue, VaBinary, VaSet, VaLString, VaNil, VaCollection);
Therefore, in the implementation of each data writing method of the Writer object, you must first write the flag bit and then write the corresponding data; and each data reading method of the Reader object must first read the flag bit to judge if it meets the reading data. , otherwise an exception event with invalid reading data is generated. The VaList logo has a special purpose. It is used to identify a series of items of the same type after it will be followed by, and the logo that identifies the end of a continuous item is VaNull. Therefore, when writing several consecutive identical items in the Writer object, first use WriteListBegin to write the VaList flag, and after writing the data items, then write the VaNull flag; and when reading these data, start with ReadListBegin, end with ReadListEnd, and use the EndofList function in the middle Determine whether there is a VaNull flag.
Let's take a look at a very important method for TWriter WriteData:
procedure TWriter.WriteData(Instance: TComponent);
...
Begin
...
WritePrefix(Flags, FChildPos);
if UseQualifiedNames then
WriteStr(GetTypeData(PTypeInfo(Instance.ClassType.ClassInfo)).UnitName + '.' + Instance.ClassName)
else
WriteStr(Instance.ClassName);
WriteStr(Instance.Name);
PropertiesPosition := Position;
if (FAncestorList <> nil) and (FAncestorPos < FAncestorList.Count) then
Begin
if Ancestor <> nil then Inc(FAncestorPos);
Inc(FChildPos);
end;
WriteProperties(Instance);
WriteListEnd;
...
end;
From the WriteData method, we can see the general picture of generating DFM file information. First write the flag (PreFix) in front of the component, and then write the class name and instance name. Then there is a sentence like this:
WriteProperties(Instance);
This is used to write the properties of the component. As mentioned earlier, in DFM files, there are both published attributes and non-published attributes. The writing methods of these two attributes should be different. Let's take a look at the implementation of WriteProperties:
procedure TWriter.WriteProperties(Instance: TPersistent);
...
Begin
Count := GetTypeData(Instance.ClassInfo)^.PropCount;
if Count > 0 then
Begin
GetMem(PropList, Count * SizeOf(Pointer));
try
GetPropInfos(Instance.ClassInfo, PropList);
for I := 0 to Count - 1 do
Begin
PropInfo := PropList^[I];
if PropInfo = nil then
Break;
If IsStoredProp(Instance, PropInfo) then
WriteProperty(Instance, PropInfo);
end;
Finally
FreeMem(PropList, Count * SizeOf(Pointer));
end;
end;
Instance.DefineProperties(Self);
end;
Please see the following code:
If IsStoredProp(Instance, PropInfo) then
WriteProperty(Instance, PropInfo);
The function IsStoredProp determines whether the property needs to be saved by storing the qualifier. If it needs to be saved, call WriteProperty to save the property, and WriteProperty is implemented through a series of RTTI functions.
After saving the Published property, the non-Published property must be saved. This is done through this code:
Instance.DefineProperties(Self);
The implementation of DefineProperties has been mentioned before. The Left and Top properties of TTimer are saved through it.
OK, so far there is still a question: how do the child components owned by the root component be saved? Let’s look at the WriteData method (this method was mentioned earlier):
procedure TWriter.WriteData(Instance: TComponent);
...
Begin
...
If not IgnoreChildren then
try
if (FAncestor <> nil) and (FAncestor is TComponent) then
Begin
if (FAncestor is TComponent) and (csInline in TComponent(FAncestor).ComponentState) then
FRootAncestor := TComponent(FAncestor);
FAncestorList := TList.Create;
TComponent(FAncestor).GetChildren(AddAncestor, FRootAncestor);
end;
if csInline in Instance.ComponentState then
FRoot := Instance;
Instance.GetChildren(WriteComponent, FRoot);
Finally
FAncestorList.Free;
end;
end;
The IgnoreChildren property enables a Writer object to store a component without storing child components owned by the component. If the IgnoreChildren property is True, the Writer object does not store the child components it has when storing the component. Otherwise, subcomponents will be stored.
Instance.GetChildren(WriteComponent, FRoot);
This is the most critical sentence for writing subcomponents. It uses the WriteComponent method as a callback function and according to the principle of traversing the tree with depth priority, if the root component FRoot has a subcomponent, WriteComponent is used to save its subcomponents. In this way, what we see in the DFM file is a tree-like component structure.