Windows中的分隔條是一種被廣泛使用的控件,絕大多數Explorer式樣的應用程序都使用了這種控件。然而卻很少有相關的資料對它的完整實現進行介紹,於是我自己實現了一個,希望對SDK的愛好者們有所幫助。
事實上,分隔條也是一個很普通的窗口,它也擁有自己的窗口類、自己的窗口過程——就像所有的預定義控件一樣。也就是說,要創建一個分隔條,也需要進行窗口類的注冊和窗口的創建。以下的示例代碼示范了如何注冊一個分隔條的窗口類:
WNDCLASS wc;
wc.cbClsExtra = 0;
wc.cbWndExtra = 0;
wc.hbrBackground = (HBRUSH)COLOR_BTNSHADOW; // 1
wc.hCursor = LoadCursor(NULL, IDC_SIZEWE); // 2
wc.hIcon = NULL;
wc.hInstance = hInstance;
wc.lpfnWndProc = (WNDPROC)ProcSplitter; // 3
wc.lpszClassName = "MySplitter"; // 4
wc.lpszMenuName = NULL;
wc.style = 0;
RegisterClass(&wc);
這段代碼相信大家已經很熟悉了,所以在此我只簡要說明四點:1、分隔條的背景顏色,這裡我取默認的對話框背景色;2、分隔條的鼠標指針,這裡我取水平的調整大小指針;3、這是分隔條的窗口過程,所有的秘密都在這個回調函數之中;4、分隔條的窗口類名稱,你可以隨便取一個你喜歡的名字。
在成功地注冊窗口類之後,就可以創建分隔條了。以下是我的示例界面,它由一個樹形視圖、一個分隔條、一個列表視圖以及一個狀態欄組成,下文的所有代碼都是以這個界面為基礎的。
在編寫分隔條的窗口過程之前,我先來處理對話框的WM_SIZE消息作為分隔條窗口過程的一個熱身。代碼如下(你會發現在整個的代碼中我沒有對hTree、hStatus、hSplitter以及hList做任何的聲明,那是因為對於這個簡單的示例,我將所有的這些東西都聲明為了全局變量):
case WM_SIZE:
{
HDWP hdwp;
RECT rect, rectStatus, rectTree;
hdwp = BeginDeferWindowPos(4);
GetClientRect(hDlg, &rect);
GetClientRect(hStatus, &rectStatus);
GetWindowRect(hTree, &rectTree);
DeferWindowPos(hdwp, hStatus, NULL, 0, rect.bottom - rectStatus.bottom, rect.right, rectStatus.bottom, SWP_NOZORDER);
DeferWindowPos(hdwp, hTree, NULL, 0, 0, rectTree.right - rectTree.left, rect.bottom - rectStatus.bottom, SWP_NOMOVE | SWP_NOZORDER);
DeferWindowPos(hdwp, hSplitter, NULL, rectTree.right - rectTree.left, 0, 2, rect.bottom - rectStatus.bottom, SWP_NOZORDER);
DeferWindowPos(hdwp, hList, NULL, rectTree.right - rectTree.left + 2, 0, rect.right - rectTree.right + rectTree.left - 2, rect.bottom - rectStatus.bottom, SWP_NOZORDER);
EndDeferWindowPos(hdwp);
}
break;
你肯定已經注意到了,這段代碼的大部分篇幅都是在和矩形做游戲。的確是這樣,因為調整窗口大小的過程就是一個改變各個子窗口的位置和大小的過程。這個過程用語言敘述就是:1、首先,將狀態欄放置在對話框的最下方;2、第二步,不改變樹形視圖的位置和寬度,重設它的高度;3、不改變分隔條的位置和寬度,重設它的高度;4、使列表視圖占滿剩余的客戶區。
如果你弄懂了上面的代碼,那麼分隔條的窗口過程也就沒有任何難度了:
LRESULT CALLBACK ProcSplitter(HWND hwnd, UINT Msg, WPARAM wParam, LPARAM lParam)
{
switch (Msg)
{
case WM_LBUTTONDOWN:
SetCapture(hwnd);
break;
case WM_LBUTTONUP:
ReleaseCapture();
break;
case WM_MOUSEMOVE:
{
if ((wParam & MK_LBUTTON) == MK_LBUTTON && GetCapture() == hwnd)
{
HDWP hdwp;
RECT rect, rectStatus, rectTree;
hdwp = BeginDeferWindowPos(3);
GetClientRect(GetParent(hwnd), &rect);
GetClientRect(hStatus, &rectStatus);
GetWindowRect(hTree, &rectTree);
DeferWindowPos(hdwp, hTree, NULL, 0, 0, rectTree.right - rectTree.left + (short)LOWORD(lParam), rect.bottom - rectStatus.bottom, SWP_NOMOVE | SWP_NOZORDER);
DeferWindowPos(hdwp, hSplitter, NULL, rectTree.right - rectTree.left + (short)LOWORD(lParam), 0, 0, 0, SWP_NOSIZE | SWP_NOZORDER);
DeferWindowPos(hdwp, hList, NULL, rectTree.right - rectTree.left + (short)LOWORD(lParam) + 2, 0, rect.right - rectTree.right + rectTree.left - (short)LOWORD(lParam) - 2, rect.bottom - rectStatus.bottom, SWP_NOZORDER);
EndDeferWindowPos(hdwp);
}
}
break;
default:
return DefWindowProc(hwnd, Msg, wParam, lParam);
}
return 0;
}
SetCapture和ReleaseCapture是分別在鼠標左鍵按下與釋放的時候捕獲和釋放鼠標,這是分隔條的一般要求。這段代碼中的核心部分就是在處理鼠標移動的事件,就是當鼠標左鍵按下並且分隔條捕獲鼠標的時候來改變三個相關窗口的位置和寬度。具體的矩形操作與主窗口WM_SIZE的代碼原理相似,我就不多說了。我之所以不使用MoveWindow之類的函數來實現改變大小,就是因為這些函數會使窗體的多次重繪而導致整個窗體的閃爍——而事實上我並不希望狀態欄也一起閃爍。
如何實現靜態的分隔條(即畫線實現分隔的分隔條)。
上邊的分隔條是一種“動態”的分隔條,就是說在移動分隔條的同時窗口的大小也發生了改變。以下我再介紹一種“靜態”的分隔條,即在拖動分隔條的時候出現一條豎線,由這一條線來指示分隔條的分隔結果。
如果說動態的分隔條是在和矩形做游戲,那麼靜態的分隔條就是在和圖形做游戲了。首先請大家看我的代碼:
LRESULT CALLBACK ProcSplitter(HWND hwnd, UINT Msg, WPARAM wParam, LPARAM lParam)
{
static int x;
switch (Msg)
{
case WM_LBUTTONDOWN:
{
HDC hdc;
RECT rectTree;
SetCapture(hwnd);
GetWindowRect(hTree, &rectTree);
hdc = GetDC(GetParent(hwnd));
SelectObject(hdc, CreatePen(PS_SOLID, 2, 0));
SetROP2(hdc, R2_NOTXORPEN);
x = rectTree.right - rectTree.left;
MoveToEx(hdc, x, 0, NULL);
LineTo(hdc, x, rectTree.bottom - rectTree.top);
ReleaseDC(GetParent(hwnd), hdc);
}
break;
case WM_LBUTTONUP:
{
HDWP hdwp;
HDC hdc;
RECT rect, rectTree, rectStatus;
GetClientRect(GetParent(hwnd), &rect);
GetWindowRect(hTree, &rectTree);
GetClientRect(hStatus, &rectStatus);
hdc = GetDC(GetParent(hwnd));
SelectObject(hdc, CreatePen(PS_SOLID, 2, 0));
SetROP2(hdc, R2_NOTXORPEN);
MoveToEx(hdc, x, 0, NULL);
LineTo(hdc, x, rectTree.bottom - rectTree.top);
ReleaseDC(GetParent(hwnd), hdc);
ReleaseCapture();
hdwp = BeginDeferWindowPos(3);
DeferWindowPos(hdwp, hTree, NULL, 0, 0, x, rect.bottom - rectStatus.bottom, SWP_NOMOVE | SWP_NOZORDER);
DeferWindowPos(hdwp, hSplitter, NULL, x, 0, 0, 0, SWP_NOSIZE | SWP_NOZORDER);
DeferWindowPos(hdwp, hList, NULL, x + 2, 0, rect.right - x - 2, rect.bottom - rectStatus.bottom, SWP_NOZORDER);
EndDeferWindowPos(hdwp);
}
break;
case WM_MOUSEMOVE:
{
if ((wParam & MK_LBUTTON) == MK_LBUTTON && GetCapture() == hwnd)
{
HDC hdc;
RECT rectTree;
GetWindowRect(hTree, &rectTree);
hdc = GetDC(GetParent(hwnd));
SelectObject(hdc, CreatePen(PS_SOLID, 2, 0));
SetROP2(hdc, R2_NOTXORPEN);
MoveToEx(hdc, x, 0, NULL);
LineTo(hdc, x, rectTree.bottom - rectTree.top);
x = rectTree.right - rectTree.left + (short)LOWORD(lParam);
MoveToEx(hdc, x, 0, NULL);
LineTo(hdc, x, rectTree.bottom - rectTree.top);
ReleaseDC(GetParent(hwnd), hdc);
}
}
break;
default:
return DefWindowProc(hwnd, Msg, wParam, lParam);
}
return 0;
}
這段代碼的核心部分,就是它的畫線和擦線部分。在這裡我玩弄了一個小把戲,就是利用了SetROP2函數的R2_NOTXORPEN模式:在這個模式下作圖,只要在先前畫過線的地方再畫一道線,那麼用戶看到的效果就是原來的線被擦除了。這樣一來,只需要再使用一個static變量x,就可以完成這個過程了。
當然分隔條的種類有很多,例如VCL中的TSplitter中就包括了不同樣式的分隔條,不過效果無非是改變了畫線的樣式之類,在此我就不多討論了。