📄 11.3.2 图形错位现象.txt
字号:
11 .3.2 图形错位现象
但是, Graphic程序这时的窗口滚动功能还不完备。例如,读者可以先把垂直接动条拖动到窗口的
最下端后,再在靠近窗口底部的位置绘制一个图形,然后切换到其他程序窗口,再切换回来,这时
会发现在程序窗口中刚才在底端绘制的图形出现在了原位置的上方。我们知道,当窗口发生切换时,
窗口会发生重绘,也就是在 OnDraw函数中将重新绘制图形。
先前 CGraphicView类以 CView类为基类时,窗口中绘制的图形无论窗口如何切换或者尺寸如何变
化,都能正确显示,但为什么增加液动功能后窗口中的内容就不能正确地显示呢?根据前面的知识,
我们知道,调用 GDI函数绘图时使用的是逻辑坐标,而 Windows需要把其转换为设备坐标,然后输
出图形。前面已经介绍了,在调用 OnDraw函数之前, OnPaint函数调用 OnPrepareDC函数来调整显
示上下文的属性。因此,我们猜想可能就是在此函数中调整了显示上下文的属性,从而导致先前的
现象,即图形跑到原位置的上方显示了。
为了更好地找到问题的原因,我们可以看看 CScrollView中己经重写的 OnPrePareDC
函数的定义,该函数的定义位于微软提供的 MFC源文件: VIEWSCRL. CPP中,具体代码如例 11-9所
示。
19IJ 11-9
/////////////////////////////////////////////////////////////////////// // CScrollView
paint工 ng
void CScrollView: :OnPrepareDC(CDC* pDC , CPrint工 nfo* p工 nfo)
ASSERT_VALIO(pOC) ;
#ifdef DEBUG
if (m_nMapMode MM_NONE)
( ) ' ) ., TRACEO("Error : must call SetScrollS工 zes() or SetScaleToFitS工 ze
TRACEO ( "\ tbefore painting scroll view . \ n 11 ) i
ASSERT(FALSE);
return;
#endif / / DEBUG
ASSERT(rn_totalDev.cx >= 0 && rn_totalDev.cy >= 0);
switch (m_DMapMode)
{
case MM_SCALETOFIT:
pDC->SetMapMode(MM_ANISOTROPIC);
pDC->SetWindowExt(mLtotalLog); // window is in log工cal coordinates
pDC一>SetViewportExt(rn_totalDev);
i f (rn_totalDev. cx 0 I I rn_totalDev. cy 0) TRACEO("Warning: CScrollView scaled to
nothing.\n" ) ; break;
default:
ASSERT(rn_nMapMode> 0);
pDC->SetMapMode(m_nMapMode);
break;
CPoint ptVpOrg(O , 0); // assurne no shift for print工ng
if (!pDC->工sPrinting () )
{
ASSERT(pDC->GetWindowOrg() CPoint(O , O));
/ / by default shift viewport origin in negative direction of scroll ptvpOrg =
-GetDevlceScrollPosltlon();
if (rn_bCenter)
{
CRect rect;
GetClientRect(&rect);
1/ if client area is larger than total device size,
1/ override scroll pos工tions to place origin such that
// output is centered in the window
if (m_totalDev.cx < rect.Width())
ptVpOrg.x = (rect.W工dth() -m_totalDev.cx) / 2; if (m_totalDev.cy < rect.Height())
ptVpOrg.y = (rect.Height() -~totalDev.cy) / 2;
}
pDC->SetViewportOrg(ptvpOrg);
CView::OnPrepareDC(pDC, p工nfo) ; // For default Printing behavior
}
"‘ 1423
第11
在如例 11-9所示的OnPrePareDC函数中,首先判断映射模式并设置相应的模式,默认是MM_TEXT映射
模式。
接着定义一个点对象(ptVp臼军),然后判断程序当前是否是在打印操作中,如果不是,则调用
GetDeviceScrollPosition函数,井将它的返回值加上负号作为视口原点坐标。然后调用
SetViewportOrg函数设置视口的原点。
下面我们就调试运行Graphic程序,看看视口原点坐标的变化。这里,读者一定要注意,调试运行前
不能在上述CScrollView类的OnPrepareDC函数中设置断点。因为在窗口重绘时会发送 WM_PA因T消
息,而在该消息的响应函数 OnPaint函数中会调用 OnPrepareDC函数。如果在调试之前就在此函数
中设置了断点,当窗口将要显示时,就要进行重绘操作,于是就会调用OnPrepareDC函数,这样就会
暂停于此函数中设置的断点处。如果继续执行程序,程序窗口又要进行重绘操作,于是又会调用
OnPrepareDC函数,这样就造成一个死循环,程序窗口永远也显示不出来。所以在开始调试Graphic
程序前,不能在OnPrepareDC函数处设置断点。
我们先调试运行Graphic程序,并将垂直滚动条拖动到窗口最下端,在窗口底部绘制一条直线,然后
切换到 VC++编辑界面,在 CScrol1View类的 OnPrepareDC函数中调用 SetViewportOrg函数的代码
行处设置断点,当再次切换回 Graphic程序窗口时,窗口要发生重绘,于是就会调用OnPrepareDC
函数,就会进入到刚刚设置的断点处(如图 11.12所示)。可以发现,现在视口原点的坐标是 (0,一
150),也就是说,当我们把垂直滚动条拖动到窗口最下端后,当窗口发生重绘时,在OnPrepareDC
中调用SetViewportOrg函数将视口
枣阳阶必
原点坐标设置为了 (0. -150)。到此,我们可以推测可能就是因为视口原点的改变而导致图形跑到
原位置的上方了。
毒冲 GetClientRect(&rect)i
黯
多忌
/1 if client area 1s largpr than total deuice size.
都" 11 override 5cyoll posit直on5 to placp origin 5ucb that
11 output 1s cpntered ln the window
if (回,_t.talDeu.cx ( rect.llidth())
d地
ptUpOrg.篇-(rect.llidth() -__t.talDeu国cx) I 2;
栩栩必d
if (_一t.talDeu.cg ( rect.Height()) ptUpOrg.g -(rect.Height() -__t.talDev.cg) I 2; }
~ J} pDC-)SetUie叩ortOrg(ptUpC[rg) ;
JJptYpOrg =(x=oy=.15011
J Emu-zzsnpveparesEtpB吨. '~.it;.-,. ,... -/, ':..<< beFault Printing behauioy
前掏钱蜘如
ψ泼'
W沪 111/111/11/1/1//1//11/1/1/111/11/1//111111//1//1111/1/IIII////lIl/I/II//.
图 11.12视口原点的当前坐标值
1.关于图形错位的说明当我们在窗口中单击鼠标左键的时候,得到的是设备坐标,例如 (680,390)。
在 MM_TEXT的映射模式下,逻辑坐标和设备坐标是相等的,因此我们利用集合类保存的这个点的坐
标是以像素为单位,坐标值为 (680, 390)。在调用OnDraw函数前,在OnPaint函数中调用了
OnPrepareDC函数,调整了显示上下文的属性,将视口的原点设置为了 (0,
-150),这样的话,窗口的原点,也就是逻辑坐标俑,0)将被映射为设备坐标 (0, -150),
在画线的时候,因为GDI的函数使用的是逻辑坐标,而图形在显示的时候, Windows需要
424 I如~~
vc.忡深λ详解
将逻辑坐标转化为设备坐标,因此,原先保存的坐标点 (680, 390)(在 GDI函数中,作为逻辑坐标
使用),根据转换公式 xViewport = xWindow-xWinOrg+xViewOrg和 yViewport =
yWindow-yWinOrg+yViewOrg,得到设备点的 x坐标为 68 0-0+0=680.设备点的 y坐标为 390-0+
(-150)=240,于是我们看到图形在原先显示地方的上方出现了。
在Graphic程序中,当把垂直滚动条拖动到窗口最下端时,在第一次画线时是在 CGraphicView类的
OnLButtonUp函数中实现的图形绘制,窗口并没有发生重绘,也就没有调用基类的 OnPrepareDC函数
去调整显示上下文的属性。也就是说,这时窗口和视口原点都是客户区左上角的 (0, 0)点,因此
这时候根据逻辑点 (680, 390)绘制出的坐标仍是 ( 680, 390 )。而窗口发生重绘时,调用了 On
PrepareDC函数,后者修改了视口原点,此时视口原点不再是客户区左上角的 (0, 0 )点,而是 ( 0.
-150)点,而窗口原点的坐标仍是 (0,0)。这时在 OnDraw函数中再次绘制直线时,所得到的设备坐
标变成了 (680, 240 ),而先前是 (680, 390),于是图形就在原位置的上方出现了。
读者应记住:不管视口原点和窗口原点如何改变,设备坐标(00)点始终位于窗口, 伍
客户区的左上角。
2.关于解决方法的说明
那么如何让图形在原先的位置上显示呢?首先在绘制图形之后,在保存坐标点之前,应该调用
OnPrepareDC函数,调整显示上下文的属性,将视口的原点设置为 (0,一 150) , 这样的话,窗口
的原点,也就是逻辑坐标 (0, 0)将被映射为设备坐标 (0,一 150),然后再调用 DPtoLP函数将设
备坐标 (680, 390)转换为逻辑坐标,根据设备坐标转换为逻辑坐标的公式:
xWindow = xViewport-xViewOrg+xWinOrg
yWindow = yViewport-yViewOrg+yWinOrg
得到逻辑点的 x坐标为 680-0+0=680, y坐标为 390一(-150)+0=540,并将得到的逻辑坐标 (680,
540)保存起来。
Graphic程序在窗口重绘时,会先调用 OnPrepareDC函数,调整显示上下文的属性,将视口的原点设
置为 (0, -150 ),然后 GDI函数使用逻辑坐标点 ( 680, 540 )绘制图形,而该坐标值将被 Windows
转换为设备坐标点 (680, 390),和原先显示图形时的设备点是一样的,于是图形就在原先的地方
显示出来了。
下面就遵照上述方法对 Graphic程序进行修改,在 CGraphicView类的 OnLButtonUp函数中添加如例
11-10所示代码中加灰显示的代码。
1--nu
M
void CGraphicView : : OnLButtonUp(U工 NT nFlags , CPoint point)
11 TODO: Add your rnessage handler code here and/ or call default
CClientDC dc(this);
CPen pen(rn_nLineStyle , rn_nLineWidth , rn_clr);
dc.SelectObject(&pen);
CBrush *pBrush=CBrush::FrornHandle((HBRUSH)GetStockObject(NULL_BRUSH));
" ‘ I 425
第 11
dc .SelectObject(pBrush) ;
switch(m_ nDrawType)
case 1:
dC . SetP工 xel(point, m_clr) ;
break;
case 2:
dc .MoveTo(m-ptOrigin) ;
dc . Lin eTo(point) ;
break;
case 3:
dc . Rectangle(CRect(m-ptOrigin , point)) ;
break;
、
case 4:
dc . Ellipse(CRe.t(m-ptOrigin, point)) ;
break;
// CGraph graph (m_nDrawType, m-p tOrigin, po工 n t) ; // m-ptrArray . Add(&graph);
OnPrepareDC(&dc) ;
dc . DPtoLP(&m-ptOrigin) ;
dc . DPtoLP (&point);
CGraph *pGraph=new CGraph(m_nDrawType, m-p tOr工 gin, point) ;
m-ptrArray .Add(pGraph) ;
CScrollVi ew: : OnLButto口Up(nFlags, point) ;
再次运行 Graphic程序,将垂直滚动条拖动到窗口最下端,井在窗口底部绘制图形。然后切换窗口
井切换回 Graph ic程序窗口,可以发现图形仍在原位置处显示了。也就是说,图形错位的问题解决
了。
这里,读者还应注意,因为每次窗口重绘时,都会调用 OnPrepareDC函数,而 OnPrepareDC会随时
根据滚动窗口的位置来调整视口的原点。也就是说,视口的原点不是一成不变的,它会随着滚动条
的位置不同而变化。我们仍可以通过调试运行程序来验证这一点。首先将 OnPrepareDC函数中设置
的断点去掉,然后调试运行 Graphic程序,把垂直滚动条拖动到窗口最下端,并在窗口底部绘制一
条直线,然后再把垂直滚动条拖动到窗口的最上端,再在 CScrollView类的 OnPrep缸eDC函数中调
用 SetViewportOrg函数的代码行处设置断点,接着切换到 Graphic程序窗口,这时程序就会进入到
OnPrepareDC函数所设断点处,可以发现这时视口的原点为 ( 0, 0 )。也就是说,当我们把垂直滚
动条拖动到窗口最上端时, OnPrepareDC会将视口原点调整为 ( 0,0 ),而不再是俑, -150 )。
为了理解上述内容,读者一定要了解,我们在绘图时使用的都是逻辑坐标,也就是说,我们都是在
页面空间,即逻辑空间中进行绘图操作的。但是这些图形实际上都要被映射到
…
详
设备空间中,因此坐标值必须要被转换为设备坐标,而这种转换不仅由映射模式,还由窗口原点和
视口原点、窗口范围和视口范围来约束。在 MM TEXT映射模式下,因为逻辑坐标单位和设备坐标单
位都是像素,所以相对来说转换比较简单,但是这时的转换仍会受到窗口原点和视口原点的影响。
⌨️ 快捷键说明
复制代码
Ctrl + C
搜索代码
Ctrl + F
全屏模式
F11
切换主题
Ctrl + Shift + D
显示快捷键
?
增大字号
Ctrl + =
减小字号
Ctrl + -