本节内容是《一步一步学习使用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
pascal
function TMyCustomAppearance.DefaultHeight: Integer;
begin
Result := MY_DEFAULT_HEIGHT; // 返回自定义的默认高度
end;
功能:定义列表项的默认高度,当未明确设置ItemHeight时使用。
2. GetGroupClass: TGroupClass
pascal
function TMyCustomAppearance.GetGroupClass: TPresetItemObjects.TGroupClass;
begin
Result := TMyCustomAppearance; // 返回当前类类型
end;
功能:返回当前类的类型,用于外观对象分组管理。
3. UpdateSizes(const FinalSize: TSizeF)
pascal
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)
pascal
constructor TMyCustomAppearance.Create(const Owner: TControl);
begin
inherited;
// 创建并初始化自定义外观对象
FMyObject := TTextObjectAppearance.Create;
FMyObject.Name := 'MyObject';
// ...其他初始化
AddObject(FMyObject, True);
end;
功能:创建和初始化所有自定义的外观对象。
5. 析构函数 Destroy
pascal
destructor TMyCustomAppearance.Destroy;
begin
FMyObject.Free; // 释放自定义对象
inherited;
end;
功能:清理创建的自定义对象。
2.2 主要的功能实现
除了重载方法之外,主要的自定义实现要点如下:
1. 自定义对象管理
声明私有字段保存自定义对象引用
pascal
private
FMyObject: TTextObjectAppearance;
FMyImage: TImageObjectAppearance;
在published部分公开自定义的属性,以便设计时可以编辑。
pascal
published
property MyObject: TTextObjectAppearance read FMyObject write SetMyObject;
property MyImage: TImageObjectAppearance read FMyImage write SetMyImage;
2. 对象初始化模板
推荐使用初始化模板避免重复代码:
pascal
procedure InitTextObject(AObject: TTextObjectAppearance);
begin
AObject.OnChange := ItemPropertyChange;
AObject.DefaultValues.Align := TListItemAlign.Leading;
// ...其他默认设置
AObject.RestoreDefaults;
AObject.Owner := Self;
end;
3. LiveBindings支持
为每个自定义对象设置DataMembers:
pascal
FMyObject.DataMembers := TObjectAppearance.TDataMembers.Create(
TObjectAppearance.TDataMember.Create(
'MyObject',
Format('Data["%s"]', ['myobject'])
)
);
TDelphiCookbookAppearance自定义类的完整代码如下所示:
Pascal
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的类图
下面是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位小数位的度数值。
现在按钮单击事件处理代码如下:
Pascal
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上添加的绑定效果:
最后运行一下,看看效果是否如预期:
效果非常好,并且只要安装好了这个包,以后在很多地方者可以重用这个自定义的样式,实在是太方便。
总结
这一节的内容并不复杂,但是非常实用。通过本课的学习,可以了解到:
- 如何设计自定义列。
- 实现自定义列的一般方法。
- 如何在应用程序中使用自定义列。
通过本课的学习,相信你也可以设计出更加美观的自定义列表项。