本节内容是《一步一步学习使用LiveBindings(14)》中天气预报小程序的进一步优化。虽然编写代码创建TListView的列表项可以提供较大的灵活民生,但是造成代码复杂性增加,而且可重用性较弱。
注意:更理想的自定义列表项的的方法是为 TListView 组件编写自定义样式;将组件放入一个包中,安装到 IDE 中,然后从对象检查器窗口使用它。这样就可以在多个项目中重复的使用。
这一节将介绍如下的几个内容:
- 创建一个Delphi包,在Delphi包中创建自定义列表项。
- 使用自定义列表项进行数据绑定
- 将天气预报数据保存到本地内存表,通过LiveBindings进行显示。
1. 自定义列表项的整体设计思路
本节将创建一个自定义的FireMonkey列表项(TListView)外观类,主要用于显示天气预报信息,包含最低温度和最高温度的特殊显示效果。它的核心设计思路是:
- 继承体系:继承自TPresetItemObjects,这是FMX框架中预定义列表项外观的基类
- 定制化显示:在标准列表项基础上增加了两个温度显示字段(最低温度和最高温度)
- 响应式布局:根据可用空间自动调整布局(当空间不足时隐藏最低温度)
- 数据绑定支持:为LiveBindings提供数据成员支持。
整体效果如下所示:
2. 新建一个Delphi包,添加自定义列表项单元
1. 单击主菜单中的 File > New > Package ,创建一个新的包,另存为比如MyListViewItemAppearance这样的包名,然后在包中添加一个新的Unit,示例命名为DelphiCookbookListViewAppearanceU.pas。
对于程序员来说,从头开始写一个类,需要添加诸多的uses引用,一步步写好继承基类有点繁琐,无论是新手还是老手。有个模板可以参照和拷贝都是很有必要的。
注意:Delphi本身提供了几个案例,其中的ListViewMultiDetailAppearance案例非常接近于本案例的需求,本节将介绍的案例见下面的路径。- C:\Users\Public\Documents\Embarcadero\Studio\23.0\Samples\Object Pascal\Multi-Device Samples\User Interface\ListView\ListViewMultiDetailAppearance
复制代码 建议打开这个案例,拷贝其中的代码,通过修改实现自己的需求。
在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; // 初始化文本对象的匿名方法 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.Create(TDelphiCookbookItemAppearance)); end.
复制代码 类创建完成后,还需要在initialization和finalization添加注册与取消注册代码,以便可以在Delphi对象检查器面板上发现。
将上面的代码与Delphi自带的示例MultiDetailAppearanceU.pas进行比较,可见代码上的发现诸多相似之处。
下面是TDelphiCookbookItemAppearance的类图
下面是MultiDetailAppearanceU.pas中的类:
可以看到TMultiDetailItemAppearance和TDelphiCookbookItemAppearance重载了相同的方法,TMultiDetailItemAppearance包含了更多的自定义的属性。
在自定义列表项代码骨架搭建后,你就可以右击Package项目,选择“Install”菜单项,将项目安装到Delphi中。然后新建一个FMX测试项目,在测试项目的加持下实现调试。
3. 在FMX程序中使用自定义列表项
在安装好后,可以为TListView指定自定义的列表项,如下图所示:
可以切换到设计模式,查看自定义列的设计效果。
现在,自定义的列表项还支持数据绑定。在这个例子中,添加了一个TFDMemTable控件,并且在Fields Editor中添加了4个永久性字段,如下所示:
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上添加的绑定效果:
最后运行一下,看看效果是否如预期:
效果非常好,并且只要安装好了这个包,以后在很多地方者可以重用这个自定义的样式,实在是太方便。
总结
这一节的内容并不复杂,但是非常实用。通过本课的学习,可以了解到:
- 如何设计自定义列。
- 实现自定义列的一般方法。
- 如何在应用程序中使用自定义列。
通过本课的学习,相信你也可以设计出更加美观的自定义列表项。
来源:程序园用户自行投稿发布,如果侵权,请联系站长删除
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作! |