摘录于《Windows程序(第5版,珍藏版).CHarles.Petzold 著》P534
14.4.7 在位图上绘图
我们已经看到可以把位图作为一种资源用来在窗口上绘图。这需要把位图选进内存设备环境,并且调用 BitBlt 或者 StretchBlt。你也可以把指向内存设备环境的句柄作为所有 GDI 函数调用的第一个参数。除了显示表面是位图外,内存设备环境和真实的设备环境功能一样。
HELLOBIT 程序展示了这项技术。程序在一个小位图上显示文本字符串“Hello,world!”,然后用 BitBlt 或 StretchBlt(基于你的菜单选择)把位图绘制到程序的客户区。
/*---------------------------------------------------
HELLOBIT.C -- Bitmap Demonstration
(c) Charles Petzold, 1998
---------------------------------------------------*/
#include <windows.h>
#include "resource.h"
LRESULT CALLBACK WndProc(HWND, UINT, WPARAM, LPARAM);
int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevinstance,
PSTR szCmdLine, int iCmdShow)
{
static TCHAR szAppName[] = TEXT("HelloBit");
HWND hwnd;
MSG msg;
WNDCLASS wndclass;
wndclass.style = CS_HREDRAW | CS_VREDRAW;
wndclass.lpfnWndProc = WndProc;
wndclass.cbClsExtra = 0;
wndclass.cbWndExtra = 0;
wndclass.hInstance = hInstance;
wndclass.hIcon = LoadIcon(NULL, IDI_INFORMATION);
wndclass.hCursor = LoadCursor(NULL, IDC_ARROW);
wndclass.hbrBackground = (HBRUSH) GetStockObject(WHITE_BRUSH);
wndclass.lpszMenuName = szAppName;
wndclass.lpszClassName = szAppName;
if (!RegisterClass(&wndclass))
{
MessageBox(NULL, TEXT("This program requires Windows NT!"),
szAppName, MB_ICONERROR);
return 0;
}
hwnd = CreateWindow(szAppName, TEXT("HelloBit"),
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;
}
LRESULT CALLBACK WndProc(HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam)
{
static HBITMAP hBitmap;
static HDC hdcMem;
static int cxBitmap, cyBitmap, cxClient, cyClient, iSize = IDM_BIG;
static TCHAR * szText = TEXT(" Hello, World! ");
HDC hdc;
HMENU hMenu;
int x, y;
PAINTSTRUCT ps;
SIZE size;
switch (message)
{
case WM_CREATE:
hdc = GetDC(hwnd);
hdcMem = CreateCompatibleDC(hdc);
GetTextExtentPoint32(hdc, szText, lstrlen(szText), &size);
cxBitmap = size.cx;
cyBitmap = size.cy;
hBitmap = CreateCompatibleBitmap(hdc, cxBitmap, cyBitmap);
ReleaseDC(hwnd, hdc);
SelectObject(hdcMem, hBitmap);
TextOut(hdcMem, 0, 0, szText, lstrlen(szText));
return 0;
case WM_SIZE:
cxClient = LOWORD(lParam);
cyClient = HIWORD(lParam);
return 0;
case WM_COMMAND:
hMenu = GetMenu(hwnd);
switch (LOWORD(wParam))
{
case IDM_BIG:
case IDM_SMALL:
CheckMenuItem(hMenu, iSize, MF_UNCHECKED);
iSize = LOWORD(wParam);
CheckMenuItem(hMenu, iSize, MF_CHECKED);
InvalidateRect(hwnd, NULL, TRUE);
break;
}
return 0;
case WM_PAINT:
hdc = BeginPaint(hwnd, &ps);
switch (iSize)
{
case IDM_BIG:
StretchBlt(hdc, 0, 0, cxClient, cyClient,
hdcMem, 0, 0, cxBitmap, cyBitmap, SRCCOPY);
break;
case IDM_SMALL:
for (y = 0; y < cyClient; y += cyBitmap)
for (x = 0; x < cxClient; x += cxBitmap)
{
BitBlt(hdc, x, y, cxBitmap, cyBitmap,
hdcMem, 0, 0, SRCCOPY);
}
break;
}
EndPaint(hwnd, &ps);
return 0;
case WM_DESTROY:
DeleteDC(hdcMem);
DeleteObject(hBitmap);
PostQuitMessage(0);
return 0;
}
return DefWindowProc(hwnd, message, wParam, lParam);
}
HELLOBIT.RC (excerpts)
// Microsoft Visual C++ generated resource script.
//
#include "resource.h"
/
//
// Menu
//
HELLOBIT MENU
BEGIN
POPUP "&Size"
BEGIN
MENUITEM "&Big", IDM_BIG, CHECKED
MENUITEM "&Small", IDM_SMALL
END
END
RESOURCE.H (excerpts)
// Microsoft Visual C++ 生成的包含文件。
// 供 HelloBit.rc 使用
//
#define IDM_BIG 40001
#define IDM_SMALL 40002
程序一开始通过调用 GetTextExtentPoint32 确定文本字符串的像素大小。此大小就是与视频显示兼容的位图的尺寸。一旦把位图选进内存设备环境(此环境也与视频显示兼容),就通过一个 TextOut 函数调用把文本绘制在位图上。内存设备环境在整个程序运行期间都被保留。在处理 WM_DESTROY 消息时,HELLOBIT 程序既删除位图,又删除内存设备环境。
HELLOBIT 程序里的菜单选项让你以实际大小横向和纵向重复显示位图,或者把位图拉伸到和程序客户区同样的大小,见图 14-10。可以看出,这种显示磅值文本的方法并不好!它仅仅是小字体的放大版,所有的锯齿都被放大了。
图 14-10 HELLOBIT 的显示
你或许会疑惑像 HELLOBIT 这样的程序是否需要处理 WM_DISPLAYCHANGE 消息。只要用户(或另一个应用程序)改变了视频显示的大小或者颜色深度,应用程序就会收到这个消息。颜色深度的改变有可能造成内存设备环境和视频设备环境不兼容。但是,这种情况不会出现,因为当视频模式发生改变时,Windows 会自动改变内存设备环境的彩色分辨率。选进内存设备环境的位图还是和原来一样,但这不会造成任何问题。
14.4.8 阴影位图
在内存设备环境(和位图)上的绘图技术是实现“阴影位图”的关键。阴影位图是包含所有在窗口客户区显示的内容的位图。这样 WM_PAINT 消息的处理就简化为一个简单的 BitBlt 操作。
阴影位图在画图程序里最有用。SKETCH 程序并不是一个最棒的画图程序,这不过是抛砖引玉罢了。
/*---------------------------------------------------
SKETCH.C -- Shadow Bitmap Demonstration
(c) Charles Petzold, 1998
---------------------------------------------------*/
#include <windows.h>
LRESULT CALLBACK WndProc(HWND, UINT, WPARAM, LPARAM);
int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevinstance,
PSTR szCmdLine, int iCmdShow)
{
static TCHAR szAppName[] = TEXT("Sketch");
HWND hwnd;
MSG msg;
WNDCLASS wndclass;
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("Sketch"),
WS_OVERLAPPEDWINDOW,
CW_USEDEFAULT, CW_USEDEFAULT,
CW_USEDEFAULT, CW_USEDEFAULT,
NULL, NULL, hInstance, NULL);
if (hwnd == NULL)
{
MessageBox(NULL, TEXT("Not enough memory to create bitmap!"),
szAppName, MB_ICONERROR);
return 0;
}
ShowWindow(hwnd, iCmdShow);
UpdateWindow(hwnd);
while (GetMessage(&msg, NULL, 0, 0))
{
TranslateMessage(&msg);
DispatchMessage(&msg);
}
return msg.wParam;
}
void GetLargestDisplayMode(int * pcxBitmap, int * pcyBitmap)
{
DEVMODE devmode;
int iModeNum = 0;
*pcxBitmap = *pcyBitmap = 0;
ZeroMemory(&devmode, sizeof(DEVMODE));
devmode.dmSize = sizeof(DEVMODE);
while (EnumDisplaySettings(NULL, iModeNum++, &devmode))
{
*pcxBitmap = max(*pcxBitmap, (int)devmode.dmPelsWidth);
*pcyBitmap = max(*pcyBitmap, (int)devmode.dmPelsHeight);
}
}
LRESULT CALLBACK WndProc(HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam)
{
static BOOL fLeftButtonDown, fRightButtonDown;
static HBITMAP hBitmap;
static HDC hdcMem;
static int cxBitmap, cyBitmap, cxClient, cyClient, xMouse, yMouse;
HDC hdc;
PAINTSTRUCT ps;
switch (message)
{
case WM_CREATE:
GetLargestDisplayMode(&cxBitmap, &cyBitmap);
hdc = GetDC(hwnd);
hBitmap = CreateCompatibleBitmap(hdc, cxBitmap, cyBitmap);
hdcMem = CreateCompatibleDC(hdc);
ReleaseDC(hwnd, hdc);
if (!hBitmap) // no memory for bitmap
{
DeleteDC(hdcMem);
return -1;
}
SelectObject(hdcMem, hBitmap);
PatBlt(hdcMem, 0, 0, cxBitmap, cyBitmap, WHITENESS);
return 0;
case WM_SIZE:
cxClient = LOWORD(lParam);
cyClient = HIWORD(lParam);
return 0;
case WM_LBUTTONDOWN:
if (!fRightButtonDown)
SetCapture(hwnd);
xMouse = LOWORD(lParam);
yMouse = HIWORD(lParam);
fLeftButtonDown = TRUE;
return 0;
case WM_LBUTTONUP:
if (fLeftButtonDown)
SetCapture(NULL);
fLeftButtonDown = FALSE;
return 0;
case WM_RBUTTONDOWN:
if (!fLeftButtonDown)
SetCapture(hwnd);
xMouse = LOWORD(lParam);
yMouse = HIWORD(lParam);
fRightButtonDown = TRUE;
return 0;
case WM_RBUTTONUP:
if (fRightButtonDown)
SetCapture(NULL);
fRightButtonDown = FALSE;
return 0;
case WM_MOUSEMOVE:
if (!fLeftButtonDown && !fRightButtonDown)
return 0;
hdc = GetDC(hwnd);
SelectObject(hdc,
GetStockObject(fLeftButtonDown ? BLACK_PEN : WHITE_PEN));
SelectObject(hdcMem,
GetStockObject(fLeftButtonDown ? BLACK_PEN : WHITE_PEN));
MoveToEx(hdc, xMouse, yMouse, NULL);
MoveToEx(hdcMem, xMouse, yMouse, NULL);
xMouse = (short)LOWORD(lParam);
yMouse = (short)HIWORD(lParam);
LineTo(hdc, xMouse, yMouse);
LineTo(hdcMem, xMouse, yMouse);
ReleaseDC(hwnd, hdc);
return 0;
case WM_PAINT:
hdc = BeginPaint(hwnd, &ps);
BitBlt(hdc, 0, 0, cxClient, cyClient, hdcMem, 0, 0, SRCCOPY);
EndPaint(hwnd, &ps);
return 0;
case WM_DESTROY:
DeleteDC(hdcMem);
DeleteObject(hBitmap);
PostQuitMessage(0);
return 0;
}
return DefWindowProc(hwnd, message, wParam, lParam);
}
要在 SKETCH 程序中画线,可以按下鼠标左键然后拖动鼠标。要擦除的话(更准确地说,是用白色的线条来覆盖),可以按鼠标右键并拖动鼠标。要清除整个窗口,你可以......好吧,你只能关掉程序,再打开它从头来过。图 14-12 用 SKETCH 程序向 Apple Macintosh 早期的广告表示了敬意。
图 14-12 SKETCH 的显示
阴影位图应该有多大呢?在这个程序中,它必须足够大以包含最大化的整个客户区。这个通过 GetSystemMetrics 很容易计算出来。但是,如果用户更改了显示设定,从而扩大了显示(相应也就扩大了最大化窗口)的尺寸,会出现什么结果呢?SKETCH 依靠 EnumDisplaySettings 函数的帮助实现了一个穷尽搜索的方案。这个函数利用 DEVMODE 结构返回了所有可能的视频显示模式配置信息。第一次调用时将 EnumDisplaySettings 的第二个参数置为 0,然后每个调用依次加 1,直到 EnumDisplaySettings 返回 FALSE 为止。
利用这些信息,SKETCH 程序能创建比当前显示区域大四倍以上的阴影位图,但是这个操作可能会需要好几兆的内存。也正因为此,SKETCH 程序要检查位图是否创建成功,如果失败,WM_CREATE 的处理就返回 -1。
SKETCH 程序捕获鼠标的左键或右键的按下操作,进而在处理 WM_MOUSEMOVE 消息时,在内存设备和客户区的设备环境上同时画线。如果画图的逻辑更加复杂,你可能需要一个函数来实现,并且调用两次——一次针对视频设备环境,另一次针对内存设备环境。
可以做一个有趣的实验:首先让 SKETCH 的窗口小于全屏的尺寸,按下鼠标,随便画一些什么,然后将鼠标拖出窗口的右下角。因为 SKETCH 在捕获鼠标动作,它会不断接收并处理 WM_MOUSEMOVE 消息。现在将窗口扩大,你会发现阴影位图包含了你在 SKETCH 窗口外绘制的内容。
14.4.9 在菜单中使用位图
你还可以在菜单项中使用位图。如果你一想到要在菜单上加上文件夹、剪贴板、回收站之类的图片就感到很麻烦的话,不要想着图片。应该想想看,对一个绘图程序来说,菜单位图时多么有用啊。或是想想在你的菜单中用上不同的字体和字号、不同的线宽、图案与颜色。
用于演示图形菜单项的程序叫做 GRAFMENU。该程序的顶级菜单见图 14-13。那些放大的方块字母是用 Visual C++ Developer Studio 生成的 40 * 16 像素的单色位图。在菜单中选择 FONT 会弹出一个子菜单,包含三个选项:Courier New、Arial 和 Times New Roman。它们是标准的 Windows TrueType 字体。并且,从图 14-14 中可以看出,这三个菜单项是按照各自的字体来显示的。这些位图时程序通过内存设备环境来创建的。
图 14-13 GRAFMENU 程序的顶级菜单
最后,下拉系统菜单里包含“Help”这个词,用于显示一些版主信息,这个词有些夸张,好像在反映一个新用户是多么绝望地需要帮助。(见图 14-15)这个 64 * 64 像素的单色位图是在 Developer Studio 中创建的。
图 14-14 GRAFMENU 程序的 FONT 子菜单
图 14-15 GRAFMENU 程序的系统菜单
GRAFMENU 程序以及在 Developer Studio 中创建的四个位图都列在图 14-16 中了。
/*---------------------------------------------------
GRAFMENU.C -- Demonstration Bitmap Menu Items
(c) Charles Petzold, 1998
---------------------------------------------------*/
#include <windows.h>
#include "resource.h"
LRESULT CALLBACK WndProc (HWND, UINT, WPARAM, LPARAM);
void AddHelpToSys (HINSTANCE, HWND);
HMENU CreateMyMenu (HINSTANCE);
HBITMAP StretchBitmap (HBITMAP);
HBITMAP GetBitmapFont (int);
void DeleteAllBitmaps (HWND);
TCHAR szAppName[] = TEXT("Sketch");
int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevinstance,
PSTR szCmdLine, int iCmdShow)
{
HWND hwnd;
MSG msg;
WNDCLASS wndclass;
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("Bitmap Menu Demostration"),
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;
}
LRESULT CALLBACK WndProc(HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam)
{
HMENU hMenu;
static int iCurrentFont = IDM_FONT_COUR;
switch (message)
{
case WM_CREATE:
AddHelpToSys(((LPCREATESTRUCT)lParam)->hInstance, hwnd);
hMenu = CreateMyMenu(((LPCREATESTRUCT)lParam)->hInstance);
SetMenu(hwnd, hMenu);
CheckMenuItem(hMenu, iCurrentFont, MF_CHECKED);
return 0;
case WM_SYSCOMMAND:
switch (LOWORD(wParam))
{
case IDM_HELP:
MessageBox(hwnd, TEXT("Help not yet implemented!"),
szAppName, MB_OK | MB_ICONEXCLAMATION);
return 0;
}
break;
case WM_COMMAND:
switch (LOWORD(wParam))
{
case IDM_FILE_NEW:
case IDM_FILE_OPEN:
case IDM_FILE_SAVE:
case IDM_FILE_SAVE_AS:
case IDM_EDIT_UNDO:
case IDM_EDIT_CUT:
case IDM_EDIT_COPY:
case IDM_EDIT_PASTE:
case IDM_EDIT_CLEAR:
MessageBeep(0);
return 0;
case IDM_FONT_COUR:
case IDM_FONT_ARIAL:
case IDM_FONT_TIMES:
hMenu = GetMenu(hwnd);
CheckMenuItem(hMenu, iCurrentFont, MF_UNCHECKED);
iCurrentFont = LOWORD(wParam);
CheckMenuItem(hMenu, iCurrentFont, MF_CHECKED);
return 0;
}
break;
case WM_DESTROY:
DeleteAllBitmaps(hwnd);
PostQuitMessage(0);
return 0;
}
return DefWindowProc(hwnd, message, wParam, lParam);
}
/*----------------------------------------------------------
AddHelpToSys: Adds bitmap Help item to system menu
----------------------------------------------------------*/
void AddHelpToSys(HINSTANCE hInstance, HWND hwnd)
{
HBITMAP hBitmap;
HMENU hMenu;
hMenu = GetSystemMenu(hwnd, FALSE);
hBitmap = StretchBitmap(LoadBitmap(hInstance, TEXT("BitmapHelp")));
AppendMenu(hMenu, MF_SEPARATOR, 0, NULL);
AppendMenu(hMenu, MF_BITMAP, IDM_HELP, (PTSTR)(LONG)hBitmap);
}
/*----------------------------------------------------------
CreateMyMenu: Assembles menu from components
----------------------------------------------------------*/
HMENU CreateMyMenu(HINSTANCE hInstance)
{
HBITMAP hBitmap;
HMENU hMenu, hMenuPopup;
int i;
hMenu = CreateMenu();
hMenuPopup = LoadMenu(hInstance, TEXT("MenuFile"));
hBitmap = StretchBitmap(LoadBitmap(hInstance, TEXT("BitmapFile")));
AppendMenu(hMenu, MF_BITMAP | MF_POPUP, (int)hMenuPopup, (PTSTR)(LONG)hBitmap);
hMenuPopup = LoadMenu(hInstance, TEXT("MenuEdit"));
hBitmap = StretchBitmap(LoadBitmap(hInstance, TEXT("BitmapEdit")));
AppendMenu(hMenu, MF_BITMAP | MF_POPUP, (int)hMenuPopup, (PTSTR)(LONG)hBitmap);
hMenuPopup = CreateMenu();
for (i = 0; i < 3; ++i)
{
hBitmap = GetBitmapFont(i);
AppendMenu(hMenuPopup, MF_BITMAP, IDM_FONT_COUR + i, (PTSTR)(LONG)hBitmap);
}
hBitmap = StretchBitmap(LoadBitmap(hInstance, TEXT("BitmapFont")));
AppendMenu(hMenu, MF_BITMAP | MF_POPUP, (int)hMenuPopup, (PTSTR)(LONG)hBitmap);
return hMenu;
}
/*----------------------------------------------------------
StretchBitmap: Scales bitmap to display resolution
----------------------------------------------------------*/
HBITMAP StretchBitmap(HBITMAP hBitmap1)
{
BITMAP bm1, bm2;
HBITMAP hBitmap2;
HDC hdc, hdcMem1, hdcMem2;
int cxChar, cyChar;
// Get the width and height of a system font character
cxChar = LOWORD(GetDialogBaseUnits());
cyChar = HIWORD(GetDialogBaseUnits());
// Create 2 memory DCs compatible with the display
hdc = CreateIC(TEXT("DISPLAY"), NULL, NULL, NULL);
hdcMem1 = CreateCompatibleDC(hdc);
hdcMem2 = CreateCompatibleDC(hdc);
DeleteDC(hdc);
// Get the dimensions of the bitmap to be stretched
GetObject(hBitmap1, sizeof(BITMAP), (PTSTR)&bm1);
// Scale these dimensions based on the system font size
bm2 = bm1;
bm2.bmWidth = (cxChar * bm2.bmWidth) / 4;
bm2.bmHeight = (cyChar * bm2.bmHeight) / 8;
bm2.bmWidthBytes = ((bm2.bmWidth + 15) / 16) * 2;
// Create a new bitmap of larger size
hBitmap2 = CreateBitmapIndirect(&bm2);
// Select the bitmaps in the memory DCs and do a StretchBlt
SelectObject(hdcMem1, hBitmap1);
SelectObject(hdcMem2, hBitmap2);
StretchBlt(hdcMem2, 0, 0, bm2.bmWidth, bm2.bmHeight,
hdcMem1, 0, 0, bm1.bmWidth, bm1.bmHeight, SRCCOPY);
// Clean up
DeleteDC(hdcMem1);
DeleteDC(hdcMem2);
DeleteObject(hBitmap1);
return hBitmap2;
}
/*----------------------------------------------------------
GetBitmapFont: Creates bitmaps with font names
----------------------------------------------------------*/
HBITMAP GetBitmapFont(int i)
{
static TCHAR * szFaceName[3] = { TEXT("Courier New"), TEXT("Arial"),
TEXT("Times New Roman") };
HBITMAP hBitmap;
HDC hdc, hdcMem;
HFONT hFont;
SIZE size;
TEXTMETRIC tm;
hdc = CreateIC(TEXT("DISPLAY"), NULL, NULL, NULL);
GetTextMetrics(hdc, &tm);
hdcMem = CreateCompatibleDC(hdc);
hFont = CreateFont(2 * tm.tmHeight, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
szFaceName[i]);
hFont = (HFONT)SelectObject(hdcMem, hFont);
GetTextExtentPoint32(hdcMem, szFaceName[i], lstrlen(szFaceName[i]), &size);
hBitmap = CreateBitmap(size.cx, size.cy, 1, 1, NULL);
SelectObject(hdcMem, hBitmap);
TextOut(hdcMem, 0, 0, szFaceName[i], lstrlen(szFaceName[i]));
DeleteObject(SelectObject(hdcMem, hFont));
DeleteDC(hdcMem);
DeleteDC(hdc);
return hBitmap;
}
/*----------------------------------------------------------
DeleteAllBitmaps: Deletes all the bitmaps in the menu
----------------------------------------------------------*/
void DeleteAllBitmaps(HWND hwnd)
{
HMENU hMenu;
int i;
MENUITEMINFO mii = { sizeof(MENUITEMINFO), MIIM_SUBMENU | MIIM_TYPE };
// Delete Help bitmap on system menu
hMenu = GetSystemMenu(hwnd, FALSE);
GetMenuItemInfo(hMenu, IDM_HELP, FALSE, &mii);
DeleteObject((HBITMAP)mii.dwTypeData);
// Delete top-level menu bitmaps
hMenu = GetMenu(hwnd);
for (i = 0; i < 3; ++i)
{
GetMenuItemInfo(hMenu, i, TRUE, &mii);
DeleteObject((HBITMAP)mii.dwTypeData);
}
// Delete bitmap items on Font menu
hMenu = mii.hSubMenu;
for (i = 0; i < 3; ++i)
{
GetMenuItemInfo(hMenu, i, TRUE, &mii);
DeleteObject((HBITMAP)mii.dwTypeData);
}
}
GRAFMENU.RC (excerpts)
// Microsoft Visual C++ generated resource script.
//
#include "resource.h"
/
//
// Menu
//
MENUFILE MENU
BEGIN
MENUITEM "&New", IDM_FILE_NEW
MENUITEM "&Open...", IDM_FILE_OPEN
MENUITEM "&Save", IDM_FILE_SAVE
MENUITEM "Save &As...", IDM_FILE_SAVE_AS
END
MENUEDIT MENU
BEGIN
MENUITEM "&Undo", IDM_EDIT_UNDO
MENUITEM SEPARATOR
MENUITEM "Cu&t", IDM_EDIT_CUT
MENUITEM "&Copy", IDM_EDIT_COPY
MENUITEM "&Paste", IDM_EDIT_PASTE
MENUITEM "De&lete", IDM_EDIT_CLEAR
END
/
//
// Bitmap
//
BITMAPFONT BITMAP "Fontlabl.bmp"
BITMAPHELP BITMAP "Bighelp.bmp"
BITMAPEDIT BITMAP "Editlabl.bmp"
BITMAPFILE BITMAP "Filelabl.bmp"
RESOURCE.H (excerpts)
// Microsoft Visual C++ 生成的包含文件。
// 供 GrafMenu.rc 使用
//
#define IDM_FONT_COUR 101
#define IDM_FONT_ARIAL 102
#define IDM_FONT_TIMES 103
#define IDM_HELP 104
#define IDM_EDIT_UNDO 40005
#define IDM_EDIT_CUT 40006
#define IDM_EDIT_COPY 40007
#define IDM_EDIT_PASTE 40008
#define IDM_EDIT_CLEAR 40009
#define IDM_FILE_NEW 40010
#define IDM_FILE_OPEN 40011
#define IDM_FILE_SAVE 40012
#define IDM_FILE_SAVE_AS 40013
EDITLABLE.BMP
FILELABL.BMP
FONTLABL.BMP
BIGHELP.BMP
图 14-16 GRAFMENU 程序
向菜单中插入位图,要用到 AppendMenu 函数或 InsertMenu 函数。位图有两个来源:你可以在 Visual C++ Developer Studio 中创建一个位图,在资源脚本中加上位图文件,在程序里用 LoadBitmap 将位图资源载入内存。然后再调用 AppendMenu 或 InsertMenu 把它加载到菜单上。不过,这个方法有个问题。用到的位图不一定适合所有可能的显示分辨率以及纵横比,你可能要把载入的位图进行拉伸才可以正确显示。另一个方法是直接在程序中创建位图,将其选入内存设备环境,在上面绘图,然后把它加载到菜单上。
GRAFMENU 程序中的 GetBitmapFont 函数接收一个参数,取值可以是 0、1 或 2,并返回一个指向位图的句柄。返回的位图中包含用各自字体显示的字符串“Courier New”、“Arial”或“Timer New Roman”,大小是正常系统字体的两倍。下面我们看看 GetBitmapFont 是如何做的。(这里的代码和 GRAMENU.C 文件中的不完全一样。为了清楚起见,我用 Arial 字体对应的值替换了对 szFaceName 数组的引用。)
第一步是通过 TEXTMETRIC 结构来获取系统字体的大小,并创建一个和屏幕兼容的内存设备环境:
hdc = CreateIC(TEXT("DISPLAY"), NULL, NULL, NULL);
GetTextMetrics(hdc, &tm);
hdcMem = CreateCompatibleDC(hdc);
CreateFont 函数创建一个逻辑字体,高度是系统字体的两倍,字体名为“Arial”:
hFont = CreateFont(2 * tm.tmHeight, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
TEXT("Arial"));
将这个字体选入内存设备环境,同时保存默认字体的句柄:
hFont = (HFONT)SelectObject(hdcMem, hFont);
现在,如果我们在内存设备环境上写字,Windows 就会使用刚刚在设备环境中选定的 TrueType Arial 字体。
但这个内存设备环境最开始只有一个像素的单色设备表面。我们需要创建一个足够大的位图来容纳要显示的文本。可以用 GetTextExtentPoint32 函数来得到文本的大小,根据这个大小通过 CreateBitmap 创建位图:
GetTextExtentPoint32(hdcMem, TEXT("Arial"), 5, &size);
hBitmap = CreateBitmap(size.cx, size.cy, 1, 1, NULL);
SelectObject(hdcMem, hBitmap);
现在这个设备环境的单色显示表面的大小和文本的大小完全相同了。我们可以将文本输出到上面:
TextOut(hdcMem, 0, 0, TEXT("Arial"), 5);
除了清理的部分,我们基本上都做完了。要进行清理,我们可以用 SelectObject 将系统字体(通过 hFont 句柄)选回到设备环境,并删除 SelectObject 返回的指向之前的 Arial 字体的句柄:
DeleteObject(SelectObject(hdcMem, hFont));
之后,我们可以将两个设备环境都删除:
DeleteDC(hdcMem);
DeleteDC(hdc);
这样,我们就有了一个位图,上面是用 Arial 字体写的“Arial”。
内存设备环境也可以帮助我们将字体大小根据不同的显示分辨率和纵横比进行调整。在 GRAFMENU 程序中,我创建了四个位图,大小对于一个高 8 像素、宽 4 像素的系统字体的显示正好合适。对于其他的系统字体尺寸,位图要有缩放。这在 GRAFMENU 的StretchBitmap 函数中实现。
首先我们要取得屏幕的设备环境和系统字体的尺寸,并创建两个内存设备环境:
hdc = CreateIC(TEXT("DISPLAY"), NULL, NULL, NULL);
GetTextMetrics(hdc, &tm);
hdcMem1 = CreateCompatibleDC(hdc);
hdcMem2 = CreateCompatibleDC(hdc);
DeleteDC(hdc);
传入的位图句柄是 hBitmap1。程序可以通过 GetObject 获得该位图的大小:
GetObject(hBitmap1, sizeof(BITMAP), (PTSTR)&bm1);
这会把位图的大小复制到 bm1 中,bm1 是 BITMAP 类型。将 bm1 的值赋给 bm2 一份,然后 bm2 的某些字段要根据系统字体的大小做调整:
bm2 = bm1;
bm2.bmWidth = (tm.tmAveCharWidth * bm2.bmWidth) / 4;
bm2.bmHeight = (tm.tmHeight * bm2.bmHeight) / 8;
bm2.bmWidthBytes = ((bm2.bmWidth + 15) / 16) * 2;
然后,基于这个改动后的大小创建一个新位图,赋给句柄 hBitmap2:
hBitmap2 = CreateBitmapIndirect(&bm2);
然后可以将这个两个位图选入两个内存设备环境:
SelectObject(hdcMem1, hBitmap1);
SelectObject(hdcMem2, hBitmap2);
我们要将第一个位图复制到第二个位图中并完成缩放。这是通过调用 StretchBlt 完成的:
StretchBlt(hdcMem2, 0, 0, bm2.bmWidth, bm2.bmHeight,
hdcMem1, 0, 0, bm1.bmWidth, bm1.bmHeight, SRCCOPY);
现在第二个位图就有了合适的大小。我们将在菜单上使用它。而如下所示,清理工作也很简单:
DeleteDC(hdcMem1);
DeleteDC(hdcMem2);
DeleteObject(hBitmap1);
在 GRAFMENU 程序中,CreateMyMenu 函数在建立菜单时,使用了 StretchBitmap 和 GetBitmapFont 函数。GRAFMENU 在资源脚本中已经定义了两个菜单。它们用于 File 和 Edit 选项的弹出菜单。函数首先取得一个指向空菜单的句柄:
hMenu = CreateMenu();
从资源脚本加载 File 的弹出菜单(包含 New、Open、Save 和 Save As 这四个菜单项):
hMenuPopup = LoadMenu(hInstance, TEXT("MenuFile"));
带有文字“File”的位图也从资源脚本中加载,并通过 StretchBitmap 缩放:
hBitmap = StretchBitmap(LoadBitmap(hInstance, TEXT("BitmapFile")));
该位图句柄和弹出菜单的句柄都作为 AppendMenu 的参数传入:
AppendMenu(hMenu, MF_BITMAP | MF_POPUP, (int)hMenuPopup, (PTSTR)(LONG)hBitmap);
Edit 菜单也遵循同样的流程:
hMenuPopup = LoadMenu(hInstance, TEXT("MenuEdit"));
hBitmap = StretchBitmap(LoadBitmap(hInstance, TEXT("BitmapEdit")));
AppendMenu(hMenu, MF_BITMAP | MF_POPUP, (int)hMenuPopup, (PTSTR)(LONG)hBitmap);
三种字体的弹出菜单利用 GetBitmapFont 函数创建:
hMenuPopup = CreateMenu();
for (i = 0; i < 3; ++i)
{
hBitmap = GetBitmapFont(i);
AppendMenu(hMenuPopup, MF_BITMAP, IDM_FONT_COUR + i, (PTSTR)(LONG)hBitmap);
}
然后将该弹出菜单加入菜单中:
hBitmap = StretchBitmap(LoadBitmap(hInstance, TEXT("BitmapFont")));
AppendMenu(hMenu, MF_BITMAP | MF_POPUP, (int)hMenuPopup, (PTSTR)(LONG)hBitmap);
这样一来,窗口菜单就准备好了,WndProc 通过调用 SetMenu 将其设置为窗口的菜单。
GRAFMENU 还在 AddHelpToSys 函数中更改了系统菜单。该函数首先获得一个指向系统菜单的句柄:
hMenu = GetSystemMenu(hwnd, FALSE);
然后载入“Help”位图,并把它缩放到合适的尺寸:
hBitmap = StretchBitmap(LoadBitmap(hInstance, TEXT("BitmapHelp")));
之后把一个分隔线和拉伸的位图加入系统菜单:
<pre name="code" class="cpp">AppendMenu(hMenu, MF_SEPARATOR, 0, NULL);
AppendMenu(hMenu, MF_BITMAP, IDM_HELP, (PTSTR)(LONG)hBitmap);
GRAFMENU 专门用了一个函数来做点清理工作,在程序结束前删除所有的位图。
使用位图时,需要注意以下几点。
在顶级菜单中,Windows 会调整菜单栏的高度来适应最高的位图,其他的位图(或者字符串)和菜单上沿顶端对齐。如果把一个位图放入顶级菜单,那么用 SM_CYMENU 调用 GetSystemMetrics 得到的菜单栏高度值不再有意义。
在使用 GRAFMENU 程序时,会发现可以在带位图的弹出菜单中使用选中标记,不过它是正常大小。如果觉得不妥,可以用 SetMenuItemBitmaps 创建一个自定义的。
还可以用“自绘“菜单来实现非文字(或者非系统字体文字)菜单,这也是一种方法。
菜单的键盘接口也是一个问题。当菜单包含文本时,Windows 会自动添加键盘接口。可以用 Alt 键加文本中的某个字符来选择菜单项。但是,一旦在菜单中加入位图,就,没有这个键盘接口了。即使位图写了明确的内容,Windows 也会对其视而不见。
这也正是 WM_MENUCHAR 消息的用武之地。在按下 Alt 键以及没有映射到任何菜单项的某个字母键时,Windows 会向应用程序窗口过程发送一个 WM_MENUCHAR 消息。GRAFMENU 应该处理这个消息,查看 wParam 的值(被按下的键的 ASCII 码)。如果和某个菜单项对应,就要向 Windows 返回一个双字,并将高位字设为 2,将低位字设成对应设成对应菜单项的索引。其余的事情可由 Windows 处理。
14.4.10 非矩形的位图图像
位图总是矩形的,但是它并不一定要显示成矩形形状。例如,设想一下你有一个矩形的位图,但是希望把它显示成一个椭圆形。
乍一看似乎很容易。把图片装入 Visual C++ Developer Studio 或者 Windows 画图程序(或者更昂贵的软件),用白色填满图片外面的区域。这样一来,便得到一个椭圆的图片,在椭圆外面是白色的。这种方法是行得通的——但仅限于在白色背景下显示这个位图。如果在任何其他颜色的背景下显示,你的椭圆形图片将会显示在一个白色矩形之上,而这个白色矩形显示在背景色上。这看上去并不好。
有一个很常见的技巧来解决这个问题。这个方法使用”掩码“位图和一些光栅操作。掩码是一个单色位图,它的大小和你要显示的矩形位图的大小相同。每个掩码像素对应要显示的位图上的一个像素。要显示出来的像素对应的掩码像素置为 1(白色),反之置为 0(黑色)以让目标背景透出来。(如果使用不同的光栅算法,相应的值可能会和这个相反。)。
让我们通过下面的 BITMASK 程序来看看它是怎样实际工作的。
/*------------------------------------------
BITMASK.C -- Bitmap Maksing Demonstration
(c) Charles Petzold, 1998
------------------------------------------*/
#include <windows.h>
LRESULT CALLBACK WndProc(HWND, UINT, WPARAM, LPARAM);
int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevinstance,
PSTR szCmdLine, int iCmdShow)
{
static TCHAR szAppName[] = TEXT("BitMask");
HWND hwnd;
MSG msg;
WNDCLASS wndclass;
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(LTGRAY_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("Bitmap Masking 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;
}
LRESULT CALLBACK WndProc(HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam)
{
static HBITMAP hBitmapImag, hBitmapMask;
static HINSTANCE hInstance;
static int cxClient, cyClient, cxBitmap, cyBitmap;
BITMAP bitmap;
HDC hdc, hdcMemImag, hdcMemMask;
int x, y;
PAINTSTRUCT ps;
switch (message)
{
case WM_CREATE:
hInstance = ((LPCREATESTRUCT)lParam)->hInstance;
// Load the original image and get its size
hBitmapImag = LoadBitmap(hInstance, TEXT("Matthew"));
GetObject(hBitmapImag, sizeof(BITMAP), &bitmap);
cxBitmap = bitmap.bmWidth;
cyBitmap = bitmap.bmHeight;
// Select the original image into a memory DC
hdcMemImag = CreateCompatibleDC(NULL);
SelectObject(hdcMemImag, hBitmapImag);
// Create the monochrome mask bitmap and memory DC
hBitmapMask = CreateBitmap(cxBitmap, cyBitmap, 1, 1, NULL);
hdcMemMask = CreateCompatibleDC(NULL);
SelectObject(hdcMemMask, hBitmapMask);
// Color the mask bitmap black with a white ellipse
SelectObject(hdcMemMask, GetStockObject(BLACK_BRUSH));
Rectangle(hdcMemMask, 0, 0, cxBitmap, cyBitmap);
SelectObject(hdcMemMask, GetStockObject(WHITE_BRUSH));
Ellipse(hdcMemMask, 0, 0, cxBitmap, cyBitmap);
// Mask the original image
BitBlt(hdcMemImag, 0, 0, cxBitmap, cyBitmap,
hdcMemMask, 0, 0, SRCAND);
DeleteDC(hdcMemImag);
DeleteDC(hdcMemMask);
return 0;
case WM_SIZE:
cxClient = LOWORD(lParam);
cyClient = HIWORD(lParam);
return 0;
case WM_PAINT:
hdc = BeginPaint(hwnd, &ps);
// Select bitmaps into memory DCs
hdcMemImag = CreateCompatibleDC(hdc);
SelectObject(hdcMemImag, hBitmapImag);
hdcMemMask = CreateCompatibleDC(hdc);
SelectObject(hdcMemMask, hBitmapMask);
// Center image
x = (cxClient - cxBitmap) / 2;
y = (cyClient - cyBitmap) / 2;
// Do the bitblts
BitBlt(hdc, x, y, cxBitmap, cyBitmap, hdcMemMask, 0, 0, 0x220326);
BitBlt(hdc, x, y, cxBitmap, cyBitmap, hdcMemImag, 0, 0, SRCPAINT);
DeleteDC(hdcMemImag);
DeleteDC(hdcMemMask);
EndPaint(hwnd, &ps);
return 0;
case WM_DESTROY:
DeleteObject(hBitmapImag);
DeleteObject(hBitmapMask);
PostQuitMessage(0);
return 0;
}
return DefWindowProc(hwnd, message, wParam, lParam);
}
BITMASK.RC (excerpts)
// Microsoft Visual C++ generated resource script.
//
#include "resource.h"
/
//
// Bitmap
//
MATTHEW BITMAP "Matthew.bmp"
资源脚本中引用的 MATTHEW.BMP 文件中是我侄子的一张黑白电子照片,宽 200 像素,高 320 像素,每像素 8 位。当然,BITMASK 程序使用的这个文件可以是任何内容。
注意,BITMASK 程序使用了一个浅灰色的画刷给它的窗口背景上色。这保证我们能正确地屏蔽位图,而不是把它的一部分涂成白色。
让我们看一下 WM_CRAETE 的处理过程。BITMASK 用 LoadBitmap 函数在变量 hBitmapImag 中取得原始图像的句柄。GetObject 函数取得位图的宽和高。然后将位图的句柄选入内存设备环境,得到的句柄是 hdcMemImag。
接下来,程序创建一个和原始图像一样大小的单色位图,相应句柄被存储在 hBitmapMask 中,并被选入内存设备环境,该环境句柄是 hdcMemMask。通过 GDI 函数在内存设备环境中对这个掩码位图用黑色背景着色,并画上一个白色的椭圆:
SelectObject(hdcMemMask, GetStockObject(BLACK_BRUSH));
Rectangle(hdcMemMask, 0, 0, cxBitmap, cyBitmap);
SelectObject(hdcMemMask, GetStockObject(WHITE_BRUSH));
Ellipse(hdcMemMask, 0, 0, cxBitmap, cyBitmap);
由于它是一个单色位图,所以黑色的区域其实是 0,白色的部分是 1。
然后通过 BitBlt 将掩码应用在原始图像上:
BitBlt(hdcMemImag, 0, 0, cxBitmap, cyBitmap,
hdcMemMask, 0, 0, SRCAND);
光栅操作 SRCAND 对源(掩码位图)和目标(原始图像)的对应位进行一个按位“与”操作。目标图上对应掩码图中的白色部分被保留,在掩码图中为黑色的部分,目标图上也变成黑色。结果就是在原始的图片上加了一个外部是黑色的椭圆。
下面让我们来看看 WM_PAINT 的处理过程。被修改了的图像位图和掩码位图都被选入了内存设备环境。然后使用两个 BitBlt 调用来变这个“戏法”。首先对窗口做一次掩码图的 BitBlt:
BitBlt(hdc, x, y, cxBitmap, cyBitmap, hdcMemMask, 0, 0, 0x220326);
这里使用了一个没有预定义名称的光栅操作,
应用的逻辑操作是 D & ~S(目标和取反的源进行按位与操作)。还记得么,这里的源(掩码位图)是一个被黑色(0)环绕的白色椭圆(1)。以上的光栅操作将其取反得到一个被白色包围的黑色椭圆,然后与目标(也就是窗口)进行一个按位的“与”操作。目标和 1 进行与操作会保持不变,和 0 与则变成黑色。于是,这个 BitBlt 操作过后窗口上会画出一个黑色的椭圆。
第二个 BitBlt 将要显示的图像画在窗口上:
BitBlt(hdc, x, y, cxBitmap, cyBitmap, hdcMemImag, 0, 0, SRCPAINT);
这个光栅操作在源和目标之间进行一个“或”操作。源图的外部是黑色的,于是目标的对应部分被保留。在椭圆内部,目标是黑色的,所以源图的对应部分被原封不动地复制过来。最终结果如图 14-18 所示。
这里有一些要注意的地方。
有时你可能会用到很复杂的掩码图——例如,如果打算抠掉源图上面的整个背景部分,可能需要在其他绘图程序中手工地建立掩码图,并存成一个文件来使用。
如果是专门为 Windows NT 编程,则可以使用 MaskBlt 函数,它使用更少的函数调用来完成和 BITMASK 程序类似的功能。Windows NT 包含 Windows 98 没有的另一个和 BitBlt 类似的函数。这个函数是 PlgBlt (parallelogram blt,平行四边形 blt),它可以旋转或者斜角拉伸。
图 14-18 BITMASK 程序的显示
如果在计算机上运行 BITMASK 时只看到了黑色、白色和一些灰色的阴影,可能是因为是在 16 色或者 256 色的显示模式下。在 16 色模式下,就没有办法改进了,不过在 256 色下,可以更改调色板来显示不同的灰度级别。在第 16 章将有具体步骤。
14.4.11 简单的动画效果
由于显示小的位图速度非常快,所以你可以将其和 Windows 计时器相结合作出一些很简单的动画效果。
没错,现在是写弹球程序的时候啦!
下面给出的 BOUNCE 程序,构造了一个在窗口客户区中四处弹跳的小球。小球本身是一个位图,程序使用定时器来控制小球动作。程序首先创建了小球的位图,选入内存设备环境,然后就是一系列简单的 GDI 函数调用。程序利用 BitBlt 在内存设备环境上画出小球。
/*------------------------------------------
BOUNCE.C -- Bouncing Ball Program
(c) Charles Petzold, 1998
------------------------------------------*/
#include <windows.h>
#define ID_TIMER 1
LRESULT CALLBACK WndProc(HWND, UINT, WPARAM, LPARAM);
int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevinstance,
PSTR szCmdLine, int iCmdShow)
{
static TCHAR szAppName[] = TEXT("Bounce");
HWND hwnd;
MSG msg;
WNDCLASS wndclass;
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("Bouncing Ball"),
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;
}
LRESULT CALLBACK WndProc(HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam)
{
static HBITMAP hBitmap;
static int cxClient, cyClient, xCenter, yCenter, cxTotal, cyTotal,
cxRadius, cyRadius, cxMove, cyMove, xPixel, yPixel;
HBRUSH hBrush;
HDC hdc, hdcMem;
int iScale;
switch (message)
{
case WM_CREATE:
hdc = GetDC(hwnd);
xPixel = GetDeviceCaps(hdc, ASPECTX);
yPixel = GetDeviceCaps(hdc, ASPECTY);
ReleaseDC(hwnd, hdc);
SetTimer(hwnd, ID_TIMER, 50, NULL);
return 0;
case WM_SIZE:
xCenter = (cxClient = LOWORD(lParam)) / 2;
yCenter = (cyClient = HIWORD(lParam)) / 2;
iScale = min(cxClient * xPixel, cyClient * yPixel) / 16;
cxRadius = iScale / xPixel;
cyRadius = iScale / yPixel;
cxMove = max(1, cxRadius / 2);
cyMove = max(1, cyRadius / 2);
cxTotal = 2 * (cxRadius + cxMove);
cyTotal = 2 * (cyRadius + cyMove);
if (hBitmap)
DeleteObject(hBitmap);
hdc = GetDC(hwnd);
hdcMem = CreateCompatibleDC(hdc);
hBitmap = CreateCompatibleBitmap(hdc, cxTotal, cyTotal);
ReleaseDC(hwnd, hdc);
SelectObject(hdcMem, hBitmap);
Rectangle(hdcMem, -1, -1, cxTotal + 1, cyTotal + 1);
hBrush = CreateHatchBrush(HS_DIAGCROSS, 0L);
SelectObject(hdcMem, hBrush);
SetBkColor(hdcMem, RGB(255, 0, 255));
Ellipse(hdcMem, cxMove, cyMove, cxTotal - cxMove, cyTotal - cyMove);
DeleteDC(hdcMem);
DeleteObject(hBrush);
return 0;
case WM_TIMER:
if (!hBitmap)
break;
hdc = GetDC(hwnd);
hdcMem = CreateCompatibleDC(hdc);
SelectObject(hdcMem, hBitmap);
BitBlt(hdc, xCenter - cxTotal / 2,
yCenter - cyTotal / 2, cxTotal, cyTotal,
hdcMem, 0, 0, SRCCOPY);
ReleaseDC(hwnd, hdc);
DeleteDC(hdcMem);
xCenter += cxMove;
yCenter += cyMove;
if ((xCenter + cxRadius >= cxClient) || (xCenter - cxRadius <= 0))
cxMove = -cxMove;
if ((yCenter + cyRadius >= cyClient) || (yCenter - cyRadius <= 0))
cyMove = -cyMove;
return 0;
case WM_DESTROY:
if (hBitmap)
DeleteObject(hBitmap);
KillTimer(hwnd, ID_TIMER);
PostQuitMessage(0);
return 0;
}
return DefWindowProc(hwnd, message, wParam, lParam);
}
每当收到一个 WM_SIZE 消息,BOUNCE 程序就会重新构造小球。这个过程需要一个和视频显示兼容的内存设备环境:
hdcMem = CreateCompatibleDC(hdc);
小球的直径设置为客户区高、宽较小一个的十六分之一。不过,程序构造的位图比球要大:在图的四边,位图比求的尺寸要多出球半径的一半:
hBitmap = CreateCompatibleBitmap(hdc, cxTotal, cyTotal);
当这个位图被选入内存设备环境之后,整个图的背景用白色填满:
Rectangle(hdcMem, -1, -1, xTotal + 1, yTotal + 1);
这些奇怪的坐标会造成矩形的边框被绘制在位图之外。一个对角斜线画刷会被选入内存设备环境,小球被画在位图的中心:
Ellipse(hdcMem, cxMove, cyMove, cxTotal - cxMove, cyTotal - cyMove);
当球移动时,球周围的边空会擦除球的前一个图像。在新位置重绘只需要用 SRCCOPY 光栅操作代码调用一次 BitBlt:
BitBlt(hdc, xCenter - cxTotal / 2, yCenter - cyTotal / 2, cxTotal, cyTotal,
hdcMem, 0, 0, SRCCOPY);
BOUNCE 程序展示了在显示器上移动图像的最简单的方式,但是这个方法并不能满足通常的需求。如果你对动画感兴趣,可以深入研究一些其他光栅操作的代码(如 SRCINVERT),这些代码可以在源和目标图片之间进行异或运算。其他的动画技术与 Windows 调色板(以及 AnimatePalette 函数)和 CreateDIBSection 函数有关。如果想要做更复杂的动画,可能需要放弃 GDI 而研究一下 DirectX 接口。
14.4.12 窗口以外的位图
SCRAMBLE 程序是一个很粗糙的程序,也许我不该给大家看。不过,它展示了一些很有趣的技术,它使用一个内存设备环境作为 BitBlt 操作的临时存储区,将显示的矩形内容进行两两交换。
/*-------------------------------------------------
SCRAMBLE.C -- Scramble (and Unscramble) Screen
(c) Charles Petzold, 1998
---------------------------------------------------*/
#include <windows.h>
#define NUM 300
int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevinstance,
PSTR szCmdLine, int iCmdShow)
{
static int iKeep[NUM][4];
HDC hdcScr, hdcMem;
int cx, cy;
HBITMAP hBitmap;
HWND hwnd;
int i, j, x1, y1, x2, y2;
if (LockWindowUpdate(hwnd = GetDesktopWindow()))
{
hdcScr = GetDCEx(hwnd, NULL, DCX_CACHE | DCX_LOCKWINDOWUPDATE);
hdcMem = CreateCompatibleDC(hdcScr);
cx = GetSystemMetrics(SM_CXSCREEN) / 10;
cy = GetSystemMetrics(SM_CYSCREEN) / 10;
hBitmap = CreateCompatibleBitmap(hdcScr, cx, cy);
SelectObject(hdcMem, hBitmap);
srand((int)GetCurrentTime());
for (i = 0; i < 2; i++)
for (j = 0; j < NUM; j++)
{
if (i == 0)
{
iKeep[j][0] = x1 = cx * (rand() % 10);
iKeep[j][1] = y1 = cy * (rand() % 10);
iKeep[j][2] = x2 = cx * (rand() % 10);
iKeep[j][3] = y2 = cy * (rand() % 10);
}
else
{
x1 = iKeep[NUM - 1 - j][0];
y1 = iKeep[NUM - 1 - j][1];
x2 = iKeep[NUM - 1 - j][2];
y2 = iKeep[NUM - 1 - j][3];
}
BitBlt(hdcMem, 0, 0, cx, cy, hdcScr, x1, y1, SRCCOPY);
BitBlt(hdcScr, x1, y1, cx, cy, hdcScr, x2, y2, SRCCOPY);
BitBlt(hdcScr, x2, y2, cx, cy, hdcMem, 0, 0, SRCCOPY);
Sleep(10);
}
DeleteDC(hdcMem);
ReleaseDC(hwnd, hdcScr);
DeleteObject(hBitmap);
LockWindowUpdate(NULL);
}
return FALSE;
}
SCRAMBLE 程序没有窗口过程。在 WinMain 函数中,首先用桌面窗口句柄调用 LockWindowUpdate。这个函数临时阻止任何其他程序对屏幕进行更新。然后,SCRAMBLE 用 DCX_LOCKWINDOWUPDATE 做参数调用 GetDCEx 取得整个屏幕的设备环境。这样,其他程序就不能在屏幕上输出了,而只有 SCRAMBLE 能。
接下来 SCRAMBLE 检测全屏幕的大小并将其分成 10 份。用这些尺寸(cx 和 cy)创建位图并将该位图选入到内存设备环境。
SCRAMBLE 用 C 的 rand 函数计算出四个随机值,分别乘以 cx 和 cy 作为两个坐标点。通过三个 BitBlt 函数,程序交换被显示的两个长方形区域。第一个 BitBlt 把从第一个坐标点开始的长方形复制到内存设备环境。第二个 BitBlt 把从第二个坐标点开始的长方形复制到第一个坐标点开始的位置。第三个则把内存设备环境中的长方形复制到第二个坐标点开始的地方。
这个过程高效率地在屏幕上交换两个长方形的内容。在 SCRAMBLE 程序重复这个过程 300 次后,屏幕会变得一团糟。不过不要担心,因为 SCRAMBLE 记下了整个过程,然后它会将屏幕进行恢复,在退出之前回到正常状态(并把屏幕解锁)。
你还可以用内存设备环境把一个位图的内容复制到另一个中去。例如,设想你要创建一个位图,要求它只包含另一个位图左上角四分之一的部分。如果源图的句柄是 hBitmap,可以将图片的大小复制到一个 BITMAP 结构中:
GetObject (hBitmap, sizeof(BITMAP), &bm);
然后创建一个四分之一大小的未初始化的位图:
hBitmap2 = CreateBitmap(bm.bmWidth / 2, bm.bmHeight / 2,
bm.bmPlanes, bm.bmBitsPixel, NULL);
在创建两个内存设备环境并将源图和新图选入:
hdcMem1 = CreateCompatibleDC(hdc);
hdcMem2 = CreateCompatibleDC(hdc);
SelectObject (hdcMem1, hBitmap);
SelectObject (hdcMem2, hBitmap2);
最后,把第一个位图左上角的四分之一复制到第二个位图中:
BitBlt (hdcMem2, 0, 0, bm.bmWidth / 2, bm.bmHeight / 2,
hdcMem1, 0, 0, SRCCOPY);
这样便基本完成了,剩下的是清理工作:
DeleteDC (hdcMem1);
DeleteDC (hdcMem2);
DeleteObject (hBitmap);
BLOWUP.C 程序也用到了锁定窗口更新技术来显示程序窗口边界之外的一块长方形屏幕截图。这个程序让你可以用鼠标来选取屏幕上任意的长方形区域。然后程序将选取内容复制到一个位图中。在 WM_PAINT 消息处理中,位图被复制到程序的客户区并根据需求拉伸或压缩。(见图 14-22。)
/*---------------------------------------------------
BLOWUP.C -- Video Magnifier Program
(c) Charles Petzold, 1998
---------------------------------------------------*/
#include <windows.h>
#include <stdlib.h> // for abs definition
#include "resource.h"
LRESULT CALLBACK WndProc(HWND, UINT, WPARAM, LPARAM);
int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevinstance,
PSTR szCmdLine, int iCmdShow)
{
static TCHAR szAppName[] = TEXT("Blowup");
HACCEL hAccel;
HWND hwnd;
MSG msg;
WNDCLASS wndclass;
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 = szAppName;
wndclass.lpszClassName = szAppName;
if (!RegisterClass(&wndclass))
{
MessageBox(NULL, TEXT("This program requires Windows NT!"),
szAppName, MB_ICONERROR);
return 0;
}
hwnd = CreateWindow(szAppName, TEXT("Blow-Up Mouse Demo"),
WS_OVERLAPPEDWINDOW,
CW_USEDEFAULT, CW_USEDEFAULT,
CW_USEDEFAULT, CW_USEDEFAULT,
NULL, NULL, hInstance, NULL);
ShowWindow(hwnd, iCmdShow);
UpdateWindow(hwnd);
hAccel = LoadAccelerators(hInstance, szAppName);
while (GetMessage(&msg, NULL, 0, 0))
{
if (!TranslateAccelerator(hwnd, hAccel, &msg))
{
TranslateMessage(&msg);
DispatchMessage(&msg);
}
}
return msg.wParam;
}
void InvertBlock(HWND hwndScr, HWND hwnd, POINT ptBeg, POINT ptEnd)
{
HDC hdc;
hdc = GetDCEx(hwndScr, NULL, DCX_CACHE | DCX_LOCKWINDOWUPDATE);
ClientToScreen(hwnd, &ptBeg);
ClientToScreen(hwnd, &ptEnd);
PatBlt(hdc, ptBeg.x, ptBeg.y, ptEnd.x - ptBeg.x, ptEnd.y - ptBeg.y,
DSTINVERT);
ReleaseDC(hwndScr, hdc);
}
HBITMAP CopyBitmap(HBITMAP hBitmapSrc)
{
BITMAP bitmap;
HBITMAP hBitmapDst;
HDC hdcSrc, hdcDst;
GetObject(hBitmapSrc, sizeof(BITMAP), &bitmap);
hBitmapDst = CreateBitmapIndirect(&bitmap);
hdcSrc = CreateCompatibleDC(NULL);
hdcDst = CreateCompatibleDC(NULL);
SelectObject(hdcSrc, hBitmapSrc);
SelectObject(hdcDst, hBitmapDst);
BitBlt(hdcDst, 0, 0, bitmap.bmWidth, bitmap.bmHeight,
hdcSrc, 0, 0, SRCCOPY);
DeleteDC(hdcSrc);
DeleteDC(hdcDst);
return hBitmapDst;
}
LRESULT CALLBACK WndProc(HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam)
{
static BOOL bCapturing, bBlocking;
static HBITMAP hBitmap;
static HWND hwndScr;
static POINT ptBeg, ptEnd;
BITMAP bm;
HBITMAP hBitmapClip;
HDC hdc, hdcMem;
int iEnable;
PAINTSTRUCT ps;
RECT rect;
switch (message)
{
case WM_LBUTTONDOWN:
if (!bCapturing)
{
if (LockWindowUpdate(hwndScr = GetDesktopWindow()))
{
bCapturing = TRUE;
SetCapture(hwnd);
SetCursor(LoadCursor(NULL, IDC_CROSS));
}
else
MessageBeep(0);
}
return 0;
case WM_RBUTTONDOWN:
if (bCapturing)
{
bBlocking = TRUE;
ptBeg.x = LOWORD(lParam);
ptBeg.y = HIWORD(lParam);
ptEnd = ptBeg;
InvertBlock(hwndScr, hwnd, ptBeg, ptEnd);
}
return 0;
case WM_MOUSEMOVE:
if (bBlocking)
{
InvertBlock(hwndScr, hwnd, ptBeg, ptEnd);
ptEnd.x = LOWORD(lParam);
ptEnd.y = HIWORD(lParam);
InvertBlock(hwndScr, hwnd, ptBeg, ptEnd);
}
return 0;
case WM_LBUTTONUP:
case WM_RBUTTONUP:
if (bBlocking)
{
InvertBlock(hwndScr, hwnd, ptBeg, ptEnd);
ptEnd.x = LOWORD(lParam);
ptEnd.y = HIWORD(lParam);
if (hBitmap)
{
DeleteObject(hBitmap);
hBitmap = NULL;
}
hdc = GetDC(hwndScr);
hdcMem = CreateCompatibleDC(hdc);
hBitmap = CreateCompatibleBitmap(hdc,
abs(ptEnd.x - ptBeg.x),
abs(ptEnd.y - ptBeg.y));
SelectObject(hdcMem, hBitmap);
StretchBlt(hdcMem, 0, 0, abs(ptEnd.x - ptBeg.x),
abs(ptEnd.y - ptBeg.y),
hdc, ptBeg.x, ptBeg.y, ptEnd.x - ptBeg.x,
ptEnd.y - ptBeg.y, SRCCOPY);
DeleteDC(hdcMem);
ReleaseDC(hwndScr, hdc);
InvalidateRect(hwnd, NULL, TRUE);
}
if (bBlocking || bCapturing)
{
bBlocking = bCapturing = FALSE;
SetCursor(LoadCursor(NULL, IDC_ARROW));
ReleaseCapture();
LockWindowUpdate(NULL);
}
return 0;
case WM_INITMENUPOPUP:
iEnable = IsClipboardFormatAvailable(CF_BITMAP) ?
MF_ENABLED : MF_GRAYED;
EnableMenuItem((HMENU)wParam, IDM_EDIT_PASTE, iEnable);
iEnable = hBitmap ? MF_ENABLED : MF_GRAYED;
EnableMenuItem((HMENU)wParam, IDM_EDIT_CUT, iEnable);
EnableMenuItem((HMENU)wParam, IDM_EDIT_COPY, iEnable);
EnableMenuItem((HMENU)wParam, IDM_EDIT_DELETE, iEnable);
return 0;
case WM_COMMAND:
switch (LOWORD(wParam))
{
case IDM_EDIT_CUT:
case IDM_EDIT_COPY:
if (hBitmap)
{
hBitmapClip = CopyBitmap(hBitmap);
OpenClipboard(hwnd);
EmptyClipboard();
SetClipboardData(CF_BITMAP, hBitmapClip);
}
if (LOWORD(wParam) == IDM_EDIT_COPY)
return 0;
// fall through for IDM_EDIT_CUT
case IDM_EDIT_DELETE:
if (hBitmap)
{
DeleteObject(hBitmap);
hBitmap = NULL;
}
InvalidateRect(hwnd, NULL, TRUE);
return 0;
case IDM_EDIT_PASTE:
if (hBitmap)
{
DeleteObject(hBitmap);
hBitmap = NULL;
}
OpenClipboard(hwnd);
hBitmapClip =(HBITMAP) GetClipboardData(CF_BITMAP);
if (hBitmapClip)
hBitmap = CopyBitmap(hBitmapClip);
CloseClipboard();
InvalidateRect(hwnd, NULL, TRUE);
return 0;
}
break;
case WM_PAINT:
hdc = BeginPaint(hwnd, &ps);
if (hBitmap)
{
GetClientRect(hwnd, &rect);
hdcMem = CreateCompatibleDC(hdc);
SelectObject(hdcMem, hBitmap);
GetObject(hBitmap, sizeof(BITMAP), (PSTR)&bm);
//SetStretchBltMode(hdc, COLORONCOLOR);
StretchBlt(hdc, 0, 0, rect.right, rect.bottom,
hdcMem, 0, 0, bm.bmWidth, bm.bmHeight, SRCCOPY);
DeleteDC(hdcMem);
}
EndPaint(hwnd, &ps);
return 0;
case WM_DESTROY:
if (hBitmap)
DeleteObject(hBitmap);
PostQuitMessage(0);
return 0;
}
return DefWindowProc(hwnd, message, wParam, lParam);
}
BLOWUP.RC (excerpts)
// Microsoft Visual C++ generated resource script.
//
#include "resource.h"
/
//
// Menu
//
BLOWUP MENU
BEGIN
POPUP "&Edit"
BEGIN
MENUITEM "Cu&t\tCtrl+X", IDM_EDIT_CUT
MENUITEM "&Copy\tCtrl+C", IDM_EDIT_COPY
MENUITEM "&Paste\tCtrl+V", IDM_EDIT_PASTE
MENUITEM "De&lete\tDelete", IDM_EDIT_DELETE
END
END
/
//
// Accelerator
//
BLOWUP ACCELERATORS
BEGIN
"C", IDM_EDIT_COPY, VIRTKEY, CONTROL, NOINVERT
"V", IDM_EDIT_PASTE, VIRTKEY, CONTROL, NOINVERT
VK_DELETE, IDM_EDIT_DELETE, VIRTKEY, NOINVERT
"X", IDM_EDIT_CUT, VIRTKEY, CONTROL, NOINVERT
END
RESOURCE.H (excerpts)
// Microsoft Visual C++ 生成的包含文件。
// 供 Blowup.rc 使用
//
#define IDM_EDIT_CUT 40001
#define IDM_EDIT_COPY 40002
#define IDM_EDIT_PASTE 40003
#define IDM_EDIT_DELETE 40004
图 14-22 BLOWUP 程序的一个运行示例
由于鼠标捕获的限制,使用 BLOWUP 程序在一开始会感觉有些复杂,需要适应一下。这个程序的使用方法如下:
- 在 BLOWUP 的客户区内按下鼠标左键并保持。鼠标指针变成一个十字。
- 继续按住鼠标左键并将指针移到屏幕的任意位置,放在你要截取的长方形的左上角。
- 保持按住左键的同时,按下鼠标右键并拖拽到要截取的长方形的右下角。松开鼠标左右键。(松开顺序无关紧要)
这是,鼠标指针恢复成箭头,选中区域被复制到 BLOWUP 的客户区中,并被压缩或者拉伸到合适的大小。
如果你是从右下角向左下角进行选择,那么 BLOWUP 会显示一个左右相反的图像。如果从左下角向右上角框取,BLOWUP 会显示一个上下相反的图像。如果是从右下角向左上角选取,得到的则是两种效果的叠加。
BLOWUP 还包含将位图复制到剪贴板,和将剪贴板上的位图复制到程序里的功能。BLOWUP 通过处理 WM_INITMENUPOPUP 消息来启用或者禁用 Edit 菜单上的不同菜单项,并通过 WM_COMMAND 消息来处理这些菜单项。这部分代码的结构看上去会很眼熟,因为它和第 12 章介绍复制合粘贴文本的那部分基本相同。
相对于位图,剪贴板项目不是全局句柄而是位图句柄。在使用 CF_BITMAP 时,GetClipboardData 函数会返回一个 HBITMAP 对象,而 SetClipboardData 函数需要接受一个 HBITMAP 对象。如果想在把一个位图传给剪贴板的同时保留一份给程序自己使用,则必须为其创建一个副本。类似地,如果从剪贴板粘贴一个位图,那么也需要创建一个副本。这个功能在 BLOWUP 程序中通过 CopyBitmap 函数实现,它获得已有图片的 BITMAP 结构并将该结构传入 CreateBitmapIndirect 函数来创建一个新位图。(代码中的变量名前缀 Src 和 Dst 分别表示“源”和“目标”。)两个位图都被选入内存设备环境,然后调用 BitBlt 完成位图位的转换。(还有另一种按位复制内容的方法,你可以分配一块和要复制的位图同样大小的一块内存,对原位图调用 GetBitmapBits,再对目标位图调用 SetBitmapBits 函数。)
我发现 BLOWUP 这个程序对检查分散在 Windows 及其应用程序中的许多小位图和图片非常有用。