以 DirectUI 方式实现的ImageButton

简介:     【文章归类】 C++,Windows 应用程序开发。     这是一篇比较简单的文章,主要讲解的是用 DirectUI 方式实现的对话框上的按钮。例如,QQ界面上的按钮。我在前一篇文章中讲解的 PS 油画滤镜的参数对话框中使用这种方式实现了放大缩小按钮。

    【文章归类】 C++,Windows 应用程序开发。

    这是一篇比较简单的文章,主要讲解的是用 DirectUI 方式实现的对话框上的按钮。例如,QQ界面上的按钮。我在前一篇文章中讲解的 PS 油画滤镜的参数对话框中使用这种方式实现了放大缩小按钮。界面截图如下所示:

    img_314cd600e9e196d12dcdbb75c2a32ad9.jpg

    这种实现在早期我是直接写在窗口过程中的,这样的话是面向过程的方式,代码不容易移植复用。因此现在我在以前实现的基础上,把代码逻辑提取出来,放到一个类中,这样就会很方便在不同项目和场合使用。当然,由于窗口过程和考虑到代码效率的关系,实际上我封装的并不彻底,对于使用者来说依然需要做一些工作。

    按钮通常有三种状态:普通,悬浮,按下,如果再加上禁用(灰化),则一共是四种状态,这里简称其为四态按钮。为此,我们需要为每种状态准备一个图片,这里我们把四副图片横向拼合成一副位图(宽度是高度的四倍)。同时,考虑到和背景融合的关系,简单的话,我们可以采用 TransparentBlt 做透明色贴图,但这样和背景的融合会有锯齿感。因此我使用的是 AlphaBlend 函数,这样就要求提供的图片是 32 BPP 且已经预先应用了 Alpha 通道的位图(预应用Alpha 这一部是我用之前的 DEMO 程序完成的)。

    按钮的四个状态图片的内容和按钮的大小,都可以由用户随意定制。这里我采用的制作方法是:其他三种状态以普通状态位图为基准进行制作。悬浮状态比其他状态向左上角各移动1个像素距离(这样鼠标移动到上面和移开时,按钮产生一种浮起动画效果),灰色状态的位图是普通状态的去色结果。制作好的位图资源如下所示:

    

    img_fc374e3547742f07b6e0bdf7a3c20e3b.jpg

    这样我们在项目中添加一个类,取名为CImgButton。其实现代码如下:

    (1)头文件:

img_1c53668bcee393edac0d7b3b3daff1ae.gif img_405b18b4b6584ae338e0f6ecaf736533.gif 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)代码文件:

img_1c53668bcee393edac0d7b3b3daff1ae.gif img_405b18b4b6584ae338e0f6ecaf736533.gif 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)按照如下方式修改窗口过程(假设我们添加一个放大按钮到对话框上):

img_1c53668bcee393edac0d7b3b3daff1ae.gif img_405b18b4b6584ae338e0f6ecaf736533.gif 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

目录
相关文章
|
11月前
|
Android开发
Android 中使用RadioGroup+Fragment实现底部导航栏的功能
Android 中使用RadioGroup+Fragment实现底部导航栏的功能
82 0
|
XML Android开发 数据格式
【Android】TextView如何实现走马灯的效果
为了增加自己学习Android的兴趣(上课教的都是些基础的组件与属性,也当时扩展学习),也为期末的课程设计做准备,我在网上学习一些好玩的Android效果,并做记录分享在笔记里,大家一起学习一起进步鸭😆
199 0
|
Android开发 容器
Android自定义TabLayout后ViewPager与TabLayout互相控制切换
Android自定义TabLayout后ViewPager与TabLayout互相控制切换 正常的Android原生TabLayout与ViewPager搭配使用,当TabLayout调用setupWithViewPager与ViewPager互相捆绑以后,就实现了原生的TabLayout与ViewPager的互相控制。
2123 0
|
Android开发 数据格式 XML
Android ToggleButton:状态切换的Button
 Android ToggleButton:状态切换的Button Android ToggleButton和Android Button类似,但是ToggleButton提供了一种选择机制,可以表达Button处于何种状态,比如常见的WiFi打开或者关闭状态等等这种类似与非门的状态机。
1040 0
|
XML Android开发 数据格式