Delphi 小谈之TList 篇


Delphi 小谈之TList 篇
2010年08月23日
  看了这里标题,大家可能以为我会谈TListBox控件,那就错了。我要谈的是Delphi提供给我们的具有列表性质的类:TstringList、TList和TObjectList。TstringList用来存放字符串,TList存放指针,而TObjectList则存放对象(Object)。在我们使用Delphi 的过程中,有很多数据的存储是要靠数组解决的。虽然Delphi现在已经支持了可变数组,不过总有那么点缺陷:我们不能在删除一个项后,使后边的项自动前靠。因此,说说Delphi现成的List还是有价值的。 TstringList (Classes.pas)在TstringList里,那些String被一行一行地储存。TstringList.Text返回全部的String。如果第一、二、三行分别是\'aa\'、\'bb\'、\'cc\' 的话,那么Text 返回的是"\'aa\'+#13#10+\'bb\'+#13#10+\'cc\'+#13#10" (不包括双引号)。所有的String都被TstringList用回车和换行符(#13#10)连接了起来。如果依次向Text赋值的话,Text就会被自动地分割成行储存在TstringList 里。这充分地体现出TstringList的一个很实用的价值:它能让我们逐行处理String。假如我们要操作第4行,只需操作TstringList[3]。相信大家会问,TstringList明明是一个类,为什么能当数组那样子用呢?其实,我们在写TstringList[3]的时候,就是在写TstringList.Strings[3]。Strings是TstringList的一个缺省属性。数组性的缺省属性就是这样子使用的。如果大家在编写类的时候要用到这么一个功能的话,可以参照如下方法: property AProperty[I: Integer] read *** write ***; default;。 Strings 是一个可读写的属性。这也就是说,大家不仅可以获得第N 行的内容,也可以改变第N 行的内容。因此我们需要知道TstringList 里S t r i n g 的总行数。TstringList的属性Count则可以满足我们的需要。上面已经说过,Text是一个返回所有字符串的属性。向Text赋值时,TstringList能够自动地把Text分成一行一行的,然后储存在TstringList里(当然,TstringList里面并不完全是这么储存的,详细过程建议看TstringList和TStrings的代码)。这样,Strings返回的字符串就是没有回车和换行的。但是,如果我们向Strings赋值的字符串有回车和换行,那么会出现什么情况呢?此时,Strings就会把那个字符串断成几行,插入到原来的位置上。如果TstringList只有这么些功能的话,那我就不必专门拿出来讲了--我是说,TstringList能让我们任意地插入或删除某行,这就要用到TstringList提供的方法。 function Add(const S: string): Integer; procedure Append(const S: string); Add方法向TstringList的末尾添加一行String(在这里和下面我们都假设输入的字符串没有回车和换行,否则String将被分割)。参数S 代表的是要插入的字符串的内容。Add的返回值代表了新的字符串在TstringList的位置--也就是最后一行的位置,即新的Count 减去一。 Append 方法与Add 唯一不同的地方是没有返回值。 procedure Insert(Index: Integer; const S: string); Insert方法向TstringList插入一行字符串。在Insert里,我们可以自由地选择字符串插入的位置。参数S 代表要插入的字符串的内容,Index 代表要插入的位置。 procedure Delete(Index: Integer); Delete 方法删除某行字符串,我们同样可以自由地选择删除任意一行字符串。参数Index代表要删除的那一行字符串的位置。学习笔记 function IndexOf(const S: string): Integer; IndexOf查找某一字符串在TstringList里的位置。参数S代表要查找的字符串。如果TstringList里不存在S的话,则返回-1。 procedure Move(CurIndex, NewIndex: Integer); procedure Exchange(Index1, Index2: Integer); TstringList另外还提供了两个方法-- Move 和Exchange。Move方法能把一行字符串抽出来并插入到另一个指定的位置上。参数CurIndex代表要移动的字符串的位置,NewIndex 代表字符串新的位置。 Exchange方法则能将随便两行字符串交换。参数Index1和Index2代表两行需要交换的字符串的位置。 procedure LoadFromFile(const FileName: string); procedure SaveToFile(const FileName: string); TstringList的LoadFromFile和SaveToFile两个方法,使得我们对文本文件的操作变得非常方便。参数FileName 代表目标文本文件的文件名。例如我们在编写记事本的时候,用到的TMemo.Lines 就是TStrings(TstringList的父类,功能几乎相同,只是因为TStrings是虚类,我们无法创建并使用)。在保存的时候只需一行代码:TMemo.Lines.SaveToFile(FileName),非常方便。 TstringList还有一项特殊功能:可以把TstringList当成ini控制器使用。不过它没有Section。现在我就来介绍TstringList的两个属性:Names和Values。如果TstringList的内容是这样子的: ID=1 Name=Vczh PositionID=8 Tel=00000000 那么,Names[2]就返回"PositionID ",Values[\'PositionID\']就返回"8"。其中"Names"是只读属性,而"Value"则可写。TstringList使我们不必拘泥于ini和注册表。关于TstringList没有Section这个问题,我们完全可以在Names 里做点手脚,只要程序能够识别就行(文 章末尾处的范例里将出现此方法供大家参照)。 TstringList还有一个可以存放Object的功能。但是我个人认为使用TObjectList比较好,因为TObjectList在这方面提供了比TstringList更多的功能。 下面我提供一个例程来介绍Values属性。 新建一个工程保存,并在dpr文件所在的文件夹里建立一个叫做"Config.txt"的文件,并输入以下内容: Name=VCZH Address=Somewhere [email protected] 然后,建立一个窗体。如图所示:并在TForm1 的Private 区段里建立一个变量: SL:TstringList; 这个例程的功能是编辑Config.txt 里的Name 、Address、Email。SL在程序启动时读入Config.txt文件;按下Cancel 按钮则退出;按下OK 按钮则改变SL 的内容并保存在Config.txt文件里;当程序再次运行时,改变后的内容就会显示在三个文本框里。代码如下: procedure TForm1.FormCreate(Sender: TObject); begin SL:=TstringList.Create; { 获取当前程序文件所在的文件夹名称以获得C o n f i g . t x t 文件的路径 } SL.LoadFromFile(ExtractFilePath(ParamStr(0))+\'Con fig. txt\'); end; procedure TForm1.FormClose(Sender: TObject; var Action: TCloseAction); begin SL.Free; end; procedure TForm1.btnCancelClick(Sender: TObject); begin Close; end; procedure TForm1.FormShow(Sender: TObject); begin { 通过上面介绍的V a l u e s 属性获得各个字段的内容 } edtName.Text:=SL.Values[\'Name\']; edtAddress.Text:=SL.Values[\'Address\']; edtEmail.Text:=SL.Values[\'Email\']; end; procedure TForm1.btnOKClick(Sender: TObject); begin { 修改各个字段的内容并保存 } SL.Values[\'Name\']:=edtName.Text; SL.Values[\'Address\']:=edtAddress.Text; SL.Values[\'Email\']:=edtEmail.Text; SL.SaveToFile(ExtractFilePath(ParamStr(0))+\'Confi g. txt\'); Close; end; 程序执行后如图: Tlist (Classes.pas) 在我刚开始接触TList的时候,TList搞得我迷雾重重,都是Capacity属性惹的祸。我查了Delphi的帮助,它说Capacity是TList的最大容量,又在什么地方说MaxIntdiv 4是TList的最大容量。最后我搞明白了,Capacity是临时的,MaxInt div 4才是真正的最大容量。只要你的内 存受得了就行,算起来一共是4G。在TList 内部有一个FList指针指向一个Pointer数组,Capacity就是这个数组的大小。奇怪的是Capacity是可写的。我当时就在想,如果一直使用Add 直到超出了Capacity的范围,会怎么样呢?为了解决这个问题,我特地打开了TList 的代码,结果发现如下几行(注释是我自己加的): function TList.Add(Item: Pointer): Integer; begin { 返回Item 所在的位置 } Result := FCount; { 如果FList 数祖被填满了装不下新的Item 那么TList 自动增加Capacity 也就是FList 指向的数组的大小 } if Result = FCapacity then Grow; { 扩大了FList 的大小后,就能把 Item 放进数组了 } FList^[Result] := Item; Inc(FCount); if Item  nil then Notify(Item, lnAdded); { 给TList 一个信号,通知TList 已经加上了一个新的Item } end; procedure TList.Grow; var Delta: Integer; begin { 增加的规则是,如果数量小于或等于8,那么增加4 ;如果数量在8 之上,小于或等于64 ,那么增加16 ;如果数量比64 还大,那么一次增加大约1/4 的空间 } if FCapacity > 64 then Delta := FCapacity div 4 else if FCapacity > 8 then Delta := 16 else Delta := 4; { 改变数组大小 } SetCapacity(FCapacity + Delta); end; 既然Capacity会自动增加,那么还要Capacity干什么呢?还不如使用链表。不过我后来意识到,在使用链表的时候,取得某个位置的指针比数组困难,要用比较费时间的循环。TList刚好解决了这个问题。我们既可以把TList 当成数组,也可以把它当成链表。 TList 除了保存的对象是指针以外,其它地方都与TstringList很像。所以接下来我只介绍二者不同之处。 我们同样可以使用TList或者TList.Items获得某一位置的指针。如果嫌TList.Items是属性没有效率的话,这里还有一个List属性,指向内部的FList,可以这么用: TList.List^。 TList提供了First和Last两个属性,分别返回第一个和最后一个指针。 TList 也提供了一个Remove方法。与Delete不同的是,Delete删除的是已知位置的指针,Remove删除的是已知指针。只要TList 包含有你想删除的指针,就可以使用Remove(Pointer)。Remove的返回值是指针在还没有被删除之前的位置。使用方法如下: procedure Delete(Index: Integer); function Remove(Item: Pointer): Integer; TList还有一个Pack方法。它能够把所有不是nil的指针聚在一起,同时把Count 的值改变,这样,所有没用的指针就会被删除,但是并不会减少Capacity。如果你想把没用的空间都释放掉的话,可以把Capacity设置成Count。 最后,我想说的是Protected里的Notify。大家在Add的代码里就能看到,在Insert、Delete之类的代码里我们也能看得到Notify的踪迹。既然FList的内容已经被改变了,Notify 还要做什么工作呢?看一下Notify的代码:学习笔记 TListNotification = (lnAdded, lnExtracted, lnDeleted); procedure TList.Notify(Ptr: Pointer; Action: TListNotification); begin end; 留着一个空的Notify 有什么用呢?再看它的声明: procedure Notify(Ptr: Pointer; Action: TListNotification); virtual; 原来Notify 是一个虚函数,当我们因为有特殊要求而继承TList类的话,只要TList 的内容一改变,我们就能得到通知。不过前提是我们要覆盖N o t i f y 这个 Procedure。 TObjectList (Contnrs.pas) T O b j e c t L i s t 中有一个不可缺少的属性:OwnsObjects。如果OwnsObjects是True(缺省值)的话,那么TObjectList会在适当的时候把它列表中的Object释放掉。 现在,让我们用一个例子来结束我对Delphi的List的介绍。这个例子是一个管理人员信息的程序。不过因为这只是一个示例,所以这个程序只是一个简单的ConsoleApplication。使用Console Application来做示例程序可以免掉一些界面设计的工作。这个程序通过Index来管理人员信息。人员信息包括Name、Telephone和Address。程序通过TstringList来读取人员信息文件,然后用那些指向Record的指针把信息存放在TList里并修改。代码如下: 程序代码 program Info ; {$APPTYPE CONSOLE} uses SysUtils, Classes; type PInfo=^TInfo; TInfo=record Name:String; Tel:String; Address:String; end; var SL:TstringList; List:TList; AppPath:String; { 开辟一个新的PInfo 指针,填入信息并返回指针将在Command_Delete 或FinaInfo 里释放 } function MakeInfo(Name,Tel,Address:String):PInfo; var P:PInfo; begin New(P); P^.Name:=Name; P^.Tel:=Tel; P^.Address:=Address; { 返回的指针将被保存在List 里 } result:=P; end; { 在屏幕上打印所有可用的命令 } procedure PrintMenu; begin writeln(\'======菜单======\'); writeln(\'V---- 查看所有人员的信息\'); writeln(\'A---- 增添新的人员信息\'); writeln(\'D----删除人员\'); writeln(\'E---- 修改人员信息\'); writeln(\'M---- 查看所有可用命令\'); writeln(\'X----退出程序\'); end; { 修改人员信息的程序 } procedure Command_Edit; var I:Integer; Name,Tel,Address:String; P:PInfo; begin write(\' 请输入要修改的人员信息的序号:\'); readln(I); if(I=List.Count)then writeln(\' 输入超出范围。\') else begin { 取得某个人员信息的指针 } P:=List.Items; writeln(\' 开始输入人员信息(若某项信息不需要修改则留空):\'); write(\' 姓名:\'); readln(Name); write(\' 电话号码:\'); readln(Tel); write(\' 地址:\'); readln(Address); { 保存输入的信息 } if Name\'\' then P^.Name:=Name; if Tel\'\' then P^.Tel:=Tel; if Address\'\' then P^.Address:=Address; writeln(\' 修改人员信息执行完毕。\'); end; end; { 增加人员信息的程序 } procedure Command_Add; var Name,Tel,Address:String; begin writeln(\' 开始输入人员信息:\'); write(\' 姓名:\'); readln(Name); write(\' 电话号码:\'); readln(Tel); write(\' 地址:\'); readln(Address); { 使用MakeInfo 生成TInfo 的指针并加入Tlist 中 } List.Add(MakeInfo(Name,Tel,Address)); writeln(\' 增加人员信息执行完毕。\'); end; { 打印所有人员信息的程序 } procedure Command_View; var I:Integer; P:PInfo; begin writeln(\' 人员信息列表:\'); for I:=0 to List.Count-1 do begin P:=List.Items; writeln(IntToStr(I)+\'号===================\'); writeln(\'姓名:\'+P^.Name); writeln(\'电话:\'+P^.Tel); writeln(\'地址:\'+P^.Address); { 满六个就暂停,刚好填满一个屏幕 } if I mod 6=5 then begin writeln(\' 请按回车键继续。\'); readln; end; end; writeln; end; { 删除人员信息的程序 } procedure Command_Delete; var I:Integer; P:PInfo; begin write(\' 请输入要删除的人员信息的序号:\'); readln(I); if(I=List.Count)then writeln(\' 输入超出范围。\') else begin P:=List.Items; List.Delete(I); Dispose(P); writeln(\' 删除执行完毕。\'); writeln; end; end; { 处理用户输入的命令 } function GetCommand:Boolean; { 返回False 表示退出 } var C:Char; begin write(\' 输入命令并回车:\'); readln(C); result:=True; case C of \'V\',\'v\':Command_View; \'A\',\'a\':Command_Add; \'D\',\'d\':Command_Delete; \'M\',\'m\':PrintMenu; \'X\',\'x\':result:=False; \'E\',\'e\':Command_Edit; else writeln(\' 未知命令。\'); end; end; { 从Info.txt 把人员信息加载入Tlist } procedure LoadInfo; var I:Integer; Name,Tel,Address,Index:String; begin SL.LoadFromFile(AppPath+\'Info.txt\'); for I:=0 to SL.Count div 3-1 do begin Index:=IntToStr(I)+\'.\'; { 文件格式:Index.Field=Value 在这里使用Index.X 区别不同序号的人员信息的字段名称 然后通过Values 属性读取信息 } Name:=SL.Values[Index+\'Name\']; Tel:=SL.Values[Index+\'Tel\']; Address:=SL.Values[Index+\'Address\']; List.Add(MakeInfo(Name,Tel,Address)); end; end; { 把TList 里的人员信息保存到Info.txt } procedure SaveInfo; var I:Integer; P:PInfo; begin SL.Clear;{ 清空TstringList } for I:=0 to List.Count-1 do begin P:=List.Items; SL.Add(IntToStr(I)+\'.Name=\'+P^.Name); SL.Add(IntToStr(I)+\'.Tel=\'+P^.Tel); SL.Add(IntToStr(I)+\'.Address=\'+P^.Address); end; SL.SaveToFile(AppPath+\'Info.txt\'); end; { 初始化程序 } procedure InitInfo; begin SL:=TstringList.Create; List:=TList.Create; { 获得本程序所在的路径 } AppPath:=ExtractFilePath(ParamStr(0)); end; { 清空程序使用的内存 } procedure FinaInfo; var I:Integer; begin for I:=0 to List.Count-1 do Dispose(PInfo(List.Items)); List.Free; SL.Free; end; begin { TODO -oUser -cConsole Main : Insert code here } InitInfo; LoadInfo; writeln(\'Information Editor V1.0 by VCZH\'); PrintMenu; while GetCommand do;{ 循环直到返回False } SaveInfo; FinaInfo; writeln(\' 谢谢使用。请按回车键退出程序。\'); readln; end. 在这个程序测试完毕(由于时间关系,并没有执行严格的测试,所以一些小细节可能会没有注意到,不过不会影响程序功能的正确性)时,Info.txt 的内容如下 在循环开始时,I 的值被指向0,然后Index的值就变成"0."。在执行Name:=SL.Values[Index+\'Name\']时,就等于执行Name:=SL.Values[\'0.Name\']于是, "0.Name"这个字段的内容就被读取,然后Name 就变成了"vczh "。接着进入下一个循环。程序就是通过在字段前加"Index."这样的方法区别不同人员的信息。在读取完一个人的信息后,程序执行List.Add(MakeInfo(Name,Tel,Address)),信息便存放在List里了。程序通过操作List 增加、删除或修改人员信息。在用户退出程序时,程序将List里的所有信息保存在"Info.txt"里。过程是这样的:程序先清空SL 里的内容,然后按照Info.txt原来的文件格式填写信息。因为Info.txt里的人员数目是会改变的,因此便不能使用TstringList.Values属性修改,而必须在清空TstringList后手动构建字段并填写信息。 0.Name=vczh 0.Tel=1234567 0.Address=Jupiter 1.Name= 陈梓瀚 1.Tel=8888888 1.Address=Venus 2.Name=chenzihan 2.Tel=9999999 2.Address=Mars 3.Name=Victor 3.Tel=0000000 3.Address=Saturn 经过这样的介绍,大家对Delphi 包含的与List,有相似性质的类都有了一定的认识。Delphi VCL 开发组为我们准备了这么多既实用又有效率的功能。而List在庞大的Delphi VCL 里只是沧海一粟。所以我们今后应当继续研究Delphi VCL,充分利用 VCL,为自己的项目增添光辉。__

猜你喜欢

转载自kvopj93o.iteye.com/blog/1362681