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

然而,我对Button组件还是不能感到满意。最大的遗憾就是:对它的外观,所能做的工作也就仅限于此了。如果你想让按钮拥有一个漂亮的、渐变色的背景和一些特殊的文字效果,怎么办呢?答案是没有办法。Button类里面似乎没有任何方法提供我想要的功能。在Eclipse.org站点上搜索,结果看到了Eclipse的工作人员这样回答提出类似问题的用户:
(原文参见
http://dev.eclipse.org/newslists/news.eclipse.platform.swt/msg19914.html

If fixing the button requires owner draw, then we aren't really interested
at this time. Owner draw has all kinds of bad problems associated with it
that don't seem worth it. Setting the background color of a control in this
day and age is dicy anways given the advent of themes and theme managers.

不知道你对此的感觉如何?我看过以后是觉得很失望,因为这段话一点说服力也没有。正确绘制一个按钮是合格的开发者应该、而且能够做到的事情,抽象的说“有很多问题”是不足以让人信服的。而以此为理由完全堵死开发者的去路,则可以说是粗暴的行为。

既然指望Eclipse的开发者来修复问题已经不太可能,我决定自己找找有没有可行的解决办法。不管怎么说,人家的代码毕竟已经全部开放出来了。具有自绘能力的按钮是界面开发工作中非常重要的功能,我过去自己就曾作过大概七八种不同风格的按钮,设计良好的按钮确实能够在很大程度上改善应用程序的外观,远胜于千篇一律的标准按钮。这些想法成为驱动我寻找解决方案的最大动力。

我曾尝试过的第一个想法是用Button.addPaintListener来修改按钮的外观。但是,结果令人失望——虽然它显示出来的时候的确按照预想进行绘制了,但是当你用鼠标去按它的时候,马上又变回了原本灰头土脸的样子。显然,在按下按钮的时候,它并不是触发paint事件,而是按照自己的想法画出原本的按钮,于是我的工作全部白费了。

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

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


下面是程序运行的界面。尽管这远远算不上完美——真正的按钮还应该考虑,是否能够和用户的任何配置下,特别是有窗口主题的时候也能正常工作?完美的按钮实现可能需要至少数百行的代码才行。不过对本文的目的来说,这样已经足够了。可惜的是按下按钮的效果无法从图中体现;你可以自己运行一下这个程序来体验一下实际的感觉。
(完整的代码在附件中)



作者:独行客 阅读() 评论()  编辑 发表于:2006-11-15 15:32
相关内容
文章评论

  • # re: SWT:实现自我绘制的Button组件
  • 既然这么麻烦的去绘制Button按钮,为和不使用Swing呢?
    不要跟我说你自从AWT时代就再也没有回头看过Swing了。
    惹祸特 | 2006-12-09 21:17
  • # re: SWT:实现自我绘制的Button组件
  • 知道了原因了
    天极博友 | 2007-07-10 14:32
  • # re: SWT:实现自我绘制的Button组件
  • 赞一个 
    纯粹的 SWT 代码
    Swing button 在 SWT 应用程序里还是感觉有点不伦不类
    不过那段 SDK 风格的代码有点难度阿
    天极博友 | 2007-07-27 12:42

    发表评论
    标题 *  
    姓名 *  
    内容 *  
       验证码: *       
           
    版权声明:天极是本Blog托管服务提供商。如本文牵涉版权问题,天极不承担相关责任,请版权拥有者直接与文章作者联系解决。
    Powered by:

    Copyright © 独行客