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)

沒有留言:

張貼留言