在所有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! ); } } 下面是程序运行的界面尽管这远远算不上完美——真正的按钮还应该考虑是否能够和用户的任何配置下特别是有窗口主题的时候也能正常工作?完美的按钮实现可能需要至少数百行的代码才行不过对本文的目的来说这样已经足够了可惜的是按下按钮的效果无法从图中体现你可以自己运行一下这个程序来体验一下实际的感觉 =) windowopen(/Article/UploadFiles//jpg); alt= src=_//jpg onload=if(thiswidth>)thiswidth=; border= twffan=done> |