`
lovecontry
  • 浏览: 1029795 次
文章分类
社区版块
存档分类
最新评论

MFC学习笔记之图形保存与重绘

 
阅读更多

先增加一个新的菜单项 绘图 ,然后在里面增加4个子菜单项 点 线 矩形 椭圆 ,在View类中响应各个子菜单项,为View类增加一个私有数据成员 int m_nDrawType 用来保存用户所做的选择 这个和上一篇日志的一样 所以代码不贴了,然后就是响应 OnLButtonDown 和 ONLButtonUp 消息 进行相应的绘图。我们知道当窗口大小改变或是窗口被切换的时候,程序就会发送一个WM_PAINT消息,窗口就会发生重绘。这个时候我们画的图像就会消失不见,这一次就是要就保存和重绘图像进行一些相关操作。

我们知道窗口重绘时,会调用OnDraw函数,因此我们可以在这个函数中完成图像的输出。

保存图形的方法有很多,在本例中绘制的图像,有三个要素:绘画类型、起点、终点,也就是说对于本例中的图形,只要保存这三个属性就可以了。窗口重绘的时候在OnDraw函数中根据这三个要素就可以进行图像的绘制了。由于这三个参数的类型不同,可以用结构体来保存,在C++中结构体就是一个类,因此,本例可以新建一个新类来保存呢这三个要素。通过单击[Insert/New Class] 就可以进行新类创建了 ,弹出的菜单中 类类型要选择 Generic Class,新类命名为CGraph ,然后为该类增加三个成员变量,都设为public的,后面要用到。分别是:int m_nDrawType;CPoint m_ptOrign;CPoint m_ptEnd;分别保存 绘画类型 起点 终点 三个要素。然后再为该类增加一个有这三个参数的构造函数,让允许用户在构造该类对象时,可以直接通过这三个参数给成员变量赋值:

在绘制图像过程中,我们会画很多个图形,每一个图形都对应一个CGraph的对象,以保存该图像的三个要素。我们可以采用数组来保存这些创建的对象,但是长度未知,只可以存储一定容量的元素,这样不好。用链表的话又比较复杂。本例中,使用MFC的一个集合类:CPtrArray,它支持void 类型的指针数组,该类成员函数与CobArray类相应的函数类似,只是CObArray类的成员函数中使用CObject指针作为参数或者返回值类型的地方,在CPtrArray类中使用void 类型指针替换即可。

在本程序中我们只需要用到它的几个函数,如果要增加一个成员,就用add函数,来增加一个void类型的指针的对象,如果想取得该集合的某个元素就用GetAt方法,如果想获得集合的数目就用GetSize函数。 下面,先为View类增加一个CPtrArray类型的变量m_ptrArray.然后就在每次绘图之后,构造一个CGraph对象,并将该对象的地址保存到m_ptrArray中.

其中注释的代码先不理会 后面会说到,在构建CGraph对象的时候要用指针,不可以直接构建,不然会由于是临时变量的关系,导致m_ptrArray中保存的CGraph对象地址中的东西为空。 接下来呢就要在OnDraw函数中将它们重绘的显示出来:

因为CPtrArray类的GetAt函数返回的是一个void类型的指针,所以要进行强制的类型转换,讲其转换为CGraph类型的指针,猜可以正确访问CGraph的成员变量(因为在这里要访问,所以才在前面定义CGraph类的时候将这几个变量设为public的)。

这个是CView类的OnDraw函数的定义 是一个纯虚函数。另外在窗口重绘的时候发送的是WM_PAINT消息,如果想让图形始终可以显示出来,就可以讲图形的绘制操作放置在该消息的响应函数OnPaint中,而OnDraw函数并不是WM_PAINT消息的响应函数,但是它为什么可以在窗口发生重绘的时候被调用呢?下面是CView类的WM_PAINT消息的响应函数OnPaint函数的代码:

该代码在VC安装目录\Microsoft Visual Studio\VC98\MFC\SRC目录下 在一个叫"VIEWCORE.cpp"的文件中,在该函数的代码中,我们看到,它先利用CPaintDC类构造了一个设备上下文对象dc。我们知道,在响应WM_PAINT消息的时候,如果想得到DC句柄,只能调用BeginPaint函数来获取,想释放这个DC句柄的时候,只可能调用EndPaint函数来释放。但是在这里,只是利用CPaintDC类构造了一个DC对象而已。其实CPaintDC类和CDC类的关系就和 CClientDC类和CDC类的关系是类似的。CPaint类将BeginPaint函数和EndPaint函数的调用分别封装在了构造函数和析构函数中。所以CPaintDC对象仍然是通过调用BeginPaint和EndPaint函数来获取和释放DC句柄。并且,CPaintDC类对象只能够在响应WM_PAINT消息时使用,通常是在OnPaint函数中使用。在上述代码中,接下来是调用了OnPrepareDC函数,这也是一个虚函数:

在为屏幕显示而调用OnDraw函数之前,以及在打印或者打印预览过程中调用OnPaint函数打印每一页文档之前,框架都会调用该函数。如果因屏幕显示而被调用,OnPrapareDC函数默认实现就是什么也不做,但是它在派生类中会被重写,例如,在CScrollView类中被重写,用以调整设备上下文的属性。因此,如果你重写了OnPrepareDC,在函数的开头,总会先调用基类的OnPrepareDC函数。最后呢,在上述代码中调用了OnDraw函数,根据上面OnDraw函数的定义,我们知道它是虚函数,商业根据多态性原理,这里调用的实际上是子类,也就是本例中的CMyView类的OnDraw函数。

根据上面的分析,因为基类CView在响应WM_PAINT消息的时候调用了OnDraw函数,所以子类中的OnDraw函数才会被调用。给我们的感觉好像是AppWizard自动生成的视图类的中OnDraw函数就是专门用来重绘窗口的,这里其实是为了我们使用方便,让我们可以在此函数中自行进行图形的绘制。

接下来说一下窗口滚动功能的实现:

利用MFC AppWizard生成工程的时候,在向导的第六步可以把视类的积累选择为CScrollView,这样,视图窗口就具有滚动功能了。但是现在我们的工程已经生成了,如果要为其增加窗口滚动功能,可以手动将程序的视类基类由CView改为CScrollView类。在CMyView类的头文件中只有继承那里需要修改,但是在源文件中就有很多地方需要修改,为了不遗漏,我们利用Edit\Replace 菜单命令进行替换。在Find What中输入CView,在Replace with中输入CScrollView,然后选中“Match whole word only”,然后按 Replace All 按钮就可以了。这时候程序可以成功编译连接,但是运行的时候就会出现一个非法操作的提示。这是因为对于滚动窗口,在创建的时候需要进行一些设置,包括整个滚动窗口的大小,以及当单击滚动条箭头的时候滚动条的滚动的数值和单击滚动栏的时候滚动条的滚动数值。要进行这些设置,需要调用CSrollView类的成员函数 SetScrollSizes:

该函数中后两个参数都有默认值了,所以我们只需要设置前面两个就OK了。第一个是映射模式,取值可以看MSDN关于这个函数的定义,第二个是设置视图窗口总大小。

该函数的调用应该在视图窗口创建完成之后。在这里可以为CMyView添加一个虚函数:OnInitialUpdate.该函数是在窗口完全创建完成之后第一个调用的函数,也就是在第一次调用OnDraw函数之前调用。

这样程序窗口打开后就多了两条滚动条了。

现在这样的滚动功能是不完备的,我们将滚动条拉到最下面然后在那里绘制一条直线或是一个矩形,然后切换窗口,再切换回绘画的窗口,会发现我们刚刚画的图像在原来绘制的地方的上面出现了。我们知道切换窗口的时候窗口会发送重绘,也就是在OnDraw函数中将重新绘制图形。当没用加入滚动条的时候,不管我们的窗口放大缩小还是怎么切换,原先画的图像都不会错位显示或者消失,现在加了滚动条就出现问题了,为什么呢?我们知道,在调用OnDraw函数之前,OnPaint函数先调用了OnPrepareDC函数来调整显示上下文的属性。因此,可能是在此函数中调整了显示上下文的属性,从而导致了先前的现象,即图像跑到原位置上面显示了。为了更好的找出原因,我们先看看CSrollView类中已经重写的OnPrepareDC函数:

该函数在 VC安装目录\Microsoft Visual Studio\VC98\MFC\SRC 目录下 在一个叫"VIEWSCRL.cpp"的文件中,上面的代码主要看下面这几句:

首先是判断映射模式并设置相应的模式,默认是MM_TEXT模式。因为我们在程序中选择的隐射模式就是MM_TEXT(看上面的OnInitialUpdate函数的代码),所以在这个函数中会执行switch的default部分,设置模式为MM_TEXT。接着定义了一个点对象 ptVpOrg,然后判断是否在打印中,不是的话就调用GetDeviceScrollPosition函数,并将它的返回值加上负号作为视口原点坐标。最后用 SetViewportOrg函数设置视口的原点。

下面我们调试运行一下程序 看看视口原点坐标的变化。我们在调试运行前不可以在什么代码的pDC->SetViewportOrg(ptVpOrg);处添加断点,因为在窗口重绘的时候会发送WM_PAINT消息,而在该消息响应函数中会调用OnPrepareDC函数。所以如果在这里设置了断点,那么当窗口要显示的时候就会发生重绘,那么就会停在断点处了,如果继续执行,窗口又要显示,又要发送重绘,就又会停在断点处,这样窗口永远都显示不出来。

所以一开始我们先不设断点,然后直接调试运行,将滚动条拉到最下,然后绘制一个矩形,然后切换窗口到VC的编辑界面,在pDC->SetViewportOrg(ptVpOrg);处设置断点,再切换回绘图窗口,这时,程序就会停在断点处了,查看此时的视口原点坐标,是(0,-222),这个数值会由于窗口大小不同而取不同的值,所以大家在测试的时候发现数值不一样是正常的。 通过上面的现象,我们知道当我们把滚动条拉到最下面后,当窗口重绘时,在OnPrepareDC函数中调用SetViewportOrg函数将视口原点设置为(0,-222).此时,我们可以推测可能就是因为视口原点的改变导致图形跑到原位置上方。

下面是解释一下错位现象是怎么发生的:

当我们在客户区中单击鼠标左键的时候,得到的是设备坐标,例如(680,390).在MM_TEXT映射模式下,逻辑坐标和设备坐标是相等的,因此我们利用集合类保存的这个点的坐标是以像素为单位的,坐标值是(680,390).在调用OnDraw函数前,OnPaint函数先调用了OnPrepareDC来调整设备上下文的属性,将视口原点设置为(0,-222),这样的话,视口原点就不再是(0,0)了,在画线的时候,页面GDI的函数使用的是逻辑坐标,而图形在显示的时候,Windows需要讲逻辑坐标转换为设备坐标,因此,原先保存的坐标点(680,390)(在GDI函数中,作为逻辑坐标使用,因为映射模式是MM_TEXT,所以该点的逻辑坐标和设备坐标是一样的),根据转换公式:

得到设备点的x坐标为680-0+0=680;设备点的y坐标为390-0+(-222)=168;于是我们看到的图像就在原先显示位置的上方出现了。

什么的参数代表的意义呢是这样的:x/yWindow是该点的逻辑坐标,x/yWinOrg 是窗口原点,x/yViewOrg 是视口原点,x/yViewport是该点的设备坐标。

再总结一下,第一次画线的时候,是在OnLButtonUp函数中完成绘图,窗口并没用发生重绘,也就没用调用到OnPrepareDC函数改变设备上下文属性,此时的视口原点和窗口原点都是客户区左上角的(0,0)点,因此此时根据逻辑点(680,390)转换出来的设备坐标仍然是(680,390).而当窗口发生重绘的时候,调用了OnPrepareDC函数,改变了视口原点坐标,此时视口原点坐标不再是左上角的(0,0),而是(0,-222),而窗口原点的坐标仍是(0,0),这时,在OnDraw函数中要再次画线的时候,所得到的设备坐标根据上面的计算知道就是(680,168)了,于是图形当然就是在原位置的上方显示了。

解决方法:

我们已经知道了为什么它会显示错位。我们在LButtonUp中绘制完图形之后,在保存坐标点之前,调用OnPrepareDC函数,调整设备上下文属性,将视口坐标的原点设置为(0,-222),然后再调用DPtoLP函数将设备坐标(680,390)转换为逻辑坐标,根据设备坐标转逻辑坐标的公式:

得到逻辑坐标x的坐标为 680-0+0=680,y坐标为 390-(-222)+0=612,并将得到的逻辑坐标(680,612)保存起来。

什么是DPtoLP函数的定义,传一个CPoint类的对象的指针参数既可。

在窗口发生重绘时,会先调用OnPrepareDC调整设备上下文属性,将视口原点设置为(0,-222),然后GDI函数使用逻辑坐标点(680,612)绘制图形,而该坐标值将被Windows转换为设备坐标(680,390)转换公式上面有给出,这样得到的坐标点就和原先显示的坐标点一样了,于是图形就在原先的位置显示出来了。添加的代码如下:

在LButtonUp的switch之后,在保存坐标点之前,加入了三行代码。

还要说一点就是,因为每次窗口重绘的时候都会调用OnPrepareDC这个函数,而OnPrepareDC函数会随时根据滚动窗口的位置来调整视口的原点。也就是说视口的原点不是一成不变的,它会随滚动条的位置不同而发生变化。

上面说到的 设备坐标 逻辑坐标 视口 窗口 等概念都是和MFC的转换模式相关的一些概念 涉及到的东西比较多,在这里就不详细说了。逻辑坐标对应的是在窗口中的坐标也就是页面空间中的,页面空间实际上就是一块内存,Windows会将页面空间平移到设备空间 此时逻辑坐标就会被转换为设备坐标 视口也就是客户区。

几乎所有的GDI函数中使用的坐标值都是逻辑坐标,Windows都会将它转换为“设备单位”,也就是像素。Windows的所有消息,所有非GDI函数和一些GDI函数永远使用设备坐标。在MM_TEXT映射模式下,因为逻辑坐标单位和设备坐标单位都是像素,所以相对来说转换比较简单,但是这时候的转换仍然会受到视口原点和窗口原点的影响。

上面介绍了在有滚动功能的窗口中如何解决图像重绘时候出现错位的方法,下面再说两种保存图形和重绘图形的方式,当然是在窗口没用滚动功能的前提下的。

第一种是元文件对图形的保存和显示:

对元文件的使用需要用到元文件设备上下文类:CMetaFileDC,该类也是派生于CDC类。一个Windwos元文件DC包含了一系列图形设备接口命令,在程序中可以重放这些命令,以便创建所需要的图形或文本。就像这里所说的,元文件是包含了一系列与图形设备接口相关的命令,例如画一条直线,一个矩形,输出一串文本,这时绘制的图形是看不见的,她们存在于元文件之中,实际上是在内存中绘制。当元文件绘制完成后,可以播放该元文件,这时就可以在窗口中看到先前在该文件中绘制的图形了。还是要提醒一下,在元文件中并没用包含所绘制图形的图形数据,它包含的指示绘制的命令。

可以通过下面的步骤来使用Windows元文件:

1、利用CMetaFileDC构造一个对象,然后利用该类的Create函数创建一个元文件设备上下文,并将其与已构造的CMetaFileDC对象相关联。

函数参数是一个文件名,是一个以NULL结尾的字符串,如果有指定文件名,那么元文件会被保存在该文件中,如果没设置,那么创建的元文件就是一个内存元文件。

2、给以创建的元文件DC对象发送一系列GDI命令,例如MoveTo LineTo等。

3、给元文件DC对象发送完需要的命令后就可以调用Close成员函数关闭元文件设备上下文,返回一个句柄(HMETAFILE类型)。

4、以得到的元文件句柄为参数,利用CDC类的PlayMetaFile成员函数播放该元文件

5、播放完以后,就不需要该元文件了,因为元文件也是一种资源,所以在使用完之后,也需要释放,这可以通过DeleteMetaFile函数将其删除。

下面为CMyView类增加一个CMetaFileDC类型的成员变量:m_dcMetaFile,然后在构造函数中调用Create函数进行创建。在LButtonUp中将原来CClientDC的对象出现的地方都改成m_dcMetaFile的。

什么就是修改过的LButtonUp函数。这时,就可以开始进行元文件操作的第三步了,在窗口重绘时,即在OnDraw函数中来播放元文件,以实现图形的显示。现在先将原来OnDraw函数中所写的重绘代码注释起来,然后加入下面几行代码:

这就是按照元文件操作步骤所进行的操作,显示关闭元文件,然后就是播放元文件,然后先再创建一个内存元文件,以便于用户再次绘制图形时使用,最后是释放元文件资源。

写好后,编译运行,选择画线 在窗口上画的时候 我们看到什么都没有 这是切换窗口再切换回来,线就出现了。我再在上面画几个矩形,依然是看不见的,然后切换,再次显示的时候,我们看到只有矩形,之前画的线不见了。这是因为我们在代码中重新创建了一个内存元文件,所以之前绘制的图形就不在了,如果想保存有之前的绘制的图形,那么可以在创建完新的元文件DC对象后,用新的元文件DC的PlayMetaFile函数去播放之前的元文件,就是下面的代码所写的这样:

当用pDC的PlayMetaFile函数时它会将指定元文件中的GDI命令在窗口中输出,而调用新的元文件DC的PlayMetaFile时,它会将先前元文件中绘制的图形在该元文件DC中绘制一遍,这就相当于将之前的图形绘制保存到新的元文件DC中。这样就可以在显示完上一次的图形后,继续绘制的时候,在下次重绘湿,之前绘制的图形依然存在。

如果想要在绘图的时候可以看到绘制的图形,那么只需要将LButtonUp中的CClientDC的dc对象的代码的注释去掉就可以了,最后的保存那里的注释不可以去掉。

接下来,我们实现把保存图形绘制命令的元文件保存为磁盘文件,以便在需要的时候可以随时打开,并在窗口中显示其中的图形内容。于是,我们可以将原本程序中的菜单项文件的子菜单项的保存和打开的响应函数中分别实现元文件的保存和打开。

为了保存元文件,可以使用CopyMetaFile函数,该函数作用是将Windows元文件的内容复制到知道的文件中。

下面是保存的响应函数:

代码中将元文件保存到一个以wmf为后缀的文件meta中,一般Windows元文件后缀是 wmf(Windows Meta File)。接下来要实现打开元文件的功能,这时需要利用GetMetaFile函数得到指定元文件的句柄。虽然这个函数现在已经被Win32 API废弃了 但是我们仍然可以用它来返回指定元文件句柄。

最后调用了Invalidate函数引起窗口重绘,来显示元文件DC对象 m_dcmetaFile中的内容。

第二种是使用兼容DC来实现图形的保存与重绘:

兼容DC在上一篇MFC学习笔记中有说到,当时是利用兼容DC在内存中准备一副图像,然后将该图像复制到目的窗口中。现在我们也可以用兼容DC来保存图形,然后在OnDraw中将兼容DC保存的图形复制到目的窗口中。

下面首先为CMyView类增加一个CDC类成员m_dcCompatible,然后在该类的LButtonUp中,利用兼容DC实现图形的保存,代码如下:

要判断是否已经创建了m_dcCompatible这个兼容DC对象,没用就创建,并与当前DC(即dc)兼容。因为这个函数是会被多次掉用的,所以需要增加这个判断,不可能重复创建这个兼容DC。之前的日志中已经有说道,兼容DC初始创建的时候,它会选择一副单色位图。之前我们是通过SelectObject函数将一副位图选入兼容DC中来确定其显示表明的大小。现在我们没有这样一副位图,需要去创建一个与当前窗口DC相兼容的DC,它的显示表面和大小与当前客户区的大小一样。可以通过创建兼容位图来实现。CBitmap类的成员函数CreateCompatibleBitmap()可以通过指定的宽和高创建一副与指定DC相兼容的位图。接下来的就是将创建好的位图选入兼容DC中。因为兼容DC实际上是一块内存,所以利用它绘制的图形在窗口中是看不到的,因此,接下来在OnDraw函数中,就可以利用已经创建的兼容DC对象,将该DC中的内容复制到目的DC中,从而实现图形的显示。

具体的就是先将原本OnDraw函数中的代码注释起来 然后加入下面的三行代码:

编译运行程序,绘制,切换窗口,我们发现程序显示的背景是一块黑色的,但是我们并没用做这样的设置,出现这样的问题的原因呢是因为CreateCompatibleBitmap返回的位图对象只包含了相应的设备描述表中的位图信息头,不包含该位图的颜色表和像素数据块。因此选入该位图对象的设备描述表不能像选入普通位图对象的设备描述表那样应用,必须在SelectObject函数之后,调用BitBlt函数将原始设备描述表的颜色表以及数据块复制到兼容设备描述表之中。也就是将我上面贴的LButtonUp中的在判断兼容DC对象是否已经创建的代码中的注释去掉 就可以了。

这里的重绘操作实际上就是利用BitBlt还念书通过贴图操作来实现的。

这一次的写了好久 总结这种东西 花的时间还真是比看书的时间还长。。。。

分享到:
评论

相关推荐

Global site tag (gtag.js) - Google Analytics