轉(zhuǎn)帖|其它|編輯:郝浩|2010-11-19 11:47:17.000|閱讀 863 次
概述:對于Windows系統(tǒng)中各種控件換膚功能,要數(shù)滾動條的換膚最難實現(xiàn)了,尤其是控件自帶的系統(tǒng)滾動條,如Edit、ListBox、ListView、TreeView等自帶的系統(tǒng)滾動條,要想實現(xiàn)其自定義的皮膚功能,用常規(guī)辦法似乎都無法實現(xiàn)。
# 界面/圖表報表/文檔/IDE等千款熱門軟控件火熱銷售中 >>
對于Windows系統(tǒng)中各種控件換膚功能,要數(shù)滾動條的換膚最難實現(xiàn)了,尤其是控件自帶的系統(tǒng)滾動條,如Edit、ListBox、ListView、TreeView等自帶的系統(tǒng)滾動條,要想實現(xiàn)其自定義的皮膚功能,用常規(guī)辦法似乎都無法實現(xiàn)。
對于常規(guī)的皮膚定制一般都是通過定制WM_PAINT、WM_ERASEBKGND、 WM_CTLCOLORxxx、NM_CUSTOMDRAW來實現(xiàn)。然而系統(tǒng)滾動條的繪制,常規(guī)的、很陽光的方法行不通,微軟把一條康莊大道堵死了。根據(jù)我的觀察測試,系統(tǒng)滾動條有許多的消息都對其執(zhí)行了繪制,這包括WM_NCPAINT、WM_NCMOUSEMOVE、WM_NCMOUSELEAVE、 WM_HSCROLL、WM_VSCROLL、WM_KEYDOWN、WM_MOUSEWHEEL等等,這些消息中有些可以定制,但有些沒法定制。比如 WM_HSCROLL就無法定制,如果我們處理這個消息,就必須自己控制滾動條的范圍、位置、翻頁值,而這些值我們通常無法得到,微軟根本沒有告訴我們,對于不同的控件它操控滾動條到底采用何種策略,不曉得。假若我們不去定制這個WM_HSCROLL,而默認的處理它又執(zhí)行滾動條的繪制,這真是進退無路,叫人束手無策。當(dāng)然不是完全沒有辦法,死路一條。為了克服障礙,翻越障礙,每個人有不同的策略,有攀巖翻越的,安全性不高;也有走小路繞行的,比較費力。
在網(wǎng)上反復(fù)搜索,自己也仔細研究,基本有兩種辦法來實現(xiàn)系統(tǒng)滾動條換膚,一種方法是HOOKAPI,也就是攔截API的辦法,還有一種是模擬法。
攔截API,實際上是修改操作系統(tǒng)的API入口。因為系統(tǒng)繪制滾動條是通過各種繪制函數(shù)來實現(xiàn)的,比如SetScrollInfo(),基本系統(tǒng)通過這一類函數(shù)實現(xiàn)滾動條的繪制,對這個API進行攔截,也就是我們自己寫一個 SetScrollInfo(),來親自實現(xiàn)滾動條的繪制,以便由此實現(xiàn)滾動條的自定義繪制,實現(xiàn)我們想要的各種風(fēng)格的皮膚外觀。API的攔截有兩種:一種是修改系統(tǒng)所裝入內(nèi)存可執(zhí)行模塊的導(dǎo)入地址,替換成我們所寫的偽API的地址,使API調(diào)用能夠自動跳轉(zhuǎn)到這個偽API上;還有一種是直接修改API函數(shù)首地址處的若干機器指令,保存現(xiàn)場,寫入跳轉(zhuǎn)指令,使系統(tǒng)在執(zhí)行到這個API時能自動跳轉(zhuǎn)到我們所寫的偽API上。我要說的是,這個攔截辦法還真是有些邪門,有安全隱患,病毒就常干這種事情。對操作系統(tǒng)進行這類暴力破解式的修改,很容易引起系統(tǒng)防火墻或反病毒軟件報錯,Windows原則上不允許干這種事;其二,一旦使用此法的程序因異常而崩潰,原本對操作系統(tǒng)的修改沒有還原,這可能會傷害到系統(tǒng)里的所有進程的穩(wěn)定性,導(dǎo)致死機或運行異常。對于商業(yè)性的工程開發(fā),往往很忌諱這一點,都傾向于追求穩(wěn)定,通常是寧用拙法,不玩巧技;其三,此法有線程同步問題:因為正當(dāng)你修改程序指令同時,若其他進程里的線程恰好執(zhí)行到這里時候問題就會爆發(fā)出來,單CPU可能沒啥問題,系統(tǒng)內(nèi)存和CPU緩沖可以做到同步,但多核系統(tǒng)就難說了;另外這個方法還有移植性問題:在一種版本的系統(tǒng)中,通過攔截API有效,在另一種版本的系統(tǒng)中,就不見得還有效,畢竟微軟實現(xiàn)滾動條的繪制,它沒規(guī)定一定就用哪個函數(shù)來實現(xiàn),也許新系統(tǒng)中它另有高招,API攔截也就不靈了。
下面我要詳細講的是模擬法了,這個辦法不用攔截API,所有的技術(shù)實現(xiàn)都約束在系統(tǒng)所允許的范圍內(nèi),咱一絲不茍的當(dāng)遵紀(jì)守法的良民。
所謂模擬法就是在系統(tǒng)滾動條的區(qū)域放置一個模擬窗口,這個窗口專門用于繪制系統(tǒng)滾動條。當(dāng)然我們也要攔截帶滾動條控件的若干消息,以確保模擬窗口的繪制正確。下面只以ListView控件的水平滾動條為例,進行說明,垂直滾動條換膚可以列推。
首先我們要在滾動條的區(qū)域上創(chuàng)建一個模擬窗口,恰好覆蓋滾動條:
HWND hListView =...;//ListView窗口的句柄
HWND pWnd = ::GetParent(hListView);
HWND hBuddy =::CreateWindowEx(WS_EX_NOACTIVATE, "Buddy_Window", "",WS_CLIPSIBLINGS|WS_DISABLED|WS_CHILD, 0, 0, 0, 0, pWnd, NULL,gModule, NULL);
Buddy_Window是你注冊的模擬窗口類。注意,窗口一定要有WS_DISABLED 風(fēng)格,這是個關(guān)鍵,這個風(fēng)格能夠確保鼠標(biāo)操作能夠透過模擬窗口,而直接操控到所覆蓋的滾動條,模擬窗口本身不接受任何鼠標(biāo)鍵盤的輸入。另外讀取滾動條的矩形以及它的各個元素的可視、可用、壓下等狀態(tài)可通過GetScrollBarInfo()這個API來完成,不過要說明一點,這個API有些Bug,大家可去下載FreeCL2.03 版源碼,里頭修正了這個問題。
完成創(chuàng)建模擬窗口之后,你要給ListView安裝一個你寫的窗口過程,以攔截各種導(dǎo)致滾動條屬性改變的種種消息:
LRESULT CALLBACK MyListViewProc(HWNDhwnd, UINT msg, WPARAM wParam, LPARAM lParam)
{
case WM_LBUTTONDOWN:
......
case WM_LBUTTONDBLCLK:
......
case WM_NCMOUSEMOVE:
......
case WM_NCMOUSELEAVE:
......
}
......
gOldListViewProc =(WNNPROC)::GetWindowLong(hListView, GWL_WNDPROC);
::SetWindowLong(hListView, GWL_WNDPROC,LONG(&MyListViewProc));//安裝窗口過程
消息處理 :
首先我們要攔截WM_NCLBUTTONDOWN 和WM_NCLBUTTONDBLCLK這兩個消息,當(dāng)你在滾動條上按下鼠標(biāo)時,就立刻觸發(fā)WM_NCLBUTTONDOWN,如果連續(xù)快速按兩下鼠標(biāo)就還有WM_NCLBUTTONDBLCLK。注意雙擊時只有一個WM_NCLBUTTONDOWN消息,而不是兩個。第二次按鼠標(biāo)會出現(xiàn)一個 WM_NCLBUTTONDBLCLK。實際上這兩個消息我們完全可以等而視之,做相同的處理。系統(tǒng)在處理這個兩個消息時,都會在其內(nèi)部處理中觸發(fā)許多其他消息,其中有若干個WM_HSCROLL、WM_CAPTURECHANGED、WM_NCMOUSELEAVE。我們主要是要處理 WM_HSCROLL,因為它最有價值。在你松開鼠標(biāo)后,系統(tǒng)發(fā)送并處理完最后一個WM_HSCROLL之后,這才從WM_NCLBUTTONDOWN或 WM_NCLBUTTONDBLCLK中返回:
if(msg == WM_NCLBUTTONDOWN || msg== WM_NCLBUTTONDBLCLK)
{
//注意默認處理會有N個WM_HSCROLL消息出現(xiàn),要等你的按下拖拽操作完成后,這個調(diào)用才返回
//在這個調(diào)用內(nèi)部,我估計系統(tǒng)會進入一種消息循環(huán),因為按住左鍵之后,WM_NCMOUSEMOVE
//和WM_NCLBUTTONUP都不再觸發(fā)了。其內(nèi)部估計是捕捉了WM_NCMOUSEMOVE消息,因之反復(fù)刷新滾動
//盒的位置,若有必要你可安裝鼠標(biāo)鉤子,以捕捉鼠標(biāo)移動消息,以及時刷新模擬窗口中滾動盒的
//的位置。若只是響應(yīng)WM_HSCROLL消息,你可能覺得滾動盒的拖拽比較滯,不平滑。
//SetWindowsHookEx(...);
LRESUTL code= ::CallWindowProc(gOldListViewProc, hListView, msg, wParam,lParam);
//UnhookWindowsHookEx(...);
returncode;
}
我建議大家去微軟的網(wǎng)站下載ControlSpy2.0 這個小工具,它用來監(jiān)視控件的消息,這東西很有用。
下面我們再看WM_HSCROLL消息,這個消息一般是系統(tǒng)處理WM_NCLBUTTONDOWN或者WM_NCLBUTTONDBLCLK時產(chǎn)生的,但有時也可能是程序刻意發(fā)送的,和滾動條操作沒有關(guān)系。
if(msg == WM_HSCROLL)
{
::CallWindowProc(gOldListViewProc, hListView, msg, wParam,lParam);
//1)讀取滾動條的數(shù)據(jù)
//2)再在模擬窗口上繪制滾動條
//......
return0;
}
另外還有許多其他的消息可能導(dǎo)致滾動條屬性的變化,如用戶的鍵盤操作、鼠標(biāo)滾輪操作可能導(dǎo)致滾動條的Thumb位置發(fā)生改變,這些操作分別導(dǎo)致WM_KEYDOWN和WM_MOUSEWHEEL,而系統(tǒng)又在這兩個消息中執(zhí)行滾動條的繪制,因此你必須截獲它們,但處理方法同WM_HSCROLL,這里不再啰嗦了。
當(dāng)我們沒有按下鼠標(biāo)左鍵時,只是簡單地在滾動條移動鼠標(biāo)時,我們會發(fā)現(xiàn)滾動條的按鈕和 Thumb都會自動高亮,其實這些變化都是系統(tǒng)在WM_NCMOUSEMOVE和WM_NCMOUSELEAVE中繪制完成的。比如當(dāng)鼠標(biāo)進入到 Thumb上時,它就高亮了,當(dāng)鼠標(biāo)離開它,它又變灰色了,你要分別處理WM_NCMOUSEMOVE和WM_NCMOUSELEAVE這兩個消息。注意只有ListView應(yīng)用主題風(fēng)格之后才可能有WM_NCMOUSELEAVE消息,傳統(tǒng)風(fēng)格的ListView就沒有這個消息,只有 WM_NCMOUSEMOVE消息,因此處理高亮還真有些麻煩。正是因為主題風(fēng)格和傳統(tǒng)風(fēng)格的差異,因此我建議你也處理WM_THEMECHANGED消息,以識別不同的主題模式,實現(xiàn)不同高亮處理。看上面那個圖,TreeView的滾動條是主題風(fēng)格的,但ListView的滾動條是傳統(tǒng)風(fēng)格的,但我加了換膚功能。
另外,我們還應(yīng)當(dāng)攔截處理ListView的WM_NCPAINT,將滾動條的區(qū)域摳掉,不讓它繪制滾動條。盡管滾動條被模擬窗口遮住了,但還是有必要這樣做,以防止出現(xiàn)意外情況:
if(msg == WM_NCPAINT)
{
HRGN wRgn =NULL;
RECTsbRect = GetScrollBarRect();//讀取滾動條矩形的一個函數(shù)
HRGNsRgn = ::CreateRectRgn(sbRect.left, sbRect.top, sbRect.right,sbRect.bottom
if(wParam ==1)
{
RECT wRect;
::GetWindowRect(hListView, &wRect);
wRgn = ::CreateRectRgn(wRect.left, wRect.top, wRect.right,wRect.bottom););
wRgn = ::CombineRgn(wRng, wRgn, sRgn, RGN_DIFF);
}
else
{
wRgn= (HRGN)wParam;
wRgn = ::CombineRgn(wRng, wRgn, sRgn, RGN_DIFF);
}
::DeleteObject(sRgn);
::CallWindowProc(gOldListViewProc, hListView, WM_NCPAINT,WPARAM(wRgn), 0);
if(wParam ==1)
::DeleteObject(wRgn);
return0;
}
另外,還要攔截WM_WINDOWPOSCHANGING消息,因為當(dāng)ListView的位置、尺寸、Z秩序發(fā)生改變時要及時調(diào)整模擬窗口的位置、尺寸、Z秩序。為何要在WM_WINDOWPOSCHANGING中進行,而不選擇在 WM_WINDOWPOSCHANGED或WM_MOVE或WM_SIZE中進行呢?因為在WM_WINDOWPOSCHANGING中處理,可讓模擬窗口先于ListView調(diào)整自己的位置、尺寸和Z,可避免一些繪制問題。實際我在FreeCL中既用了WM_WINDOWPOSCHANGING,也用到 WM_WINDOWPOSCHANGED兩個消息。在處理WM_WINDOWPOSCHANGING時調(diào)整位置、尺寸、Z秩序,在處理 WM_WINDOWPOSCHANGED時,強制重繪模擬窗口。
末了還要說明一點的是,系統(tǒng)可能因為用戶改變控件尺寸導(dǎo)致其滾動條自動消失或自動顯示,或者調(diào)整滾動條的可用狀態(tài),如你拉寬支持多行顯示的Edit控件,會導(dǎo)致滾動條箭頭按鈕變灰,Thumb消失。這些動作通常都是在 WM_WINDOWPOSCHANGED中完成的,因此這個消息也需要攔截處理:
if(msg == WM_WINDOWPOSCHANGED)
{
LRESULTlReturn = ::CallWindowProc(gOldListViewProc, hListView,WM_WINDOWPOSCHANGED,
wParam, lParam);
if(::GetNextWindow(hBuddy, GW_HWNDNEXT ) !=hListView)
{ //調(diào)整模擬窗口的Z-Order,確保其緊貼ListView之上
HWND hAbove = ::GetNextWindow(hListView, GW_HWNDPREV);
UINT flag =SWP_NOACTIVATE|SWP_NOREDRAW|SWP_NOMOVE|SWP
_NOSIZE|SWP_NOSENDCHANGING;
::SetWindowPos(hBuddy, hAbove?hAbove:HWND_TOP, 0, 0, 0, 0,flag);
}
//測試滾動條的顯隱狀態(tài)是否改變,并因之調(diào)整模擬窗口的顯隱狀態(tài)
//如果滾動條仍舊顯示或滾動條某些元素的顯示狀態(tài)有變化或控件位置、尺寸可能改變,
//強制重繪模擬窗口
//......
returnlReturn;
}
最后要處理WM_SHOWWINDOW和WM_DESTROY,當(dāng)ListView隱藏時讓模擬窗口也隨之消失;當(dāng)它顯示時,讓模擬窗口也隨之顯示。當(dāng)ListView銷毀時會觸發(fā)WM_DESTROY,這時也需要銷毀模擬窗口,這很重要。
總的來講,模擬法是蠻麻煩的,但比較安全,可靠性高。對開發(fā)者來講,都是對消息進行特定處理,是常規(guī)手段,對最終用戶來講,用此法實現(xiàn)的滾動條自繪,完全能夠滿足要求,具有充分的自由度,甚至可以對滾動條添加更多特定的功能,比如可以在上面添加其他按鈕,就算是加動畫、加廣告也都是可以的。
本站文章除注明轉(zhuǎn)載外,均為本站原創(chuàng)或翻譯。歡迎任何形式的轉(zhuǎn)載,但請務(wù)必注明出處、不得修改原文相關(guān)鏈接,如果存在內(nèi)容上的異議請郵件反饋至chenjj@fc6vip.cn
文章轉(zhuǎn)載自:博客轉(zhuǎn)載