以 DirectUI 方式实现的ImageButton
【文章歸類】 C++,Windows 應用程序開發。
這是一篇比較簡單的文章,主要講解的是用 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_LPARAMBOOL 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
超強干貨來襲 云風專訪:近40年碼齡,通宵達旦的技術人生總結
以上是生活随笔為你收集整理的以 DirectUI 方式实现的ImageButton的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 知识管理促进企业组织能力提升
- 下一篇: fatal error LNK1561: