关闭

SWT:实现自我绘制的Button组件

498人阅读 评论(0) 收藏 举报
写在前面:

本文的目的在于说明,如何在Windows平台上突破SWT对Button组件的限制,实现一个可以自由定制的、漂亮的按钮界面。

本文的读者应当具有基本的Eclipse和SWT/JFace开发经验,知道如何创建并运行一个最简单的SWT应用程序。
对Windows平台上的SDK开发有所了解是最好的,即使没有也不妨碍你理解本文的基本内容。

我 想,大概有的读者在读过本文之后会说:这个方法很有趣,不过它不是跨平台的。如果有这种意见的话,我想先声明一下自己的立场。那就是,我自己并不将跨平台 作为一条神圣不可侵犯的准则——特别是对于客户端的桌面程序开发来说,如果用户需要某个特性,那么我们没有理由因为“这个特性不能跨平台”就不去实现它。 这也是我喜爱SWT的理由之一——因为我在这个框架的设计中所看到的务实的作法实在是深合我心。例如,像ActiveX和OLE Automation这样的技术并不是跨平台的,但是SWT仍然对它们提供了不错的支持。对并非跨平台标准、但事实上被广泛采用的技术提供支持,即使出于 保护客户投资的需要也是值得欢迎的作法。当然Sun肯定不会认同此观点,它必须捍卫自己“Write once, run anywhere”的至高准则。厂商观点归厂商观点,作为一个小程序员,我认为务实一点不是什么坏事,你说呢?

我没有对本文中的方法提供 跨平台的实现,很大一个原因也是因为:虽然我本人有多年在Windows平台上的开发经验,但是对其他平台的界面开发技术却所知甚少。或许像Linux这 样的平台上也有对应的解决办法,但是我并不了解,当然也无法实现一个完全跨平台的解决方案。尽管如此,我仍然相信:如果了解相关的技术,那么实现不同平台 下的移植应该不算困难;在后面你会看到,基于Windows平台的办法是非常简单的。

如果你看完了上面一大段废话,那么我应当感谢你。下面进入正文。


问题的提出

在所有SWT组件中,Button几乎是最常用的,其功能在对于一般的情况来说也足够丰富了。你可以为Button组件设置要显示在其中的文本或者图像、设定ToolTip,甚至只要修改一个风格样式就能得到一个看上去相当不错的方向箭头按钮。

 
然而,我对Button组件还是不能感到满意。最大的遗憾就是:对它的外观,所能做的工作也就仅限于此了。如果你想让按钮拥有一个漂亮的、渐变色的背景和一些特殊的文字效果,怎么办呢?答案是没有办法。我曾试过用Button.addPaintListener来修改按钮的外观,结果令人失望——虽然它显示出来的时候的确按照预想进行绘制了,但是当你用鼠标去按它的时候,马上又变回了原本灰头土脸的样子。

 
如 果尝试为按钮设定图像会怎么样呢?这也不是一个好主意。首先,不管你选择什么样的图像,都没办法去掉按钮四周的边框,而正是这些边框严重破坏了图像的和谐 感;其次,如果你的程序有几十甚至上百个按钮,为每个按钮都维护一幅图像(甚至更多——理论上每个按钮在普通状态和被按下、禁用的状态下,甚至当鼠标移进 移出按钮的时候,都应当显示不同的图像)明显是在浪费系统资源;如果你们的美工听说需要做几百个图片,大概也不会给你好脸色看。此外,图像有一个严重的缺 点是:它所拥有的像素数目是固定的,难以随着界面的放大和缩小同时变化。如果强制进行缩放的话,会出现明显的锯齿和失真,最终让你精心设计的窗口变得惨不 忍睹。最好还是放弃这个想法。

 如果以Canvas为基础,设计一个伪装的按钮组件又如何呢?听起来好像很不错,因为采用这种办法的话,我们对如何绘制组件的表面就有了完整的控制权。不过这也意味着你必须对按钮的状态进行手工维护。虽然Button本身是一个很简单的组件,但是重复去做标准按钮已经作好的工作似乎还是有点无谓。还有一件事情是应当考虑的:我们知道,JFace中的Action机制可以将标准按钮、菜单项和工具栏按钮这三种界面组件纳入一个统一的事件处理体系。然而,如果我们从Canvas派生去模拟一个按钮的话,不论你模拟到多么相似的地步,它毕竟不是一个真正的ButtonAction也不会给它同等的待遇。也就是说手工制作的按钮无法和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!" );

    }

}

 

下面是程序运行的界面。可惜的是按下按钮的效果无法从图中体现;你可以自己运行一下这个程序来体验一下实际的感觉。

 


结论:

本文介绍了一种越过SWT限制、(在 一定程度上)直接和操作系统对话,从而达到特殊效果的途径。这样做需要一定的技巧,如果(我也这样期望)SWT本身有能力做到这种效果的话,那么你应当使 用框架内置的方法,而将本文介绍的办法当作一种非常规的、Hack风格的后备手段,不要随意滥用。然而,当SWT本身就存在限制的时候——虽然你可以直接 修改SWT的源代码,不过这并不值得提倡,除非你本人就是SWT的代码贡献者——那么你或许可以考虑一下用本文介绍的的“走后门”的办法实现一些特效。为 此,你可能还是要阅读并理解一些SWT的源代码,并挖出你感兴趣的那一部分——这并不轻松,但是请相信我,这是一件相当有趣、而且有挑战性的任务。而且, 程序员最大的乐趣之一正在于此。
0
0

查看评论
* 以上用户言论只代表其个人观点,不代表CSDN网站的观点或立场
    个人资料
    • 访问:1716次
    • 积分:33
    • 等级:
    • 排名:千里之外
    • 原创:1篇
    • 转载:1篇
    • 译文:0篇
    • 评论:1条
    文章分类
    文章存档
    最新评论