一步一步学习使用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. 如何在应用程序中使用自定义列。

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

相关推荐
lincats13 天前
一步一步学习使用FireMonkey动画(6) 用实例理解动画的运行状态
ide·delphi·livebindings·delphi 12.3·firemonkey
lincats13 天前
一步一步学习使用FireMonkey动画(5) 动画图解11种动画插值类型
ide·移动开发·delphi 12.3·firedac·firemonkey
lincats13 天前
一步一步学习使用FireMonkey动画(4) 使用Delphi的基本动画组件类,路径和位图列表动画 弹跳小球和奔跑的小人示例
livebindings·delphi 12.3·firemonkey
lincats14 天前
一步一步学习使用FireMonkey动画(3) 使用Delphi的基本动画组件类
ide·delphi·delphi 12.3·firemonkey
lincats14 天前
一步一步学习使用FireMonkey动画(2) 使用TAnimator类创建动画
ide·delphi 12.3·firedac·firemonkey
lincats14 天前
一步一步学习使用FireMonkey动画(1) 使用动画组件为窗体添加动态效果
android·ide·delphi·livebindings·delphi 12.3·firemonkey
lincats18 天前
一步一步学习使用LiveBindings(16)使用代码创建LiveBindings绑定
delphi·livebindings·delphi 12.3·firedac·firemonkey
lincats21 天前
一步一步学习使用LiveBindings(14)TListView进阶使用(2),打造天气预报程序
delphi·livebindings·delphi 12.3·firedac·firemonkey·tlistview
lincats23 天前
一步一步学习使用LiveBindings(13) TListView的进阶使用(1)
delphi·livebindings·delphi 12.3·firemonkey·tlistview