双缓冲绘图

合集下载
  1. 1、下载文档前请自行甄别文档内容的完整性,平台不提供额外的编辑、内容补充、找答案等附加服务。
  2. 2、"仅部分预览"的文档,不可在线预览部分如存在完整性等问题,可反馈申请退款(可完整预览的文档不适用该条件!)。
  3. 3、如文档侵犯您的权益,请联系客服反馈,我们会尽快为您处理(人工客服工作时间:9:00-18:30)。

1创建画笔,画刷
void CGraphView::OnDraw(CDC* pDC)
{
CGraphDoc* pDoc = GetDocument( );
ASSERT_V ALID(pDoc);
// TODO: add draw code for native data here
// 使用画笔第一步:创建一支新画笔
CPen pen(PS_SOLID, 1, RGB(255,0,0));
CPen pen(PS_DASHDOT, 1, RGB(255,0,0));
//第二步:选择新画笔到设备环境中,同时保存旧画笔CPen * pOldPen = pDC->SelectObject(&pen);
// 使用画刷第一步:创建一支新画刷
CBrush brush(RGB(255,0,0)); //创建实体画刷
CBrush brush(HS_CROSS, RGB(255,0,0)); //创建阴影模式画刷//第二步:选择新画刷到设备环境中,同时保存旧画刷CBrush * pOldBrush = pDC->SelectObject(&brush);
// 常用的绘图函数//
//绘制一个彩色点
pDC->TextOut(20, 20, "点");
pDC->SetPixel(100, 40, RGB(255,0,0));
//绘制直线
pDC->TextOut(320, 20,"线段");
pDC->MoveTo(400, 40);
pDC->LineTo(500, 40);
//绘制折线
pDC->TextOut(20, 170, "折线");
POINT polyline[4]={{240,240},{80,120},{240,120},{80,240}}; pDC->Polyline(polyline,4);
//绘制矩形
pDC->TextOut(320, 170, "矩形");
pDC->Rectangle(390, 110, 600, 230);
//绘制椭圆
pDC->TextOut(20, 320, "椭圆");
pDC->Ellipse(80, 260, 280, 380);
//绘制多边形
pDC->TextOut(320, 320, "多边形");
POINT polygon[3]={{380,330},{530,260},{500,360}};
pDC->Polygon(polygon,3);
//第三步:恢复设备环境的旧画笔,以便释放新画笔
// pDC->SelectObject(pOldPen);
//第三步:恢复设备环境的旧画刷,以便释放新画刷
// pDC->SelectObject(pOldBrush);
}
2创建字体的两种方法
第一种创建字体的方法:
使用CreateFont函数,如下定义:
CFont::CreateFont
BOOL CreateFont
( int nHeight, //字体的高度
int nWidth, //字体的宽度
int nEscapement, //字体显示的角度
int nOrientation, //字体的角度
int nWeight, //字体的磅数
BYTE bItalic, //斜体字体
BYTE bUnderline, //带下划线的字体
BYTE cStrikeOut, //带删除线的字体
BYTE nCharSet, //所需的字符集
BYTE nOutPrecision, //输出的精度
BYTE nClipPrecision, //裁减的精度
BYTE nQuality, //逻辑字体与输出设备的实际
//字体之间的精度
BYTE nPitchAndFamily, //字体间距和字体集LPCTSTR lpszFacename //字体名称
);
例子:font.CreateFont(
12, // nHeight
0, // nWidth
0, // nEscapement
0, // nOrientation
FW_NORMAL, // nWeight
FALSE, // bItalic
FALSE, // bUnderline
0, // cStrikeOut
ANSI_CHARSET, // nCharSet
OUT_DEFAULT_PRECIS, // nOutPrecision
CLIP_DEFAULT_PRECIS, // nClipPrecision DEFAULT_QUALITY, // nQuality
DEFAULT_PITCH | FF_SWISS, // nPitchAndFamily "Arial"); // lpszFacename一般只修改几项。

1.nWeight(磅数):
Value Weight
FW_DONTCARE 0
FW_THIN 100
FW_EXTRALIGHT 200
FW_ULTRALIGHT 200
FW_LIGHT 300
FW_NORMAL 400
FW_REGULAR 400
FW_MEDIUM 500
FW_SEMIBOLD 600
FW_DEMIBOLD 600
FW_BOLD 700
FW_EXTRABOLD 800
FW_ULTRABOLD 800
FW_HEA VY 900
FW_BLACK 900
2.nCharSet(字符集):
把ANSI_CHARSET,改成DEFAULT_CHARSET 或者GB2312_CHARSET
3.lpszFacename(字体名称):改为,宋体,黑体.......
4.nHeight(字体高度)改为,数字。

第二种创建字体的方法
使用CreateFontIndirect函数,如下定义:
CFont::CreateFontIndirect
BOOL CreateFontIndirect(const LOGFONT* lpLogFont );
其实他需要一个LOGFONT结构体,里面定义的东西和上面函数中的参数几乎是一样的。

具体说明参考百度百科:/view/1080749.htm?fr=ala0_1
下面是我做的一个例子,里面有创建字体的两种方法,还有文本的设置方面的东西,都做了注释:只需要在你的View类中的OnDraw函数中添加如下代码:
CFont font1,font2;
CFont *pOfont;
CRect rc(100,100,600,0);
pOfont = pDC->SelectObject(&font1);
pDC->SetBkMode(OPAQUE);
pDC->SetBkColor(RGB(0,255,0));
font1.CreateFont(20,15,30,15,600,1,1,1,ANSI_CHARSET,OUT_DEFAULT_PRECIS, CLIP_DEFAULT_PRECIS,DEFAULT_QUALITY,
DEFAULT_PITCH|FF_SWISS,"OWnFont");
pDC->SelectObject(&font1);
pDC->SetTextAlign(TA_NOUPDATECP|TA_LEFT|TA_TOP);
pDC->SetTextColor(RGB(0,0,255));
//pDC->TextOut(30,80,"CreateFont方法创建的字体");
pDC->ExtTextOut(200,50,ETO_CLIPPED,rc,"CreateFont方法创建的字体",NULL); font1.DeleteObject();
LOGFONT log;
log.lfCharSet = ANSI_CHARSET;
log.lfHeight = 20;
log.lfWidth = 10;
log.lfWeight = 800;
log.lfStrikeOut = 1;
log.lfUnderline = 1;
log.lfEscapement = 0;
strcpy(log.lfFaceName,"自定义字体");
font2.CreateFontIndirect(&log);
pDC->SelectObject(&font2);
pDC->SetTextAlign(TA_NOUPDATECP|TA_LEFT|TA_TOP);
pDC->SetTextColor(RGB(255,0,255));
//pDC->TextOut(30,150,"CreateFontIndirect");
pDC->ExtTextOut(100,80,ETO_CLIPPED,rc,"CreateFontIndirect",NULL);
font2.DeleteObject();
pDC->SelectObject(pOfont);
void CLi3_8View::OnDraw(CDC* pDC)
{
CLi3_8Doc* pDoc = GetDocument();
ASSERT_V ALID(pDoc);
// TODO: add draw code for native data here
CString outstr[5];
outstr[1]="1.使用函数CreatPointFont()创建字体字";
outstr[2]="2.使用函数CreatFontIndirect()创建倾斜、带下划线的黑体字";
outstr[3]="3.使用函数CreateFont()创建带删除线的大号字";
outstr[4]="4.使用库存字体对象创建ANSI标准的等宽字";
CFont *OldFont,NewFont;
LOGFONT MyFont={
30,
10,
0,
0,
0,
1,
1,
0,
ANSI_CHARSET,
OUT_DEFAULT_PRECIS,
CLIP_DEFAULT_PRECIS,
DEFAULT_QUALITY,
DEFAULT_PITCH,
"黑体"
};
pDC->TextOut(0,10,"创建字体的几种方法:");
for(int i=1;i<5;i++){
switch(i){
case 1:
//使用函数CreatPointFont()创建字体
NewFont.CreatePointFont(200,"宋体",NULL);
break;
case 2:
//使用函数CreateFontIndirect()创建字体
NewFont.CreateFontIndirect(&MyFont);
break;
case 3:
//使用函数CreateFont()创建字体
NewFont.CreateFont(30,10,0,0,FW_HEA VY,false,false,
true,ANSI_CHARSET,OUT_DEFAULT_PRECIS,
CLIP_DEFAULT_PRECIS,DEFAULT_QUALITY,
DEFAULT_PITCH|FF_DONTCARE,"大号字");
break;
case 4:
//使用库存字体对象创建字体
pDC->SelectStockObject(ANSI_FIXED_FONT);
break;
}
OldFont=pDC->SelectObject(&NewFont);
pDC->TextOut(0,60*i,outstr[i]);
pDC->SelectObject(OldFont);
NewFont.DeleteObject();
}
}
总结:
经过例程的学习研究,我掌握了四种方法来创建四种不同的字体的方法:
1.使用函数CreatePointFont()创建宋体字
2.使用函数CreateFontIndirect()创建倾斜、带下划线的黑体字
3.使用函数CreateFont()创建带删除线的大号字
4.使用库存字体对象创建ANSI标准的等宽字
我已经掌握了使用CFont创建新字体对象和旧字体对象指针的方法,我已经能够使用LOGFONT来创建一个结构体变量,并将它传送给函数CreateFontIndirect()来创建一个新字体,并了解各个变量的意义,我懂得了使用文档指针来调用它的成员函数来输出新字体,使用完新字体之后,恢复旧字体,并删除旧字体。

3显示位图
1. 显示程序资源中的位图(位图的所有数据均存在于可执行文件中)
(1)从资源中装入位图
定义位图对象数据成员CBitmap m_Bitmap;
调用CBitmap成员函数LoadBitmap(),
如m_Bitmap.LoadBitmap(IDB_BITMAP1);
传入LoadBitmap的参数是位图在图形编辑器中生成或从位图文件中引入时赋予的识别符。

(2)生成与位图相联系的内存设备情境对象
CDC MemDC;
MemDC.CreateCompatibleDC(NULL);
MemDC.SelectObject(&m_Bitmap);
(3)显示位图
CClientDC ClientDC(this);
BITMAP BM;
m_Bitmap.GetObject(sizeof(BM),&BM);
ClientDC.BitBlt ( X,Y, //目标设备逻辑横、纵坐标
BM.bmWidth, BM.bmHeight, //显示位图的像素宽、高度
&MemDC, //待显示位图数据的设备情境对象
0,0, //源数据中的横、纵坐标
SRCCOPY); //位操作方式
这种方法显示位图速度快,但不是很灵活,而且会使可执行文件增大。

2. 显示独立文件方式的位图
HBITMAP *hBitmap; //定义位图对象句柄
BITMAP BM;
CDC MemDC;
CClientDC ClientDC(this);
MemDC.CreateCompatibleDC(&ClientDC);
hBitmap=::LoadImage(AfxGetInstanceHandle(),//取得应用程序句柄
“demo1.bmp”, //位图文件名
IMAGE_BITMAP, //类型为Windows位图
0,0,
LR_LOADFROMFILE);
//从文件中取位图数据
MemDC.SelectObject(hBitmap);
:: GetObject(hBitmap,sizeof(BM),&BM);
ClientDC.BitBlt(……)
//使用格式与方法一同
这种方法显示位图速度较之前一种慢了一点,但其灵活性较大,可以任意变换位图文件,而无需重新编译源程序, 也减小了可执行文件的大小。

实现方法
下面介绍各种图形显示技巧的具体实现原理及方法。

以下所有程序算法的实现均可放在视类(CView,也可视自己的需要放在其他类)中处理,且有必要进行如下的相关操作:
增加如下类成员变量:
BITMAP m_Bm;
//保存位图的宽、高度等数据
HBITMAP *m_hBitmap;
//保存位图数据句柄
CDC m_MemDC; //内存设备情境对象
在OnInitUpdate()函数中加入如下代码:
m_MemDC.CreateCompatibleDC(NULL); //产生内存设备情境对象
//从文件中装入位图数据
m_hBitmap=::LoadImage(AfxGetInstanceHandle(), “demo1.bmp”,
IMAGE_BITMAP, 0,0, LR_LOADFROMFILE ); m_MemDC.SelectObject(m_hBitmap); //将位图选入内存设备情境对象::GetObject(m_hBitmap,sizeof(m_Bm),&m_Bm);
1. 水平交错效果
原理:将内存设备情境对象(如MemDC)中的位图数据拆分成奇、偶扫描线两部分,其中奇数条扫描线由上往下移动,偶数条扫描线则由下往上移动,且两者同时进行。

屏幕上的效果为分别由上下两端出现的较淡栅栏图形,逐渐相互靠近,直至整个位图完全清楚。

垂直交错效果的实现原理与之类似。

程序算法:
int i,j;
for ( i=0; i<=m_Bm.bmHeight; i+=2 )
{j = i;
while ( j>0 )
{ClientDC.StretchBlt(
//奇数,由上至下
0,j-1,
//目标设备逻辑横、纵坐标
m_Bm.bmWidth,1,
//显示位图的像素宽、高度
&m_MemDC,
//源位图设备情境对象
0,m_Bm.bmHeight-(i-j-1),
//源位图的起始横、纵坐标
//源位图的像素宽、高度
SRCCOPY);
ClientDC.StretchBlt(
//偶数,由下至上
0,m_Bm.bmHeight-j,
//目标设备逻辑横、纵坐标
m_Bm.bmWidth,1,
//显示位图的像素宽、高度
&m_MemDC,
//源位图设备情境对象
0,i-j,
//源位图的起始横、纵坐标
m_Bm.bmWidth,1,
//源位图的像素宽、高度
SRCCOPY);
j-=2; }
// while ( j>0 )
Sleep(10);
}
//for ( i=0; i<=m_Bm.bmHeight; i+ =2 )
2. 雨滴效果
原理:将内存设备情境对象(如MemDC)中位图数据的最后一条扫描线,顺序地从目标设备(如ClientDC)中待显示位图的第一条扫描线所在位置移动至最后一条处,并保留此条扫描线在屏幕上移动时留下的轨迹。

接着再把MemDC中位图数据的倒数第二条扫描线,顺序地从目标设备(如ClientDC)中待显示位图的第一条扫描线所在位置移动至倒数第二条处。

其余的扫描线依此类推。

程序算法:
int i,j;
for ( i=0; i<=m_Bm.bmHeight; i++ )
{for ( j=0; j<=m_Bm.bmHeight-i; j++ )
ClientDC.StretchBlt(
0,j,
//目标设备逻辑横、纵坐标
m_Bm.bmWidth,1,
//显示位图的像素宽、高度
&m_MemDC,
//源位图设备情境对象
//源位图的起始横、纵坐标
m_Bm.bmWidth,1,
//源位图的像素宽、高度
SRCCOPY);
Sleep(20);
}
//for ( i=0; i<=m_Bm.bmHeight; i++ )
3. 百叶窗效果
原理:将内存设备情境对象(如MemDC)中的位图数据分成若干组,然后分别从第一组到最后一组进行搬移,第一次搬移每组中第一条扫描线到目标设备(如ClientDC)中待显示位图的相应位置,第二次搬移每组中第二条扫描线,接着第三条、第四条扫描线。

程序算法:
int i,stepi,j;
stepi=m_Bm.bmHeight/10;
for ( i=0; i<=stepi; i++ )
{for ( j=0; j<10; j++ )
ClientDC.StretchBlt(
0,j*stepi+i,
//目标设备逻辑横、纵坐标
m_Bm.bmWidth,1,
//显示位图的像素宽、高度
&m_MemDC,
//源位图设备情境对象
0,j*stepi+i,
//源位图的起始横、纵坐标
m_Bm.bmWidth,1,
//源位图的像素宽、高度
SRCCOPY);
Sleep(20);
} //for ( i=0; i<=stepi; i++ )
4. 随机积木效果
原理:将内存设备情境对象(如MemDC)中的位图数据分成纵横十等份共一百组数据,然后随
机地取出这一百组数据中的某一组显示到目标设备(如ClientDC)中待显示位图的相应位置,如此反复直到所有一百组数据均显示完毕为止。

程序算法:
int i,j,stepx,stepy,dispnum,x,y;
int pxy[10][10];
//使用本数组记录已显示过的数据组
for ( i=0; i<10; i++ )
for ( j=0; j<10; j++ )
pxy[i][j]=0;
stepx=m_Bm.bmWidth/10;
stepy=m_Bm.bmHeight/10;
srand( (unsigned)time( NULL ) );
dispnum=0;
//记录已显示过的数据组的个数
while(1)
{ x=rand() % 10;
y=rand() % 10;
if ( pxy[x][y] )
//本组x,y所代表的数据组是否已显示过?
continue;
pxy[x][y]=1;
//表明本组x,y所代表的数据组已显示过
ClientDC.StretchBlt(
x*stepx, y*stepy,
//目标设备逻辑横、纵坐标
stepx,stepy,
//显示位图的像素宽、高度
&m_MemDC,
//源位图设备情境对象
x*stepx, y*stepy,
//源位图的起始横、纵坐标
stepx,stepy,
//源位图的像素宽、高度
SRCCOPY);
dispnum++;
if ( dispnum >=100 )
break;
Sleep(30);
} // while(1)
结语
以上程序代码均在Visual C++ 6.0中调试通过,所有片断均可编写成独立的函数,灵活使用。

如果对以上几种显示效果进行变换,我们还可以实现多种其他特技效果。

4 双缓冲绘图
显示图形如何避免闪烁,如何提高显示效率是问得比较多的问题。

而且多数人认为MFC的绘图函数效率很低,总是想寻求其它的解决方案。

MFC的绘图效率的确不高但也不差,而且它的绘图函数使用非常简单,只要使用方法得当,再加上一些技巧,用MFC可以得到效率很高的绘图程序。

我想就我长期(呵呵当然也只有2年多)使用MFC绘图的经验谈谈我的一些观点。

1、显示的图形为什么会闪烁?
我们的绘图过程大多放在OnDraw或者OnPaint函数中,OnDraw在进行屏幕显示时是由OnPaint 进行调用的。

当窗口由于任何原因需要重绘时,总是先用背景色将显示区清除,然后才调用OnPaint,而背景色往往与绘图内容反差很大,这样在短时间内背景色与显示图形的交替出现,使得显示窗口看起来在闪。

如果将背景刷设置成NULL,这样无论怎样重绘图形都不会闪了。

当然,这样做会使得窗口的显示乱成一团,因为重绘时没有背景色对原来绘制的图形进行清除,而又叠加上了新的图形。

有的人会说,闪烁是因为绘图的速度太慢或者显示的图形太复杂造成的,其实这样说并不对,绘图的显示速度对闪烁的影响不是根本性的。

例如在OnDraw(CDC *pDC)中这样写:
pDC->MoveTo(0,0);
pDC->LineTo(100,100);
这个绘图过程应该是非常简单、非常快了吧,但是拉动窗口变化时还是会看见闪烁。

其实从道理上讲,画图的过程越复杂越慢闪烁应该越少,因为绘图用的时间与用背景清除屏幕所花的时间的比例越大人对闪烁的感觉会越不明显。

比如:清除屏幕时间为1s绘图时间也是为1s,这样在10s 内的连续重画中就要闪烁5次;如果清楚屏幕时间为1s不变,而绘图时间为9s,这样10s内的连续重画只会闪烁一次。

这个也可以试验,在OnDraw(CDC *pDC)中这样写:
for(int i=0;i<100000;i++)
{
pDC->MoveTo(0,i);
pDC->LineTo(1000,i);
}
呵呵,程序有点变态,但是能说明问题。

说到这里可能又有人要说了,为什么一个简单图形看起来没有复杂图形那么闪呢?这是因为复杂图形占的面积大,重画时造成的反差比较大,所以感觉上要闪得厉害一些,但是闪烁频率要低。

那为什么动画的重画频率高,而看起来却不闪?这里,我就要再次强调了,闪烁是什么?闪烁就是反差,反差越大,闪烁越厉害。

因为动画的连续两个帧之间的差异很小所以看起来不闪。

如果不信,可以在动画的每一帧中间加一张纯白的帧,不闪才怪呢。

2、如何避免闪烁
在知道图形显示闪烁的原因之后,对症下药就好办了。

首先当然是去掉MFC提供的背景绘制过程了。

实现的方法很多,
* 可以在窗口形成时给窗口的注册类的背景刷付NULL
* 也可以在形成以后修改背景
static CBrush brush(RGB(255,0,0));
SetClassLong(this->m_hWnd,GCL_HBRBACKGROUND,(LONG)(HBRUSH)brush);
* 要简单也可以重载OnEraseBkgnd(CDC* pDC)直接返回TRUE
这样背景没有了,结果图形显示的确不闪了,但是显示也象前面所说的一样,变得一团乱。

怎么办?这就要用到双缓存的方法了。

双缓冲就是除了在屏幕上有图形进行显示以外,在内存中也有图形在绘制。

我们可以把要显示的图形先在内存中绘制好,然后再一次性的将内存中的图形按照一个点一个点地覆盖到屏幕上去(这个过程非常快,因为是非常规整的内存拷贝)。

这样在内存中绘图时,随便用什么反差大的背景色进行清除都不会闪,因为看不见。

当贴到屏幕上时,因为内存中最终的图形与屏幕显示图形差别很小(如果没有运动,当然就没有差别),这样看起来就不会闪。

3、如何实现双缓冲
首先给出实现的程序,然后再解释,同样是在OnDraw(CDC *pDC)中:
CDC MemDC; //首先定义一个显示设备对象
CBitmap MemBitmap;//定义一个位图对象
//随后建立与屏幕显示兼容的内存显示设备
MemDC.CreateCompatibleDC(NULL);
//这时还不能绘图,因为没有地方画^_^
//下面建立一个与屏幕显示兼容的位图,至于位图的大小嘛,可以用窗口的大小
MemBitmap.CreateCompatibleBitmap(pDC,nWidth,nHeight);
//将位图选入到内存显示设备中
//只有选入了位图的内存显示设备才有地方绘图,画到指定的位图上
CBitmap *pOldBit=MemDC.SelectObject(&MemBitmap);
//先用背景色将位图清除干净,这里我用的是白色作为背景
//你也可以用自己应该用的颜色
MemDC.FillSolidRect(0,0,nWidth,nHeight,RGB(255,255,255));
//绘图
MemDC.MoveTo(……);
MemDC.LineTo(……);
//将内存中的图拷贝到屏幕上进行显示
pDC->BitBlt(0,0,nWidth,nHeight,&MemDC,0,0,SRCCOPY);
//绘图完成后的清理
MemBitmap.DeleteObject();
MemDC.DeleteDC();
上面的注释应该很详尽了,废话就不多说了。

4、如何提高绘图的效率
我主要做的是电力系统的网络图形的CAD软件,在一个窗口中往往要显示成千上万个电力元件,而每个元件又是由点、线、圆等基本图形构成。

如果真要在一次重绘过程重画这么多元件,可想而知这个过程是非常漫长的。

如果加上了图形的浏览功能,鼠标拖动图形滚动时需要进行大量的重绘,速度会慢得让用户将无法忍受。

怎么办?只有再研究研究MFC的绘图过程了。

实际上,在OnDraw(CDC *pDC)中绘制的图并不是所有都显示了的,例如:你在OnDraw中画了两个矩形,在一次重绘中虽然两个矩形的绘制函数都有执行,但是很有可能只有一个显示了,这是因为MFC本身为了提高重绘的效率设置了裁剪区。

裁剪区的作用就是:只有在这个区内的绘图过程才会真正有效,在区外的是无效的,即使在区外执行了绘图函数也是不会显示的。

因为多数情况下窗口重绘的产生大多是因为窗口部分被遮挡或者窗口有滚动发生,改变的区域并不是整个图形而只有一小部分,这一部分需要改变的就是pDC中的裁剪区了。

因为显示(往内存或者显存都叫显示)比绘图过程的计算要费时得多,有了裁剪区后显示的就只是应该显示的部分,大大提高了显示效率。

但是这个裁剪区是MFC设置的,它已经为我们提高了显示效率,在进行复杂图形的绘制时如何进一步提高效率呢?那就只有去掉在裁剪区外的绘图过程了。

可以先用pDC->GetClipBox()得到裁剪区,然后在绘图时判断你的图形是否在这个区内,如果在就画,不在就不画。

如果你的绘图过程不复杂,这样做可能对你的绘图效率不会有提高。

在图形图象处理编程过程中,双缓冲是一种基本的技术。

我们知道,如果窗体在响应WM_PAINT 消息的时候要进行复杂的图形处理,那么窗体在重绘时由于过频的刷新而引起闪烁现象。

解决这一问题的有效方法就是双缓冲技术。

因为窗体在刷新时,总要有一个擦除原来图象的过程OnEraseBkgnd,它利用背景色填充窗体绘图区,然后在调用新的绘图代码进行重绘,这样一擦一写造成了图象颜色的反差。

当WM_PAINT 的响应很频繁的时候,这种反差也就越发明显。

于是我们就看到了闪烁现象。

我们会很自然的想到,避免背景色的填充是最直接的办法。

但是那样的话,窗体上会变的一团糟。

因为每次绘制图象的时候都没有将原来的图象清除,造成了图象的残留,于是窗体重绘时,画面往往会变的乱七八糟。

所以单纯的禁止背景重绘是不够的。

我们还要进行重新绘图,但要求速度很快,于是我们想到了使用BitBlt函数。

它可以支持图形块的复制,速度很快。

我们可以先在内存中作图,然后用此函数将做好的图复制到前台,同时禁止背景刷新,这样就消除了闪烁。

以上也就是双缓冲绘图的基本的思路。

一、普通方法:
先按普通做图的方法进行编程。

即在视类的OnDraw函数中添加绘图代码。

在此我们绘制若干同心圆,代码如下:CBCDoc* pDoc = GetDocument();
ASSERT_V ALID(pDoc);
CPoint ptCenter;
CRect rect,ellipseRect;
GetClientRect(&rect);
ptCenter = rect.CenterPoint();
for(int i=20;i>0;i--)
{
ellipseRect.SetRect(ptCenter,ptCenter);
ellipseRect.InflateRect(i*10,i*10);
pDC->Ellipse(ellipseRect);
}
编译运行程序,尝试改变窗口大小,可以发现闪烁现象。

二、双缓冲方法:
在双缓冲方法中,首先要做的是屏蔽背景刷新。

背景刷新其实是在响应WM_ERASEBKGND 消息。

我们在视类中添加对这个消息的响应,可以看到缺省的代码如下:BOOL CMYView::OnEraseBkgnd(CDC* pDC)
{
return CView::OnEraseBkgnd(pDC);
}
是调用父类的OnEraseBkgnd函数,我们屏蔽此调用,只须直接return TRUE;即可。

下面是内存缓冲作图的步骤。

CPoint ptCenter;
CRect rect,ellipseRect;
GetClientRect(&rect);
ptCenter = rect.CenterPoint();
CDC dcMem; //用于缓冲作图的内存DC
CBitmap bmp; //内存中承载临时图象的位图
dcMem.CreateCompatibleDC(pDC); //依附窗口DC创建兼容内存DC
bmp.CreateCompatibleBitmap(pDC,rect.Width(),rect.Height());//创建兼容位图
dcMem.SelectObject(&bmp); //将位图选择进内存DC
//按原来背景填充客户区,不然会是黑色
dcMem.FillSolidRect(rect,pDC->GetBkColor());
for(int i=20;i>0;i--) //在内存DC上做同样的同心圆图象
{
ellipseRect.SetRect(ptCenter,ptCenter);
ellipseRect.InflateRect(i*10,i*10);
dcMem.Ellipse(ellipseRect);
}
pDC->BitBlt(0,0,rect.Width(),rect.Height(),
&dcMem,0,0,SRCCOPY);//将内存DC上的图象拷贝到前台
dcMem.DeleteDC(); //删除DC
bm.DeleteObject(); //删除位图
由于复杂的画图操作转入后台,我们看到的是速度很快的复制操作,自然也就消除了闪烁现象。

注意:bmp.CreateCompatibleBitmap(pDC,rect.Width(),rect.Height());
这里面CreateCompatibleBitmap第一个参数不能用dcMem,这样的话创建的是黑白位图。

如果你要创建彩色位图,需要用pDC,它用来创建了内存DC. 详细请见下面的MSDN:When a memory device context is created, it initially has a 1-by-1 monochrome bitmap selected into it. If this memory device context is used in CreateCompatibleBitmap, the bitmap that is created is a monochrome bitmap. To create a color bitmap, use the hDC that was used to create the memory device context, as shown in the following code:
HDC memDC = CreateCompatibleDC ( hDC );
HBITMAP memBM = CreateCompatibleBitmap ( hDC, nWidth, nHeight );
SelectObject ( memDC, memBM );
在上篇VC 中用IPicture 在窗口中显示图片文件讲到的是VC 窗口(或控件)中显示本地图片文件,本文延伸这一话题,来演示如何显示网上的图片,即提供图片的HTTP URL 地址,把它显示到VC 的界面中来。

本文参考我原来写的一篇日志VC中使用CInternetSession抓取网页内容来修改上篇代码中用来显示图片的函数:HRESULT ShowPic(char *lpstrFile,HWND hWnd)。

把研究过的东西整理记下来总是好处多多,瞧,至少现在展开其他话题,可参考时便能信手拈来。

重新实现的函数HRESULT ShowPic(char *lpstrFile,HWND hWnd) 代码如下:
// 显示图片, lpstrImgUrl 为图片URL地址,hWnd 为窗口句柄
HRESULT Utils::ShowPic(char *lpstrImgUrl,HWND hWnd)
{
HDC hDC_Temp=GetDC(hWnd);
IPicture *pPic;
IStream *pStm;
BOOL bResult;
DWORD dwFileSize,dwByteRead;
//读取网页上图片文件,实际是个CHttpFile指针
CInternetSession session("HttpClient");
CFile* httpFile = (CFile*)session.OpenURL(lpstrImgUrl);
if (httpFile!=INVALID_HANDLE_V ALUE)
{
dwFileSize=httpFile->GetLength();//获取文件字节数
if (dwFileSize==0xFFFFFFFF)
return E_FAIL;
}
else
{
return E_FAIL;
}
//分配全局存储空间
HGLOBAL hGlobal = GlobalAlloc(GMEM_MOVEABLE, dwFileSize);
LPVOID pvData = NULL;
if (hGlobal == NULL)
return E_FAIL;
if ((pvData = GlobalLock(hGlobal)) == NULL)//锁定分配内存块
return E_FAIL;
//把文件读入内存缓冲区
dwByteRead = httpFile->Read(pvData,dwFileSize);
GlobalUnlock(hGlobal);
CreateStreamOnHGlobal(hGlobal, TRUE, &pStm);
//装入图形文件
bResult=OleLoadPicture(pStm,dwFileSize,TRUE,IID_IPicture,(LPVOID*)&pPic);
if(FAILED(bResult))
return E_FAIL;
OLE_XSIZE_HIMETRIC hmWidth; //图片的真实宽度, 单位为英寸
OLE_YSIZE_HIMETRIC hmHeight; //图片的真实高度, 单位为英寸
pPic->get_Width(&hmWidth);
pPic->get_Height(&hmHeight);
//转换hmWidth和hmHeight为pixels距离,1英寸=25.4毫米
int nWidth = MulDiv(hmWidth,GetDeviceCaps(hDC_Temp,LOGPIXELSX),2540); int nHeight = MulDiv(hmHeight,GetDeviceCaps(hDC_Temp,LOGPIXELSY),2540);
//将图形输出到屏幕上(有点像BitBlt)
bResult=pPic->Render(hDC_Temp,0,0,nWidth,nHeight,
0,hmHeight,hmWidth,-hmHeight,NULL);
pPic->Release();
httpFile->Close();//关闭打开的文件
if (SUCCEEDED(bResult))
{
return S_OK;
}
else
{
return E_FAIL;
}
}
// 显示图片, lpstrImgUrl 为图片URL地址,hWnd 为窗口句柄HRESULT Utils::ShowPic(char *lpstrImgUrl,HWND hWnd)
{
HDC hDC_Temp=GetDC(hWnd);
IPicture *pPic;
IStream *pStm;
BOOL bResult;
DWORD dwFileSize,dwByteRead;
//读取网页上图片文件,实际是个CHttpFile指针
CInternetSession session("HttpClient");
CFile* httpFile = (CFile*)session.OpenURL(lpstrImgUrl);
if (httpFile!=INVALID_HANDLE_V ALUE)
{
dwFileSize=httpFile->GetLength();//获取文件字节数
if (dwFileSize==0xFFFFFFFF)
return E_FAIL;
}
else
{
return E_FAIL;
}
//分配全局存储空间
HGLOBAL hGlobal = GlobalAlloc(GMEM_MOVEABLE, dwFileSize);
LPVOID pvData = NULL;
if (hGlobal == NULL)
return E_FAIL;
if ((pvData = GlobalLock(hGlobal)) == NULL)//锁定分配内存块return E_FAIL;
//把文件读入内存缓冲区
dwByteRead = httpFile->Read(pvData,dwFileSize);
GlobalUnlock(hGlobal);
CreateStreamOnHGlobal(hGlobal, TRUE, &pStm);
//装入图形文件
bResult=OleLoadPicture(pStm,dwFileSize,TRUE,IID_IPicture,(LPVOID*)&pPic);
if(FAILED(bResult))
return E_FAIL;
OLE_XSIZE_HIMETRIC hmWidth; //图片的真实宽度, 单位为英寸
OLE_YSIZE_HIMETRIC hmHeight; //图片的真实高度, 单位为英寸
pPic->get_Width(&hmWidth);
pPic->get_Height(&hmHeight);
//转换hmWidth和hmHeight为pixels距离,1英寸=25.4毫米
int nWidth = MulDiv(hmWidth,GetDeviceCaps(hDC_Temp,LOGPIXELSX),2540);
int nHeight = MulDiv(hmHeight,GetDeviceCaps(hDC_Temp,LOGPIXELSY),2540);
//将图形输出到屏幕上(有点像BitBlt)
bResult=pPic->Render(hDC_Temp,0,0,nWidth,nHeight,
0,hmHeight,hmWidth,-hmHeight,NULL);
pPic->Release();
httpFile->Close();//关闭打开的文件
if (SUCCEEDED(bResult))
{
return S_OK;
}
else
{
return E_FAIL;
}
}
5 坐标系
一设备坐标和逻辑坐标
设备坐标(Device Coordinate)又称为物理坐标(Physical Coordinate),是指输出设备上的坐标。

通常将屏幕上的设备坐标称为屏幕坐标。

设备坐标用对象距离窗口左上角的水平距离和垂直距离来指定对象的位置,是以像素为单位来表示的,设备坐标的X轴向右为正,Y轴向下为正,坐标原点位于窗口的左上角。

逻辑坐标(Logical Coordinate)是系统用作记录的坐标。

在缺省的模式(MM_TEXT)下,逻辑坐标的方向和单位与设备坐标的方向和单位相同,也是以像素为单位来表示的,X轴向右为正,Y轴向下为正,坐标原点位于窗口的左上角。

逻辑坐标和设备坐标即使在缺省模式下其数值也未必一致,除了在以下两种情况下:
1. 窗口为非滚动窗口
2. 窗口为滚动窗口,但垂直滚动条位于滚动边框的最上端,水平滚动条位于最左端,但如果移动了滚动条这两种坐标就不一致了。

在VC中鼠标坐标的坐标位置用设备坐标表示,但所有GDI绘图都用逻
坐标表示,所以用鼠标绘图时,那么必须将设备坐标转换为逻辑坐标,可以使用CDC 函数DptoLP()将设备坐标转化为逻辑坐标,同样可以用LptoDP()将逻辑坐标转化为设备坐标。

二坐标模式
为了在不同的领域使用逻辑坐标,Windows提供了以下8种坐标模式:
分别为MM_TEXT、MM_HIENGLISH、MM_LOENGLISH、MM_HIMETRIC、MM_LOMETRIC、MM_TWIPS、MM_ANISOTROPIC和MM_ISOTROPIC。

三实例解析
(一)建立以左上角为原点,X轴和Y轴为1000的坐标,如下图
我们可以用以下代码:
void CTtView::OnDraw(CDC* pDC)
{
CTtDoc* pDoc = GetDocument();
ASSERT_V ALID(pDoc);
CRect rect;
GetClientRect(&rect);
pDC->SetMapMode(MM_ANISOTROPIC);
pDC->SetViewportOrg(0,0);
pDC->SetViewportExt(rect.right,rect.bottom);
pDC->SetWindowOrg(0,0);
pDC->SetWindowExt(1000,1000);
pDC->MoveTo(50,50);
pDC->LineTo(50,950);
pDC->LineTo(950,950);
pDC->LineTo(50,50);
}
代码分析:
1. GetClientRect(&rect); 取得客户区矩形区域,将其存放在rect中
2. 用pDC->SetMapMode(MM_ANISOTROPIC); 设置映射模式
3. 通过pDC->SetViewportOrg(0,0);设置逻辑坐标的原点
4. 通过pDC->SetViewportExt(rect.right,rect.bottom);和
pDC->SetWindowExt(1000,1000);来确定逻辑坐标下和设备坐标下的尺寸对应关系
5. 在MM_ANISOTROPIC模式下,X轴单位和Y轴单位可以不相同
6. 坐标方向的确定方法是如果逻辑窗范围和视口范围符号相同,则逻辑坐标的方向和视口的方向相同,即X轴向右为正,Y轴向下为正。

7. 如果将显示模式改为MM_ISOTROPIC,那么X轴单位和Y轴单位一定相同,感兴趣的读者可以自己使一下。

(二)建立以视窗中心为原点的坐标,如下图
用如下代码:
void CTtView::OnDraw(CDC* pDC)
{
CTtDoc* pDoc = GetDocument();
ASSERT_V ALID(pDoc);
CRect rect;
GetClientRect(&rect);
pDC->SetMapMode(MM_ANISOTROPIC);
pDC->SetViewportOrg(rect.right/2,rect.bottom/2);
pDC->SetViewportExt(rect.right,rect.bottom);
pDC->SetWindowOrg(0,0);
pDC->SetWindowExt(1000,-1000);
pDC->MoveTo(150,150);
pDC->LineTo(-150,-200);
pDC->LineTo(150,-150);
pDC->LineTo(150,150);
}
代码分析:
1. 用pDC->SetViewportOrg(rect.right/2,rect.bottom/2); 设置视口的原点。

2. 用pDC->SetViewportExt(rect.right,rect.bottom);和pDC->SetWindowExt(1000,-1000);来确定设备坐标和逻辑坐标的单位对应关系。

3. 因为逻辑窗范围和视口范围的符号不一致,纵坐标取反,所以Y轴向上为正。

楼上回答的仍然是Win32API实现的双缓冲,我来给个用MFC实现的。

例子可能特殊了一点,不过应该能看明白。

双缓冲当然需要一个缓冲C,如果你要画的东西总需要固定的背景(比如图片)的话,可能还需要一个背景DC:
在窗口类的声明中,即头文件中:
CDC m_dcMem,m_dcBack; //缓冲DC和背景DC
在窗口类的实现,即CPP文件中:
在OnInitDialog或者OnCreate里初始化两个DC,以下的m_nWidth和m_nHeight之类的变量肯定要换成你自己的:
m_dcMem.CreateCompatibleDC(GetDC());
m_dcBack.CreateCompatibleDC(GetDC());
CBitmap tmp,tmpp;
tmp.CreateCompatibleBitmap(GetDC(), m_nWidth, m_nHeight);
tmpp.CreateCompatibleBitmap(GetDC(), m_nWidth, m_nHeight);
m_dcBack.SelectObject(&tmp);
m_dcMem.SelectObject(&tmpp);
tmp.DeleteObject();
tmpp.DeleteObject();
然后每次先把背景DC画到缓冲DC上:
m_dcMem.BitBlt(0, 0, m_nWidth, m_nHeight, &m_dcBack, 0, 0, SRCCOPY);。

相关文档
最新文档