顯示具有 Mobile 標籤的文章。 顯示所有文章
顯示具有 Mobile 標籤的文章。 顯示所有文章

2015年11月28日 星期六

如何建立自己的 Xcode .a 檔案

在使用 Delphi 建立自己的 app 時, 有時候會需要使用到第三方廠商的 source code, 這些 source 如果是用 Xcode objective-c 撰寫, 用來提供 iOS 的某些功能, 直接使用這些驗證過的程式碼, 會比自己照著原始碼重寫一次來的快、來的可靠。


所以,最快的方法就是用 Xcode 把這些原始碼 (通常會是 .m 跟 .h 檔案) 建立一個 .a 檔案, .a 檔案是靜態函式庫, 等同於在 C 語言裡面的 .lib 檔, 連結後就不能動態改變執行檔的內容了。

以下是建立 .a 檔案的簡單步驟說明:

1.     使用 XCode建立一個新的專案

      

2.     選擇 Framework & Library -> Cocoa Touch Static Library, 然後選Next












3.     填寫 library 的基本資料, 任填就行了.
 
4.     選擇一個 folder 來存放專案檔案, 我選擇在 iOS SDK8 目錄下, Xcode會自動建立專案目錄

5.     finder找到要加入的.m .h 檔案, 拖拉進 Xcode 專案中


6.     選擇編譯的專案組態, 必須對Device Simulator分別都進行 Build.



7.     Device Simulator 都編譯好了之後, 找到製作好的 .a 檔案, 快速找尋方法: Products 點選 lib, 然後用滑鼠右鍵點選它, 選擇 Show In Finder, 就會出現一個新的 Finder 視窗, 顯示該檔案的位置。





8.     上面的 Finder 是顯示 Debug-iphones Debug-iphonesimulator兩個目錄裡面的libmyPackage.a 檔案.
9.     iphoneos 目錄裡面的, 是給實際裝置使用的 .a 檔案, iphonesimulator目錄裡面的, 是給模擬器使用的 .a 檔案, 我們可以透過以下的 command line 來把兩個檔案合一, 檔案會變大, 但就不用選擇不同的檔案了, 這個指令的用法是 lipo –create 第一個.a 第二個.a… -output 合併檔案

Mac OSX 裡面, Terminal (終端機)有個很方便的設計, 就是可以從 finder 把檔案直接拖拉到終端機畫面, 可以直接把該檔案的完整路徑拖拉進去:




然後 key in –output, 再拖拉 folder 進去:

10. 輸入檔名, 剛剛指令已經完成輸入到 –output 目錄 , 我們把檔案名輸入一下, 本例中是 libmyPackage.a
11. 按下 Enter, lipo 就會執行, 把兩個 .a 檔案合併成同一個, 完成。

補充: 預設建立的 .a 檔案是 debug 模式的, 裡面會有 debug information, 如果要建立 Release 的版本, 則需要修改一下 Build Configuration:


點選 Edit Scheme, 會出現設定畫面:
下拉 Build Configuration, 選擇 Release 即可.
重複6-12步驟, 這次要選擇 Release-iphoneos Release-iphonesimulator當中的 .a檔案, 建立出來的 .a , 就會是 Release 版本的了.

注意事項: 如果第三方的 source code 裡面有使用到 Delphi IDE 預設匯入的 iOS framework 以外的 framework, 也要記得把他們透過 Delphi IDE 的 Option->Tool->SDK Manager 匯入, 才能正確運作喔.

2015年1月6日 星期二

Get HTML/JSON from TWebBrowser FireMonkey (Delphi XE7)

Starting


FireMonkey starts from Delphi XE2, this new framework provides many powerful components and runtime library, and the most important point is, most of the components can be used cross-platform.

XE2 -> iOS fundmental features ready.
XE3-XE5 -> Android features were appended.
XE6 -> Almost features were ready, but minor performance issue remained.
XE7 -> Everything is ready, we still need some completion for couple components.

With mobile apps development, we need to connect the web server from time to time. Indy components play as a proper role for Delphi and C++ Builder, but the SSL feature is not perfect yet.

Hence, we still need TWebBrowser for user to interact with website, if the feedback or redirect path contains HTTPS URL, e.g., Facebook login, Google+ login.

However, there is no interfaces provided by TWebBrowser for retrieving the content of the page.

A->B, B->C, C->D

It's said in some movies, "Genius can get A to D without intermediate path, other cannot", I am not genius, so I have to find out A->B, B->C, C->D to achieve A->D.

The following is my thought:
A->D, A is the TWebBrowser, D is retrieveing content.

TWebBrowser cannot get content, but it can run Javascript without return value.
Javascript can get the content, but there is no way to send the content back to Delphi.
TWebBrowser can get the URL, which the browser wish to navigate.
Javascript can redirect the web browser to another specific URL.

So, the workaround is:
1. waiting for the target URL we want to get the content, e.g. the Google+ login result page.

http://www.moveinpocket.com/demo/GoogleLogin_API/index2.php (login page)
https://www.googleapis.com/oauth2/v1/userinfo?access_token= (login success page, the user information will be sent back with JSON format.)

2. if the login success page is done, the onDidFinishLoad event handler will handle the JSON Page:
if (Pos('https://www.googleapis.com/oauth2/v1/userinfo?access_token=',
        self.WebBrowser1.URL) = 1) then begin

            js := 'var markup = document.documentElement.innerText;' + #13 + #10
                + 'var newURL = "http://1.1.1.1/" + markup;' + #13 + #10 +
                'window.location = newURL;';
            self.WebBrowser1.EvaluateJavaScript(js);
    end;

3. Javascript redirect the browser to http://1.1.1.1/ (you can modify it to another http url), and append the JSON data (innerText in above codes) to the URL.

4. Let onShouldStartLoadURL of Browser to get "http://1.1.1.1/" url, then we got the content in Delphi!
if (Pos('http://1.1.1.1/', URL) = 1) then begin
        self.WebBrowser1.Height := 1;
        jsonStr := URL;
        Fetch(jsonStr, 'http://1.1.1.1/');

        jsonStr := TIdURI.URLDecode(jsonStr, IndyTextEncoding_UTF8);

        ShowMessage('got it');
        self.Memo1.Lines.Text := jsonStr;
        self.TabControl1.ActiveTab := self.TabItemResult;
    end;
I had search some work around with the following key words in Google, Stackoverflow, and it's pity that there is no any solution yet:
Delphi, FireMonkey, WebBrowser, get HTML, get JSON, get Content.

So, this work around might be the only way to get content from TWebBrowser, if you need the sample project, please download from here.

Best Regard, and Enjoy your Delphi.
Dennies Chang.

如何用 FireMonkey 的 TWebBrowser 取回 JSON 資料

緣起

從 Delphi XE2 開始, FireMonkey就包含了跨平台的TWebBrowser元件(Windows版本跟行動版本是分開的, 我記得Windows版本的 TWebBrowser 元件, 是 VCL 版本的 ActiveX),但從 XE2 開始到 XE6, 每一版的 TWebBrowser 都少了點東西.

XE2到XE4都沒有Android版本的WebBrowser, 到 XE5 總算行動平台的 WebBrowser 都具備了, 但在網頁當中的 Form 輸入文字, 卻有選擇了文字無法傳送到文字框的缺失, 這個問題在 XE6 被解決掉了,XE6 的 WebBrowser 又有了效能不夠好的問題。

到了 XE7,終於大多數問題都被解決掉了,也加入了強制執行 Javascript 的功能,可謂十全八美 (按:iOS執行 Javascript之後,可以把執行後的字串當成回傳值讓 UIWebBrowserDelegate 處理,Delphi XE7 目前還不行)。

另外,也還無法直接 access WebBrowser 裡面的內容,在以往我們使用 VCL 版本的 TWebBrowser 時,我們至少還可以透過 ActiveX 介面取得 innerHTML 或者 innerText,然而目前在行動裝置上,FireMonkey 並沒有可以讓我們可以取得內容的途徑。

由A到B 由B到C 由C到D的轉折

俗話說「山不轉路轉」,TWebBrowser沒有可以 access 內容的途徑,但是Javascript有。Javascript 不能把字串直接傳給 Delphi 程式碼,但是可以把字串當成Javascript 的變數。TWebBrowser 不能直接取得內容,但可以取得網址。

所以,筆者兜兜轉轉,拼湊出了這麼一套方法:
1. 先確定進入了我們需要的內容網址,筆者以自己寫的 Google+ 登入網頁作為範例。
http://www.moveinpocket.com/demo/GoogleLogin_API/index2.php (登入)
https://www.googleapis.com/oauth2/v1/userinfo?access_token= (登入成功頁, 會以 JSON 格式回傳使用者的資料)
2. 檢查是否進入了登入成功頁,如果進入了,就讓 WebBrowser 執行Javascript,把 innerText 抓出來當字串變數。
if (Pos('https://www.googleapis.com/oauth2/v1/userinfo?access_token=',
        self.WebBrowser1.URL) = 1) then begin

            js := 'var markup = document.documentElement.innerText;' + #13 + #10
                + 'var newURL = "http://1.1.1.1/" + markup;' + #13 + #10 +
                'window.location = newURL;';
            self.WebBrowser1.EvaluateJavaScript(js);
    end;
3. 用 Javascript 的 redirect 方法:windows.location,轉到一個不存在的網址,把innerText 當成該網址的參數,不用名字,直接接在網址後面就行了。
4. 透過 TWebBrowser 的 ShouldStartLoadURL, 就可以抓到這串字了。
if (Pos('http://1.1.1.1/', URL) = 1) then begin
        self.WebBrowser1.Height := 1;
        jsonStr := URL;
        Fetch(jsonStr, 'http://1.1.1.1/');

        jsonStr := TIdURI.URLDecode(jsonStr, IndyTextEncoding_UTF8);

        ShowMessage('got it');
        self.Memo1.Lines.Text := jsonStr;
        self.TabControl1.ActiveTab := self.TabItemResult;
    end;

也太費功夫了吧⋯⋯ 不過沒辦法,這方法還真的能抓到TWebBrowser的內容喔。

使用時機

大家可能會問我:為什麼不用IdHttp,或者 TRESTClient來抓資料,這麼費神幹嘛,你自己不是Indy的愛用者嗎?

是的,筆者愛用Indy是事實,也還寫了Indy的書沒有錯,但有時候還是會有需要用TWebBrowser的時候,像是如果需要用 Google+ 當作登入頁,試問除了TWebBrowser,還有什麼方式可以做到?

這時候千萬別回我「用IdHTTP」喔,在行動裝置上的IdHTTP,對https網址是無法載入的,不信可以試試看。所以認命點,這是你在全球唯一能找到的解決方法了,問問我怎麼這麼有自信?因為我才剛在寫這篇文章之前做過功課,從 stackoverflow,到 edn,到Marco cantu的 Blog,expert-exchange,KTOP我都找過了,到2015年一月,目前也只有這個解法了,請相信!

以下,附上範例程式碼,太長的內容我不保證一定成功喔,但JSON資料還OK啦。
本篇文章範例程式專案

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)

2014年7月12日 星期六

FireMonkey - 行動裝置上虛擬鍵盤的出現與隱藏

在 Win32 的應用程式開發中, VCL 架構當中, 我們常常會在畫面上有多個 TEdit 或 TMemo 的時候, 設定這些元件的 TabOrder, 用以決定每個文字編輯元件的順序, 待使用者輸入文字後,按下Tab鍵,便可切換到下一個文字輸入元件。

在行動裝置上卻沒有所謂以Tab鍵進行文字輸入元件切換的功能,要不是在虛擬鍵盤或手寫輸入控制元件上緣有上一個、下一個的按鍵進行切換,就會直接在鍵盤或手寫輸入框裏有換行、完成的按鍵,待使用者按下之後切換到下一個輸入的文字框去。

不僅如此,在行動裝置上輸入文字時,還會因為虛擬鍵盤的出現,需要把畫面整個向上挪移,才不會有文字框被虛擬鍵盤擋住,使用者輸入了什麼都看不見的窘況。

在 FireMonkey 的 TEdit,TMemo,就是扮演文字輸入框的角色,然而遍尋這兩個元件的所有Event,卻也找不著要如何達成前述切換文字輸入框的相關事件,更別提要像obj-c提供個像是 textFieldBeginEdit 這樣的Delegate method來偵測虛擬鍵盤的出現了。

雖然在TEdit裡面有onChange,onKeyUp,onKeyDown,onTyping這些事件,但這些事件卻無法接收到虛擬鍵盤的Return被觸發的事件,當然Windows系統上的Return可以測得,但行動裝置的不行,所以很令人煩躁。

然而,在FireMonkey 中,還是能夠達到前述功能的,只是要多花一些功夫,稍微多幾個步驟就是了:

KillFocusByReturn
這個屬性(property)是 TEdit 的新屬性,在設定接受到虛擬鍵盤的 Return 值時,是否要停止編輯,並讓目前的TEdit 失去Focus,這個屬性預設值是false,我們要把它改設定成true。

這樣一來,按下虛擬鍵盤的Return時,虛擬鍵盤就會自動縮回去了,也就是該文字框完成了編輯。

 

















































































TEdit 與鍵盤相關的新屬性
                        
TEdit 的事件


onEnter 事件
在每一個TEdit的事件表裡面,都有 onEnter 跟 onExit 這兩個事件,可以偵測到行動裝置上,該欄位是否開始/結束編輯了」

我們得自己計算一下每個TEdit 進入編輯模式時,該TEdit會不會被擋到?以iPad 為例,iPad的虛擬鍵盤(橫向)高度約為250,所以我會在橫向畫面最底下的TEdit 的 onEnter 事件當中設定一個畫面座標向上挪移250的動作:

procedure THeaderFooterForm.Edit1Enter(Sender: TObject);
begin
   shiftY := -150;
end;

設定了-150 的Y軸位移量之後,接著要偵測虛擬鍵盤出現與消失的事件:onVirtualKeyboardShow, onVirtualKeyboardHidden 這兩個事件,別急,看完再去寫 code。

Form 的虛擬鍵盤顯示/隱藏事件

onVirtualKeyboardShow, onVirtualKeyboardHidden 這兩個事件並不是歸屬給單一 TEdit 或TMemo 的哦,它們是歸屬於 TForm 的事件。

所以,我們接下來要先點選 TForm 元件,找到 onVirtualKeyboardShow, onVirtualKeyboardHidden 這兩個事件,雙擊它們,來製作出處理這兩個事件的事件處理常式。

如果Form 的畫面是由一個 align 設定為全螢幕(alClient)的TTabControl 元件所構成的,則在 onVirtualKeyboardShow 這個事件裡,我們要先把 TTabControl 元件的 align 屬性改設定為 alNone,否則修改其Position.Y 就不會有任何效果。

出現虛擬鍵盤
輸入完畢以後, 恢復原位














接著,把TTabControl Position.Y 設定為 ShiftY,就會看到畫面向上移動,露出正在輸入當中的 TEdit 或 TMemo 了。

最後,onVirtualKeyboardHidden 的時候,只要把 Position.Y 設定為 0,畫面就會恢復正常了。


心得分享:每一個文字輸入框的 ShiftY 都是獨立的,不用去管其他文字框的位移量是多少,彼此並沒有交互作用。


範例程式: 下載點

2014年6月30日 星期一

FireMonkey 桌面應用程式最小化

問題 - 視窗縮小時不會整個應用程式縮小

在製作 Delphi 或 VB 程式時, 常常會使用多個 Form, 在每個 Form 的畫面當中, 會賦予不同的功能, 但每個 Form 都會被當成獨立的畫面, 也就是 MainForm.

我們對 MainForm 的預期, 就是在按下右上方的縮小鍵時, 整個應用程式縮小到工作列上, 按了工作列上的按鈕時, 整個程式畫面還原回來原來的狀態.

但從過去 VCL 的時期到現在的 FireMonkey, 都並沒有這麼容易, 多個 Form 的時候, 只要不是按下 MainForm 的縮小按鈕, 視窗都會縮在桌面左下角。

視窗沒有縮到工作列上, 而是成為縮小視窗
以往這種 Form 的特性, 是為了製作 MDI Application, 也就是一個大的 Form 當中有多個文件會被開啟, 就像 Word, Powerpoint 那樣, 可以開啟多個文件, 讓每個文件有獨立的小視窗用來檢視文件。

以往在 VCL 架構的解決方法

在 VCL 架構的應用程式中, 解決這個問題的方法, 是在任何一個 Form 裡面建立一個 onMessage 的事件處理常式, 並在 FormCreate 事件中把它指派給 Application.OnMessage 事件:

procedure TForm1.HandleMessage(var Msg: TMsg; var Handled: Boolean); 
begin
   if (Msg.Message=WM_SYSCOMMAND) and (Msg.wParam=SC_MINIMIZE) then
     Application.Minimize;
end;

procedure TForm1.FormCreate(Sender: TObject);
begin
   Application.onMessage := self.HandleMessage;
end;

當我們發現 SC_MiniMize 事件發生, 就直接執行 Application.Minimize, 整個應用程式就會縮到工作列上了, VCL 上面如此, FireMonkey 卻不然...... 時至今日, 網路上的各個論壇還在尋找著跟 VCL 架構當中的 Application.onMessage 對應的事件...... 然而, 我也找不到, 但是在 FireMonkey 當中, 有很多玩法跟 VCL 不相同, 花了一陣子搜尋 XE5 裡面的 RTL 原始碼, 找到了這個玩法, 有效, 簡易, 跟大家分享。

範例-製作兩個 Form 互相切換

先建立一個 FireMonkey 應用程式, 包含兩個 Form, Form1 跟 Form2, Form1 是 預設的MainForm, Form2 則是一般的 Form.

Form1, 放一個 button 切換到 Form2
Form2, 放一個 button 切換到 Form1













Form1 上面的 Button1Click 內容如下
procedure TForm1.Button1Click(Sender: TObject);
begin
   form2.Show;

   self.Hide;
end;

Form2 上面的 Button1Click 內容如下
procedure TForm2.Button1Click(Sender: TObject);
begin
   form1.Show;

   self.Hide;
end;

這是最常見的寫法, 然而切換到 Form2 的時候, 按下視窗上的縮小按鈕, 就會跟本文最上方的圖片所顯示的一樣:

這時候, 只要在 Form1 的 Button1Click 加上一行:
Application.MainForm := form2;

就可以把 Application.MainForm 從 form1 切換成 form2, 此時再按 form2 的縮小按鈕, 就不會再出現縮小的視窗了耶, 超神奇, 兩個 click 事件處理常式如下
修改後, Form1 上面的 Button1Click 內容如下:
procedure TForm1.Button1Click(Sender: TObject);
begin
   form2.Show;
   Application.MainForm := form2;
   self.Hide;
end;

修改後, Form2 上面的 Button1Click 內容如下
procedure TForm2.Button1Click(Sender: TObject);
begin
   form1.Show;
   Application.MainForm := form1;
   self.Hide;
end;

結語

FireMonkey 上面許多用法都很神奇, 由於所有元件都有 Container 的特性, 所以可以直接收納其他元件, Application.MainForm 也不是唯讀屬性, 所以可以在切換 form 的時候順帶把 Application.MainForm 做重新指派, 雖然很難想到這個用法, 不過它確確實實的有效, 程式碼也簡潔, 所以推薦給大家使用.

範例程式連結

2014年6月28日 星期六

FireMonkey 分享 - TImageButton 的製作

緣起

在以 VCL 撰寫桌面應用程式的時候, 常會遇到一個狀況, 就是需要讓某個按鈕用美術人員設計的圖片當成底圖, 而且該按鈕還要有四種狀態:

  1. Normal (一般狀態時)
  2. HighLighted (滑鼠游標進入按鍵區域時)
  3. Pressed (滑鼠按下時)
  4. Disabled (設定為不能點選時)
這樣的按鈕, 在過去 VCL 的時代, 有 TImageButton, TBitmapButton 等各種第三方元件可以使用, Delphi 內部也有一兩個內建的元件 (TBitButton? 不知道我有沒有記錯) 可以讓我們把圖片放上來, 然後設定一張圖片 (或多張) 給該按鈕不同屬性作為顯示圖片.

VCL 時代, 由於對圖片格式的支援, 在每一版的 Delphi 都不太一樣, Delphi 6 好像預設只有支援 Bitmap, 如果要使用 Jpeg 格式的圖片, 就得自己在 use 的區塊輸入 Jpeg 這個 unit 的名字.

如果要使用 Png 的話, 在 2005 年前後則有了 TPngObject 這個第三方工具可以使用, 但還是必須由程式人員自己去找到、引入才行.

所以我在 2005 年也自己寫了 TPngImageButton 這個 VCL 元件, 在 TButton 元件的屬性中加入了四種狀態的圖片, 可以在設計階段 (Design Time)把 Png格式的圖片設定好, 並存在 dfm 檔案裡面.

但當時只想著直接用圖片來解決一切, 並沒有想到有一天在處理多國語系的時候, 裡面還要多放個 TLabel, 好讓自己處理多國語系的介面文字顯示時能夠簡單一點, 這一切, FireMonkey 都已經處理好了.

元件容器的功能 (Container feature)

在 VCL 架構的應用程式裡面, 如果我們要讓兩個 VCL 元件結合在一起, 並且在 Design Time 的 Form 上面, 則其中一個必須具備包容其他 VCL 元件的功能,在 VCL 的元件當中, 並非全部都具備此一特性, 僅有以下幾個元件有元件容器的特性:

  • TPanel
  • TPageController (其中的各個分頁)
  • TGroupBox

很明顯的,TButton 並不屬於其中一員,所以,要在TButton裡面放上一張圖片,並在Design Time進行各項屬性的調整、設定,在 VCL 版本的 TButton 是無法達成的,真的需要這樣的Button。我們只好新建一個 VCL 元件,繼承 TButton,然後 implement 各項 TButton 的屬性與方法,然後再透過 dpk 與 bpl 把這個新的 VCL 元件安裝到 VCL 元件盤 (component plate), 這樣就能達成 VCL 的 TImageButton 了,但這篇分享文章的主題不在 VCL,所以要跟各位說聲抱歉,如果有朋友需要我的TPngButton的話,下次有機會再把該元件分享給大家。

FireMonkey 的所有視覺化元件元件都是 Container

元件容器的功能,在容納其他的元件,VCL 並非所有元件都具備此功能,但在FireMonkey 就完全改變了這一點,FireMonkey 的所有視覺化元件都具備 container 的功能。

換句話說,我們可以從Delphi 的IDE環境左上角的 Structure 畫面,任意把任一元件拖拉放到其他元件上,立刻就能把多個元件結合起來,成為一個新的元件,不需安裝,也不像 bpl 得在功能有所變動的時候就得重新編譯、安裝,當然,如果您想要把這個複合的元件建立成一個新的元件放到元件盤上面去,也是做得到的。

步驟一:拉一個 TButton 到 form 上面

從元件盤上找到 TButton (如果您對 Delphi 還不熟悉,請從 Delphi 畫面右下角找到元件盤,在搜尋框裏輸入 button, 就會顯示很多名稱當中有 button 這個字詞的元件, 第一個符合條件的就是 TButton),找到以後,把它拖拉到您的 form 上面任意的地方。

從元件盤進行關鍵字搜尋

不麻煩的話,幫您的元件取個容易看出其用途的名字吧,一堆 Button1,Button2的按鍵很容易讓您的程式失去可讀性,也增加了維護的難度。

步驟二:拉兩個 TImage 到 form 上面

從元件盤裏拉兩個 TImage 元件到您的 form 上面來。

請注意,名稱裡有 Image 這個字詞的元件還不少,我們只要 TImage,不要 TImageControl 哦,TImage 在 FireMonkey 的元件盤裡,被歸類在 Shapes 裡面,在 Common 跟 Advance 分頁裡面是找不到的。

從元件盤上面搜尋 TImage 元件

設定圖片
TImage元件拉好了以後,請從左下角的屬性檢視視窗(Object Inspector) 設定要顯示的圖片:
幫 Image 元件設定要顯示的圖片
點擊屬性檢視視窗裡面的 MultiResPicture 欄位,裡面會出現一個小按鍵,點擊它,就可以選擇要放進 TImage的圖片檔案了。

選擇要放在 Image 裡面的圖片檔案
FireMonkey 所支援的圖片格式很多,我們最常用的是 Jpeg 跟 Png 這兩種格式,請自行審酌您要用哪一種。

設定 Tag
Image 拉進來之後,要不要給他們命名?不重要,因為在這個範例裡面,我們用來判別圖片的依據是tag,而不是 Image 的name。

請把要設定為一般狀態的 Image 元件的 tag 欄位填為 1,要設定為被點擊 (Pressed) 狀態的 Image元件的 tag 欄位填為 2。tag 欄位一樣可以從左下角的屬性檢視視窗找到,所有元件的 tag 預設值都是 0. (請參照上圖的下半部)

Tag 是物件導向程式設計當中相當重要的一個概念,當同一個 Class 在 RunTime 被建立出多個實體 (Instance),這些個實體是無法事先命名的,此時,Tag 就可以用來分辨到底是哪一個實體被點擊或被觸發了事件,在這個案例裡面,也會得到練習。

圖片沒有填滿整個 Button?
如果您的圖片沒有填滿整個Button的區域,此時上下左右一定有些空白區塊。請把 Image 設定成拉伸填滿即可,這個屬性是ImageWrapMode,在屬性檢視視窗的最後一個,請將它設定為 iwStretch,預設是 iwFit,只會上下拉伸或左右拉伸。
請選擇 iwStretch, 讓圖片能變形填滿整個 Image 的區塊

步驟三:把 TImage 拉進 TButton 裡面

聽起來很玄吧? 要怎麼把一個元件拉到另一個元件裡面?

從 Delphi 2005 以後,Delphi 的 IDE畫面就是現在這個模樣,左上角的視窗叫做 structure,也就是結構。我們可以從這個結構視窗裡面任意拉動元件,在這裡拉動元件,會改變元件的從屬關係,我們在結構視窗裡面把剛剛的 Image1 拖放到剛剛的 Button 元件上,看看發生了什麼事?
結構視窗拖拉元件的畫面 (拖拉中)
Delphi 當掉了,沒有回應!!! 這是 Delphi 做這個動作時還蠻常發生的事,在結構視窗裡拖拉元件前請先記得存檔啊!

正常情況下,Image1 會變成 Button 的子元件,如同畫面上的樹狀結構所顯示的。接著請把 Image2 也拉進去 Button 裡面。

這時候,您可以看到Button的小小範圍裡面出現了兩張圖,這兩張圖還重疊呢。
先別急,就是要它們重疊。先把兩張圖的 align 屬性 都設定成 alClient,也就是讓它們都佔滿整個Button 的顯示範圍。

align 屬性,請從屬性檢視視窗中尋找,應該會是 Image 元件的第一個屬性,您點擊其右半部,就會出現下拉式選單讓您選擇,找到 alClient,點擊它即可完成設定。

把 Image 的 align 屬性設定成 alClient

迫不及待想執行看看嗎?就執行吧,這時候的Button只會顯示最上面那張圖片,會是哪一張?我也不知道,端看您拖拉Image到Button裡面的時候,哪一張圖片在比較前方,這無法用直覺操作來設定,需要透過滑鼠右鍵的功能選單來設定。別急,距離完成還有幾個步驟。

先從結構視窗找到 Image2,把它的 Visible 屬性設定成 false,此時在IDE當中並不會有任何改變,這些屬性的變化只會在RunTime 反映在介面上。您可以按 F9 執行看看。

到了這裡,我們的Button上面有圖片,但對於點擊還沒有畫面上的反應。

步驟四:設定 TButton 的 OnMouseDown 跟 OnMouseUp 這兩個 eventHandler

接著,我們要來寫程式了!

請點擊 Button,並從元件檢視視窗中選擇 events 分頁,這裡會列出所有可以直接處理的事件,例如 onClick,onDblClick 等等。

請找到 onMouseDown 這個事件,目前它在視窗右半部是空的,請雙擊它。這樣就能讓 Delphi IDE 幫我們產生一個 event handler (事件處理常式),畫面也會切回程式碼編輯視窗了。

在 onMouseDown 的時候,我們要讓有按壓效果的圖片顯示,隱藏其他所有的圖片,但位於 Button 內部的圖片最好別每個元件的 name 都寫在程式碼裡面,不然,如果有 100 個按鍵,豈不是要寫 100 個 eventHandler?! 那可要人命了。

所以我用一個迴圈來解決,程式碼如下:
procedure TForm1.btnCheckInMouseDown(Sender: TObject;
   Button: TMouseButton; Shift: TShiftState; X, Y: Single);
var
   enumObj: TFMXObject;
   clickedBtn: TButton;
begin
   clickedBtn := Sender as TButton;

   for enumObj in clickedBtn.Children do begin
      TImage(enumObj).Visible := (enumObj.Tag = 2);
   end;
end;
 
procedure TForm1.Button1MouseUp(Sender: TObject; Button: TMouseButton;
  Shift: TShiftState; X, Y: Single);
var
   enumObj: TFMXObject;
   clickedBtn: TButton;
   selectedIdx: integer;
begin
   clickedBtn := Sender as TButton;

   for enumObj in clickedBtn.Children do begin
      TImage(enumObj).Visible := (enumObj.Tag = selectedIdx);
   end;
end; 
從上面的程式碼,可以看到,eventHandler 的參數是 Sender,型別是 TObject,這是 Delphi 的標準 eventHandler 宣告,在執行時,Sender就會是該事件被觸發的元件,例如我們有個最簡單的 onClick eventHandler,當它被呼叫時,Sender 參數就是被點擊的那個Button,也可以是被點擊的TLabel,或任何對該eventHandler進行了binding 的元件。

因此,雖然 Sender 的型別是TObject,但實際上可以轉型成 TButton,TLabel,當然也可以是 TImage。不過剛剛請您點選的是 Button 的 onMouseDown 事件,所以這裡的 Sender 是一個 TButton,不過也要請您留意,直接在 form 上面點元件的話,還是很可能點到放在 Button 裡面的 Image 哦,要小心,從結構視窗點擊比較準確。

第一行,我就把 Sender 轉型成 clickedButton,透過 as 語法,Sender 會被《當成》TButton,存放在 clickedButton 這個變數名稱中。

在 TFMXObject 裡面,都有個通用的屬性:Children,用來存放該元件所內含、收容的所有子元件,所以我們也能從這裡面找出剛剛拉進去的兩個 Image。

上頭的程式碼裡面,我用了for..in 迴圈,這是Delphi 2009 以後加到 Object Pascal 語言裡面的新語法,可以列舉某個 container 元件或資料結構裡面的所有內容物,進入到 for..in 之後,我們所取得的名為 enumObj 的元件,就是被收納在 clickedButton.Children 裡面的每個元件。

把元件一一找到了,接下來得分辨哪些是圖片,然後把按壓效果的圖片的Visible 屬性設成 true,其他的都設定成 false,這樣就能做出按鍵被壓下去的視覺效果了。

但是要先找出型別是 TImage 的元件,然後才設定其 Visible 屬性?還得多做一次判斷,這時候我們直接檢查 tag 就行了,前面提到過,預設的元件 tag 都是 0,只有我們拉進去的兩張圖片 tag 是 1跟2,所以超容易判斷,tag 等於 2 的元件, Visible 就是 true,其他的都是 false,夠簡單吧?

步驟五:完成一半了!來讓 image 不干擾 Button 的位置拖拉 (Design Time)

完成至此,Button在執行時期 (RunTime) 的效果大致完成了,但有時候介面設計的同仁或美術設計的同仁總是會天外飛來一筆,更改介面設計!

如果只是改張圖片,那算簡單的,如果位置、規則流程全變了,也是常有的事,但目前的Button還沒辦法直接用拖拉的方式調整,您試著拉拉看,現在應該只會拉到圖片,而且因為我們把圖片的align 屬性設定成 alClient 了,所以根本連圖片也拉不動了,如何是好?

在 FireMonkey 的元件當中有一個新的屬性,稱為 Locked,預設值是 false,也就是允許拖拉,我們現在把 Image的 Locked 屬性設定成 true,就可以把 Image 鎖定在現在的位置,可以讓我們在DesignTime 拖拉時,把拖拉的事件直接轉給父元件,這麼一來,就變成在拖拉 Button,而 Image 就變成了一張不會動彈的底圖了,很棒吧?!

設定 HitTest 跟 Locked.

別忘了,兩個 Image 元件的 Locked 屬性都設定為 true,這樣在 DesignTime 的拖拉動作,就可以直接拖拉 Button, 而不會誤拉到 Image 了.
正常狀態
點擊時狀態

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

步驟:讓底圖的點擊事件不干擾到 Button 的點擊事件

HitTest 這個屬性, 也是 FireMonkey 元件特有的, 這個屬性設定成 true 的時候, 該元件就會取得滑鼠點擊的事件, 我們現在不希望底圖有被點擊的機會, 以免干擾 Button 的所有事件, 所以要把兩個 Image HitTest 設定成 false,  這樣就可以把 Image 變成完全不會干擾 Button 事件的底圖了。

步驟:加個 TLabel,以不時之需

有時按鈕上的圖片只能是底圖的呈現, 如果按鈕上有需要顯示文字, 最好用 TLabel 來顯示, 這樣一來如果有多國文字要呈現, 才能用一套圖片, 以及準備好的多國文字直接完成多國語系的需求。

首先, 我們先重複前面幾個步驟:
  • 拉一個 TLabel 到 Form 上面
  • 把 TLabel 拉進 Button 元件裡面
  • 把 TLabel 拉到 Button 裡面適當的位置
  • 設定 TLabel 的 Tag 為 11
  • 修改 Button 的 onMouseDown 跟 onMouseUp
  • 設定 TLabel 的 Locked, HitTest 為 false
以下就是修改完成的 onMouseDown, onMouseUp 這兩個 eventHandler 的程式碼, 請注意, 這次加上了 Tag > 10 以上的元件也都要顯示喔, 至於是否要轉型成 TImage, 這段程式沒有影響, 因為並沒有直接存取到 enumObj 的屬性或方法, 只有對所有元件共通的 Visible 屬性進行修改, 所以不至於會造成 Access Violoation.

如果有要對其他屬性做修改的話, 記得要適當進行轉型後才行.

procedure TForm1.Button1MouseDown(Sender: TObject; Button: TMouseButton;
  Shift: TShiftState; X, Y: Single);
var
   enumObj: TFMXObject;
   clickedBtn: TButton;
begin
   clickedBtn := Sender as TButton;

   for enumObj in clickedBtn.Children do begin
      TImage(enumObj).Visible := (enumObj.Tag = 2) or (enumObj.Tag > 10);
   end;
end;

procedure TForm1.Button1MouseUp(Sender: TObject; Button: TMouseButton;
  Shift: TShiftState; X, Y: Single);
var
   enumObj: TFMXObject;
   clickedBtn: TButton;
   selectedIdx: integer;
begin
   clickedBtn := Sender as TButton;

   if clickedBtn.StaysPressed then selectedIdx := 2
   else selectedIdx := 1;
   for enumObj in clickedBtn.Children do begin
      TImage(enumObj).Visible := (enumObj.Tag = selectedIdx) or (enumObj.Tag > 10);
   end;
end;

完成這幾步以後,Label 就成為了 Button 裡面的一員,我們的 Button 裡面就可以顯示文字了.
 

步驟:完成了,如何在同一個 form 裡面複製這樣的 Button?

只要在結構視窗裡面, 先點選已經做好的 Button, 按下 Ctrl + C, 就像我們在 Windows 的其他任何的編輯器裡面進行複製一樣。

 接著點選我們要放置這個 Button 的元件, 例如在不同的 TabItem 裡面, 或者另一個 TRectangle 裡面, 點選好以後, 我們按下 Ctrl + V, 就像我們在 Windows 的其他任何編輯器裡面進行貼上的動作一樣

Button 就會被貼在該元件上面了, 這時候我們就可以編輯 Label 的文字內容, 拖拉 Button 的位置到適當的地方去, 跟一個原生的 TButton 元件一樣

總結

透過本篇的介紹,和大家分享了如何在 FireMonkey 架構下製作出內含有圖片、Label 的按鍵,在這個過程中,大家也一併了解、認識了幾個 Delphi 的重要概念:

  • FireMonkey 的所有視覺元件都是 Container, 具備包含其他元件的能力
  • FireMonkey 元件的 Locked 與 HitTest 這兩個屬性的特性與用途
  • Delphi 的 for..in 迴圈寫法
  • 元件當中的 Tag 屬性對於 Delphi 元件的重要性
  • Delphi 元件的 as 用法
  • 如何改變 FireMonkey 裡面的 TLabel 字體、大小,以及用RGB色碼改變 TLabel 的顏色

自我練習

本篇的介紹不一定適用於所有情況,有些時候Button 會因為美術人員的設定、企劃人員的規劃而有許多呈現上的變化,這時候大家就需要發揮創意對本篇的概念做一些微調,大致上可能有以下這幾種變化:

Label 的位置不一定正好在 Button 位置的正中間,需要跟畫面上的其他小圖示有相對位置的設定。
加上 MouseOver 的圖片效果時,要怎麼實作?
除了主要的圖片外,還有位置可能變動的小圖示,要怎麼處理?

請發揮您的想像力,想想上述這幾個狀況要如何修改程式或元件設定才能達成需求?