65.9K
CodeProject 正在变化。 阅读更多。
Home

配置器

starIconstarIconstarIconstarIconstarIcon

5.00/5 (6投票s)

2011年11月20日

CPOL

11分钟阅读

viewsIcon

30810

downloadIcon

493

这将允许您在 Delphi 中创建 INI 或 XML 格式的配置文件,而无需所有繁重的工作。

引言

这个小库将允许您在 Delphi 中创建/更新/保存/加载配置文件。提供的示例将能够创建 INI 和 XML 配置文件。作为额外内容,我还包含了一些额外的编码技巧,这些技巧将帮助您理解编码原理和 Delphi 设计/编码原理的某些方面。由于你们中的一些人可能没有安装 Delphi,我提供了两个编译后的源文件可执行文件,一个用于 INI 配置,一个用于 XML 配置实现。

背景

我,和你们大多数人一样,编写了大量的软件。如果你像我一样在游戏中待了足够长的时间,那么你就会发现需要编写/读取应用程序在其功能中使用的配置设置。你可能还使用了几种不同的配置文件格式选项

  • INI 文件
  • XML 文件
  • Windows 注册表
  • 某种或其他持久化格式,以你自己的邪恶方式(一次一个可编程蓝光播放器地占领世界)工作

考虑到所有这些方法,我发现了两件重要的事情

  • 微软并不完全赞同你使用 Windows 注册表。不是因为他们不允许你这样做(显然),而是因为使用你应用程序的用户可能实际上没有权限写入甚至读取你需要从注册表条目中获取的信息。你多少次发现(尤其是在 Vista 和 Windows 7 等较新平台上)你无法访问某些东西,这仅仅是因为邪恶的网络管理员不允许你访问该特定数据。
  • 你不想浪费时间反复地找出并写出你需要编写/读取应用程序配置设置的所有内容。

基于这些观察,我创建了一组(或小库)文件,我希望这些文件能帮助解决创建、保存、加载和更新 Delphi 应用程序配置设置的情况。我创建了一个基本配置对象和两个派生类,它们将创建 INI 配置文件或 XML 配置文件(巧合的是,这与 Visual Studio 中创建的 *.config 文件非常相似)。
重要提示:当使用 INI 配置对象时,暴露方法中的“AParentSections”和“ASectionName”参数会合并在一起形成实际的节名称。这是有意为之,因为 INI 文件的限制是不能像 XML 文件那样嵌套设置。就我个人而言,我更喜欢使用 XML 配置对象。

配置对象允许您为以下类型创建配置条目(如果有人希望我添加任何其他类型的设置来保存,请告诉我,我将相应地更新文章)

  • 字符串
  • 整数
  • 布尔值
  • real (注意:您还可以保存 TDateTime 值,因为 TDateTime 实际上是一种特殊的 real 类型。)

此代码将向您展示什么

此代码将介绍 Pascal/Delphi 中的许多不同编码场景,包括如何使用abstract 类。你们中的一些人会知道。为了所有人的利益,我将解释一下我所做的一些事情

  • 继承多态:这是设计原则,您可以在其中定义一个类,并从该类派生出其他几个类。您可以在其中定义一个或多个派生类,当您调用相同的公共函数时,这些类会做不同的事情。
  • 类引用:您可以在其中实例化一个具体类,仅使用一个变量作为实例化对象。
  • 方法重载:您可以在代码中定义多个具有相同方法名称的方法,这些重载方法唯一不同的是参数列表。
  • 何时使用 virtualabstract 方法。
  • 何时使用 protected 关键字。
  • 模板设计模式:这基本上是众多面向对象设计原则之一,用于简化创建基类和派生类的过程。
  • 自定义事件:您将学习如何创建自己的事件(就像您自己的OnClick OnCreate 事件),但更酷。
  • 一些您可能不知道的 Delphi VCL 中隐藏方法的酷技巧。

好吧,说得够多了,让我开始解释...

继承、多态、virtual、abstract、模板设计、protected、类引用和 Delphi 事件概念...

你们中的大多数人(如果不是全部)都理解继承和多态的核心概念,但因为这是我的文章,而且可能有一些 Delphi 编码的新手,所以我就直说了。多态的概念被描述为“两个(或更多)不同的类派生自同一个祖先,其中一个或多个公共方法在被派生类调用时会产生不同的结果”。用白话来说,这意味着如果我定义以下祖先基类

  // in the interface section...

  TSaw = class(TObject)
  private
    FBusy : Boolean;
    procedure MyOnClickEvent(ASender : TObject);
  protected
    procedure Do_Cut; virtual; abstract;
    procedure Initialise; virtual;
    procedure Finalise; virtual;
  public
    procedure Cut;
  public
    property Busy : Boolean read FBusy;
  end;

  TMyRefSaw = class of TSaw;

  // in the implementation section...

  procedure TSaw.Cut;
  begin
    if not(FBusy) then
    begin
      FBusy := True;
      try
        Do_Cut;
      finally
        FBusy := False;
      end;
  end
  else
    begin
      // Show some or other message to indicate that the TSaw is still busy cutting...
    end;
  end;

  procedure TSaw.Initialise;
  begin
    FBusy := False;
  end;

  procedure TSaw.Finalise;
  begin
    // This, for the time being is still blank, 
    // because we don't need to clear up anything yet...
  end;

  procedure TSaw.MyOnClickEvent(ASender : TObject);
  begin
    // Perform some or other custom type of coding here...
  end;    

现在,我们面临着我们面前的。这种类为您提供了(第一次?)一瞥模板设计模式理想情况下想要实现的目标。这个类可以做某些事情。在这种情况下,对象知道,并且可以设置,它何时在Cut 方法中忙碌。但是,它不知道如何剪切。所以,我们有一个模板,但我们需要在我们的派生类中对其进行细化,以告诉它如何Do_Cut。这就引出了为什么Initialise Finalise 定义旁边只有virtual; 关键字,而Do_Cut 旁边有virtual; abstract; 。前两个意味着它们将有一个“基本”实现,将在基类中可用。我们希望这样做,因为,就像Initialise 的情况一样,我们需要确保FBusy 设置为False。当涉及到Do_Cut时,这个基类对如何执行该方法一无所知,因此它应该始终在基类中实现。这是两个派生类会是什么样子的两个示例

// in the interface section...

TRipSaw = class(TSaw)
private
  FWood : TWood;
protected
  procedure Do_Cut; override;
  procedure Initialise; override;
  procedure Finalise; override;
end;

// in the implementation section...

procedure TRipSaw.Do_Cut;
begin
  // Here, We will perform some or other actions that work with the FWood object...
end;

procedure TRipSaw.Initialise;
begin
  inherited Initialise;
  FWood := TWood.Create; // instantiate the TWood class...
end;

procedure TRipSaw.Finalise;
begin
  FreeAndNil(FWood); // Free the TWood class...
end;

// in the interface section...

THackSaw = class(TSaw)
private
  FMetal : TMetal;
protected
  procedure Do_Cut; override;
  procedure Initialise; override;
  procedure Finalise; override;
end;

// in the implementation section...

procedure THackSaw.Do_Cut;
begin
  // Here, We will perform some or other actions that world with the FMetal object...
end;

procedure THackSaw.Initialise;
begin
  inherited Initialise;
  FMetal := TMetal.Create; // instantiate the TWood class...
end;

procedure THackSaw.Finalise;
begin
  FreeAndNil(FMEtal); // Free the TMeta=l class...
end;    

我选择上面的例子是因为以下事实

  1. 手锯和钢锯都是锯子
  2. 手锯可以锯木头,但不能锯金属
  3. 钢锯可以锯金属,但不能锯木头

现在,我将这些方法放在protected 关键字上下文中,因为我希望能够覆盖派生类的‎方法,同时将方法上下文保留在类中,但仍将其公开以供派生类访问。它比private高一个级别,但更酷,因为它只能在派生类中访问,而不能在创建类实例时访问。

我们都使用过 Delphi 中的OnClick OnCreate 事件。我们通常只需双击事件查看器,然后 BOOM!我们就有了为我们创建的代码,我们可以做我们需要做的事情。现在,让我们假设在窗体上有一个TButton ,我们在其中创建了TRipSaw THackSaw 对象。我们不想双击TButton OnClick 事件(在此示例中,我们将其称为MyButton),因为我们已经为按钮单击时定义了自己的事件MyOnClickEvent 。在这种情况下,我们只需要这样做

MyButton.OnClick := MySawInstance.MyOnClickEvent;

对于概念的最后一部分,我想阐述:类引用。这是Delphi中的一个小技巧,它允许您使用类类型的变量来创建类。因此,当我要创建一个派生自TSaw的类时,我将执行以下操作

var
  Saw_Ref : TMyRefSaw;
  MySaw : TSaw;
begin
  SawRef := THackSaw;     // or Saw := TRipSaw;
  MySaw := Saw_Ref.Create;     // which will create either of the THackSaw 
            // or the TRipSaw classes :)
end;

很酷的技巧,是吧?和往常一样,如果有人需要更深入地解释这是如何工作的,只需发消息,我将在力所能及的范围内解释。这就引出了...

Using the Code

使用代码其实非常简单。要创建其中一个派生类的实例,构造函数要求您提供文件扩展名和分隔符类型(用于父节)。对于文件扩展名参数,您只需使用“ini”或“xml”或在我的情况下使用“config”。您不得输入“.”值,因为配置器已经为您完成了此操作。对于分隔符参数,这将用于AParentSections 分隔符。默认情况下,它是正常的“,”值,因此您应该不需要更改它。您只需包含基本文件(uBaseConfigurator.pas)和包含您希望在要使用它的单元的uses 子句中的实际配置派生类的文件

为了本文的目的,我创建了一个基本窗体,它具有配置器的基本加载和读取(窗体的位置、宽度和高度)以及两个派生窗体,其中一个执行更多加载和保存(主窗体),另一个什么也不做(显示事件触发时)。在包含字符串网格的主窗体中,我使用AParentSections 参数告诉配置对象我想要保存的配置必须是嵌套的。AParentSections 参数基本上只是一个带分隔符的string 值(或TStrings 对象)。配置对象还会检查您传递给它的参数,因此如果您为ASectionName AValueName 参数发送空string 值,它将引发异常,并且由于您不能在节和/或值名称中使用空格,它将用下划线替换空格。这将确保配置对象能够正确持久化您的配置设置。以下是使用 XML 配置对象创建的配置文件的示例

<Configurator>
    <LogEventActionsForm>
        <Left>873</Left>
        <Top>259</Top>
        <Width>551</Width>
        <Height>346</Height>
    </LogEventActionsForm>
    <MainForm>
        <Left>294</Left>
        <Top>364</Top>
        <Width>490</Width>
        <Height>335</Height>
        <Edit1>Edit1</Edit1>
        <Edit2>0</Edit2>
        <DateTimePicker1>40866.6819596412</DateTimePicker1>
        <CheckBox1>True</CheckBox1>
        <Edit3>0</Edit3>
        <StringGrid1>
            <Columns>
                <Col0>64</Col0>
                <Col1>64</Col1>
                <Col2>64</Col2>
                <Col3>64</Col3>
                <Col4>64</Col4>
            </Columns>
        </StringGrid1>
    </MainForm>
</Configurator>

以下是使用 INI 配置对象创建的配置文件的示例

[LogEventActionsForm]
Left=655
Top=501
Width=687
Height=346
[MainForm]
Left=118
Top=415
Width=490
Height=335
Edit1=Edit1
Edit2=0
DateTimePicker1=40866.6819596412
CheckBox1=1
Edit3=0
[MainForm_StringGrid1_Columns]
Col0=64
Col1=64
Col2=64
Col3=64
Col4=64

关注点

我包含了自己自定义的事件,如果你有兴趣,可以尝试根据我的设计方式创建自己的自定义事件。请仔细阅读代码,看看我是如何实现这些事件的,并随时提出任何相关问题。我将介绍一个自定义事件,以便为您提供一个基本概念,了解发生了什么

TOnBeforeManipulateSection = procedure(
    ASender : TConfigurator;
    AParentSection : TStrings;
    var AValueName : string) of object;

我将其定义为一种能够在对象添加、删除或检查配置文件中的某个节之前(如其名称所示以及实现此事件的最佳用途)触发的方式。因此,在我的(基)类中,我定义...

// Please check the code for the full implementation...
property OnBeforeAddSection : TOnBeforeManipulateSection read FOnBeforeAddSection 
    write FOnBeforeAddSection;
property OnBeforeRemoveSection : TOnBeforeManipulateSection read FOnBeforeRemoveSection 
    write FOnBeforeRemoveSection;
property OnBeforeHasSection : TOnBeforeManipulateSection read FOnBeforeHasSection 
    write FOnBeforeHasSection;

要实现(其中之一),我这样做

if Assigned(OnBeforeAddSection) then
  OnBeforeAddSection(Self, LParentSections, LSectionName);
Do_AddSection(LParentSections, LSectionName);

各位,这就是您如何实现自己的自定义事件。在我提供的示例中,我只实现了配置器中内置的一小部分自定义事件。

作为额外奖励,配置类也将正确地为服务应用程序创建配置文件。你可能会问,为什么这是一个额外奖励?答案很简单:服务应用程序以与普通应用程序不同的方式执行,因此,当您使用ExtractFilePath(Application.ExeName)时,它不会给您预期的结果(应用程序的实际路径),而是%Windows%\System32文件夹。这就是为什么在基配置类的构造函数中,您会找到解决此特定问题的代码

constructor TConfigurator.Create(
  const AConfigurationFileExtension : string;
  const AParentSectionsDelimiter : Char);
var
  LApplicationFileName : array[0..MAX_PATH] of Char;
begin
  inherited Create;

  FConfigurationFileExtenstion := AConfigurationFileExtension;
  FParentSectionsDelimiter := AParentSectionsDelimiter;

  //  This allows to get the executing module's full name whether
it's a normal or service application...
  FillChar(LApplicationFileName, SizeOf(LApplicationFileName), #0);
  GetModuleFileName(HInstance, LApplicationFileName, MAX_PATH);

  FModuleName := LApplicationFileName;
  FModulePath := ExtractFilePath(FModuleName);

  FRootName := ChangeFileExt(ExtractFileName(FModuleName), '');
  FSettingsFileName := ChangeFileExt(FModuleName,
'.'+ConfigurationFileExtenstion);

  Initialise;
end;

如果您需要将配置选项持久化到满足您特定需求的其他格式,您可以从基类 (TConfigurator) 创建自己的派生类。您只需覆盖以下方法

    // Initialization and finalization of your specific objects for
your implementation...
    procedure Initialise; virtual;
    procedure Finalise; virtual;

    //For creating/adding/removing sections of the configuration
file...
    procedure Do_AddSection(
      const ASectionName : string;
      const AParentSections : TStrings); overload; virtual;
abstract;
    procedure Do_RemoveSection(
      const ASectionName : string;
      const AParentSections : TStrings); overload; virtual;
abstract;
    function Do_HasSection(
      const ASectionName : string;
      const AParentSections : TStrings) : Boolean; overload;
virtual; abstract;

    // For writing the associated values to the configuration
file...
    procedure Do_WriteConfigurationValue(
      const AParentSections : TStrings;
      const ASectionName : string;
      const AValueName : string;
      const AValue : string); overload; virtual; abstract;
    procedure Do_WriteConfigurationValue(
      const AParentSections : TStrings;
      const ASectionName : string;
      const AValueName : string;
      const AValue : Integer); overload; virtual; abstract;
    procedure Do_WriteConfigurationValue(
      const AParentSections : TStrings;
      const ASectionName : string;
      const AValueName : string;
      const AValue : Boolean); overload; virtual; abstract;
    procedure Do_WriteConfigurationValue(
      const AParentSections : TStrings;
      const ASectionName : string;
      const AValueName : string;
      const AValue : Real); overload; virtual; abstract;

    // For reading the associated values from the configuration
file...
    function Do_ReadConfigurationValue(
      const AParentSections : TStrings;
      const ASectionName : string;
      const AValueName : string;
      const ADefaultValue : string) : string; overload; virtual;
abstract;
    function Do_ReadConfigurationValue(
      const AParentSections : TStrings;
      const ASectionName : string;
      const AValueName : string;
      const ADefaultValue : Integer) : Integer; overload; virtual;
abstract;
    function Do_ReadConfigurationValue(
      const AParentSections : TStrings;
      const ASectionName : string;
      const AValueName : string;
      const ADefaultValue : Boolean) : Boolean; overload; virtual;
abstract;
    function Do_ReadConfigurationValue(
      const AParentSections : TStrings;
      const ASectionName : string;
      const AValueName : string;
      const ADefaultValue : Real) : Real; overload; virtual;
abstract;

注意:唯一不需要被覆盖的方法是Initialise Finalise 方法。如果您不覆盖其他方法,当您尝试调用未覆盖的方法时,将收到EAbstractError 异常。如果您对如何创建自己的实现感到有点困惑,只需查看我派生的派生类,您应该能够从中找出答案 :)。

要求

此示例要求您安装 Delphi 7 或更高版本(您可能可以使用 Delphi 6,但由于我没有安装,您可能需要调整一两个地方才能使此演示工作。如果您对此有任何问题,请告诉我,我将在可能的情况下提供帮助),尽管您可能需要根据 Delphi 2009 及更高版本的默认选项更改其一部分以与 AnsiString/UnicodeString 配合使用,但我真的不认为这将是一个问题。如果有人能证实这一点,请告诉我,我将相应地更新此文章。

历史

  • 2011年11月20日:初始版本
  • 2011年11月22日:对基础代码进行大修,并重新整理/重写了初始文章
© . All rights reserved.