1. 程式人生 > >用VC++實現應用程式視窗的任意分割(2)

用VC++實現應用程式視窗的任意分割(2)

一、關於CSplitterWnd類
我們在使用CuteFtp或者NetAnt等工具的時候,一般都會被其複雜的介面所吸引,在這些介面中視窗被分割為若干的區域,真正做到了視窗的任意分割。那麼我們自己如何建立類似的介面,也實現視窗的任意的分割呢?在VC6.0中這就需要使用到CSplitterWnd類。CSplitterWnd看上去像是一種特殊的框架視窗,每個視窗都被相同的或者不同的檢視所填充。當視窗被切分後用戶可以使用滑鼠移動切分條來調整視窗的相對尺寸。雖然VC6.0支援從AppWizard中建立分割視窗,但是自動加入的分割條總是不能讓我們滿意,因此我們還是通過手工增加程式碼來熟悉這個類。
CSplitterWnd的建構函式主要包括下面三個。


BOOL Create(CWnd* pParentWnd,int nMaxRows,int nMaxCols,SIZE sizeMin,CCreateContext* pContext,DWORD dwStyle,UINT nID);
功能描述:該函式用來建立動態切分視窗。 引數含義:pParentWnd 切分視窗的父框架視窗。 nMaxRows,nMaxCols是建立的最大的列數和行數。 sizeMin是窗格的現實大小。 pContext 大多數情況下傳給父視窗。 nID是字視窗的ID號.
BOOL CreateStatic(CWnd* pParentWnd,int nRows,int nCols,DWORD dwStyle,UINT nID)
功能描述:用來建立切分視窗。 引數含義同上。
BOOL CreateView (int row,int col,CruntimeClass* pViewClass,SIZE sizeinit,CcreateContext* pContext);
功能描述:為靜態切分的視窗的網格填充檢視。在將檢視於切分視窗聯絡在一起的時候必 須先將切分視窗建立好。
引數含義:同上。
從CSplitterWnd源程式可以看出不管是使用動態建立Create還是使用靜態建立CreateStatic,在函式中都呼叫了一個保護函式CreateCommon,從下面的CreateCommon函式中的關鍵程式碼可以看出建立CSplitterWnd的實質是建立了一系列的MDI子視窗。

DWORD dwCreatestyle=dwStyle & ~(WS_HSCROLL|WS_VSCROLL);
if (afxData.bWin4)
dwCreateStyle &= ~WS_BORDER; //create with the same wnd-class as MDI-Frame (no erase bkgnd)
if (!CreateEx(0, _afxWndMDIFrame, NULL, dwCreateStyle,
0, 0, 0, 0,pParentWnd->m_hWnd, (HMENU)nID, NULL))
return FALSE; // create invisible


二、建立巢狀分割視窗
2.1建立動態分割視窗
動態分割視窗使用Create方法。下面的程式碼將建立2x2的窗格。

m_wndSplitter.Create(this,2,2,CSize(100,100),pContext);

但是動態建立的分割視窗的窗格數目不能超過2x2,而且對於所有的窗格,都必須共享同一個檢視,所受的限制也比較多,因此我們不將動態建立作為重點。我們的主要精力放在靜態分割視窗的建立上。
2.2建立靜態分割視窗
與動態建立相比,靜態建立的程式碼要簡單許多,而且可以最多建立16x16的窗格。不同的窗格我們可以使用CreateView填充不同的檢視。
在這裡我們將建立CuteFtp的視窗分割。CuteFtp的分割情況如下:
CCuteFTPView
CView2 CView3
CView4

建立步驟:
▲ 在建立之前我們必須先用AppWizard生成單文件CuteFTP,生成的視類為 CCuteFTPView.同時在增加三個視類或者從視類繼承而來的派生類CView2,CView3 CView4.
▲ 增加成員:
在Cmainfrm.h中我們將增加下面的程式碼:


CSplitterWnd wndSplitter1;
CSplitterWnd wndSplitter2;
▲ 過載CMainFrame::OnCreateClient()函式:
BOOL CMainFrame::OnCreateClient( LPCREATESTRUCT , CCreateContext* pContext)
{ //建立一個靜態分欄視窗,分為三行一列
if(m_wndSplitter1.CreateStatic(this,3,1)==NULL)
return FALSE;
//將CCuteFTPView連線到0行0列窗格上
m_wndSplitter1.CreateView(0,0,RUNTIME_CLASS(CCuteFTPView),CSize(100,100), pContext);
m_wndSplitter1.CreateView(2,0,RUNTIME_CLASS(CView4),CSize(100,100),pContext);
//將CView4連線到0行2列
if(m_wndSplitter2.CreateStatic(&m_wndSplitter,1,2,WS_CHILD|WS_VISIBLE,
m_wndSplitter.IdFromRowCol(1, 0))==NULL)
return FALSE; //將第1行0列再分開1行2列
//將CView2類連線到第二個分欄物件的0行0列
m_wndSplitter2.CreateView(0,0,RUNTIME_CLASS(CView2),CSize(400,300),pContext);
//將CView3類連線到第二個分欄物件的0行1列
m_wndSplitter2.CreateView(0,1,RUNTIME_CLASS(CView3),CSize(400,300),pContext);
return TRUE;
}
2.3實現各個分割區域的通訊
■有文件相連的檢視之間的通訊
由AppWizard生成的CCuteFTPView是與文件相連的,同時我們也讓CView2與文件相連,因此我們需要修改CCuteFTPApp的InitInstance()函式,我們將增加下面的部分。

AddDocTemplate (new CMultiDocTemplate(IDR_VIEW2TYPE,

RUNTIME_CLASS(CMainDoc),
RUNTIME_CLASS(CMDIChildWnd),
RUNTIME_CLASS(CView2)));
我們現在來實現CCuteFTPView與CView2之間的通訊。由於跟文件類相連的檢視類是不能安全的與除文件類之外的其餘的檢視類通訊的。因此我們只能讓他們都與文件類通訊。在文件中我們設定相應的指標以用來獲的各個檢視。我們過載 CCuteFTPView::OnOpenDocument()函式;

CCuteFTPView* pCuteFTPView;CView2* pView2;
POSITION pos;
CView* pView;
while(pos!=NULL)
{
pView=GetNextView(pos);
if(pView->IsKindOf(RUNTIME_CLASS(CCuteFTPView))==NULL)
pCuteFTPView=(CCuteFTPView*)pView;
else(pView->IsKindOf(RUNTIME_CLASS(CCuteFTPView))==NULL)
pView2=(CView2*)pView;
}
這樣我們在文件類中就獲的了跟它相連的所有的檢視的指標。
如果需要在 CCuteFTPView中呼叫CView2中的一個方法DoIt()則程式碼如下:

CCuteFTPDoc* pDoc=GetDocument();CView2* pView2=pDoc->pView3;pView3.DoIt();

■無文件檢視與文件關聯檢視之間的通訊
CView3和CView4都是不與文件相關聯的。我們現在實現CView3與CView2的通訊.正如前面所說,CView2只能安全的與CCuteFTPDoc通訊,因此,CView3如果需要跟CView2通訊,也必須藉助於文件類。因此程式的關鍵是如何在CView3中獲得文件的指標。檢視類中沒有這樣的類成員可以用來直接訪問文件類。但是我們知道在主視窗類MainFrame中我們可以獲得程式的任意視窗類的指標。因此我們只要獲得程式主視窗了的指標,就可以解決問題了。程式碼實現在CView3中訪問CView2中的DoIt()方法。

CView3中的程式碼如下:
CMainFrame* MainFrame=(CMainFrame*)this->GetParent()->GetParent();

CCuteFTPDoc* Doc=(CCuteFTPDoc*)MainFrame->GetActiveDocument();
if(Doc!=NULL) Doc->DoIt();

CCuteFTPDoc中的相應的處理函式DoIt()程式碼如下:

CView2* pView2;
POSITION pos;
CView* pView;
while(pos!=NULL)
{
pView=GetNextView(pos);
if(pView->IsKindOf(RUNTIME_CLASS(CView2))==NULL)
pView2=(CView2*)pView;
}
pView2->DoIt();
■無文件關聯檢視之間的通訊
CView3和CView4都是不跟文件相連的,如何實現他們之間的通訊呢。正如我們在上面所說的那樣,由於在主框架中我們可以訪問任意的檢視,因此我們的主要任務還是在程式中獲得主框架的指標。在CView3中訪問CView4中的方法DoIt()。

CMainFrame* MainFrame=(CMainFrame*)this->GetParent()->GetParent();

CView4* View4=(CView4*)MainFrame->m_wndSplitter1.GetPane(2,0);
View4->DoIt();

到現在我們已經實現了CuteFTP的主視窗的框架並且能夠實現他們之間相互通訊的框架。同樣的我們可以實現其他的一些流行介面例如NetAnts,Foxmail的分割。

三、關於對話方塊的分割
到目前為止,只有基於文件/檢視的程式才能使用CSplitterWnd,而基於對話方塊的應用程式卻不支援CSplitterWnd,但是如果我們在繼承類中過載一些虛擬方法,也能使CSplitterWnd 在對話方塊程式中使用。從MFC的源程式WinSplit.cpp中可以看出,為了獲得父視窗的地方程式都呼叫了虛擬方法GetParentFrame(),因此如果在對話方塊中使用,我們必須將它改為GetParent();因此我們將CSplitterWnd的下面幾個方法過載。

virtual void StartTracking(int ht);
virtual CWnd* GetActivePane(int* pRow = NULL, int* pCol = NULL);
virtual void SetActivePane( int row, int col, CWnd* pWnd = NULL );
virtual BOOL OnCommand(WPARAM wParam, LPARAM lParam);
virtual BOOL OnNotify( WPARAM wParam, LPARAM lParam, LRESULT* pResult );
virtual BOOL OnWndMsg( UINT message, WPARAM wParam, LPARAM lParam, LRESULT* pResult );
具體實現如下,實現中我將給出原有程式碼的主要部分以及修改後的程式碼以作對比。
在cpp檔案中加入下面的列舉型別。

enum HitTestValue
{
noHit = 0,//表示沒有選中任何物件
vSplitterBox = 1,
hSplitterBox = 2,
bothSplitterBox = 3,
vSplitterBar1 = 101,//代表各個方向的水平分割條
vSplitterBar15 = 115,
hSplitterBar1 = 201,//代表垂直方向的各個分割條
hSplitterBar15 = 215,
splitterIntersection1 = 301,//代表各個交叉點
splitterIntersection225 = 525
};

CWnd* CxSplitterWnd::GetActivePane(int* pRow, int* pCol)
{
ASSERT_VALID(this);
//獲得當前的獲得焦點的視窗
//下面註釋粗體的是原有的程式碼的主要部分。
// CWnd* pView = NULL;
//CFrameWnd* pFrameWnd = GetParentFrame();
//ASSERT_VALID(pFrameWnd);
//pView = pFrameWnd->GetActiveView();
//if (pView == NULL)
// pView = GetFocus();
CWnd* pView = GetFocus();
if (pView != NULL && !IsChildPane(pView, pRow, pCol))
pView = NULL;
return pView;
}

void CxSplitterWnd::SetActivePane( int row, int col, CWnd* pWnd)
{
CWnd* pPane = pWnd == NULL ? GetPane(row, col) : pWnd;
//下面加註釋粗體的是原有程式碼的主要部分。
//FrameWnd* pFrameWnd = GetParentFrame();
//ASSERT_VALID(pFrameWnd);
//pFrameWnd->SetActiveView((CView*)pPane);
pPane->SetFocus();//修改後的語句
}

void CxSplitterWnd::StartTracking(int ht)
{
ASSERT_VALID(this);
if (ht == noHit)
return;
// GetHitRect will restrict ''''m_rectLimit'''' as appropriate

GetInsideRect(m_rectLimit);
if (ht >= splitterIntersection1 && ht <= splitterIntersection225)

{
// split two directions (two tracking rectangles)

int row = (ht - splitterIntersection1) / 15;

int col = (ht - splitterIntersection1) % 15;

GetHitRect(row + vSplitterBar1, m_rectTracker);

int yTrackOffset = m_ptTrackOffset.y;
m_bTracking2 = TRUE;
GetHitRect(col + hSplitterBar1, m_rectTracker2);

m_ptTrackOffset.y = yTrackOffset;
}
else if (ht == bothSplitterBox)
{
// hit on splitter boxes (for keyboard)
GetHitRect(vSplitterBox, m_rectTracker);
int yTrackOffset = m_ptTrackOffset.y;
m_bTracking2 = TRUE;
GetHitRect(hSplitterBox, m_rectTracker2);
m_ptTrackOffset.y = yTrackOffset; // center it
m_rectTracker.OffsetRect(0, m_rectLimit.Height()/2); m_rectTracker2.OffsetRect(m_rectLimit.Width()/2,
0);
}
else
{
// only hit one bar
GetHitRect(ht, m_rectTracker);
}

//下面加註釋的將從程式中刪去。
//CView* pView = (CView*)GetActivePane();
//if (pView != NULL && pView->IsKindOf(RUNTIME_CLASS(CView)))
//{
// ASSERT_VALID(pView);
// CFrameWnd* pFrameWnd = GetParentFrame();
//ASSERT_VALID(pFrameWnd);
//pView->OnActivateFrame(WA_INACTIVE, pFrameWnd);
// }
// steal focus and capture
SetCapture();
SetFocus();
// make sure no updates are pending
RedrawWindow(NULL, NULL, RDW_ALLCHILDREN | RDW_UPDATENOW);

// set tracking state and appropriate cursor
m_bTracking = TRUE;
OnInvertTracker(m_rectTracker);
if (m_bTracking2)
OnInvertTracker(m_rectTracker2);
m_htTrack = ht;
SetSplitCursor(ht);
}

BOOL CxSplitterWnd::OnCommand(WPARAM wParam, LPARAM lParam)
{
if (CWnd::OnCommand(wParam, lParam))
return TRUE;
//下面粗體的是原程式的語句
//return GetParentFrame()->SendMessage(WM_COMMAND, wParam, lParam);

return GetParent()->SendMessage(WM_COMMAND, wParam, lParam);

}
BOOL CxSplitterWnd::OnNotify( WPARAM wParam, LPARAM lParam, LRESULT* pResult )
{
if (CWnd::OnNotify(wParam, lParam, pResult))
return TRUE;
//下面粗體的是源程式的語句
//*pResult = GetParentFrame()->SendMessage(WM_NOTIFY,
wParam, lParam);
*pResult = GetParent()->SendMessage(WM_NOTIFY, wParam, lParam);
return TRUE;
}

BOOL CxSplitterWnd::OnWndMsg(UINT message, WPARAM wParam, LPARAM lParam, LRESULT* pResult)
{
// The code line below is necessary if using CxSplitterWnd
in a regular dll
// AFX_MANAGE_STATE(AfxGetStaticModuleState());
return CWnd::OnWndMsg(message, wParam, lParam, pResult);

}
這樣我們就可以在對話方塊中使用CxSplitterWnd類了。

四、CSplitterWnd的擴充套件
CSplitterWnd擴充套件話題是很多的,我們可以通過對原有方法的覆蓋或者增加新的方法來擴充套件CSplitterWnd。我們在此僅舉兩個方面的例子。
4.1鎖定切分條
當用戶建立好分割視窗後,有時並不希望通過拖動切分條來調節視窗的大小。這時就必須鎖定切分條。鎖定切分條的最簡單的方法莫過於不讓CSplitterWnd來處理WM_LBUTTONDOWN,WM_MOUSEMOVE,WM_SETCURSOR訊息,而是將這些訊息交給CWnd視窗進行處理,從而遮蔽掉這些訊息。拿WM_LBUTTONDOWN處理過程來說。修改為如下:

void CXXSplitterWnd::OnLButtonDown(UINT nFlags,CPoint point) {
CWnd::OnLButtonDown(nFlags,point);
}
其餘的處理方法類似。
4.2切分條的定製
由Window自己生成的切分條總是固定的,沒有任何的變化,我們在使用一些軟體比如ACDSee的時候卻能發現它們的切分條卻是和自動生成的切分條不一樣的。那麼如何定製自己的切分條呢?通過過載CSplitterWnd的虛方法OnDrawSplitter和OnInvertTracker可以達到這樣的目的。下面的程式碼生成的效果是分割視窗的邊界顏色為紅色,分割條的顏色為綠色.程式碼如下:

void CSplitterWndEx::OnDrawSplitter(CDC *pDC, ESplitType nType, const CRect &rectArg)
{
if(pDC==NULL)
{
RedrawWindow(rectArg,NULL,RDW_INVALIDATE|RDW_NOCHILDREN);
return;
}
ASSERT_VALID(pDC);
CRect rc=rectArg;
switch(nType)
{
case splitBorder:
//重畫分割視窗邊界,使之為紅色
pDC->Draw3dRect(rc,RGB(255,0,0),RGB(255,0,0));
rc.InflateRect(-CX_BORDER,-CY_BORDER);
pDC->Draw3dRect(rc,RGB(255,0,0),RGB(255,0,0));

return;
case splitBox:
pDC->Draw3dRect(rc,RGB(0,0,0),RGB(0,0,0));
rc.InflateRect(-CX_BORDER,-CY_BORDER);
pDC->Draw3dRect(rc,RGB(0,0,0),RGB(0,0,0));
rc.InflateRect(-CX_BORDER,-CY_BORDER);
pDC->FillSolidRect(rc,RGB(0,0,0));
pDC->Draw3dRect(rc,RGB(0,0,0),RGB(0,0,0));
return;
case splitBar:
//重畫分割條,使之為綠色
pDC->FillSolidRect(rc,RGB(255,255,255));
rc.InflateRect(-5,-5);
pDC->Draw3dRect(rc,RGB(255,0,0),RGB(255,0,0));

return;
default:
ASSERT(FALSE);
}
pDC->FillSolidRect(rc,RGB(0,0,255));
}
void CSplitterWndEx::OnInvertTracker(CRect &rect)
{
ASSERT_VALID(this);
ASSERT(!rect.IsRectEmpty());
ASSERT((GetStyle()&WS_CLIPCHILDREN)==0);
CRect rc=rect;
rc.InflateRect(2,2);
CDC* pDC=GetDC();
CBrush* pBrush=CDC::GetHalftoneBrush();
HBRUSH hOldBrush=NULL;
if(pBrush!=NULL) hOldBrush=(HBRUSH)SelectObject(pDC->m_hDC,pBrush->m_hObject);
pDC->PatBlt(rc.left,rc.top,rc.Width(),rc.Height(),BLACKNESS);

if(hOldBrush!=NULL)
SelectObject(pDC->m_hDC,hOldBrush);
ReleaseDC(pDC);
}