[转贴] Windows编程和面向对象技术 chap11

第十一讲 多媒体编程

 

  随着多媒体技术的迅猛发展和PC性能的大幅度提高,在PC机上运行的应用程序越来越多地采用了多媒体技术.如果你编写的应用程序能够发出美妙的声音,播放有趣的动画,无疑将会给人留下深刻的映象.

  Windows 95提供了对多媒体编程的良好支持,本章将帮助读者迅速掌握一些实用的多媒体编程技术,主要的内容包括:

调色板

位图

依赖于设备的位图(DDB)

与设备无关的位图(DIB)

动画控件

媒体控制接口(MCI).

小结 

 

11.1调色板

 

11.1.1 调色板的原理

  PC机上显示的图象是由一个个像素组成的,每个像素都有自己的颜色属性。在PC的显示系统中,像素的颜色是基于RGB模型的,每一个像素的颜色由红(B)、绿(G)、蓝(B)三原色组合而成。每种原色用8位表示,这样一个的颜色就是24位的。以此推算,PC的SVGA适配器可以同时显示224约一千六百多万种颜色。24位的颜色通常被称作真彩色,用真彩色显示的图象可达到十分逼真的效果。

  但是,真彩色的显示需要大量的视频内存,一幅640×480的真彩色图象需要约1MB的视频内存。由于数据量大增,显示真彩色会使系统的整体性能迅速下降。为了解决这个问题,计算机使用调色板来限制颜色的数目。调色板实际上是一个有256个表项的RGB颜色表,颜色表的每项是一个24位的RGB颜色值。使用调色板时,在视频内存中存储的不是的24位颜色值,而是调色板的4位或8位的索引。这样一来,显示器可同时显示的颜色被限制在256色以内,对系统资源的耗费大大降低了。

  显示器可以被设置成16、256、64K、真彩色等显示模式,前两种模式需要调色板。在16或256色模式下,程序必须将想要显示的颜色正确地设置到调色板中,这样才能显示出预期的颜色。图11.1显示了调色板的工作原理。使用调色板的一个好处是不必改变视频内存中的值,只需改变调色板的颜色项就可快速地改变一幅图象的颜色或灰度。

  在DOS中,调色板的使用不会有什么问题。由于DOS是一个单任务操作系统,一次只能运行一个程序,因此程序可以独占调色板。在Windows环境下,情况就不那么简单了。Windows是一个多任务操作系统,可以同时运行多个程序。如果有几个程序都要设置调色板,就有可能产生冲突。为了避免这种冲突,Windows使用逻辑调色板来作为使用颜色的应用程序和系统调色板(物理调色板)之间的缓冲。

图11.1 调色板工作原理

 

  在Windows中,应用程序是通过一个或多个逻辑调色板来使用系统调色板(物理调色板)。在256色系统调色板中,Windows保留了20种颜色作为静态颜色,这些颜色用作显示Windows界面,应用程序一般不能改变。缺省的系统调色板只包含这20种静态颜色,调色板的其它项为空。应用程序要想使用新的颜色,必须将包含有所需颜色的逻辑调色板实现到系统调色板中。在实现过程中,Windows首先将逻辑调色板中的项与系统调色板中的项作完全匹配,对于逻辑调色板中不能完全匹配的项,Windows将其加入到系统调色板的空白项中,系统调色板总共有236个空白项可供使用,若系统调色板已满,则Windows将逻辑调色板的剩余项匹配到系统调色板中尽可能接近的颜色上。

  每个设备上下文都拥有一个逻辑调色板,缺省的逻辑调色板只有20种保留颜色,如果要使用新的颜色,则应该创建一个新的逻辑调色板并将其选入到设备上下文中。但光这样还不能使用新颜色,程序只有把设备上下文中的逻辑调色板实现到系统调色板中,新的颜色才能实现。在逻辑调色板被实现到系统调色板时,Windows会建立一个调色板映射表。当设备上下文用逻辑调色板中的颜色绘图时,GDI绘图函数会查询调色板映射表以把像素值从逻辑调色板的索引转换成系统调色板的索引,这样当像素被输出到视频内存中时就具有了正确的颜色值。图11.2说明了这种映射关系,从图中读者可以体会到逻辑调色板的缓冲作用。在该图中,GDI绘图函数使用逻辑调色板的索引1中的颜色来绘图,通过查询调色板映射表,得知系统调色板中的第23号索引与其完全匹配,这样实际输出到视频内存中的像素值是23。注意图中还演示了颜色的不完全匹配,即逻辑调色板中的索引15和系统调色板中的索引46。

  每个要使用额外颜色的窗口都会实现自己的逻辑调色板,逻辑调色板中的每种颜色在系统调色板中都有相同或相近的匹配。调色板的实现优先权越高,匹配的精度也就越高。Windows规定,活动窗口的逻辑调色板(如果有的话)具有最高的实现优先权。这是因为活动窗口是当前与用户交互的窗口,应该保证其有最佳的颜色显示。非活动窗口的优先权是按Z顺序自上到下确定的(Z顺序就是重叠窗口的重叠顺序)。活动窗口有权将其逻辑调色板作为前景调色板实现,非活动窗口则只能实现背景调色板。

提示:术语活动窗口(Active window)或前台窗口(Foreground window)是指当前与用户交互的窗口,活动窗口的顶端的标题条呈高亮显示,而非活动窗口的标题条则是灰色的。活动窗口肯定是一个顶层窗口(Top-level window),顶层窗口是指没有父窗口或父窗口是桌面窗口的窗口,这种窗口一般都有标题和边框,主要包括框架窗口和对话框。术语重叠窗口是指作为应用程序主窗口的窗口,我们可以把对话框看成是一种特殊的重叠式窗口。

 

图11.2 调色板的映射关系

 

11.1.2 调色板的创建和实现

MFC的CPalette类对逻辑调色板进行了封装。该类的成员函数CreatePalette负责创建逻辑调色板,该函数的声明为:

BOOL CreatePalette( LPLOGPALETTE lpLogPalette ); //成功则返回TRUE。

参数lpLogPalette是一个指向LPLOGPALETTE结构的指针,LPLOGPALETTE结构描述了逻辑调色板的内容,该结构的定义为:

typedef struct tagLOGPALETTE {

WORD palVersion; //Windows版本号,一般是0x300

WORD palNumEntries; //调色板中颜色表项的数目

PALETTEENTRY palPalEntry[1]; //每个表项的颜色和使用方法

} LOGPALETTE;

  结构中最重要的成员是PALETTEENTRY数组,数组项的数目由palNumEntries成员指定。PALETTEENTRY结构对调色板的某一个颜色表项进行了描述,该结构的定义为:

typedef struct tagPALETTEENTRY {

BYTE peRed; //红色的强度(0~255,下同)

BYTE peGreen; //绿色的强度

BYTE peBlue; //蓝色的强度

BYTE peFlags;

} PALETTEENTRY;

  成员peFlags说明了颜色表项的使用方法,在一般应用时为NULL,若读者对peFlags的详细说明感兴趣,可以查看Visual C++的联机帮助。

  可以看出,创建调色板的关键是在PALETTEENTRY数组中指定要使用的颜色。这些颜色可以是程序自己指定的特殊颜色,也可以从DIB位图中载入。逻辑调色板的大小可根据用户使用的颜色数来定,一般不能超过256个颜色表项。

  CreatePalette只是创建了逻辑调色板,此时调色板只是一张孤立的颜色表,还不能对系统产生影响。程序必需调用CDC::SelectPalette把逻辑调色板选入到要使用它的设备上下文中,然后调用CDC::RealizePalette把逻辑调色板实现到系统调色板中。函数的声明为:

CPalette* SelectPalette( CPalette* pPalette, BOOL bForceBackground );
该函数把指定的调色板选择到设备上下文中。参数pPalette指向一个CPalette对象。参数bForceBackground如果是TRUE,那么被选择的调色板总是作为背景调色板使用,如果bForceBackground是FALSE并且设备上下文是附属于某个窗口的,那么当窗口是活动窗口或活动窗口的子窗口时,被选择的调色板将作为前景调色板实现,否则作为背景调色板实现。如果使用调色板的是一个内存设备上下文,则该参数被忽略。函数返回设备上下文原来使用的调色板,若出错则返回NULL。

UINT RealizePalette( );
该函数把设备上下文中的逻辑调色板实现到系统调色板中。函数的返回值表明调色板映射表中有多少项被改变了。

 

  如果某一个窗口要显示特殊的颜色,那么一般应该在处理WM_PAINT消息时实现自己的逻辑调色板。也就是说,在OnPaint或OnDraw函数中重绘以前,要调用SelectPalette和RealizePalette。如果窗口显示的颜色比较重要,则在调用SelectPalette时应该指定bForceBackground参数为FALSE。

  前景调色板具有使用颜色的最高优先级,它有无条件占用系统调色板(20种保留颜色除外)的权力,也就是说,如果需要,前景调色板将覆盖系统调色板的236个表项,而不管这些表项是否正被别的窗口使用。背景调色板则无权破坏系统调色板中的已使用项。

  请读者注意,前景调色板应该是唯一。如果一个活动窗口同时要实现几个逻辑调色板,那么只能有一个调色板作为前景调色板实现,也即在调用CDC::SelectPalette时只能有一个bForceBackground被指定为FALSE,其它的bForceBackground必需为TRUE。通常是把具有输入焦点的窗口的调色板作为前景调色板实现,其它窗口只能使用背景调色板。如果活动窗口的子窗口全都使用前景调色板,则会导致程序的死循环。

提示:请读者注意区分活动窗口和有输入焦点的窗口。有输入焦点的窗口要么是活动窗口本身,要么是活动窗口的子窗口。也就是说,活动窗口不一定具有输入焦点,当活动窗口的子窗口获得输入焦点时,活动窗口就会失去输入焦点。

 

11.1.3 使用颜色的三种方法

  在调用GDI函数绘图时,可以用不同的方法来选择颜色。Windows用COLORREF数据类型来表示颜色,COLORREF型值的长度是4字节,其中最高位字节可以取三种不同的值,分别对应三种使用颜色的方法。表11.1列出了这些不同的取值及其含义。

 

表11.1 COLORREF型值的最高位字节的含义

取值

含义

0x00

指定RGB引用。此时三个低位字节含有红、绿、蓝色的强度,Windows将抖动20种保留的颜色来匹配指定的颜色,而不管程序是否实现了自己的调色板。

0x01

指定调色板索引引用。此时最低位字节含有逻辑调色板的索引,Windows根据该索引在逻辑调色板中找到所需的颜色。

0x02

指定调色板RGB引用。此时三个低位字节含有红、绿、蓝色的强度,Windows会在逻辑调色板中找到最匹配的颜色。

为了方便用户的使用,Windows提供了三个宏来构建三种不同的COLORREF数据,它们是:

COLORREF RGB(BYTE bRed,BYTE bGreen,BYTE bBlue); //RGB引用

COLORREF PALETTEINDEX(WORD wPaletteIndex); //调色板索引引用

COLORREF PALETTERGB(BYTE bRed,BYTE bGreen, //调色板RGB引用
BYTE bBlue);

例如,我们可以用上述三种方法来指定刷子的颜色。下面的代码用系统调色板中的红色建立一个刷子:

CBrush brush;

brush.CreateSolidBrush(RGB(255,0,0));

pDC->SelectObject(&brush);

下面的代码用逻辑调色板的索引2中的颜色来创建一个刷子:

pDC->SelectPalette(&m_Palette,FALSE);

pDC->RealizePalette( );

CBrush brush;

brush.CreateSolidBrush(PALETTEINDEX(2));

pDC->SelectObject(&brush);

下面的代码用逻辑调色板中最匹配的深灰色来创建一个刷子:

pDC->SelectPalette(&m_Palette,FALSE);

pDC->RealizePalette( );

CBrush brush;

brush.CreateSolidBrush(PALETTERGB(20,20,20));

pDC->SelectObject(&brush);

11.1.4 与系统调色板有关的消息

  为了协调各个窗口对系统调色板的使用,Windows在必要的时侯会向顶层窗口和重叠窗口发送消息WM_QUERYNEWPALETTE和WM_PALETTECHANGED。

  当某一顶层或重叠窗口(如主框架窗口)被激活时,会收到WM_QUERYNEWPALETTE消息,在窗口创建之初也会收到该消息,该消息先于WM_PAINT消息到达窗口。如果活动窗口要使用特殊的颜色,则在收到该消息时应该实现自己的逻辑调色板并重绘窗口。如果窗口实现了逻辑调色板,那么WM_QUERYNEWPALETTE消息的处理函数应返回TRUE。通常窗口在收到该消息后应该为有输入焦点的窗口(如视图)实现前景调色板,但如果程序觉得它显示的颜色并不重要,那么在收到该消息后可以把逻辑调色板作为背景调色板实现(指定CDC::SelectPalette函数的bForceBackground参数为TRUE),这样程序就失去了使用系统调色板的最高优先权。

  当活动窗口实现其前景调色板并改变了系统调色板时,Windows会向包括活动窗口在内的所有的顶层窗口和重叠窗口发送WM_PALETTECHANGED消息,在该消息的wParam参数中包含了改变系统调色板的窗口的句柄。其它窗口如果使用了自己的逻辑调色板,那么应该重新实现其逻辑调色板,并重绘窗口。这是因为系统调色板已经被改变了,必需重新建立调色板映射表并重绘,否则可能会显示错误的颜色。当然,非活动窗口只能使用背景调色板,所以显示的颜色肯定没有在前台的时侯好。要注意只有在活动窗口实现了前景调色板且改变了系统调色板时,才会产生WM_PALETTECHANGED消息。也就是说,如果窗口在调用CDC::SelectPalette时指定bForceBackground参数为TRUE,那么是不会产生WM_PALETTECHANGED消息。

  总之,WM_QUERYNEWPALETTE消息为活动窗口提供了实现前景调色板的机会,而WM_PALETTECHANGED消息为窗口提供了适应系统调色板变化的机会。

  需要指出的是,子窗口是收不到与调色板有关的消息的。因此,如果子窗口(如视图)要使用自己的逻辑调色板,那么顶层窗口或重叠窗口应该及时通知子窗口与调色板有关的消息。

11.1.5 具体实例

  现在让我们来看一个使用调色板的演示程序。该程序名为TestPal,如图11.3所示,该程序显示了两组红色方块,每组方块都是16×16共256个。左边的这组方块是用逻辑调色板画的,红色的强度从0到255递增,作为对比,在右边用RGB引用画出了256个递增的红色方块。读者可以对比这两组方块的颜色质量,以体会调色板索引引用和RGB引用的区别。该程序也着重向读者演示了处理调色板消息的方法。

图11.3 TestPal程序

 

  首先,请读者用AppWizard建立一个名为TestPal的MFC单文挡应用程序。然后,用ClassWizard为CMainFrame类加入WM_QUERYNEWPALETTE和WM_PALETTECHANGED消息的处理函数,使用缺省的函数名。接着,在TestPal.h文件中类CTestPalApp的定义前加入下面一行:

#define WM_DOREALIZE WM_USER+200

当收到调色板消息时,主框架窗口会发送用户定义的WM_DOREALIZE消息通知视图。

最后,请读者按清单11.1和11.2修改程序。

 

清单11.1 CMainFrame类的部分代码

BOOL CMainFrame::OnQueryNewPalette()

{

// TODO: Add your message handler code here and/or call default

 

GetActiveView()->SendMessage(WM_DOREALIZE);

return TRUE; //返回TRUE表明实现了逻辑调色板

}

 

void CMainFrame::OnPaletteChanged(CWnd* pFocusWnd)

{

CFrameWnd::OnPaletteChanged(pFocusWnd);

// TODO: Add your message handler code here

 

if(GetActiveView()!=pFocusWnd)

GetActiveView()->SendMessage(WM_DOREALIZE);

}

 

清单11.2 CTestPalView类的部分代码

// TestPalView.h : interface of the CTestPalView class

class CTestPalView : public CView

{

. . .

protected:

 

CPalette m_Palette;

. . .

afx_msg LRESULT OnDoRealize(WPARAM wParam, LPARAM lParam);

DECLARE_MESSAGE_MAP()

};

 

// TestPalView.cpp : implementation of the CTestPalView class

 

BEGIN_MESSAGE_MAP(CTestPalView, CView)

 

. . .

ON_MESSAGE(WM_DOREALIZE, OnDoRealize)

END_MESSAGE_MAP()

 

CTestPalView::CTestPalView()

{

// TODO: add construction code here

 

LPLOGPALETTE pLogPal;

pLogPal=(LPLOGPALETTE)malloc(sizeof(LOGPALETTE)+

sizeof(PALETTEENTRY)*256);

pLogPal->palVersion=0x300;

pLogPal->palNumEntries=256;

for(int i=0;i<256;i++)

{

pLogPal->palPalEntry[i].peRed=i; //初始化为红色

pLogPal->palPalEntry[i].peGreen=0;

pLogPal->palPalEntry[i].peBlue=0;

pLogPal->palPalEntry[i].peFlags=0;

}

if(!m_Palette.CreatePalette(pLogPal))

AfxMessageBox("Can't create palette!");

}

 

void CTestPalView::OnDraw(CDC* pDC)

{

CTestPalDoc* pDoc = GetDocument();

ASSERT_VALID(pDoc);

 

// TODO: add draw code for native data here

 

CBrush brush,*pOldBrush;

int x,y,i;

 

pDC->SelectPalette(&m_Palette,FALSE);

pDC->RealizePalette();

pDC->SelectStockObject(BLACK_PEN);

for(i=0;i<256;i++)

{

x=(i%16)*16;

y=(i/16)*16;

brush.CreateSolidBrush(PALETTEINDEX(i)); //调色板索引引用

pOldBrush=pDC->SelectObject(&brush);

pDC->Rectangle(x,y,x+16,y+16);

pDC->SelectObject(pOldBrush);

brush.DeleteObject();

}

for(i=0;i<256;i++)

{

x=(i%16)*16+300;

y=(i/16)*16;

brush.CreateSolidBrush(RGB(i,0,0)); //RGB引用

pOldBrush=pDC->SelectObject(&brush);

pDC->Rectangle(x,y,x+16,y+16);

pDC->SelectObject(pOldBrush);

brush.DeleteObject();

}

}

 

 

LRESULT CTestPalView::OnDoRealize(WPARAM wParam, LPARAM)

{

CClientDC dc(this);

dc.SelectPalette(&m_Palette,FALSE);

if(dc.RealizePalette()) //若调色板映射被改变则刷新视图

GetDocument()->UpdateAllViews(NULL);

return 0L;

}

  在CTestPalView的构造函数中创建了一个含有256个递增红色的逻辑调色板。

  当变为活动窗口以及窗口创建时,TestPal程序的主框架窗口都会收到WM_QUERYNEWPALETTE消息,该消息的处理函数OnQueryNewPalette负责发送WM_DOREALIZE消息通知视图, 并返回TRUE以表明活动窗口实现了逻辑调色板。WM_DOREALIZE消息的处理函数CTestPalView::OnDoRealize为视图实现一个前景调色板,该函数中有一个判断语句可提高程序运行的效率:如果CDC::RealizePalette返回值大于零,则说明调色板映射表发生了变化,此时必须刷新视图,否则制图中的颜色将失真。如果RealizePalette返回零则说明调色板映射没有变化,这时就没有必要刷新视图。

  无论是TestPal还是别的应用程序在实现前景调色板并改变了系统调色板时,TestPal程序的主框架窗口都会收到WM_PALETTECHANGED消息。请注意该消息的处理函数CMainFrame::OnPaletteChanged有一个pFocusWnd参数,该参数表明是哪一个窗口改变了系统调色板。函数用pFocusWnd来判断,如果是别的应用程序实现了前景调色板,则通知视图调用OnDoRealize实现其逻辑调色板,注意虽然CDC::SelectPalette的bForceBackground参数是FALSE,但这时视图的逻辑调色板是作为背景调色板实现的。如果是TestPal自己的视图实现了前景调色板,则没有必要调用OnDoRealize。

  请读者将Windows当前的显示模式设置为256色,然后编译并运行TestPal,对比一下RGB引用与调色板索引引用的效果,读者不难发现左边用调色板索引引用输出的颜色比右边好的多。通过该程序我们可以看出,即使在系统调色板中已实现了丰富的红色的情况下,RGB引用得到的红色仍然是20种保留颜色的抖动色。

  读者可以打开Windows的画笔程序,并在该程序中打开一幅256色的位图(如Windows目录下的Forest.bmp)。在画笔和TestPal程序之间来回切换,读者可以看到,由于两个应用程序都正确的处理了调色板消息,在前台的应用程序总是具有最好的颜色显示,而后台程序的颜色虽然有些失真,但还比较令人满意。

  需要指出的是,TestPal程序只使用了一个逻辑调色板,所以它处理调色板消息的方法比较简单。如果程序要用到多个逻辑调色板,那么就需要采取一些新措施来保证只有一个逻辑调色板作为前景调色板使用。在11.4节读者可以看到使用多个逻辑调色板时的处理方法。

11.2 位图

  Windows用位图(Bitmap)来显示和保存图像,从单色的到24位真彩色图像都可以存储到位图中。

  位图实际上是一个像素值阵列,像素阵列存储在一个字节数组中,每一个像素的位数可以是1、4、8或24。单色位图的字节数组中的每一位代表一个像素,16色位图的字节数组中每一个字节存储两个像素,256色的位图每一个字节存储一个像素,而真彩色位图中每个像素用三个字节来表示。在256色以下的位图中存储的像素值实际上是调色板索引,在真彩色位图中存储的则是像素的RGB颜色值。

  位图分为依赖于设备的位图(DDB)和与设备无关的位图(DIB),二者有不同的用途。 

11.3 依赖于设备的位图(DDB)

DDB(Device-dependent bitmap)依赖于具体设备,这主要体现在以下两个方面:

  • DDB的颜色模式必需与输出设备相一致。例如,如果当前的显示设备是256色模式,那么DDB必然也是256色的,即一个像素用一个字节表示。

  • 在256色以下的位图中存储的像素值是系统调色板的索引,其颜色依赖于系统调色板。

 

由于DDB高度依赖输出设备,所以DDB只能存在于内存中,它要么在视频内存中,要么在系统内存中。

 

11.3.1 DDB的创建

MFC的CBitmap类封装了DDB。该类提供了几个函数用来创建DDB:

BOOL LoadBitmap( LPCTSTR lpszResourceName );
BOOL LoadBitmap( UINT nIDResource );
该函数从资源中载入一幅位图,若载入成功则返回TRUE。资源位图实际上是一个DIB,该函数在载入时把它转换成了DDB。

BOOL CreateBitmap( int nWidth, int nHeight, UINT nPlanes, UINT nBitcount, const void* lpBits );
该函数用来创建一幅空白的DDB。参数nWidth和nHeight以像素为单位说明了位图的宽度和高度。nPlanes是DDB的色平面数,nBitcount是每个色平面的颜色位数。一般来说,nPlanes为1,而nBitcount代表DDB中每个像素值所占的位数,但在创建16色DDB时,nPlanes为4,而nBitcount为1。参数lpBits指向存储像素阵列的数组,该数组应该逐行存储位图的每个像素值。注意,数组中每行像素的数目必需是偶数个字节,如果是奇数,则应该用0补足。若创建成功函数返回TRUE。

BOOL CreateCompatibleBitmap( CDC* pDC, int nWidth, int nHeight );
该函数创建一个与指定设备上下文兼容的DDB。参数pDC指向一个设备上下文,nWidth和nHeight是DDB的尺寸。若创建成功函数返回TRUE。

 

可以调用CBitmap的成员函数GetBitmap来查询DDB的各种属性(如尺寸):

int GetBitmap( BITMAP* pBitMap );
该函数用来获得与DDB有关的信息,参数pBitMap指向一个BITMAP结构。BITMAP结构的定义为:

typedef struct tagBITMAP {

LONG bmType; //必需为0

LONG bmWidth; //位图的宽度(以像素为单位)

LONG bmHeight; //位图的高度(以像素为单位)

LONG bmWidthBytes; //每一扫描行所需的字节数,应是偶数

WORD bmPlanes; //色平面数

WORD bmBitsPixel; //色平面的颜色位数

LPVOID bmBits; //指向存储像素阵列的数组

} BITMAP;

 

11.3.2 DDB的用途

  DDB的主要用途是保存位图。要保存的位图可以来自资源位图,也可以是一个绘图的结果。

  前面说过,在256色以下的显示模式中,DDB中的像素值是系统调色板的索引。一般在系统调色板中除了保留的20种静态颜色外,其它表项都有可能被应用程序改变。如果DDB中有一些像素值是指向20种静态颜色以外的颜色,那么该位图的颜色将是不稳定的。因此,DDB不能用来长期存储色彩丰富的位图。如果位图使用的大部分颜色都是20种保留色,则该位图可以用CBitmap对象保存在内存中。例如,用CDC::LoadBitmap载入的资源位图一般都是颜色较简单的位图,对于那些颜色比较丰富的位图,只有使用下面将要介绍的DIB才能长期保存。

在窗口中显示DDB的方法有些特别,其过程分以下几步:

构建一个CDC对象,然后调用CDC::CreateCompatibleDC创建一个兼容的内存设备上下文。

调用CDC::SelectObject将DDB选入内存设备上下文中。

调用CDC::BitBlt或CDC::StretchBlt将DDB从内存设备上下文中输出到窗口的设备上下文中。

调用CDC::SelectObject把原来的DDB选入到内存设备上下文中并使新DDB脱离出来。

 

下面这段代码在视图中显示了一个DDB:

void CMyView::OnDraw( CDC* pDC)

{

. . .

CDC MemDC;

CBitmap *oldBmp;

BITMAP bmpInfo;

int bmWidth,bmHeight;

MemDC.CreateCompatibleDC(pDC);

oldBmp=MemDC.SelectObject(&m_Bitmap); //m_Bitmap是一个CBitmap对象

m_Bitmap.GetBitmap(&bmpInfo); //获取位图的尺寸

bmWidth=bmpInfo.bmWidth;

bmHeight=bmpInfo.bmHeight;

pDC->BitBlt(0,0,bmWidth,bmHeight,&MemDC,0,0,SRCCOPY);

MemDC.SelectObject(oldBmp); //使位图m_Bitmap脱离设备上下文

. . .

}

函数CDC::BitBlt的声明为:

BOOL BitBlt( int x, int y, int nWidth, int nHeight, CDC* pSrcDC, int xSrc, int ySrc, DWORD dwRop );

  该函数把源设备上下文中的位图复制到本身的设备上下文中,两个设备上下文可以是内存设备上下文,也可以是同一个设备上下文。参数x和y是目的矩形的逻辑坐标,参数nWidth和nHeight说明了目的矩形及源位图的宽和高。pSrcDC指向源设备上下文,xSrc和ySrc说明了源矩形相对于源位图左上角的偏移。参数dwRop指定了光栅操作(ROP)代码,一些常用的ROP代码如表11.2所示。

 

表11.2 常用的ROP代码

ROP码

含义

BLACKNESS

输出黑色

DSTINVERT

反转目的位图

MERGECOPY

用与操作把图案(Pattern)与源位图融合起来

MERGEPAINT

用或操作把反转的源位图与目的位图融合起来

NOTSRCCOPY

把源位图反转然后拷贝到目的地

NOTSRCERASE

用或操作融合源和目的位图,然后再反转

PATCOPY

把图案拷贝到目的位图中

PATINVERT

用异或操作把图案与目的位图相融合

PATPAINT

用或操作融合图案和反转的源位图,然后用或操作把结果与目的位图融合

SRCAND

用与操作融合源位图和目的位图

SRCCOPY

把源位图拷贝到目的位图

SRCERASE

先反转目的位图,再用与操作将其与源位图融合

SRCINVERT

用异或操作融合源位图和目的位图

SRCPAINT

用或操作融合源位图和目的位图

WHITENESS

输出白色

 

 

函数CDC::StretchBlt的声明为:

BOOL StretchBlt( int x, int y, int nWidth, int nHeight, CDC* pSrcDC, int xSrc, int ySrc, int nSrcWidth, int nSrcHeight, DWORD dwRop );

 

  该函数把位图从源矩形拷贝到目的矩形中,如果源和目的矩形尺寸不同,那么将缩放位图的功能以适应目的矩形的大小。函数的大部分参数与BitBlt的相同,但多了两个参数nSrcWidth和nSrcHeight用来指定源矩形的宽和高。

  DDB的一个重要用途是用作设备上下文的显示表面。每一个设备上下文都包含有一个DDB,该位图实际上是在显示设备的缓冲区中(如视频内存),我们可以把它看做设备上下文的显示表面,设备上下文用GDI函数绘图实际上就是修改它所包含的DDB(显示表面)的过程。

  普通的设备上下文都是在屏幕上绘图的,而使用内存设备上下文则可以在系统内存中绘制图形。内存设备上下文是一种特殊的设备上下文,它将系统内存用作显示表面。程序可以使用内存设备上下文预先在系统内存中绘制复杂的图形,然后再快速地将其复制到实际的设备上下文的显示表面上,而绘制图形的结果仍保存在内存设备上下文的DDB中。

提示:有人可能会想到用BitBlt函数把绘图结果从显示设备拷贝到内存设备上下文中,这种方法可以工作,但有时会出错。当源矩形被别的窗口遮住时,BitBlt会把别的窗口中的像素拷贝下来。

  内存设备上下文缺省的DDB是一个1×1的单色位图,如此小的显示表面显然是没有用的,因此程序一般要为内存设备对象选择一个合适大小的彩色DDB。

下面这段代码创建了一个内存设备上下文,并在其包含的DDB中画了一个灰色实心矩形,然后再把DDB输出到屏幕上。

void CMyView::OnDraw(CDC* pDC)

{

. . .

CDC MemDC;

CBitmap bm,*oldBmp;

MemDC.CreateCompatibleDC(pDC); //创建一个兼容的内存设备上下文

bm.CreateCompatibleBitmap(pDC,100,50); //创建一个兼容的DDB

oldBmp=MemDC.SelectObject(&bm);

MemDC.SelectStockObject(BLACK_PEN);

MemDC.SelectStockObject(GRAY_BRUSH);

MemDC.Rectangle(0,0,50,50); //在DDB中画一个矩形

pDC->BitBlt(0,0,100,50,&MemDC,0,0,SRCCOPY);

MemDC.SelectObject(oldBmp); //使位图bm对象脱离设备上下文

. . .

}

  在上面的代码中,绘图的结果保存在位图bm中,一旦调用MemDC.SelectObject(oldBmp)使位图bm脱离设备上下文,该位图就可以被其它对象使用。

11.4 与设备无关的位图(DIB)

 

DIB(Device-indepentent bitmap)的与设备无关性主要体现在以下两个方面:

  • DIB的颜色模式与设备无关。例如,一个256色的DIB即可以在真彩色显示模式下使用,也可以在16色模式下使用。

  • 256色以下(包括256色)的DIB拥有自己的颜色表,像素的颜色独立于系统调色板。

 

  由于DIB不依赖于具体设备,因此可以用来永久性地保存图象。DIB一般是以*.BMP文件的形式保存在磁盘中的,有时也会保存在*.DIB文件中。运行在不同输出设备下的应用程序可以通过DIB来交换图象。

DIB还可以用一种RLE算法来压缩图像数据,但一般来说DIB是不压缩的。

11.4.1 DIB的结构

  与Borland C++下的框架类库OWL不同,MFC未提供现成的类来封装DIB。尽管Microsoft列出了一些理由,但没有DIB类确实给MFC用户带来很多不便。用户要想使用DIB,首先应该了解DIB的结构。

  在内存中,一个完整的DIB由两部分组成:一个BITMAPINFO结构和一个存储像素阵列的数组。BITMAPINFO描述了位图的大小,颜色模式和调色板等各种属性,其定义为

typedef struct tagBITMAPINFO {

BITMAPINFOHEADER bmiHeader;

RGBQUAD bmiColors[1]; //颜色表

} BITMAPINFO;

RGBQUAD结构用来描述颜色,其定义为

typedef struct tagRGBQUAD {

BYTE rgbBlue; //蓝色的强度

BYTE rgbGreen; //绿色的强度

BYTE rgbRed; //红色的强度

BYTE rgbReserved; //保留字节,为0

} RGBQUAD;

注意,RGBQUAD结构中的颜色顺序是BGR,而不是平常的RGB。

BITMAPINFOHEADER结构包含了DIB的各种信息,其定义为

typedef struct tagBITMAPINFOHEADER{

DWORD biSize; //该结构的大小

LONG biWidth; //位图的宽度(以像素为单位)

LONG biHeight; //位图的高度(以像素为单位)

WORD biPlanes; //必须为1

WORD biBitCount //每个像素的位数(1、4、8、16、24或32)

DWORD biCompression; //压缩方式,一般为0或BI_RGB (未压缩)

DWORD biSizeImage; //以字节为单位的图象大小(仅用于压缩位图)

LONG biXPelsPerMeter; //以目标设备每米的像素数来说明位图的水平分辨率

LONG biYPelsPerMeter; //以目标设备每米的像素数来说明位图的垂直分辨率

DWORD biClrUsed; /*颜色表的颜色数,若为0则位图使用由biBitCount指定的最大颜色数*/

DWORD biClrImportant; //重要颜色的数目,若该值为0则所有颜色都重要

} BITMAPINFOHEADER;

  与DDB不同,DIB的字节数组是从图象的最下面一行开始的逐行向上存储的,也即等于把图象倒过来然后在逐行扫描。另外,字节数组中每个扫描行的字节数必需是4的倍数,如果不足要用0补齐。

DIB可以存储在*.BMP或*.DIB文件中。DIB文件是以BITMAPFILEHEADER结构开头的,该结构的定义为

typedef struct tagBITMAPFILEHEADER {

WORD bfType; //文件类型,必须为“BM”

DWORD bfSize; //文件的大小

WORD bfReserved1; //为0

WORD bfReserved2; //为0

DWORD bfOffBits; //存储的像素阵列相对于文件头的偏移量

} BITMAPFILEHEADER;

  紧随该结构的是一个BITMAPINFOHEADER结构,然后是RGBQUAD结构组成的颜色表(如果有的话),文件最后存储的是DIB的像素阵列。

  DIB的颜色信息储存在自己的颜色表中,程序一般要根据颜色表为DIB创建逻辑调色板。在输出一幅DIB之前,程序应该将其逻辑调色板选入到相关的设备上下文中并实现到系统调色板中,然后再调用相关的GDI函数(如::SetDIBitsToDevice或::StretchDIBits)输出DIB。在输出过程中,GDI函数会把DIB转换成DDB,这项工作主要包括以下两步:

将DIB的颜色格式转换成与输出设备相同的颜色格式。例如,在真彩色的显示模式下要显示一个256色的DIB,则应该将其转换成24位的颜色格式。

将DIB像素的逻辑颜色索引转换成系统调色板索引。

 

11.4.2 编写DIB类

  由于MFC未提供DIB类,用户在使用DIB时将面临繁重的Windows API编程任务。幸运的是,Visual C++提供了一个较高层次的API,简化了DIB的使用。这些API函数实际上是由MFC的DibLook例程提供的,它们位于DibLook目录下的dibapi.cpp、myfile.cpp和dibapi.h文件中,主要包括:

ReadDIBFile //把DIB文件读入内存

SaveDIB //把DIB保存到文件中

CreateDIBPalette //从DIB中创建一个逻辑调色板

PaintDIB //显示DIB

DIBWidth //返回DIB的宽度

DIBHeight //返回DIB的高度

 

如果读者对这些函数的内部细节感兴趣,那么可以研究一下dibapi.cpp和myfile.cpp文件,但要做好吃苦的准备。

  即使利用上述API,编写使用DIB的程序仍然不是很轻松。为了满足读者的要求,笔者编写了一个名为CDib的较简单的DIB类,该类是基于上述API的,它的主要成员函数包括:

BOOL Load(LPCTSTR lpszFileName);
该函数从文件中载入DIB,参数lpszFileName说明了文件名。若成功载入则函数返回TRUE,否则返回FALSE。

BOOL LoadFromResource(UINT nID);
该函数从资源中载入位图,参数nID是资源位图的ID。若成功载入则函数返回TRUE,否则返回FALSE。

CPalette* GetPalette()
返回DIB的逻辑调色板。

BOOL Draw(CDC *pDC, int x, int y, int cx=0, int cy=0);
该函数在指定的矩形区域内显示DIB,它具有缩放位图的功能。参数pDC指向用于绘图的设备上下文,参数x和y说明了目的矩形的左上角坐标,cx和cy说明了目的矩形的尺寸,cx和cy若有一个为0则该函数按DIB的实际大小绘制位图,cx和cy的缺省值是0。若成功则函数返回TRUE,否则返回FALSE。

int Width(); //以像素为单位返回DIB的宽度

int Height(); //以像素为单位返回DIB的高度

  CDib类的源代码在清单11.3和11.4列出,CDib类的定义位于CDib.h中,CDib类的成员函数代码位于CDib.cpp中。对于CDib类的代码这里就不作具体解释了,读者只要会用就行。

清单11.3 CDib.h

#if !defined MYDIB

#define MYDIB

 

#include "dibapi.h"

 

class CDib

{

public:

CDib();

~CDib();

protected:

HDIB m_hDIB;

CPalette* m_palDIB;

public:

BOOL Load(LPCTSTR lpszFileName);

BOOL LoadFromResource(UINT nID);

CPalette* GetPalette() const

{ return m_palDIB; }

BOOL Draw(CDC *pDC, int x, int y, int cx=0, int cy=0);

int Width();

int Height();

void DeleteDIB();

};

 

#endif

 

 

清单11.4 Cdib.cpp

#include

#include "CDib.h"

 

#ifdef _DEBUG

#undef THIS_FILE

static char BASED_CODE THIS_FILE[] = __FILE__;

#endif

 

CDib::CDib()

{

m_palDIB=NULL;

m_hDIB=NULL;

}

 

CDib::~CDib()

{

DeleteDIB();

}

 

void CDib::DeleteDIB()

{

if (m_hDIB != NULL)

::GlobalFree((HGLOBAL) m_hDIB);

if (m_palDIB != NULL)

delete m_palDIB;

}

 

//从文件中载入DIB

BOOL CDib::Load(LPCTSTR lpszFileName)

{

HDIB hDIB;

CFile file;

CFileException fe;

if (!file.Open(lpszFileName, CFile::modeRead|CFile::shareDenyWrite, &fe))

{

AfxMessageBox(fe.m_cause);

return FALSE;

}

TRY

{

hDIB = ::ReadDIBFile(file);

}

CATCH (CFileException, eLoad)

{

file.Abort();

return FALSE;

}

END_CATCH

 

DeleteDIB(); //清除旧位图

m_hDIB=hDIB;

 

m_palDIB = new CPalette;

if (::CreateDIBPalette(m_hDIB, m_palDIB) == NULL)

{

// DIB有可能没有调色板

delete m_palDIB;

m_palDIB = NULL;

}

return TRUE;

}

 

//从资源中载入DIB

BOOL CDib::LoadFromResource(UINT nID)

{

HINSTANCE hResInst = AfxGetResourceHandle();

HRSRC hFindRes;

HDIB hDIB;

LPSTR pDIB;

LPSTR pRes;

HGLOBAL hRes;

//搜寻指定的资源

hFindRes = ::FindResource(hResInst, MAKEINTRESOURCE(nID), RT_BITMAP);

if (hFindRes == NULL) return FALSE;

hRes = ::LoadResource(hResInst, hFindRes); //载入位图资源

if (hRes == NULL) return FALSE;

DWORD dwSize=::SizeofResource(hResInst,hFindRes);

 

hDIB = (HDIB) ::GlobalAlloc(GMEM_MOVEABLE | GMEM_ZEROINIT, dwSize);

if (hDIB == NULL) return FALSE;

pDIB = (LPSTR)::GlobalLock((HGLOBAL)hDIB);

pRes = (LPSTR) ::LockResource(hRes);

memcpy(pDIB, pRes, dwSize); //把hRes中的内容复制hDIB中

::GlobalUnlock((HGLOBAL) hDIB);

 

DeleteDIB();

m_hDIB=hDIB;

m_palDIB = new CPalette;

if (::CreateDIBPalette(m_hDIB, m_palDIB) == NULL)

{

// DIB有可能没有调色板

delete m_palDIB;

m_palDIB = NULL;

}

return TRUE;

}

 

int CDib::Width()

{

if(m_hDIB==NULL) return 0;

LPSTR lpDIB = (LPSTR) ::GlobalLock((HGLOBAL) m_hDIB);

int cxDIB = (int) ::DIBWidth(lpDIB); // Size of DIB - x

::GlobalUnlock((HGLOBAL) m_hDIB);

return cxDIB;

}

 

int CDib::Height()

{

if(m_hDIB==NULL) return 0;

LPSTR lpDIB = (LPSTR) ::GlobalLock((HGLOBAL) m_hDIB);

int cyDIB = (int) ::DIBHeight(lpDIB); // Size of DIB - y

::GlobalUnlock((HGLOBAL) m_hDIB);

return cyDIB;

}

 

//显示DIB,该函数具有缩放功能

//参数x和y说明了目的矩形的左上角坐标,cx和cy说明了目的矩形的尺寸

//cx和cy若有一个为0则该函数按DIB的实际大小绘制,cx和cy的缺省值是0

BOOL CDib::Draw(CDC *pDC, int x, int y, int cx, int cy)

{

if(m_hDIB==NULL) return FALSE;

CRect rDIB,rDest;

rDest.left=x;

rDest.top=x;

if(cx==0||cy==0)

{

cx=Width();

cy=Height();

}

rDest.right=rDest.left+cx;

rDest.bottom=rDest.top+cy;

rDIB.left=rDIB.top=0;

rDIB.right=Width();

rDIB.bottom=Height();

return ::PaintDIB(pDC->GetSafeHdc(),&rDest,m_hDIB,&rDIB,m_palDIB);

}

 

11.4.3 使用CDib类的例子

  现在让我们来看一个使用CDib类的例子。如图11.4所示,程序名为ShowDib,是一个多文档应用程序,它的功能与VC的DibLook例程有些类似,可同时打开和显示多个位图。

图11.4 用ShowDib来显示位图

 

  请读者用AppWizard建立一个名为ShowDib的MFC工程。程序应该用滚动视图来显示较大的位图,所以在MFC AppWizard的第6步应把CShowDibView的基类改为CScrollView。

  由于ShowDib程序要用到CDib类,所以应该把dibapi.cpp、myfile.cpp、dibapi.h、CDib.cpp和CDib.h文件拷贝到ShowDib目录下,并选择Project->Add to Project->Files命令把这些文件加到ShowDib工程中。

  在ShowDib.h文件中CShowDibApp类的定义之前加入下面一行:

#define WM_DOREALIZE WM_USER+200

  当收到调色板消息时,主框架窗口会发送用户定义的WM_DOREALIZE消息通知视图。

  接下来,需要用ClassWizard为CMainFrame加入WM_QUERYNEWPALETTE和WM_PALETTECHANGED消息的处理函数,为CShowDibDoc类加入OnOpenDocument函数。

  最后,请读者按清单11.5、11.6和11.7修改程序。

清单11.5 CMainFrame类的部分代码

// MainFrm.cpp : implementation of the CMainFrame class

void CMainFrame::OnPaletteChanged(CWnd* pFocusWnd)

{

CMDIFrameWnd::OnPaletteChanged(pFocusWnd);

// TODO: Add your message handler code here

 

SendMessageToDescendants(WM_DOREALIZE, 1); //通知所有的子窗口

}

 

BOOL CMainFrame::OnQueryNewPalette()

{

// TODO: Add your message handler code here and/or call default

 

CMDIChildWnd* pMDIChildWnd = MDIGetActive();

if (pMDIChildWnd == NULL)

return FALSE; // 没有活动的MDI子框架窗口

CView* pView = pMDIChildWnd->GetActiveView();

pView->SendMessage(WM_DOREALIZE,0); //只通知活动视图

return TRUE; //返回TRUE表明实现了逻辑调色板

}

 

 

清单11.6 CShowDibDoc类的部分代码

// ShowDibDoc.h : interface of the CShowDibDoc class

 

#include "CDib.h"

 

class CShowDibDoc : public CDocument

{

. . .

// Attributes

public:

 

CDib m_Dib;

. . .

};

 

// ShowDibDoc.cpp : implementation of the CShowDibDoc class

 

BOOL CShowDibDoc::OnOpenDocument(LPCTSTR lpszPathName)

{

if (!CDocument::OnOpenDocument(lpszPathName))

return FALSE;

// TODO: Add your specialized creation code here

 

BeginWaitCursor();

BOOL bSuccess=m_Dib.Load(lpszPathName); //载入DIB

EndWaitCursor();

return bSuccess;

}

 

 

 

清单11.7 CShowDibView类的部分代码

// ShowDibView.h : interface of the CShowDibView class

class CShowDibView : public CScrollView

{

. . .

afx_msg LRESULT OnDoRealize(WPARAM wParam, LPARAM lParam);

DECLARE_MESSAGE_MAP()

};

 

// ShowDibView.cpp : implementation of the CShowDibView class

 

BEGIN_MESSAGE_MAP(CShowDibView, CScrollView)

. . .

ON_MESSAGE(WM_DOREALIZE, OnDoRealize)

END_MESSAGE_MAP()

 

void CShowDibView::OnInitialUpdate()

{

CScrollView::OnInitialUpdate();

CSize sizeTotal;

// TODO: calculate the total size of this view

 

CShowDibDoc* pDoc = GetDocument();

sizeTotal.cx = pDoc->m_Dib.Width();

sizeTotal.cy = pDoc->m_Dib.Height();

SetScrollSizes(MM_TEXT, sizeTotal); //设置视图的滚动范围

}

 

void CShowDibView::OnActivateView(BOOL bActivate, CView* pActivateView, CView* pDeactiveView)

{

// TODO: Add your specialized code here and/or call the base class

 

if(bActivate)

OnDoRealize(0,0); //刷新视图

CScrollView::OnActivateView(bActivate, pActivateView, pDeactiveView);

}

 

 

LRESULT CShowDibView::OnDoRealize(WPARAM wParam, LPARAM)

{

CClientDC dc(this);

//wParam参数决定了该视图是否实现前景调色板

dc.SelectPalette(GetDocument()->m_Dib.GetPalette(),wParam);

if(dc.RealizePalette())

GetDocument()->UpdateAllViews(NULL);

return 0L;

}

 

void CShowDibView::OnDraw(CDC* pDC)

{

CShowDibDoc* pDoc = GetDocument();

ASSERT_VALID(pDoc);

// TODO: add draw code for native data here

 

pDoc->m_Dib.Draw(pDC,0,0); //输出DIB

}

  在程序中使用CDib对象的代码很简单。当用户在ShowDib程序中选择File->Open命令并从打开文件对话框中选择了一个BMP文件后,CShowDibDoc::OnOpenDocument函数被调用,该函数调用CDib::Load载入位图。在CShowDibView::OnDraw中,调用CDib::Draw输出位图。在CShowDibView::OnInitialUpdate中,根据DIB的尺寸来确定视图的滚动范围。

  需要重点研究的是ShowDib如何处理调色板问题的。ShowDib是一个多文档应用程序,可以同时显示多幅位图。由于每个位图一般都有不同的调色板,这样就产生了共享系统调色板的问题。程序必须采取措施来保证只有一个视图的逻辑调色板作为前景调色板使用。

  当主框架窗口收到WM_QUERYNEWPALETTE消息时,主框架窗口向具有输入焦点的视图发送wParam参数为0的WM_DOREALIZE消息,该视图的消息处理函数CShowDibView::OnDoRealize为视图实现前景调色板并在必要时重绘视图,这样活动视图中的位图就具有最佳颜色显示。

  如果活动视图在实现其前景调色板时改变了系统调色板,或是别的应用程序的前景调色板改变了系统调色板,那么Windows会向所有顶层窗口和重叠窗口发送WM_PALETTECHANGED消息,DibLook的主框架窗口也会收到该消息。主框架窗口对该消息的处理是向所有的视图发送wParam参数为1的WM_DOREALIZE消息,通知它们实现各自的背景调色板并在必要时重绘,这样所有的位图都能显示令人满意的颜色。

  当某一视图被激活时,需要调用OnDoRealize来实现其前景调色板,这一任务由CShowDibView:: OnActivateView函数来完成。

11.5 动画控件

  Windows 95支持一种动画控件(Animate control),动画控件可以播放AVI格式的动画片(AVI Clip),动画片可以来自一个AVI文件,也可以来自资源中。合理地使用动画控件,可以使程序的界面更加形象生动。

11.5.1 动画控件的使用

MFC的CAnimateCtrl类封装了动画控件,该类的Create成员函数负责创建动画控件,其声明为:

BOOL Create( DWORD dwStyle, const RECT& rect, CWnd* pParentWnd, UINT nID );

  参数dwStyle是如表11.3所示的控件风格的组合,参数rect指定了控件的尺寸,pParentWnd指向父窗口,nID是控件的ID。若创建成功则函数返回TRUE。

表11.3 动画控件的风格

风格

含义

ACS_CENTER

使动画片居于控件中央,并使动画片打开后控件窗口的尺寸和位置保持不变。如果不指定该风格,则控件的尺寸会自动调整来适应动画片的大小。

ACS_TRANSPARENT

使动画片的背景透明(不输出动画片的背景色)。

ACS_AUTOPLAY

一旦打开动画片后就一直重复播放。

  除表中的风格外,一般还要为动画控件指定WS_CHILD、WS_VISIBLE和WS_BORDER窗口风格。例如,要创建一个能自动播放的动画控件,应该指定其风格为WS_CHILD|WS_VISIBLE|WS_BORDER|ACS_AUTOPLAY。

  用户可以向对话框模板中加入动画控件,在模板编辑器的控件面板上,动画控件是用一个电影胶片的图形来表示的。在动画控件的属性对话框中可以指定上表列出的风格。只要不指定ACS_CENTER风格,用户就不必关心动画控件的尺寸,因为在打开动画片时控件的尺寸会被自动调整成动画片的幅面大小。

CAnimateCtrl类主要的成员函数包括:

BOOL Open( LPCTSTR lpszFileName );
BOOL Open( UINT nID );
Open函数从AVI文件或资源中打开动画片,如果参数lpszFileName或nID为NULL,则系统将关闭以前打开的动画片。若成功则函数返回TRUE。

BOOL Play( UINT nFrom, UINT nTo, UINT nRep );
该函数用来播放动画片。参数nFrom指定了播放的开始帧的索引,索引值必须小于65536,若为0则从头开始播放。nTo指定了结束帧的索引,它的值必须小于65536,若为-1则表示播放到动画片的末尾。nRep是播放的重复次数,若为-1则无限重复播放。若成功则函数返回TRUE。

BOOL Seek( UINT nTo );
该函数用来静态地显示动画片的某一帧。参数nTo是帧的索引,其值必须小于65536,若为0则显示第一帧,若为-1则显示最后一帧。若成功则函数返回TRUE。

BOOL Stop( );
停止动画片的播放。若成功则函数返回TRUE。

BOOL Close( );
关闭并从内存中清除动画片。若成功则函数返回TRUE。

  一般来说,应该把动画片放在资源里,而不是单独的AVI文件中。这样做可以使应用程序更容易管理,否则,如果应用程序要附带一大堆BMP或AVI文件,会给人一种凌乱和不专业的感觉。Visual C++不直接支持AVI资源,但用户可以创建一种新的资源类型来包含AVI。在VC的一个名为cmnctrls的MFC例子中提供了几个AVI文件(如dillo.avi),如果用户要把象dillo.avi这样的AVI文件包含到程序的资源中,则应按以下几步去做:

在程序的资源视图中单击鼠标右键,并在弹出菜单中选择Import...命令。

在文件选择对话框中选择dillo.avi文件,按Import按钮退出。

按Import按钮退出后,会出现一个Custom Resource Type对话框,如图11.5所示。如果是第一次向资源中加入AVI文件,那么应该在Resource type编辑框中为动画片类资源起一个名字(如AVI),若以前已创建过AVI型资源,则可以在直接在列表框中选择AVI型。按OK后,dillo.avi就被加入到资源中。

按Alt+Enter键后,可以在属性对话框中修改资源的ID。

图11.5 Custom Resource Type对话框

 

  创建动画控件的方法与创建普通控件相比并没有什么不同,用户可以用ClassWizard把动画控件和CAnimateCtrl对象联系起来。动画控件的使用很简单,下面的这段代码打开并不断重复播放一个资源动画,它们通常是位于OnInitDialog函数中:

m_AnimateCtrl.Open(IDR_AVI1);

m_AnimateCtrl.Play(0,-1,-1);

  如果为动画控件指定了ACS_AUTOPLAY风格,则在调用Open后就会自动重复播放,不必调用Play。程序一般不需要调用Close来关闭动画片,因为这个任务在控件被删除时会自动完成。但如果在控件已包含一个动画片的情况下,需要打开一个新的动画片,则程序应先调用Close删除原来的动画片。

11.5.2 动画控件的局限

动画控件并不能播放所有的AVI文件,只有满足下列条件的AVI文件才能被播放:

  • AVI文件必须是无声的,不能有声道。

  • AVI文件必须是未压缩的,或是用RLE算法压缩的。

  • AVI的调色板必须保持不变。

  动画控件最大的局限性在于它只能显示系统调色板中缺省的颜色,因此如果用动画控件来播放一个256色的AVI文件,那么播放效果看起来就象一个16色的动画一样,很不理想。

  总之,动画控件只能播放一些简单的,颜色数较少的AVI动画。如果要较满意地播放256色的AVI文件,就要利用下面介绍的MCI接口。

11.6 Win 32的多媒体服务

  Windows 95/NT提供了丰富的多媒体服务功能,包括大量从低级到高级的多媒体API函数。利用这些功能强大的API,用户可以在不同层次上编写多媒体应用程序。有关多媒体服务的内容完全可以写一本书,本节只是向读者简要地介绍一些最常用的多媒体服务。

  在用Visual C++开发多媒体应用时,用户必须在所有要用到多媒体函数的源程序中包含MMSYSTEM.H头文件,并且该文件位置应在WINDOWS.H头文件的后面。另外,在连接程序时要用到WINMM.LIB引入库,所以用户应该在Project Settings对话框的Link页的Object/library modules栏中加入WINMM.LIB,或者在源程序中加入下面一行:

#pragma comment(lib, "winmm.lib")

11.6.1 高级音频函数

  Windows提供了三个特殊的播放声音的高级音频函数:MessageBeep、PlaySound和sndPlaySound。这三个函数可以满足播放波形声音的一般需要,但它们播放的WAVE文件(波形声音文件)的大小不能超过100KB,如果要播放较大的WAVE文件,则应该使用MCI服务。

  MessageBeep读者已经用过了,该函数主要用来播放系统报警声音。系统报警声音是由用户在控制面板中的声音(Sounds)程序中定义的,或者在WIN.INI的[sounds]段中指定。该函数的声明为:

BOOL MessageBeep(UINT uType);

参数uType说明了告警级,如表11.4所示。若成功则函数返回TRUE。

 

表11.4 系统告警级

级别

描述

-1

从机器的扬声器中发出蜂鸣声。

MB_ICONASTERISK

播放由SystemAsterisk定义的声音。

MB_ICONEXCLAMATION

播放由SystemExclamation定义的声音。

MB_ICONHAND

播放由SystemHand定义的声音。

MB_ICONQUESTION

播放由SystemQuestion定义的声音。

MB_OK

播放由SystemDefault定义的声音

  在开始播放后,MessageBeep函数立即返回。如果该函数不能播放指定的报警声音,它就播放SystemDefault定义的系统缺省声音,如果连系统缺省声音也播放不了,那么它就会在计算机的扬声器上发出嘟嘟声。在缺省时上表的MB_系列声音均未定义。

  MessageBeep只能用来播放少数定义的声音,如果程序需要播放数字音频文件(*.WAV文件)或音频资源,就需要使用PlaySound或sndPlaySound函数。

  PlaySound函数的声明为:

BOOL PlaySound(LPCSTR pszSound, HMODULE hmod,DWORD fdwSound);

 

  参数pszSound是指定了要播放声音的字符串,该参数可以是WAVE文件的名字,或是WAV资源的名字,或是内存中声音数据的指针,或是在系统注册表WIN.INI中定义的系统事件声音。如果该参数为NULL则停止正在播放的声音。参数hmod是应用程序的实例句柄,当播放WAV资源时要用到该参数,否则它必须为NULL。参数fdwSound是标志的组合,如表11.5所示。若成功则函数返回TRUE,否则返回FALSE。

 

表11.5 播放标志

标志

含义

SND_APPLICATION

用应用程序指定的关联来播放声音。

SND_ALIAS

pszSound参数指定了注册表或WIN.INI中的系统事件的别名。

SND_ALIAS_ID

pszSound参数指定了预定义的声音标识符。

SND_ASYNC

用异步方式播放声音,PlaySound函数在开始播放后立即返回。

SND_FILENAME

pszSound参数指定了WAVE文件名。

SND_LOOP

重复播放声音,必须与SND_ASYNC标志一块使用。

SND_MEMORY

播放载入到内存中的声音,此时pszSound是指向声音数据的指针。

SND_NODEFAULT

不播放缺省声音,若无此标志,则PlaySound在没找到声音时会播放缺省声音。

SND_NOSTOP

PlaySound不打断原来的声音播出并立即返回FALSE。

SND_NOWAIT

如果驱动程序正忙则函数就不播放声音并立即返回。

SND_PURGE

停止所有与调用任务有关的声音。若参数pszSound为NULL,就停止所有的声音,否则,停止pszSound指定的声音。

SND_RESOURCE

pszSound参数是WAVE资源的标识符,这时要用到hmod参数。

SND_SYNC

同步播放声音,在播放完后PlaySound函数才返回。

 

 

  在C:/WINDOWS/MEDIA目录下有一个名为The Microsoft Sound.wav的声音文件,在Windows 95启动时会播放这个声音。下面我们用三种方法来调用PlaySound函数播出Windows 95的启动声音。

  第一种方法是直接播出声音文件,相应的代码为:

PlaySound("c://win95//media//The Microsoft Sound.wav", NULL, SND_FILENAME | SND_ASYNC);

  注意参数中的路径使用两个连续的反斜杠转义代表一个反斜杠。

  第二种方法是把声音文件加入到资源中,然后从资源中播放声音。Visual C++支持WAVE型资源,用户在资源视图中单击鼠标右键并选择Import命令,然后在文件选择对话框中选择The Microsoft Sound.wav文件,则该文件就会被加入到WAVE资源中。假定声音资源的ID为IDR_STARTWIN,则下面的调用同样会输出启动声音:

PlaySound((LPCTSTR)IDR_STARTWIN, AfxGetInstanceHandle(), SND_RESOURCE | SND_ASYNC);

  第三种方法是用PlaySound播放系统声音,Windows启动的声音是由SystemStart定义的系统声音,因此可以用下面的方法播放启动声音:

PlaySound("SystemStart",NULL,SND_ALIAS|SND_ASYNC);

函数sndPlaySound的功能与PlaySound类似,但少了一个参数。函数的声明为:

BOOL sndPlaySound(LPCSTR lpszSound, UINT fuSound);

 

  除了不能指定资源名字外,参数lpszSound与PlaySound的是一样的。参数fuSound是如何播放声音的标志,可以是SND_ASYNC、SND_LOOP、SND_MEMORY、SND_NODEFAULT、SND_NOSTOP和SND_SYNC的组合,这些标志的含义与PlaySound的一样。

可以看出,sndPlaySound不能直接播放声音资源。要用该函数播放WAVE文件,可按下面的方式调用:

sndPlaySound(“MYSOUND.WAV”,SND_ASYNC);

11.6.2 MCI

  MCI(Media Control Interface,媒体控制接口)向Windows程序提供了在高层次上控制媒体设备接口的能力。程序不必关心具体设备,就可以对激光唱机(CD)、视盘机、波形音频设备、视频播放设备和MIDI设备等媒体设备进行控制。对于程序员来说,可以把MCI理解为设备面板上的一排按键,通过选择不同的按键(发送不同的MCI命令)可以让设备完成各种功能,而不必关心设备内部实现。比如,对于play,视盘机和CD机有不同的反应(一个是播放视频,一个播放音频),而对用户来说却只需要按同一按钮。

  应用程序通过向MCI发送命令来控制媒体设备。MCI命令接口分命令字符串和命令消息两种,两者具有相同的功能。命令字符串具有使用简单的特点,但是它的执行效率不如命令消息。

  所有的MCI命令字符串都是通过多媒体API函数mciSendString传递给MCI的,该函数的声明为:

MCIERROR mciSendString(

LPCTSTR lpszCommand, //MCI命令字符串

LPTSTR lpszReturnString, //存放反馈信息的缓冲区

UINT cchReturn, //缓冲区的长度

HANDLE hwndCallback //回调窗口的句柄,一般为NULL

); //若成功则返回0,否则返回错误码。

 

  该函数返回的错误码可以用mciGetErrorString函数进行分析,该函数的声明为:

BOOL mciGetErrorString(

DWORD fdwError, //函数mciSendString或mciSendCommand返回的错误码

LPTSTR lpszErrorText, //接收描述错误的字符串的缓冲区

UINT cchErrorText //缓冲区的长度

);

  下面是使用mciSendString函数的一个简单例子:

char buf[50];

MCIERROR mciError;

mciError=mciSendString(“open cdaudio”,buf,strlen(buf),NULL);

if(mciError)

{

mciGetErrorString(mciError,buf,strlen(buf));

AfxMessageBox(buf);

return;

}

  open cdaudio命令打开CD播放器,如果出错(如驱动器内没有CD)则返回错误码,此时可以用mciGetErrorString函数取得错误信息字符串。open是MCI打开设备的命令,cdaudio是MCI设备名。MCI的设备类型在表11.6列出。

 

表11.6 MCI设备类型

设备类型

描述

animation

动画设备

cdaudio

CD播放器

dat

数字音频磁带机

digitalvideo

某一窗口中的数字视频(不基于GDI)

other

未定义的MCI设备

overlay

重叠设备(窗口中的模拟视频)

scanner

图象扫描仪

sequencer

MIDI序列器

videodisc

视盘机

waveaudio

播放数字波形文件的音频设备

  请读者注意,设备类型和设备名是不同的概念。设备类型是指响应一组共用命令的一类MCI设备,而设备名则是某一个MCI设备的名字。系统需要用不同的设备名来区分属于同一设备类型的不同设备。

  设备名是在注册表或SYSTEM.INI的[mci]部分定义的,典型的[mci]段如下所示:

[mci]

cdaudio=mcicda.drv

sequencer=mciseq.drv

waveaudio=mciwave.drv

avivideo=mciavi.drv

videodisc=mcipionr.drv

  等号的左边是设备名,右边是对应的MCI驱动程序。当安装了新的MCI驱动程序时,系统要用不同的设备名来区分。设备名通常与驱动程序中的设备类型名相同,如cdaudio和waveaudio等,但也有例外,如avivideo设备是一个digitalvideo类型的设备。

  使用MCI设备一般包括打开、使用和关闭三个过程。MCI的大部分命令可以控制不同的媒体设备。例如,可以用play命令来播放WAVE文件、视频文件或CD。表11.7列出常用的MCI命令字符串,表中大部分命令都具有通用性。在MCI命令的后面一般要跟一个设备名以指定操作的对象。

 

表11.7 常用的MCI命令

命令

描述

capacility

查询设备能力

close

关闭设备

info

查询设备的信息

open

打开设备

pause

暂停设备的播放或记录

play

开始设备播放

record

开始

resume

恢复暂停播放或记录的设备

seek

改变媒体的当前位置

set

改变设置

status

查询设备状态信息

stop

停止设备的播放或记录

 

 

  例如,上面的例子打开了一个CD播放机后,可以发送常用的命令来控制CD机:

play cdaudio from <位置> to <位置>。若省略from则从当前磁道开始播放,若省略to则播放到结束。

pause cdaudio。暂停播放。

stop cdaudio。停止播放。

resume cdaudio。继续被暂停的播放。

status cdaudio number of tracks。查询CD的磁道数。status cdaudio current track可以查询当前磁道。

seek cdaudio to <位置>。移动到指定磁道。

set cdaudio door open/closed。弹出或缩进CD盘。

close cdaudio。关闭设备。

 

  MCI设备可以按简单设备和复合设备进行分类。象cdaudio这样的设备不使用文件,我们称之为简单设备,而复合设备在播放时要用到数据文件,如数字视频(digitalvideo)和波形音频(waveaudio)设备,我们把这些数据文件叫做设备元素。

  在打开一个复合设备时要指定设备名和设备元素。例如,下面命令打开一个波形音频设备:

open mysound.wav type waveaudio

  可以只为复合设备指定设备元素,例如:

open mysound.wav

  如下面所示,系统通过查找注册表或WIN.INI的[mci extensions]可以确定打开哪一个设备。

[mci extensions]

mid=Sequencer

rmi=Sequencer

wav=waveaudio

avi=AVIVideo

  有时,程序需要多次打开同一设备来播放不同的数据文件。例如,谁也不能否认在屏幕上同时播放两个AVI文件的可能性,在这种情况下,需要为每次打开的设备起一个不同的别名,这样MCI才能区分两个播放设备。例如,下面这段代码打开并播放了两个AVI文件:

char buf[50];

mciSendString("open dillo.avi type avivideo alias dillo",buf,strlen(buf),NULL);

mciSendString("play dillo repeat",buf,strlen(buf),NULL); //重复播放

mciSendString("open search.avi type avivideo alias search",buf,strlen(buf),NULL);

mciSendString("play search",buf,strlen(buf),NULL);

在用open命令打开设备时,如果指定了别名,则以后对该设备的操作都要使用别名。

  到目前为止,我们使用的都是MCI命令字符串。读者可能己经有了这样的体会,命令字符串具有简单易学的优点,但这种接口与C/C++的风格相去甚远,如果程序要查询和设置大量数据,那么用字符串的形式将很不方便。

  MCI的命令消息接口提供了C语言接口,它速度更快,并且更能符合C/C++程序员的需要。所有MCI命令消息都是通过mciSendCommand函数发送的,该函数的声明为:

MCIERROR mciSendCommand(

MCIDEVICEID IDDevice, //设备的ID,在打开设备时不用该参数

UINT uMsg, //命令消息

DWORD fdwCommand, //命令消息的标志

DWORD dwParam //指向包含命令消息参数的结构

); //若成功则返回0,否则返回错误码

 

清单11.8的代码演示了用MCI命令消息来打开和重复播放一个AVI文件:

 

清单11.8

MCI_DGV_OPEN_PARMS mciOpen;

UINT wDeviceID;

MCIERROR mciError;

 

mciOpen.lpstrDeviceType = "avivideo"; //设备名

mciOpen.lpstrElementName = "dillo.avi"; //设备元素

 

mciError=mciSendCommand(0, MCI_OPEN,

MCI_OPEN_TYPE|MCI_OPEN_ELEMENT, //使用了设备元素

(DWORD)&mciOpen);

if(mciError)

{

char s[80];

mciGetErrorString(mciError,s,80);

AfxMessageBox(s);

return ;

}

wDeviceID=mciOpen.wDeviceID; //保存设备ID

MCI_DGV_PLAY_PARMS mciPlay;

mciError=mciSendCommand(wDeviceID, MCI_PLAY, MCI_DGV_PLAY_REPEAT,

(DWORD)&mciPlay);

. . .

  可以看出,用命令消息比用命令字符串要复杂的多。命令消息与命令字符串是对应的,例如,open与MCI_OPEN完成的是一样的功能。变量wDeviceID用来保存设备的ID,系统用ID来标识不同的设备,以保证命令发给正确的对象。

  限于篇幅,对MCI的命令消息就不作详细介绍了。

 

你可能感兴趣的:(技术文章)