341 lines
9.2 KiB
ObjectPascal

// SPDX-License-Identifier: LGPL-3.0-linking-exception
{ Useful tools for RTTI. Functions are used expecialy for save/load styles.
Styles has construction similar to INI files:
[Header]
Author=Krzysztof Dibowski
Description=My test style
ControlClass=TBCButton
[Properties]
State.Border.Width=2
.....
But instead of IniFiles unit, we have own functions for read and write styles.
------------------------------------------------------------------------------
originally written in 2012 by Krzysztof Dibowski dibowski at interia.pl
}
{******************************* CONTRIBUTOR(S) ******************************
- Edivando S. Santos Brasil | mailedivando@gmail.com
(Compatibility with delphi VCL 11/2018)
***************************** END CONTRIBUTOR(S) *****************************}
unit BCRTTI;
{$I bgracontrols.inc}
interface
uses
Classes;
type
PBCStyleHeader = ^TBCStyleHeader;
TBCStyleHeader = record
Author: String;
ControlClass: String;
Description: String;
end;
// Function return data of specified section (header, properties, etc).
// This is smart function, because it doesn't read whole file but read file
// line by line and return only needed section. So it should fastest for reading
// header info instead of TIniFile object which read, parse and index all file.
function GetSectionData(const AFileName, ASectionName: String): TStrings;
// Methods which read header from list or file and parse it into pascal record
procedure GetStyleHeader(const AFileName: String; AOutHeader: PBCStyleHeader);
// Function check if specified name is on ignored list
function IsPropIgnored(const AName: String): Boolean;
// Method load style saved by SaveStyle method
procedure LoadStyle(AControl: TObject; const AFileName: String; ALogs: TStrings = nil);
// Method save all (which are not on ignored list or readonly) public propertys to
// the output string list. This method have support for property
// tree (Propert1.Subpropert1.Color = 543467). Values are represented as "human readable"
// (e.g. Align = alClient). Header info is save too.
procedure SaveStyle(AControl: TObject; const AAuthor, ADescription: String;
ATargetList: TStrings);
implementation
uses typinfo, variants, sysutils, {%H-}strutils;
const
tIGNORED_PROPS: array[0..5] of string =
('name','caption','left','top','height','width');
sSECTION_HEADER_NAME = 'HEADER';
sSECTION_PROP_NAME = 'PROPERTIES';
sSECTION_HEADER = '['+sSECTION_HEADER_NAME+']';
sSECTION_PROP = '['+sSECTION_PROP_NAME+']';
procedure RemovePadChars(var S: String; const CSet: TSysCharset);
var
I,J,K: LONGINT;
begin
I:=Length(S);
IF (I>0) Then
Begin
J:=I;
While (j>0) and (S[J] IN CSet) DO DEC(J);
if j=0 Then
begin
s:='';
exit;
end;
k:=1;
While (k<=I) And (S[k] IN CSet) DO
INC(k);
IF k>1 Then
begin
move(s[k],s[1],j-k+1);
setlength(s,j-k+1);
end
else
setlength(s,j);
end;
end;
function TrimSet(const S: String;const CSet:TSysCharSet): String;
begin
result:=s;
RemovePadChars(result,cset);
end;
function IsPropIgnored(const AName: String): Boolean;
var
i: Integer;
begin
Result := False;
for i := Low(tIGNORED_PROPS) to High(tIGNORED_PROPS) do
if SameText(tIGNORED_PROPS[i],Trim(AName)) then
Exit(True);
end;
procedure LoadStyle(AControl: TObject; const AFileName: String;
ALogs: TStrings = nil);
var
i, iDot: Integer;
sPath, sVal: String;
obj: TObject;
sl: TStrings;
const
sLOG_NO_PROP = 'Can not find property "%s"';
sLOG_SET_ERR = 'Can not set value "%s" to property "%s"';
sLOG_READ_ONLY = 'Property "%s" is read-only';
procedure _AddLog(const AText: String);
begin
if ALogs<>nil then
ALogs.Add(AText);
end;
function _ValidateProp(AObj: TObject; const APropName: String): Boolean;
begin
Result := True;
// If can't find property
if not IsPublishedProp(AObj,APropName) then
begin
_AddLog(Format(sLOG_NO_PROP,[APropName]));
Exit(False);
end;
// If read-only property
if (GetPropInfo(AObj,APropName)^.SetProc=nil) then
begin
_AddLog(Format(sLOG_READ_ONLY,[APropName]));
Exit(False);
end;
end;
begin
if not FileExists(AFileName) then
Exit;
if ALogs<>nil then
ALogs.Clear;
sl := GetSectionData(AFileName, sSECTION_PROP_NAME);
try
for i:=0 to Pred(sl.Count) do
begin
// Full path with hierarchy tree
sPath := Trim(sl.Names[i]);
// "Human readable" value
sVal := Trim(sl.ValueFromIndex[i]);
iDot := Pos('.', sPath);
// If simple property then write it value
if iDot=0 then
begin
if not _ValidateProp(AControl,sPath) then
Continue;
// Writting property value
try
SetPropValue(AControl,sPath,sVal)
except
_AddLog(Format(sLOG_SET_ERR,[sVal, sPath]));
end
end
else
begin
//... else we must go down in hierarchy tree to the last
// object and then write value to property
obj := AControl;
while iDot>0 do
begin
if not _ValidateProp(obj,Copy(sPath,1,iDot-1)) then
begin
obj := nil;
Break;
end;
obj := GetObjectProp(obj,Copy(sPath,1,iDot-1));
Delete(sPath,1,iDot);
iDot := Pos('.', sPath);
end;
// If no dots, then this word is property name
if (obj<>nil) and (sPath<>'') and _ValidateProp(obj,sPath) then
begin
try
SetPropValue(obj,sPath,sVal)
except
_AddLog(Format(sLOG_SET_ERR,[sVal, sPath]));
end
end;
end;
end;
finally
sl.Free;
end;
end;
procedure SaveStyle(AControl: TObject; const AAuthor, ADescription: String;
ATargetList: TStrings);
procedure _SaveProp(AObj: TObject; APath: String = '');
var
iCount, i: Integer;
lst: TPropList;
s: String;
begin
if AObj=nil then Exit;
iCount := GetPropList(PTypeInfo(AObj.ClassInfo), tkProperties, @lst);
for i := 0 to Pred(iCount) do
{ Notice:
- IsPublishedProp return true for ALL public properties, not only
for properties in Published section. For saving styles, we don't need
all public properties, but only published (visible in object inspector).
I don't know if this is a bug, I leave it. Maybe it will start
working in future ;)
- Second argument check if property should be ignored (but only from root tree),
because we can't save basic properties of control like Name, Top, Left etc.
- SetProc<>nil mean "not read only"
}
if IsPublishedProp(AObj,lst[i]^.Name) and
((AControl<>AObj) or (not IsPropIgnored(lst[i]^.Name))) and
(lst[i]^.SetProc<>nil)
then
begin
// Building property tree
if APath=''
then s := lst[i]^.Name
else s := APath+'.'+lst[i]^.Name;
// If property has subproperty, then we start recurrence to
// build hierarchy tree.
if (lst[i]^.PropType^.Kind = tkClass) then
_SaveProp(GetObjectProp(AObj,lst[i]),s)
else
begin
// We are in bottom node, so we can save final property with value
s := s + ' = ' + String(GetPropValue(AObj,lst[i]^.Name,True));
ATargetList.Add(s);
end;
end;
end;
begin
if ATargetList=nil then
Exit;
ATargetList.Clear;
ATargetList.Add(sSECTION_HEADER);
ATargetList.Add('Author='+AAuthor);
ATargetList.Add('Description='+ADescription);
ATargetList.Add('ControlClass='+AControl.ClassName);
ATargetList.Add('');
ATargetList.Add(sSECTION_PROP);
_SaveProp(AControl);
end;
function GetSectionData(const AFileName, ASectionName: String): TStrings;
var
f: TextFile;
s: String;
bReading: Boolean;
begin
Result := TStringList.Create;
Result.Clear;
if (not FileExists(AFileName)) or (ASectionName='') then
Exit;
AssignFile(f,AFileName);
try
Reset(f);
bReading := False;
while not EOF(f) do
begin
ReadLn(f,s);
s := Trim(s);
if s='' then
Continue;
// If current line is section tag
if s[1]='[' then
begin
// If we currently reading section then we read it all and we must
// break because another section occur
if bReading then
begin
bReading := False;
Break;
end
else
// Otherwise if this is section we are looking for, then set flag
// to "start reading"
if SameText(ASectionName,TrimSet(s,['[',']'])) then
bReading := True;
end else
// Read section line
if bReading then
Result.Add(s);
end;
finally
CloseFile(f);
end;
end;
procedure GetStyleHeader(const AFileName: String; AOutHeader: PBCStyleHeader);
var sl: TStrings;
begin
if (AOutHeader=nil) or (not FileExists(AFileName)) then
Exit;
sl := GetSectionData(AFileName,sSECTION_HEADER_NAME);
try
// Header info (with format Author=Foo) should be at the top of file
with AOutHeader^ do
begin
Author := sl.Values['Author'];
Description := sl.Values['Description'];
ControlClass := sl.Values['ControlClass'];
end;
finally
sl.Free;
end;
end;
end.