本文原创,在图形开发中遇到问题,花费了大量时间与精力,现在终于解决问题,分享给大家。
问题描述:在CScrollView中使用CRectTracker绘制橡皮筋矩形拖动滚动条时,矩形与辅助框始终保持与显示器绝对位置(窗口移动,矩形位置不变)。
问题分析:要分析问题,就要明确这个问题是怎么产生的,首先在使用CRectTracker之前,在MSDN查阅了类文档,发现其中有示例程序,tracker.sln。打开,编译,运行,得到如下:
首先请忽略掉区域1234,把他们整体看做一个矩形,然后,注意周围的辅助框(虚线,定点的黑点)。这些是可以设置的,鼠标移动到黑点(辅助点)时会变化,示意鼠标在这个位置可以进行哪些操作。
我们来看一下源代码:(有兴趣的同学可以自己在vs2010安装目录下的samples/下的2052.zip,解压,然后搜索tracker.sln)
首先它的实例化,与我的需求不同,我需要自己用鼠标绘制矩形,而实例中是已经绘制好的矩形
CTrackerDoc::CTrackerDoc()
{
m_tracker.m_rect.left = 10; //这四行设置初始矩形的位置大小
m_tracker.m_rect.top = 10;
m_tracker.m_rect.right = 101;
m_tracker.m_rect.bottom = 101;
m_bAllowInvert = FALSE; //这个设置这个矩形是否允许翻转,如果为TRUE,则允许矩形区域12 34互换或者 13 24互换。
}
实例化好后,通过CView::OnDraw(CDC *pDC)显示到屏幕上,实例太复杂了,我简化一下:
void CTrackerView::OnDraw(CDC* pDC)
{
CTrackerDoc* pDoc = GetDocument();
CBrush* pOldBrush = NULL;
TRY
{
// draw inside in various colors
CBrush brush1, brush2;
CRect rect;
brush1.CreateSolidBrush(RGB(255, 0, 0));//创建红色画刷
pOldBrush = pDC->SelectObject(&brush1);//选择画刷
m_tracker,GetTrueRect(&rect);//获取到矩形位置
// draw tracker on outsidepDoc->m_tracker.Draw(pDC);//绘制辅助框就是有8个点的黑色框}CATCH_ALL(e){if (pOldBrush != NULL)pDC->SelectObject(pOldBrush);}END_CATCH_ALL}pDC->Rectangle(rect);//绘制矩形
接下来,通过捕获鼠标单击事件,对辅助框进行操作(简化):
void CTrackerView::OnLButtonDown(UINT nFlags, CPoint point) { CTrackerDoc* pDoc = GetDocument(); CRect rectSave; pDoc->m_tracker.GetTrueRect(rectSave);//这个是保存了操作之前的矩形,如果你是新手,想想如果操作失败时的还原,应该能理解 if (pDoc->m_tracker.HitTest(point) < 0) //这个是检测鼠标点击的point是否在矩形中,它的返回值可以表示点击的区域,具体可以参考MSDN,-1为未选中任何区域 {
//未选中的操作 //do something
//示例实现了重新新建一个tracker,画框,如果与存在的矩形有交集,就改变它的样式,我直接把代码都去掉了,没用
} else if (pDoc->m_tracker.Track(this, point, pDoc->m_bAllowInvert))//如果选中,进行橡皮筋操作,这个函数如果你改变位置或者大小都会返回TRUE。 { // normal tracking action, when tracker is "hit" pDoc->SetModifiedFlag(); pDoc->UpdateAllViews(NULL, (LPARAM)(LPCRECT)rectSave); //如果位置被改变,就刷新view,此时矩形被重绘 pDoc->UpdateAllViews(NULL); } CView::OnLButtonDown(nFlags, point); }
最后就是如何让鼠标显示操作:BOOL CTrackerView::OnSetCursor(CWnd* pWnd, UINT nHitTest, UINT message) { // forward to tracker CTrackerDoc* pDoc = GetDocument(); if (pWnd == this && pDoc->m_tracker.SetCursor(this, nHitTest)) return TRUE; return CView::OnSetCursor(pWnd, nHitTest, message); }
至此,如和绘制一个橡皮筋矩形已经完成了。但这还没有接触到问题,我必须先描述我另外一个问题之后,再回过头来才遇到标题要解决的问题。我的需求:用鼠标左键单击,拖拽绘制一个或多个橡皮筋矩形,每个矩形能够被选中,并且进行橡皮筋操作,并且能够删除。
Doc对象中不在包含CRectTracker对象,改为CPtrArray,在每次重绘(OnDraw)的时候,在屏幕上依次绘制出所有的矩形,同样的,在单击的时候要对每一个tracker hittest进行判断。如果一个都没选中,则new 一个tracker,用鼠标TrackRubberBand设置好之后,pDoc->arrTracker.add(tracker)交给容器管理。
至此,似乎已经实现了除删除外的所有需求,但当我移动滚动条,并且试图绘制一个新的矩形时,红色的矩形跑到了上面(表面现象),而辅助框则保持了正确的位置(事实上也不正确)。
注意括号的内容,如果想不明白为什么,请慢慢往下读,我会尽量详细一些。
首先,矩形本身跑到了上面,这是为何,相信很多朋友在网上都找到了一个例子,大致是这样的:
在他的OnDraw中:
void CCRectTrackerView::OnDraw(CDC* pDC) { CCRectTrackerDoc* pDoc = GetDocument(); ASSERT_VALID(pDoc); if (!pDoc) return; //自己添加 CBrush brush(RGB(255,0,0));//画刷为红色 CBrush *pbrush=pDC->SelectObject(&brush); CRect rect; m_RectTracker.GetTrueRect(&rect);//得到矩形区域的大小 pDC->Rectangle(&rect);//画出矩形 if(m_IsChosen) m_RectTracker.Draw(pDC);//若选择了该区域,则显示边框以及8个调整点 }
这个代码放在没有滚动条的view中是正常工作的。因为此时view与设备之间是o偏移的。(这里引申出两个概念,逻辑坐标与设备坐标,view的坐标是逻辑的,而pDC绘制的坐标是设备的,如果把一个逻辑坐标交给pDC绘制,则需要进行转换)一旦发生滚动,tracker.Draw只会根据自己的m_rect绘制辅助框,此时问题就明显了,无论如何滚动,辅助框都是在屏幕的绝对位置,不会发生移动。然后左思右想得不到解决,原因就是因为这两种坐标的转换上。
把设备坐标用来画逻辑坐标矩形,所以位置一直都不准确。
下面是我的一个解决办法,并且已经测试正确:
void CCAEImageTesterView::OnDraw(CDC* pDC) { RECT clientRECT; CCAEImageTesterDoc *pDoc=GetDocument(); CPen pPen(PS_SOLID,1,RGB(255,0,0)); //使用pen划线 pDC->SelectObject(pPen); pDC->SelectStockObject(NULL_BRUSH);//中间无任何填充(这些都是需求问题) int tackerCount = pDoc->m_arrTestRegon.GetSize(); for (int i=0; i<tackerCount; ++i) //遍历容器依次绘制 { TestingRegon *region = (TestingRegon *)pDoc->m_arrTestRegon.GetAt(i); region->getTracker().m_rect = region->m_Lrect; //这m_Lrect个是在鼠标绘制矩形时保存的逻辑坐标,并且这个坐标才是唯一坐标,在view滚动时根据逻辑坐标,不停的调整tracker矩形的位置,使得他看起来似乎一直与矩形相对静止 pDC->LPtoDP(®ion->getTracker().m_rect);//把tracker的逻辑坐标,转换成设备坐标,结合前一句注释理解 pDC->Rectangle(region->m_Lrect);//绘制矩形 region->Draw(pDC);//绘制辅助框 } }
void CCAEImageTesterView::OnLButtonDown( UINT nFlags, CPoint point ) { CCAEImageTesterDoc* pDoc = GetDocument(); int tackerCount = pDoc->m_arrTestRegon.GetCount(); BOOL hasSelectedOne = FALSE; TestingRegon *region = NULL; 计算偏移 CClientDC dc(this); //判断是否选中某个tracker for (int i=0; i<tackerCount; ++i) { region = (TestingRegon*)pDoc->m_arrTestRegon.GetAt(i); //选中tacker,操作tacker if(region->CheckHit(point)>=0 &&!hasSelectedOne) { region->SetChosen(TRUE); if (region->Track(this,point)) { OnPrepareDC(&dc); region->getTracker().GetTrueRect(®ion->m_Lrect);//操作后,重新计算设备坐标的逻辑坐标,并保存 dc.DPtoLP(®ion->m_Lrect); region->Draw(&dc); } region->DisplayTracker(); hasSelectedOne = TRUE; } else { region->HideTracker(); } } //未选中,创建tacker if (!hasSelectedOne) { region = new TestingRegon; region->TrackRubberBand(this,point); if (!region->IsRectEmpty()) { OnPrepareDC(&dc); region->getTracker().GetTrueRect(®ion->m_Lrect); dc.DPtoLP(®ion->m_Lrect); region->DisplayTracker(); pDoc->m_arrTestRegon.Add(region); }else{ delete region; } Invalidate(); } pDoc->UpdateAllViews(NULL); CView::OnLButtonDown(nFlags, point); }
至此问题解决,如果有什么不清楚的,或者我写的不对,请与我联系,虽然问题解决了,但是代码很丑,旨在解决问题,下午我开始对这部分代码重构,不过这博客中的代码不会更新了,请不要对代码可读性或其他进行评论,旨在分析问题与解决问题。谢谢大家。