科技行者

行者学院 转型私董会 科技行者专题报道 网红大战科技行者

知识库

知识库 安全导航

至顶网软件频道送给初学者的礼物:游戏编程起源连载三

送给初学者的礼物:游戏编程起源连载三

  • 扫一扫
    分享文章到微信

  • 扫一扫
    关注官方公众号
    至顶头条

学完这一章,你就可以做GDI基础的游戏了 。

作者:51CTO.com 来源:51CTO.com 2007年9月6日

关键字: 游戏 编程

  • 评论
  • 分享微博
  • 分享邮件
跟踪你的窗口和使用GDI

简介

如果你看过了头两篇连载,这次我们将学习WINDOWS GDI(图形设备接口)和其它一些相关的东西,象响应用户输入和处理Windows产生的一些消息。至于显示图形,我们将接触三个课题:文本显示,绘制象素,显示位图。我们先来研究一下几个Windows消息的细节。重复的话:你需要C语言的基础知识,最好看过上两章。由于本章将使你能做一个具体的图形DEMO,有一个源代码例程附在本章后面。是用Visual C++写的和编译的。在连载一里,我们创建和注册了一个窗口类,其中有一行定义了窗口的风格(功能),是这个样子:

sampleClass.style = 
CS_DBLCLKS | CS_OWNDC | CS_HREDRAW | CS_VREDRAW;
// standard settings


其中三个属性是很一般的,但这个——CS_OWNDC,需要解释一下。设备上下文是一个结构,是一个表现一组图形对象和属性的结构,还有一些输出设备的设置和属性。使用设备上下文允许你直接操纵图形,不用考虑低级细节。Windows GDI是一个图形翻译系统,是介于应用程序和图形硬件之间的一层。GDI可以输出到任意的兼容设备,不过最常使用的设备是视频监视器、图形硬拷贝设备(如打印机或绘图仪),或者是内存中的图元文本。GDI函数能够绘制直线、曲线、封闭的图形和文本。所有访问GDI的Windows函数都需要一个设备上下文句柄作为参数。这是非常容易做到的。你若想得到一个窗口的设备上下文句柄,你可以用这个函数:

HDC GetDC(
    HWND hWnd  // handle to a window
);


很简单,所有你做的是,把要操作的窗口的句柄传递给它,然后返回一个设备上下文句柄。如果你传递的是NULL,将返回整个屏幕的设备上下文(DC,以后都用DC表示)句柄。如果函数调用失败,将返回NULL。

处理显示图形的DC类型,称作显示DC,处理打印的,称作打印DC;处理位图数据的,称作内存DC,还有其它一些设备DC。感觉有点复杂吧,不要紧,这是Windows,它的主要功能就是迷惑群众。一旦我们接触一些代码,就不会觉得难了。

当你结束使用DC时,一定要释放它,也就是释放它占用的内存空间。要把这种思想贯穿到以后的编程中去,占用了内存,不用时要释放,切记!释放DC是一个很简单的函数:

int ReleaseDC(
    HWND hWnd, // handle to window
    HDC hDC    // handle to device context
);


若成功释放,返回值是1,否则是0。参数有注释,我还是说一下:

※ HWND hWnd:你所要控制的那个窗口的句柄。如果你开始传递的是NULL,现在还要传递NULL。

※ HDC hDC:DC的句柄。

在用DC和GDI进行图形显示前,我们先看看创建窗口实例时要遇到的几条重要的消息。我将要提到的四条消息是:WM_MOVE、WM_SIZE、WM_ACTIVATE、WM_PAINT。

追踪窗口状态

头两个是很简单的。当窗口被用户移动时将发送WM_MOVE消息,窗口新位置的坐标储存在lparam中。(还记得吗,消息在lparam和wparam中被进一步描述,它们是消息控制函数的参数)lparam的低端字中存储窗口客户区左上角的坐标x,高端字中存储坐标y。当窗口的大小被改变时,将发送WM_SIZE消息。同WM_MOVE消息差不多,lparam的低端字中存储客户区的宽度,高端字存储高度。同WM_MOVE不同的是,wparam参数也控制了一些重要的东西。它可以是下列中任意一个值:

※ SIZE_MAXHIDE:其它的窗口被最大化了。

※ SIZE_MAXIMIZED:本窗口被最大化了。

※ SIZE_MAXSHOW:其它的窗口被还原了。

※ SIZE_MINIMIZED:本窗口被最小化了。

※ SIZE_RESTORED:窗口被改变了尺寸,但既没最大化,也没有最小化。

当我编写窗口实例时,我通常喜欢把窗口的当前位置和大小保留在几个全局变量里。假设我们命名这些全局变量为xPos,yPos,xSize和ySize,你最好这样控制WM_SIZE和WM_MOVE这两个消息:

if (msg == WM_SIZE)
{
    xSize = LOWORD(lparam);
    ySize = HIWORD(lparam);
}
if (msg == WM_MOVE)
{
    xPos = LOWORD(lparam);
    yPos = HIWORD(lparam);
}


现在轮到WM_ACTIVATE消息了。它告诉你一个新窗口被激活。这是很有用的,因为如果出现优先的申请,你就不可能处理程序里的所有逻辑。有时,例如写一个全屏的DIRECTX程序,忽略WM_ACTIVATE消息将导致你的程序出现致命的错误,可能它做了一些你不希望它做的事情。在任何情况下,守候WM_ACTIVATE消息从而采取行动,是一个好主意。

窗口被激活和被解除激活都会发出WM_ACTIVATE消息,我们可以通过检测wparam的低端字来得知是被激活还是被取消。这将有三种可能的值:

※ WA_CLICKACTIVE:窗口被鼠标激活。

※ WA_ACTIVE:窗口被其它东西激活。(键盘、函数调用、等等)

※ WA_INACTIVE:窗口被解除激活。

为了处理这个消息,我保留了另一个全局变量bFocus,当接收到WM_ACTIVATE消息,它的值将改变。示例如下:

if (msg == WM_ACTIVATE)
{
    if (LOWORD(wparam) == WA_INACTIVE)
        focus = FALSE;
    else
        focus = TRUE; 
    // tell Windows we handled it
    return(0);
}


有两个相关联的消息WM_KILLFOCUS和WM_SETFOCUS,在窗口接收到输入焦点的时候,Windows消息WM_SETFOCUS被发送给它,在失去焦点的时候则发送WM_KILLFOCUS消息。应用程序可以截取这些消息以得知输入焦点的任何改变情况。什么是输入焦点呢?存有输入焦点的应用程序(窗口)就是被激活的那个窗口。你就认为被激活的窗口就是输入焦点就行了。因为可能出现没有窗口具有输入焦点,所以我建议用WM_ACTIVATE消息跟踪你的窗口状态。往下进行。 WM_PAINT 消息

WN_PAINT消息通知程序,全部或部分客户窗口需要重新绘制。当用户在最小化、重叠或调整客户窗口区域的时候,就会产生这条消息。重新绘制,你需要做两件事,首先是要用到WM_PAINT消息专用的一对函数,第一个是BeginPaint()函数,这是它的原形:

HDC BeginPaint(
    HWND hwnd,             // handle to window
    LPPAINTSTRUCT lpPaint  // pointer to structure for paint information
);


在我告诉你返回值是什么之前,让我们先看看参数:

※ HWND hwnd:需要重绘的窗口的句柄。你应该已经对于这种参数比较熟悉了。

※ LPPAINTSTRUCT lpPaint:这是很重要的一个。是指向PAINTSTRUCT结构的指针,该结构包含所有的要被重绘区域的信息。

继续之前,我应该给你看看PAINTSTRUCT结构:

typedef struct tagPAINTSTRUCT { // ps
    HDC hdc;
    BOOL fErase;
    RECT rcPaint;
    BOOL fRestore;
    BOOL fIncUpdate;
    BYTE rgbReserved[32];
} PAINTSTRUCT;


结构内的成员如下:

※ HDC hdc

※ BOOL fErase:指明应用程序是否应该抹去背景。如果是FALSE,说明系统已经删除了背景。还记得在Windows类中我们曾经用黑色画刷定义了一个背景吗?这就意味着系统将用这个画刷抹去无效的区域。

※ RECT rcPaint:这是最重要的一个成员。它将告诉你需要被重绘的无效区域的矩形。我将稍后告诉你RECT结构。

※ BOOL fRestore,BOOL fIncUpdate,BYTE rgbReserved[32]:好消息,这些是保留成员,为老Windows服务的,所有你我都不必管它们。:)

现在我已经给你看了这么多,这就是BeginPaint()函数的全部。它做了三件事儿。首先,它使窗口再次有效,直到下一次被改变,WM_PAINT消息发出前,这个窗口都是有效的。第二,如果在窗口类(Windows class)里定义了背景画刷,就像我们做过的那样,就用这个画刷重绘无效的区域。(所谓无效,就是被改变的)第三,返回了被重绘区域的DC句柄。重绘的区域,是由RECT结构定义的:

typedef struct _RECT {
    LONG left;
    LONG top;
    LONG right;
    LONG bottom;
} RECT;


我们已经指出这个结构描绘了一个矩形,但是有一件事情需要说说。RECT包含左上角,但不包含右下角。什么意思呢?让我们先定义一个RECT对象:

RECT myRect = {0, 0, 5, 5};


这个RECT包含象素(0,0),但是没有达到(5,5),所以矩形的右下角实际是(4,4)。看起来没有什么意义,但是你得习惯它。现在,还记得我所说的关于使用DC的事儿吗?一旦你用完了,你就必须释放它。用EndPaint()函数释放。回应WM_PAINT消息,每次调用完BeginPaint()函数,必须匹配一个EndPaint()函数释放DC。这是函数的原形:

BOOL EndPaint(
    HWND hWnd,                 // handle to window
    CONST PAINTSTRUCT *lpPaint // pointer to structure for paint data
);


函数通过返回TRUE或FALSE分别表明成功还是失败。有两个简单的参数:

※ HWND hWnd:就是窗口的句柄。

※ CONST PAINSTRUCT *lpPaint:指向PAINTSTRUCT类型的结构变量地址。同BeginPaint()的第二个参数是一回事。不要被CONST迷惑了,它只是保证和确认函数没有改变结构的内容。你还可以通过调用ValidateRect()函数代替BeginPaint()函数使得窗口再次有效。但你得手工操作一切。可能我们真的什么时候就要用到它。所以给你它的原形:

BOOL ValidateRect(
    HWND hWnd,         // handle of window
    CONST RECT *lpRect // address of validation rectangle coordinates
);


通过返回TRUE或FALSE来确定函数调用成功还是失败。参数很简单:

※ HWND hWnd

※ CONST RECT *lpRect:是指向RECT结构是否有效的指针。如果你传递NULL,则整个客户区域都是有效的。

现在把以上讲到的做个样子给你看吧,假设我们已经定义了一个全局的变量hMainWindow作为我们的窗口句柄。

if (msg == WM_PAINT)
{
    PAINTSTRUCT ps; // declare a PAINTSTRUCT for use with this message
    HDC hdc; // display device context for graphics calls

    hdc = BeginPaint(hMainWindow, &ps); // validate the window

    // your painting goes here!

    EndPaint(hMainWindow, &ps); // release the DC

    // tell Windows we took care of it
    return(0);
}


这段代码很简单。

有三个消息看起来差不多,都是处理关闭的事情的。它们是WM_DESTROY,WM_CLOSE,和WM_QUIT。它们的确很相似,但你需要知道它们之间的不同!一个窗口或者应用程序应该被关闭时发出WM_CLOSE消息,当接收到WM_CLOSE消息时,如果你愿意,向用户提出是否真的退出。你知道让用户作确认或有错误出现或有什么应该注意的事情发生的时候,往往弹出一个消息框。消息框的制作是很容易的,由于它用途广泛,我们还是介绍一下:

int MessageBox(
    HWND hWnd,         // handle of owner window
    LPCTSTR lpText,    // address of text in message box
    LPCTSTR lpCaption, // address of title of message box
    UINT uType         // style of message box
);


这些参数,尤其是最后一个,需要一些解释:

※ HWND hWnd:过一会儿我将向你介绍一个不含有它的函数,我保证。

※ LPCTSTR lpText:这是将要显示在消息框里的文本。你可以用\n等调整一下格式。

※ LPCCTSTR lpCaption:这是显示在消息框标题栏里的文本。

※ UINT uType:这个参数可以联合使用几个不同的标记。这些标记可以根据你的目的选择,有好多MB_打头的标记供你选择,联合使用时要用“|”分隔开。下面列出了一些常用的:

按钮类

◎ MB_ABORTRETRYIGNORE:建立有“Abort”、“Retry”、“Ignore”按钮的消息框。

◎ MB_OK:建立有“OK”按钮的消息框。

◎ MB_OKCANCEL:建立有“OK”和“Cancel”按钮的消息框。

◎ MB_RETRYCANCEL:建立有“Retry”、和“Cancel”按钮的消息框。

◎ MB_YESNO:建立有“Yes”和“NO”按钮的消息框。

◎ MB_YESNOCANCEL:建立有“Yes”、“No”和“Cancel”按钮的消息框。

图标类

◎ MB_ICONEXCLAMATION:加个惊叹号图标。

◎ MB_ICONINFORMATION:加个消息图标。(好像是个问号)

◎ MB_ICONQUESTION:加个问号图标

◎ MB_ICONSTOP:加个停止图标。

默认按钮标志

◎ MB_DEFBUTTON1:设置第一个按钮为默认按钮。(默认按钮即消息框弹出后,直接敲回车就被按下的那个按钮)

◎ MB_DEFBUTTON2:第二个为默认按钮。

◎ MB_DEFBUTTON3:第三个为默认按钮。

◎ MB_DEFBUTTON4:第四个为默认按钮。

其它的标志

◎ MB_HELP:添加一个帮助按钮。通常按下该按钮或者敲F1键都将产生WM_HELP消息。

◎ MB_RIGHT:文本右对齐。

◎ MB_TOPMOST:设置消息框总在窗口的最上面。

如果消息框建立失败,返回值为0,否则是下列任一个值:

◎ IDABORT:“Abort”按钮被选择。

◎ IDCANCEL:“Cancel”按钮被选择。

◎ IDIGNORE:“Ignore”按钮被选择。

◎ IDNO:“No”按钮被选择。

◎ IDOK:“OK”按钮被选择。

◎ IDRETRY:“Retry”按钮被选择。

◎ IDYES:“Yes”按钮被选择。

总之,当收到WM_CLOSE消息,你可以做两件事儿。一件是你接受默认的处理返回一个值,你若这样做了,应用程序或窗口按照计划关闭;再者,你返回0,应用程序或窗口将保持原样。以下是代码的基本部分:

if (msg == WM_CLOSE)
{
    if (MessageBox(hMainWindow, "Are you sure want to quit?",
"Notice", MB_YESNO | MB_ICONEXCLAMATION) == IDNO)
        return(0);

    // otherwise, let the default handler take care of it
}


WM_DESTROY消息有点儿不同。它是窗口正在关闭时发出的。当得到WM_DESTROY消息的时候,窗口已经从视觉上被删除。一个主窗口被关闭,并不意味着应用程序结束了,它将在没有窗口的条件下继续运行。然而,当一个用户关闭了主窗口,也就意味着他要结束应用程序,所以如果你希望应用程序结束,在收到WM_DESTROY消息的时候,你必须发出一个WM_QUIT消息。你可以使用PostMessage()函数,但由于这是一个特殊的情况,就为它准备了一个特殊的函数:

VOID PostQuitMessage(int nExitCode);


参数nExitCode是你的应用程序返回给Windows的一个退出代码(通常是0)。记住,WinMain()返回的是一个int(实数),不是void(空的)。nExitCode参数的值被赋值给wparam。WM_QUIT消息表示要关闭应用程序,所以得到这个消息后,你应跳出循环,把wparam返回给Windows。下面是一个简单的WinMain()函数实例:

int WinMain(HINSTANCE hinstance,HINSTANCE hPrevInstance,LPSTR lpCmdLine,int nCmdShow)
{
    // initialization stuff goes here

    // main loop - infinite!
    while (TRUE)
    {
        // check the message queue
        if (PeekMessage(&msg, NULL, 0, 0, PM_REMOVE))
        {
            if (msg.message == WM_QUIT) // exit main loop on WM_QUIT
            break;
            TranslateMessage(&msg);
            DispatchMessage(&msg);
        }

        // main program logic goes here
    }

    // perform any shutdown functions here - releasing objects and such

    return(msg.wparam); // return exit code to Windows
}


绘制象素

只要你取得了显示设备上下文的句柄,就可以用GDI绘制象素了。记住,要调用GetDC()取得句柄。绘制单个的象素,不出意外的话,你就用SetPixel()函数:

COLORREF SetPixel(
    HDC hdc,           // handle to device context
    int X,             // x-coordinate of pixel
    int Y,             // y-coordinate of pixel
    COLORREF crColor   // pixel color
);


返回类型COLORREF是我们没有讲过的。它不是一个结构,但是在表格0x00bbggrr里是一个32位的值。Bb是蓝色成分的8位值,gg是绿色,rr是红色。高字节00没有用,总是00。让我们看看函数里面的参数:

※ HDC hdc:你将要GetDC()得到的DC句柄。(DC就是设备上下文)你只能调用GetDC()一次,然后其它的函数也可以用这个DC句柄了。你每次绘制单个象素不用再取得新的DC句柄了。

※ int X,Y:是象素的x和y坐标。是在客户区的坐标,(0,0)即是窗口客户区左上角的坐标,不是屏幕左上角的坐标。

※ COLORREF crColor:象素的颜色。设置象素的颜色,用RGB()宏最简单。RGB()括号中的三个值分别是红色、绿色和蓝色,各个颜色的值可从0-255间取,不同的值组合成不同的颜色。

如果函数调用成功,将返回一个颜色,就是你要绘制的象素的颜色。由于视频硬件的限制,返回的颜色可能与调用函数时要求的颜色并不一致。如果调用失败,返回-1。如果你要设置窗口客户区的左上角为红色,函数将是这样:

SetPixel(hdc, 0, 0, RGB(255,0, 0));


是在假设你已经取得了DC句柄hdc后,调用这个函数。这个函数的更快的版本是SetPixelV():

BOOL SetPixelV(
    HDC hdc,           // handle to device context
    int X,             // x-coordinate of pixel
    int Y,             // y-coordinate of pixel
    COLORREF crColor   // new pixel color
);


参数相同,但返回类型不同。SetPixelV()返回的是布尔类型,成功,TRUE;失败,FALSE。如果你没有必要使用SetPixel()提供的额外信息,那我们当然是选择使用SetPixelV()了。(我们总是希望快一些)

还有一件事情,我们需要得到绘制好的象素的值(颜色)。没问题,使用GetPixel()函数就解决了:

COLORREF GetPixel(
    HDC hdc,  // handle to device context
    int XPos, // x-coordinate of pixel
    int nYPos // y-coordinate of pixel
);


很明显,返回值是象素所在位置的颜色。如果坐标出了DC句柄控制的区域,返回值是CLR_INVALID。参数同SetPixel()差不多:DC句柄,象素的x,y坐标。绘制象素就说到这吧,下面看看GDI的文本输出。

GDI的文本输出函数

有两个关于绘制文本的函数,其中简单一点儿的是TextOut()函数,原形是:

BOOL TextOut(
    HDC hdc,          // handle to device context
    int nXStart,      // x-coordinate of starting position
    int nYStart,      // y-coordinate of starting position
    LPCTSTR lpString, // pointer to string
    int cbString      // number of characters in string
);


我们已经看过很多返回类型是布尔类型的函数了,意思都一样:成功TRUE,失败FALSE。函数的参数如下:

※ HDC hdc:DC句柄。它既可以使GetDC的返回值,也可以是在处理WM_PAINT消息时BeginPaint的返回值。

※ int nXStart,nYStart:定义客户区内字符串开始的位置。字符串中第一个字符的左上角位于坐标(xXStart,nYStart)。在默认DC中,客户区的左上角为(0,0)。

※ LPCTSTR lpString:要显示的字符串。由于下一个参数给出字符串的长度,所有字符串不需要空的中止符。

※ int cbString:串中字符的个数。(字符串的长度)

TextOut()函数使用当前的文本颜色,背景颜色和背景类型。你若想自己改变这些,就得用如下的函数:

COLORREF SetTextColor(
    HDC hdc,         // handle to device context
    COLORREF crColor // text color
);

COLORREF SetBkColor(
    HDC hdc,         // handle of device context
    COLORREF crColor // background color value
);


SetTextColor()函数设置当前文本颜色,SetBkColor()函数设置当前背景颜色。参数的解释是显而易见的。第一个是DC句柄。第二个是颜色,你当然记得可以用RGB()宏来设置颜色。例如设置文本为蓝色:SetTextColor(hdc,RGB(0,0,255))。设置背景为白色:SetBkColor(hdc,RGB(255,255,255))。最后,设置背景类型用SetBkMode()函数:

int SetBkMode(
    HDC hdc,     // handle of device context
    int iBkMode  // flag specifying background mode
);


第一个参数就不讲了。第二个参数iBkMode可以分别取两个值:TRANSPARENT或OPAQUE。如果取TRANSPARENT,文本不会对背景造成影响。如选择OPAQUE,则相反。函数的返回值是背景的模式。

关于TextOut()函数还有一点要说,你可以控制文本的排列方式,用SetTextAlign()实现:

UINT SetTextAlign(
    HDC hdc,    // handle to device context
    UINT fMode  // text-alignment flag
);


参数如下:

※ HDC hdc:DC句柄。没有什么特别的。

※ UINT fMode:一个标志或一组标志(用“|”分开)将决定TextOut()输出文本的对齐模式。你不可以随便组合,要合理组合。这些标志如下:

◎ TA_BASELINE:参考点将在文本的基线。

◎ TA_BOTTOM:参考点将在矩形范围的底边。

◎ TA_TOP:参考点将在矩形范围的顶部。

◎ TA_CENTER:参考点将在矩形范围的中心。

◎ TA_LEFT:参考点将在矩形范围的左边。

◎ TA_RIGHT:参考点将在矩形范围的右边。

◎ TA_NOUPDATECP:当前的位置没有被文本输出函数调用,参考点被每次调用传递。

◎ TA_UPDATECP:当前的位置被每次文本函数输出调用,当前的位置作为参考点。

默认的设置是TA_LEFT|TA_TOP|TA_NOUPDATECP。如果你设置成TA_UPDATECP,后来调用的TextOut()函数将忽略nXStart和nYStart这两个参数,把文本放置在……。OK,告一段落,我们来看看TextOut()函数的兄弟DrawText():

int DrawText(
    HDC hDC,          // handle to device context
    LPCTSTR lpString, // pointer to string to draw
    int nCount,       // string length, in characters
    LPRECT lpRect,    // pointer to struct with formatting dimensions
    UINT uFormat      // text-drawing flags
);


DrawText()函数能格式化文本,多种排列方式。返回值是文本象素的高度。返回0,说明调用失败。让我们看看它的参数:

※ HDC hDC:一样的东东。我们的好朋友DC句柄。

※ LPCTSTR lpString:要显示的字符串。用双引号引起来。

※ int nCount:字符串中字符的数量。(字符串长度)

※ LPRECT lpRect:是RECT类型结构的地址,该结构包含了将要显示字符串的区域的逻辑坐标。

※ UINT uFormat:文本格式选项,你可以用“|”符号组合。下面列出最常用到的标志:

◎ DT_BOTTOM:指定底部对齐文本。必须与DT_SINGLELINE组合使用。

◎ DT_CALCRECT:返回矩形的宽度和高度。在多文本行的情况下,DrawText()将使用lpRect所指向的矩形的宽度,并扩展矩形的底部直到包含文本的最后一行。在单文本行的情况中,DrawText()将改变矩形的右边界,使它包含最后一个字符。不管在那种情况下,DrawText()都返回格式化后的文本高度,但是不重新绘制文本。

◎ DT_CENTER:文本水平居中。

◎ DT_EXPANDATABS:扩充Tab键跳跃的字符数,默认情况下,每按一次Tab键跳跃8个字符。

◎ DT_LEFT:指定文本左对齐。

◎ DT_NOCLIP:绘制屏幕时无需剪切。当使用DT_NOCLIP后,程序性能提高。

◎ DT_RIGHT:指定文本右对齐。

◎ DT_SINGLELINE:指定单行文本,忽略回车和换行。

◎ DT_TABSTOP:设置Tab键停止。在uFormat的低端字的高阶字节(15-8)中存放Tab键每按一次跳跃的字符数。默认是8。

◎ DT_TOP:顶部对齐文本(仅用于单行文本)。

◎ DT_VCENTER:指定垂直居中(仅对单行文本)。

还有一些其它的标志,但你看到的已经足够了。有了这些,你就可以轻松驾驭文本了,但记住,是以牺牲函数速度为代价的。你可以选择比较常用的TextOut()函数。

用GDI显示位图

位图是很容易操纵的,因为Windows本身就是位图。现在让我们看看到底有多容易吧!用GDI显示位图需要四个基本的步骤:

1、得到你要操作的窗口的DC句柄。

2、获得位图的句柄。

3、为位图创建设备上下文。

4、传送位图。

你已经知道第一步怎么做了,以前我也间接提到过第二步的做法,但没有具体说。我说过通过函数LoadBitmap()可以得到位图资源的句柄,但它有些过时了,有一个更灵活的函数LoadImage()取代了它,现在让我们看看怎么使用这个新函数。原形如下:

HANDLE LoadImage(
    HINSTANCE hinst,  // handle of the instance containing the image
    LPCTSTR lpszName, // name or identifier of image
    UINT uType,       // type of image
    int cxDesired,    // desired width
    int cyDesired,    // desired height
    UINT fuLoad       // load flags
);


如果函数调用失败,返回NULL。成功,你得到位图的句柄,意味着你就可以从资源或外部文件调用位图了。注意,这个函数还可以取得光标、图标的句柄,所以返回类型只是简单的HANDLE。在Visual C++6.0中,你需要用HBITMAP类型定义位图的句柄变量,否则编译器会生气的。例如:

HBITMAP hBitmap;
hBitmap =LoadImage(……);


下面是LoadImage()函数的参数说明:

※ HINSTANCE hinst:如果你从资源调用位图,这应该是你的应用程序的实例。如果你要从外部文件调用位图,就把它设置为NULL。

※ LPCTSTR lpszName:这个要么是资源标识符,记住用MAKEINTRESOURCE()宏转变数字常量;要么就是你要调用的图象的完整文件名称。

※ UINT uType:根据你的调用对象来决定。应该是IMAGE_BITMAP、IMAGE_CURSOR和IMAGE_ICON中的一种。

※ int cxDesired,cyDesired:这是你希望的图象的尺寸。如果你都设置为0,将是图象的真实尺寸。

※ UINT fuLoad:这是可以组合用的标志,当然是用“|”来连接。以下是一些常用的标志:

◎ LR_CREATEDIBSECTION:如果uType是IMAGE_BITMAP,将导致函数返回一个DIB(DIB,设备无关的位图)。基本的意思就是返回一个不依赖于显示设备的位图。

◎ LR_DEFAULTSIZE:对于光标和图标,如果cxDesired和cyDesired都设置为0,这个标志将启用Windows的默认尺寸,而不是图形的实际尺寸。

◎ LR_LOADFROMFILE:如果你要从外部文件中调入图象,你就必须用这个标志。 条件允许的话,你应当尽量使用LR_CREATEDIBSECTION和LR_LOADFROMFILE这两个标志。现在,你已经得到了图象(过去我们总说位图,好像不太准确,毕竟有时我们不从资源里调用)的句柄,下一步你必须建立设备上下文把图象放进去。位图的应该独有的特点是:它只能被选入到内存设备上下文中。内存设备上下文被定义为一个具有显示表面的设备上下文。它只存在于内存中,并且与特定的设备上下文相关。使用内存设备上下文,你必须首先创建它。CreateCompatibleDC()函数正是用于这个目的。它的一般形式如下:

HDC CreateCompatibleDC(HDC hdc);


函数唯一的参数是与内存设备上下文相兼容的设备上下文句柄。如果内存设备上下文与视频屏幕兼容,则这个参数可以为NULL。我们就用NULL。如果函数失败,返回NULL。现在,我们把位图(或图象)放入内存失败上下文,我们用这个函数:

HGDIOBJ SelectObject(
    HDC hdc,         // handle to device context
    HGDIOBJ hgdiobj  // handle to object
);


函数的返回类型HGDIOBJ是一个比HBITMAP更通用的类型。不用担心,HGDIOBJ和HBITMAP的一致性没有任何问题。以下是函数的参数说明:

※ HDC hdc:是设备上下文的句柄。要调用图象,必须是内存设备上下文的句柄。

※ HGDIOBJ hgdiobj:要调用对象的的句柄。可调用的有位图、画刷、字体、画笔等。这里是位图(图象)的句柄。

返回值是要调入设备上下文中的对象的句柄。这里是位图的句柄。如果失败,返回NULL。

现在你已经把位图装入设备上下文,你还需要进行最后的一步:把内存DC里的内容拷贝到显示设备上下文中。然而,我们首先要得到位图的一些信息,如尺寸,这是显示图象时必须的。所以,我们还需要另一个函数GetObject(),它可以用于获得对象的信息,当然,这里我们是要获得位图的信息。函数的一般形式如下:

int GetObject(
    HGDIOBJ hgdiobj,  // handle to graphics object of interest
    int cbBuffer,     // size of buffer for object information
    LPVOID lpvObject  // pointer to buffer for object information
);


返回值是一个字节数。如果失败,返回0。当GetObject()调用的目标是位图时,返回的信息是与位图的宽度、高度和颜色格式有关的结构成员。参数说明如下:

※ HGDIOBJ hgdiobj:要得到信息的对象的句柄。这里我们传送位图的句柄。

※ int cbBuffer:存放调用返回的信息的缓冲区的大小。对于位图,我们将得到BITMAP类型结构,所以这里设置成sizeof(BITMAP)。

※ LPVOID lpvObject:指向存放由调用返回的信息的缓冲区的指针。

你需要定义一个BITMAP结构类型的变量,调用GetObject()函数放入缓冲区的信息。由于BITMAP结构对我们来说是一个新的结构,所以就介绍一下:

typedef struct tagBITMAP { // bm
    LONG bmType;
    LONG bmWidth;
    LONG bmHeight;
    LONG bmWidthBytes;
    WORD bmPlanes;
    WORD bmBitsPixel;
    LPVOID bmBits;
} BITMAP;


很多成员,但我们实际上只对其中的两个有兴趣。但我们还是都介绍一下:

※ LONG bmType:指定位图类型,必须为0。

※ LONG bmWidth,bmHeight:分别是位图的宽度和高度,以象素为单位。必须都大于0。

※ LONG bmWidthBytes:指定每一行扫描线中的字节数。因为Windows假定位图是字对齐的,所以这个值必须能够被2整除。

※ LONG bmPlanes:指定颜色面的数目。

※ LONG bmBitsPixel:指定表述象素颜色所需的位数。(好像没什么用)

※ LPVOID bmBits:如果你想存取实际的位图数据,这个指针指向位图位值得位置。

一旦位图被选入内存设备上下文,且代码已经得到了位图宽度和高度的必要信息后,我们就可以将内存中存储的位图通过位块传输到达屏幕,然后在任意位置对它进行显示。有两个函数需要说明,先说第一个:

BOOL BitBlt(
    HDC hdcDest, 
// handle to destination device context
    int nXDest,  
// x-coordinate of destination rectangle's upper-left corner
    int nYDest,  
// y-coordinate of destination rectangle's upper-left corner
    int nWidth,  
// width of destination rectangle
    int nHeight, 
// height of destination rectangle
    HDC hdcSrc,  
// handle to source device context
    int nXSrc,   
// x-coordinate of source rectangle's upper-left corner
    int nYSrc,   
// y-coordinate of source rectangle's upper-left corner
    DWORD dwRop  
// raster operation code
);


BitBlt()函数是执行位图显示操作最简单且最直接的方法。根据函数调用的成功或失败,返回值是TRUE或FALSE。布尔函数都是这样。有很多参数,但很好理解:

※ HDC hdcDest:目标设备上下文句柄。根据我们的情况,应该是显示设备上下文句柄。

※ int nXDest,nYDest:目标矩形左上角的x、y坐标,也就是被显示的位图左上角的屏幕位置。

※ int nWidth,nHeight:位图的宽度和高度。

※ HDC hdcSrc:原来的设备上下文句柄。根据我们的情况,应该是内存设备上下文句柄。

※ int nXSrc,nYSrc:源位图的x、y坐标。由于进行位块传输的矩形必须与在参数nWidth和参数nHeight中定义的尺寸相同,所以通常都设为0。但不一定总是这样,例如你只想显示位图的一部分,就不能都设置为0。

※ DWORD dwRop:有很多光栅代码你可以选择,但只有一个我们感兴趣,SRCCOPY。它直接把源DC内的内容拷贝到目标DC中。

以上就是第一个函数。下面说说第二个函数StretchBlt(),简单的说,位图实际的拉伸或压缩就是通过它来实现的。函数的一般形式如下:

BOOL StretchBlt(
    HDC hdcDest,      
// handle to destination device context
    int nXOriginDest, 
// x-coordinate of upper-left corner of dest. rectangle
    int nYOriginDest, 
// y-coordinate of upper-left corner of dest. rectangle
    int nWidthDest,   
// width of destination rectangle
    int nHeightDest,  
// height of destination rectangle
    HDC hdcSrc,      
// handle to source device context
    int nXOriginSrc, 
// x-coordinate of upper-left corner of source rectangle
    int nYOriginSrc,  
// y-coordinate of upper-left corner of source rectangle
    int nWidthSrc,    
// width of source rectangle
    int nHeightSrc,   
// height of source rectangle
    DWORD dwRop       
// raster operation code
);


它比BitBlt()复杂一些,所以它比BitBlt()慢。它们的参数差不多,并且有了注释,这里就不再重复了。光栅代码也是选择SRCCOPY。现在,只剩下最后一件事情——清除。建立一个设备上下文【CreateCompatibleDC(HDC hdc)】不同于得到设备上下文【(GetDC)】,不能用ReleaseDC(),要用:

BOOL DeleteDC(HDC hdc);


参数是建立的DC的句柄。返回值是一个布尔类型,下面我们把以上步骤合并。事先假设你已经定义了一个全局的应用程序实例的句柄hinstance。

int ShowBitmapResource(HDC hDestDC, int xDest, int yDest, int nResID)
{
    HDC hSrcDC;      // source DC - memory device context
    HBITMAP hbitmap; // handle to the bitmap resource
    BITMAP bmp;      // structure for bitmap info
    int nHeight, nWidth; // bitmap dimensions

    // first load the bitmap resource
    if ((hbitmap = (HBITMAP)LoadImage
(hinstance, MAKEINTRESOURCE(nResID), 
IMAGE_BITMAP, 0, 0, LR_CREATEDIBSECTION))
== NULL)
        return(FALSE);

    // create a DC for the bitmap to use
    if ((hSrcDC = CreateCompatibleDC(NULL)) == NULL)
        return(FALSE);

    // select the bitmap into the DC
    if (SelectObject(hSrcDC, hbitmap) == NULL)
        return(FALSE);

    // get image dimensions
    if (GetObject(hbitmap, sizeof(BITMAP), &bmp) == 0)
        return(FALSE);

    nWidth = bmp.bmWidth;
    nHeight = bmp.bmHeight;

    // copy image from one DC to the other
    if (BitBlt(hDestDC, xDest, yDest, nWidth,
nHeight, hSrcDC, 0, 0, SRCCOPY) == NULL)
        return(FALSE);

    // kill the memory DC
    DeleteDC(hSrcDC);

    // return success!
    return(TRUE);
}


你现在有足够的知识用Windows GDI在窗口里建立一个小游戏了!你能创建窗口,显示图形。游戏的逻辑同DOS下的C语言游戏逻辑一样,即使你不知道DOS下的C,玩过游戏就应该有个初步的认识。你甚至能用鼠标处理产生的消息。但有一件事儿我们漏了,可能它不属于本章的范围,但我们不能不提它——键盘支持。Windows提供了一个非常好的函数GetAsyncKeyState()检测键盘的状态。它返回一个16位的值,高字节显示了一个键子是否被按下。它的原形如下:

SHORT GetAsyncKeyState(int vKey);


参数是键子的标识符。都以VK_开头,例如一些最常用的:VK_RETURN,VK_ESCAPE,VK_UP,VK_LEFT,VK_RIGHT和VK_DOWN。你甚至可以用VK_LBUTTON和VK_RBUTTON标识鼠标按键。多方便呀。观察高字节,如果是1,键子就是正被按下。我总是用一个宏来做这些:

#define KEYSTATE(vknum) ((GetAsyncKeyState(vknum) & 0x8000) ? TRUE : FALSE)



如果你不仔细,可能还没有注意到这个条件符号(?),在C语言中是一个三元算子——评估它左边的表达式。如果表达式的值为true,表达式就是冒号左边的值,如果false,就是冒号右边的值。

总结

现在,你可以做GDI基础的游戏了。

查看本文来源

    • 评论
    • 分享微博
    • 分享邮件
    邮件订阅

    如果您非常迫切的想了解IT领域最新产品与技术信息,那么订阅至顶网技术邮件将是您的最佳途径之一。

    重磅专题
    往期文章
    最新文章