Delphi:是否可以枚举全局命名空间中记录(~类型化常量)的所有实例?
从我到目前为止所做的研究来看,我已经猜到答案是否定的,只是为了确保。。。(此外,此条目可在提供支持后更新) 我想问题的标题应该是自给自足的,但FWIW我想做的是:我有一个围绕记录常量构建的配置框架:我应用程序中的每个配置选项都以键入常量的形式定义在中心位置,其中包含注册表(或INI)键的名称,它的数据类型和默认值。这些常量是我传递给框架中的访问器方法的,然后框架实现检索和存储选项值所需的逻辑 现在,我想扩展这些记录中的信息,以包括我可以用来自动生成描述这些选项的ADM/ADMX文件(Delphi:是否可以枚举全局命名空间中记录(~类型化常量)的所有实例?,delphi,record,rtti,Delphi,Record,Rtti,从我到目前为止所做的研究来看,我已经猜到答案是否定的,只是为了确保。。。(此外,此条目可在提供支持后更新) 我想问题的标题应该是自给自足的,但FWIW我想做的是:我有一个围绕记录常量构建的配置框架:我应用程序中的每个配置选项都以键入常量的形式定义在中心位置,其中包含注册表(或INI)键的名称,它的数据类型和默认值。这些常量是我传递给框架中的访问器方法的,然后框架实现检索和存储选项值所需的逻辑 现在,我想扩展这些记录中的信息,以包括我可以用来自动生成描述这些选项的ADM/ADMX文件(ifdef)
ifdef
)的元数据
但对于这一点,我需要能够枚举这些常量,除非我添加某种显式的注册机制,这似乎是不必要的重复
理想情况下,我宁愿以属性的形式声明元信息,而不是向记录类型添加额外的字段,但这些属性(尚未?)不能应用于常量。而且,这不会改变首先枚举常量的必要性
假设当前通过RTTI是不可能的,我可能会考虑将元数据放入评论中,并以某种方式解析出来。这可能是另一个问题
[平台信息:目前正在使用Delphi 2010,但我已经有了XE许可证-只是没有时间安装它而已]接下来是很长的答案….:-) 与尝试枚举全局常量不同,您可能希望尝试一种不同的方法来完成所做的工作 不久前,罗伯特·洛夫有一个非常有趣的想法。 他使用自定义属性和RTTI来指定如何存储和检索.ini文件中的值 在他的博客中,他对它的工作原理做了很好的解释:我在下面的代码中对此进行了一些扩展:
- 您现在可以拥有除字符串以外的其他类型(字符串、整数、双精度、布尔值)
- 可以在属性中指定默认值
- 有一个要从中继承的基本设置类。您可以在此处为INI文件设置文件名,它会为您加载和保存
- 基本AppSettings类。。TAppSettings自动将设置以以下格式存储在文件中:
.config.ini
var
DbSettings : TDbSettings
begin
DbSettings := TDbSettings.Create;
try
// show some settings
WriteLn(DbSettings.Host);
WriteLn(DbSettings.Port);
// write setting
DbSettings.UserName := 'Me';
// store it in the ini file
DbSettings.Save;
finally
DbSettings.Free;
end;
end;
如果您想指定一组新的设置,这非常简单
TServiceSettings=class(TAppSettings)
public
[IniValue('Service','Description','MyServiceDesc')]
ServiceDescription: String;
[IniValue('Service','DisplayName','MyServiceName')]
ServiceDisplayName: String;
end;
这比直接读写文件要干净得多。罗伯特,如果你读到这篇文章:谢谢你让我的生活轻松多了
以下是更新的代码:
unit WvN.Configuration.Persist.Ini;
// MIT License
//
// Copyright (c) 2009 - Robert Love
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE
//
// Wouter van Nifterick: 2010-11: added TSettings abstract class and some derivatives to load database and cs settings
interface
uses SysUtils,Classes, Rtti,TypInfo;
type
IniValueAttribute = class(TCustomAttribute)
private
FName: string;
FDefaultValue: string;
FSection: string;
public
constructor Create(const aSection : String;const aName : string;const aDefaultValue : Integer = 0);overload;
constructor Create(const aSection : String;const aName : string;const aDefaultValue : Double = 0.0);overload;
constructor Create(const aSection : String;const aName : string;const aDefaultValue : Boolean = false);overload;
constructor Create(const aSection : String;const aName : string;const aDefaultValue : String = '');overload;
property Section : string read FSection write FSection;
property Name : string read FName write FName;
property DefaultValue : string read FDefaultValue write FDefaultValue;
end;
EIniPersist = class(Exception);
TIniPersist = class (TObject)
private
class procedure SetValue(aData : String;var aValue : TValue);
class function GetValue(var aValue : TValue) : String;
class function GetIniAttribute(Obj : TRttiObject) : IniValueAttribute;
public
class procedure Load(FileName : String;obj : TObject);
class procedure Save(FileName : String;obj : TObject);
end;
TSettings=class abstract(TComponent)
private
FOnChange: TNotifyEvent;
FFileName:String;
procedure SetOnChange(const Value: TNotifyEvent);
function GetFileName: String;virtual;
procedure SetFileName(const Value: String);virtual;
public
property FileName:String read GetFileName write SetFileName;
procedure CreateDefaults;
procedure Load;virtual;
procedure Save;virtual;
constructor Create(AOwner: TComponent); override;
procedure DoOnChange;
property OnChange:TNotifyEvent read FOnChange write SetOnChange;
end;
TAppSettings=class abstract(TSettings)
function GetFileName: String;override;
end;
TServiceSettings=class(TAppSettings)
public
[IniValue('Service','Description','')]
ServiceDescription: String;
[IniValue('Service','DisplayName','')]
ServiceDisplayName: String;
end;
TCsSettings=class(TAppSettings)
public
[IniValue('CS','SourceAppId',9999)]
SourceAppId: LongWord;
[IniValue('CS','SourceCSId',9999)]
SourceCSId: LongWord;
[IniValue('CS','Host','Localhost')]
Host: String;
[IniValue('CS','Port',42000)]
Port: LongWord;
[IniValue('CS','ReconnectInvervalMs',30000)]
ReconnectInvervalMs: Integer;
end;
TFTPSettings=class(TAppSettings)
public
[IniValue('FTP','Host','Localhost')]
Host: String;
[IniValue('FTP','Port',21)]
Port: LongWord;
[IniValue('FTP','RemotePath','/')]
RemotePath: String;
[IniValue('FTP','LocalPath','.')]
LocalPath: String;
[IniValue('FTP','Username','')]
Username: String;
[IniValue('FTP','Password','')]
Password: String;
[IniValue('FTP','BlockSize',4096)]
BlockSize: Cardinal;
end;
TDbSettings=class(TAppSettings)
private
function GetURL: String;
public
[IniValue('DB','Host','Localhost')]
Host: String;
[IniValue('DB','Port',3306)]
Port: LongWord;
[IniValue('DB','Database','')]
Database: String;
[IniValue('DB','Username','root')]
Username: String;
[IniValue('DB','Password','')]
Password: String;
[IniValue('DB','Protocol','mysql-5')]
Protocol: String;
[IniValue('DB','UseSSL',True)]
UseSSL: Boolean;
[IniValue('DB','Compress',True)]
Compress: Boolean;
[IniValue('DB','TimeOutSec',0)]
TimeOutSec: Integer;
[IniValue('DB','SSL_CA','U:\Efkon2\AMM_mysql_cas.crt')]
SSL_CA: String;
[IniValue('DB','SSL_CERT','U:\Efkon2\AMM_ARS_mysql_user.pem')]
SSL_CERT: String;
[IniValue('DB','SSL_KEY','U:\Efkon2\AMM_ARS_mysql_user_key.pem')]
SSL_KEY: String;
property URL:String read GetURL;
end;
TPathSettings=class(TAppSettings)
public
[IniValue('Paths','StartPath','.')]
StartPath: String;
[IniValue('Paths','InPath','In')]
InPath: String;
[IniValue('Paths','OutPath','Out')]
OutPath: String;
[IniValue('Paths','ErrorPath','Error')]
ErrorPath: String;
end;
implementation
uses IniFiles;
{ TIniValue }
constructor IniValueAttribute.Create(const aSection, aName, aDefaultValue: String);
begin
FSection := aSection;
FName := aName;
FDefaultValue := aDefaultValue;
end;
{ TIniPersist }
class function TIniPersist.GetIniAttribute(Obj: TRttiObject): IniValueAttribute;
var
Attr: TCustomAttribute;
begin
for Attr in Obj.GetAttributes do
begin
if Attr is IniValueAttribute then
begin
exit(IniValueAttribute(Attr));
end;
end;
result := nil;
end;
class procedure TIniPersist.Load(FileName: String; obj: TObject);
var
ctx : TRttiContext;
objType : TRttiType;
Field : TRttiField;
Prop : TRttiProperty;
Value : TValue;
IniValue: IniValueAttribute;
Ini : TIniFile;
Data : string;
begin
ctx := TRttiContext.Create;
try
Ini := TIniFile.Create(FileName);
try
objType := ctx.GetType(Obj.ClassInfo);
for Prop in objType.GetProperties do
begin
IniValue := GetIniAttribute(Prop);
if Assigned(IniValue) then
begin
Data := Ini.ReadString(IniValue.Section, IniValue.Name, IniValue.DefaultValue);
Value := Prop.GetValue(Obj);
SetValue(Data, Value);
Prop.SetValue(Obj, Value);
end;
end;
for Field in objType.GetFields do
begin
IniValue := GetIniAttribute(Field);
if Assigned(IniValue) then
begin
Data := Ini.ReadString(IniValue.Section, IniValue.Name, IniValue.DefaultValue);
Value := Field.GetValue(Obj);
SetValue(Data, Value);
Field.SetValue(Obj, Value);
end;
end;
finally
Ini.Free;
end;
finally
ctx.Free;
end;
end;
class procedure TIniPersist.SetValue(aData: String;var aValue: TValue);
var
I : Integer;
begin
case aValue.Kind of
tkWChar,
tkLString,
tkWString,
tkString,
tkChar,
tkUString : aValue := aData;
tkInteger,
tkInt64 : aValue := StrToInt(aData);
tkFloat : aValue := StrToFloat(aData);
tkEnumeration: aValue := TValue.FromOrdinal(aValue.TypeInfo,GetEnumValue(aValue.TypeInfo,aData));
tkSet: begin
i := StringToSet(aValue.TypeInfo,aData);
TValue.Make(@i, aValue.TypeInfo, aValue);
end;
else raise EIniPersist.Create('Type not Supported');
end;
end;
class procedure TIniPersist.Save(FileName: String; obj: TObject);
var
ctx : TRttiContext;
objType : TRttiType;
Field : TRttiField;
Prop : TRttiProperty;
Value : TValue;
IniValue: IniValueAttribute;
Ini : TIniFile;
Data : string;
begin
ctx := TRttiContext.Create;
try
Ini := TIniFile.Create(FileName);
try
objType := ctx.GetType(Obj.ClassInfo);
for Prop in objType.GetProperties do
begin
IniValue := GetIniAttribute(Prop);
if Assigned(IniValue) then
begin
Value := Prop.GetValue(Obj);
Data := GetValue(Value);
Ini.WriteString(IniValue.Section, IniValue.Name, Data);
end;
end;
for Field in objType.GetFields do
begin
IniValue := GetIniAttribute(Field);
if Assigned(IniValue) then
begin
Value := Field.GetValue(Obj);
Data := GetValue(Value);
Ini.WriteString(IniValue.Section, IniValue.Name, Data);
end;
end;
finally
Ini.Free;
end;
finally
ctx.Free;
end;
end;
class function TIniPersist.GetValue(var aValue: TValue): string;
begin
if aValue.Kind in [tkWChar, tkLString, tkWString, tkString, tkChar, tkUString,
tkInteger, tkInt64, tkFloat, tkEnumeration, tkSet] then
result := aValue.ToString
else
raise EIniPersist.Create('Type not Supported');
end;
constructor IniValueAttribute.Create(const aSection, aName: string;
const aDefaultValue: Integer);
begin
FSection := aSection;
FName := aName;
FDefaultValue := IntToStr(aDefaultValue);
end;
constructor IniValueAttribute.Create(const aSection, aName: string;
const aDefaultValue: Double);
begin
FSection := aSection;
FName := aName;
FDefaultValue := FloatToStr(aDefaultValue);
end;
constructor IniValueAttribute.Create(const aSection, aName: string;
const aDefaultValue: Boolean);
begin
FSection := aSection;
FName := aName;
FDefaultValue := BoolToStr(aDefaultValue);
end;
{ TAppSettings }
procedure TSettings.CreateDefaults;
begin
Load;
Save;
end;
procedure TSettings.DoOnChange;
begin
if Assigned(FOnChange) then
FOnChange(Self)
end;
procedure TSettings.SetOnChange(const Value: TNotifyEvent);
begin
FOnChange := Value;
end;
{ TAppSettings }
function TAppSettings.GetFileName: String;
begin
Result := ChangeFileExt(ParamStr(0),'.config.ini')
end;
{ TSettings }
constructor TSettings.Create(AOwner: TComponent);
begin
inherited;
end;
function TSettings.GetFileName: String;
begin
Result := FFileName
end;
procedure TSettings.Load;
begin
TIniPersist.Load(FileName,Self);
DoOnChange;
end;
procedure TSettings.Save;
begin
TIniPersist.Save(FileName,Self);
end;
procedure TSettings.SetFileName(const Value: String);
begin
FFileName := Value
end;
{ TDbSettings }
function TDbSettings.GetURL: String;
begin
Result := Format('%s://%s:%s@%s:%d/%s?compress=%s&timeout=%d',
[
self.Protocol,
self.Username,
self.Password,
self.Host,
self.Port,
self.Database,
booltostr(self.Compress),
self.TimeOutSec
]);
end;
end.
这里有一个想法:您已经将这些常量传递给了配置框架。让它记住看到了哪些选项,然后它可以在课程结束时给你一份报告。这将为您提供即时注册,还可以让您审核实际使用的选项。@Rob:这确实是一个有趣的想法,但我更愿意将此功能保留在发布版本之外。生成ADM/ADMX文件所需的元数据量很大,在运行时也完全无用。这就是为什么我计划将这些附加字段放在
ifdef
s中,并将包含常量的单元包含在一个单独的项目中,该项目专门用于生成ADM/ADMX文件。这听起来确实是一个很酷的方法,当我决定从头开始为一个新项目重写配置框架时。。。然而,手头的项目已经大量使用了当前的框架,使用您的方法,我不仅要重写框架,还要重写消费应用程序本身的很大一部分……此外,我还要将加载/保存功能移动到一个单独的类,并将设置对象传递给它。这样,我仍然可以在registry/INI/XML/DB/which之间切换存储后端。