Sealyu

          --- 博客已遷移至: http://www.sealyu.com/blog

            BlogJava :: 首頁 :: 新隨筆 :: 聯(lián)系 :: 聚合  :: 管理 ::
            618 隨筆 :: 87 文章 :: 225 評論 :: 0 Trackbacks
          在所有SWT組件中,Button幾乎是最常用的,其功能在對于一般的情況來說也足夠豐富了。你可以為Button組件設(shè)置要顯示在其中的文本或者圖像、設(shè)定ToolTip,甚至只要修改一個風(fēng)格樣式就能得到一個看上去相當(dāng)不錯的方向箭頭按鈕。

          然而,我對Button組件還是不能感到滿意。最大的遺憾就是:對它的外觀,所能做的工作也就僅限于此了。如果你想讓按鈕擁有一個漂亮的、漸變色的背景和一些特殊的文字效果,怎么辦呢?答案是沒有辦法。Button類里面似乎沒有任何方法提供我想要的功能。

          我曾嘗試過的第一個想法是用Button.addPaintListener來修改按鈕的外觀。但是,結(jié)果令人失望——雖然它顯示出來的時候的確按照預(yù) 想進行繪制了,但是當(dāng)你用鼠標(biāo)去按它的時候,馬上又變回了原本灰頭土臉的樣子。顯然,在按下按鈕的時候,它并不是觸發(fā)paint事件,而是按照自己的想法 畫出原本的按鈕,于是我的工作全部白費了。

          如果嘗試為按鈕設(shè)定圖像會怎么樣呢?這也不是一個好主意。首先,不管你選擇什么樣的圖像, 都沒辦法去掉按鈕四周的邊框,而正是這些邊框嚴(yán)重破壞了圖像的和諧感;其次,如果你的程序有幾十甚至上百個按鈕,為每個按鈕都維護一幅圖像(甚至更多—— 理論上每個按鈕在普通狀態(tài)和被按下、禁用的狀態(tài)下,甚至當(dāng)鼠標(biāo)移進移出按鈕的時候,都應(yīng)當(dāng)顯示不同的圖像)明顯是在浪費系統(tǒng)資源;如果你們的美工聽說需要 做幾百個圖片,大概也不會給你好臉色看。此外,圖像有一個嚴(yán)重的缺點是:它所擁有的像素數(shù)目是固定的,難以隨著界面的放大和縮小同時變化。如果強制進行縮 放的話,會出現(xiàn)明顯的鋸齒和失真,最終讓你精心設(shè)計的窗口變得慘不忍睹。最好還是放棄這個想法。

          如果以Canvas為基礎(chǔ),設(shè)計一個 偽裝的按鈕組件又如何呢?聽起來好像很不錯,因為采用這種辦法的話,我們對如何繪制組件的表面就有了完整的控制權(quán)。不過這也意味著你必須對按鈕的狀態(tài)進行 手工維護。雖然Button本身是一個很簡單的組件,但是重復(fù)去做標(biāo)準(zhǔn)按鈕已經(jīng)作好的工作似乎還是有點無謂。還有一件事情是應(yīng)當(dāng)考慮的:我們知道, JFace中的Action機制可以將標(biāo)準(zhǔn)按鈕、菜單項和工具欄按鈕這三種界面組件納入一個統(tǒng)一的事件處理體系。然而,如果我們從Canvas派生去模擬 一個按鈕的話,不論你模擬到多么相似的地步,它畢竟不是一個真正的Button,Action也不會給它同等的待遇。也就是說手工制作的按鈕無法和 JFace Action體系協(xié)同工作——除非你去修改Action的處理方法,讓它去接納新的按鈕對象。這可不是一件輕松的工作。

          如果上面的方法都行不通的話,應(yīng)當(dāng)怎么辦呢?我們知道,和Swing這樣的框架不同,SWT中的按鈕其實就是操作系統(tǒng)底層所實現(xiàn)的按鈕(這一點也可以用 SPY++或者Winsight32之類的工具證實)。同時我們也知道,操作系統(tǒng)——至少是Windows系統(tǒng),對按鈕已經(jīng)提供了自我繪制的機制,這就是 所謂的Owner Draw(稱為所有者繪制的原因是因為默認(rèn)情況下繪制消息是發(fā)送給按鈕的父窗口處理的,但是父窗口也可以把這個皮球再踢回給按鈕,讓它自己解決)。在 Win32 API中,凡是使用BS_OWNERDRAW風(fēng)格創(chuàng)建、并且能夠(通過消息反射)響應(yīng)WS_DRAWITEM消息的按鈕,都可以獲得這種定制的能力。 了解這一點,接下來的任務(wù)就是研究Button組件有沒有開放這個接口供我們修改了。對Button組件的源代碼進行粗略的瀏覽后,我發(fā)現(xiàn)了如下的方法:

          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結(jié)構(gòu)的出現(xiàn)是一個明顯的提示:這里就是WM_DRAWITEM消息的響應(yīng)函數(shù),很幸運它沒有聲明為final的,只要重載它并提供自己的實現(xiàn)就行了。

          看起來是個小case,實際上也是。不過,還有一處小麻煩需要克服。注意wmDrawChild方法沒有使用任何訪問限定符,這意味著它是 package friendly的——同一個包中的對象可以訪問和重載此方法,其他包中的對象就沒有這個權(quán)力了。也就是說,要定制按鈕對象,我們新建的對象也需要放在同 一個包(org.eclipse.swt.widgets)中。看起來有點像在使用Hack手段,不過為了突破SWT給我們的限制,眼下也只好稍稍將就一 下。好在swt的包沒有密封(Sealed),不然我就不得不再次宣稱此路不通了。

          既然障礙已經(jīng)掃清,接下來我們可以來實現(xiàn)前面的想 法了。這里我做了一個決定,在上述包中只加入一個抽象類,目的是把必要的接口暴露出來;至于如何繪制按鈕,則留給具體的按鈕對象根據(jù)應(yīng)用程序的需求來決 定。這樣,不管你希望實現(xiàn)Windows XP風(fēng)格的按鈕、還是卡通風(fēng)格的按鈕、或是平面樣式的,總之不論什么千奇百怪的風(fēng)格,只要繼承一個類并重載一個繪制方法就行了,而不必每次都要和 Button類的內(nèi)部打交道。

          基于這種考慮,實現(xiàn)自繪按鈕的抽象類如下:

          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 );
          }

          注意這個抽象類所作的工作。在構(gòu)造函數(shù)中,它調(diào)用操作系統(tǒng)方法為自己加入了BS_OWNERDRAW風(fēng)格。如果沒有這一步,那么操作系統(tǒng)將不 會把這個按鈕視為自繪的按鈕,也不會向其發(fā)送任何繪制消息。接下來是WM_DRAWITEM消息的響應(yīng)函數(shù)。在這個函數(shù)中,我們簡單的把必要的繪制參數(shù)提 取出來,然后調(diào)用抽象方法ownerDraw去進行實際的繪制工作。任何從OwnerDrawButton類派生的按鈕對象必須重載此ownerDraw 方法,來決定如何繪制自身。
          作為一個例子,我實現(xiàn)了一個具體的按鈕類。這個按鈕用從上至下的漸變色背景添充整個按鈕,然后繪制出按鈕的文字。 如果當(dāng)前按鈕被按下,該類還調(diào)整了一下文字的位置,以顯示出“按下”的外觀效果。代碼稍微有些長,這是因為消息函數(shù)所提供的是一個操作系統(tǒng)才了解的原生 HDC對象,而不是我們所熟悉的GC類,因此也需要相應(yīng)的用原生API進行處理。不過,其原理是相當(dāng)簡單的——你只需要在給出的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標(biāo)記去掉以后才能編譯,因為這是一個Java 5.0中才有的特性。此外,我重載了checkSubclass方法并提供了一個空的實現(xiàn);如果不這么做的話,那么SWT在默認(rèn)情況下是不允許你從Button類繼承的。

          這個地方請允許我稍稍跑一下題。上面代碼中的fillGradientRectangle方法——從它的名字你大概可以猜到,這個方法的作用是畫出一個 漸變色的矩形區(qū)域。我是從GC.fillGradientRectangle中“偷”來的代碼,針對按鈕類作了一些修改就可以了。讓我感到訝異的是,在整 理這段代碼的時候,我發(fā)現(xiàn)從SWT中調(diào)用Win32 API實在是太方便了——比我原先猜想的還要容易得多。即便是微軟的P/Invoke也要比這麻煩。當(dāng)然,這很大程度上要歸功于SWT將系統(tǒng)函數(shù)很好的封 裝在了一個OS靜態(tài)類中。(如果你不知道P/Invoke是什么的話,簡單的說它就是微軟在.Net平臺中提供的、用來調(diào)用系統(tǒng)API和自定義DLL中的 方法的技術(shù))。

          上面那些繪圖的代碼基本上是Windows SDK的編程風(fēng)格。因為我本人有很多這方面的開發(fā)經(jīng)驗,所以這些代碼對我來說是相當(dāng)清晰且直觀的。不過我估計純粹的Java程序員或許對這段代碼不會有很 大的好感。理論上講,我可以把這些代碼用更加OO的方式包裝起來,從而看上去能好看一些。不過,本文的目的在于講述實現(xiàn)技術(shù),用包裝的話反而會破壞效果。 如果你感興趣的話,也可以嘗試自己來包裝一下。

          需要講解的地方到這里就全部結(jié)束了。為了完整起見,我把程序框架類的代碼也列在下面,但是不做什么說明——基本上每個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!" );
          }
          }

          下面是程序運行的界面。盡管這遠遠算不上完美——真正的按鈕還應(yīng)該考慮,是否能夠和用戶的任何配置下,特別是有窗口主題的時候也能正常工作? 完美的按鈕實現(xiàn)可能需要至少數(shù)百行的代碼才行。不過對本文的目的來說,這樣已經(jīng)足夠了。可惜的是按下按鈕的效果無法從圖中體現(xiàn);你可以自己運行一下這個程 序來體驗一下實際的感覺。
          posted on 2008-04-10 22:41 seal 閱讀(623) 評論(0)  編輯  收藏 所屬分類: Eclipse SWT/JFace RCP
          主站蜘蛛池模板: 张家界市| 绥滨县| 阜平县| 茌平县| 铁岭县| 乌拉特后旗| 阿拉善盟| 周口市| 定西市| 肥乡县| 乌兰县| 成安县| 洛浦县| 留坝县| 台北县| 隆子县| 轮台县| 治多县| 灌阳县| 海丰县| 五大连池市| 依兰县| 视频| 高要市| 乌兰浩特市| 迁西县| 思茅市| 绵阳市| 阜新市| 仁化县| 榆社县| 福泉市| 泽库县| 安泽县| 兴化市| 延津县| 礼泉县| 沽源县| 香港 | 吉木萨尔县| 祁东县|