以 DirectUI 方式实现的ImageButton
原文链接: http://www.cnblogs.com/hoodlum1980/archive/2011/02/15/1954779.html
这是一篇比较简单的文章,主要讲解的是用 DirectUI 方式实现的对话框上的按钮。例如,QQ界面上的按钮。我在前一篇文章中讲解的 PS 油画滤镜的参数对话框中使用这种方式实现了放大缩小按钮。界面截图如下所示:
    
这种实现在早期我是直接写在窗口过程中的,这样的话是面向过程的方式,代码不容易移植复用。因此现在我在以前实现的基础上,把代码逻辑提取出来,放到一个类中,这样就会很方便在不同项目和场合使用。当然,由于窗口过程和考虑到代码效率的关系,实际上我封装的并不彻底,对于使用者来说依然需要做一些工作。
按钮通常有三种状态:普通,悬浮,按下,如果再加上禁用(灰化),则一共是四种状态,这里简称其为四态按钮。为此,我们需要为每种状态准备一个图片,这里我们把四副图片横向拼合成一副位图(宽度是高度的四倍)。同时,考虑到和背景融合的关系,简单的话,我们可以采用 TransparentBlt 做透明色贴图,但这样和背景的融合会有锯齿感。因此我使用的是 AlphaBlend 函数,这样就要求提供的图片是 32 BPP 且已经预先应用了 Alpha 通道的位图(预应用Alpha 这一部是我用之前的 DEMO 程序完成的)。
按钮的四个状态图片的内容和按钮的大小,都可以由用户随意定制。这里我采用的制作方法是:其他三种状态以普通状态位图为基准进行制作。悬浮状态比其他状态向左上角各移动1个像素距离(这样鼠标移动到上面和移开时,按钮产生一种浮起动画效果),灰色状态的位图是普通状态的去色结果。制作好的位图资源如下所示:
    
这样我们在项目中添加一个类,取名为CImgButton。其实现代码如下:
(1)头文件:
ImgButton.h
#pragma once
#include <windows.h>
enum BUTTON_STATES
{
    STATE_NORMAL = 0,
    STATE_HOVER = 1,
    STATE_DOWN = 2,
    STATE_GRAY = 3,
};
class CImgButton
{
public:
    CImgButton(void);
    ~CImgButton(void);
private:
    int m_left;
    int m_top;
    int m_width;
    int m_height;
    int m_state; //当前所处的状态:0-正常;1-鼠标悬浮;2-按下;3-禁止;
    int m_buttonId; //点击时,发送给父窗口的消息
    RECT m_bounds;
    HWND m_hwndParent;
    BOOL m_bIsTracking; //是否正在被跟踪(鼠标按下时)
    BOOL m_bMouseDown; //鼠标是否按下
    BOOL m_bEnabled;
    HBITMAP m_hBitmap; //四个状态的位图
public:
    void SetParentWnd(HWND hParent);
    void SetBitmap(HBITMAP hBitmap);
    void SetBitmap(HINSTANCE hInst, LPCTSTR lpBitmapName);
    void SetBounds(int left, int top, int width, int height);
    void SetButtonID(int buttonId);
    void EnableWindow(BOOL bEnabled);
    void OnMouseDown(int x, int y);
    void OnMouseUp(int x, int y);
    void OnMouseMove(int x, int y);
    void OnMouseLeave();
    void OnPaint(HDC hdc);
    void OnPaint(HDC hdc, HDC hMemDC);
//下面的GET函数有用
    BOOL IsTracking() const { return m_bIsTracking; };
//下面这些是一些用处不大的GET函数
    HWND GetParentWnd() const { return m_hwndParent; };
    HBITMAP GetBitmap() const { return m_hBitmap; };
    int GetLeft() const { return m_left; };
    int GetTop() const { return m_top; };
    int GetWidth() const { return m_width; };
    int GetHeight() const { return m_height; };
    int GetButtonID() const { return m_buttonId; };
    BOOL GetEnabled() const { return m_bEnabled; };    
};
(2)代码文件:
ImgButton.cpp
#include "StdAfx.h"
#include "ImgButton.h"
#pragma comment(lib, "Msimg32.lib")
CImgButton::CImgButton(void)
{
    this->m_bIsTracking = FALSE;
    this->m_bMouseDown = FALSE;
this->m_hBitmap = NULL;
    this->m_hwndParent = NULL;
this->m_bEnabled = TRUE;
    this->m_state = STATE_NORMAL;
this->m_buttonId = 0;
}
CImgButton::~CImgButton(void)
{
    if(this->m_hBitmap != NULL)
    {
        DeleteObject(this->m_hBitmap);
        this->m_hBitmap = NULL;
    }
}
void CImgButton::SetParentWnd(HWND hParent)
{
    this->m_hwndParent = hParent;
}
void CImgButton::SetBitmap(HBITMAP hBitmap)
{
    if(this->m_hBitmap != NULL)
    {
        DeleteObject(this->m_hBitmap);
    }
    this->m_hBitmap = hBitmap;
}
void CImgButton::SetBitmap(HINSTANCE hInst, LPCTSTR lpBitmapName)
{
    if(this->m_hBitmap != NULL)
    {
        DeleteObject(this->m_hBitmap);
    }
    this->m_hBitmap = LoadBitmap(hInst, lpBitmapName);
}
void CImgButton::SetBounds(int left, int top, int width, int height)
{
    this->m_left = left;
    this->m_top = top;
    this->m_width = width;
    this->m_height = height;
this->m_bounds.left = left;
    this->m_bounds.top = top;
    this->m_bounds.right = left + width;
    this->m_bounds.bottom = top + height;
}
void CImgButton::SetButtonID(int buttonId)
{
    this->m_buttonId = buttonId;
}
void CImgButton::EnableWindow(BOOL bEnabled)
{
    if(this->m_bEnabled != bEnabled)
    {
        this->m_bEnabled = bEnabled;
        this->m_state = bEnabled ? STATE_NORMAL : STATE_GRAY;
InvalidateRect(this->m_hwndParent, &this->m_bounds, TRUE);
    }
}
void CImgButton::OnMouseDown(int x, int y)
{
    POINT pt;
    pt.x = x;
    pt.y = y;
int tmpState = this->m_state;
    this->m_bMouseDown = TRUE;
if(this->m_bEnabled)
    {
        if(PtInRect(&this->m_bounds, pt))
        {
            this->m_bIsTracking = TRUE;
            tmpState = STATE_DOWN; //按下
            SetCapture(this->m_hwndParent);            
        }
        else
        {
            this->m_bIsTracking = FALSE; 
        }
    }
    else
    {
        tmpState = STATE_GRAY;
    }
if(this->m_state != tmpState)
    {
        this->m_state = tmpState;
            InvalidateRect(this->m_hwndParent, &this->m_bounds, TRUE);
    }            
}
void CImgButton::OnMouseUp(int x, int y)
{
    POINT pt;
    pt.x = x;
    pt.y = y;
int tmpState = this->m_state;
    this->m_bMouseDown = FALSE;
if(this->m_bEnabled)
    {
        ReleaseCapture(); //释放鼠标
        if(PtInRect(&this->m_bounds, pt))
        {
            tmpState = STATE_HOVER; //悬浮;
//在按钮内抬起,发送消息给父窗口!
            if(this->m_bIsTracking && this->m_buttonId != 0)
            {
                SendMessage(this->m_hwndParent, WM_COMMAND, MAKELONG(this->m_buttonId, 0), 0);
            }
        }
        else
        {
            tmpState = STATE_NORMAL;
        }
    }
    else
    {
        tmpState = STATE_GRAY;
    }
this->m_bIsTracking = FALSE;
    if(this->m_state != tmpState)
    {
        this->m_state = tmpState;
        InvalidateRect(this->m_hwndParent, &this->m_bounds, TRUE);
    }
}
void CImgButton::OnMouseMove(int x, int y)
{
    POINT pt;
    pt.x = x;
    pt.y = y;
BOOL bMouseOnButton = PtInRect(&this->m_bounds, pt);
int tmpState = this->m_state;
    if(this->m_bEnabled)
    {
        if(this->m_bMouseDown)
        {
            if(this->m_bIsTracking)
            {
                tmpState = bMouseOnButton? STATE_DOWN : STATE_HOVER;
            }
        }
        else
        {
            //鼠标在抬起状态下的,普通热点跟踪
            tmpState = bMouseOnButton? STATE_HOVER : STATE_NORMAL;
        }        
    }
    else
    {
        tmpState = STATE_GRAY;
    }
if(this->m_state != tmpState)
    {
        this->m_state = tmpState;
        InvalidateRect(this->m_hwndParent, &this->m_bounds, TRUE);
    }
}
//鼠标离开窗口(仅仅在悬浮状态下需要处理)
void CImgButton::OnMouseLeave()
{
    if(this->m_state == STATE_HOVER)
    {
        this->m_state = STATE_NORMAL;
        InvalidateRect(this->m_hwndParent, &this->m_bounds, TRUE);
    }
}
void CImgButton::OnPaint(HDC hdc)
{
    if(this->m_hBitmap == NULL)
        return;
HDC hMemDC = CreateCompatibleDC(hdc);
    HGDIOBJ hOldBitmap = SelectObject(hMemDC, this->m_hBitmap);
BLENDFUNCTION blendFunc;
    blendFunc.BlendOp = AC_SRC_OVER;
    blendFunc.BlendFlags = 0;
    blendFunc.SourceConstantAlpha = 255; //整体的不透明度
    blendFunc.AlphaFormat = AC_SRC_ALPHA;
    AlphaBlend(hdc, this->m_left, this->m_top, this->m_width, this->m_height,
        hMemDC, this->m_width * this->m_state, 0, this->m_width, this->m_height, blendFunc);
SelectObject(hMemDC, hOldBitmap);
    DeleteDC(hMemDC);
}
void CImgButton::OnPaint(HDC hdc, HDC hMemDC)
{
    if(this->m_hBitmap == NULL)
        return;
HGDIOBJ hOldBitmap = SelectObject(hMemDC, this->m_hBitmap);
BLENDFUNCTION blendFunc;
    blendFunc.BlendOp = AC_SRC_OVER;
    blendFunc.BlendFlags = 0;
    blendFunc.SourceConstantAlpha = 255; //整体的不透明度
    blendFunc.AlphaFormat = AC_SRC_ALPHA;
    AlphaBlend(hdc, this->m_left, this->m_top, this->m_width, this->m_height,
        hMemDC, this->m_width * this->m_state, 0, this->m_width, this->m_height, blendFunc);
SelectObject(hMemDC, hOldBitmap);
}
注意,这个类我们主要需要做的工作如下,处理以下消息:WM_PAINT, WM_LBUTTONDOWN / WM_LBUTTONDBLCLK, WM_MOUSEMOVE, WM_LBUTTONUP, WM_MOUSELEAVE。需要说明的是以下几点:
(A)关于 WM_MOUSELEAVE 消息。
该消息在鼠标离开窗口时发送给窗口,我们需要处理这个消息主要是基于鼠标移动时间的“离散性”。即鼠标事件有以下特点,当鼠标移动的速度很快时,相邻的鼠标移动消息的跨距就会比较大。通常我们是用鼠标移动消息去更新按钮的状态的,如果按钮正处于热点跟踪的悬浮状态时,当鼠标快速离开时,很可能无法恢复成正常状态(因为未能收到后续鼠标移动消息),因此我们需要处理鼠标离开的消息。在这个消息里我们主要是把悬浮状态的按钮复原成正常状态。
另外必须注意的是,处于效率考虑,系统默认不会给窗口发送该消息。因此我们必须用 TrackMouseEvent 函数请求系统为我们发送该消息,且该函数是一次性的,即消息收到一次以后,TrackMouseEvent 就会失效。所以我们需要在鼠标移动事件中重复调用该函数。
(B)关于 WM_LBUTTONDBLCLK 消息。
这是鼠标双击事件,当鼠标以很快频率在对话框窗口上双击时,窗口过程就会收到这个消息(因为窗口类中含有 CS_DBLCLK 样式)。这样可能和点击按钮的预期不符(用户希望的是连续快速的点击按钮),即我们不希望系统把快速点击转换成双击事件。考虑双击事件的消息顺序是:按下,抬起,双击,抬起。因此我们在这里只要把双击消息等效的看着鼠标按下事件处理即可。
(C)按钮在按下时的追踪(m_bIsTracking)
在上面的类中有这样一个成员变量:m_bIsTracking。这个变量是干什么用的呢,我需要更多的解释以下。观察 windows 操作系统的按钮的交互方式可知,用户在按钮上按下鼠标并保持按下状态,这时不要放开鼠标并移动鼠标时,按钮的外观就会根据鼠标是否停留在按钮上而发生变化。如果用户在按钮以外抬起鼠标,按钮事件不会触发。如果在按钮之内抬起鼠标,就会触发按钮事件。注意,这是 windows 系统中按钮的用户交互的一个小细节(我在之前写的文章中提到过),这样做的主要好处是给用户提供了一种“撤销点击按钮事件”的选择,即按下按钮以后如果不希望点击按钮,则把鼠标从按钮上滑开再放开鼠标按键即可。
注意,我们通常说的热点跟踪通常是指鼠标在没有按下时进行移动。这时对话框上所有按钮都会对鼠标移动进行外观反馈,我把这种情况(普通热点跟踪)称为当前没有需要跟踪外观反馈的对象。如果鼠标在某个按钮上按下然后保持,这时该按钮就成为一个被跟踪外观反馈的对象,这时只有该对象会对鼠标移动进行外观上的响应,所有其他非跟踪对象一律不对鼠标移动做任何反馈。因此,这是该按钮的 m_bIsTracking 变量就会为 TRUE,表示该按钮是一个被跟踪反馈的对象(鼠标按下时期),该对象应该是具有排他性的,即鼠标按下时,任何时刻至多只有一个按钮被跟踪。这里的 Tracking 专指鼠标按下时期的跟踪对象。同时这个变量也有另一个含义,即表示鼠标按下时是否在该按钮内部按下,如果该变量为 TRUE,则表示鼠标一定是在该按钮上按下的。如果是这样,则只要鼠标抬起时仍然处于按钮内部,即触发按钮事件。
有了以上代码以后,我们把它加入已有项目就会很方便了。但是我们依然需要在窗口过程中做一些手工工作。主要有以下步骤:
(1)把ImgButton代码添加到项目。
(2)用图像处理软件制作一个预先应用了Alpha通道的位图资源,添加到项目中。
(2)按照如下方式修改窗口过程(假设我们添加一个放大按钮到对话框上):
wndproc
#include <windowsx.h> //for GET_X_LPARAM
BOOL WINAPI MyWndProc(HWND hDlg, UINT wMsg, WPARAM wParam, LPARAM lParam)
{
    static CImgButton *pBtnZoomIn;
    static TRACKMOUSEEVENT tme;
switch(wMsg)
    {
    case WM_INITDIALOG:
        {
            //Direct UI (ImgButton)
            tme.cbSize = sizeof(TRACKMOUSEEVENT);
            tme.dwFlags = TME_LEAVE; //我们需要WM_LEAVE;
            tme.hwndTrack = hDlg;
pBtnZoomIn = new CImgButton();
            pBtnZoomIn->SetParentWnd(hDlg);
            pBtnZoomIn->SetButtonID(IDC_BT_ZOOMIN);
            pBtnZoomIn->SetBounds(100, 20, 24, 24);
            pBtnZoomIn->SetBitmap(hInst, MAKEINTRESOURCE(IDB_ZOOMIN));
        }
        return TRUE;
case WM_COMMAND:
        {
            WORD ctlid = LOWORD(wParam);        
            switch(ctlid)
            {
            case IDC_BT_ZOOMIN: //放大
                {
                    //在这里处理按钮事件
                }
                return TRUE;
            }
        }
        break;
case WM_PAINT:
        {
            PAINTSTRUCT ps;
            HDC hdc, hMemDC;
            hdc = BeginPaint(hDlg, &ps);
            hMemDC = CreateCompatibleDC(hdc);
            pBtnZoomIn->OnPaint(hdc, hMemDC);
            DeleteDC(hMemDC);
            EndPaint(hDlg, &ps);
        }
        return TRUE;
case WM_LBUTTONDBLCLK: 
    case WM_LBUTTONDOWN:
        {
            int x = GET_X_LPARAM(lParam);
            int y = GET_Y_LPARAM(lParam);
            pBtnZoomIn->OnMouseDown(x, y);
        }
        return TRUE;
case WM_LBUTTONUP:
        {
            int x = GET_X_LPARAM(lParam);
            int y = GET_Y_LPARAM(lParam);
            pBtnZoomIn->OnMouseUp(x, y);
        }
        return TRUE;
case WM_MOUSEMOVE:
        {
            int x = GET_X_LPARAM(lParam);
            int y = GET_Y_LPARAM(lParam);
            pBtnZoomIn->OnMouseMove(x, y);
            TrackMouseEvent(&tme);
        }
        return TRUE;
case WM_MOUSELEAVE:
        {
            pBtnZoomIn->OnMouseLeave();
        }
        return TRUE;
case WM_DESTROY:
        {
            delete pBtnZoomIn;
        }
        return TRUE;
    }
    return FALSE;
}
在上面的使用过程中,主要是初始化工作,即设置按钮的坐标位置,图片资源。以及按钮的ID(相当于普通控件的ID,通过这个数字,向所在窗口发送 WM_COMMAND 消息)。在WM_PAINT消息中,我主动创建了一个内存DC,目的是当有多个这样的 ImageButton 时,可以重复使用该内存DC,而不必每次都再次创建。
当修改窗口过程时,原窗口过程中没有列出的消息可以简单的添加到窗口过程里,而有的消息可能在窗口过程中已经列出,这就需要和已有代码进行合并。这里可能需要一些和现有代码逻辑的配合,但总体来讲这个融合过程依然是比较容易的。
通过以上步骤,我们就用 DirectUI 方式实现了标准四态按钮。以上代码原理并不复杂,这里再提供一个很小的DEMO程序:
http://files.cnblogs.com/hoodlum1980/ImgButtonDemo.rar
【补充--by hoodlum1980;on 2011年5月15日】
上面的代码中仅仅是一个比较简单的演示,后续过程中我又在代码中增加了对 Toggle Button(行为类似CheckBox),ShowWindow, EnableWindow 等方法的支持,增加了从 PNG 文件加载按钮图片的支持,增加了 ApplyAlpha 方法(这样就不必对图片资源预先应用alpha通道,只要是 32 bpp的图片即可)。
改进后的 CImgButton 代码和使用说明请从下面下载:
http://files.cnblogs.com/hoodlum1980/CImgButton.rar
补充下:在现有代码中存在的一个瑕疵。在CImgButton类的 OnLButtonUp 函数里,应该使用 PostMessage 而不是 SendMessage。(因为SendMessage的处理中如果用编码方式改变了按钮状态,SendMessage 因为必须处理结束才能返回,这可能导致按钮的视图不能得到正确的更新)。用PostMessage时,由于处理 posted message 的时机晚于 OnLButtonUp,因此按钮的外观可以在随后的消息处理中得到正确更新。
----
这段话说得有点绕,由于改动很小,所以用评论补充,源文件就暂时先不更新了。
以 DirectUI 方式实现的ImageButton的更多相关文章
- Button和ImageButton
		
Button----button ImageButton----图片button 共同拥有特征: 都能够作为一个button产生点击事件 不同点 1. Button有text的属性.ImageButt ...
 - Wince下实现ImageButton
		
我们在winform中给按钮设置个背景图片超级简单,是不?可是在wince下面就没那么简单了,下面我来介绍一种方式来实现ImageButton. 实现思路是重新写一个usercontrol就ok.具体 ...
 - windowsUI的总结
		
1,MFC 基于VC6.0的微软基础库 2,WPF 做绚丽界面一律用WPF,做一般绚丽界面用WinForm,做windows标准界面用MFC WPF也有个致命缺点,就是要.net framework支 ...
 - Direct UI 思想阐述(好多相关文章)
		
在界面开发中,目前DirectUI是个热门的技术名称,因为众多的知名公司都是用DirectUI方式作出了很炫丽的界面.而对于大多数熟悉Win32控件,熟悉MFC开发的开发人员来说,我们应该做何选择? ...
 - Direct UI
		
在界面开发中,眼下DirectUI是个热门的技术名称,由于众多的知名公司都是用DirectUI方式作出了非常炫丽的界面.而对于大多数熟悉Win32控件,熟悉MFC开发的开发者来说,我们应该做何选择? ...
 - windows下VC界面 DIY系列1----写给想要写界面的C++程序猿的话
		
非常早就想写关于C++ UI开发的一系列博文,博客专栏刚审核通过,就立即開始刷博文,不能辜负自己的一番热血,我并非写界面的高手,仅仅想通过写博文提高我自己的技术积累,也顺便帮助大家解决界面开发的瓶颈. ...
 - QWidget之Alien与Native小记(果然是DirectUI的方式,QWidget居然提供了nativeParentWidget函数,而且可以动态设置native父窗口)good
		
在QWidget 之paint部分杂记提到了从Qt4.4开始,Alien Widget被引入.那么...这是什么东西呢,我们在使用中又可能会感受到什么东西? 用例子来说话似乎比用源码来说话来得容易,所 ...
 - WebForm(二)——控件和数据库连接方式
		
一.简单控件 1.Label(作用:显示文字) Web中: <asp:Label ID="Label1" runat="server" Text=&quo ...
 - ASP.NET中的Image和ImageButton控件
		
Image 控件用来显示图形.Image 控件可以显示来自位图.图标或元文件的图形,也可以显示增强的元文件.JPEG 或 GIF文件. ImageButton 控件用于显示可点击的图像. Image ...
 
随机推荐
- 你应该知道的30个jQuery代码开发技巧
			
1. 创建一个嵌套的过滤器 .filter(":not(:has(.selected))") //去掉所有不包含class为.selected的元素 2. 重用你的元素查询 var ...
 - 牛气冲天的Iframe应用笔记
			
纵观时下网站,本来网速就有些慢,可是几乎每页都要放什么Banner,栏目图片,版权等一大堆雷同的东西,当然,出于网站风格统一.广告效应的需要,本无可厚非,可毕竟让用户的钱包为这些“点缀“的东西”日益消 ...
 - Android的硬件抽象层模块编写规范
			
硬件抽象层模块编写规范  Android系统的硬件抽象层以模块的形式来管理各个硬件訪问接口.每个硬件模块都相应有一个动态链接库文件.这些动态链接库文件的命令须要符合一定的规范.同一时候,在系统内部. ...
 - (转)U3D DrawCall优化手记
			
自:http://www.cnblogs.com/ybgame/p/3588795.html 在最近,使用U3D开发的游戏核心部分功能即将完成,中间由于各种历史原因,导致项目存在比较大的问题,这些问题 ...
 - JAVA 读取计算机中相关信息
			
java读取 计算机 cup号 读取版本号 显卡 .. . . ........ .. . . . package com.swt.common.util; import java.io.Buffer ...
 - JSON字符串转换为Map
			
本文是利用阿里巴巴封装的FastJSON来转换json字符串的.例子如下: package com.zkn.newlearn.json; import com.alibaba.fastjson.JSO ...
 - cocos2d-x -3.81+win7+vs2013开发环境创建新的项目
			
cocos2d-x -3.81+win7+vs2013开发环境创建新的项目 1.准备阶段 (1) vs2013下载及安装 (2)cocos2d-x 3.8.1下载及解压 (3)python下载及安装( ...
 - Android指令处理流程源代码追踪
			
1.拨号界面输入*#06#.显示IMEI号,这是怎么出来的? 2.怎样高速的找出Android平台中的指令? 1. 在DialpadFragment中的EditText注冊一个Textchanged的 ...
 - JavaScript 之 特殊运算符
			
一.=== 下面的规则用来判断两个值是否===相等: 首先,== equality 等同,=== identity 恒等. ==, 两边值类型不同的时候,要先进行类型转换,再比 ...
 - uni - 条件渲染
			
vue官方文档和uni官方同步:https://cn.vuejs.org/v2/guide/conditional.html 1.多次切换建议使用v-show(始终保存在BOM) 2.因为if是惰性判 ...