2014年8月29日 星期五

FireMonkey v.s. VCL 之一 - 動態產生視覺元件的差異

在撰寫程式的過程中,對於畫面的製作,會隨著程式經驗與類型的積累,產生幾種演進:
所有畫面都事先在DesignTime,也就是設計階段,把畫面都先處理好
能在設計階段製作的,先在設計階段處理,不行的部分,在RunTime即時產生
所有畫面都在RunTime 產生

演進的狀況不外乎上述三種,但未必每個程式人員都有機會遭遇到全部三種狀況,也很難說那一種做法是功力最高的,因為所有的程式製作方法,都應該視當時的條件來決定製作的方式,這些條件包含了:
開發時程 - 有時候很多專案會被要求在極短的時間之內完成,沒時間,就沒機會使用全RunTime的做法完成。

對FootPrint (整個編譯結果的檔案大小)的要求- 如果遇到只能在非常局限的磁碟空間中安裝,完全RunTime 的做法或許是達成要求的唯一方法。

便於團隊對程式碼的長久維護- 此時就要考慮整個團隊的程式設計習慣,挑選適合的做法。

且不管最後選了哪一種,在RunTime建立畫面的需求都是可能會存在的,所以我們在此對完成這個需求的方法做些介紹。,也就是動態產生元件的方法,並就VCL跟 FireMonkey 兩種架構的差異進行比較。

我們以最簡單的,在 Form 上面建立 Button 的程序進行比較

VCL版:
procedure TForm1.btnAddButtonClick(Sender: TObject);
var
    btn: TButton;
    countByRow, idx: integer;
begin
    btn := TButton.Create(self);
    btn.Parent := self.Panel1;

    self.ButtonList.Add(btn);
    idx := self.ButtonList.IndexOf(btn);
    btn.Caption := 'btn' + IntToStr(idx);

    countByRow := (self.Panel1.Width div btn.Width);
    btn.Left := idx mod (countByRow) * btn.Width + 10;
    btn.Top := idx div (countByRow) * btn.Height + 10;
end;
上面這段程式可以在 VCL 建立一個 Button, 但在 FireMonkey 當中, 建立的方法就不一樣了:

FireMonkey版:
procedure TForm2.btnAddNewButtonClick(Sender: TObject);
var
    btn: TButton;
    countByRow, idx: integer;
begin
    btn := TButton.Create(self);
    self.scrollBox.AddObject(btn);

    self.ButtonList.Add(btn);
    idx := self.ButtonList.IndexOf(btn);
    btn.Text := 'btn' + IntToStr(idx);

    countByRow := Trunc(self.scrollBox.Width / btn.Width);
    btn.Position.X := idx mod (countByRow) * btn.Width + 10;
    btn.Position.Y := idx div (countByRow) * btn.Height + 10;
end;
在 VCL 當中,動態建立了一個新的按鈕以後,我們只需指定該按鈕的 Parent, 就可以把建立的按鈕放到 Parent 這個元件上面,當然,也需要先指定新按鈕的位置 (Left, Top) 與大小 (Width, Height).



而 FireMonkey 要做到相同的效果,則是看我們要把建立出來的按鈕放在哪個元件上面,就呼叫該元件的AddObject 方法,例如我們要把新建立的按鈕顯示在表單上面,就直接呼叫 scrollBox.AddObject(newButton);



帶一句題外話:從上面的兩個範例 procedure 裡面, 可以看出 FireMonkey 的座標與元件大小都可以有小數點, 但 VCL 不行。

要動態移除該元件的時候,也非常不同,VCL只需要呼叫該元件的 Free 方法即可:
procedure TForm1.btnRemoveBtnClick(Sender: TObject);
var
    btn: TButton;
begin
    if self.ButtonList.Count > 0 then begin
        btn := self.ButtonList.Last;

        if Assigned(btn) then begin
            self.ButtonList.Remove(btn);
            btn.Free;
        end;
    end else begin
        ShowMessage('No Button');
    end;
end;
FireMonkey 則不一樣:
procedure TForm2.btnRemoveClick(Sender: TObject);
var
    btn: TButton;
begin
    if self.ButtonList.Count > 0 then begin
        btn := self.ButtonList.Last;

        if Assigned(btn) then begin
            self.ButtonList.Remove(btn);
            self.scrollBox.RemoveObject(btn);
            btn.DisposeOf;
        end;
    end else begin
        ShowMessage('No Button');
    end;
end;
在 VCL 裡面,呼叫了 Free 方法以後,obj 所佔用的記憶體就會在 Windows 系統裡面被釋放出來,但在 FireMonkey 當中,由於要適應絕大多數系統上的特性,所以 Free 方法只會把該元件的 Reference Count 減一,在 Android 跟 iOS, Mac OS 裡面,一個元件要被系統釋放掉的話,必須要 Reference Count 等於 0, 才會真正把元件刪除掉。

所以 RemoveObject 被呼叫以後,obj 元件會在畫面上看不見了,但它還沒有真的被刪除,在 FireMonkey 當中,我們可以呼叫 obj 元件的 DisposeOf 方法,這個方法在Windows 平台上,會有等同於呼叫了 Free 方法的作用。

但在 iOS, Andorid, Mac OSX 上面, 則會強制把 Reference Count 遞減為 0, 真正強制的把佔用的記憶體釋放出來。

結語
許多已經很習慣於傳統 Delphi 或 傳統 Windows 應用程式設計的 Programmer, 會很習慣於把所有的畫面都放在同一個 Form 裡面。

在傳統的 Windows 或桌機型的應用程式環境當中,這是沒有問題的,但在行動裝置或者嵌入式裝置的應用程式當中,這是行不通的,因為在行動裝置當中,記憶體極其有限,有時只要使用超過了 40MB 的記憶體,應用程式就會被強制停止,也就是俗稱的閃退。

大家或許會覺得,40MB 很少,或者自己的應用程式不會用這麼多,但請大家記得一個數字,一張 2048x1536 的圖片,就可能佔用了約 10-15MB 的記憶體。

為什麼我提到 2048x1536 這個數字?

因為它正是 iPad Retina 螢幕的實際解析度,包含 iPad 3, 4, 5 (iPad Air), 以及 iPad Mini 2, 都是這個解析度,我們一旦有這個解析度大小的應用程式,只要兩張全螢幕的圖片,就已經處在危險邊緣。

如果還像以往的寫法,把所有的圖片、畫面都放在同一個 Form 裡面的話,只要換個兩頁,應用程式就會閃退,這種應用程式連上架的機會都沒有,所以,動態產生、動態刪除元件或畫面的需求將會越來越大,大家也一定要實際學會怎麼處理這種情形,不然前路絕對是坎坷不平的。

動態產生範例 (VCL & FireMonkey)

Delphi 用 for..in 迴圈釋放物件的注意事項 (XE5, XE6)

從 Delphi 2009 開始, 新增了不少原本 Object Pascal 沒有支援的語法, 包含了我還蠻常用的 for-each 迴圈. (在 Perl, obj-c 我都還蠻常用這個功能的)

為了搭配動態產生元件, 用 for-each 迴圈來處理釋放動態新增的物件, 在 Delphi XE5 分外重要,因為在 XE5 當中,我常用 TVertScrollBox 這個元件來處理需要動態新增、高度不確定的 Contents.

舉個大家最常見的例子來說,即時通訊軟體的對話內容,內容會有多少?誰也說不準,可能跟朋友A的對話內容有一千則,但跟不熟的路人甲則只有三則。

這時候,要用什麼元件來顯示這些對話內容呢? 從 XE2 開始,大多數的範例程式都是用 TListBox 或者 TListView 來載入這些資料,TListBox 跟 TListView 也很好,但這兩個元件的內容呈現比較制式,用來顯示通訊錄裡面的聯絡人非常適合,但要用來顯示對話內容的話,就不很合適了。

請回憶一下,現在大家都常用的通訊軟體,Line, What's app, iMessage 等等,都是以左邊顯示對方傳來的訊息,右邊顯示我方傳過去的訊息,底下會用個氣泡圖片來襯底,我們看一下 iMessage 的畫面:


要在 Delphi XE5, XE6 裡面製作完全相同的效果的話, 要怎麼做?

透過 TListView, 很抱歉, 我一時半刻創意不足, 想不出來, TListBox 倒是可以。

因為TListBox 的 Items 是 TListItem,建立TListItem 出來以後,可以用 AddObject 這個方法來把圖片 (TImage), 文字 (TLabel) 加到 TListItem 裡,所以也可以製作出像上圖的畫面,但如果要在上圖的畫面上顯示圖片呢?現在即時通訊軟體都流行貼圖,這種功能要怎麼在 TListBox 做出來呢?

抱歉,我又創意不足,一時技窮了。

這時候,大概也只有 TVertScrollBox 可以用來製作這樣的畫面效果了。

TVertScrollBox 跟 THorzScrollBox 都是從 TScrollBox 繼承而來的,從名稱來看,TVertScrollBox 是垂直方向的捲動視窗,而 THorzScrollBox 則是水平方向的捲動視窗,所以要製作上圖的畫面的話,就可以用 TVertScrollBox 來製作。

讓我寫一段簡單的 procedure 來示範怎麼把一個 TStrings 裡面的文字放進 TVertScrollBox 裡面去:

procedure addTextToScrollBox(info : TStringList);
var
   oneLine : string;
   currentY : Single;
   idx : integer;
   newLabel : TLabel;
begin
    currentY := 0.0;

    for idx := 0 to info.Count -1 do begin
         oneLine := info.Strings[idx];
         newLabel := TLabel.Create(self);
         newLabel.Position.X := 10;
         newLabel.Position.Y := currentY;
         newLabel.Text := oneLine;

         vScrollBox.AddObject(newLabel);

         currentY := currentY + 10.0 + newLabel.Height;  
    end;
end;

以上這段程式就可以把傳進去的 TStrings 的內容一行行顯示在名為 vScrollBox 的 TVertScrollBox 裡面去了,但這範例會有個小困擾,就是在對話內容要釋放,或者刷新的時候,需要把所有的資料全部清除,在 XE6 底下,可以用以下這個 procedure 來達成:

procedure clearScrollBox;
var
    obj : TFMXObject;
begin
    for obj in vScrollBox.Children do begin
         vScrollBox.removeObject(obj);
         obj.DisposeOf;
    end;
end;

在行動平台中,請記得除了透過 vScrollBox.removeObject(obj); 把 obj 從 vScrollBox 的可視範圍移除以外,還要呼叫 obj.DisposeOf; 這樣 obj 才會實際消失,不然的話,雖然obj 當時是看不見的,但卻也還存在記憶體當中沒有沒事放掉。

視覺效果上,我們會看到 vScrollBox 內容空無一物,但捲動軸卻不會消失,這就會讓使用者覺得很納悶了,所以記得,除了RemoveObj之外,還要呼叫 DisposeOf 把物件是放掉。

如果是 XE5 的話, TVertScrollBox 的 Children 就不只存放我們透過 addObject 放進去的物件,我實測的結果,會連同捲動軸也列名在 Children 裡面,所以得先判斷其 Class 是否是我們要釋放的,因此要加入 Delphi 的 RTTI 功能,改為:

procedure clearScrollBox;
var
    obj : TFMXObject;
begin
    for obj in vScrollBox.Children do begin
         if (obj is TLabel) then begin
             vScrollBox.removeObject(obj);
             obj.DisposeOf;
         end;
    end;
end;

不然也可能會把捲動軸弄消失,但這一點在 XE6 以後的版本應該不用再擔心了,真是太好了。