在所有SWT組件中,Button幾乎是最常用的,其功能在對于一般的情況來說也足夠豐富了。你可以為Button組件設置要顯示在其中的文本或者圖像、設定ToolTip,甚至只要修改一個風格樣式就能得到一個看上去相當不錯的方向箭頭按鈕。
然而,我對Button組件還是不能感到滿意。最大的遺憾就是:對它的外觀,所能做的工作也就僅限于此了。如果你想讓按鈕擁有一個漂亮的、漸變色的背景和一些特殊的文字效果,怎么辦呢?答案是沒有辦法。Button類里面似乎沒有任何方法提供我想要的功能。
我曾嘗試過的第一個想法是用Button.addPaintListener來修改按鈕的外觀。但是,結果令人失望——雖然它顯示出來的時候的確按照預 想進行繪制了,但是當你用鼠標去按它的時候,馬上又變回了原本灰頭土臉的樣子。顯然,在按下按鈕的時候,它并不是觸發paint事件,而是按照自己的想法 畫出原本的按鈕,于是我的工作全部白費了。
如果嘗試為按鈕設定圖像會怎么樣呢?這也不是一個好主意。首先,不管你選擇什么樣的圖像, 都沒辦法去掉按鈕四周的邊框,而正是這些邊框嚴重破壞了圖像的和諧感;其次,如果你的程序有幾十甚至上百個按鈕,為每個按鈕都維護一幅圖像(甚至更多—— 理論上每個按鈕在普通狀態和被按下、禁用的狀態下,甚至當鼠標移進移出按鈕的時候,都應當顯示不同的圖像)明顯是在浪費系統資源;如果你們的美工聽說需要 做幾百個圖片,大概也不會給你好臉色看。此外,圖像有一個嚴重的缺點是:它所擁有的像素數目是固定的,難以隨著界面的放大和縮小同時變化。如果強制進行縮 放的話,會出現明顯的鋸齒和失真,最終讓你精心設計的窗口變得慘不忍睹。最好還是放棄這個想法。
如果以Canvas為基礎,設計一個 偽裝的按鈕組件又如何呢?聽起來好像很不錯,因為采用這種辦法的話,我們對如何繪制組件的表面就有了完整的控制權。不過這也意味著你必須對按鈕的狀態進行 手工維護。雖然Button本身是一個很簡單的組件,但是重復去做標準按鈕已經作好的工作似乎還是有點無謂。還有一件事情是應當考慮的:我們知道, JFace中的Action機制可以將標準按鈕、菜單項和工具欄按鈕這三種界面組件納入一個統一的事件處理體系。然而,如果我們從Canvas派生去模擬 一個按鈕的話,不論你模擬到多么相似的地步,它畢竟不是一個真正的Button,Action也不會給它同等的待遇。也就是說手工制作的按鈕無法和 JFace Action體系協同工作——除非你去修改Action的處理方法,讓它去接納新的按鈕對象。這可不是一件輕松的工作。
如果上面的方法都行不通的話,應當怎么辦呢?我們知道,和Swing這樣的框架不同,SWT中的按鈕其實就是操作系統底層所實現的按鈕(這一點也可以用 SPY++或者Winsight32之類的工具證實)。同時我們也知道,操作系統——至少是Windows系統,對按鈕已經提供了自我繪制的機制,這就是 所謂的Owner Draw(稱為所有者繪制的原因是因為默認情況下繪制消息是發送給按鈕的父窗口處理的,但是父窗口也可以把這個皮球再踢回給按鈕,讓它自己解決)。在 Win32 API中,凡是使用BS_OWNERDRAW風格創建、并且能夠(通過消息反射)響應WS_DRAWITEM消息的按鈕,都可以獲得這種定制的能力。 了解這一點,接下來的任務就是研究Button組件有沒有開放這個接口供我們修改了。對Button組件的源代碼進行粗略的瀏覽后,我發現了如下的方法:
其中DRAWITEMSTRUCT結構的出現是一個明顯的提示:這里就是WM_DRAWITEM消息的響應函數,很幸運它沒有聲明為final的,只要重載它并提供自己的實現就行了。
看起來是個小case,實際上也是。不過,還有一處小麻煩需要克服。注意wmDrawChild方法沒有使用任何訪問限定符,這意味著它是 package friendly的——同一個包中的對象可以訪問和重載此方法,其他包中的對象就沒有這個權力了。也就是說,要定制按鈕對象,我們新建的對象也需要放在同 一個包(org.eclipse.swt.widgets)中。看起來有點像在使用Hack手段,不過為了突破SWT給我們的限制,眼下也只好稍稍將就一 下。好在swt的包沒有密封(Sealed),不然我就不得不再次宣稱此路不通了。
既然障礙已經掃清,接下來我們可以來實現前面的想 法了。這里我做了一個決定,在上述包中只加入一個抽象類,目的是把必要的接口暴露出來;至于如何繪制按鈕,則留給具體的按鈕對象根據應用程序的需求來決 定。這樣,不管你希望實現Windows XP風格的按鈕、還是卡通風格的按鈕、或是平面樣式的,總之不論什么千奇百怪的風格,只要繼承一個類并重載一個繪制方法就行了,而不必每次都要和 Button類的內部打交道。
基于這種考慮,實現自繪按鈕的抽象類如下:
注意這個抽象類所作的工作。在構造函數中,它調用操作系統方法為自己加入了BS_OWNERDRAW風格。如果沒有這一步,那么操作系統將不 會把這個按鈕視為自繪的按鈕,也不會向其發送任何繪制消息。接下來是WM_DRAWITEM消息的響應函數。在這個函數中,我們簡單的把必要的繪制參數提 取出來,然后調用抽象方法ownerDraw去進行實際的繪制工作。任何從OwnerDrawButton類派生的按鈕對象必須重載此ownerDraw 方法,來決定如何繪制自身。
作為一個例子,我實現了一個具體的按鈕類。這個按鈕用從上至下的漸變色背景添充整個按鈕,然后繪制出按鈕的文字。 如果當前按鈕被按下,該類還調整了一下文字的位置,以顯示出“按下”的外觀效果。代碼稍微有些長,這是因為消息函數所提供的是一個操作系統才了解的原生 HDC對象,而不是我們所熟悉的GC類,因此也需要相應的用原生API進行處理。不過,其原理是相當簡單的——你只需要在給出的HDC上畫出你想要的任何 效果就行了。
如果你使用的是JDK 1.4或者更低的版本,請把@Override標記去掉以后才能編譯,因為這是一個Java 5.0中才有的特性。此外,我重載了checkSubclass方法并提供了一個空的實現;如果不這么做的話,那么SWT在默認情況下是不允許你從Button類繼承的。
這個地方請允許我稍稍跑一下題。上面代碼中的fillGradientRectangle方法——從它的名字你大概可以猜到,這個方法的作用是畫出一個 漸變色的矩形區域。我是從GC.fillGradientRectangle中“偷”來的代碼,針對按鈕類作了一些修改就可以了。讓我感到訝異的是,在整 理這段代碼的時候,我發現從SWT中調用Win32 API實在是太方便了——比我原先猜想的還要容易得多。即便是微軟的P/Invoke也要比這麻煩。當然,這很大程度上要歸功于SWT將系統函數很好的封 裝在了一個OS靜態類中。(如果你不知道P/Invoke是什么的話,簡單的說它就是微軟在.Net平臺中提供的、用來調用系統API和自定義DLL中的 方法的技術)。
上面那些繪圖的代碼基本上是Windows SDK的編程風格。因為我本人有很多這方面的開發經驗,所以這些代碼對我來說是相當清晰且直觀的。不過我估計純粹的Java程序員或許對這段代碼不會有很 大的好感。理論上講,我可以把這些代碼用更加OO的方式包裝起來,從而看上去能好看一些。不過,本文的目的在于講述實現技術,用包裝的話反而會破壞效果。 如果你感興趣的話,也可以嘗試自己來包裝一下。
需要講解的地方到這里就全部結束了。為了完整起見,我把程序框架類的代碼也列在下面,但是不做什么說明——基本上每個SWT程序中這段代碼都是大同小異的。
下面是程序運行的界面。盡管這遠遠算不上完美——真正的按鈕還應該考慮,是否能夠和用戶的任何配置下,特別是有窗口主題的時候也能正常工作? 完美的按鈕實現可能需要至少數百行的代碼才行。不過對本文的目的來說,這樣已經足夠了。可惜的是按下按鈕的效果無法從圖中體現;你可以自己運行一下這個程 序來體驗一下實際的感覺。
然而,我對Button組件還是不能感到滿意。最大的遺憾就是:對它的外觀,所能做的工作也就僅限于此了。如果你想讓按鈕擁有一個漂亮的、漸變色的背景和一些特殊的文字效果,怎么辦呢?答案是沒有辦法。Button類里面似乎沒有任何方法提供我想要的功能。
我曾嘗試過的第一個想法是用Button.addPaintListener來修改按鈕的外觀。但是,結果令人失望——雖然它顯示出來的時候的確按照預 想進行繪制了,但是當你用鼠標去按它的時候,馬上又變回了原本灰頭土臉的樣子。顯然,在按下按鈕的時候,它并不是觸發paint事件,而是按照自己的想法 畫出原本的按鈕,于是我的工作全部白費了。
如果嘗試為按鈕設定圖像會怎么樣呢?這也不是一個好主意。首先,不管你選擇什么樣的圖像, 都沒辦法去掉按鈕四周的邊框,而正是這些邊框嚴重破壞了圖像的和諧感;其次,如果你的程序有幾十甚至上百個按鈕,為每個按鈕都維護一幅圖像(甚至更多—— 理論上每個按鈕在普通狀態和被按下、禁用的狀態下,甚至當鼠標移進移出按鈕的時候,都應當顯示不同的圖像)明顯是在浪費系統資源;如果你們的美工聽說需要 做幾百個圖片,大概也不會給你好臉色看。此外,圖像有一個嚴重的缺點是:它所擁有的像素數目是固定的,難以隨著界面的放大和縮小同時變化。如果強制進行縮 放的話,會出現明顯的鋸齒和失真,最終讓你精心設計的窗口變得慘不忍睹。最好還是放棄這個想法。
如果以Canvas為基礎,設計一個 偽裝的按鈕組件又如何呢?聽起來好像很不錯,因為采用這種辦法的話,我們對如何繪制組件的表面就有了完整的控制權。不過這也意味著你必須對按鈕的狀態進行 手工維護。雖然Button本身是一個很簡單的組件,但是重復去做標準按鈕已經作好的工作似乎還是有點無謂。還有一件事情是應當考慮的:我們知道, JFace中的Action機制可以將標準按鈕、菜單項和工具欄按鈕這三種界面組件納入一個統一的事件處理體系。然而,如果我們從Canvas派生去模擬 一個按鈕的話,不論你模擬到多么相似的地步,它畢竟不是一個真正的Button,Action也不會給它同等的待遇。也就是說手工制作的按鈕無法和 JFace Action體系協同工作——除非你去修改Action的處理方法,讓它去接納新的按鈕對象。這可不是一件輕松的工作。
如果上面的方法都行不通的話,應當怎么辦呢?我們知道,和Swing這樣的框架不同,SWT中的按鈕其實就是操作系統底層所實現的按鈕(這一點也可以用 SPY++或者Winsight32之類的工具證實)。同時我們也知道,操作系統——至少是Windows系統,對按鈕已經提供了自我繪制的機制,這就是 所謂的Owner Draw(稱為所有者繪制的原因是因為默認情況下繪制消息是發送給按鈕的父窗口處理的,但是父窗口也可以把這個皮球再踢回給按鈕,讓它自己解決)。在 Win32 API中,凡是使用BS_OWNERDRAW風格創建、并且能夠(通過消息反射)響應WS_DRAWITEM消息的按鈕,都可以獲得這種定制的能力。 了解這一點,接下來的任務就是研究Button組件有沒有開放這個接口供我們修改了。對Button組件的源代碼進行粗略的瀏覽后,我發現了如下的方法:
package org.eclipse.swt.widgets; public class Button extends Control { … LRESULT wmDrawChild (int wParam, int lParam) { if ((style & SWT.ARROW) == 0) return super.wmDrawChild (wParam, lParam); DRAWITEMSTRUCT struct = new DRAWITEMSTRUCT (); .... |
其中DRAWITEMSTRUCT結構的出現是一個明顯的提示:這里就是WM_DRAWITEM消息的響應函數,很幸運它沒有聲明為final的,只要重載它并提供自己的實現就行了。
看起來是個小case,實際上也是。不過,還有一處小麻煩需要克服。注意wmDrawChild方法沒有使用任何訪問限定符,這意味著它是 package friendly的——同一個包中的對象可以訪問和重載此方法,其他包中的對象就沒有這個權力了。也就是說,要定制按鈕對象,我們新建的對象也需要放在同 一個包(org.eclipse.swt.widgets)中。看起來有點像在使用Hack手段,不過為了突破SWT給我們的限制,眼下也只好稍稍將就一 下。好在swt的包沒有密封(Sealed),不然我就不得不再次宣稱此路不通了。
既然障礙已經掃清,接下來我們可以來實現前面的想 法了。這里我做了一個決定,在上述包中只加入一個抽象類,目的是把必要的接口暴露出來;至于如何繪制按鈕,則留給具體的按鈕對象根據應用程序的需求來決 定。這樣,不管你希望實現Windows XP風格的按鈕、還是卡通風格的按鈕、或是平面樣式的,總之不論什么千奇百怪的風格,只要繼承一個類并重載一個繪制方法就行了,而不必每次都要和 Button類的內部打交道。
基于這種考慮,實現自繪按鈕的抽象類如下:
package org.eclipse.swt.widgets; import org.eclipse.swt.internal.win32.*; public abstract class OwnerDrawButton extends Button { public OwnerDrawButton( Composite parent, int style ) { super( parent, style ); int osStyle = OS.GetWindowLong( handle, OS.GWL_STYLE ); osStyle |= OS.BS_OWNERDRAW; OS.SetWindowLong( handle, OS.GWL_STYLE, osStyle ); } LRESULT wmDrawChild( int wParam, int lParam ) { super.wmDrawChild( wParam, lParam ); DRAWITEMSTRUCT struct = new DRAWITEMSTRUCT(); OS.MoveMemory( struct, lParam, DRAWITEMSTRUCT.sizeof ); ownerDraw( struct ); return null; } protected abstract void ownerDraw( DRAWITEMSTRUCT dis ); } |
注意這個抽象類所作的工作。在構造函數中,它調用操作系統方法為自己加入了BS_OWNERDRAW風格。如果沒有這一步,那么操作系統將不 會把這個按鈕視為自繪的按鈕,也不會向其發送任何繪制消息。接下來是WM_DRAWITEM消息的響應函數。在這個函數中,我們簡單的把必要的繪制參數提 取出來,然后調用抽象方法ownerDraw去進行實際的繪制工作。任何從OwnerDrawButton類派生的按鈕對象必須重載此ownerDraw 方法,來決定如何繪制自身。
作為一個例子,我實現了一個具體的按鈕類。這個按鈕用從上至下的漸變色背景添充整個按鈕,然后繪制出按鈕的文字。 如果當前按鈕被按下,該類還調整了一下文字的位置,以顯示出“按下”的外觀效果。代碼稍微有些長,這是因為消息函數所提供的是一個操作系統才了解的原生 HDC對象,而不是我們所熟悉的GC類,因此也需要相應的用原生API進行處理。不過,其原理是相當簡單的——你只需要在給出的HDC上畫出你想要的任何 效果就行了。
import org.eclipse.swt.SWT; import org.eclipse.swt.graphics.*; import org.eclipse.swt.internal.win32.*; import org.eclipse.swt.widgets.*; public class TestButton extends OwnerDrawButton { TestButton( Composite parent ) { super( parent, SWT.PUSH ); } @Override protected void ownerDraw( DRAWITEMSTRUCT dis ) { Rectangle rc = new Rectangle( dis.left, dis.top, dis.right - dis.left,dis.bottom - dis.top ); Color clr1 = new Color( getDisplay(), 0, 255, 128 ); Color clr2 = new Color( getDisplay(), 0, 128, 255 ); fillGradientRectangle( dis.hDC, rc, true, clr1, clr2 ); clr1.dispose(); clr2.dispose(); SIZE size = new SIZE(); String text = getText(); char[] chars = text.toCharArray(); int oldFont = OS.SelectObject( dis.hDC, getFont().handle ); OS.GetTextExtentPoint32W( dis.hDC, chars, chars.length, size ); RECT rcText = new RECT(); rcText.left = rc.x; rcText.top = rc.y; rcText.right = rc.x + rc.width; rcText.bottom = rc.y + rc.height; if ( (dis.itemState & OS.ODS_SELECTED) != 0 ) OS.OffsetRect( rcText, 1, 1 ); OS.SetBkMode( dis.hDC, OS.TRANSPARENT ); OS.DrawTextW( dis.hDC, chars, -1, rcText, OS.DT_SINGLELINE | OS.DT_CENTER | OS.DT_VCENTER ); OS.SelectObject( dis.hDC, oldFont ); } private void fillGradientRectangle( int handle, Rectangle rc,boolean vertical, Color clr1, Color clr2 ) { final int hHeap = OS.GetProcessHeap(); final int pMesh = OS.HeapAlloc( hHeap, OS.HEAP_ZERO_MEMORY,GRADIENT_RECT.sizeof + TRIVERTEX.sizeof * 2 ); final int pVertex = pMesh + GRADIENT_RECT.sizeof; GRADIENT_RECT gradientRect = new GRADIENT_RECT(); gradientRect.UpperLeft = 0; gradientRect.LowerRight = 1; OS.MoveMemory( pMesh, gradientRect, GRADIENT_RECT.sizeof ); TRIVERTEX trivertex = new TRIVERTEX(); trivertex.x = rc.x; trivertex.y = rc.y; trivertex.Red = (short)(clr1.getRed() << 8); trivertex.Green = (short)(clr1.getGreen() << 8); trivertex.Blue = (short)(clr1.getBlue() << 8); trivertex.Alpha = -1; OS.MoveMemory( pVertex, trivertex, TRIVERTEX.sizeof ); trivertex.x = rc.x + rc.width; trivertex.y = rc.y + rc.height; trivertex.Red = (short)(clr2.getRed() << 8); trivertex.Green = (short)(clr2.getGreen() << 8); trivertex.Blue = (short)(clr2.getBlue() << 8); trivertex.Alpha = -1; OS.MoveMemory( pVertex + TRIVERTEX.sizeof, trivertex, TRIVERTEX.sizeof ); boolean success = OS.GradientFill( handle, pVertex, 2, pMesh, 1,vertical ? OS.GRADIENT_FILL_RECT_V : OS.GRADIENT_FILL_RECT_H ); OS.HeapFree( hHeap, 0, pMesh ); if ( success ) return; } @Override protected void checkSubclass() {} } |
如果你使用的是JDK 1.4或者更低的版本,請把@Override標記去掉以后才能編譯,因為這是一個Java 5.0中才有的特性。此外,我重載了checkSubclass方法并提供了一個空的實現;如果不這么做的話,那么SWT在默認情況下是不允許你從Button類繼承的。
這個地方請允許我稍稍跑一下題。上面代碼中的fillGradientRectangle方法——從它的名字你大概可以猜到,這個方法的作用是畫出一個 漸變色的矩形區域。我是從GC.fillGradientRectangle中“偷”來的代碼,針對按鈕類作了一些修改就可以了。讓我感到訝異的是,在整 理這段代碼的時候,我發現從SWT中調用Win32 API實在是太方便了——比我原先猜想的還要容易得多。即便是微軟的P/Invoke也要比這麻煩。當然,這很大程度上要歸功于SWT將系統函數很好的封 裝在了一個OS靜態類中。(如果你不知道P/Invoke是什么的話,簡單的說它就是微軟在.Net平臺中提供的、用來調用系統API和自定義DLL中的 方法的技術)。
上面那些繪圖的代碼基本上是Windows SDK的編程風格。因為我本人有很多這方面的開發經驗,所以這些代碼對我來說是相當清晰且直觀的。不過我估計純粹的Java程序員或許對這段代碼不會有很 大的好感。理論上講,我可以把這些代碼用更加OO的方式包裝起來,從而看上去能好看一些。不過,本文的目的在于講述實現技術,用包裝的話反而會破壞效果。 如果你感興趣的話,也可以嘗試自己來包裝一下。
需要講解的地方到這里就全部結束了。為了完整起見,我把程序框架類的代碼也列在下面,但是不做什么說明——基本上每個SWT程序中這段代碼都是大同小異的。
import org.eclipse.swt.layout.FillLayout; import org.eclipse.swt.widgets.*; public class Application { public static void main( String[] args ) { Display display = Display.getDefault(); Shell shell = new Shell( display ); init( shell ); shell.pack(); shell.open(); while ( !shell.isDisposed() ) { if ( !display.readAndDispatch() ) display.sleep(); } } private static void init( Shell shell ) { shell.setText( "Owner Draw Button Test" ); FillLayout layout = new FillLayout(); layout.marginWidth = layout.marginHeight = 8; shell.setLayout( layout ); Button btn = new TestButton( shell ); btn.setText( "Owner Draw Button" ); btn.setToolTipText( "Hello, I'm a OwnerDraw Button!" ); } } |
下面是程序運行的界面。盡管這遠遠算不上完美——真正的按鈕還應該考慮,是否能夠和用戶的任何配置下,特別是有窗口主題的時候也能正常工作? 完美的按鈕實現可能需要至少數百行的代碼才行。不過對本文的目的來說,這樣已經足夠了。可惜的是按下按鈕的效果無法從圖中體現;你可以自己運行一下這個程 序來體驗一下實際的感覺。