一步一步学习使用LiveBindings(15)TListView进阶使用(3),创建自定义的列表项打造天气预报程序

本节内容是《一步一步学习使用LiveBindings(14)》中天气预报小程序的进一步优化。虽然编写代码创建TListView的列表项可以提供较大的灵活民生,但是造成代码复杂性增加,而且可重用性较弱。

注意:更理想的自定义列表项的的方法是为 TListView 组件编写自定义样式;将组件放入一个包中,安装到 IDE 中,然后从对象检查器窗口使用它。这样就可以在多个项目中重复的使用。

这一节将介绍如下的几个内容:

  1. 创建一个Delphi包,在Delphi包中创建自定义列表项。
  2. 使用自定义列表项进行数据绑定
  3. 将天气预报数据保存到本地内存表,通过LiveBindings进行显示。

1. 自定义列表项的整体设计思路

本节将创建一个自定义的FireMonkey列表项(TListView)外观类,主要用于显示天气预报信息,包含最低温度和最高温度的特殊显示效果。它的核心设计思路是:

  1. 继承体系:继承自TPresetItemObjects,这是FMX框架中预定义列表项外观的基类

  2. 定制化显示:在标准列表项基础上增加了两个温度显示字段(最低温度和最高温度)

  3. 响应式布局:根据可用空间自动调整布局(当空间不足时隐藏最低温度)

  4. 数据绑定支持:为LiveBindings提供数据成员支持。

整体效果如下所示:

一步一步学习使用LiveBindings(15)TListView进阶使用(3),创建自定义的列表项打造天气预报程序

2. 新建一个Delphi包,添加自定义列表项单元

1. 单击主菜单中的 File > New > Package ,创建一个新的包,另存为比如MyListViewItemAppearance这样的包名,然后在包中添加一个新的Unit,示例命名为DelphiCookbookListViewAppearanceU.pas。

对于程序员来说,从头开始写一个类,需要添加诸多的uses引用,一步步写好继承基类有点繁琐,无论是新手还是老手。有个模板可以参照和拷贝都是很有必要的。

注意:Delphi本身提供了几个案例,其中的ListViewMultiDetailAppearance案例非常接近于本案例的需求,本节将介绍的案例见下面的路径。

C:UsersPublicDocumentsEmbarcaderoStudio23.0SamplesObject PascalMulti-Device SamplesUser InterfaceListViewListViewMultiDetailAppearance 

建议打开这个案例,拷贝其中的代码,通过修改实现自己的需求。

在Delphi的FireMonkey框架中,从TPresetItemObjects继承创建自定义列表项外观时,需要重点实现以下几个核心方法和功能:

2.1 必须重载的关键方法

1. DefaultHeight: Integer

function TMyCustomAppearance.DefaultHeight: Integer; begin   Result := MY_DEFAULT_HEIGHT; // 返回自定义的默认高度 end; 

功能:定义列表项的默认高度,当未明确设置ItemHeight时使用。

2. GetGroupClass: TGroupClass

function TMyCustomAppearance.GetGroupClass: TPresetItemObjects.TGroupClass; begin   Result := TMyCustomAppearance; // 返回当前类类型 end; 

功能:返回当前类的类型,用于外观对象分组管理。

3. UpdateSizes(const FinalSize: TSizeF)

procedure TMyCustomAppearance.UpdateSizes(const FinalSize: TSizeF); begin   BeginUpdate;   try     inherited;     // 自定义布局逻辑     Text.InternalWidth := FinalSize.Width * 0.6;     Detail.PlaceOffset.X := FinalSize.Width * 0.6;   finally     EndUpdate;   end; end; 

功能:根据最终尺寸调整子对象的布局和大小,实现响应式布局。

4. 构造函数 Create(const Owner: TControl)

constructor TMyCustomAppearance.Create(const Owner: TControl); begin   inherited;   // 创建并初始化自定义外观对象   FMyObject := TTextObjectAppearance.Create;   FMyObject.Name := 'MyObject';   // ...其他初始化   AddObject(FMyObject, True); end; 

功能:创建和初始化所有自定义的外观对象。

5. 析构函数 Destroy

destructor TMyCustomAppearance.Destroy; begin   FMyObject.Free; // 释放自定义对象   inherited; end; 

功能:清理创建的自定义对象。

2.2 主要的功能实现

除了重载方法之外,主要的自定义实现要点如下:

1. 自定义对象管理

声明私有字段保存自定义对象引用

private   FMyObject: TTextObjectAppearance;   FMyImage: TImageObjectAppearance; 

在published部分公开自定义的属性,以便设计时可以编辑。

published   property MyObject: TTextObjectAppearance read FMyObject write SetMyObject;   property MyImage: TImageObjectAppearance read FMyImage write SetMyImage; 

2. 对象初始化模板

推荐使用初始化模板避免重复代码:

procedure InitTextObject(AObject: TTextObjectAppearance); begin   AObject.OnChange := ItemPropertyChange;   AObject.DefaultValues.Align := TListItemAlign.Leading;   // ...其他默认设置   AObject.RestoreDefaults;   AObject.Owner := Self; end; 

3. LiveBindings支持

为每个自定义对象设置DataMembers:

FMyObject.DataMembers := TObjectAppearance.TDataMembers.Create(   TObjectAppearance.TDataMember.Create(     'MyObject',      Format('Data["%s"]', ['myobject'])   ) ); 

TDelphiCookbookAppearance自定义类的完整代码如下所示:

unit DelphiCookbookListViewAppearanceU;  interface  uses System.Types, FMX.ListView, FMX.ListView.Types, FMX.ListView.Appearances,   System.Classes, System.SysUtils,   FMX.Types, FMX.Controls, System.UITypes, FMX.MobilePreview;  type   // 定义DelphiCookbook的外观名称常量类   TDelphiCookbookAppearanceNames = class   public const     ListItem = 'DelphiCookbookWeatherAppearance';  // 列表项外观名称     MinTemp = 'mintemp';  // 最低温度字段名称     MaxTemp = 'maxtemp';  // 最高温度字段名称   end;  implementation  type   // 自定义列表项外观类,继承自TPresetItemObjects   TDelphiCookbookItemAppearance = class(TPresetItemObjects)   public const     DEFAULT_HEIGHT = 40;  // 默认列表项高度   private     FMinTemp: TTextObjectAppearance;  // 最低温度文本对象     FMaxTemp: TTextObjectAppearance;  // 最高温度文本对象     procedure SetMinTemp(const Value: TTextObjectAppearance);  // 设置最低温度属性     procedure SetMaxTemp(const Value: TTextObjectAppearance);  // 设置最高温度属性   protected     function DefaultHeight: Integer; override;  // 获取默认高度     procedure UpdateSizes(const FinalSize: TSizeF); override;  // 更新尺寸     function GetGroupClass: TPresetItemObjects.TGroupClass; override;  // 获取组类   public     constructor Create(const Owner: TControl); override;  // 构造函数     destructor Destroy; override;  // 析构函数   published     property Accessory;  // 继承的附件属性     property Text;  // 继承的文本属性     //自定义的属性     property MinTemp: TTextObjectAppearance read FMinTemp write SetMinTemp;  // 最低温度属性     property MaxTemp: TTextObjectAppearance read FMaxTemp write SetMaxTemp;  // 最高温度属性   end;  const   MIN_TEMP_MEMBER = 'MinTemp';  // 最低温度成员名称   MAX_TEMP_MEMBER = 'MaxTemp';  // 最高温度成员名称  // 构造函数实现 constructor TDelphiCookbookItemAppearance.Create(const Owner: TControl); var   LInitTextObject: TProc<TTextObjectAppearance>;  // 初始化文本对象的匿名方法 begin   inherited;   // 定义初始化文本对象的匿名方法   LInitTextObject := procedure(pTextObject: TTextObjectAppearance)     begin       pTextObject.OnChange := ItemPropertyChange;  // 设置变更事件为基类的ItemPropertyChange事件       pTextObject.DefaultValues.Align := TListItemAlign.Leading;  // 默认左对齐       pTextObject.DefaultValues.VertAlign := TListItemAlign.Center;  // 默认垂直居中       pTextObject.DefaultValues.TextVertAlign := TTextAlign.Center;  // 文本垂直居中       pTextObject.DefaultValues.TextAlign := TTextAlign.Trailing;  // 文本右对齐       pTextObject.DefaultValues.PlaceOffset.Y := 0;  // Y偏移量为0       pTextObject.DefaultValues.PlaceOffset.X := 0;  // X偏移量为0       pTextObject.DefaultValues.Width := 80;  // 默认宽度80       pTextObject.DefaultValues.Visible := True;  // 默认可见       pTextObject.RestoreDefaults;  // 恢复默认值       pTextObject.Owner := Self;  // 设置所有者     end;    // 创建并初始化最低温度文本对象   FMinTemp := TTextObjectAppearance.Create;   FMinTemp.Name := TDelphiCookbookAppearanceNames.MinTemp;  // 设置名称   FMinTemp.DefaultValues.TextColor := TAlphaColorRec.Blue;  // 设置蓝色文本   LInitTextObject(FMinTemp);  // 调用初始化方法    // 创建并初始化最高温度文本对象   FMaxTemp := TTextObjectAppearance.Create;   FMaxTemp.Name := TDelphiCookbookAppearanceNames.MaxTemp;  // 设置名称   FMaxTemp.DefaultValues.TextColor := TAlphaColorRec.Red;  // 设置红色文本   LInitTextObject(FMaxTemp);  // 调用初始化方法    // 定义最低温度的LiveBindings数据成员   FMinTemp.DataMembers := TObjectAppearance.TDataMembers.Create     (TObjectAppearance.TDataMember.Create(MIN_TEMP_MEMBER,     // 用于LiveBindings显示的表达式     Format('Data["%s"]', [TDelphiCookbookAppearanceNames.MinTemp])));     // 从TListViewItem访问值的表达式    // 定义最高温度的LiveBindings数据成员   FMaxTemp.DataMembers := TObjectAppearance.TDataMembers.Create     (TObjectAppearance.TDataMember.Create(MAX_TEMP_MEMBER,     // 用于LiveBindings显示的表达式     Format('Data["%s"]', [TDelphiCookbookAppearanceNames.MaxTemp])));     // 从TListViewItem访问值的表达式    // 添加外观对象到列表项   AddObject(Text, True);  // 添加文本对象   AddObject(MinTemp, True);  // 添加最低温度对象   AddObject(MaxTemp, True);  // 添加最高温度对象 end;  // 获取默认高度 function TDelphiCookbookItemAppearance.DefaultHeight: Integer; begin   Result := DEFAULT_HEIGHT;  // 返回常量定义的默认高度 end;  // 析构函数实现 destructor TDelphiCookbookItemAppearance.Destroy; begin   FMinTemp.Free;  // 释放最低温度对象   FMaxTemp.Free;  // 释放最高温度对象   inherited;  // 调用父类析构函数 end;  // 设置最低温度属性 procedure TDelphiCookbookItemAppearance.SetMinTemp   (const Value: TTextObjectAppearance); begin   FMinTemp.Assign(Value);  // 赋值最低温度对象 end;  // 设置最高温度属性 procedure TDelphiCookbookItemAppearance.SetMaxTemp   (const Value: TTextObjectAppearance); begin   FMaxTemp.Assign(Value);  // 赋值最高温度对象 end;  // 获取组类 function TDelphiCookbookItemAppearance.GetGroupClass   : TPresetItemObjects.TGroupClass; begin   Result := TDelphiCookbookItemAppearance;  // 返回当前类类型 end;  // 更新尺寸方法 procedure TDelphiCookbookItemAppearance.UpdateSizes(const FinalSize: TSizeF); var   LColWidth: Extended;  // 列宽度   LFullWidth: Boolean;  // 是否全宽标志 begin   BeginUpdate;  // 开始更新   try     inherited;  // 调用父类方法     LColWidth := FinalSize.Width / 12;  // 计算每列宽度(将总宽度分为12列)     LFullWidth := LColWidth * 4 >= MinTemp.Width;  // 判断是否有足够空间显示最低温度     if LFullWidth then  // 如果有足够空间     begin       MinTemp.Visible := True;  // 显示最低温度       Text.InternalWidth := LColWidth * 6;  // 设置文本宽度为6列       MinTemp.PlaceOffset.X := LColWidth * 6;  // 设置最低温度X偏移       MinTemp.InternalWidth := LColWidth * 2;  // 设置最低温度宽度为2列       MaxTemp.PlaceOffset.X := LColWidth * 9;  // 设置最高温度X偏移       MaxTemp.InternalWidth := LColWidth * 2;  // 设置最高温度宽度为2列     end     else  // 如果空间不足     begin       MinTemp.Visible := False;  // 隐藏最低温度       Text.InternalWidth := LColWidth * 8;  // 设置文本宽度为8列       MaxTemp.PlaceOffset.X := LColWidth * 8;  // 设置最高温度X偏移       MaxTemp.InternalWidth := LColWidth * 4;  // 设置最高温度宽度为4列     end;   finally     EndUpdate;  // 结束更新   end; end;  const   sThisUnit = 'DelphiCookbookListViewAppearanceU';  // 当前单元名称常量  initialization  // 注册自定义外观 TAppearancesRegistry.RegisterAppearance(TDelphiCookbookItemAppearance,   TDelphiCookbookAppearanceNames.ListItem, [TRegisterAppearanceOption.Item],   sThisUnit);  finalization  // 注销自定义外观 TAppearancesRegistry.UnregisterAppearances   (TArray<TItemAppearanceObjectsClass>.Create(TDelphiCookbookItemAppearance));  end. 

类创建完成后,还需要在initialization和finalization添加注册与取消注册代码,以便可以在Delphi对象检查器面板上发现。

将上面的代码与Delphi自带的示例MultiDetailAppearanceU.pas进行比较,可见代码上的发现诸多相似之处。

下面是TDelphiCookbookItemAppearance的类图

一步一步学习使用LiveBindings(15)TListView进阶使用(3),创建自定义的列表项打造天气预报程序

下面是MultiDetailAppearanceU.pas中的类:

一步一步学习使用LiveBindings(15)TListView进阶使用(3),创建自定义的列表项打造天气预报程序

可以看到TMultiDetailItemAppearance和TDelphiCookbookItemAppearance重载了相同的方法,TMultiDetailItemAppearance包含了更多的自定义的属性。

在自定义列表项代码骨架搭建后,你就可以右击Package项目,选择“Install”菜单项,将项目安装到Delphi中。然后新建一个FMX测试项目,在测试项目的加持下实现调试。

一步一步学习使用LiveBindings(15)TListView进阶使用(3),创建自定义的列表项打造天气预报程序

3. 在FMX程序中使用自定义列表项

在安装好后,可以为TListView指定自定义的列表项,如下图所示:

一步一步学习使用LiveBindings(15)TListView进阶使用(3),创建自定义的列表项打造天气预报程序

可以切换到设计模式,查看自定义列的设计效果。

一步一步学习使用LiveBindings(15)TListView进阶使用(3),创建自定义的列表项打造天气预报程序

现在,自定义的列表项还支持数据绑定。在这个例子中,添加了一个TFDMemTable控件,并且在Fields Editor中添加了4个永久性字段,如下所示:

一步一步学习使用LiveBindings(15)TListView进阶使用(3),创建自定义的列表项打造天气预报程序

day和description是string类型的字段,mintemp和maxtemp是float类型的字段,并指定DisplayFormat为#0.00°,显示2位小数位的度数值。

现在按钮单击事件处理代码如下:

procedure TMainForm.btnGetForecastsClick(Sender: TObject); begin   // 清空ListView1中的项目(当前被注释掉)   // ListView1.Items.Clear;      // 设置REST请求参数:将城市和国家编辑框内容用逗号连接作为country参数值   RESTRequest1.Params.ParameterByName('country').Value :=      String.Join(',', [EditCity.Text, EditCountry.Text]);      // 设置REST请求参数:语言参数   RESTRequest1.Params.ParameterByName('lang').Value := Lang;      // 显示并启用加载指示器   AniIndicator1.Visible := True;   AniIndicator1.Enabled := True;      // 禁用获取预报按钮,防止重复请求   btnGetForecasts.Enabled := False;      // 异步执行REST请求   RESTRequest1.ExecuteAsync(     procedure     var       LForecastDateTime: TDateTime;       // 预报日期时间       LJValue: TJSONValue;                // JSON值对象       LJObj, LMainForecast, LForecastItem, LJObjCity: TJSONObject; // 各层JSON对象       LJArrWeather, LJArrForecasts: TJSONArray; // JSON数组       LTempMin, LTempMax: Double;         // 最低和最高温度       LDay, LWeatherDescription, LAppRespCode: string; // 日期、天气描述、响应码     begin       // 将响应内容转换为JSON对象       LJObj := RESTRequest1.Response.JSONValue as TJSONObject;        // 检查错误响应       // 获取响应状态码       LAppRespCode := LJObj.GetValue('cod').Value;              // 处理404错误(城市未找到)       if LAppRespCode.Equals('404') then       begin         lblInfo.Text := '没有找到城市信息';         Exit; // 退出处理过程       end;              // 处理非200的成功响应       if not LAppRespCode.Equals('200') then       begin         lblInfo.Text := 'Error ' + LAppRespCode;         Exit; // 退出处理过程       end;        // 准备内存表接收数据       FDMemTable1.EmptyView;     // 清空内存表视图       FDMemTable1.DisableControls; // 禁用控件刷新,提高批量操作性能       try         // 解析预报数据数组         LJArrForecasts := LJObj.GetValue('list') as TJSONArray;                  // 遍历每个预报项         for LJValue in LJArrForecasts do         begin           // 将当前JSON值转换为对象           LForecastItem := LJValue as TJSONObject;                      // 解析预报时间戳(Unix时间戳转换为Delphi的TDateTime)           LForecastDateTime := UnixToDateTime((LForecastItem.GetValue('dt')             as TJSONNumber).AsInt64);                        // 获取主要天气信息对象           LMainForecast := LForecastItem.GetValue('main') as TJSONObject;                      // 解析最低温度           LTempMin := (LMainForecast.GetValue('temp_min')             as TJSONNumber).AsDouble;                        // 解析最高温度           LTempMax := (LMainForecast.GetValue('temp_max')             as TJSONNumber).AsDouble;                        // 获取天气描述数组           LJArrWeather := LForecastItem.GetValue('weather') as TJSONArray;                      // 获取第一个天气项的描述           LWeatherDescription := TJSONObject(LJArrWeather.Items[0])             .GetValue('description').Value;                        // 格式化日期显示(星期几 日 月 年)           LDay := FormatDateTime('ddd d mmm yyyy', DateOf(LForecastDateTime));            // 将数据添加到内存表           FDMemTable1.Append; // 添加新记录           FDMemTable1day.AsString := LDay; // 设置日期字段           FDMemTable1description.Value := FormatDateTime('HH',             LForecastDateTime) + ' ' + LWeatherDescription; // 时间+天气描述           FDMemTable1mintemp.Value := LTempMin; // 设置最低温度           FDMemTable1maxtemp.Value := LTempMax; // 设置最高温度           FDMemTable1.Post; // 提交记录         end; // 结束遍历       finally         // 确保以下操作无论是否发生异常都会执行         FDMemTable1.EnableControls; // 重新启用控件刷新         BindSourceDB1.ResetNeeded;   // 通知绑定组件数据已更新         FDMemTable1.First;          // 定位到第一条记录       end;              // 解析城市信息       LJObjCity := LJObj.GetValue('city') as TJSONObject;              // 更新界面显示城市和国家信息       lblInfo.Text := LJObjCity.GetValue('name').Value + ', ' +         LJObjCity.GetValue('country').Value;              // 隐藏并禁用加载指示器       AniIndicator1.Visible := False;       AniIndicator1.Enabled := False;              // 重新启用获取预报按钮       btnGetForecasts.Enabled := True;     end); end; 

在单击事件处理中,解析JSON数据后,现在将数据直接添加到了内存表中,然后调用BindSourceDB1.ResetNeeded通知绑定数据已经更新,刷新控件的显示。

现在单击事件处理代码并不负责ListView的显示工作,而是交给LiveBindings实现了这一切,下图是LiveBindings Designer上添加的绑定效果:

一步一步学习使用LiveBindings(15)TListView进阶使用(3),创建自定义的列表项打造天气预报程序

最后运行一下,看看效果是否如预期:

一步一步学习使用LiveBindings(15)TListView进阶使用(3),创建自定义的列表项打造天气预报程序

效果非常好,并且只要安装好了这个包,以后在很多地方者可以重用这个自定义的样式,实在是太方便。

总结

这一节的内容并不复杂,但是非常实用。通过本课的学习,可以了解到:

  1. 如何设计自定义列。
  2. 实现自定义列的一般方法。
  3. 如何在应用程序中使用自定义列。

通过本课的学习,相信你也可以设计出更加美观的自定义列表项。

发表评论

评论已关闭。

相关文章