科技行者

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

知识库

知识库 安全导航

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

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

  • 扫一扫
    分享文章到微信

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

C++WINDOWS编程基础。

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

关键字: 游戏 编程

  • 评论
  • 分享微博
  • 分享邮件
第一章 Windows编程基础

简介

本章目的是介绍WINDOWS编程基础。在本章结束时,你应该能够很好的工作了,虽燃可能是简单的WINDOWS程序。你需要有C语言的基础知识,我很少将C++的代码扩充到程序中。当然,由于WINDOWS本身就是面向对象的,一点类的知识是不会对你有什么损害的。如果你不熟悉C++,没有关系,我想你还是能从我这里学到大部分的东西。所有的程序代码都通过了MICROSOFT VISUAL C++6.0的编译,如果你还没有合适的编译器,那就用它好了,还是很棒的。开动吧!

多数的Windows程序都需要Windows.h和Windowsx.h这两个头文件,要确保使用它们。当然,你还需要其它的标准的C的头文件,象stdio.h,conio.h等。除了这些,你还会经常看到在程序的开始有这样一行代码:

#define WIN32_LEANAND_MEAN


它表示Windows的头文件中将拒绝接受MFC的东西,这将加速你的build时间。如果你从没有打算应用MFC在你的游戏编程中,那就使用它吧。如果你以前从没有看过这种声明类型——在#define后,直接加上一个“单词”,那么它的作用就是有条件编译。看看下面的例子:

#ifdef DEBUG_MODE
    printf("Debug mode is active!");
#endif


意思是:如果程序的开始包含#define DEBUG_MODE,那么就printf(),否则退出。这个对于你跟踪程序的逻辑错误是很有帮助的。

WinMain()函数

DOS下的C语言从main()开始,Windows下的C语言从WinMain()开始,一个空的WinMain()函数是这样的:

int WINAPI WinMain(HINSTANCE hinstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nCmdShow)
{
    return(0);
}


一个函数即使什么也没做,也应该返回一个值。是的,有好多东西我们不熟悉。首先的首先,WINAPI是个什么声明?WINAPI是在windows.h头文件中定义的一个宏,它把函数调用翻译成正确的调用约定。当我们在程序中需要用到汇编语言的时候,我们在来深究它好了,记住,如果要用WinMain(),就必须要有WINAPI。

下一步让我们来看看括号里的四个参数:

◎ HINSTANCE hinstance:HINSTANCE是一个句柄类型的标识符。变量hinstance是一个整数,用于标识程序实例。Windows设置这个参数的值,并把它传递给你的程序代码。很多Windows函数都要用到它。

◎ HINSTANCE hPreInstance:你不用担心这个参数,它已经被废掉了。它只是为古老的Windows版本服务的。你将还会看到类似的情况。

◎ LPSTR lpCmdLine:是一个指向字符串的指针,它仅在程序名是从DOS命令行输入或是从Run对话框中输入时才起作用。因此,很少被程序代码所用。

◎ int nCmdShow:决定了窗口在初始显示时的状态。Windows通常给这个参数分配一个值。通常是SW_打头的一个常量。例如SW_SHOWNORMAL表示默认状态,SW_MAXINIZE或SW_MINIMIZE分别表示最大和最小模式等等。

以上大体上是WinMain()的参数的介绍。下面对变量、参数、常量、类等的命名方法介绍一下。

一、消息

当你在DOS下编程的时候,你不必担心其它程序的运行,因为DOS是独占模式。但你在Windows平台上编程时,你不得不考虑其它正在运行的程序。鉴于此,Windows通过“消息”来连接操作申请和具体操作。简单的说,就是我们指示程序或程序本身向Windows发出诸如移动窗口、放大窗口、关闭窗口等申请,Windows再根据申请,考察实地情况,拒绝或发出指令,让程序(计算机)作出相应的动作。再例如,鼠标随时向Windows发出消息,汇报光标位置,左键或右键是否按下等,Windows再根据消息作出相应的反应。总之,无论何时,Windows都要随时掌控所有的消息,而且,Windows是一直不断地接收到各种消息。

这种功能是通过一种被命名为CALLBACK函数类型实现的。不用害怕,消息传递来传递去都是由Windows自己完成的,你只要声明一个CALLBACK函数就可以了,就像WINAPI用在WinMain()前一样。如果还没有明白,不要紧,往下看你就明白了。现在,我要离开这个话题一会儿,因为你只有先建立窗口(Windows),传递消息才有可能实现。 二、窗口类

现在谈论一点C++的知识,因为要想建立一个窗口,你就得先建立一个窗口类。窗口类包含所有的有关窗口的信息,如用什么样的鼠标符号,菜单样式等等。开发任何一个窗口程序,都离不开窗口类的建立。为了达到此目的,你必须填写WNDCLASSEX结构。EX的意思是“扩充”的意思,因为有一个老的结构叫作WNDCLASS,这里,我们将使用WNDCLASSEX结构,它的样子如下:

typedef struct _WNDCLASSEX {
        UINT cbSize;
        UINT style;
        WNDPROC lpfnWndProc;
        int cbClsExtra;
        int cbWndExtra;
        HANDLE hInstance;
        HICON hIcon;
        HCURSOR hCursor;
        HBRUSH hbrBackground;
        LPCTSTR lpszMenuName;
        LPCTSTR lpszClassName;
        HICON hIconSm;
} WNDCLASSEX;


这个结构有不少成员,讨厌的是,你必须为窗口类设置每一个成员。莫发愁,纸老虎一个。让我们来个速成。

※ UINT cbSize:指定了以字节为单位的结构的大小。这个成员是通过sizeof(WNDCLASSEX)实现的。你将会经常看到它,尤其是你使用了DirectX。

※ UINT style:指定了窗口的风格。它经常被以CS_打头的符号常量定义。两种或两种以上的风格可以通过C语言中的“或”(|)运算符加以组合。大多数情况我们只应用四种风格,出于对文章长度的考虑,我们只列出这四种。若你还需要其它的,到MSDN里找一下好了。当然前提是你使用的是Visual C++。

◎ CS_HREDRAW:一旦移动或尺寸调整使客户区的宽度发生变化,就重新绘制窗口。

◎ CS_VREDRAW:一旦移动或尺寸调整使客户区的高度发生变化,就重新绘制窗口。

◎ CS_OWNDC:为该类中的每一个窗口分配一个唯一的设备上下文。

◎ CS_DBLCLKS:当用户双击鼠标时向窗口过程发送双击消息。

※ WNDPROC lpfnWndProc:是指向窗口过程的指针。一般都指向CALLBACK函数。如果你没有用过函数指针,简单理解为函数的地址就是函数的名字,名字后面别带括号。

※ int cbClsExtra:它是为类保留的额外信息 。大多数程序员不用它,你在在写游戏程序时也不太可能用它,所以,设为0好了。

※ int cbWndExtra:同上一个差不多,设为0好了。

※ HANDLE hInstance:是指向窗口过程实例的句柄。同时也是WinMain()函数的参数之一。应该设置为hinstance。

※ HICON hIcon:指向窗口图标的句柄,它通常被LoadIcon()函数设置。在你学会如何在你的程序中使用资源前,你先设置成如下样子:LoadIcon(NULL,IDI_WINLOGO)。当然,还有一些其它的IDI_打头的符号常量,可以自己去帮助文件里寻找。

※ HCURSOR hCursor:指向窗口光标的句柄,它通常被LoadCursor()函数设置,在你学会如何在你的程序中使用资源前,你先用Windows默认的吧,LoadCursor(NULL,IDC_ARROW)。

※ HBRUSH hbrBackground:当你的窗口过程得到消息,要求刷新(或重画)窗口时,至少要用一种纯色或“brush”(画刷)重画窗口区域,画刷是由参数确定的。你可以使用GetStockObject()函数调用几种常备的画刷,如BLACK_BRUSH, WHITE_BRUSH, GRAY_BRUSH等。现在,你就用GetStockObject(BLACK_BRUSH)吧。也许你觉得我说的太简单了,是因为不想把开始弄得太复杂。在以后的篇幅里会详细讲述的。

※ LPCTSTR lpszMenuName:如果你想建立一个有下拉菜单的窗口,你得给这个参数赋一个菜单名称(这涉及到资源),由于你还不知道怎么创建菜单,你就先用NULL设置成一个没有菜单的窗口吧。

※ LPCSTR lpszClassName:很显然,你需要给类起个名字,随你便,如“**”。要用双引号引上。

※ HICON hIconSm:指向小图标的句柄。小图标用来显示在窗口的标题栏里。要用到LoadIcon()函数,现在,先用Windows默认的吧,LoadIcon(NULL,IDI_WINLOGO)。

好了,现在你关于WNDCLASSEX结构知道的已经差不多了,你可以自己设置它了。下面是一个例子:

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; // we need to write this!
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


我想,现在你已经有点儿不太崇拜Windows程序员了吧。言归正传,有一点我得提醒你,注意函数GetStockObject()前的(HBRUSH)类型配置,这是因为GetStockObject()可以调用其它的对象,不仅仅是“brush”,所以你需要一个HBRUSH类型配置。在Visual C++旧版本里不用配置,但新的6.0版本需要它,否则会编译出错。

下一件事是注册这个窗口类,只有这样,你才能创建新的窗口。十分简单,你只需要调用一个RegisterClassEX()函数,它只有一个参数,就是你的窗口类的地址(名字),根据我上面给的例子,这里应该这样:

RegisterClassEx(&sampleClass);


我们的窗口类创建完了,我们可以用它创建一个窗口了。 三、创建窗口

好消息,创建窗口你所要做的只是调用一个CreateWindowEx()函数。坏消息是,这个函数有好多的参数。以下便是函数原形:

HWND CreateWindowEx(
        DWORD dwExStyle, // extended window style
        LPCTSTR lpClassName, // pointer to registered class name
        LPCTSTR lpWindowName, // pointer to window name
        DWORD dwStyle, // window style
        int x, // horizontal position of window
        int y, // vertical position of window
        int nWidth, // window width
        int nHeight, // window height
        HWND hWndParent, // handle to parent or owner window
        HMENU hMenu, // handle to menu, or child-window identifier
        HINSTANCE hInstance, // handle to application instance
        LPVOID lpParam // pointer to window-creation data
);


首先:函数的返回值。也就是函数的类型。是不是所有创建窗口用的函数的类型的讨厌样子都感觉亲切了一点儿?不要紧,你会习惯的,肯定比你想象的速度要快。这里返回的类型是HWND,是一个窗口的句柄(句柄就是窗口的标识符)。你将把CreateWindowEx()的返回值传递给一个窗口的句柄,就像一个参数一样。现在,我们来琢磨一下这些参数,很多根据名字就知道它是干什么的了。

※ DWORD dwExStyle:扩充的窗口风格。你将很少使用扩充的窗口风格,所以多数时间你会把它设置为NULL。如果有兴趣,查一下帮助文件,可以一试由WS_EX_打头的扩充风格。

※ LPCTSTR lpClassName:还记得你的窗口类的名称吗?再用一次。

※ LPCTSTR lpWindowName:将显示在窗口的标题栏里的简短文字。

※ DWORD dwStyle:窗口的风格。它将允许你详细的描绘你所要创建的窗口的风格。有很多风格你可以利用哦,都是以WS_打头的,你可以利用(|)符号组合利用它们。我将在这儿介绍几个常用的。

◎ WS_POPUP 指定一个弹出的窗口。

◎ WS_OVERLAPPED 指定一个具有标题栏和边界的重叠窗口。

◎ WS_OVERLAPPEDWINDOW 指定一个具有所有标准控件的窗口。

◎ WS_VISIBLE 指定一个初始时可见的窗口。

看得出,WS_OVERLAPPEDWINDOW是一个组合体。简单的说,你可以按照如下规律:如果你要创建一个可以最大化、最小化、随意改变大小等等地窗口,就选择WS_OVERLAPPEDWINDOW;如果你只想要一个具有标题栏、可改变大小的窗口,就选择WS_OVERLAPPED;如果你只想要一个光秃秃的窗口,就选择WS_POPUP;如果你只想显示一个黑色的大方框,可能你要用它写一个全屏的游戏,选择WS_VISIBLE是没错的。

※ int x,y:你所要创建的窗口的左上角的坐标。

※ int nWidth,nHeight:猜也猜到了,是窗口的长和高。

※ HWND hWndParent:指向父窗口的句柄。你若想在窗口下再建立一个窗口,那么第一个窗口就叫父窗口。咱先建立一个主窗口,所以设置为NULL,也就意味着Windows桌面是父窗口。

※ HMENU hMenu:这是用在窗口上的菜单句柄。若你学会建立和使用资源,即建立自己的菜单,你可以用LoadMenu()函数调用自己的菜单资源。目前,先设为NULL。

※ HINSTANCE hInstance:是一个名柄,它指向由Windows传递给WinMain()的实例。

※ LPVOID lpParam:对于游戏编程来说,没有什么用的东西,只有简单的窗口程序用到它。设置为NULL好了。

我们现在万事具备,东风也有了。我先给个示例:

HWND hwnd;
if (!(hwnd = CreateWindowEx(NULL,                 // extended style, not needed
                            "Sample Class",       // class identifier
                            "Sample Window",      // window title
                            WS_POPUP | WS_VISIBLE,// parameters
                            0, 0, 320, 240,       // initial position, size
                            NULL,                 // handle to parent (the desktop)
                            NULL,                 // handle to menu (none)
                            hinstance,            // application instance handle
                            NULL)))               // who needs it?
    return(0);


你可能会在游戏编程中用上这这段代码,因为它是一个弹出式窗口。注意,我用了if形式,目的是一旦CreateWindowsEX()函数失灵,返回一个NULL,也就意味着如果窗口由于某种原因不能被建立,那么WinMain()就被简单的返回,程序结束。现在我们学会了足够的知识建立一个小有功能的窗口了。还记得我们建立窗口类“sample class”时,一个指向“CALLBACK”类型函数的指针吗?对,是“lpfnWndProc”。要想让你的窗口真正做点事儿,我们还得来处理一下它指向的“窗口过程”函数。

四、显示窗口

CreateWindowEx()从内部创建窗口,但并不显示它。要显示这个窗口,必须调用另外两个函数:ShowWindow()和UpdateWindow()。头一个设置窗口的显示状态,后一个则更新窗口的客户区。对于程序的主窗口,ShowWindow()必须被调用一次,调用代码如下:

ShowWindow(hwnd,nCmdShow);


第一个参数是由CreateWindowEx()函数返回的窗口句柄;第二个参数就是窗口的显示模式参数,在☆WinMain()函数中提到过,就不重复了。UpdateWindow()函数的调用代码如下:

UpdateWindow(hwnd);


参数hwnd同ShowWindow()函数的hwnd一样。

五、消息的处理

我已经说过消息在窗口里的作用了,下面让我们来仔细学习一下它。处理消息的函数结构如下:

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


这个LRESULT类型要求返回一个32位的整数。实际取值依赖于消息,但是这个值很少在应用程序代码中得到应用。以前我们谈到过一点CALLBACK协定,它的参数很简单:

※ HWND hwnd:是接收消息的窗口的句柄,也是由CreateWindowEx()函数返回的句柄。

※ UINT msg:这是一个消息标识符,都是以WM_打头的符号常量,意思是“Windows Message”。很多的,这里只介绍一些常用的:

◎ WM_ACTIVATE:一个新窗口被激活。

◎ WM_CLOSE:一个窗口被关闭。

◎ WM_COMMAND:一个菜单功能被选择。

◎ WM_CREATE:一个窗口被建立。

◎ WM_LBUTTONDBLCLK:鼠标左键被双击。

◎ WM_LBUTTONDOWN:鼠标左键被按下。

◎ WM_MOUSEMOVE:鼠标被移动。

◎ WM_MOVE:一个窗口被移动。

◎ WM_PAINT:窗口的一部分需要重画。

◎ WM_RBUTTONDBLCLK:鼠标的右键被双击。

◎ WM_RBUTTONDOWN:鼠标的右键被按下。

◎ WM_SIZE:窗口的大小被改变。

◎ WM_USER:做你想做的。

※ WPARAM wparam,LPARAM lparam:消息参数。它们提供有关消息的附加信息,这两个值对于每条消息来说都是特定的。

你要把所有要发生的消息都写进程序代码的话,我想你可能已经累疯了。我想我会的。感谢上帝,Windows提供了默认消息处理,如果你没有任何特殊的消息需要处理了,你总是要用DefWindowPorc()函数的,下面给一个最简单的例子,没有任何特定的消息要处理的例子:

LRESULT CALLBACK MsgHandler(HWND hwnd, UINT msg, WPARAM wparam, LPARAM lparam)
{
    return(DefWindowProc(hwnd, msg, wparam, lparam));
}


简单吧!但通常你都需要处理一些自己的消息,你要写自己的程序代码,然后返回0,来告诉程序你干完了。下面是一个例子,当窗口建立时,你调用了一个初始化的函数Initialize_Game(),然后返回0,最后告诉程序自己处理那些默认的消息吧:

LRESULT CALLBACK MsgHandler(HWND hwnd, UINT msg, WPARAM wparam, LPARAM lparam)
{
    if (msg == WM_CREATE)
    {
        Initialize_Game();
        return(0);
    }

    return(DefWindowProc(hwnd, msg, wparam, lparam));
}


你很可能需要一个“switch”结构来手动完成你想要控制的消息,然后把剩下的交给DefWindowProc()去做。大功告成前,我不得不提醒您一件事,就是怎样使你的消息控制得到响应呢?

六、读取消息队列

这里先给你一个switch结构的例子吧:

LRESULT CALLBACK MsgHandler(HWND hwnd, UINT msg, WPARAM wparam, LPARAM lparam)
{
    switch(msg)
    {
    case WM_CREAT:
        [初始化游戏]
        return 0;
    case WM_PAINT:
        [画一架飞机]
        return 0;
    case ……………………
        ……………………
    }

    return(DefWindowProc(hwnd, msg, wparam, lparam));
}


在进入程序的主循环前,你需要看看你的消息控制(就是你在switch结构里编的那些),尤其是还没有用到的消息控制是否被机器存了起来,以备一旦用到,马上响应。做到正确的响应,你需要做几件事。首先你需要PeekMessage()函数。下面是它的原形:

BOOL PeekMessage(
    LPMSG lpMsg,          // pointer to structure for message
    HWND hWnd,            // handle to window
    UINT wMsgFilterMin,   // first message
    UINT wMsgFilterMax,   // last message
    UINT wRemoveMsg       // removal flags
);


这是一个布尔类型,也就是一个int型,不过只有两个值,TRUE和FALSE,如果有一条消息在队列中等待,函数返回TRUE,否则,返回FALSE。它的参数也很简单:

※ LPMSG lpMsg:这是一个MSG类型的指针变量。如果有消息在等待,消息信息将被填入该变量。

※ HWND hWnd:你所要检查的消息队列的窗口的句柄。

※ UINT wMsgFilterMin,wMsgFilterMax:索引第一个和最后一个消息,一般你都从第一个消息开始检索,所以把它们都设置为0好了。

※ UINT wRemoveMsg:一般来说,它有两个指,PM_REMOVE或者PM_NOREMOVE。使用前者会在消息被读取后从队列中移除,后者是继续保留。通常,我们选择前者PM_REMOVE。

真正处理消息时,你需要做两件事,很简单,第一件是TranslateMessage(),第二件是DispatchMessage()。它们的原形很相似:

BOOL TranslateMessage(CONST MSG *lpmsg);
LONG DispatchMessage(CONST MSG *lpmsg);


头一个是把消息翻译过来,第二个是从MSG结构中调用相应的信息。你只需要知道这么多。伴随着程序主循环的反复执行,如果有消息出现,你就调用这两个函数,函数MsgHandler()会安排好一切的。下面是个例子:

if (PeekMessage(&msg, NULL, 0, 0, PM_REMOVE))
{
    TranslateMessage(&msg);
    DispatchMessage(&msg);
}


没问题,你现在完全可以写一个窗口程序了。不坏吧?在结束本章前,我还有几点要提醒你。还记得我们在消息时,说要在后面进一步讨论它吗?那么怎样主动向Windows发送消息呢?

七、发送消息

有两种办法可以做到:PostMessage()函数或SendMessage()函数。

它们的原形很相似:

BOOL PostMessage(
            HWND hWnd,     // handle of destination window
            UINT Msg,      // message to post
            WPARAM wParam, // first message parameter
            LPARAM lParam  // second message parameter
);

LRESULT SendMessage(
            HWND hWnd,     // handle of destination window
            UINT Msg,      // message to post
            WPARAM wParam, // first message parameter
            LPARAM lParam  // second message parameter
);


它们的参数相同,并且和前面讲过的函数MsgHandler()的参数功能相同,就不重复了。现在我们只谈谈它们之间的区别。

PostMessage()被经常用来向队列中加入消息,成功,返回TRUE,否则,返回FALSE。它只是简单的把消息加入到队列中,然后返回。多数情况下,调用它将返回TRUE。

SendMessage()则有些不同,它并不是把消息加入到队列里,而是直接翻译消息和调用消息处理,直到消息处理完成后才返回。所以,SendMessage()比PostMessage()有更高的应急性。你想立刻干的事情,就应该调用它。消息是DOS和Windows编程之间重要的区别标志。

八、程序的流程

在DOS中,我们不必担心消息这种东西,不必担心多个程序同时运行,但在Windows里,你必须考虑这些。在Windows平台上编程,有一些不同于DOS下编程的地方。让我们看看下面这段虚拟的代码:

// main game loop
do
{
    // handle messages here

    // ...

    // update screen if necessary
    if (new_screen)
    {
        FadeOut();
        LoadNewMap();
        FadeIn();
    }

    // perform game logic
    WaitForInput();
    UpdateCharacters();
    RenderMap();

} while (game_active);



假设FadeOut()函数这样工作:当函数被调用,在一秒内屏幕图象暗淡下来,当屏幕完全黑了,函数返回。LoadNewMap()调用一个新的图象;FadeIn()使屏幕逐渐亮起来,好显示新图象。当有键子按下,调用WaitForInput()函数,再继续调用下去。这在DOS游戏编程里是合情合理的,但在Windows下不行。为什么呢?让我们看看新画面诞生的过程。画面逐渐变黑,调用图片,逐渐恢复。这大概要2秒钟,用户可以等待,也可能要移动一下窗口,但程序只专心的干调用图片的工作,不会对窗口的移动作出反应。这是很糟糕的,你做了机器不知道的事情,这可能导致系统崩溃,我们必须要让机器对用户的任何操作作出正确的反应。不多说了,总之你要换一换脑筋,如果你从来就没在DOS下编过程序,那正好,你赶上潮流了!

九、总结

本章我们讲了Windows编程的基础,虽然只是一个空白的窗口,但包含了最基本的东西。接下来的连载我们将学习创建资源和利用资源,你就可以用有自己风格的光标、图标、声音、菜单等等,还要生成一个EXE文件呢!

查看本文来源

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

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

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