本文原创,在图形开发中遇到问题,花费了大量时间与精力,现在终于解决问题,分享给大家。
问题描述:在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);//获取到矩形位置
pDC->Rectangle(rect);//绘制矩形
// draw tracker on outsidepDoc->m_tracker.Draw(pDC);//绘制辅助框就是有8个点的黑色框}CATCH_ALL(e){if (pOldBrush != NULL)pDC->SelectObject(pOldBrush);}END_CATCH_ALL}
接下来,通过捕获鼠标单击事件,对辅助框进行操作(简化):
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);
}
至此问题解决,如果有什么不清楚的,或者我写的不对,请与我联系,虽然问题解决了,但是代码很丑,旨在解决问题,下午我开始对这部分代码重构,不过这博客中的代码不会更新了,请不要对代码可读性或其他进行评论,旨在分析问题与解决问题。谢谢大家。