熱點推薦:
您现在的位置: 電腦知識網 >> 編程 >> Java編程 >> Java核心技術 >> 正文

SWT:實現自我繪制的Button組件

2022-06-13   來源: Java核心技術 
在所有SWT組件中Button幾乎是最常用的其功能在對於一般的情況來說也足夠豐富了你可以為Button組件設置要顯示在其中的文本或者圖像設定ToolTip甚至只要修改一個風格樣式就能得到一個看上去相當不錯的方向箭頭按鈕

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

  我曾嘗試過的第一個想法是用ButtonaddPaintListener來修改按鈕的外觀但是結果令人失望——雖然它顯示出來的時候的確按照預想進行繪制了但是當你用鼠標去按它的時候馬上又變回了原本灰頭土臉的樣子顯然在按下按鈕的時候它並不是觸發paint事件而是按照自己的想法畫出原本的按鈕於是我的工作全部白費了

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

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

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

package orgeclipseswtwidgets;

public class Button extends Control {
 …
 LRESULT wmDrawChild (int wParam int lParam) {
 if ((style & SWTARROW) == ) return superwmDrawChild (wParam lParam);
 DRAWITEMSTRUCT struct = new DRAWITEMSTRUCT ();
 
  其中DRAWITEMSTRUCT結構的出現是一個明顯的提示這裡就是WM_DRAWITEM消息的響應函數很幸運它沒有聲明為final的只要重載它並提供自己的實現就行了

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

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

  基於這種考慮實現自繪按鈕的抽象類如下

package orgeclipseswtwidgets;

import orgeclipseswtinternalwin*;

public abstract class OwnerDrawButton extends Button
{
 public OwnerDrawButton( Composite parent int style )
 {
  super( parent style );

  int osStyle = OSGetWindowLong( handle OSGWL_STYLE );
  osStyle |= OSBS_OWNERDRAW;
  OSSetWindowLong( handle OSGWL_STYLE osStyle );
 }

 LRESULT wmDrawChild( int wParam int lParam )
 {
  superwmDrawChild( wParam lParam );
  DRAWITEMSTRUCT struct = new DRAWITEMSTRUCT();
  OSMoveMemory( struct lParam DRAWITEMSTRUCTsizeof );
  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核心技術免費提供,內容來源於互聯網,本文歸原作者所有。

推薦文章
Copyright © 2005-2022 電腦知識網 Computer Knowledge   All rights reserved.