[分享]一种将 Delphi 组件属性信息保存到JSON的方法

此代码支持 Delphi 2007+,使用 QJSON 做为序列化的支持库。你可以自己改为使用其它的 JSON库(注:此文写法未完善,仅为示意)。

uses typinfo, qjson, qstring;

type
  TComponentJsonHelper = class helper for TComponent
  protected
    procedure DoSaveToJson(AStream: TStream; AJson: TQJson);
    procedure DoLoadFromJson(AParent: TComponent; AJson: TQJson;
      ALoaded: TList);
  public
    procedure SaveToJson(AJson: TQJson);
    procedure LoadFromJson(AJson: TQJson);
  end;
{ TComponentJsonHelper }

procedure TComponentJsonHelper.LoadFromJson(AJson: TQJson);
var
  ALoaded: TList;
  I: Integer;
begin
  ALoaded := TList.Create;
  GlobalNameSpace.BeginWrite;
  try
    DoLoadFromJson(nil, AJson, ALoaded);
    for I := 0 to ALoaded.Count - 1 do
      TComponent(ALoaded[I]).Loaded;
    Loaded;
  finally
    GlobalNameSpace.EndWrite;
  end;
end;

procedure TComponentJsonHelper.DoLoadFromJson(AParent: TComponent;
  AJson: TQJson; ALoaded: TList);
type
  PComponentState = ^TComponentState;
var
  AChildPos: Integer;
  AList: TQJson;
  ACompState: PComponentState;
  function JsonToSet(AValues: TQJson): String;
  var
    I: Integer;
  begin
    Result := '[';
    for I := 0 to AValues.Count - 1 do
    begin
      if I = 0 then
        Result := AValues[I].AsString
      else
        Result := Result + ',' + AValues[I].AsString;
    end;
    Result := Result + ']';
  end;
  function PropByPath(var Instance: TObject; APath: String): PPropInfo;
  var
    AName: String;
    APos: Integer;
  begin
    AName := DecodeTokenW(APath, '.', #0, true, true);
    Result := GetPropInfo(Instance, AName);
    if (Length(APath) > 0) then
    begin
      if (Result.PropType^.Kind = tkClass) then
      begin
        Instance := TObject(GetOrdProp(Instance, Result));
        if Assigned(Instance) then
          Result := PropByPath(Instance, APath)
        else
          Result := nil;
      end
      else
        Result := nil;
    end;
  end;
  procedure LoadProps(ARootInstance: TObject; AProps: TQJson);
  var
    I: Integer;
    AProp: PPropInfo;
    APropJson: TQJson;
    AInstance: TObject;
  begin
    if not Assigned(ARootInstance) then
      Exit;
    for I := 0 to AProps.Count - 1 do
    begin
      APropJson := AProps[I];
      AInstance := ARootInstance;
      AProp := PropByPath(AInstance, APropJson.Name);
      if Assigned(AProp) then
      begin
        case AProp.PropType^.Kind of
          tkInteger:
            SetOrdProp(AInstance, AProp, APropJson.AsInteger);
          tkChar, tkWChar:
            begin
              if APropJson.IsString then
                SetOrdProp(AInstance, AProp, Ord(PChar(APropJson.AsString)^))
              else
                SetOrdProp(AInstance, AProp, APropJson.AsInteger);
            end;
          tkEnumeration:
            SetEnumProp(AInstance, AProp, APropJson.AsString);
          tkFloat:
            SetFloatProp(AInstance, AProp, APropJson.AsFloat);
          tkString, tkLString, tkWString, tkUString:
            SetStrProp(AInstance, AProp, APropJson.AsString);
          tkSet:
            SetSetProp(AInstance, AProp, JsonToSet(APropJson));
          tkClass:
            LoadProps(Pointer(GetOrdProp(AInstance, AProp)), APropJson);
          tkVariant:
            SetVariantProp(AInstance, AProp, APropJson.AsVariant);
          // tkArray,tkDynArray,tkRecord, tkInterface,tkClassRef,tkPointer, tkProcedure
          tkInt64:
            SetInt64Prop(AInstance, AProp, APropJson.AsInt64);
        end;
      end;
    end;
  end;

  procedure LoadChildren(AObjects: TQJson);
  var
    I: Integer;
    AChildComp: TComponent;
    AClass: TClass;
    AClassName: String;
  begin
    for I := 0 to AObjects.Count - 1 do
    begin
      AClassName := AObjects[I].ValueByName('ClassName', '');
      AClass := FindClass(AClassName);
      if AClass = TComponent then
      begin
        if Assigned(Owner) then
          AChildComp := TComponentClass(AClass).Create(Owner)
        else
          AChildComp := TComponentClass(AClass).Create(Self);
        AChildComp.DoLoadFromJson(Self, AObjects[I], ALoaded);
      end
      else
        raise EClassNotFound.CreateFmt('Class %s not found', [AClassName]);
    end;
  end;

begin
  ACompState := @ComponentState;
  ACompState^ := ACompState^ + [csLoading, csReading];
  try
    Name := AJson.ValueByName('Name', '');
    if AJson.HasChild('Props', AList) then
      LoadProps(Self, AList);
    if AJson.HasChild('Children', AList) then
      LoadChildren(AList);
  finally
    ACompState^ := ACompState^ - [csLoading, csReading];
    if Assigned(ALoaded) then
      ALoaded.Add(Self);
  end;
  if Assigned(AParent) then
  begin
    AChildPos := AJson.IntByName('ChildPos', -1);
    if AChildPos > 0 then
      AParent.SetChildOrder(Self, AChildPos);
  end;
end;

procedure TComponentJsonHelper.DoSaveToJson(AStream: TStream; AJson: TQJson);
var
  Reader: TReader;
  ObjectName, PropName: string;

procedure ConvertValue(AItem: TQJson); forward;

  procedure ConvertHeader(AItem: TQJson);
  var
    ClassName: string;
    Flags: TFilerFlags;
    Position: Integer;
  begin
    Reader.ReadPrefix(Flags, Position);
    ClassName := Reader.ReadStr;
    ObjectName := Reader.ReadStr;
    AItem.Add('Name').AsString := Name;
    AItem.Add('ClassName').AsString := ClassName;
    if ffInherited in Flags then
      AItem.Add('InstanceType').AsString := 'inherited'
    else if ffInline in Flags then
      AItem.Add('InstanceType').AsString := 'inline'
    else
      AItem.Add('InstanceType').AsString := 'object';
    if ffChildPos in Flags then
      AItem.Add('ChildPos').AsInteger := Position;
  end;

  procedure ConvertBinary(AItem: TQJson);
  var
    Count: Integer;
    Buffer: TBytes;
  begin
    Reader.ReadValue;
    Reader.Read(Count, SizeOf(Count));
    SetLength(Buffer, Count);
    Reader.Read(Buffer, Count);
    AItem.AsBytes := Buffer;
  end;

procedure ConvertProperty(AItem: TQJson); forward;

  procedure ConvertValue(AItem: TQJson);
  const
    LineLength = 64;
  var
    S: String;
  begin
    case Reader.NextValue of
      vaList:
        begin
          Reader.ReadValue;
          AItem.DataType := jdtArray;
          while not Reader.EndOfList do
            ConvertValue(AItem.Add);
        end;
      vaInt8, vaInt16, vaInt32:
        AItem.AsInteger := Reader.ReadInteger;
      vaExtended, vaDouble:
        AItem.AsFloat := Reader.ReadFloat;
      vaSingle:
        AItem.AsFloat := Reader.ReadSingle;
      vaCurrency:
        AItem.AsFloat := Reader.ReadCurrency;
      vaDate:
        AItem.AsDateTime := Reader.ReadDate;
      vaWString, vaUTF8String, vaString, vaLString:
        AItem.AsString := Reader.ReadString;
      vaIdent:
        AItem.AsString := Reader.ReadIdent;
      vaFalse:
        AItem.AsBoolean := False;
      vaTrue:
        AItem.AsBoolean := true;
      vaNil, vaNull:
        AItem.ResetNull;
      vaBinary:
        ConvertBinary(AItem);
      vaSet:
        begin
          Reader.ReadValue;
          AItem.DataType := jdtArray;
          while true do
          begin
            S := Reader.ReadStr;
            if S = '' then
              Break;
            AItem.Add.AsString := S;
          end;
        end;
      vaCollection:
        begin
          Reader.ReadValue;
          AItem.DataType := jdtObject;
          while not Reader.EndOfList do
          begin
            with AItem.AddArray('items') do
            begin
              if Reader.NextValue in [vaInt8, vaInt16, vaInt32] then
                ConvertValue(Add);
            end;
            Reader.CheckValue(vaList);
            while not Reader.EndOfList do
              ConvertProperty(AItem);
          end;
          Reader.ReadListEnd;
        end;
      vaInt64:
        AItem.AsInt64 := Reader.ReadInt64;
    else
      raise EReadError.CreateFmt('Can not read %s property %s',
        [ObjectName, PropName]);
    end;
  end;

  procedure ConvertProperty(AItem: TQJson);
  begin
    ConvertValue(AItem.Add(Reader.ReadStr));
  end;

  procedure ConvertObject(AItem: TQJson);
  var
    AObject, AList: TQJson;
  begin
    ConvertHeader(AItem);
    AList := AItem.Add('Props');
    while not Reader.EndOfList do
      ConvertProperty(AList);
    Reader.ReadListEnd;
    AList := AItem.Add('Children');
    AList.DataType := jdtObject;
    while not Reader.EndOfList do
      ConvertObject(AList);
    Reader.ReadListEnd;
  end;

begin
  Reader := TReader.Create(AStream, 4096);
  try
    Reader.ReadSignature;
    ConvertObject(AJson);
  finally
    Reader.Free;
  end;
end;

procedure TComponentJsonHelper.SaveToJson(AJson: TQJson);
var
  AStream: TMemoryStream;
begin
  AStream := TMemoryStream.Create;
  try
    AStream.WriteComponent(Self);
    AStream.Position := 0;
    DoSaveToJson(AStream, AJson);
  finally
    FreeAndNil(AStream);
  end;
end;

使用示例:

procedure TForm1.Button1Click(Sender: TObject);
var
  AJson: TQJson;
begin
  AJson := AcquireJson;
  try
    LabeledEdit1.SaveToJson(AJson);
    ShowMessage(AJson.AsJson);
    LabeledEdit1.EditLabel.Caption := 'Hello';
    LabeledEdit1.Text := '';
    Button2.Caption:='Json Converted';
    ShowMessage('from json');
    LabeledEdit1.LoadFromJson(AJson);
  finally
    ReleaseJson(AJson);
  end;
end;

如上面的示例,由于写成了 class helper,所以保存到 Json 里,直接调用组件实例的 SaveToJson 就可以了,而从 Json 中恢复则直接调用组件实例的 LoadFromJson 就可以了。

上面的代码加入 C++Builder,然后可以直接引用。

分享到: