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

2023年7月28日 星期五

Delphi 開發 Linux 圖形介面程式: 以 Ubuntu 22.04 LTS 為遠端環境

Delphi + Linux

從 2002-2005,Delphi 還在 Borland 的那個年代, Borland 就已經開始著手想讓 Delphi 可以開發跨平台的應用程式了。在那個年代裡,Borland 以 Object Pascal 作為基礎語言,開發了 Kylix 這個整合開發環境,提供給開發人員可以開發 Linux 應用程式的工具。

然而好景不常,Kylix 3 在 2005 年之後,就沒有更新的版本,而 Delphi、C++ Builder 也在 2009 獨立出來由 CodeGear 維護與開發後續的版本,之後又歷經了 Embacadero, Idera 兩次的變革,直到 Delphi 10,才又提供了 Linux 開發的功能。

一開始 Delphi 10 提供 Linux 開發功能,僅限於 Linux 64bit 純文字模式,無法支援使用者圖形介面 (GUI),但 Delphi 的社群在全球仍然都很強大,隔不到一年,就有歐洲的團隊開發出了 FMXLinx 的套件,可以支援 GUI 程式的開發了。可惜的是,目前 Delphi 對於 Linux 的支援仍僅支援 64bit (AMD 或者 Intel 64), ARM 64 仍舊是不支援的。

但 Delphi 對於 MacOS 應用程式的支援較廣、較深,可以支援 Intel Chip, 也支援 Apple Chip 的機種,但 Apple Chip 的機種就等同於 ARM 64,所以 Delphi 支援 ARM 64 的 Compiler 理應不是什麼問題,如果 Delphi 能製作出 ARM 64 的 Linux 應用程式,那麼 Delphi 也就可以用來開發樹莓派或者其他 IoT 開發版使用的應用程式才對,我相信這一天不會太遠了,且讓我們拭目以待。

Linux 的環境多元

Linux 從 1990 年代首次發佈之後,全球各種社群、商業團體對它都有各自的愛好,所以也如同 Unix 的發展,有著各種不同面向的分支出現,有興趣的朋友可以參考以下的圖片,圖片是從這篇網路文章找到的。


目前在全球的使用者中,使用 Ubuntu 作為個人工作電腦的作業系統應該是所有 Linux 分支當中最多的,當然 Red Hat, CentOS, Fedora, Debian 也都不少,但 Ubuntu 應該還是比例最高的,佔了全球 Linux 使用率的 33.9% (資料來源: Enterprise Apps Today)。

所以,我們就用 Ubuntu 22.04 LTS 這個版本作為介紹的對象,這是筆者在 2023/7/24 下載 Ubuntu 的時候,Ubuntu 最新的官方穩定版本。

在 Linux 的作業系統生態中,安裝程式的工具軟體分為 apt/apt-get 跟 yum 兩大陣營,Debian 系列常使用 yum,CentOS, Ubuntu系列常使用 apt/apt-get 系列,兩個陣營安裝同樣的軟體套件時,套件的名稱或多或少會有點不太一樣,所以我先挑選了最多使用者的 Ubuntu,先涵蓋最多使用者的情境。

Embarcadero 的官方文件

Embacadero 的官方網站上,有提供 Linux 系統的開發環境設定(請點擊這裡),當中的大多數步驟都正確,但在網頁中的『準備你的 Linux 開發環境: Preparing Your Linux Development Environment』當中,所提到的安裝步驟,可能是適合 Ubuntu 20.04 的,在我安裝 Ubuntu 22.04 LTS 的時候,還是遇到了不少問題。

我安裝完成所有步驟之後,建立了一個多平台裝置應用程式,不做任何異動,只用 Delphi 初始建立的空專案來跑,就出現了 cannot find -lgcc_s 的錯誤,後來還出現過 cannot find -lz 等其他錯誤。

後來我發現 Linux 上面的連結程式 ld,除了基礎安裝的程式,也需要使用到 python 的函式庫,可是 Delphi PAServer 22.0 預設會認為系統上安裝的是 python 3.7, 但 Ubuntu 22.04 預設安裝的是 python 3.10, 所以 PAServer 22.0 尋找的函式庫檔案連結不存在,在 Delphi 裡面怎麼按 F9, 或者 Ctrl + F9 都沒有用。

Ubuntu 22.04 的逃生路徑

所以筆者直接整理了從乾淨的 Ubuntu 22.04 剛安裝好,到可以直接使用 PAServer 22.0 從 Delphi 連線過來讓 GUI 順利執行起來的所有安裝指令分享給大家,Delphi 上面的設定仍然請參考 Embacadero 的官方文件喔,但仍然有點需要注意的小地方待會再分享,以下就是所有指令:

1. sudo apt update && sudo apt upgrade
2. sudo apt install -y openssh-server
3. sudo systemctl restart ssh.service

1: 先把 Ubuntu apt 的軟體包更新、升級到最新.
2: 安裝 ssh server, 讓我們可以用 FileZilla, SSH terminal 連到 Ubuntu
3. 啟動 ssh server.

4. sudo apt install -y libgl1-mesa-glx libglu1-mesa libgtk-3-common libgstreamer1.0-0 libgstreamer-plugins-base1.0-0 g++ linux-libc-dev zlib1g zlib1g-dev

4: 安裝所有需要的函式庫, Embarcadero 官方文件寫的是 libgstreamer1.0, 但 Ubuntu 22.04 的 apt 用的是 1.0-0, 所以這兩個都需要調整一下名字.

5. ls /usr/lib/x86_64-linux-gnu/libpython3.10.so.1

5: 確認 python3.10.so.1 存在於上述路徑

把 LinuxPAServer22.0.tar.gz 從 Delphi 開發機上傳到 Ubuntu 裡,我自己是把它放在使用者 dennieschang 目錄的桌面上, 路徑為 /home/dennieschang/Desktop/LinuxPAServer22.0.tar.gz

請把這一行的 dennieschang 換成您 Ubuntu 系統登入的使用者帳號名稱

6. cd /home/dennieschang/Desktop
7. tar xzvf LinuxPAServer22.0.tar.gz

6: 切換到桌面
7: 把 LinuxPAServer22.0.tar.gz 解壓縮, 此時會解出一個名為PAServer-22.0 的目錄

8. unlink /home/dennieschang/Desktop/PAServer-22.0/lldb/lib/libpython3.so 

8: 把 PAServer 22.0 裡面預設要連結的 libpython3.so 解除連結

9. sudo ln -s /usr/lib/x86_64-linux-gnu/libpython3.10.so.1 /home/dennieschang/Desktop/PAServer-22.0/lldb/lib/libpython3.so

    9: 把系統中 /usr/lib/x86_64-linux-gnu/libpython3.10.so.1 連結為 /home/dennieschang/Desktop/PAServer-22.0/lldb/lib/libpthon3.so

到了這裡,請在 Ubuntu 桌面上打開一個 Terminal (終端機),切換到 /home/dennieschang/Desktop/PAServer-22.0,然後執行 sudo ./paserver

按下 Enter 之後,可能會詢問您 root 的密碼,輸入 root 密碼後,則會請您設定 PAServer 的驗證密碼 (由您自行設定).

只有第一次要使用 sudo ./paserver,第一次如果沒有透過 sudo 來執行,Delphi 在透過 Connection Profile 連線到 Linux 的時候,可能會出現 DS Authentication Error 的錯誤訊息。

但用了第一次 sudo ./paserver 之後,之後就不用再 sudo 了,用您登入的帳號來執行 paserver 就行,而此時 Delphi 透過 Linux profile 傳送到 Linux 的應用程式,會被儲存在 /home/dennieschang/PAServer/scratch-files/您Windows上Profile名稱/ 這個目錄中,之後就無須再透過 Delphi,可以直接從 Ubuntu 執行這個應用程式了.

Parallels Desktop 注意事項

如果您使用的開發機是 Apple Chip 的 Mac 電腦,例如 M1 或者 M2 晶片系列。像筆者使用的是 Macbook Pro 2022 M2,目前是 2023 7月,我過去十多年慣用的 VMWare Fusion 還沒有 M1/M2 上面完整支援 Windows 11 Arm 的版本,所以我只好又買了一套 Parallels Desktop 18 來跑 Windows 11 Arm (不用想 Windows 10 Arm 了, 看起來微軟沒有要做這個了),然後在 Windows 11 Arm 上面安裝 Delphi Alexandria (仍然是 X86 版本喔),Windows 11 Arm 上面可以執行 Windows 32 的應用程式,除了驅動程式之外都可以。

Parallels Desktop 有一個壞處,就是預設會讓使用者把 MacOS 上面的桌面、文件等等預設的資料夾分享在 Parallels Desktop 安裝的 Windows 裡面。這個功能乍聽之下很貼心,為什麼會是壞處呢?

因為這樣的分享,在 Windows 11 Arm 裡面,會讓原本 C:\ProgramData\Users\我的使用者\Documents 這樣的路徑,變成 //Mac/home/dennieschang/Documents

而這樣的路徑,在 Windows 命令模式下,會被認為是『參數』,而非路徑,所以 ld 會找不到函式庫。

所以,如果您也是使用 Parallels Desktop 來跑 Windows 11 Arm + Delphi 的話,請記得在 Parallels Desktop 的設定當中,取消讓 Windows 使用 MacOS 的文件,取消後,再重新從 Linux 系統中下載一次 SDK 即可.

如果您也正在處理用 Delphi 開發 Linux 的環境, 提供上述的流程與指令給您參考.

2015年5月11日 星期一

Delphi XE7, XE8 在 Windows XP 上面佈建時要注意的事項

緣起

之前有個專案, 目標平台是 Windows XP, 這個專案的 Scope 是要把原本記載在 XP 上面的 Access MDB 檔案資料, 透過 Someway 回傳到 Web server 上, 好讓管理人員能夠 Centralize 管理各個不同節點的資料.

好死不死, 我的 Windows 開發系統早從 2010 年起就已經全面 VM 化, 且從那時候就一直保留在 Windows 7 上面, 因為真的又快又穩定.

直到 2014 年, 我的開發系統已經是 Windows 7 + Delphi XE7, 所以對於 Windows XP 已經有種日薄西山的感覺.

在開發這個系統的過程當中, 並沒有特別注意什麼, 就很順利的用 FireDAC 連接遠端的 MySQL server,  Client 端用 idHTTPServer 作為 WebServer, 中央的 Web Server 在用戶從網頁進行資料同步索取的時候, 發一個 jquery 向 idHTTPServer 指定要索取的資料的日期、並且給 session string, 以及資料的 offset number.

IdHTTPServer 在接到 Request 的時候, 透過 TIdCommander, 建立一個 Thread, 把 server 端傳來的資料 select 出來, 然後一筆一筆寫成單一一個 Insert 指令, 用 FireDAC 連接 MySQL 資料庫, 直接執行 Insert 指令, 把資料全寫進去.

在 Windows 7, Windows 8 上面, 執行的超快樂的, 去年的實習生對於這種架構, 完全就是看傻了眼, 壓根沒想過能這樣搞, 但跑的超順的, 他也 Happy 的把 PHP 程式搞定了.


直到去幫業主做 Deployment 的時候, 這下是我傻眼了, 四個營業點, 用的系統都是 Windows XP, 其中三個更是 Windows XP HOME.......

看倌們應該有些已經跟我一樣, 心裡咒罵連連了吧.
Windows XP 也就算了, Windows XP "HOME", 這就是說, 系統一開始就沒打算讓外面的機器或網路能夠連進去 (當然網路芳鄰可能是唯一例外吧, 是叫我們這些寫 TCP/IP Protocol 去跳樓嗎?)

和業主進行反映之後, 業主先跟設備商詢問是否有升級為 Windows Vista/Windows 7/Windows 8的可能性?

設備商的反應是: 沒辦法, 因為他們的 VB6 程式, 在控制實體設備的時候, 用的是 Windows Message 來傳遞同步資訊跟指令的, 他們不會, 也沒有打算升級到 Windows XP 之後的技術.

這一點, 筆者在 2008 年有吃到苦頭, 在 2 個月當中, 把原本用來做 IPC (Inter-Process Communication) 的 Windows Message 改成 Named Pipe, 這是在 Vista 把 Windows 的 Session 改為真實多使用者 Session 之後的重大變革.

在 Windows XP 當中, 雖然可以有多使用者的假象, 但實際上, 所有登入、登出的使用者都還是共用同一個 Session, 也就是 Session 0, 因此驅動程式、病毒、駭客, 都很熟悉透過 Windows Message 來傳遞訊息.

回憶 2007 年的時候, Vista 剛推出,Nvidia, AMD 哀鴻遍野, 因為這兩家視訊卡大廠的驅動程式跟工具程式也是完全透過 Windows Message 作為傳遞介面跟工具程式之間訊息的管道, 直到 2008 年中, 整整過了半年多, 新版能支援 Vista 的驅動程式跟工具程式才 release...

WIC 元件


這是多說的了, 回頭說, 在 Delphi XE7 的 FireMonkey, 要 Deploy 到 Windows XP 的時候, 可能會遇到直接 Crash 的狀況, 我自己也在此卡關許久, 後來發現, 原來是需要自己在Windows XP 上面, 安裝 WIC 元件, 這個元件的取得路徑如下:

http://www.microsoft.com/zh-CN/download/confirmation.aspx?id=32

只有簡體中文版的網頁, 但下載回來, 安裝的都是 enu, 也就是英文版, 反正沒有什麼複雜的操作要進行, 不斷的按下一步就行了.

這個情形會發生在一執行的時候, 而且沒有什麼訊息提示, 就跟一般我們遇到 Memory Access Violation 的時候差不多, 只會在 XP 上面說要回報給 Microsoft, 但 Microsoft 已經停止了對 Windows 的支援, 所以回傳也沒什麼幫助.

我是後來想了很久, 覺得不對, 在透過 PAServer 從 Windows 8 VM 去連 Windows XP VM做遠端偵錯的時候, 才從 Delphi Debug message 裡面找到了這個錯誤訊息, 不然真的不知道為什麼會 Crash.....

我懷疑過的點挺多的, 例如 MDAC 版本不對, MySQL dll 版本不對, 什麼都試過了, 但是在業主的營業點沒得測試, 所以這次用完全乾淨的 Windows XP Pro 來做這個驗證, 得到這個經驗, 或許遇到這情形的先進不多, 但他山之石, 可以攻錯, 所以也把這個體驗留在這個園地, 讓大家有需要的時候可以互相分享一下.

2015年1月6日 星期二

如何用 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)

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 以後的版本應該不用再擔心了,真是太好了。

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 的圖片效果時,要怎麼實作?
除了主要的圖片外,還有位置可能變動的小圖示,要怎麼處理?

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