在所有SWT組件中
Button幾乎是最常用的
其功能在對於一般的情況來說也足夠豐富了
你可以為Button組件設置要顯示在其中的文本或者圖像
設定ToolTip
甚至只要修改一個風格樣式就能得到一個看上去相當不錯的方向箭頭按鈕
然而
我對Button組件還是不能感到滿意
最大的遺憾就是
對它的外觀
所能做的工作也就僅限於此了
如果你想讓按鈕擁有一個漂亮的
漸變色的背景和一些特殊的文字效果
怎麼辦呢?答案是沒有辦法
Button類裡面似乎沒有任何方法提供我想要的功能
我曾嘗試過的第一個想法是用Button
addPaintListener來修改按鈕的外觀
但是
結果令人失望——雖然它顯示出來的時候的確按照預想進行繪制了
但是當你用鼠標去按它的時候
馬上又變回了原本灰頭土臉的樣子
顯然
在按下按鈕的時候
它並不是觸發paint事件
而是按照自己的想法畫出原本的按鈕
於是我的工作全部白費了
如果嘗試為按鈕設定圖像會怎麼樣呢?這也不是一個好主意
首先
不管你選擇什麼樣的圖像
都沒辦法去掉按鈕四周的邊框
而正是這些邊框嚴重破壞了圖像的和諧感
其次
如果你的程序有幾十甚至上百個按鈕
為每個按鈕都維護一幅圖像(甚至更多——理論上每個按鈕在普通狀態和被按下
禁用的狀態下
甚至當鼠標移進移出按鈕的時候
都應當顯示不同的圖像)明顯是在浪費系統資源
如果你們的美工聽說需要做幾百個圖片
大概也不會給你好臉色看
此外
圖像有一個嚴重的缺點是
它所擁有的像素數目是固定的
難以隨著界面的放大和縮小同時變化
如果強制進行縮放的話
會出現明顯的鋸齒和失真
最終讓你精心設計的窗口變得慘不忍睹
最好還是放棄這個想法
如果以Canvas為基礎
設計一個偽裝的按鈕組件又如何呢?聽起來好像很不錯
因為采用這種辦法的話
我們對如何繪制組件的表面就有了完整的控制權
不過這也意味著你必須對按鈕的狀態進行手工維護
雖然Button本身是一個很簡單的組件
但是重復去做標准按鈕已經作好的工作似乎還是有點無謂
還有一件事情是應當考慮的
我們知道
JFace中的Action機制可以將標准按鈕
菜單項和工具欄按鈕這三種界面組件納入一個統一的事件處理體系
然而
如果我們從Canvas派生去模擬一個按鈕的話
不論你模擬到多麼相似的地步
它畢竟不是一個真正的Button
Action也不會給它同等的待遇
也就是說手工制作的按鈕無法和JFace Action體系協同工作——除非你去修改Action的處理方法
讓它去接納新的按鈕對象
這可不是一件輕松的工作
如果上面的方法都行不通的話
應當怎麼辦呢?我們知道
和Swing這樣的框架不同
SWT中的按鈕其實就是操作系統底層所實現的按鈕(這一點也可以用SPY++或者Winsight
之類的工具證實)
同時我們也知道
操作系統——至少是Windows系統
對按鈕已經提供了自我繪制的機制
這就是所謂的Owner Draw(稱為所有者繪制的原因是因為默認情況下繪制消息是發送給按鈕的父窗口處理的
但是父窗口也可以把這個皮球再踢回給按鈕
讓它自己解決)
在Win
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) ==
) 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
win
*;
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 orgeclipseswtSWT;
import orgeclipseswtgraphics*;
import orgeclipseswtinternalwin*;
import orgeclipseswtwidgets*;
public class TestButton extends OwnerDrawButton
{
TestButton( Composite parent )
{
super( parent SWTPUSH );
}
@Override
protected void ownerDraw( DRAWITEMSTRUCT dis )
{
Rectangle rc = new Rectangle( disleft distop disright disleftdisbottom distop );
Color clr = new Color( getDisplay() );
Color clr = new Color( getDisplay() );
fillGradientRectangle( dishDC rc true clr clr );
clrdispose();
clrdispose();
SIZE size = new SIZE();
String text = getText();
char[] chars = texttoCharArray();
int oldFont = OSSelectObject( dishDC getFont()handle );
OSGetTextExtentPointW( dishDC chars charslength size );
RECT rcText = new RECT();
rcTextleft = rcx;
rcTexttop = rcy;
rcTextright = rcx + rcwidth;
rcTextbottom = rcy + rcheight;
if ( (emState & OSODS_SELECTED) != )
OSOffsetRect( rcText );
OSSetBkMode( dishDC OSTRANSPARENT );
OSDrawTextW( dishDC chars rcText OSDT_SINGLELINE | OSDT_CENTER | OSDT_VCENTER );
OSSelectObject( dishDC oldFont );
}
private void fillGradientRectangle( int handle Rectangle rcboolean vertical Color clr Color clr )
{
final int hHeap = OSGetProcessHeap();
final int pMesh = OSHeapAlloc( hHeap OSHEAP_ZERO_MEMORYGRADIENT_RECTsizeof + TRIVERTEXsizeof * );
final int pVertex = pMesh + GRADIENT_RECTsizeof;
GRADIENT_RECT gradientRect = new GRADIENT_RECT();
gradientRectUpperLeft = ;
gradientRectLowerRight = ;
OSMoveMemory( pMesh gradientRect GRADIENT_RECTsizeof );
TRIVERTEX trivertex = new TRIVERTEX();
trivertexx = rcx;
trivertexy = rcy;
trivertexRed = (short)(clrgetRed() << );
trivertexGreen = (short)(clrgetGreen() << );
trivertexBlue = (short)(clrgetBlue() << );
trivertexAlpha = ;
OSMoveMemory( pVertex trivertex TRIVERTEXsizeof );
trivertexx = rcx + rcwidth;
trivertexy = rcy + rcheight;
trivertexRed = (short)(clrgetRed() << );
trivertexGreen = (short)(clrgetGreen() << );
trivertexBlue = (short)(clrgetBlue() << );
trivertexAlpha = ;
OSMoveMemory( pVertex + TRIVERTEXsizeof trivertex TRIVERTEXsizeof );
boolean success = OSGradientFill( handle pVertex pMesh vertical ? OSGRADIENT_FILL_RECT_V : OSGRADIENT_FILL_RECT_H );
OSHeapFree( hHeap pMesh );
if ( success )
return;
}
@Override
protected void checkSubclass()
{}
}
如果你使用的是JDK 或者更低的版本請把@Override標記去掉以後才能編譯因為這是一個Java 中才有的特性此外我重載了checkSubclass方法並提供了一個空的實現如果不這麼做的話那麼SWT在默認情況下是不允許你從Button類繼承的
這個地方請允許我稍稍跑一下題上面代碼中的fillGradientRectangle方法——從它的名字你大概可以猜到這個方法的作用是畫出一個漸變色的矩形區域我是從GCfillGradientRectangle中偷來的代碼針對按鈕類作了一些修改就可以了讓我感到訝異的是在整理這段代碼的時候我發現從SWT中調用Win API實在是太方便了——比我原先猜想的還要容易得多即便是微軟的P/Invoke也要比這麻煩當然這很大程度上要歸功於SWT將系統函數很好的封裝在了一個OS靜態類中(如果你不知道P/Invoke是什麼的話簡單的說它就是微軟在Net平台中提供的用來調用系統API和自定義DLL中的方法的技術)
上面那些繪圖的代碼基本上是Windows SDK的編程風格因為我本人有很多這方面的開發經驗所以這些代碼對我來說是相當清晰且直觀的不過我估計純粹的Java程序員或許對這段代碼不會有很大的好感理論上講我可以把這些代碼用更加OO的方式包裝起來從而看上去能好看一些不過本文的目的在於講述實現技術用包裝的話反而會破壞效果如果你感興趣的話也可以嘗試自己來包裝一下
需要講解的地方到這裡就全部結束了為了完整起見我把程序框架類的代碼也列在下面但是不做什麼說明——基本上每個SWT程序中這段代碼都是大同小異的
import orgeclipseswtlayoutFillLayout;
import orgeclipseswtwidgets*;
public class Application
{
public static void main( String[] args )
{
Display display = DisplaygetDefault();
Shell shell = new Shell( display );
init( shell );
shellpack();
shellopen();
while ( !shellisDisposed() )
{
if ( !displayreadAndDispatch() )
displaysleep();
}
}
private static void init( Shell shell )
{
shellsetText( Owner Draw Button Test );
FillLayout layout = new FillLayout();
layoutmarginWidth = layoutmarginHeight = ;
shellsetLayout( layout );
Button btn = new TestButton( shell );
btnsetText( Owner Draw Button );
btnsetToolTipText( Hello Im a OwnerDraw Button! );
}
}
下面是程序運行的界面盡管這遠遠算不上完美——真正的按鈕還應該考慮是否能夠和用戶的任何配置下特別是有窗口主題的時候也能正常工作?完美的按鈕實現可能需要至少數百行的代碼才行不過對本文的目的來說這樣已經足夠了可惜的是按下按鈕的效果無法從圖中體現你可以自己運行一下這個程序來體驗一下實際的感覺
Java核心技術免費提供,內容來源於互聯網,本文歸原作者所有。