Ø TReader First look at the Delphi project file, 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. Let’s briefly talk about the meaning of these lines of code: Application.Initialize performs some necessary initialization work for the running application, Application.CreateForm(TForm1, Form1) creates the necessary forms, and the Application.Run program starts running and enters the message cycle. What we are most concerned about now is the sentence of creating a form. How are forms and components on the form created? As mentioned before: all components in the form, including the properties of the form itself, are included in the DFM file. When Delphi compiles the program, it uses the compilation instruction {$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 the DFM information. What should you use to read it? Of course it is TReader! By tracing the program step by step, we can find that the program calls the ReadRootComponent method of TReader during the process of creating the form. The function of this method is to read the root component and all the components it owns. 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 Result := 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 := Result; FFinder := TClassFinder.Create(TPersistentClass(Result.ClassType), True); try FLookupRoot := Result; 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 prevents inadvertent reading of invalid or outdated data. Take another look at the sentence ReadPrefix(Flags, I). The function of the ReadPrefix method is very similar to that of ReadSignature, except that it reads the flag (PreFix) in front of the component in the stream. When a Write object writes a component into the stream, it pre-writes two values in front of the component. The first value indicates whether the component is a form inherited from an ancestor form and whether its position in the form is Important flag; the second value specifies the order in which the ancestor form was created. Then, if the Root parameter is nil, create a new component with the class name read by ReadStr, and read the component's Name attribute from the stream; otherwise, ignore the class name and determine the uniqueness of the Name attribute. FRoot.ReadState(Self); This is a very critical sentence. The ReadState method reads the properties of the root component and its owned components. Although this ReadState method is a method of TComponent, further tracking can reveal that it actually eventually locates 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 before, there are both published properties of the component itself and non-published properties, such as TTimer's Left and Top. 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 a description: FPropName is read from the file attribute name. PropInfo := GetPropInfo(Instance.ClassInfo, FPropName); This code is to obtain the information of the published attribute FPropName. As you can see from the following code, 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 described in detail here. If the attribute information is empty, it means that the attribute FPropName is unpublished and it must be read through another mechanism. This is the DefineProperties method mentioned earlier, 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 they are the same and the read method ReadData is not empty, the ReadData method is called to read. attribute value. Okay, the root component has been read, and the next step is to read the components owned by the root component. Let’s look at the method: procedure TReader.ReadDataInner(Instance: TComponent); There is a code like this after this method: while not EndOfList do ReadComponent(nil); This is used to read subcomponents. The reading mechanism of subcomponents is the same as the reading of the root component introduced above, which is a deep traversal of the tree. So far, the component reading mechanism has been introduced. Let’s look at the writing mechanism of components. When we add a component to the form, its related properties will be saved in the DFM file. This process is completed by TWriter. Ø TWriter The TWriter object is an instantiable Filer object that writes data to the stream. The TWriter object directly inherits from TFiler. In addition to overriding 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 for writing various types of data into the stream. The TWrite object writes data into the stream in different formats based on different data. Therefore, to master the implementation and application methods of the TWriter object, you must understand the format in which the Writer object stores data. The first thing to note is that the stream of each Filer object contains a Filer object tag. This tag occupies four bytes and its value is "TPF0". The Filer object accesses the tag for the WriteSignature and ReadSignature methods. This label is mainly used to guide the reading operation when the Reader object reads data (components, etc.). Secondly, the Writer object must leave a byte flag before storing data to indicate what type of data is stored later. This byte is a value of type TValueType. TValueType is an enumeration type that occupies one byte space and is defined as follows: TValueType = (VaNull, VaList, VaInt8, VaInt16, VaInt32, VaEntended, VaString, VaIdent, VaFalse, VaTrue, VaBinary, VaSet, VaLString, VaNil, VaCollection); Therefore, for every data writing method of the Writer object, in terms of implementation, the flag bit must be written first and then the corresponding data; and every data reading method of the Reader object must first read the flag bit for judgment. If it matches the read data , otherwise an exception event indicating that the read data is invalid is generated. The VaList flag has a special purpose. It is used to identify that there will be a series of items of the same type later, and the flag that identifies the end of consecutive items is VaNull. Therefore, when the Writer object writes several consecutive identical items, it first writes the VaList flag with WriteListBegin, and then writes the VaNull flag after writing the data items; when reading these data, it starts with ReadListBegin, ends with ReadListEnd, and uses the EndofList function in the middle. Determine whether there is a VaNull flag. Let's take a look at a very important method of 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 overview of generating DFM file information. First write the flag (PreFix) in front of the component, then write the class name and instance name. Immediately following this statement: WriteProperties(Instance); This is used to write the properties of the component. As mentioned earlier, in the DFM file, there are both published attributes and non-published attributes. The writing methods of these two attributes should be different. Let's 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 uses the storage qualifier to determine whether the property needs to be saved. If it needs to be saved, call WriteProperty to save the property, and WriteProperty is implemented through a series of RTTI functions. After the Published properties are saved, the non-published properties 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. . Okay, so far there is still a question: How are the subcomponents owned by the root component 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 attribute allows a Writer object to store a component without storing the child components owned by the component. If the IgnoreChildren property is True, the Writer object stores the component without storing the child components it owns. Otherwise, the subcomponent must be stored. Instance.GetChildren(WriteComponent, FRoot); This is the most critical sentence when writing a subcomponent. It uses the WriteComponent method as a callback function and follows the principle of depth-first traversal of the tree. If the root component FRoot has a subcomponent, use WriteComponent to save it. subcomponent. In this way, what we see in the DFM file is a tree-like component structure.