今天,我要跟你分享开发Windows游戏的第一步,建立窗体。

上一节,我讲解Python和C++的编译器,以及它们各自对应的IDE该如何选择,并且测试了C/C++的运行,编译了一个Lua静态库。准备工作基本上算是完成了。

如果你有一些编程功底,应该知道建立Windows的窗体所需的一些基础知识。如果你经验稍丰富一些,还应该知道Delphi、C++Builder、C#等等。这些工具都可以帮助你非常方便地做出一个空白窗体,但是这些窗体并没有游戏的绘图系统,所以它们只是“建立了一个标准窗体”而已。因此,虽然建立窗体是我们这一节的内容,但我们要探讨的是,在窗体背后,Windows系统做了什么。

Windows窗体由哪些部分构成?

我们常规意义上的Windows窗体,由下列几个部分组成。

我们来看一张标准的Windows窗体截图,这个软件名是Notepad++。

这是MSDN上对于窗体结构的说明:

typedef struct tagWNDCLASSEX {
  UINT      cbSize; //结构体大小,等于 sizeof(WNDCLASSEX)
  UINT      style;  //窗体的风格
  WNDPROC   lpfnWndProc; //窗体函数指针
  int       cbClsExtra;  //附加在窗体类后的字节数,初始化是零
  int       cbWndExtra;  //附加在窗体实例化的附加字节数。系统初始化是零,如果一个应用程序使用WNDCLASSEX注册一个通过在资源中使用CLASS指令建立的对话框时,必须把这个成员设成DLGWINDOWEXTRA。
  HINSTANCE hInstance; //该对象的实例句柄
  HICON     hIcon;     //该对象的图标句柄
  HCURSOR   hCursor;   //该对象的光标句柄
  HBRUSH    hbrBackground; //该对象的背景刷子
  LPCTSTR   lpszMenuName;  //菜单指针
  LPCTSTR   lpszClassName;  //类名指针
  HICON     hIconSm;       //与窗体关联的小图标,如果这个值为NULL,那么就把hIcon转换为大小比较合适的小图标
} WNDCLASSEX, *PWNDCLASSEX;

使用C/C++编写Windows窗体

接下来,我将使用C/C++IDE来编写代码,完成一个默认窗体的开发,并让它运行起来。

#include <windows.h>
LRESULT CALLBACK WindowProcedure(HWND, UINT, WPARAM, LPARAM);
char szClassName[ ] = "WindowsApp";
int WINAPI WinMain(HINSTANCE hThisInstance, HINSTANCE hPrevInstance, LPSTR lpszArgument, int nFunsterStil)

 {
    HWND hwnd;               /* 指向我们窗体的句柄 */
    MSG messages;            /* 保存发往应用的消息 */
    WNDCLASSEX wincl;        /* 前面详细介绍过的WNDCLASSEX结构的对象 */
    wincl.hInstance = hThisInstance;
    wincl.lpszClassName = szClassName;
    wincl.lpfnWndProc = WindowProcedure;      
    wincl.style = CS_DBLCLKS;                
    wincl.cbSize = sizeof(WNDCLASSEX);

上述代码开始给WNDCLASSEX结构对象赋值。

 /* 使用默认图标以及鼠标指针 */
    wincl.hIcon = LoadIcon(NULL, IDI_APPLICATION);
    wincl.hIconSm = LoadIcon(NULL, IDI_APPLICATION);
    wincl.hCursor = LoadCursor(NULL, IDC_ARROW);
    wincl.lpszMenuName = NULL; /* 没有菜单栏 */
    wincl.cbClsExtra = 0;                      /* 没有多余的字节跟在窗体类的后面 */
    wincl.cbWndExtra = 0;                      
    wincl.hbrBackground = (HBRUSH) GetStockObject(LTGRAY_BRUSH);
    if(!RegisterClassEx(&wincl)) return 0;

代码在窗口过程调用函数的时候,将地址赋值给lpfnWndProc,然后呼叫RegisterClassEx(&wincl)注册窗口类,系统就拥有了窗口过程函数的地址。如果注册失败,则返回0。

 hwnd = CreateWindowEx( 0,            /* 扩展风格为0*/                               
           szClassName,         /* 类名 */
           "Windows App",         /* 窗体抬头标题 */
           WS_OVERLAPPEDWINDOW, /* 默认窗体 */
           CW_USEDEFAULT,       /* 让操作系统决定窗体对应Windows的X位置在哪里 */
           CW_USEDEFAULT,       /* 让操作系统决定窗体对应Windows的Y位置在哪里 */
           544,                 /* 程序宽度 */
           375,                 /* 程序高度 */
           HWND_DESKTOP,        /* 父窗体的句柄,父窗体定义为Windows桌面,HWND_DESKTOP 是系统定义的最顶层的托管的窗体 */
           NULL,                /* 没有菜单 */
           hThisInstance,       /* 程序实例化句柄 */
           NULL                 /* 指向窗体的创建数据为空 */
           );
    ShowWindow(hwnd, nFunsterStil);
    /* 要显示窗体,使用的是ShowWindow函数 */
    while(GetMessage(&messages, NULL, 0, 0))
    {      
           TranslateMessage(&messages);
           DispatchMessage(&messages);    
    }
    return messages.wParam;
}

建立并显示窗体,在循环内将虚拟键消息转换为字符串消息,随后调度一个消息给窗体程序。

LRESULT CALLBACK WindowProcedure(HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam)
{
    switch (message)                  /* 指向消息的句柄 */
    {
           case WM_DESTROY:
           PostQuitMessage(0);        
           break;
           default:                   
           return DefWindowProc(hwnd, message, wParam, lParam);
                 
    }
    return 0;
}

最后是消息处理。当窗体程序接收到某些操作的时候,比如键盘、鼠标等等,就会呼叫 DispatchMessage(&messages);函数将消息回调给系统,系统通过注册的窗口类得到函数指针并且通过函数指针调用函数对消息进行处理。

还有一个经常用到的函数就是MoveWindow,就是移动已经建立的窗体。MoveWindow函数用来改变窗口的位置和尺寸,如果窗体本身就按照计算机的屏幕对齐左上角,对于窗体内的子窗体,就对齐父窗体的左上角。

BOOL MoveWindow( HWND hWnd,/* 窗体句柄 */
         int x,  /* 窗体左上角起点x轴 */
         int y,  /* 窗体左上角起点y轴 */
         int nWidth, /* 窗体宽度 */
         int nHeight, /* 窗体高度 */
         BOOL bRepaint = TRUE /* 是否重新绘制,如果是true系统会发送WM_PAINT到窗体,然后呼叫UpdateWindow函数进行重新绘制,如果是false则不重新绘制*/
        );

MoveWindow会给窗体发送WM_WINDOWPOSCHANGING,WM_WINDOWPOSCHANGED,WM_MOVE,WM_SIZE和WM_NCCALCSIZE消息。

类似的功能还有SetWindowPos,SetWindowPos功能更强大,可以设置更多的参数。

这是基本的使用C/C++绘制Windows窗体的流程,也是标准的Windows窗体的创建和显示。在后续的分享中,我也会使用GDI或者GDI+来绘制一些的内容。

使用Python编写Windows窗体

说完了C/C++系统编程编写的Windows窗体,接下来来看一下,如何使用Python来编写Windows窗体。

Python的Windows窗体编程一般会使用默认的Tinker库。不过用别的窗体库也可一建立一个窗体,比如Python版本的QT库或者wxPython。

现在来看一下,使用默认的Tinker来建立一个窗体。

import Tkinter

def my_window(w, h):
  ws = root.winfo_screenwidth()
  hs = root.winfo_screenheight()
  x = (ws/2) - (w/2)
  y = (hs/2) - (h/2)
  root.geometry("%dx%d+%d+%d" % (w, h, x, y))

root = Tkinter.Tk(className='python windows app')
my_window(100, 100)
root.mainloop()

运行的结果是这样的。

我们可以看到左上角有一个Tk的标识,这是Tinker的默认图标。目前,我们只是建立了一个Windows的窗体,并不能直接编写游戏。除此之外,我们还必须要知道这些建立窗体的具体的细节。

不过,就像前面的文章所说,OpenGL并不附带任何关联窗体的编程,所以如果你使用的是OpenGL的接口来编写代码,稍微修改一下,这些窗体就能成为游戏屏幕窗体。

游戏所有的内容都是在一个循环内完成的,即我们所有的绘图、线程、操作、刷新,都在一个大循环内完成,类似我们在前面看到的代码。

while(GetMessage(&messages, NULL, 0, 0))
  {
      TranslateMessage(&messages);
      DispatchMessage(&messages);
  }

以及使用Python编写的代码的窗体中,也会看到一个循环函数:

root.mainloop()

在这个while循环中,消息的派发都在此完成。游戏也一样,我们所有游戏内的代码几乎都在循环内完成。你可以想象一个循环完成一个大的绘制过程,第二个循环刷新前一次绘制过程,最终类似电影一样,完成整个动画的绘制以及不间断的操作。

在建立Windows窗体的时候,程序会从入口函数WinMain开始运行,定义和初始化窗体类,然后将窗体类实例化,随后进行消息循环获取消息,然后将消息发送给消息处理函数,最后做出相应的操作。

小结

总结一下今天所说的内容,我们编写了一个标准的Windows窗体,在编写的过程中:

最后,给你留一道小思考题吧。

你经常会看到有一些游戏是需要全屏才能进行的。既然我们在这里建立了一个窗体,那请问你,全屏是怎么做到的呢?

欢迎留言说出你的看法,我在下一节的挑战中等你!

评论