一步一步学习使用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提供数据成员支持。

整体效果如下所示:

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上添加的绑定效果:

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

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

总结

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

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

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

相关推荐
lincats16 小时前
一步一步学习使用LiveBindings(14)TListView进阶使用(2),打造天气预报程序
delphi·livebindings·delphi 12.3·firedac·firemonkey·tlistview
lincats2 天前
一步一步学习使用LiveBindings(13) TListView的进阶使用(1)
delphi·livebindings·delphi 12.3·firemonkey·tlistview
lincats3 天前
一步一步学习使用LiveBindings(12) LiveBindings与具有动态呈现的TListView
delphi·livebindings·delphi 12.3·firemonkey
lincats5 天前
一步一步学习使用LiveBindings(11) 绑定到自定义外观的ListBox
list·delphi·delphi 12.3·firedac·firemonkey·tlistview
lincats6 天前
# 一步一步学习使用LiveBindings(10) LiveBindings绑定到漂亮的TCombobox
ide·delphi·livebindings·delphi 12.3
lincats7 天前
一步一步学习使用LiveBindings(9) LiveBindings图像绑定与自定义绑定方法(2)
delphi·livebindings·delphi 12.3·firedac·firemonkey
lincats9 天前
一步一步学习使用LiveBindings(8) 使用向导创建用户界面,绑定格式化入门
delphi·livebindings·delphi 12.3·firedac·firemonkey
lincats13 天前
一步一步学习使用LiveBindings(7) 实现对JSON数据的绑定
android·delphi·livebindings·delphi 12.3·firedac
lincats14 天前
一步一步学习使用LiveBindings(6) 实现Master-Detail主从关系的绑定
delphi·livebindings·delphi 12.3·firedac·firemonkey