Stories

Detail Return Return

一步一步學習使用LiveBindings(14)TListView進階使用(2),打造天氣預報程序 - Stories Detail

在《一步一步學習使用LiveBindings(12)》課中,非常詳細的介紹瞭如何在設計時手工的編輯DynamicAppearance類型的項,大大方便了構建自定義的列表項。但是很多情況下,仍然要面對編程創建列表項的情形,特別是當要實現自定義的列表項時,將不得不面對編程創建列表項的挑戰。

注意:更理想的自定義列表項的的方法是為 TListView 組件編寫自定義樣式;將組件放入一個包中,安裝到 IDE 中,然後從對象檢查器窗口使用它。這樣就可以在多個項目中重複的使用。

這一課將主要介紹如下內容:

  • 使用TRestClient從遠端服務器獲取服務。
  • 解析JSON,根據JOSN的內容,創建TListViewItem。
  • 根據JSON的內容,生成不同的列表項呈現。

這一課將是創建自定義列表項的基礎,掌握了這一課的內容,基本對TListView,也就沒有大的問題了。

1. 關於天氣預報網站OpenWeatherMap:

OpenWeatherMap 是一個提供全球天氣數據的知名服務平台,主要面向開發者、企業和個人用户。以下是它的核心特點:

  1. 實時與預報天氣數據
    提供實時天氣、分鐘級降水預報、短期(5天)及長期(16天)預報,甚至歷史天氣數據。
    覆蓋全球任意座標(包括偏遠地區),數據來源自氣象站、衞星和雷達。
  2. 多樣化的數據接口
    通過 REST API 和 地圖圖層API 提供數據,支持JSON/XML格式。
    免費層有限制(如60次/分鐘調用),付費層可獲取更高精度(如小時級降水)和更多功能。
  3. 豐富的天氣指標
    温度、濕度、風速、氣壓、降水、紫外線指數等。
    特殊數據如空氣質量(PM2.5)、花粉濃度、自然災害警報等需訂閲高級服務。
  4. 應用場景廣泛
    開發者常用其API集成到移動應用、網站或IoT設備(如智能家居)。
    企業用户可能用於物流、農業或旅遊行業的天氣分析。
  5. 數據可視化工具
    提供交互式地圖,可疊加雲層、降水等實時天氣圖層。
  6. 費用與權限
    免費方案 適合小規模測試(如1,000次/天API調用)。
    商業用途需選擇付費計劃(如企業級數據或高頻率訪問)。
  7. 數據覆蓋與更新
    覆蓋40多萬個城市,部分數據更新頻率可達每分鐘。
    注意事項
    免費數據可能不如專業氣象機構精確,付費服務(如History API或Agro API)更適合高要求場景。
    使用前需註冊獲取API Key,並遵守其條款(如註明數據來源)。
    如果需要替代方案,可考慮 WeatherAPI、AccuWeather 或 ClimaCell(現為Tomorrow.io)。具體選擇取決於精度需求和預算。

在該網站上查詢北京的天氣如下:

img

網站列出了8天的天氣,本課的小程序的效果如下:

img

本課示例代碼來自《Delphi Cookbook》

你需要在該網站註冊一個賬號,然後申請一個免費的API Key,不過使用《Delphi Cookbook》作者的api key一樣可以使用,但是這個示例調用的是舊版本的API,新版本的API已經進化到3.0,可以使用本課學到的知識打造一個真正有價值的世界天氣預報應用程序。

img

2. 構建多設備應用程序。

1. 單擊主菜單中的 File > New > Multi-Device Application - Delphi > Header/Footer ,創建一個新的多設備應用程序。

建議立即單擊工具欄上的Save All按鈕,將單元文件保存為MainFormU.pas,將項目保存為WeatherForecastEx.dproj。

項目結構應該像這樣:

img

2.1 設置用户界面

用户界面大概按如下的步驟:

  1. Header/Footer會自動添加Header和Footer兩個TToolBar,在Header上面添加了一個名為HeaderLabel的TLabel控件,指定其Text屬性為“天氣預報小程序”。

  2. 在Header上添加一個TAniIndictor控件,設置其Align屬性為Right,這個控件將顯示一個動態的指示器,提供用户運行中的反饋。

  3. 在Header下面添加一個Align為Top的TPanel控件,在上面放2個TEdit,分別命名為EditCity和EditCountry,在最右側放一個TButton控件,指定name屬性為btnGetForecasts,將EditCity的Align設置為Client,EditCountry和btnGetForecasts的Align指定為Right。

  4. 在Footer上放一個TLable控件,指定其name屬性為lblInfo,用來顯示一些系統消息。

  5. 接下來放一個Align為Client的TListView控件,指定其ItemAppearance.ItemAppearance屬性為Custom,將用來編程控制Item。

  6. 最後,手一個TRestClient和TRestRequest控件到主窗體,指定TRestRequest.Client屬性為TRestClient。

現在用户界面相關的控件已經準備好了。

img

2.2 測試天氣預報服務

接下來,打開Delphi主菜單的 Tools >REST Debugger,
在彈出的窗口的第一頁,Method指定為Get,在URL文本框上指定URL為:
http://api.openweathermap.org/data/2.5/forecast

Content-Type使用默認的Application/json

img

接下來切換到Parameters頁,添加如下的鍵值對:

img

單擊右上角的“Send Request”按鈕,如果URL構建成功,則Response會返回200-OK狀態,Response的Header區可以看到請求的頭信息,Body區則可以看到具體的JSON數據。

img

通過分析JSON,可以得知應用程序將要提供哪些數據給用户,從而構建真正的用户界面。

img
整個JSON包含了一個list數組,每個數組裏面的元素內容如下:

  • dt:Unix時間戳值。
  • main.temp_min:指定時間戳範圍內的最低温度。
  • main.temp_max: 指定時間戳範圍內的最高温度。
  • weather.main: 天氣情況的英語表達。
  • weather.description:天氣情況的本地語言表達。

3. 獲取天氣JSON數據,並進行JSON解析,生成用户界面

1. 在主窗體加載時,將會初始化RestClient和RESTRequest兩個控件,FormCreate事件如下:

// 主窗體創建時觸發的事件處理過程
procedure TMainForm.FormCreate(Sender: TObject);
var
  LocaleService: IFMXLocaleService;  // 聲明一個FMX本地化服務接口變量
begin
  // 檢查當前平台是否支持IFMXLocaleService接口
  if TPlatformServices.Current.SupportsPlatformService(IFMXLocaleService) then
  begin
    // 獲取FMX本地化服務接口實例
    LocaleService := TPlatformServices.Current.GetPlatformService
      (IFMXLocaleService) as IFMXLocaleService;
      
    // 獲取當前系統的語言ID(例如:"en-US"、"zh-CN"等)
    Lang := LocaleService.GetCurrentLangID;
  end
  else
    // 如果不支持本地化服務,則默認使用美國英語('US')
    Lang := 'US';

  // 設置國家/地區文本框默認值為中國('CN')
  EditCountry.Text := 'CN';
  
  // 設置REST客户端的基礎URL為OpenWeatherMap API地址
  RESTClient1.BaseURL := 'http://api.openweathermap.org/data/2.5';
  
  // 設置REST請求的資源路徑,包含參數佔位符:
  // {country} - 國家/地區代碼
  // {lang} - 語言代碼
  // {APPID} - API密鑰
  RESTRequest1.Resource :=
    'forecast?q={country}&mode=json&lang={lang}&units=metric&APPID={APPID}';
    
  // 設置REST請求中的APPID參數值
  RESTRequest1.Params.ParameterByName('APPID').Value := APPID;
  
  // 初始化時將活動指示器設置為不可見(不顯示加載動畫)
  AniIndicator1.Visible := False;
end;

當訪問一個RESTful服務器時,一般在TRESTClient控件中指定基本的URL,而查詢字符串參數則在TRESTReuqest控件中指定。花括號內是查詢字符串參數的佔位和會,可以通過RESTRequest1.Params.ParameterByName這樣的語法來進行設置。

2.為天氣預報的刷新按鈕添加事件處理代碼,這裏將使用標準的RESTRequest1.ExecuteAsync異步獲取服務器端數據,獲取完成後會執行一個匿名方法。
在這個事件處理代碼中,調用了3個自定義的過程:

// 在列表視圖中添加日期分組標題項
// 參數:
//   AItems: 列表視圖的項集合
//   ADateStr: 要顯示的日期字符串(格式為yyyy/mm/dd)
procedure TMainForm.AddHeader(AItems: TListViewItems; const ADateStr: string);
// 在列表視圖中添加單個天氣預報項
// 參數:
//   AItems: 列表視圖的項集合
//   ADateTime: 預報日期時間
//   ADescription: 天氣描述(如"晴"、"多雲"等)
//   ATempMin: 最低温度
//   ATempMax: 最高温度
procedure TMainForm.AddForecastItem(AItems: TListViewItems; 
  ADateTime: TDateTime; const ADescription: string; 
  ATempMin, ATempMax: Double);
// 在列表視圖中添加日期分組的彙總信息(當天温度極值)
// 參數:
//   AItems: 列表視圖的項集合
//   AMinTemp: 當天最低温度
//   AMaxTemp: 當天最高温度
procedure TMainForm.AddFooter(AItems: TListViewItems; 
  AMinTemp, AMaxTemp: Double);

有了這些子程序的輔助,事件處理的代碼就會相對簡潔不少,完整的單擊事件處理代碼如下所示:

// 獲取天氣預報按鈕點擊事件處理過程
procedure TMainForm.btnGetForecastsClick(Sender: TObject);
begin
  // 清空列表視圖中的所有項
  ListView1.Items.Clear;
  
  // 設置REST請求中的country參數值,將城市和國家用逗號連接
  RESTRequest1.Params.ParameterByName('country').Value :=
    String.Join(',', [EditCity.Text, EditCountry.Text]);
    
  // 設置REST請求中的lang參數值,使用之前獲取的語言ID
  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,             // JSON對象
      LForecastItem, LJObjCity: TJSONObject;
      LJArrWeather, LJArrForecasts: TJSONArray;  // JSON數組
      LTempMin, LTempMax: Double;       // 最低和最高温度
      LDay, LLastDay: string;           // 當前日期和上一個日期
      LWeatherDescription: string;      // 天氣描述
      LAppRespCode: string;             // 應用響應代碼
      LMinInTheDay, LMaxInTheDay: Double; // 當天最低和最高温度

    begin
      try
        // 將響應內容轉換為JSON對象
        LJObj := RESTRequest1.Response.JSONValue as TJSONObject;

        // 檢查錯誤響應
        LAppRespCode := LJObj.GetValue('cod').Value;
        if LAppRespCode.Equals('404') then
        begin
          // 城市未找到的錯誤處理
          lblInfo.Text := '沒有找到指定城市';
          Exit;
        end;
        if not LAppRespCode.Equals('200') then
        begin
          // 其他錯誤處理
          lblInfo.Text := '錯誤 ' + LAppRespCode;
          Exit;
        end;

        // 解析天氣預報數據
        LMinInTheDay := 1000;    // 初始化當天最低温度為極大值
        LMaxInTheDay := -LMinInTheDay; // 初始化當天最高温度為極小值
        
        // 獲取預報列表數組
        LJArrForecasts := LJObj.GetValue('list') as TJSONArray;
        
        // 遍歷所有預報項
        for LJValue in LJArrForecasts do
        begin
          // 獲取單個預報項對象
          LForecastItem := LJValue as TJSONObject;
          
          // 將Unix時間戳轉換為Delphi日期時間
          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;
            
          // 格式化日期為yyyy/mm/dd
          LDay := FormatDateTime('yyyy/mm/dd', DateOf(LForecastDateTime));
          
          // 檢查是否是新的日期
          if LDay <> LLastDay then
          begin
            // 如果不是第一個日期,添加前一天的最高和最低温度的統計信息
            if not LLastDay.IsEmpty then
            begin
              AddFooter(ListView1.Items, LMinInTheDay, LMaxInTheDay);
            end;
            
            // 添加新日期的標題
            AddHeader(ListView1.Items, LDay);
            
            // 重置當天温度極值
            LMinInTheDay := 1000;
            LMaxInTheDay := -LMinInTheDay;
          end;
          
          // 保存當前日期供下次比較
          LLastDay := LDay;
          
          // 更新當天最低和最高温度
          LMinInTheDay := Min(LMinInTheDay, LTempMin);
          LMaxInTheDay := Max(LMaxInTheDay, LTempMax);

          // 添加預報項到列表視圖
          AddForecastItem(ListView1.Items, LForecastDateTime,
            LWeatherDescription, LTempMin, LTempMax);
        end; // 結束預報項遍歷

        // 添加最後一天的彙總信息
        if not LLastDay.IsEmpty then
          AddFooter(ListView1.Items, LMinInTheDay, LMaxInTheDay);

        // 獲取並顯示城市和國家信息
        LJObjCity := LJObj.GetValue('city') as TJSONObject;
        lblInfo.Text := LJObjCity.GetValue('name').Value + ', ' +
          LJObjCity.GetValue('country').Value;

      finally
        // 無論成功或失敗,都執行以下清理操作
        AniIndicator1.Visible := False;  // 隱藏活動指示器
        AniIndicator1.Enabled := False;  // 禁用活動指示器
        btnGetForecasts.Enabled := True; // 重新啓用按鈕
      end;
    end);
end;

代碼的詳細解説如下:

  1. RESTRequest1.Params 是 TRESTRequest 組件中用於管理所有請求參數的集合屬性,它允許你在發送 HTTP 請求前配置各種類型的參數,參數country配置為“城市,國家”,這是API的定義需求,直接寫城市有時也能找到。

  2. RESTRequest1.Response包含很多屬性用來獲取來自服務器的響應,如果服務器端返回JOSN數據,則使用JSONValue屬性可以獲取到JSON實例。

  3. 服務器返回的JSON中包含了狀態碼,可以根據狀態碼判斷查詢成功還是失敗。

  4. 接下來對JSON中的list數組進行了解析,LMinInTheDay和LMinInTheDay 變量是統計1天中最高與最低温度的變量,在每天的日期發生變化後清零,在每天會進行Min與Max的統計運算。

  5. 在每天的日期發生變化後,會調用AddHeader添加頁眉,在添加頁眉之前,總是會先對上一個日期結束位置調用AddFooter添加頁腳。

  6. 在最後一天也會添加一個頁腳,避免遺漏。

  7. 在每一天的日期記錄中,調用AddForecastItem添加項。

  8. 在代碼最後為主窗體的footer區的TLabel控件也設置了顯示文本。

接下來看看3個過程的具體實現,它們並未涉及到具體的顯示位置等邏輯,這一切要在TListView的OnUpdateObject事件中完成。

// 添加日期分組標題項到列表視圖
procedure TMainForm.AddHeader(AItems: TAppearanceListViewItems;
  const ADay: String);
var
  LItem: TListViewItem;  // 聲明列表項變量
begin
  // 在列表項集合中添加新項
  LItem := AItems.Add;

  // 設置該項為分組標題類型
  LItem.Purpose := TListItemPurpose.Header;

  // 在名為'HeaderLabel'的繪製對象中設置日期文本
  LItem.Objects.FindDrawable('HeaderLabel').Data := ADay;
end;

// 添加單個天氣預報項到列表視圖
procedure TMainForm.AddForecastItem(AItems: TAppearanceListViewItems;
  const AForecastDateTime: TDateTime;  // 預報日期時間
  const AWeatherDescription: String;   // 天氣描述文本
  const ATempMin, ATempMax: Double);   // 最低/最高温度
var
  LItem: TListViewItem;  // 聲明列表項變量
begin
  // 在列表項集合中添加新項
  LItem := AItems.Add;

  // 設置'WeatherDescription'繪製對象的數據:
  // 格式為"小時時+天氣描述"(如"14時 多雲")
  LItem.Objects.FindDrawable('WeatherDescription').Data :=
    FormatDateTime('HH', AForecastDateTime) + '時  ' + AWeatherDescription;

  // 設置'MinTemp'繪製對象的數據:
  // 格式為"最低温度°"(如"12.50°")
  LItem.Objects.FindDrawable('MinTemp').Data :=
    FormatFloat('#0.00', ATempMin) + '°';

  // 設置'MaxTemp'繪製對象的數據:
  // 格式為"最高温度°"(如"24.80°")
  LItem.Objects.FindDrawable('MaxTemp').Data :=
    FormatFloat('#0.00', ATempMax) + '°';
end;

// 添加日期分組彙總信息到列表視圖
procedure TMainForm.AddFooter(AItems: TAppearanceListViewItems;
  const LMinInTheDay, LMaxInTheDay: Double);  // 當日最低/最高温度
var
  LItem: TListViewItem;  // 聲明列表項變量
begin
  // 在列表項集合中添加新項
  LItem := AItems.Add;

  // 設置該項為分組腳註類型
  LItem.Purpose := TListItemPurpose.Footer;

  // 設置腳註文本:
  // 格式為"最低 XX.XX° 最高 XX.XX°"(如"最低 12.50° 最高 24.80°")
  LItem.Text := Format('最低 %2.2f° 最高 %2.2f°', [LMinInTheDay, LMaxInTheDay]);
end;

這裏的過程僅負則添加和設置文本或Data數據,每一次AItems.Add過程調用之後,需要指定Purpose為具體的項類型,以便於OnUpdateObjects進行處理。

每次在Item.Txt或者是FindDrawable語句觸發時,會自動調用OnUpdateObject來實現顯示對象的真正呈現工作,事件處理代碼如下:

// 列表視圖項更新事件處理過程
procedure TMainForm.ListView1UpdateObjects(const Sender: TObject;
  const AItem: TListViewItem);
var
  AQuarter: Double;     // 用於存儲四分之一寬度的變量
  lb: TListItemText;    // 列表項文本對象引用
  lListView: TListView; // 列表視圖引用
begin
  // 將Sender轉換為TListView類型
  lListView := Sender as TListView;
  
  // 根據列表項的目的類型進行不同處理
  case AItem.Purpose of
    // 處理普通列表項
    TListItemPurpose.None:
      begin
        // 計算每列寬度(總寬度減去左右邊距後分成4份)
        AQuarter := (lListView.Width - lListView.ItemSpaces.Left -
          lListView.ItemSpaces.Right) / 4;
        
        // 設置普通項的高度為24像素
        AItem.Height := 24;

        // 處理天氣描述文本控件
        lb := TListItemText(AItem.Objects.FindDrawable('WeatherDescription'));
        if not Assigned(lb) then  // 如果對象不存在則創建
        begin
          lb := TListItemText.Create(AItem);    // 創建新文本對象
          lb.PlaceOffset.X := 0;                // 設置X偏移為0
          lb.TextAlign := TTextAlign.Leading;  // 文本左對齊
          lb.Name := 'WeatherDescription';     // 設置對象名稱
        end;
        lb.PlaceOffset.X := 0;  // 確保X偏移為0

        // 處理最低温度文本控件
        lb := TListItemText(AItem.Objects.FindDrawable('MinTemp'));
        if not Assigned(lb) then  // 如果對象不存在則創建
        begin
          lb := TListItemText.Create(AItem);  // 創建新文本對象
          lb.TextAlign := TTextAlign.Trailing;  // 文本右對齊
          lb.TextColor := TAlphaColorRec.Blue;  // 設置文本顏色為藍色
          lb.Name := 'MinTemp';  // 設置對象名稱
        end;
        lb.PlaceOffset.X := AQuarter * 2;  // 設置X位置為第二列
        lb.Width := AQuarter;  // 設置寬度為四分之一寬度

        // 處理最高温度文本控件
        lb := TListItemText(AItem.Objects.FindDrawable('MaxTemp'));
        if not Assigned(lb) then  // 如果對象不存在則創建
        begin
          lb := TListItemText.Create(AItem);  // 創建新文本對象
          lb.TextAlign := TTextAlign.Trailing;  // 文本右對齊
          lb.TextColor := TAlphaColorRec.Red;  // 設置文本顏色為紅色
          lb.Name := 'MaxTemp';  // 設置對象名稱
        end;
        lb.PlaceOffset.X := AQuarter * 3;  // 設置X位置為第三列
        lb.Width := AQuarter;  // 設置寬度為四分之一寬度
      end;
      
    // 處理列表頭項
    TListItemPurpose.Header:
      begin
        // 設置頭部高度為48像素
        AItem.Height := 48;
        
        // 處理頭部標籤文本控件
        lb := TListItemText(AItem.Objects.FindDrawable('HeaderLabel'));
        if not Assigned(lb) then  // 如果對象不存在則創建
        begin
          lb := TListItemText.Create(AItem);  // 創建新文本對象
          lb.TextAlign := TTextAlign.Center;  // 文本居中對齊
          lb.Align := TListItemAlign.Center;  // 控件居中對齊
          lb.TextColor := TAlphaColorRec.Red;  // 設置文本顏色為紅色
          lb.Name := 'HeaderLabel';  // 設置對象名稱
        end;
        lb.PlaceOffset.Y := AItem.Height / 4;  // 設置Y偏移為高度的四分之一
      end;
      
    // 處理列表腳註項
    TListItemPurpose.Footer:
      begin
        // 設置腳註文本右對齊
        AItem.Objects.TextObject.TextAlign := TTextAlign.Trailing;
      end;
  end;
end;

可以看到,在OnUpdateObject事件中,動態的創建了可繪製項元素,動態計算並設置其寬度和高度,所有與呈現相關的工作都在這個事件中得以完成。

總結

這篇文章主要分享瞭如下幾個知識點:

  1. 使用TRESTClient和TRESTResponse訪問遠程服務器,獲取JSON數據源。
  2. 使用System.JSON命名空間中的類解析JSON。
  3. 根據JSON數據源動態創建列表項。
  4. 處理OnUpdateObject事件創建列表項的呈現對象。

相信通過對這篇文章的學習,可以對TListView有較為深入的理解。

Add a new Comments

Some HTML is okay.