什么是序列化Delphi应用程序配置的最佳方法?

10
我将回答这个问题,但如果你比我更快或者不喜欢我的解决方案,请随时提供你的答案。我刚想出这个想法,想听听大家的意见。
目标:创建一个可读的配置类(像INI文件一样),但无需编写(并在添加新配置项后进行调整)加载和保存方法。
我想创建一个类似于:
TMyConfiguration = class (TConfiguration)
  ...
  property ShowFlags : Boolean read FShowFlags write FShowFlags;
  property NumFlags : Integer read FNumFlags write FNumFlags;
end;

调用 TMyConfiguration.Save 方法(继承自 TConfiguration)应该会创建一个像这样的文件

[Options]
ShowFlags=1
NumFlags=42

问题:如何以最佳方式完成此操作?

7个回答

7
这是我的解决方案。
我有一个基类。
TConfiguration = class
protected
  type
    TCustomSaveMethod = function  (Self : TObject; P : Pointer) : String;
    TCustomLoadMethod = procedure (Self : TObject; const Str : String);
public
  procedure Save (const FileName : String);
  procedure Load (const FileName : String);
end;

载入方法看起来像这样(保存方法类似):
procedure TConfiguration.Load (const FileName : String);
const
  PropNotFound = '_PROP_NOT_FOUND_';
var
  IniFile : TIniFile;
  Count : Integer;
  List : PPropList;
  TypeName, PropName, InputString, MethodName : String;
  LoadMethod : TCustomLoadMethod;
begin
  IniFile := TIniFile.Create (FileName);
  try
    Count := GetPropList (Self.ClassInfo, tkProperties, nil) ;
    GetMem (List, Count * SizeOf (PPropInfo)) ;
    try
      GetPropList (Self.ClassInfo, tkProperties, List);
      for I := 0 to Count-1 do
        begin
        TypeName  := String (List [I]^.PropType^.Name);
        PropName  := String (List [I]^.Name);
        InputString := IniFile.ReadString ('Options', PropName, PropNotFound);
        if (InputString = PropNotFound) then
          Continue;
        MethodName := 'Load' + TypeName;
        LoadMethod := Self.MethodAddress (MethodName);
        if not Assigned (LoadMethod) then
          raise EConfigLoadError.Create ('No load method for custom type ' + TypeName);
        LoadMethod (Self, InputString);
        end;
    finally
      FreeMem (List, Count * SizeOf (PPropInfo));
    end;
  finally
    FreeAndNil (IniFile);
  end;

基类可以为Delphi默认类型提供加载和保存方法。然后我可以像这样为我的应用程序创建配置:

TMyConfiguration = class (TConfiguration)
...
published
  function  SaveTObject (P : Pointer) : String;
  procedure LoadTObject (const Str : String);
published
  property BoolOption : Boolean read FBoolOption write FBoolOption;
  property ObjOption : TObject read FObjOption write FObjOption;
end;

自定义保存方法的示例:

function TMyConfiguration.SaveTObject (P : Pointer) : String;
var
  Obj : TObject;
begin
  Obj := TObject (P);
  Result := Obj.ClassName;  // does not make sense; only example;
end;       

这对我来说看起来还不错。你为什么认为可能有更聪明的解决方案呢? - Jeroen Wiert Pluimers
@Jeroen:在我提问时,大多数情况下我会得到很多聪明的评论、改进建议和批评的经验。除此之外,我还想分享一下这段代码,以便其他人可能从中受益。 - jpfollenius

6
我在所有应用程序中都使用XML作为配置的手段。它具有以下特点:
  • 灵活
  • 未来功能可扩展性强
  • 易于使用任何文本阅读器阅读
  • 非常容易在应用程序中扩展,无需修改类
我有一个XML库,使得读取或修改配置变得非常容易,甚至不需要关注缺失的值。现在,如果速度是问题,或某些值经常被读取,您还可以将XML映射到应用程序内部的类以实现更快的访问。
我发现其他配置方法远不如XML优秀:
  • Ini文件:没有深入结构,灵活性较差
  • 注册表:最好远离它。

1
我做的事情差不多,但我使用Delphi的XML数据绑定向导将文件映射到对象。 - Craig Stuntz
1
事实是:我认为INI文件甚至可以被用户轻松理解,而不是每个人都能理解XML。我知道我应该有设置对话框,但我希望任何人都能在不深入了解XML的情况下更改更高级的设置。 - jpfollenius
1
XML文件非常容易阅读。我说的是基本的XML,只有节点、属性和值(文本)。 与INI相比,它几乎没有任何不可读性。好吧,在标签中你需要更多的“锅炉板”,但如果你担心用户修改,那就为他们制作一个GUI。你不能依赖用户自己修改文件,即使是INI文件。 - Runner
1
你可能是对的,但在我的情况下,用户习惯于使用INI文件,我希望保持这种方式。如果我使用XML以如此扁平的方式(没有深度等),那么文件中会有很多开销。 - jpfollenius
1
如果用户必须手动编辑配置文件,那么你已经输掉了游戏。 - Craig Stuntz
显示剩余7条评论

3

我的首选方法是在全局接口单元中创建一个接口:

type
  IConfiguration = interface
    ['{95F70366-19D4-4B45-AEB9-8E1B74697AEA}']
    procedure SetConfigValue(const Section, Name,Value:String);
    function GetConfigValue(const Section, Name:string):string;
  end;

然后,这个接口在我的主表单中“暴露”出来:

type
  tMainForm = class(TForm,IConfiguration)
  ...
  end;

大多数情况下,实际的实现不在主表单中,它只是一个占位符,并且我使用implements关键字将接口重定向到主表单拥有的另一个对象。这样做的目的是将配置的责任委托给其他对象。每个单元都不关心配置是存储在表格、ini文件、xml文件,甚至是注册表中。这样做允许我在使用全局接口单元的任何单元中进行以下调用:

var
  Config : IConfiguration;
  Value : string;
begin
  if Supports(Application.MainForm,IConfiguration,Config) then
    value := Config.GetConfiguration('section','name');
  ...      
end;

只需要在我正在工作的单元中添加表格和我的全局接口单元即可。由于它不使用主窗体,如果我决定以后将其重用于另一个项目,我就不必做任何进一步的更改...它只需正常运行,即使配置存储方案完全不同。
我通常喜欢创建一个表格(如果我正在处理数据库应用程序)或一个XML文件。如果这是一个多用户数据库应用程序,则会创建两个表格。一个用于全局配置,另一个用于用户配置。

+1,因为我同意你写的大部分内容,而且差异主要是风格问题。但是目前的答案与问题关系不大,我理解的问题是“编写一个基类的最佳方式,使其能够从持久存储中加载自身并将自身存储到其中,而无需更改派生类”。这至少是OP回答的问题,标题只是与问题文本不匹配。尽管如此,smasher应该遵循您的建议,而不是硬编码INI文件解决方案。 - mghie
我希望我的配置设置所需的时间尽可能短,因为它会在每个应用程序中重复出现。虽然我在某种程度上喜欢你的方法,但对我来说不够简单。一个简单的全局单例模式,配合XML后端和良好的接口访问,对于大多数情况来说已经足够了。即使是完全基于数据库的应用程序,仍然需要一个简单的配置文件来访问这些数据 :) - Runner
我完全同意mghie的评论。你提出了一个很好的替代方案,使用接口和委托来避免全局变量,但你没有提到序列化是如何执行的。从我的解决方案中提取IPropertyStorer以将配置与具体实现(INI文件/XML/数据库)分离并不是问题。但这并不改变关于最小化持久存储工作量的观点。 - jpfollenius
@mghie:我更新了标题。我同意拆分INI文件的部分是个不错的点子。 - jpfollenius

1

基本上,您正在寻求一种将给定对象序列化的解决方案(在您的情况下是配置到ini文件)。有现成的组件可用,您可以从这里这里开始查找。


1

前段时间,我编写了一个小单元,用于相同的任务 - 将应用程序的配置保存/加载到xml文件中。

请查看我们免费的SMComponent库中的Obj2XML.pas单元: http://www.scalabium.com/download/smcmpnt.zip


0

这是针对Java的。

我喜欢使用java.util.Properties类来读取配置文件或属性文件。我喜欢的是,您可以按照上面展示的方式将文件与行放在同一个位置(键=值)。 此外,它使用#(井号)表示注释行,就像许多脚本语言一样。

所以你可以使用:

ShowFlags=true
# this line is a comment    
NumFlags=42

然后你只需要像这样编写代码:

Properties props = new Properties();
props.load(new FileInputStream(PROPERTIES_FILENAME));
String value = props.getProperty("ShowFlags");
boolean showFlags = Boolean.parseBoolean(value);

就是这么简单。


并不是很有帮助,因为我在询问Delphi相关的内容,而你提供的类是针对Java的... - jpfollenius
是的,对此我感到抱歉。实际上您没有进行特定说明,所以我尝试回答了问题。问题表述得非常笼统,我没有看到您的Delphi标签。 - Nick

0
Nicks的答案(使用Java Properties)有一定道理:这种简单的读取和传递应用程序各部分之间的配置的方法不会引入对特殊配置类的依赖。一个简单的键/值列表可以减少应用程序模块之间的依赖关系,使代码重用更容易。
在Delphi中,基于简单TStrings的配置是一种实现配置的简单方法。例如:
mail.smtp.host=192.168.10.8    
mail.smtp.user=joe    
mail.smtp.pass=*******

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接