循序漸進實現仿QQ界面(四):圓形按鈕與工具欄自繪(轉載)

這一篇本來應該演示如何實現仿QQ界面的中間客戶區與底部工具欄,不過在實現底部工具欄的時候發現圓形按鈕與工具欄自繪有不少取巧的方法,因此加插這麼一篇,講解一下如何實現圓形按鈕和工具欄自繪。
前面幾篇都是在講解如何實現QQ頂部的標題欄,是用窗口貼圖實現,也講到底部區域會用不同的方法實現,因此這裏底部的QQ按鈕和工具欄不是在主窗口上畫圖了,而是用控件實現。並且這裏講解的方法不侷限於使用RingSDK界面庫及實現這個仿QQ界面程序,類似的效果用MFC或API都可以輕易實現。
講到圓形按鈕,大家一定會想到要實現按鈕的自繪,然而有取巧的方法,就是用靜態文本控件進行模擬,完全不需要自繪。先截個QQ的圖作爲資源,如下圖:

這張圖包含了兩個按鈕,一個收起/展開側邊欄的按鈕和彈出主菜單的QQ按鈕,兩個都是圓形按鈕。我們先建一個跟這張圖一樣大小的無邊框的子窗口,用這張圖作爲窗口背景,想來這個大家都會,用RingSDK界面庫只要調用一下SetBkgBitmap就一切搞定。然後建兩個靜態文本控件,一個13*13大小,一個36*36大小,位置正好覆蓋那兩個按鈕。靜態文本控件本來就是透明的,只要不設置文字,兩個控件就跟不存在一樣,不影響背景,但是佔據了位置卻正好可以模擬出按鈕的動作。當然,這兩個控件還要先CreateEllipticRgn,再SetWindowRgn一下,使其成爲圓形,這樣鼠標就必須進入到這個圓形按鈕區域纔會有響應動作,這下不用費勁在主窗口判斷鼠標位置了:)
首先是模擬鼠標移上去的高亮狀態,鼠標移到按鈕上,父窗口是沒有WM_MOUSEMOVE消息的,因此這裏必須要由控件處理這個消息,需要子類化這兩個靜態文本控件,MFC有個第3方的靜態文本控件,類名不記得了,是實現超文本鏈接的,如果有這個類的話,可以不用自己進行子類化,用這個類就可以了,RingSDK界面庫則已經封裝了這個功能,只要調用RingStatic::SetHyperlink就可以實現。實現這個超文本鏈接功能的原理是調用TrackMouseEvent,然後處理WM_MOUSEHOVER和WM_MOUSELEAVE消息,這兩個消息會在鼠標移入控件和離開控件各發送一次,因此不需要象前面的貼圖按鈕一樣用個標誌記錄是否繪製過按鈕的各種狀態,只需要在WM_MOUSEHOVER消息裏繪製按鈕的高亮狀態,在WM_MOUSELEAVE消息裏恢復按鈕的原始狀態就行了。界面庫的RingStatic類因爲在這兩個消息裏有自己的處理,沒有把這兩個消息轉發給父窗口,而是發送了WM_LINKHOVER和WM_LINKLEAVE兩個自定義消息,因此演示程序的繪製是在這兩個消息裏處理,實際是跟WM_MOUSEHOVER和WM_MOUSELEAVE消息一樣的。繪製代碼很簡單,把兩張用到的資源圖片貼出來,大家一看就知道了,就是把圖片的不同區域繪製到窗口。第2張圖片採用連續繪製實現了動畫顯示,因爲需要調色的關係,這張圖片是實時生成的。

C/C++ code

RINGMSG(WndQQButton,WM_LINKHOVER) { if((HWND)param.wParam == m_btn->Handle()) { int sx = BTN_PICWIDTH; if(m_bIsSideToolHide) sx += m_dibArrBtn.Width()/2; m_dibArrBtn.Draw((BTN_WIDTH - BTN_PICWIDTH)/2,0,sx,0,BTN_PICWIDTH,BTN_PICHEIGHT,BTN_PICWIDTH,BTN_PICHEIGHT); } else if((HWND)param.wParam == m_btnQQ->Handle()) { m_dibQQBtn.Draw(0,0,0,0,QQBTN_WIDTH,QQBTN_HEIGHT,QQBTN_WIDTH,QQBTN_HEIGHT); Sleep(80); m_dibQQBtn.Draw(0,0,QQBTN_WIDTH,0,QQBTN_WIDTH,QQBTN_HEIGHT,QQBTN_WIDTH,QQBTN_HEIGHT); Sleep(80); m_dibQQBtn.Draw(0,0,QQBTN_WIDTH*2,0,QQBTN_WIDTH,QQBTN_HEIGHT,QQBTN_WIDTH,QQBTN_HEIGHT); } return TRUE; } RINGMSG(WndQQButton,WM_LINKLEAVE) { if((HWND)param.wParam == m_btn->Handle()) { int sx = 0; if(m_bIsSideToolHide) sx = m_dibArrBtn.Width()/2; m_dibArrBtn.Draw((BTN_WIDTH - BTN_PICWIDTH)/2,0,sx,0,BTN_PICWIDTH,BTN_PICHEIGHT,BTN_PICWIDTH,BTN_PICHEIGHT); } else if((HWND)param.wParam == m_btnQQ->Handle()) { m_dibQQBtn.Draw(0,0,QQBTN_WIDTH,0,QQBTN_WIDTH,QQBTN_HEIGHT,QQBTN_WIDTH,QQBTN_HEIGHT); Sleep(80); m_dibQQBtn.Draw(0,0,0,0,QQBTN_WIDTH,QQBTN_HEIGHT,QQBTN_WIDTH,QQBTN_HEIGHT); Sleep(80); m_dibBkg.Draw(0,0,QQBTN_X,QQBTN_Y,QQBTN_WIDTH,QQBTN_HEIGHT,QQBTN_WIDTH,QQBTN_HEIGHT); } return TRUE; }


代碼裏面的m_dibXXX幾個對象在初始化的時候設定了繪製目標窗口是兩個靜態文本控件,因此這裏看不到GetDC之類的代碼,封裝起來了,相應的因爲目標DC是兩個靜態文本控件,座標也比較好計算。這些代碼改成GDI操作也比較容易。靜態文本控件已經設置成圓形,DC以外的圖象會繪製到父窗口,這樣正好滿足我們的要求,因爲那個小按鈕鼠標移上去會有一圈光暈(不是很明顯),而這個光暈是在按鈕範圍以外。
除去初始化創建靜態文本控件,圖片調色和子類化靜態文本控件代碼以外,只要響應上面兩個消息就實現了QQ2009左下角兩個按鈕的模擬,是不是很簡單?子類化靜態文本控件的代碼可以去看RingSDK界面庫的ringstatic.cpp或者是擴展的MFC超文本鏈接控件類的代碼。QQ2009的這兩個按鈕沒有按下的狀態,但是很容易添加實現,演示程序裏的相關代碼也很容易就可以改成MFC或API的,可以實現自己的比較酷的按鈕效果。順便說一下,創建的靜態文本控件必須帶上SS_NOTIFY窗口類型,否則點擊後父窗口是收不到消息的。父窗口的窗口類型不需要帶上WS_CLIPCHILDREN和WS_CLIPSIBLINGS類型,否則不會繪製靜態文本控件佔據的區域背景。
接下來說一下工具欄的自繪。
實現自繪工具欄一種方法是子類化,自己處理其WM_PAINT消息。但是工具欄按鈕的狀態實在太多,按鈕形態,扁平形態,高亮,按下,不可用等等狀態,是否顯示文字,是否顯示下拉箭頭等等,要做出一個通用的自繪工具欄實在是一件很麻煩的事。這裏教你一個取巧的辦法,以QQ2009的工具欄爲例,子類化工具欄是逃不掉的,但是隻要處理WM_ERASEBKGND消息,刷上你要的背景就可以了,不需要處理WM_PAINT消息。先創建工具欄,帶上TBSTYLE_FLAT類型,扁平按鈕,然後準備好如下兩張圖片:


接下來,下面所列出的代碼是調用的API:
C/C++ code

HBITMAP = LoadBitmap(...); HIMAGELIST himg = ImageList_Create(20,20,ILC_COLOR32|ILC_MASK,9,4); //這裏的參數請根據圖片自行調整 ImageList_AddMasked(himg,hbm,0x00FF00FF); //指定紫紅爲透明色 SendMessage(hWndToolbar,TB_SETIMAGELIST,0,(LPARAM)himg);


另一張圖片如法炮製,SendMessage(m_hWndToolbar,TB_SETHOTIMAGELIST,0,(LPARAM)himg);設置爲高亮圖案。然後發送TB_INSERTBUTTON消息給工具欄添加按鈕,TBBUTTON結構的iBitmap設置爲圖片中相應按鈕圖案的序號(從0開始),這樣生成的工具欄按鈕圖案是不侷限於256色的。鼠標移到按鈕上,就顯示爲第2張圖相應的高亮圖案,但是按鈕周圍還是有一個邊框,這是系統幫你畫的,高亮圖案上已經畫了個漂亮的邊框,我們不需要這個邊框,怎麼去掉呢,父窗口響應NM_CUSTOMDRAW的WM_NOTIFY消息:
C/C++ code

case WM_NOTIFY: { LPNMHDR lpnm = (LPNMHDR)lParam; if(lpnm->code == NM_CUSTOMDRAW) { LPNMCUSTOMDRAW lpnmdraw = (LPNMCUSTOMDRAW)lParam; if(lpnmdraw->dwDrawStage == CDDS_PREPAINT) return CDRF_NOTIFYITEMDRAW; //指定通知按鈕自繪 else if(lpnmdraw->dwDrawStage == CDDS_ITEMPREPAINT) return TBCDRF_NOEDGES; //其餘交給系統繪製,指定不需要繪製按鈕外邊框 } break; }


要求自繪按鈕時返回TBCDRF_NOEDGES,系統就不繪製按鈕邊框了,這樣這個工具欄乍看就跟QQ2009的工具欄一樣了。但是按鈕按下去的狀態就露餡了,因爲高亮圖案上畫的邊框也跟着圖標一起按下去了,這個邊框畢竟不是真的邊框,只是高亮圖案罷了。當然如果這個效果你能接受,那也可以了,至此打住,可以省不少事。
現在要使按鈕按下去僅按下圖標,邊框位置不動。可是工具欄只能設置兩個IMAGELIST,沒有按下狀態的IMAGELIST可以設置,因此只能自繪,就需要判斷lpnmdraw->uItemState標誌,如果是CDIS_SELECTED,就自繪按鈕的按下狀態,可是要自繪按鈕,又很麻煩了,畫邊框,畫圖標,畫文字...,嘿嘿,這裏又有取巧的方法,我們只要畫個按下狀態的邊框就可以了,還是返回TBCDRF_NOEDGES,其他的系統會幫我們畫的。不過系統幫我們畫上去的是高亮圖案,又有一個按下去的邊框,所以我們要取消高亮圖案,創建工具欄的時候只需要發送TB_SETIMAGELIST消息設置按鈕圖標就可以了,高亮和按下的邊框我們自己畫,準備好下面一張圖:

左邊是高亮邊框,右邊是按下的邊框。自繪時根據lpnmdraw->uItemState標誌把這張圖的相應邊框畫上去:
C/C++ code

case WM_NOTIFY: { LPNMHDR lpnm = (LPNMHDR)lParam; if(lpnm->code == NM_CUSTOMDRAW) { LPNMCUSTOMDRAW lpnmdraw = (LPNMCUSTOMDRAW)lParam; if(lpnmdraw->dwDrawStage == CDDS_PREPAINT) return CDRF_NOTIFYITEMDRAW; //指定通知按鈕自繪 else if(lpnmdraw->dwDrawStage == CDDS_ITEMPREPAINT) { int sx; int off = (lpnmdraw->rc.bottom - lpnmdraw->rc.top - 20)/2; if((lpnmdraw->uItemState & CDIS_SELECTED)) sx = 20; else if((lpnmdraw->uItemState,CDIS_HOT)) sx = 0; else return TBCDRF_NOEDGES; HDC hMemDC = CreateCompatibleDC(lpnmdraw->hdc); SelectObject(hMemDC,hbm); //hbm爲事先加載的邊框圖案的HBITMAP BitBlt(lpnmdraw->hdc,lpnmdraw->rc.left + off,lpnmdraw->rc.top + off,20,20,hMemDC,sx,0,SRCCOPY); DeleteDC(hMemDC); return TBCDRF_NOEDGES; //其餘交給系統繪製,指定不需要繪製按鈕外邊框 } } break; }


OK,就這麼簡單,現在這個工具欄就跟QQ2009的工具欄的效果一樣了。
現在說說演示程序裏的工具欄實現,代碼差不多,但是實際複雜得多,因爲界面庫對此進行了封裝。界面庫實現了任意子窗口和控件都可以停靠,因此QQ按鈕窗口和工具欄是作爲停靠窗口停靠在主界面的下方,只是設置了不能拖動。因爲是停靠窗口,所以你在主窗口的WM_SIZE消息裏看不到移動QQ按鈕窗口和工具欄的代碼,封裝起來了。這個不是本篇重點,有興趣的可以去看界面庫裏ringdocksite.cpp的代碼。
現在看看程序的截圖:

變化不是很大,本篇旨在教大家一個自繪按鈕和自繪工具欄的比較簡單的方法,不知道說清楚沒有,有問題或是不同意見歡迎留言討論。下一篇會實現QQ2009的客戶區域和左下角按鈕彈出的異型菜單。
演示程序下載地址:http://d.download.csdn.net/down/2025754/ringphone
本文來自CSDN博客,轉載請標明出處:http://blog.csdn.net/ringphone/archive/2010/01/26/5259819.aspx
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章