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

转自: http://java.chinaitlab.com/advance/531489.html

在所有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!" );
  }
}


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


補充內容:

在VC中透明浮动按键的实现
    有一种按键,看起来是一幅完整的图片,当鼠标移到按键区域时,图片的一部分凸现,形成一个按键,当鼠标移走时又恢复原来状态。

     最近,看了一些关于浮动按键的代码,其原理大致上跟CBitmapButton差不多,用数幅位图代表按键的各个状态,响应鼠标的各种消息来设置按键的状态,实现按键的浮动显示,但是这样的按键却不能和周围的背景混和成一幅图片。

     为了实现“透明”按键,可以简单地做个试验:先在对话框中加入一个BUTTON,通过属性框选“Owner Draw”风格,再加入一个PICTURE,并加入图片,将BUTTON移到PICTURE上。运行结果发现,按键没有显示出来,但在按键区域按下鼠标时,该按键仍然能发出WM_COMMAND消息,这样一个纯透明的按键建立了。显然,这个按键是毫无使用意义的,因为用户不知道按键的位置,必须让用户容易觉察到按键的位置,可以把这个按键改造一下:

     (首先从CButton派生出一个新类CDrawButton)

     ·把按键的标题显示出来

     这个实现起来比较简单,我们可以重载CButton类的成员函数DrawItem(),

void CDrawButton::DrawItem
(LPDRAWITEMSTRUCT lpDrawItemStruct)
{
CDC dc;
CRect rect=lpDrawItemStruct- >rcItem;//得到按键区域
CString sCaption;
dc.Attach(lpDrawItemStruct- >hDC); //得到设备环境CDC
VERIFY(lpDrawItemStruct- >CtlType==ODT_BUTTON);
GetWindowText(sCaption);//得到按键的标题
dc.SetBkMode(TRANSPARENT);//透明显示
CFont* m_pOldFont=dc.SelectObject(m_pFont);
dc.DrawText(sCaption,&rect,DT_CENTER|DT_VCENTER|DT_SINGLELINE);
dc.SelectObject(m_pOldFont);
}

     其中的m_pFont是成员变量,它保存了对话框的字体指针,为了按键的标题风格与对话框的字体风格一致,在初始化时调用对话框的成员函数GetFont()即可得到指向对话框字体的CFont类指针。

     ·使按键浮动显示

    要通过自绘来表示按键的各种状态,可填写DRAWITEMSTRUCT来通知DrawItem()函数需要做什么,我们先了解一下DRAWITEMSTRUCT:

typedef struct tagDRAWITEMSTRUCT{
    UINT CtlType; // 控件类型
    UINT CtlID;// 控件的ID号
    UNIT itemID;//菜单项的索引
    UINT itemAction;// 绘图操作
    UINT itemState; // 状态
    HWND hwndItem; // 控件的窗口句柄
    HDC hDC; // 相关的设备环境
    RECT rcItem;//控件的范围
    DWORD itemData;//指定与菜单项相联系的应用程序定义的32位值
}DRAWITEMSTRUCT;

    利用这个结构先做一个按键状态设置函数:
void CDrawButton::SetButtonMode(UINT action, UINT mode)
{
// TODO: Add your message handler code
here and/or call default
    DRAWITEMSTRUCT DIS;
    DIS.CtlType = ODT_BUTTON;
    DIS.CtlID = GetDlgCtrlID();
    DIS.itemAction = action;
    DIS.itemState = mode;
    DIS.hwndItem = GetSafeHwnd();
    DIS.hDC = GetDC()- >GetSafeHdc();
    GetClientRect(&(DIS.rcItem));
    SendMessage(WM_DRAWITEM,(WPARAM)
GetSafeHwnd(),(LPARAM)&DIS);
    ReleaseDC(CDC::FromHandle(DIS.hDC));
}

    这样,我们可以响应鼠标的各种消息来设置按键的各种状态:
void CDrawButton::OnMouseMove
(UINT nFlags, CPoint point)
{
    // TODO: Add your message handler code
here and/or call default
    CRect rect;
    GetClientRect(&rect);
    if(rect.PtInRect(point)){
        if (mBtnStats==BTN_NORMAL){
    SetButtonMode(ODA_SELECT, ODS_FOCUS);
            SetCapture();
        }
    }
    else{
//AutoLoad(GetDlgCtrlID(),GetParent());
SetButtonMode(ODA_DRAWENTIRE,ODS_DEFAULT);
        ReleaseCapture();
    }

    CButton::OnMouseMove(nFlags, point);
}

    这里,mBtnStats是个UINT类型的成员变量,它可以有三种自定义状态:
BTN_NORMAL    正常状态
BTN_UP        鼠标移入按键区域或释放鼠标
BTN_DOWN    按下鼠标
(可以再加一种DISABLE状态)

    当在按键区域释放鼠标时,必须发送WM_COMMAND消息:
void CDrawButton::OnLButtonUp(UINT nFlags, CPoint point)
{
    // TODO: Add your message handler code
here and/or call default
    CRect rect;
    GetClientRect(&rect);
    if(rect.PtInRect(point)){
        if (mBtnStats==BTN_DOWN)
            GetParent()- >SendMessage(WM_COMMAND,
        MAKELPARAM(GetDlgCtrlID(),BN_CLICKED),
        (LPARAM)GetSafeHwnd());
        SetCapture();
    }
    else{
    SetButtonMode(ODA_DRAWENTIRE,ODS_DEFAULT);
        ReleaseCapture();
    }

    CButton::OnLButtonUp(nFlags, point);
}

    接着就是绘制按键的各种状态:由于按键必须“透明”,所以在按下和释放时只在按键区域的四周加上一个3D边框就行了。而在正常状态下,则必须去掉边框恢复背景。但如何恢复背景图象呢?我是这样做的:在按键初始化时,先把被按键覆盖了的区域保存在一个CBitmap类中,以后需要重绘按键时就把这个CBitmap画在按键上就行了。
void CDrawButton::DrawItem
(LPDRAWITEMSTRUCT lpDrawItemStruct)
{
    // TODO: Add your code to draw the specified item
    CDC dc;
    CRect rect=lpDrawItemStruct- >rcItem;
    CString sCaption;
    dc.Attach(lpDrawItemStruct->hDC);
//得到绘制的设备环境CDC
    VERIFY(lpDrawItemStruct- >CtlType==ODT_BUTTON);

    if (lpDrawItemStruct- >itemAction & ODA_DRAWENTIRE){
        //重绘控件(正常状态)
        mBtnStats=BTN_NORMAL;
        if (m_pBitmap!=0){
            CDC memDC;
            memDC.CreateCompatibleDC(&dc);
            memDC.SelectObject(m_pBitmap);
    dc.BitBlt(0, 0, rect.Width(), rect.Height(),
                &memDC, 0, 0, SRCCOPY);
            memDC.DeleteDC();
        }
        //显示按键标题
        GetWindowText(sCaption);
        dc.SetBkMode(TRANSPARENT);
        if (m_pFont!=0){
        CFont* m_pOldFont=dc.SelectObject(m_pFont);
        dc.DrawText(sCaption,&rect,
    DT_CENTER|DT_VCENTER|DT_SINGLELINE);
            dc.SelectObject(m_pOldFont);
        }
    }

if ((lpDrawItemStruct- >itemState & ODS_SELECTED) &&
(lpDrawItemStruct- >itemAction & ODA_SELECT)){
//按下鼠标
mBtnStats=BTN_DOWN;
dc.Draw3dRect(&rect,RGB(128,128,128),RGB(192,192,192));
rect.top=rect.top+1;rect.bottom=rect.bottom-1;
rect.left=rect.left+1;rect.right=rect.right-1;
dc.Draw3dRect(&rect,RGB(0,0,0),RGB(255,255,255));
    }

if(!(lpDrawItemStruct- >itemState & ODS_SELECTED) &&
(lpDrawItemStruct- >itemAction & ODA_SELECT)){
//释放鼠标或鼠标进入按键区域
mBtnStats=BTN_UP;
dc.Draw3dRect(&rect,RGB(255,255,255),RGB(0,0,0));
rect.top=rect.top+1;rect.bottom=rect.bottom-1;
rect.left=rect.left+1;rect.right=rect.right-1;
dc.Draw3dRect(&rect,RGB(192,192,192),RGB(128,128,128));
    }

    dc.Detach();
}

    接着就必须一些初始化工作,其中最关键就是把被按键覆盖了的区域保存进CBitmap类中,我们知道CDC::StretchBlt()函数可以把位图的指定区域从一个设备拷贝到另一个设备中,这样可以很方便地把窗口或对话框的某个区域保存,条件是获得其DC:
void CDrawButton::LoadBack(CWnd *pParent)
{
    ASSERT(GetStyle() & BS_OWNERDRAW);
    if (m_pBitmap!=0) return;

    CRect rect;
    GetWindowRect(&rect);
    pParent- >ScreenToClient(&rect);//获得按键区域
    CPaintDC dc(pParent);
    if (m_pBitmap==0) m_pBitmap=new CBitmap;//初始化位图
    m_pBitmap- >CreateCompatibleBitmap
(&dc,rect.Width(),rect.Height());
    CDC memDC;
    memDC.CreateCompatibleDC(&dc);
    memDC.SelectObject(m_pBitmap);
memDC.StretchBlt(0, 0, rect.Width(),rect.Height(), &dc,
    rect.left, rect.top,
rect.Width(),rect.Height(), SRCCOPY);//保存
    memDC.DeleteDC();
m_pFont=pParent- >GetFont();//获得窗口或对话框的字体

    ModifyStyle(0,WS_VISIBLE);//显示按键

    SetBitmapMode(ODA_DRAWENTIRE,0);//绘制按键
}

    而使这个类和对话框上的按键产生联系还必须调用一下SubclassDlgItem():
BOOL CDrawButton::AutoLoad(UINT nID, CWnd *pParent)
{
// first attach the CDrawButton to the dialog control

    if (m_pBitmap!=0) return FALSE;

    if (!SubclassDlgItem(nID, pParent)) return FALSE;

    LoadBack(pParent);

    return TRUE;
}


    这个类还必须具有三个成员变量:
    CFont* m_pFont;
    CBitmap* m_pBitmap;
    UINT mBtnStats;

在构造函数中初始化这些变量
    m_pBitmap=0;
    m_pFont=0;
    //赋予0是可以的
    mBtnStats=BTN_NORMAL;

在折构函数中拆除位图
    if(m_pBitmap!=0) delete m_pBitmap;

    这样,一个透明的浮动式按键类就做好了,具体实现方法以下:
    1.接管对话框的BUTTON,首先在对话框上画一个BUTTON,再加一个PICTURE图片,BUTTON的风格必须加入OWNER DRAW及去掉VISIBLE,把BUTTON移到PICTURE上适当的位置,在对话框类加入CDrawButton类成员m_myButton,由于按键初始化时必须保存对话框的图象,而对话框在运行InitDialog()或第一次运行OnPaint()时对话框的控件还没有真正显示出来,我们只好在OnMouseMove()中进行初始化:

m_myButton.AutoLoad(IDC_BUTTON1,this);
AutoLoad()只运行一次。

    2.动态建立CDrawButton,在对话框类或CxxxView类加入CDrawButton类成员m_myButton,可以在对话框的InitDialog()或CxxxView类的InitialUpdate()中加入:m_myButton.Create()函数,必须包含BS_OWNERDRAW而不能有WS_VISIBLE风格,然后在OnMouseMove()或OnDraw()中进行初始化:m_myButton.LoadBack(this);注意应加在OnDraw()的最后。

    同样地,LoadBack()只运行一次。

    (如果按键比背景的图片迟建立而具有可见(Visible)属性,则会把图片抹掉,所以必须去掉VISIBLE属性或不能加入WS_VISIBLE风格)

    ·当鼠标移到按键区域时,改变鼠标

    这个很容易实现,不在这里多说了。  
 


DRAWITEMSTRUCT 为需要自绘的控件或者菜单项提供了必要的信息。在需要绘制的控件或者菜单项对应的WM_DRAWITEM消息函数中得到一个指向该结构的指针。 DRAWITEMSTRUCT结构的定义如下:
typedef struct tagDRAWITEMSTRUCT {
UINT
CtlType; UINT CtlID; UINT itemID; UINT itemAction; UINT itemState;
HWND
hwndItem; HDC hDC; RECT rcItem; ULONG_PTR itemData;
} DRAWITEMSTRUCT;

结构成员:

CtlType
指定了控件的类型,其取值如下表所示。

ODT_BUTTON:按钮控件

ODT_COMBOBOX:组合框控件

ODT_LISTBOX:列表框控件

ODT_LISTVIEW:列表视图控件

ODT_MENU:菜单项

ODT_STATIC:静态文本控件

ODT_TAB:Tab控件

CtlID

指定了自绘控件的ID值,而对于菜单项则不需要使用该成员

itemID
表示菜单项ID,也可以表示列表框或者组合框中某项的索引值。对于一个空的列表框或组合框,该成员的值为–1。这时应用程序只绘制焦点矩形(该矩形的坐标由rcItem 成员给出)虽然此时控件中没有需要显示的项,但是绘制焦点矩形还是很有必要的,因为这样做能够提示用户该控件是否具有输入焦点。当然也可以设置itemAction 成员为合适值,使得无需绘制焦点。

itemAction
指定绘制行为,其取值可以为下表中所示值的一个或者多个的联合。
ODA_DRAWENTIRE:当整个控件都需要被绘制时,设置该值
ODA_FOCUS:如果控件需要在获得或失去焦点时被绘制,则设置该值。此时应该检查itemState成员,以确定控件是否具有输入焦点。

ODA_SELECT
如果控件需要在选中状态改变时被绘制,则设置该值。此时应该检查itemState 成员,以确定控件是否处于选中状态。

itemState
指定了当前绘制操作完成后,所绘项的可见状态。例如,如果菜单项应该被灰色显示,则可以指定ODS_GRAYED状态标志。其取值可以为下表中所示值的一个或者多个的联合。

ODS_CHECKED:如果菜单项将被选中,则可设置该值。该值只对菜单项有用。
ODS_COMBOBOXEDIT:在自绘组合框控件中只绘制选择区域。
ODS_DEFAULT:默认值。
ODS_DISABLED:如果控件将被禁止,则设置该值。
ODS_FOCUS:如果控件需要输入焦点,则设置该值。
ODS_GRAYED:如果控件需要被灰色显示,则设置该值。该值只在绘制菜单时使用。
ODS_HOTLIGHT:Windows 98/Me, Windows 2000/XP: 如果鼠标指针位于控件之上,则设置该值,这时控件会显示高亮颜色。
ODS_INACTIVE:Windows 98/Me, Windows 2000/XP: 表示没有激活的菜单项。
ODS_NOACCEL:Windows 2000/XP: 控件是否有快速键盘。
ODS_NOFOCUSRECT:Windows 2000/XP: 不绘制捕获焦点的效果。
ODS_SELECTED:选中的菜单项。

hwndItem
指定了组合框、列表框和按钮等自绘控件的窗口句柄;如果自绘的对象时菜单项,则表示包含该菜单项的菜单句柄。

hDC
指定了绘制操作所使用的设备环境。

rcItem
指定了将被绘制的矩形区域。这个矩形区域就是上面hDC的作用范围。系统会自动裁剪组合框、列表框或按钮等控件的自绘制区域以外的部分。也就是说rcItem中的坐标点(0,0)指的就是控件的左上角。但是系统不裁剪菜单项,所以在绘制菜单项的时候,必须先通过一定的换算得到该菜单项的位置,以保证绘制操作在我们希望的区域中进行。

itemData
对于菜单项,该成员的取值可以是由

CMenu::AppendMenu、
CMenu::InsertMenu或者
CMenu::ModifyMenu

等函数传递给菜单的值。

对于列表框或这组合框,该成员的值可以为由

ComboBox::AddString、
CComboBox::InsertString、
CListBox::AddString或者
CListBox::InsertString

等传递给控件的值。

如果ctlType 的取值是ODT_BUTTON或者ODT_STATIC, itemData的取值为0。

 
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值