构建框架和窗口
在开始编写DirectX11代码之前,我建议先写一个简单的程序框架。这个框架实现基本的窗口功能,组织清晰可读性强同时又能容易的添加代码。当然本教程的目的仅仅是使用DirectX11的新特性,我们会让架构尽可能的简单。
框架包含四个结构。WinMain函数作为程序的进入点。系统类(SystemClass)囊括了所有WinMain需要调用的函数。在系统类里包括一个输入类,用来处理用户的输入,还有一个图形类,来处理 DirectX相关代码。下面是系统结构图
现在我们知道了程序框架的结构,让我们从 main.cpp文件的WinMain 函数开始吧。
main.cpp
// Filename: main.cpp
#include "systemclass.h"
int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, PSTR pScmdline, int iCmdshow)
{
SystemClass* System;
bool result;
// Create the system object.
System = new SystemClass;
if(!System)
{
return 0;
}
// Initialize and run the system object.
result = System->Initialize();
if(result)
{
System->Run();
}
// Shutdown and release the system object.
System->Shutdown();
delete System;
System = 0;
return 0;
}
WinMain函数尽可能的简单。我们创建了系统类(SystemClass)然后初始化。如果初始化成功,我们则调用系统类(SystemClass)的Run 函数。Run函数会运行自己的循环处理程序的所有消息直到退出。Run函数执行完后,我们会调用ShutDown()关闭系统类(SystemClass)对象,然后删除该类对象。这样我们让执行过程变得非常简单同时系统类(SystemClass)包含了所有的程序功能。现在让我们来看看系统类(SytemClass)的头文件。
Systemclass.h
Systemclass.h
// Filename: systemclass.h
#ifndef _SYSTEMCLASS_H_
#define _SYSTEMCLASS_H_
我们在这里声明一个WIN32_LEAN_AND_MEAN。这样做是为了加快编译速度,它不包含某些很少使用的
API函数的头文件,减少Win32头文件的大小。
///
// PRE-PROCESSING DIRECTIVES //
///
#define WIN32_LEAN_AND_MEAN
Windows.h包含创建和销毁窗口的函数,还有其他很有用的 Win32函数
//
// INCLUDES //
//
#include <windows.h>
我们要包含其他两个类的头文件,这样我们就能在系统类里定义着俩个类。
///
// MY CLASS INCLUDES //
///
#include "inputclass.h"
#include "graphicsclass.h"
定义类非常的简单。我们知道Initialize,shutdown,和Run函数在WinMain中被调用。
一些私有函数会被这三个函数调用。我们会在类里添加一个消息响应函数,用来处理运行时发送给窗口系统的消息。
最后我们还要加入两个私有指针变量m_Input和m_Graphics,分别指向处理输入和渲染的类。
// Class name: SystemClass
class SystemClass
{
public:
SystemClass();
SystemClass(const SystemClass&);
~SystemClass();
bool Initialize();
void Shutdown();
void Run();
LRESULT CALLBACK MessageHandler(HWND, UINT, WPARAM, LPARAM);
private:
bool Frame();
void InitializeWindows(int&, int&);
void ShutdownWindows();
private:
LPCWSTR m_applicationName;
HINSTANCE m_hinstance;
HWND m_hwnd;
InputClass* m_Input;
GraphicsClass* m_Graphics;
};
/
// FUNCTION PROTOTYPES //
/
static LRESULT CALLBACK WndProc(HWND, UINT, WPARAM, LPARAM);
/
// GLOBALS //
/
static SystemClass* ApplicationHandle = 0;
#endif
WndProc函数和ApplicationHandle和ApplicationHandle 指针也包含在这个类中,这样我们就能把 windows系统消息重定向到我们自己写的消息响应函数里。
下面让我们再来看看系统类的源文件
Systemclass.cpp
// Filename: systemclass.cpp
#include "systemclass.h"
在类的构造函数中,我初始化对象的指针为空。这很重要,因为如果这些对象的初始化失败Shutdown函数将会尝试释放掉这些对象。如果对象不是空,就会假设它们已经成功初始化,最后需要被释放。在编程时,把指针和对象全部初始化为空是一个良好的习惯。如果你不初始化有可能在编译 release版的时候失败。
这里我写了一个空的拷贝构造函数和空的类析构函数。在这个类里我不会用到它们。但如果不定义,有些编译器会自动为你加上这两个函数的实现,而有些情况下我希望这两个函数为空。
你可以注意到,我没有在析构函数里对类对象进行释放。我把所有的释放工作都放到了 Shutdown函数里,你会在下面的代码中看到。理由就是我不确定析构函数会被调用。像 windows系统函数ExitThread()执行时不会调用类的析构函数,造成内存泄露。当然你能调用这个函数的安全版本,但在 windows下编程我要非常谨慎。
SystemClass::SystemClass(const SystemClass& other)
{
}
SystemClass::~SystemClass()
{
}
接下来是初始化函数Initialize,这个函数执行程序所有的设置。首先调用 InitializeWindows函数来为程序创建窗口。它同时创建和初始化 input类对象和graphic 类对象,程序将通过这两个类处理用户的输入和渲染图形到显示器。
bool SystemClass::Initialize()
{
int screenWidth, screenHeight;
bool result;
// Initialize the width and height of the screen to zero before sending the variables into the function.
screenWidth = 0;
screenHeight = 0;
// Initialize the windows api.
InitializeWindows(screenWidth, screenHeight);
// Create the input object. This object will be used to handle reading the keyboard input from the user.
m_Input = new InputClass;
if(!m_Input)
{
return false;
}
// Initialize the input object.
m_Input->Initialize();
// Create the graphics object. This object will handle rendering all the graphics for this application.
m_Graphics = new GraphicsClass;
if(!m_Graphics)
{
return false;
}
// Initialize the graphics object.
result = m_Graphics->Initialize(screenWidth, screenHeight, m_hwnd);
if(!result)
{
return false;
}
return true;
}
ShutDown函数执行清理任务。它关闭和释放所有图形类和输入类有关的东西。同时关闭窗口,释放掉相应的句柄。
void SystemClass::Shutdown()
{
// Release the graphics object.
if(m_Graphics)
{
m_Graphics->Shutdown();
delete m_Graphics;
m_Graphics = 0;
}
// Release the input object.
if(m_Input)
{
delete m_Input;
m_Input = 0;
}
// Shutdown the window.
ShutdownWindows();
return;
}
Run函数是程序的循环体同时执行所有的程序进程直到我们决定退出。程序的处理是在Frame函数中完成的,每一次的循环都会调用这个函数。这是一个重要的概念,从现在开始,在完成程序剩余部分的时候必须把这个概念牢记在心。下面是这个函数的伪代码:
while not done
check for windowsmessages
process systemmessages
process applicationloop
check if user want toquit during the frame processing
void SystemClass::Run()
{
MSG msg;
bool done, result;
// Initialize the message structure.
ZeroMemory(&msg, sizeof(MSG));
// Loop until there is a quit message from the window or the user.
done = false;
while(!done)
{
// Handle the windows messages.
if(PeekMessage(&msg, NULL, 0, 0, PM_REMOVE))
{
TranslateMessage(&msg);
DispatchMessage(&msg);
}
// If windows signals to end the application then exit out.
if(msg.message == WM_QUIT)
{
done = true;
}
else
{
// Otherwise do the frame processing.
result = Frame();
if(!result)
{
done = true;
}
}
}
return;
}
程序所有的处理都在Frame函数中完成。目前这个函数还比较简单,我们判断输入对象看用户是否按下ESC健退出。如果不是,我们会调用图形对象完成框架的处理,图像对象会完成图像的渲染。随着程序的完善,我们会在这里添加更多的代码。
bool SystemClass::Frame()
{
bool result;
// Check if the user pressed escape and wants to exit the application.
if(m_Input->IsKeyDown(VK_ESCAPE))
{
return false;
}
// Do the frame processing for the graphics object.
result = m_Graphics->Frame();
if(!result)
{
return false;
}
return true;
}
MessangeHander函数会接受我们输入的窗口系统消息。这样我们就能监听我们感兴趣的消息。目前我们仅仅判断按键按下和释放的消息,我们会把这个消息传入input对象。我们会把其他消息交给窗口默认的消息处理函数处理。
LRESULT CALLBACK SystemClass::MessageHandler(HWND hwnd, UINT umsg, WPARAM wparam, LPARAM lparam)
{
switch(umsg)
{
// Check if a key has been pressed on the keyboard.
case WM_KEYDOWN:
{
// If a key is pressed send it to the input object so it can record that state.
m_Input->KeyDown((unsigned int)wparam);
return 0;
}
// Check if a key has been released on the keyboard.
case WM_KEYUP:
{
// If a key is released then send it to the input object so it can unset the state for that key.
m_Input->KeyUp((unsigned int)wparam);
return 0;
}
// Any other messages send to the default message handler as our application won't make use of them.
default:
{
return DefWindowProc(hwnd, umsg, wparam, lparam);
}
}
}
我们把创建窗口的代码写在InitializeWindows函数里,将来我们会通过这个窗口渲染输出图像。调用该函数会返回屏幕的宽和高,这样我们能在程序中使用这两个参数。我们会用默认的参数创建一个无边框的全黑窗口。通过判断FULL_SCREEN这个全局变量,程序能创建一个普通或者全屏的窗口。如果这个变量的值是TRUE生成的窗口会覆盖整个屏幕。如果这个变量的值是FALSE,程序则会在屏幕中间生成一个800x600的窗口。我把FULL_SCREEN全局变量写在了graphicclass.h的开头,方便你去修改它。以后你就会明白我为什么把全局变量放在graphicclass.h文件里而不是这个类的头文件中。
void SystemClass::InitializeWindows(int& screenWidth, int& screenHeight)
{
WNDCLASSEX wc;
DEVMODE dmScreenSettings;
int posX, posY;
// Get an external pointer to this object.
ApplicationHandle = this;
// Get the instance of this application.
m_hinstance = GetModuleHandle(NULL);
// Give the application a name.
m_applicationName = L"Engine";
// Setup the windows class with default settings.
wc.style = CS_HREDRAW | CS_VREDRAW | CS_OWNDC;
wc.lpfnWndProc = WndProc;
wc.cbClsExtra = 0;
wc.cbWndExtra = 0;
wc.hInstance = m_hinstance;
wc.hIcon = LoadIcon(NULL, IDI_WINLOGO);
wc.hIconSm = wc.hIcon;
wc.hCursor = LoadCursor(NULL, IDC_ARROW);
wc.hbrBackground = (HBRUSH)GetStockObject(BLACK_BRUSH);
wc.lpszMenuName = NULL;
wc.lpszClassName = m_applicationName;
wc.cbSize = sizeof(WNDCLASSEX);
// Register the window class.
RegisterClassEx(&wc);
// Determine the resolution of the clients desktop screen.
screenWidth = GetSystemMetrics(SM_CXSCREEN);
screenHeight = GetSystemMetrics(SM_CYSCREEN);
// Setup the screen settings depending on whether it is running in full screen or in windowed mode.
if(FULL_SCREEN)
{
// If full screen set the screen to maximum size of the users desktop and 32bit.
memset(&dmScreenSettings, 0, sizeof(dmScreenSettings));
dmScreenSettings.dmSize = sizeof(dmScreenSettings);
dmScreenSettings.dmPelsWidth = (unsigned long)screenWidth;
dmScreenSettings.dmPelsHeight = (unsigned long)screenHeight;
dmScreenSettings.dmBitsPerPel = 32;
dmScreenSettings.dmFields = DM_BITSPERPEL | DM_PELSWIDTH | DM_PELSHEIGHT;
// Change the display settings to full screen.
ChangeDisplaySettings(&dmScreenSettings, CDS_FULLSCREEN);
// Set the position of the window to the top left corner.
posX = posY = 0;
}
else
{
// If windowed then set it to 800x600 resolution.
screenWidth = 800;
screenHeight = 600;
// Place the window in the middle of the screen.
posX = (GetSystemMetrics(SM_CXSCREEN) - screenWidth) / 2;
posY = (GetSystemMetrics(SM_CYSCREEN) - screenHeight) / 2;
}
// Create the window with the screen settings and get the handle to it.
m_hwnd = CreateWindowEx(WS_EX_APPWINDOW, m_applicationName, m_applicationName,
WS_CLIPSIBLINGS | WS_CLIPCHILDREN | WS_POPUP,
posX, posY, screenWidth, screenHeight, NULL, NULL, m_hinstance, NULL);
// Bring the window up on the screen and set it as main focus.
ShowWindow(m_hwnd, SW_SHOW);
SetForegroundWindow(m_hwnd);
SetFocus(m_hwnd);
// Hide the mouse cursor.
ShowCursor(false);
return;
}
ShutDownWindows函数仅仅把窗口设置回普通模式并释放与类相关的窗口和句柄。
void SystemClass::ShutdownWindows()
{
// Show the mouse cursor.
ShowCursor(true);
// Fix the display settings if leaving full screen mode.
if(FULL_SCREEN)
{
ChangeDisplaySettings(NULL, 0);
}
// Remove the window.
DestroyWindow(m_hwnd);
m_hwnd = NULL;
// Remove the application instance.
UnregisterClass(m_applicationName, m_hinstance);
m_hinstance = NULL;
// Release the pointer to this class.
ApplicationHandle = NULL;
return;
}
WndProc函数接收窗口消息的地方。你会注意到在InitializeWindows函数中,当我们初始化窗口类的时候,通过 wc.lpfnWndProc =WndProc把函数的名字告诉了窗口。我包含头文件在这个类文件中自从我们绑定它到系统类通过发送所有的消息到MessageHandler函数在SystemClass中定义的。这允许我们得到消息,然后把消息传入我们的类中同时保持代码整洁。
LRESULT CALLBACK WndProc(HWND hwnd, UINT umessage, WPARAM wparam, LPARAM lparam)
{
switch(umessage)
{
// Check if the window is being destroyed.
case WM_DESTROY:
{
PostQuitMessage(0);
return 0;
}
// Check if the window is being closed.
case WM_CLOSE:
{
PostQuitMessage(0);
return 0;
}
// All other messages pass to the message handler in the system class.
default:
{
return ApplicationHandle->MessageHandler(hwnd, umessage, wparam, lparam);
}
}
}
sysInputclass.h
为了保证教程简单,直到我写有关DirectInput(属于高级篇)教程之前,我会使用windows的输入方式。输入类处理用户来自键盘的输入。这个类的输入来自SystemClass::MessageHandler函数。
输入对象会把每个键的状态放入keyboard数组中。当遍历这个数组的时候,函数就能知道某个键是否被按下。下面是头文件。
// Filename: inputclass.h
#ifndef _INPUTCLASS_H_
#define _INPUTCLASS_H_
// Class name: InputClass
class InputClass
{
public:
InputClass();
InputClass(const InputClass&);
~InputClass();
void Initialize();
void KeyDown(unsigned int);
void KeyUp(unsigned int);
bool IsKeyDown(unsigned int);
private:
bool m_keys[256];
};
#endif
InputClass.cpp
// Filename: inputclass.cpp
#include "inputclass.h"
InputClass::InputClass()
{
}
InputClass::InputClass(const InputClass& other)
{
}
InputClass::~InputClass()
{
}
void InputClass::Initialize()
{
int i;
// Initialize all the keys to being released and not pressed.
for(i=0; i<256; i++)
{
m_keys[i] = false;
}
return;
}
void InputClass::KeyDown(unsigned int input)
{
// If a key is pressed then save that state in the key array.
m_keys[input] = true;
return;
}
void InputClass::KeyUp(unsigned int input)
{
// If a key is released then clear that state in the key array.
m_keys[input] = false;
return;
}
bool InputClass::IsKeyDown(unsigned int key)
{
// Return what state the key is in (pressed/not pressed).
return m_keys[key];
}
Graphicsclass.h
图形类是另一个系统类创建的对象。所有和图形相关的功能都包括在这个类中。我也将使用头文件在这个文件为了所有与图形有关的全局变量那些我们想要修改的像全屏或窗口模式。目前这个类是空的但在将来的教程中这个类会包含所有的图形对象。
•-
// Filename: graphicsclass.h
#ifndef _GRAPHICSCLASS_H_
#define _GRAPHICSCLASS_H_
//
// INCLUDES //
//
#include <windows.h>
/
// GLOBALS //
/
const bool FULL_SCREEN = false;
const bool VSYNC_ENABLED = true;
const float SCREEN_DEPTH = 1000.0f;
const float SCREEN_NEAR = 0.1f;
程序运行的时候需要这四个变量
// Class name: GraphicsClass
class GraphicsClass
{
public:
GraphicsClass();
GraphicsClass(const GraphicsClass&);
~GraphicsClass();
bool Initialize(int, int, HWND);
void Shutdown();
bool Frame();
private:
bool Render();
private:
};
#endif
Graphicsclass.cpp
目前我让这个类空着,因为本次的教程仅仅是要完成框架。
// Filename: graphicsclass.cpp
#include "graphicsclass.h"
GraphicsClass::GraphicsClass()
{
}
GraphicsClass::GraphicsClass(const GraphicsClass& other)
{
}
GraphicsClass::~GraphicsClass()
{
}
bool GraphicsClass::Initialize(int screenWidth, int screenHeight, HWND hwnd)
{
return true;
}
void GraphicsClass::Shutdown()
{
return;
}
bool GraphicsClass::Frame()
{
return true;
}
bool GraphicsClass::Render()
{
return true;
}
总结
现在我们有了一个框架和一个能显示在屏幕正中间的窗口。以后的教程都会以这个框架为基础,所以理解这个框架的工作方式是非常重要的。在进入下一个教程之前请试着做下面的练习确保代码正确并能运行。如果你不理解这个框架,也能进入到接下来的教程,在以后的教程中你会对整个框架有更深的理解。
练习
把FULL_SCREEN参数改为true,重新编译运行程序。按下ESC键退出程序。m