在Lazarus下的Free Pascal编程教程——应用程序配置数据的管理
0.前言
我想通过编写一个完整的游戏程序方式引导读者体验程序设计的全过程。我将采用多种方式编写具有相同效果的应用程序,并通过不同方式形成的代码和实现方法的对比来理解程序开发更深层的知识。
了解我编写教程的思路,请参阅体现我最初想法的那篇文章中的“1.编程计划”和“2.已经编写完成的文章(目录)”:
学习编程从游戏开始——编程计划(目录) - lexyao - 博客园
我已经在下面这篇文章中介绍了使用LCL和FCL组件构建一个项目(pTetris)的过程,后续的使用Lazarus的文章中使用的例子都是以向这个项目添加新功能的方式表述的:
在Lazarus下的Free Pascal编程教程——用向导创建一个使用LCL和FCL组件的项目(pTetris) - lexyao - 博客园
在前面写的文章中我们已经构建了pTetris项目的框架,并逐步添加了一些功能,作为示例的应用程序俄罗斯方块游戏已经达到了可玩的程度,并提供了丰富的操作方法。在这篇文章中我们将通过示例讲述更多组件的使用方法。
俄罗斯方块游戏中操作方块是核心。在前面的示例中我们已经让作为示例的应用程序俄罗斯方块游戏已经达到了可玩的程度,并提供了丰富的操作方法,在这篇文章中我们将增加定制游戏计分方法、加速算法和难度设置的功能,而在设置的操作界面中加入了能够识别当前情景的智能感知能力。
在这篇文章里,我主要讲述以下几个方面的内容:
- 应用程序配置数据管理概述
- 管理配置数据的类的实现方法
- 给pTetris项目定制配置数据管理的类
- 在pTetris项目中使用配置数据
- 结束语
1.应用程序配置数据管理概述
应用程序的配置数据是需要保存在磁盘文件中的,否则就需要每次打开应用程序时都需要重新设置一次。
早期的应用程序配置数据通常是保存在扩展名为cfg或ini的文件中,前者的格式因应用程序而异,后者是一种通用的格式。Windows早期版本的注册表就是使用了这种格式,现在还有很多应用程序在使用这种格式。
ini格式的文件有很大的局限性,或许这就是Windows的注册表更换了文件格式的原因吧。
随着xml格式文件得到认可,越来越多的应用程序开始使用xml格式的文件来保存配置数据。xml的全称是可扩展标记语言(Extensible Markup Language),是W3C推荐的在不同系统之间交换信息语言。它是一种基于文本的方式来存储信息。
在互联网上作为数据交换使用的另一种格式是json (JavaScript Object Notation)。json是一种轻量级的数据交换格式,它提供了简单的文本标准化数据交换格式。正如其名称所暗示的,它是基于JavaScript编程语言的一个子集,但它与语言无关,除了易于阅读和编写,更易于机器解析和生成。json作为xml的有力竞争者,在应用程序数据管理中占有很重要的地位,也是非常适合用于保存配置数据的一种格式。
无论是Delphi还是Lazarus,都有支持ini、xml和json的类,其中作为配置文件使用的类,包括:
- TIniFile:Delphi和Lazarus中都可以使用的用于存取ini格式文件的一个类。可参阅Using INI Files/zh CN - Free Pascal wiki
- TXMLConfig:Lazarus定制的用于管理xml格式配置数据管理的类。可参阅xmlconf - Free Pascal wiki
- TJsonConfig:Lazarus定制的用于管理json格式配置数据管理的类。可参阅fcl-json/zh CN - Free Pascal wiki
其中,TXMLConfig和TJsonConfig提供了相似的成员函数,用户使用时只需要使用他们提供的成员函数存取数据,而不需要关心它内部是怎样使用xml或json格式的数据。
2.管理配置数据的类的实现方法
- 构建与存储格式无关的数据管理类,在存取函数中实现数据格式的转换
- 在存储数据的函数中将配置数据转换成特定格式的数据存入磁盘文件
- 在读取数据的函数中将从磁盘文件中读入特定格式的数据转换成配置数据
- 构建基于特定格式的数据管理类,使用特定格式数据管理对象管理配置数据
- 在配置数据管理类中设置一个管理特定格式数据的对象
- 特定格式数据管理对象负责配置数据在内存中的管理、数据的存盘和读取
我们在pTetris中将采用后一种方式。
数据存盘文件可以有多种格式可供选择:
- 二进制数据文件:与内存中的数组结构相对应,读入内存后不需要解析就可以直接使用,便捷快速,但记录结构发生变化后无法读取原来的数据,所以兼容性差
- 简单的记录文件:这类文件通常由特定的应用程序约定和存取
- 复杂的数据库文件
- 单文件数据库:一个数据库文件中保存若干个表
- 多文件数据库:一个数据库由多个文件构成,每个文件包含一个表
- 复杂格式文件:这类文件通常由特定的应用程序或者规范约定,采用专用的应用程序存取,比如图形文件、视频文件、音频文件
- 文本文件:读入内存时按需要将字符串转换为其他数据类型,消耗时间比二进制要长一些,但由于是按标识符识别数据,所以不受记录格式的影响,兼容能力强
- 多行文本文件
- 简单表格文件:每个行存在用特定符号分隔的数据,所有行的数据格式和个数相同。
- 复杂表格文件:每一行都有一个特定的标识符,对应的数据格式各不相同。比如注册表文件
- 复杂格式文件:这类文件通常由特定的应用程序或者规范约定,采用专用的应用程序存取
- 按一定格式组织的文本文件
- 文件格式有json、xml等多种格式,每一种格式的数据都有成熟的类
- 多行文本文件
在编写应用程序时管理数据的对象尽可能采用成熟的数据管理工具,这样可以减少工作量又能提供可靠的数据管理能力。
配置数据通常数量较少,结构简单,有升级版本后的兼容性要求,所以不需要复杂的数据结构,但优选以文本格式保存。
基于以上分析,我们确定在pTetris项目中采用json或xml结构的文本格式保存配置数据。
3.json、xml简介
3.1 关于在Lazarus中使用json的介绍
JSON (JavaScript Object Notation) 是种数据表现形式,它提供了简单的文本标准化数据交换格式。正如其名称所暗示的,它是基于JavaScript编程语言的一个子集,但它与语言无关,除了易于阅读和编写,更易于机器解析和生成。相对于XML,它易于阅读。
想了解更多关于json以及在Lazarus中使用json的知识,请阅读以下链接中的文章:
- JSON官方网站
- JSON/zh CN - Free Pascal wiki
- fcl-json/zh CN - Free Pascal wiki
- Streaming JSON/zh CN - Lazarus wiki
- 【delphi】 JSON 操作详解(TJSONObject)_delphi tjsonobject-CSDN博客:Delphi官方的json支持方案
- Delphi 与 JSON - 随笔分类 - 万一 - 博客园:虽然写的是SuperObject在Delphi中的应用,但内容同样适用于Lazarus
- GitHub - pult/SuperObject.Delphi: Pascal (Delphi, FPC) json parser library SuperObject:SuperObject的下载及使用说明
- SuperObject 库的强大功能:JSON 数据处理的不二之选-易源AI资讯 | 万维易源
本来想写一篇在Lazarus中使用json的专题文章,可在网上搜索的时候,发现这方面的文章太多了,而且都比我写得好,所以我就不敢写了。
无论是Delphi还是Lazarus,作为官方首推的json支持都有各自的特点,在网上也有大量的第三方的可用的实现方法。在这些方法中,我曾经对能找到的方法都做了测试,经过对比,我最终选择了SuperObject。我编写的很多应用程序的配置数据管理都是使用了SuperObject,甚至有些内部数据管理也使用了SuperObject。
SuperObject最大的好处是使用方便,但并不是说它是完美无缺的。以下是我的几点体会:
- SuperObject采用的是接口技术,使用的时候不必为了内存漏洞或者说内存残留而烦恼,这是因为接口是自动释放的,不用了也就释放了内存,不会出现残留
- SuperObject定义了丰富的数据存取函数,几乎涵盖了我们能够用到的所有数据格式
- SuperObject的路径管理很方便,使用的时候你只需要关心你的路径(小数点分隔的字符串),而不需要关心节点对象
- SuperObject的容错能力有一点缺陷,我在用它来解析网页中的json数据的时候,经常因为数据错误而造成解析失败,后来我重写了它的解析函数,提高了容错能力
- SuperObject作为内存数据管理虽然方便,但若干用来管理频繁使用的大量数据是不合适的,这是因为它解析路径的方法是单字符循环分析,耗费时间太长导致程序运行变得很慢。对于少量的数据管理,这种变慢是可以忽略的,但大量数据就不一样了,会让你能够感觉到程序运行中短暂的“停顿”
如果你想使用SuperObject,可以从网上下载,上面给出的链接中有一个是SuperObject的下载地址。下载的文件包中有两个文件:
- superobject.pas:提供了完整的json支持,我们使用的就是这个文件
- superxmlparser.pas:提供了xml格式转换为json格式的方法,算是一个辅助文件,如果你不需要数据转换就可以不用管这个文件
不过,既然我们介绍的是Lazarus程序设计,还是使用Lazarus提供的方案更合适。
3.2 关于在Lazarus中使用xml的介绍
以下这段话是摘录的百度百科《可扩展标记语言_百度百科》中关于xml的介绍:
可扩展标记语言 (Extensible Markup Language, XML) ,标准通用标记语言的子集,可以用来标记数据、定义数据类型,是一种允许用户对自己的标记语言进行定义的源语言。 XML是标准通用标记语言 可扩展性良好,内容与形式分离,遵循严格的语法要求,保值性良好等优点。
在电子计算机中,标记指计算机所能理解的信息符号,通过此种标记,计算机之间可以处理包含各种的信息比如文章等。它可以用来标记数据、定义数据类型,是一种允许用户对自己的标记语言进行定义的源语言。 它非常适合万维网传输,提供统一的方法来描述和交换独立于应用程序或供应商的结构化数据。是Internet环境中跨平台的、依赖于内容的技术,也是当今处理分布式结构信息的有效工具。早在1998年,W3C就发布了XML1.0规范,使用它来简化Internet的文档信息传输。
想了解更多关于xml以及在Lazarus中使用xml的知识,请阅读以下链接中的文章:
- XML Tutorial/zh CN - Free Pascal wiki
- XML Decoders/zh CN - Lazarus wiki
- fcl-xml - Free Pascal wiki
- Category:XML - Lazarus wiki
- XML从入门到深入(超详细) - 蚂蚁小哥 - 博客园
- 【学习】lazarus的Laz2_XMLCfg用法 - 秋·风 - 博客园
- Using TXMLDocument - RAD Studio:Delphi中的xml支持,Lazarus同样适用
- NativeXml (1):下载、安装、测试 - 万一 - 博客园:NativeXml是一个非常优秀的第三方xml实现代码,深受Delphi用户的欢迎
- JCL Help:TJclSimpleXML - Project JEDI Wiki:TJclSimpleXML 是JEDI的jcl中的xml支持方案,我比较喜欢这个,真正的简单易用,但功能一点也不简单。
Delphi官方推出的xml支持使用的是TXMLDocument,由于使用比较繁琐,我不太喜欢这个。Lazarus中的xml也是使用了TXMLDocument,我还没有研究它与Delphi中TXMLDocument有什么不一样,估计没有太多的差别。
在使用Delphi的时候,我们尝试了很多xml的支持方案,最终确定使用的是TJclSimpleXML ,真正的简单易用,但功能一点也不简单。
4.给pTetris项目定制配置数据管理的类
4.1 在项目中使用管理配置数据的方法分析
Lazarus提供了TXMLConfig和TJsonConfig来管理配置数据,有了这样的类可以达到我们的目的,我们就没有必要再费心劳神去编写自己的代码。那么,我们怎样利用这些现成的代码去管理配置数据呢?
具体说来,管理和使用配置数据需要做以下几个方面的工作:
- 创建一个变量指向配置数据管理类,可以命名为FConfigData。在这里变量FConfigData的类型应该改是TXMLConfig或TJsonConfig
- 建立一个函数ConfigData用来引用变量FConfigData,之所以需要使用函数,目的是确保在引用变量之前对变量进行检查,确保变量已经创建并初始化,具体包括以下几个方面:
- 如果变量FConfigData是空的,则创建一个新的对象,然后:
- 如果存在以前保存的配置数据文件,则读入配置文件
- 如果还没有配置文件,则初始化FConfigData所指对象的数据成员为默认值
- 如果变量FConfigData是空的,则创建一个新的对象,然后:
- 在使用配置数据的地方使用函数ConfigData引用FConfigData
- 在选择配置数据的页面的事件代码中将改变的配置数据存入FConfigData对象的成员
- 在展示配置页面之前使用上次保存的或默认的配置数据设置配置页面组件的显示值
- 在使用配置数据的地方将FConfigData成员中的配置数据提供给应用程序
- 游戏开始时使用配置数据设置初始速度、初始方块高度
- 游戏中使用配置数据设置方块组合样式、方块表面图案或颜色
- 移动一组方块结束时使用配置数据计算得分、加速、
- 在程序结束之前
- 如果FConfigData中的数据有修改,则保存到磁盘文件
- 释放FConfigData所指的对象
实现以上功能的代码可以有两种方式:
- 在TfrmMain中定义变量和函数
- 在TfrmMain中使用可直接调用函数
- 在cxBoxQueue、cxBoxMove、cxBoxHeap等类中使用可通过变量frmMain引用TfrmMain的成员函数
- 需要在pTetrisUint的uses中添加pTetrisMain
- 在pTetrisUint单元中定义变量和函数
- 直接定义变量和函数
- 可以使用全局函数,也可以打包在一个静态类中
- 定义一个变量指向配置数据类的实例
- 定义一个函数引用配置数据类
- TfrmMain、cxBoxQueue、cxBoxMove、cxBoxHeap通过这个函数引用配置数据类的实例
- 操作函数通过这个函数引用配置数据类的实例
- 定义多个函数操作配置数据类:初始化、读入、保存
- TfrmMain、cxBoxQueue、cxBoxMove、cxBoxHeap通过函数使用配置数据
- 在finalization释放这个类的实例
- 将变量和函数打包在一个类中
- 配置数据类的变量、引用、操作函数等都打包在一个类中
- 定义一个变量指向这个类的实例
- 定义一个函数引用这个类
- TfrmMain、cxBoxQueue、cxBoxMove、cxBoxHeap通过使用这个类的实例使用配置数据
- 在finalization释放这个类的实例
- 直接定义变量和函数
以上分析中使用的方法说不上哪一个更好,都是合理的方法,选用哪一个全在个人的习惯。
4.2 配置数据管理类cxConfig的实现
从面向对象的程序设计考虑,在这里我们选择将函数打包在一个类中。下面开始介绍配置数据管理函数代码编写的步骤。
TXMLConfig与TJsonConfig具有相同的成员函数,使用参数的格式也是相同的,所以在这里我们以TXMLConfig为例。如果想使用TJsonConfig,只需要将下面代码中的TXMLConfig替换为TJsonConfig就行了,其他的代码不需要任何改动,差别只是存盘文件的格式发生了变化。
4.2.1 引用xmlConf单元,创建配置数据管理类cxConfig的框架
要使用TXMLConfig类,首先需要引用TXMLConfig所在的单元。在pTetrisUint单元的uses中添加xmlConf,就可以使用TXMLConfig类了。
依据面向对象的程序设计思路,我们需要把保存配数据的TXMLConfig类实例及对其数据的操作封装在一个类中。在这里我们创建配置数据管理类cxConfig的框架,其中:
- 变量FConfigData: TXMLConfig用于管理配置数据
- cxConfig类的所有成员函数都是为了操作FConfigData中的数据而设置的
- 使用属性ConfigData引用FConfigData变量所指的对象,这是封装的习惯做法
- 在类的外部使用ConfigData而不能使用FConfigData
cxConfig = class(TComponent) private FConfigData: TXMLConfig; protected public property ConfigData: TXMLConfig read FConfigData; end;
4.2.2 创建、初始化和销毁FConfigData对象
定义了FConfigData对象后,在使用FConfigData前要创建FConfigData对象的实例,初始化其中的数据,在退出程序前要销毁FConfigData对象的实例。
- 创建和初始化FConfigData对象:在使用中的FConfigData中的配置数据之前必须先创建FConfigData对象并初始化FConfigData中的数据,具体需要完成以下操作:
- 创建TXMLConfig类的实例保存在FConfigData中
- 初始化FConfigData对象中的数据,包括以下两种情况:
- 如果不存在配置数据文件,需要FConfigData能够提供配置数据的默认值
- 如果存在配置数据文件,需要将配置数据文件中的数值装入FConfigData对象中
- 销毁FConfigData对象:一般情况下,销毁FConfigData对象的代码放在重写的析构函数destructor Destroy中最合适。这个没有争议。我们把定义为组件,TXMLConfig也是组件,只要FConfigData的Owner不是空值,FConfigData会在它的Owner所指的组件销毁之前销毁,我们不必再编写销毁FConfigData的代码
在cxConfig类定义中添加以下代码:
private FConfigData: TXMLConfig; public property ConfigData: TXMLConfig read FConfigData; procedure Init;
在定义的代码中,函数Init将包含创建和初始化FConfigData对象的代码。在这里,我们先加入创建FConfigData对象的代码,初始化的代码将在后面加入。
procedure cxConfig.Init; begin
if not Assigned(FConfigData) then begin //创建配置文件对象 FConfigData := TXMLConfig.Create(Self); end; end;
4.2.3 设定配置数据的初始值
按着一般的编程习惯,程序员会选择每个配置选项最常用的数值作为选项的初始值(有时也称作默认值)。在用户第一次运行程序的时候,所有选项的当前值都是初始值,这样做可以保证多数用户不做任何选项的修改就可以使用应用程序。
一般情况下,程序员需要为每个选项定义一个常数作为默认值,定义一个读取配置数据的函数,当配置数据中没有这个选项的数据时,就返回默认值。
为了减少编写代码,在这里我们采用了另一种方法:
- 在界面设计中将选项的组件当前值设置成默认值
- 在第一次运行应用程序的时候将组件的当前值保存到配置文件中,这样确保配置文件中有每个选项的值,不再需要单独为其设置默认值常数
为此,我们添加两个函数:
- ValueFrom(ACtrl: TComponent)将选项组件ACtrl的当前值保存到配置文件FConfigData中
- cxConfig.PathOf函数用来生成保存文件的路径,如果想修改路径,只需要修改这个函数的代码就可以了
- 在ValueFrom中采用按组件分类,采用组件名作为保存数据的路径,这样可以避免为每一个选项编写一个存取函数、定义一个路径常数,从而减少的代码量。
需要提示一点:ValueFrom的这种设计方法是一种技巧,这种技巧的好处将会在后面的代码中体现出来
- DataInit(ACtrl: TComponent)从配置表的顶层容器PageControl1开始,通过递归的方法将其包含的所有选项组件通过ValueFrom函数保存数据到配置文件FConfigData中
- 为了保证使用FConfigData的时候在FConfigData中有选项的数据,需要在其他使用FConfigData的代码之前运行DataInit函数的代码
function cxConfig.PathOf(ACtrl: TControl): string; begin Result := ACtrl.Name; end; procedure cxConfig.ValueFrom(ACtrl: TComponent); var pth: string; begin if not (ACtrl is TControl) then Exit; //获得数据保存的路径 pth := PathOf(ACtrl as TControl); //为了简化代码,按组件类型保存组件属性值作为配置数据 if ACtrl is TPageControl then ConfigData.SetValue(pth + '/ActivePageIndex', (ACtrl as TPageControl).ActivePageIndex) else if ACtrl is TCheckBox then ConfigData.SetValue(pth + '/Checked', (ACtrl as TCheckBox).Checked) else if ACtrl is TRadioGroup then ConfigData.SetValue(pth + '/ItemIndex', (ACtrl as TRadioGroup).ItemIndex) else if ACtrl is TComboBox then ConfigData.SetValue(pth + '/ItemIndex', (ACtrl as TComboBox).ItemIndex) else if ACtrl is TColorButton then ConfigData.SetValue(pth + '/ButtonColor', (ACtrl as TColorButton).ButtonColor) else if ACtrl is TTrackBar then ConfigData.SetValue(pth + '/Position', (ACtrl as TTrackBar).Position); end; procedure cxConfig.DataInit(ACtrl: TComponent); var i: integer; ctrl: TWinControl; child: TControl; begin //采用递归方法遍历所有组件 if ACtrl is TWinControl then begin ctrl := ACtrl as TWinControl; for i := 0 to ctrl.ControlCount - 1 do begin child := ctrl.Controls[i]; DataInit(child); end; end; //保存组件当前值到配置文件 ValueFrom(ctrl); end;
4.2.4 保存配置数据到磁盘文件
如果每次运行程序都需要重新设置配置数据是不合理的,这不仅仅是重复做同样的工作消耗时间。要想在下一次进入程序时能够重现上次使用时设置的配置数据,就需要在退出程序之前把配置数据保存到磁盘文件。要把配置数据保存到磁盘文件,需要做以下三件事:
- 确定存盘文件名,为此我们添加函数cxConfig.FileName
- 编写存盘函数cxConfig.SaveToFile
- 调用cxConfig.SaveToFile,我们在重写的析构函数destructor Destroy中调用
function cxConfig.FileName: string; var fn: string; begin fn := Application.ExeName; Result := ChangeFileExt(fn, '.xml'); end; procedure cxConfig.SaveToFile; begin //仅在配置数据修改时保存配置数据到文件中 if Assigned(FConfigData) and FConfigData.Modified then begin FConfigData.SaveToFile(FileName); end; end; destructor cxConfig.Destroy; begin SaveToFile; inherited Destroy; end;
4.2.5 从磁盘文件中读入配置数据
在上次使用应用程序的时候修改的配置数据在退出应用程序的时候保存到了磁盘文件,再次运行应用程序的时候要使用上次的配置数据,就需要从磁盘文件中读入配置数据,为此需要编写以下几个方面的代码:
- 编写读入磁盘文件的函数cxConfig.LoadFromFile
- 将读入的配置数据在配置表组件中重现,需要编写以下函数实现与ValueFrom、DataInit相反的操作:
- ValueTo(ACtrl: TComponent)将FConfigData中的配置数据恢复的对应的组件ACtrl中
- DataReset(ACtrl: TComponent)从配置表的顶层容器PageControl1开始,通过递归的方法将其包含的所有选项组件通过ValueTo函数从配置文件FConfigData中获得上次的选项值
- 为了保证使用FConfigData的时候在FConfigData中有选项的数据,需要在其他使用FConfigData的代码之前运行LoadFromFile、DataReset函数的代码
procedure cxConfig.ValueTo(ACtrl: TComponent); var pth: string; iDef: Integer; begin if not (ACtrl is TControl) then Exit; //获得数据保存的路径 pth := PathOf(ACtrl as TControl); //为了简化代码,按组件类型获取组件属性值作为配置数据 if ACtrl is TPageControl then (ACtrl as TPageControl).ActivePageIndex := ConfigData.GetValue(pth + '/ActivePageIndex', 0) else if ACtrl is TCheckBox then (ACtrl as TCheckBox).Checked := ConfigData.GetValue(pth + '/Checked', True) else if ACtrl is TRadioGroup then (ACtrl as TRadioGroup).ItemIndex := ConfigData.GetValue(pth + '/ItemIndex', 0) else if ACtrl is TComboBox then (ACtrl as TComboBox).ItemIndex := ConfigData.GetValue(pth + '/ItemIndex', 0) else if ACtrl is TColorButton then (ACtrl as TColorButton).ButtonColor := ConfigData.GetValue(pth + '/ButtonColor', 0) else if ACtrl is TTrackBar then begin //如果有不同的默认值,可以按组件名识别组件,确定不同的默认值 if IndexText(ACtrl.Name, ['trcStartTime', 'trcNextNumber'])>=0 then iDef := (ACtrl as TTrackBar).Max else iDef := (ACtrl as TTrackBar).Min; (ACtrl as TTrackBar).Position := ConfigData.GetValue(pth + '/Position', iDef); end; end; procedure cxConfig.DataReset(ACtrl: TComponent); var child: TControl; ctrl: TWinControl; i: integer; begin if ACtrl is TWinControl then begin //采用递归方法遍历所有组件 ctrl := ACtrl as TWinControl; for i := 0 to ctrl.ControlCount - 1 do begin child := ctrl.Controls[i]; DataReset(child); end; //用配置文件中的数据设置组件的值 ValueTo(ctrl); end; end;
4.2.6 集成初始化FConfigData的代码
前面提到,为了保证使用FConfigData的时候在FConfigData中有选项的数据,需要在其他使用FConfigData的代码之前运行LoadFromFile、DataReset、DataInit函数的代码。现在我们把调用这三个函数的代码集成在一个函数Init中:
procedure cxConfig.Init; begin if not Assigned(FConfigData) then begin //创建配置文件对象 FConfigData := TXMLConfig.Create(Self); //从磁盘中装入上次退出时保存的配置数据 if LoadFromFile then DataReset(Owner) //如果存在上次的配置数据,则用配置数据设置配置界面中的组件 else DataInit(Owner); //如果不存在上次的配置数据,则用配置界面中组件的值为配置数据的初始值 end; end;
4.2.7 确定运行cxConfig.Init的位置
cxConfig.Init函数包含了创建和初始化FConfigData对象的代码,运行Init函数的位置需要满足以下两个要求:
- 需要在创建cxConfig类的实例之后运行
- 需要在使用FConfigData中数据的代码之前运行
满足以上两个条件的位置有以下几种:
- 在cxConfig类内部隐式调用,通常有两种方式
- 在cxConfig的构造函数中,需要重写constructor Create(AOwner: TComponent); override;
- 在cxConfig的AfterConstruction函数中,需要重写procedure AfterConstruction; override;
- 在cxConfig类外部显示调用
- 需要紧跟在代码 FConfig := cxConfig.Create(...)之后添加代码FConfig.Init;
方案一:在cxConfig的构造函数中调用Init,代码如下:
constructor cxConfig.Create(AOwner: TComponent); begin inherited Create(AOwner); Init; end;
方案二:在cxConfig.AfterConstruction函数中调用Init,代码如下:
procedure cxConfig.AfterConstruction; begin inherited AfterConstruction; Init; end;
方案三:在cxConfig类之外显示调用Init,需要在创建cxConfig类实例的地方添加以下两行代码代码:
FConfig := cxConfig.Create(ARootCtrl); FConfig.Init;
为了简化代码,我们可以把这两行代码打包在一个函数中:
class procedure cxConfig.CreateAndInit(ARootCtrl: TComponent); begin FConfig := cxConfig.Create(ARootCtrl); FConfig.Init; end;
这样在创建cxConfig类实例的地方使用cxConfig.CreateAndInit(...)就行了。
如果使用方案一或者方案二,也可以使用CreateAndInit函数,只是需要注释掉其中的FConfig.Init。
4.3 配置数据管理类cxConfig的应用
前面已经完成了cxConfig类及其成员的代码编写,现在我们可以在程序中使用cxConfig类了。
4.3.1 定义cxConfig类类型的变量
首先定义保存cxConfig实例的变量。由于需要在pTetrisUint、pTetrisMain两个单元中使用,在pTetrisUint单元中有多个类使用,所以将变量定义为pTetrisUint单元中的全局变量:
var FConfig: cxConfig;
4.3.2 创建cxConfig类的实例
创建cxConfig类的实例的代码要在所有使用它之前运行,而创建后需要设置配置表组件的值,需要在TfrmMain.FormCreate之后执行,所以选择在TfrmMain.FormShow中添加创建cxConfig类的实例的代码:
cxConfig.CreateAndInit(PageControl1);
至此,我们就可以编译运行pTetris项目进行代码测试了。
按着在Delphi中编写程序的经验,前面的三种方案都没有问题,但在Lazarus下编写的隐式调用Init的代码(方案一和方案二)却出现了运行错误,经过多次调试没有找出原因。
方案三运行正常,我们只好选择方案三的代码。
现在进行测试只能看能不能正常运行,从界面上是看不到变化的。我们能看到的只有保存的配置文件。由于配置文件在退出应用程序的时候才会保存,所以我们运行pTetris后退出程序,到pTetris.exe所在的文件夹找到一个pTetris.xml的文件,这就是存盘后的配置数据文件,里面的内容是配置表界面中组件的初始值。用记事本打开pTetris.xml文件,你会看到如下内容:
<?xml version="1.0" encoding="utf-8"?> <CONFIG> <cbBoxLine ItemIndex="0"/> <grpBoxLine ItemIndex="1"/> <trcScoreBase Position="1"/> <ckScoreHeight Checked="True"/> <trcScoreHeight Position="1"/> <ckScoreDesBase Checked="True"/> <trcScoreDesBase Position="1"/> <ckScoreDesRows Checked="True"/> <trcScoreDesRows Position="1"/> <ckScoreDesHeight Checked="True"/> <trcScoreDesHeight Position="1"/> <trcSpeedBase Position="1"/> <ckSpeedKey Checked="True"/> <trcSpeedKey Position="1"/> <ckSpeedTimer Checked="True"/> <trcSpeedTimer Position="1"/> <grpTimeCalc ItemIndex="0"/> <trcStartTime Position="9"/> <trcNextNumber Position="4"/> <trcStartHeight Position="0"/> <grpBoxStyle ItemIndex="0"/> <ckPenetrate Checked="False"/> <PageControl1 ActivePageIndex="2"/> </CONFIG>
4.3.3 保存修改的配置数据
在前面的测试中你会看到,无论在配置表中怎么修改选项,当再次运行pTetris应用程序的时候,修改的选项又恢复了原来的数值。这是因为我们的配置数据保存的是设计时在窗体设计器和属性列表中设定的组件的值,也就是选项的初始值,修改的值并没有保存在文件中。
要想保存修改的值,就需要添加代码将修改后的数据保存到FConfigData中,这样在退出pTetris的时候修改的数据就保存了。
我们先在一个组件中添加代码进行一个测试,查看保存配置数据的效果。选择配置表的起始难度页面中的方块组合样式,也就是组件grpBoxStyle,给它添加OnSelectionChanged事件的处理函数,在实现中添加如下代码:
procedure TfrmMain.grpBoxStyleSelectionChanged(Sender: TObject); begin FConfig.ValueFrom(Sender as TComponent); end;
编译运行pTetris项目,修改方块组合样式的选中的项目,然后退出pTetris,再次运行pTetris,看修改的选项是不是恢复到了你上次选中的项目?这说明我们添加的代码起作用了。
在添加的保存选项的代码中我们使用了事件处理函数参数中的Sender作为ValueFrom函数的参数,这样就不必为每个选项编写一个函数或路径了。
给需要保存数据的组件都添加这行代码,这样凡是添加了这行代码的组件修改后的选项都会保存下来:
FConfig.ValueFrom(Sender as TComponent);
不同类型的组件添加这行代码的事件是不同的,以下是各种组件使用事件处理函数:
- TPageContro、TComboBoxl、TTrackBar、TCheckBox的事件是OnChange
- TRadioGroup的事件是OnSelectionChanged
- TColorButton的事件是OnColorChanged
需要保存数据的组件都添加了这行代码后再编译运行pTetris项目,测试修改选项,当再次运行的时候所有的选项都会重现修改后的数据而不是设计时设置的初始值。打开pTetris.xml文件,你也会看到其中的数据改成了你在选项表中设置的数值。
5.结束语
在这篇文章里我们实现了配置数据的把保存、恢复、使用。在编写代码的过程中,讲述了xml和json格式数据的使用,并应用xml格式保存了我们的配置数据。在确定保存配置数据的路径时我们使用了一点小技巧,达到了节省代码的效果。
任何一个应用程序都可能用到各种各样的配置数据,管理和使用配置数据的方法会有所不同,但大同小异,道理是一样的。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· 单线程的Redis速度为什么快?
· SQL Server 2025 AI相关能力初探
· AI编程工具终极对决:字节Trae VS Cursor,谁才是开发者新宠?
· 展开说说关于C#中ORM框架的用法!