摘录于《Windows程序(第5版,珍藏版).CHarles.Petzold 著》P297
许多按钮的显示看起来不太对。按键按钮还好,但其他按钮都有一个本不该有的长方形灰色背景。这是因为按钮是被设计用于在对话框中显示的,而 Windows 98 中的对话框的表明是灰色的。我们的窗口表面是白色的,因为这是我们在 WNDCLASS 结构中已经定义好的:
wndclass.hbrBackground = (HBRUSH) GetStockObject (WHITE_BRUSH);
我们之所以这样设置,是因为我们常常要在客户区显示文本,GDI 使用了定义在默认设备环境中的文本颜色与背景颜色。这些颜色总是黑色和白色。为了使这些按钮更耐看,我们必须要么改变客户区的颜色,让它与按钮的背景颜色相同,要么就更改按钮的背景颜色为白色。
要解决这个问题,首先要理解 Windows 是如何使用系统颜色的。
Windows 有 29 种系统颜色来支持各部分的显示。可以使用 GetSysColor 和 SetSysColors 获取并设置这些颜色。定义在 Windows 头文件中的标识符指定了系统颜色。用 SetSysColors 设置系统颜色仅仅影响到当前的窗口会话。
可以用 Windows 控制面板的【显示】工具来改变某些(但不是所有的)系统颜色。选定的颜色分别存储在 Windows NT 的注册表(Registry)中或 Windows 98 的 WIN.INI 文件中。注册表和 WIN.INI 文件使用关键字来表示 29 种系统颜色(这些关键字不同于 GetSysColor 和 SetSysColors 的标识符),紧接着是范围 0 ~ 255 的红、绿、蓝 RGB 值。下表列出了 29 种系统颜色的定义,它使用 GetSysColor,SetSysColors 的常量和 WIN.INI 中的关键字。此表以 COLOR_ 常数为序,从 0 开始,到 28 结束。
GetSysColor 和 SetSysColors | 注册表键值或 WIN.INI 标识符 | 默认 RGB 值 |
---|---|---|
COLOR_SCROLLBAR | Scrollbar | C0-C0-C0 |
COLOR_BACKGROUND | Background | 00-80-80 |
COLOR_ACTIVECAPTION | ActiveTitle | 00-00-80 |
COLOR_INACTIVECAPTION | InactiveTitle | 80-80-80 |
COLOR_MENU | Menu | C0-C0-C0 |
COLOR_WINDOW | Window | FF-FF-FF |
COLOR_WINDOWFRAME | WindowFrame | 00-00-00 |
COLOR_MENUTEXT | MenuText | C0-C0-C0 |
COLOR_WINDOWTEXT | WindowText | 00-00-00 |
COLOR_CAPTIONTEXT | TitleText | FF-FF-FF |
COLOR_ACTIVEBORDER | ActiveBorder | C0-C0-C0 |
COLOR_INACTIVEBORDER | InactiveBorder | C0-C0-C0 |
COLOR_APPWORKSPACE | AppWorkspace | 80-80-80 |
COLOR_HIGHLIGHT | Highlight | 00-00-80 |
COLOR_HIGHLIGHTTEXT | HighlightText | FF-FF-FF |
COLOR_BTNFACE | ButtonFace | C0-C0-C0 |
COLOR_BTNSHADOW | ButtonShadow | 80-80-80 |
COLOR_GRAYTEXT | GrayText | 80-80-80 |
COLOR_BTNTEXT | ButtonText | 00-00-00 |
COLOR_INACTIVECAPTIONTEXT | InactiveTitleText | C0-C0-C0 |
COLOR_BTNHIGHLIGHT | ButtonHighlight | FF-FF-FF |
COLOR_3DDKSHADOW | ButtonDkShadow | 00-00-00 |
COLOR_3DLIGHT | ButtonLight | C0-C0-C0 |
COLOR_INFOTEXT | InfoText | 00-00-00 |
COLOR_INFOBK | InfoWindow | FF-FF-FF |
[没有标识符,使用值 25] | ButtonAlternateFace | B8-B4-B8 |
COLOR_HOTLIGHT | HotTrackingColor | 00-00-FF |
COLOR_GRADIENTACTIVECAPTION | GradientActiveTitle | 00-00-80 |
COLOR_GRADIENTINACTIVECAPTION | GradientInactiveTile | 80-80-80 |
有一个坏消息:虽然有许多颜色似乎不言自明(例如,COLOR_BACKGROUND 是所有窗口之下的桌面颜色),但是最近版本的 Windows 中系统色彩变得相当混乱。在过去,Windows 看上去比今天的简单很多。事实上,在 Windows 3.0 之前,只有上述的前 13 种被定义为系统颜色。随着更复杂的有着三维外观的可视控件的使用,就需要更多的系统颜色。
这个问题对按钮而言尤为明显,因为每种按钮都要用到多种颜色。COLOR_BTNFACE 用于按键按钮的主表面颜色和其他按钮的背景颜色。(这也是对话框和消息框使用的系统颜色。)COLOR_BTNSHADOW 用在按键按钮的右侧和底部、复选框方框的内部和单选按钮的圆圈内,用来表示阴影。对于按键按钮,COLOR_BTNTEXT 用于文本的颜色;其他控件的文本颜色使用的则是COLOR_WINDOWTEXT。还有好几种其他系统颜色也用于按钮的设计。
因此,如果我们要在客户区表面显示按钮,一个避免颜色冲突的途径就是使用这些系统颜色。首先,在设计窗口类时,使用 COLOR_BTNFACE 作为客户区的背景颜色:
wndclass.hbrBackground = (HBRUSH) (COLOR_BTNFACE + 1);
可以在 BTNLOOK 程序中自行尝试一下这种做法。Windows 知道,在 WNDCLASS 结构中hbrBackground 值很低时,它实际上指的是系统颜色,而不是一个实际的句柄。Windows 要求在使用这些标识符时,要加上 1,并在 WNDCLASS 结构的 hbrBackground 字段中加以指定,但这样做并没有什么深远的目的,只是为了防止出现空值(NULL)。如果系统颜色发送了改变,而你的程序正在运行,那么客户区表面会变为无效,Windows 将使用新的 COLOR_BTNFACE 值。但现在我们又引发了另一个问题。在使用 TextOut 显示文本时,Windows 使用设备环境中定义的值来作为文本的背景颜色(它清楚了文本的背景和文本的颜色。预设值是白色(背景)和黑色(文本),而不管系统颜色或是窗口类结构的 hbrBackground 定义的颜色。所以需要使用 SetTextColor 和 SetBkColor 改变文本和文本背景颜色以匹配系统颜色。可以在获得设备环境的句柄之后做这件事情:
SetBkColor(hdc, GetSysColor(COLOR_BTNFACE));
SetTextColor(hdc, GetSysColor(COLOR_WINDOWTEXT));
现在,客户区的背景、文本背景和文本颜色与按钮的颜色都是一致的。但是,当程序正在运行时,如果用户更改了系统颜色,则需要更改文本的背景颜色和文本颜色。为此,使用以下代码即可:
case WM_SYSCOLORCHANGE:
InvalidateRect(hwnd, NULL, TRUE);
break;
前面介绍了如何调整客户区的颜色和文本颜色以匹配按钮背景颜色。这是否意味着可以在程序中把按钮的颜色也相应调为自己喜欢的颜色呢?在理论上是可以的,但在实际中有问题。最好不要用 SetSysColors 改变按钮的外观。这将影响到 Windows 环境下正在运行的所有程序,用户不会喜欢这样的。
一个更好的办法(也是理论上如此)是处理 WM_CTLCOLORBTN 消息。当子窗口即将重绘其客户区时,按钮控件会把这个消息发给其父窗口的窗口过程。父窗口可以利用这个机会来改变子窗口的背景颜色。(在 16 位版本的 Windows 中,有一个名为 WM_CTLCOLOR 的消息用于所有的控件。此消息已被针对每种标准控件的不同消息所取代。)
当父窗口的窗口过程收到 WM_CTLCOLORBTN 消息时,wParam 消息参数是按钮的设备环境句柄。lParam 是按钮的窗口句柄。当父窗口的窗口过程收到此消息,按钮控件便已经取得设备环境。当在窗口过程中处理 WM_CTLCOLORBTN 信息时,可以选择以下做法:
从理论上讲,子窗口使用这个画刷来着色背景。在不再需要画刷时,你需要负责销毁画刷。
这里有一个关于 WM_CTLCOLORBTN 的问题:只有按键按钮和自绘按钮会发送 WM_CTLCOLORBTN 到它们的父窗口,而只有自绘按钮需要对父窗口使用画刷绘制背景的消息处理过程作出 反应。这是相当没有必要的,因为父窗口负责绘制自绘按钮。
在本章后面,我们将讨论类似于 WM_CTLCOLORBTN 的消息的有用情况,不过这些消息是应用于其他类型控件的。
如果你想自己完全定义一个按钮的外观同时又不想处理太多的键盘以及鼠标逻辑,那么可以创建一个 BS_OWNERDRAW 样式的按钮。
/*--------------------------------------------------------
OWNDRAW.C -- Owner-Draw Button Demo Program
(c) Charles Petzold, 1998
--------------------------------------------------------*/
#include
#define ID_SMALLER 1
#define ID_LARGER 2
#define BTN_WIDTH (8 * cxChar)
#define BTN_HEIGHT (4 * cyChar)
LRESULT CALLBACK WndProc (HWND, UINT, WPARAM, LPARAM) ;
HINSTANCE hInst;
int WINAPI WinMain (HINSTANCE hInstance, HINSTANCE hPrevInstance,
PSTR szCmdLine, int iCmdShow)
{
static TCHAR szAppName[] = TEXT ("OwnDraw") ;
HWND hwnd ;
MSG msg ;
WNDCLASS wndclass ;
hInst = hInstance;
wndclass.style = CS_HREDRAW | CS_VREDRAW ;
wndclass.lpfnWndProc = WndProc ;
wndclass.cbClsExtra = 0 ;
wndclass.cbWndExtra = 0 ;
wndclass.hInstance = hInstance ;
wndclass.hIcon = LoadIcon (NULL, IDI_APPLICATION) ;
wndclass.hCursor = LoadCursor (NULL, IDC_ARROW) ;
wndclass.hbrBackground = (HBRUSH) GetStockObject (WHITE_BRUSH) ;
wndclass.lpszMenuName = NULL ;
wndclass.lpszClassName = szAppName ;
if (!RegisterClass (&wndclass))
{
MessageBox (NULL, TEXT ("This program requires Windows NT!"),
szAppName, MB_ICONERROR) ;
return 0 ;
}
hwnd = CreateWindow (szAppName, TEXT ("Owner-Draw Button Demo"),
WS_OVERLAPPEDWINDOW,
CW_USEDEFAULT, CW_USEDEFAULT,
CW_USEDEFAULT, CW_USEDEFAULT,
NULL, NULL, hInstance, NULL) ;
ShowWindow (hwnd, iCmdShow) ;
UpdateWindow (hwnd) ;
while (GetMessage (&msg, NULL, 0, 0))
{
TranslateMessage (&msg) ;
DispatchMessage (&msg) ;
}
return msg.wParam ;
}
void Triangle (HDC hdc, POINT pt[])
{
SelectObject(hdc, GetStockObject(BLACK_BRUSH));
Polygon(hdc, pt, 3);
SelectObject(hdc, GetStockObject(WHITE_BRUSH));
}
LRESULT CALLBACK WndProc (HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam)
{
static HWND hwndSamller, hwndLarger;
static int cxClient, cyClient, cxChar, cyChar;
int cx, cy;
LPDRAWITEMSTRUCT pdis;
POINT pt[3];
RECT rc;
switch (message)
{
case WM_CREATE:
cxChar = LOWORD(GetDialogBaseUnits());
cyChar = HIWORD(GetDialogBaseUnits());
// Create the owner-draw pushbuttons
hwndSamller = CreateWindow(TEXT("button"), TEXT(""),
WS_CHILD | WS_VISIBLE | BS_OWNERDRAW,
0, 0, BTN_WIDTH, BTN_HEIGHT,
hwnd, (HMENU) ID_SMALLER, hInst, NULL);
hwndLarger = CreateWindow(TEXT("button"), TEXT(""),
WS_CHILD | WS_VISIBLE | BS_OWNERDRAW,
0, 0, BTN_WIDTH, BTN_HEIGHT,
hwnd, (HMENU) ID_LARGER, hInst, NULL);
return 0;
case WM_SIZE:
cxClient = LOWORD(lParam);
cyClient = HIWORD(lParam);
// Move the buttons to the new center
MoveWindow(hwndSamller, cxClient / 2 - 3 * BTN_WIDTH / 2,
cyClient / 2 - BTN_HEIGHT / 2,
BTN_WIDTH, BTN_HEIGHT, TRUE);
MoveWindow(hwndLarger, cxClient / 2 + BTN_WIDTH / 2,
cyClient / 2 - BTN_HEIGHT / 2,
BTN_WIDTH, BTN_HEIGHT, TRUE);
return 0;
case WM_COMMAND:
GetWindowRect(hwnd, &rc);
// Make the window 10% smaller or larger
switch (wParam)
{
case ID_SMALLER:
rc.left += cxClient / 20;
rc.right -= cxClient / 20;
rc.top += cyClient / 20;
rc.bottom -= cyClient / 20;
break;
case ID_LARGER:
rc.left -= cxClient / 20;
rc.right += cxClient / 20;
rc.top -= cyClient / 20;
rc.bottom += cyClient / 20;
break;
}
MoveWindow(hwnd, rc.left, rc.top, rc.right - rc.left,
rc.bottom - rc.top, TRUE);
return 0;
case WM_DRAWITEM:
pdis = (LPDRAWITEMSTRUCT) lParam;
// Fill area with white and frame it black
FillRect(pdis->hDC, &pdis->rcItem,
(HBRUSH) GetStockObject(WHITE_BRUSH));
FrameRect(pdis->hDC, &pdis->rcItem,
(HBRUSH) GetStockObject(BLACK_BRUSH));
// Draw inward and outward black triangles
cx = pdis->rcItem.right - pdis->rcItem.left;
cy = pdis->rcItem.bottom - pdis->rcItem.top;
switch (pdis->CtlID)
{
case ID_SMALLER:
pt[0].x = 3 * cx / 8; pt[0].y = 1 * cy / 8;
pt[1].x = 5 * cx / 8; pt[1].y = 1 * cy / 8;
pt[2].x = 4 * cx / 8; pt[2].y = 3 * cy / 8;
Triangle(pdis->hDC, pt);
pt[0].x = 7 * cx / 8; pt[0].y = 3 * cy / 8;
pt[1].x = 7 * cx / 8; pt[1].y = 5 * cy / 8;
pt[2].x = 5 * cx / 8; pt[2].y = 4 * cy / 8;
Triangle(pdis->hDC, pt);
pt[0].x = 5 * cx / 8; pt[0].y = 7 * cy / 8;
pt[1].x = 3 * cx / 8; pt[1].y = 7 * cy / 8;
pt[2].x = 4 * cx / 8; pt[2].y = 5 * cy / 8;
Triangle(pdis->hDC, pt);
pt[0].x = 1 * cx / 8; pt[0].y = 5 * cy / 8;
pt[1].x = 1 * cx / 8; pt[1].y = 3* cy / 8;
pt[2].x = 3 * cx / 8; pt[2].y = 4 * cy / 8;
Triangle(pdis->hDC, pt);
break;
case ID_LARGER:
pt[0].x = 5 * cx / 8; pt[0].y = 3 * cy / 8;
pt[1].x = 3 * cx / 8; pt[1].y = 3 * cy / 8;
pt[2].x = 4 * cx / 8; pt[2].y = 1 * cy / 8;
Triangle(pdis->hDC, pt);
pt[0].x = 5 * cx / 8; pt[0].y = 5 * cy / 8;
pt[1].x = 5 * cx / 8; pt[1].y = 3 * cy / 8;
pt[2].x = 7 * cx / 8; pt[2].y = 4 * cy / 8;
Triangle(pdis->hDC, pt);
pt[0].x = 3 * cx / 8; pt[0].y = 5 * cy / 8;
pt[1].x = 5 * cx / 8; pt[1].y = 5 * cy / 8;
pt[2].x = 4 * cx / 8; pt[2].y = 7 * cy / 8;
Triangle(pdis->hDC, pt);
pt[0].x = 3 * cx / 8; pt[0].y = 3 * cy / 8;
pt[1].x = 3 * cx / 8; pt[1].y = 5 * cy / 8;
pt[2].x = 1 * cx / 8; pt[2].y = 4 * cy / 8;
Triangle(pdis->hDC, pt);
break;
}
// Invert the rectangle if the button is selected
if (pdis->itemState & ODS_SELECTED)
InvertRect(pdis->hDC, &pdis->rcItem);
// Draw a focus rectangle if the button has the focus
if (pdis->itemState & ODS_FOCUS)
{
pdis->rcItem.left += cx / 16;
pdis->rcItem.top += cy / 16;
pdis->rcItem.right -= cx / 16;
pdis->rcItem.bottom -= cy / 16;
DrawFocusRect(pdis->hDC, &pdis->rcItem);
}
return 0;
case WM_DESTROY:
PostQuitMessage (0) ;
return 0 ;
}
return DefWindowProc (hwnd, message, wParam, lParam) ;
}
如果只需要在按钮上显示一个图标或位图,可以使用 BS_ICON 或 BS_BITMAP 样式,设置位图可使用 BM_SETIMAGE 消息。但 BS_OWNERDRAW 样式的按钮允许你完全控制按钮的绘制。
在处理 WM_CREATE 消息时,OWNDRAW 创建了两个 BS_OWNERDRAW 样式的按钮;按钮的宽度是系统字体宽度的 8 倍,高度是系统字体高度的 4 倍。(使用预定的位图绘制按钮时,应该知道这些位图在 VGA 上显示的大小是 64 * 64 像素。)按钮尚未被定位。在处理 WM_SIZE 消息时,OWNDRAW 会用 MoveWindow 函数把按钮放置在客户区中心。
图 9-4 OWNDRAW 程序的显示
按下这些按钮,它们会产生 WM_COMMAND 消息。为了处理 WM_COMMAND 消息,OWNDRAW 调用 GetWindowRect 来把整个窗口(不仅仅是客户区)的位置和大小存储在一个 RECT(矩形)结构中。这一位置是相对于屏幕的。根据是左边按钮还是右边按钮被单击,OWNDRAW 会相应调整矩形结构的字段值。然后,程序会用 MoveWindow 重新设定窗口位置和尺寸。这会产生一个 WM_SIZE 消息,按钮会被重新放在客户区的中心。
如果程序完成了这些步骤,按钮应该完全会工作,但是你却看不见按钮。对于 BS_OWNERDRAW 样式的按钮,在需要重新绘制时,它会向其父窗口发送 WM_DRAWITEM 消息。这包括以下几种情况:在新建按钮时,在按钮被按下或释放时,在按钮得到或失去焦点时,或者在其他需要重绘这个按钮的时候。
在 WM_DRAWITEM 消息中,lParam 参数是一个指针,指向一个 DRAWITEMSTRUCT 类型的结构。OWNDRAW 程序将这个指针保存在变量 pdis 中。这个结构中与按钮有非常重要的关系的字段是:hDC(按钮设备环境),rcItem(RECT 结构,提供按钮的尺寸),CtlID(控件窗口 ID)和 itemState(它显示按钮是否按下或有输入焦点)。
OWNDRAW 调用 FillRect 来开始 WM_DRAWITEM 处理过程,首先用白色的画刷将按钮的表面涂成白色,之后调用 FrameRect 在按钮周围绘制一个黑框。接下来,OWNDRAW 调用 Polygon 在按钮上绘制四个黑色的实心三角符号。这是正常情况的处理。
如果目前按钮处于被按下状态,那么 DRAWITEMSTRUCT 中的 itemState 的一位将被设置。可以用ODS_SELECTED 常数来测试这位的值。如果它被设置,OWNDRAW 调用 InvertRect 颠倒按钮的颜色。如果按钮有输入焦点,itemState 的ODS_FOCUS 状态位将被设置。在这种情况下,OWNDRAW 调用 DrawFocusRect 在按钮边缘绘制虚线矩形。
对于使用自绘按钮的人,我想提出一个忠告:Windows 会为你获得设备环境,并把它放在 DRAWITEMSTRUCT 结构的一个字段中。必须原样保持这个设备环境的状态。任何一个被选入该设备环境的 GDI 对象必须设置回原有的不被选中的状态。此外不能再按钮的区域以外绘制任何图形。