科技行者

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

知识库

知识库 安全导航

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

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

  • 扫一扫
    分享文章到微信

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

C++WINDOWS编程基础。

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

关键字: 游戏 编程

  • 评论
  • 分享微博
  • 分享邮件
第二章 使用Win32程序资源

一、简介

欢迎继续收看!通过本章题目可能你已经猜出了本章论题,我将教会你在Windows程序中使用资源。简单的讲,资源即数据,它们通常是和程序的EXE文件相关联的,但是它们又是独一无二的。首先,资源在运行过程中不能被修改。它们实际上都是只读文件,而且程序代码不能够直接访问它们。另外,资源并不在程序的数据区内。在装入时,程序资源通常在某个磁盘文件中,直到程序需要它们时才被装入。使用资源是一件很容易的事情,并且它的妙处无穷。Windows为我们提供了大量的资源类型,但我们这里只学一些最常用,最容易的:图标(icon)、光标(cursor)、位图(bitmap)、菜单(menu)和字符串(string)。此后,我还将教你建立自己风格类型的资源,使你为所欲为。

重复一下,要想看懂本章,你得有点C语言的基础。C++有时用一用,但不影响你学习本章内容。并且我假定你已经读过了上一章内容“Windows编程基础”。还是用Microsoft Visual C++的编译器。

资源脚本

在进行细节之前,我们要先搞懂怎样要编译器知道它所要编译的资源类型。方法是使用称之为资源脚本的特殊文件,它是一个简单的文本文件,可以手工编辑,也可以让Visual C++自动编辑,或者你用其它的自动编辑器编辑。无论如何,资源脚本文件要有一个.rc的扩展名。大多数的脚本文件都从定义资源行开始,最简单的资源行通常要用到资源类型,就像这样:

[identifier]   [resource type]   [filename]
  【标识符】       【资源类型】     【文件名称】


标识符可以用两种方式表示:一种是能表示资源意思的字符串,另一种是在资源相对应的头文件中用#define定义过的数字常量。如果你选择数字常量,这通常是一个好主意,别忘了把相应的头文件加入到你的资源脚本。资源脚本使用C语言风格的文件格式好像比较容易理解。以下是一个比较简单的资源脚本实例:

#include "resource.h"
// icons
ICON_MAIN ICON myicon.ico
// bitmaps
IMG_TILESET1 BITMAP tileset.bmp
IMG_TILESET2 BITMAP tileset2.bmp


例子中的ICON_MAIN和IMG_TILESET是字符串呢,还是数字常量?这无伤大雅,编译器编译的时候会自己判断。如果发现在头文件中有#define的定义,那就认为是字符常量,否则,就是字符串。如果有些迷茫,不要紧。我将解释我们要用到的每一个资源类型。如果您觉得麻烦那让我们用全自动的资源插入系统吧!(在Visual C++中,在“插入”下拉菜单中,选择“资源”)现在你知道了建立资源脚本的基础知识,让我们开始进一步的行程吧!

图标和光标

你每天在使用的大多数的Windows程序,都有自己的图标,简单的说,就是EXE文件同这个图标资源相关联了,独特风格的光标也是如此。你已经知道图标的脚本行样子了,光标的和它很相似,看看吧:

[identifier] CURSOR [filename]
[identifier] ICON [filename]


增加了一行脚本行后,也就是意味着你的EXE文件又多了一个关联。也就是说你的EXE文件要根据标识符去相应的位置寻找相应的文件[filename]。你可以使用任何你喜欢用的图标/光标编辑器去编辑相应的文件。我通常利用Visual C++中的编辑器。

把资源脚本做出来后,并没有完事儿,因为你还不知道怎么调用相应的资源,要想知道图标和光标是怎样在你的程序中被调用的,让我们回过头来,看一看上一章中的窗口类(windows class)文件:

WNDCLASSEX sampleClass; // declare structure variable
sampleClass.cbSize = sizeof(WNDCLASSEX);
// always use this!
sampleClass.style = CS_DBLCLKS | CS_OWNDC | CS_HREDRAW | CS_VREDRAW;
// standard settings
sampleClass.lpfnWndProc = MsgHandler; 
// message handler function
sampleClass.cbClsExtra = 0; 
// extra class info, not used
sampleClass.cbWndExtra = 0; 
// extra window info, not used
sampleClass.hInstance = hinstance; 
// parameter passed to WinMain()
sampleClass.hIcon = LoadIcon(NULL, IDI_WINLOGO);
// Windows logo
sampleClass.hCursor = LoadCursor(NULL, IDC_ARROW);
// standard cursor
sampleClass.hbrBackground = (HBRUSH)GetStockObject(BLACK_BRUSH); 
// a simple black brush
sampleClass.lpszMenuName = NULL; 
// no menu
sampleClass.lpszClassName = "Sample Class" 
// class name
sampleClass.hIconSm = LoadIcon(NULL, IDI_WINLOGO);
// Windows logo again


还记得它吧?这个hIcon用来表示整个程序;hIconSm用来出现在开始菜单和窗口的标题栏里;hCursor用来表示在你所创建的窗口中的光标的样子。我向你保证,我们要实现自己的风格一点都不复杂。下面是它们的原形:

HICON LoadIcon(
        HINSTANCE hInstance,
// handle to application instance
        LPCTSTR lpIconName  
// icon-name string or icon resource identifier
);
HCURSOR LoadCursor(
        HINSTANCE hInstance, 
// handle to application instance
        LPCTSTR lpCursorName 
// name string or cursor resource identifier
);


返回的类型是它们自己相对应的类型。其内部的参数都很直观:

※ HINSTANCE hInstane:但程序执行时,把图标或光标相对应的句柄传递给WinMain()函数。若要使用Windows的标注图标或光标,就把它设置为NULL。

※ LPCTSTR lpIconName,lpCursorName:是你要调用的资源的标识符字符串。如果你在脚本文件中用字符串直接作为标识符,就直接传送它好了;如果你是用数字常量,就要使用一个Windows头文件里的宏MAKEINTRESOURCE()来把它们协调一致。

让我们看一看下面的资源脚本,是关于图标和光标的:

#include "resource.h"
ICON_MAIN ICON myicon.ico
CURSOR_ARROW CURSOR arrow.cur


如果标识符ICON_MAIN合CURSOR_ARROW在头文件resource.h中没有被#define定义过,那么我们将直接传递它给资源调用函数,象这样:

sampleClass.hIcon = LoadIcon(hinstance, "ICON_MAIN");


如果它们在头文件resource.h中这样定义过:

#define ICON_MAIN 1000
#define CURSOR_ARROW 2000


你就必须用宏MAKEINTRESOURCE()把它们转变为LPCTSTR类型。下面给出你几种意义相同的调用方法,都是正确的喔!

sampleClass.hIcon = LoadIcon(hinstance,
MAKEINTRESOURCE(ICON_MAIN));
or...
sampleClass.hIcon = LoadIcon(hinstance,
MAKEINTRESOURCE(1000));
or...
int ident = 1000;
sampleClass.hIcon = LoadIcon(hinstance, 
MAKEINTRESOURCE(ident));


关于图标和光标的调用,你学的差不多了。就这个话题,我还想告诉你一件事儿。如果你除了在程序的开始设置光标外,在程序中还要设置光标,有一个简单的Windows函数可以完成它:

HCURSOR SetCursor(HCURSOR hCursor);


仅仅一个参数,它是一个句柄,是在调用LoadCursor()时得到的,函数返回调用的上一个光标的句柄,如果没有设置过上一个光标,返回值是NULL。 位图

想要往程序里添加图象,通过位图资源可能是最简单的办法了。位图是Windows之本,当然提供了一些函数来处理位图,请记住,如果你使用了太多的位图,你的EXE文件将要非常巨大。在资源脚本中设置位图同图标和光标没什么区别:

[identifier] BITMAP [filename]


有一个函数LoadBitmap(),同LoadCursor()和LoadIcon()的用法很相似,它将得到一个句柄,由于我还没有讲过图形(graphics),就不具体说函数的功能了,你可以猜一猜它是怎样工作的,一旦你得到了图形句柄,你将怎样使用它呢?更多的留待以后再讲。不要担心,现在只是要你有点儿准备。下面看看我们还应该学点儿什么。

字符串表格

字符串表

字符串表是我最喜欢的资源类型。正象你所想的:一个充满字符串的庞大表格。字符串表有很多用处。你可以用它存储你的文件名称,游戏中的人物对话,消息框中的文本,菜单中的文本等等。在资源脚本里建立一个字符串表很容易,就像这样:

STRINGTABLE
{
// entries go here
}


一个字符串表由几部分组成:一个标识字符串的数字;紧跟着一个逗号;然后是加了双引号的字符串本身。字符串表里的字符串被允许使用溢出符号,如

或 。注意,字符串表本身并没有标识符,所以每个程序只能有一个字符串表。一个简单的字符串表可能象下面这个样子:

// program information
STRINGTABLE
{
    1, "3D Space Game v1.0"
    2, "Written by The Masked Coder"
    3, "(C) 2000 WienerDog Software"
}


从程序的字符串表里调用字符串,将使用——你可能猜到了——LoadString()函数。这是它的原形:

int LoadString(
    HINSTANCE hInstance, // handle to module containing string resource
    UINT uID,            // resource identifier
    LPTSTR lpBuffer,     // pointer to buffer for resource
    int nBufferMax       // size of buffer
);


函数返回的实数是字符的数量,不包括空字符,它将被赋值到程序数据段的缓冲区中去,相当于字符串的长度。如果你调用了一个空字符串或者调用失败,都将返回0。下面来看看具体参数:

※ HINSTANCE hInstance:同以前的一样,你所有操纵项目的句柄。

※ UINT uID:你想要调用的字符串的数码标识符。

※ LPTSTR lpBuffer:指向接收字符串的字符数组的指针。

※ int nBufferMax:缓冲区的字节长度。如果被调用的字符串的长度大于缓冲区的长度,字符串将被按照缓冲区的大小缩减。

例如,调用“WienerDog Software’s copyright”的信息,代码应该如下:

char buffer[80];
LoadString(hinstance, 3, buffer, sizeof(buffer));


尽管在资源脚本中字符串使用数字声明,而不是标识符,但我通常在使用字符串表时,习惯于在头文件中用#define定义一下字符串的声明数字。针对上面的代码,我可能加一行:

#define ST_WIENERDOGCOPYRIGHT 3


这样一来,用LoadString()函数时,你的程序代码更容易读懂,也使你的思路更加清晰。但也并不是意味着你必须为字符串表里的每一个字符串都定义一个常量标识符。当字符串表相当大时,或者你感觉记不清时,就应该定义常量标识符。我通常在每个常量标识符的前面加上一个前缀ST_。具体的说,ST_FILENAMES作为存储文件名称字符串的索引,ST_DIALOGUE作为人物对话字符串的索引,等等。

菜单

这是我们要讲的最后一个Windows资源,当然,不是为了凑数才讲的哦。窗口的菜单条紧接在标题栏的下发显示,这个菜单有时被称为“主菜单”或“顶层菜单”。菜单通常在建立窗口类时被调用。还记得吗?上一章中窗口类建立过程中,有这样一行:

sampleClass.lpszMenuName = NULL;


如果你正在建立一个窗口程序,并希望有菜单,你就得需要用到菜单资源。它的脚本文件可能要复杂一点儿,但下面是一个最基本的框架:

[identifier] MENU
{
    POPUP [menu name]
    {
        MENUITEM [item name], [identifier]
    }
}


[identifier]标识符是你知道的:一个字符串或一个数字常量。在MENU的大括号中,可以有一个或者几个POPUP(弹出式)菜单,每一个都有一个下拉菜单,[menu name]中填入菜单名称。在POPUP的大括号中,可以有一个或者多个菜单条,[item name]中填入菜单条名称,后面必须跟着一个数字常量的标识符。(所谓数字常量的标识符,就是用#define定义过的标识符。如:#define MENUID_NEW 101)如果你还想在菜单里建立热键,就要用(&)符号。在你想成为热键的字符前加上&,例如,你想用Alt+F代替用鼠标点击File按钮,你就应该写成 &File ,菜单的名称都要用双引号引上。看看下面的例子就更清楚了:

MAIN_MENU MENU
{
    POPUP "&File"
    {
        MENUITEM "&New", MENUID_NEW
        MENUITEM "&Open...", MENUID_OPEN
        MENUITEM "&Save", MENUID_SAVE
        MENUITEM "Save &As...", MENUID_SAVEAS
        MENUITEM "E&xit", MENUID_EXIT
    }

    POPUP "&Help"
    {
        MENUITEM "&Contents", MENUID_CONTENTS
        MENUITEM "&Index...", MENUID_INDEX
        MENUITEM "&About", MENUID_ABOUT
    }
}


你还可以在POPUP下建立子菜单,你自己琢磨吧,我就不讲了,我们还得往下进行。获得菜单资源的句柄,我们需要用LoadMenu()函数,它的原形如下:

HMENU LoadMenu(
    HINSTANCE hInstance, // handle to application instance
    LPCTSTR lpMenuName   // menu name string or menu-resource identifier
);


现在你应该已经熟悉这些参数了。第一个参数是你的程序实例的句柄,第二个是你的菜单资源的标识符。如果你使用了数字常量作为标识符,别忘了使用MAKEINTRESOURCE()这个宏转换一下哦!现在,你有两个方法为窗口创建菜单。第一个方法是在创建窗口类时直接设置:

sampleClass.lpszMenuName = LoadMenu(hinstance, MAKEINTRESOURCE(MAIN_MENU));


第二个方法是在设置窗口类时,让lpszMenuName等于NULL,以后再加入菜单。当你要建立两个独立的菜单,而又不想定义不同的窗口类时,这个选择是很有意义的,你需要用SetMenu()函数:

BOOL SetMenu(
    HWND hWnd,   // handle to window
    HMENU hMenu, // handle to menu
);
如果创建菜单的功能实现了,将返回TRUE,否则返回FALSE。它的参数是很容易理解的:

※ HWND hWnd:是你所要创建菜单的那个窗口的句柄。也就是你在调用CreateWindowEx()时产生的那个窗口句柄。

※ HMENU hMenu:识别菜单,使用它的形式是:hMenu=LoadMenu(hInstance,菜单标识符),所以它得到的是LoadMenu()函数返回的菜单句柄。如果给它赋值NULL,指定窗口的菜单将被移除。

资源是个好东西,因为它使我们很容易就生成了菜单。但是当我们点击了菜单上的选项,将会发生什么呢?答案是Windows将会发出一个WM_COMMAND的消息给程序,程序再让Windows作出相应的反应。让我们具体看一看。

控制菜单事件

你可能记得,Windows的消息都是通过CALLBACK函数控制的,通常它是这个样子:WindowProc()或类似的样子。我们在上一章中用到的是这个样子:MsgHandler()。它的原形如下:

LRESULT CALLBACK MsgHandler(
    HWND hwnd,     // window handle
    UINT msg,      // the message identifier
    WPARAM wparam, // message parameters
    LPARAM lparam, // more message parameters
};


当一个菜单消息被送到,msg将等于WM_COMMAND,所选择的菜单项目将被包含进wparam。这就是为什么菜单的标识符不能是字符串的原因,它需要适合wparam参数。更特别的是,菜单标识符只占用wparam的低位字。WPARAM,LPARAM,int等都是32位,分高、低位字的变量。Windows提供了宏LOWORD()和HIWORD()分别来提取变量中的低位字和高位字,原形如下:

#define LOWORD(l) ((WORD) (l))
#define HIWORD(l) ((WORD) (((DWORD) (l) >> 16) & 0xFFFF))


LOWORD()宏的实际情况是,由于简单的定义为WORD,就自然的取得了低端的16位。HIWORD()函数把高端的16位向右移,然后同0xFFFF之间调用了逻辑“和”(AND),确保把高于16位的字节变为0。可能你不太熟悉>>和<<操作符号,它们是位移操作符。“<<”操作符把变量中的每一个字节中的数字向左移动,“>>”就是向右移动。例如,我们有一个16位的变量x,它的值是224,二进制表示为0000 0000 1111 0100。下面是一个关于位移的例子:

short int x = 244, y;
y = x << 4;
Contents(内容) of x: 0000 0000 1111 0100
Contents (内容)of y: 0000 1111 0100 0000


总之,使用LOWORD()宏你得到了wparam的低端字,也就是说你得到了被选择菜单的ID(标识符)。所以,在你的MsgHandler()函数中,你应该这样做:

// handle menu selections
if (msg == WM_COMMAND)
{
    switch (LOWORD(wparam))
    {
    case MENUID_NEW:
        // code to handle File->New goes here
        break;
    case MENUID_OPEN:
        // code to handle File->Open goes here
        break;

        // the rest of the option handlers go here

    }

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


当然,还有一些其它的资源类型,如加速表(快捷键)、HTML页、WAV文件等。但我想以上这些是最有用,最要紧学习的。在结束之前,我还要告诉你Windows编程的一大强力特色——定制自己的资源类型。

定制资源

标准的程序资源给我们带来了很大方便。但不仅仅是这些标准的类型,你还可以创建自己的资源类型。资源可以是你希望的任何一种数据。使用自己定制的资源需要多付出一点劳动,因为你必须手工定位和读取资源数据。比想象的要容易,因为你已经习惯了定义资源的格式:

[identifier] [resource type name] [filename]


[resource type name]资源类型名称是让你命名的一个字符串。还是举例说明吧:假设我们要用到plconfig.dat文件作为资源,它包含初始化游戏人物的必需信息。我们将把它定义为CHARCONFIG资源类型,脚本文件应该是这个样子:

DATA_PLAYERINIT CHARCONFIG p1config.dat


现在,你已经拥有了数据(plconfig.dat),你还必须分三步使一个指针指向资源数据。这包括我们还没有提到过的需要调用的函数让我们一起解决。第一步,我们必须调用FindResource()函数去发现资源。函数原形如下:

HRSRC FindResource(
    HMODULE hModule, // module handle
    LPCTSTR lpName,  // pointer to resource name
    LPCTSTR lpType   // pointer to resource type
);


返回值是一个资源信息块儿的句柄,如果调用失败,返回NULL。参数意义如下:

※ HMODULE hModule:HMODULE相当于HINSTANCE。不要问我为什么换了另一个名字,你只要把你的程序实例句柄传送给它就好了,你不需要什么类型转换,它们是相同的。

※ LPCTSTR lpName:这个是资源的标识符。如果你使用了数字的常量作为标识符,别忘了使用MAKEINTRESOURCE()宏。

※ LPCTSTR lpType:这个是资源的类型,你需要把你定义的资源类型名称的字符串传递给它。我们的是CHARCONFIG。

调用函数方式如下:

HRSRC hRsrc = FindResource(hinstance, MAKEINTRESOURCE(DATA_PLAYERINIT), "CHARCONFIG");


这是信息块儿所在资源的句柄。下一步是要得到指向数据的指针。需要把句柄传递给LoadResource()函数,来调用数据。这将产生一个资源本身的句柄。下面是函数的原形:

HGLOBAL LoadResource(
    HMODULE hModule, // resource-module handle
    HRSRC hResInfo   // resource handle
);


返回类型HGLOBAL是一个普通句柄类型,是相对于我们说过的那些HBITMAP或HICON等句柄类型。如果调用函数失败,将返回NULL。参数解释如下:

※ HMODULE hModule:老东西,程序实例的句柄。

※ HRSRC hResInfo:把FindResource()得到的句柄传递给它。

现在,我们有了资源的句柄,就可以得到指向数据(自定义的)的指针了,这需要调用LockResource()函数来完成。原形如下:

LPVOID LockResource(HGLOBAL hResData);


仅仅把调用LoadResource()函数得到的句柄传递给它就万事大吉了。如果返回值是NULL,说明函数调用失败。否则,我们就得到梦寐以求的指针!现在我们可以自由得处理数据了。注意:返回的类型是LPVOID,(相当于void*),所以若你想把指针指向队列符号,你还要注意转换成类似BYTE*型的哦!现在,我们完成了所有的步骤,这里,我将展示给你一个指针指向特殊资源的实例:

UCHAR* LoadCustomResource(int resID)
{
    HRSRC hResInfo;
    HGLOBAL hResource;

    // first find the resource info block
    if ((hResInfo = FindResource(hinstance, MAKEINTRESOURCE(resID), "CUSTOMRESOURCETYPE")) == NULL)
        return(NULL);

    // now get a handle to the resource
    if ((hResource = LoadResource(hinstance, hResInfo)) == NULL)
        return(NULL);

    // finally get and return a pointer to the resource
    return ((UCHAR*)LockResource(hResource));
}


总结

以上就是关于资源的部分。Windows编程比想象的容易吧。学了这么多,好像还是不能做什么,所以,下一章,我将向你介绍一些基本的Windows图形设备接口函数,你就可以利用我们所学过的所有东西作一点作品出来了。
 
    • 评论
    • 分享微博
    • 分享邮件
    邮件订阅

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

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