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

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

沒有留言:

張貼留言