上一页 下一页 返回

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使用逻辑调色板来作为使用颜色的应用程序和系统调色板(物理调色板)之间的缓冲。

T11_1.tif (194188 bytes)

图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),顶层窗口是指没有父窗口或父窗口是桌面窗口的窗口,这种窗口一般都有标题和边框,主要包括框架窗口和对话框。术语重叠窗口是指作为应用程序主窗口的窗口,我们可以把对话框看成是一种特殊的重叠式窗口。

 

T11_2.tif (192888 bytes)

图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引用的区别。该程序也着重向读者演示了处理调色板消息的方法。

T11_3.tif (237628 bytes)

图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节读者可以看到使用多个逻辑调色板时的处理方法。