使用CRectTracker类进行对象动态定位 内容提要 使用VC,VB,Delphi等可视化工具进行程序设计的时候用的最多的可能就是控件的拾取,拖动以及动态定位了。例如我们可以在VC中用鼠标一次拾取数个控件,然后通过鼠标或者通过左右上下方向键进行控件的微移,从而使控件移到合适的位置,这种技术就是对象动态定位。动态定位也是交互式程序设计中最基本的操作之一。在VC中我们可以通过CRectTracker类来实现这些操作。 一 关于CRectTracker类 MFC中的CrectTRacker类已经进行了很好的封装,从而使得我们只需要很少的开发就可以实现上面的几个操作。对象允许我们在窗口里面移动一个矩形对象或者改变一个矩形对象的大小,它适合与任何的包括OLE在内的应用程序。首先我们了解一下类的成员变量和成员函数. 1.CRectTracker类的成员变量: m_nHandleSize: 对象的调整句柄的数目,默认情况下为8个 CRect m_rect: 对象矩形目前所占大小的位置 m_sizeMin: 对象所占的矩形的最小宽度和最小高度 m_nStyle: 目前对象边框和调整句柄的类型 2.CRectTracker类的成员函数 Draw(CDC* pDC):通过调用这个函数来画对象边框和调整句柄的类型。对象边框和调整句柄的具体类型由成员变量m_nStyle决定,一共有以下几种形式: CRectTracker::solidLine 外部边界使用实线形式 CRectTracker::dottedLine 外部边界使用点虚线形式 CRectTracker::hatchedBorder 外部边界使用带阴影的形式 CRectTracker::resizeInside 调整句柄处于对象内部区域 CRectTracker::resizeOutside 调整句柄处于对象外部区域 CRectTracker::hatchInside 内部所有区域使用带阴影的形式 需要注意的是 CRectTracker::solidLine和CRectTracker::dottedLine不能同时并存 CRectTracker::resizeInside和CRectTracker::resizeOutside不能同时共存 Track(CWnd* pWnd, CPoint point, BOOL bAllowInvert = FALSE, CWnd* pWndClipTo = NULL) 这个函数和下面的TrackRubberBand函数是整个CRectTracker类中最重要的函数,通常在消息WM_LBUTTONDOWN中调用处理,如果鼠标指针落在矩形的边框上,用户就可以拖动以调整矩形的大小;如果鼠标落在了矩形的内部,则用户可以拖动鼠标移动矩形。当ESC键按下时候,函数返回FALSE,函数没有起作用, 否则松开鼠标时候,返回TRUE; TrackRubberBand(CWnd* pWnd,CPoint point,BOOL bAllowInvert=FALSE,CWnd* pWndClipTo=NULL) 如果鼠标没有点中对象矩形,那么鼠标移动时候将会产生橡皮条,这个函数就是用来定位”橡皮条”,橡皮条内的项目称之为被选中. GetTrueRect(LPRECT lpTrueRect):获取对象所占矩形的大小,如果边框形式为CRectTracker::hatchOutside则矩形大小包括外部调整句柄的范围 Int HitTest(Cpoint point):函数返回光标在CrectTrack类中的位置,通过返回值可以确定拖动句柄的位置 CRectTracker::hitNothing -1: 没有点击任何地方 CRectTracker::hitTopLeft 0: 点击调整标记的左上角 CRectTracker::hitTopRight 1: 点击调整标记的右上角 CRectTracker::hitBottomRight 2: 点击调整标记的右下角 CRectTracker:hitBottomLeft 3: 点击调整标记的左下角 CRectTracker:hitTop 4: 点击调整标记的上方 CRectTracker:hitRight 5: 点击调整标记的右方 CRectTracker:hitBottom 6: 点击调整标记的下方 CRectTracker:hitLeft 7: 点击调整标记的左方 CRectTracker:hitMiddle 8: 点击调整标记的中央 BOOL SetCursor(CWnd* pWnd,UINT nHitTest):当点击特定的位置时改变光标的形状 二 .应用示例 整个程序界面如下,我们可以通过工具栏来设置图像的边界和调整句柄的类型,同时我们可以用鼠标移动和调整整个图像,另外程序还支持用←↑→↓来微调矩形的位置,通过Shift+←↑→↓来微调矩形的大小.程序的开发步骤如下: 步骤一:使用VC的MFC AppWizard创建单文档应用程序ExamTracker,创建过程中保留默认值。 步骤二:在CExamTrackerDoc中增加两个成员变量 CRectTracker m_tracker; BOOL m_bAllowInvert; 步骤三:在CrectTrckerDemoDoc的构造函数中给m_tracker对象赋初值 m_tracker.m_rect.left = 200; m_tracker.m_rect.top = 100; m_tracker.m_rect.right = 301; m_tracker.m_rect.bottom = 201; m_tracker.m_nStyle=CRectTracker::solidLine; m_bAllowInvert=TRUE; 步骤四;在CExamTrackerView中编写函数OnDraw()绘制图像所示的矩形.下面的代码相信大家都看的懂. void CExamTrackerView::OnDraw(CDC* pDC) { CExamTrackerDoc* pDoc = GetDocument(); ASSERT_VALID(pDoc); CBrush* pOldBrush = NULL; TRY { CBrush brush1, brush2; CRect rect; int nWidth = pDoc->m_tracker.m_rect.Width(); int nHeight = pDoc->m_tracker.m_rect.Height(); int nSgnX = nWidth != 0 ? nWidth / abs(nWidth) : 1; int nSgnY = nHeight != 0 ? nHeight / abs(nHeight) : 1; pDC->SetTextAlign(TA_CENTER); pDC->SetBkMode(TRANSPARENT); int nCenterX, nCenterY; TEXTMETRIC tm; pDC->GetTextMetrics(&tm); brush1.CreateSolidBrush(RGB(255, 0, 0)); pOldBrush = pDC->SelectObject(&brush1); SetNormalRect(rect, pDoc->m_tracker.m_rect.left, pDoc->m_tracker.m_rect.top, nWidth/2, nHeight/2); pDC->PatBlt(rect.left, rect.top, rect.Width(), rect.Height(), PATCOPY); nCenterX = rect.left + rect.Width()/2; nCenterY = rect.top + rect.Height()/2 - tm.tmHeight/2; pDC->ExtTextOut(nCenterX, nCenterY, ETO_CLIPPED, rect, _T( "1" ), 1, NULL); brush2.CreateSolidBrush(RGB(0, 255, 0)); pDC->SelectObject(&brush2); brush1.DeleteObject(); SetNormalRect(rect, pDoc->m_tracker.m_rect.left+nWidth/2, pDoc->m_tracker.m_rect.top, (nWidth+nSgnX)/2, nHeight/2); pDC->PatBlt(rect.left, rect.top, rect.Width(), rect.Height(), PATCOPY); nCenterX = rect.left + rect.Width()/2; nCenterY = rect.top + rect.Height()/2 - tm.tmHeight/2; pDC->ExtTextOut(nCenterX, nCenterY, ETO_CLIPPED, rect, _T( "2" ), 1, NULL); brush1.CreateSolidBrush(RGB(0, 0, 255)); pDC->SelectObject(&brush1); brush2.DeleteObject(); SetNormalRect(rect, pDoc->m_tracker.m_rect.left, pDoc->m_tracker.m_rect.top+nHeight/2, nWidth/2, (nHeight+nSgnY)/2); pDC->PatBlt(rect.left, rect.top, rect.Width(), rect.Height(), PATCOPY); nCenterX = rect.left + rect.Width()/2; nCenterY = rect.top + rect.Height()/2 - tm.tmHeight/2; pDC->ExtTextOut(nCenterX, nCenterY, ETO_CLIPPED, rect, _T( "3" ), 1, NULL); brush2.CreateSolidBrush(RGB(192, 192, 192)); pDC->SelectObject(&brush2); brush1.DeleteObject(); SetNormalRect(rect, pDoc->m_tracker.m_rect.left+nWidth/2, pDoc->m_tracker.m_rect.top+nHeight/2, (nWidth+nSgnX)/2, (nHeight+nSgnY)/2); pDC->PatBlt(rect.left, rect.top, rect.Width(), rect.Height(), PATCOPY); nCenterX = rect.left + rect.Width()/2; nCenterY = rect.top + rect.Height()/2 - tm.tmHeight/2; pDC->ExtTextOut(nCenterX, nCenterY, ETO_CLIPPED, rect, _T( "4" ), 1, NULL); if (pOldBrush != NULL) pDC->SelectObject(pOldBrush); brush2.DeleteObject(); pDoc->m_tracker.Draw(pDC); } CATCH_ALL(e) { if (pOldBrush != NULL) pDC->SelectObject(pOldBrush); } END_CATCH_ALL } 在程序中用到了自定义的全局函数 static void SetNormalRect(CRect& rect, int left, int top, int width, int height),函数用来设置矩形的大小,同时对矩形进行相应的校正.这种校正一般在逆向拖动时候需要,作用相当于CRect::NormalizeRect().函数定义如下: static void SetNormalRect(CRect& rect, int left, int top, int width, int height) { rect.left = left; rect.top = top; rect.right = left + width; rect.bottom = top + height; int nTemp; if (rect.left > rect.right) { nTemp = rect.left; rect.left = rect.right; rect.right = nTemp; } if (rect.top > rect.bottom) { nTemp = rect.top; rect.top = rect.bottom; rect.bottom = nTemp; } } 步骤五:使用AppWizard为CExamTrackerView增加WM_LBUTTONDOWN处理消息,在函数中我们必须处理三种情况;鼠标选中矩形对象但是不处于矩形边界,这种情况仅仅是对矩形进行移动,第二种情况:鼠标处于矩形边界,这时候拖动鼠标将会调整矩形的大小.第三种情况时鼠标没有选中矩形的任何地方,这个时候将会产生橡皮条效果,橡皮条内的所有内容被选中.具体代码如下: void CExamTrackerView::OnLButtonDown(Uint nFlags, CPoint point) { CExamTrackerDoc* pDoc = GetDocument(); CRect rectSave; pDoc->m_tracker.GetTrueRect(rectSave); if (pDoc->m_tracker.HitTest(point) < 0) { CRectTracker tracker; if (tracker.TrackRubberBand( this , point, pDoc->m_bAllowInvert)) { CRect rectT; tracker.m_rect.NormalizeRect(); if (rectT.IntersectRect(tracker.m_rect, pDoc->m_tracker.m_rect)) { if (pDoc->m_tracker.m_nStyle & CRectTracker::resizeInside) { pDoc->m_tracker.m_nStyle &= ~CRectTracker::resizeInside; pDoc->m_tracker.m_nStyle |= CRectTracker::resizeOutside; } else { pDoc->m_tracker.m_nStyle &= ~CRectTracker::resizeOutside; pDoc->m_tracker.m_nStyle |= CRectTracker::resizeInside; } pDoc->UpdateAllViews(NULL, (LPARAM)(LPCRECT)rectSave); pDoc->UpdateAllViews(NULL); } } } else if (pDoc->m_tracker.Track( this , point, pDoc->m_bAllowInvert)) { pDoc->UpdateAllViews(NULL, (LPARAM)(LPCRECT)rectSave); pDoc->UpdateAllViews(NULL); } CView::OnLButtonDown(nFlags, point); } 步骤六:到目前程序的原型基本上已经定下了,可以运行了!运行结果试试看!但是运行中你会发现一个问题,在VC,Delphi中鼠标再不同的区域可以有不同的形状,比如如果选中图形区域鼠标为十字形状,选中边界时成一字形.在程序中我们通过处理WM_SETCURSOR消息来获取这种效果。 BOOL CExamTrackerView::OnSetCursor(CWnd* pWnd, Uint nHitTest, Uint message) { CExamTrackerDoc* pDoc = GetDocument(); if (pWnd == this && pDoc->m_tracker.SetCursor( this , nHitTest)) return TRUE; return CView::OnSetCursor(pWnd, nHitTest, message); } 从上面的代码可以看出函数中仅仅是调用了CRectTracker自己的SetCursor函数,具体的处理过程我们已经不需要做了. 写到这儿的时候程序已经基本上结束了,用鼠标可以做大部分的事情,剩下的事情就是位置和大小的微调了,具体的微调通过处理WM_KEYDOWN消息来实现.具体的代码如下。 void CExamTrackerView::OnKeyDown(Uint nChar, Uint nRepCnt, Uint nFlags) { CExamTrackerDoc* pDoc = GetDocument(); switch (nChar) { case VK_LEFT: if (::GetKeyState(VK_SHIFT)&0xff00) { pDoc->m_tracker.m_rect.right=pDoc->m_tracker.m_rect.right-5; pDoc->UpdateAllViews(NULL); } else { pDoc->m_tracker.m_rect.left=pDoc->m_tracker.m_rect.left-5; pDoc->m_tracker.m_rect.right=pDoc->m_tracker.m_rect.right-5; pDoc->UpdateAllViews(NULL); } break ; if (::GetKeyState(VK_SHIFT)&0xff00) { pDoc->m_tracker.m_rect.right=pDoc->m_tracker.m_rect.right+5; pDoc->UpdateAllViews(NULL); } else { pDoc->m_tracker.m_rect.left=pDoc->m_tracker.m_rect.left+5; pDoc->m_tracker.m_rect.right=pDoc->m_tracker.m_rect.right+5; pDoc->UpdateAllViews(NULL); } break ; case VK_UP: if (::GetKeyState(VK_SHIFT)&0xff00) { pDoc->m_tracker.m_rect.bottom=pDoc->m_tracker.m_rect.bottom-5; pDoc->UpdateAllViews(NULL); } else { pDoc->m_tracker.m_rect.top=pDoc->m_tracker.m_rect.top-5; pDoc->m_tracker.m_rect.bottom=pDoc->m_tracker.m_rect.bottom-5; pDoc->UpdateAllViews(NULL); } break ; case VK_DOWN: if (::GetKeyState(VK_SHIFT)&0xff00) { pDoc->m_tracker.m_rect.bottom=pDoc->m_tracker.m_rect.bottom+5; pDoc->UpdateAllViews(NULL); } else { pDoc->m_tracker.m_rect.top=pDoc->m_tracker.m_rect.top+5; pDoc->m_tracker.m_rect.bottom=pDoc->m_tracker.m_rect.bottom+5; pDoc->UpdateAllViews(NULL); } break ; } CView::OnKeyDown(nChar, nRepCnt, nFlags); } 所有的任务都完成了, 慢,还有通过工具栏改变区域边界的方法,我就提供一个吧,剩下的大家就看源代码吧,应该很简单的,大家都能看的懂,需要注意的是几种形式的不可兼容性,否则会出错,其余的我就不费笔墨和口舌了 void CExamTrackerDoc::OnSolidline() { CRect rectTrue; m_tracker.GetTrueRect(&rectTrue); m_tracker.m_nStyle &= ~CRectTracker::dottedLine; m_tracker.m_nStyle ^= CRectTracker::solidLine; UpdateAllViews(NULL, (LPARAM)(LPCRECT)rectTrue); UpdateAllViews(NULL); }