教程2:创建框架和窗口

原文链接

在开始使用 DirectX 11 编码之前,我建议构建一个简单的代码框架。该框架将处理基本的窗口功能,并为学习 DirectX 11 提供一种以有组织和可读的方式扩展代码的简单方法。由于这些教程的目的只是尝试 DirectX 11 的不同功能,而不是构建完整的渲染引擎,所以我们将有目的地尽可能保持框架精简。一旦你掌握了DirectX 11,你就可以研究如何构建一个现代化的图形渲染引擎。

框架简介

框架从以下四部分着手搭建:

  1. WinMain 函数来处理应用程序的入口点。
  2. 定义 SystemClass,封装从 WinMain 函数中调用的整个应用程序
  3. 在系统类中,我们定义 GraphicsClass 来处理 DirectX 图形代码;
  4. 除此之外,定义 InputClass 来处理用户的输入。

框架设置示意图如下:
框架示意图

WinMain

  1. ////////////////////////////////////////////////////////////////////////////////
  2. // Filename: main.cpp
  3. ////////////////////////////////////////////////////////////////////////////////
  4. #include "systemclass.h"
  5. int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, PSTR pScmdline, int iCmdshow)
  6. {
  7. SystemClass* System;
  8. bool result;
  9. // Create the system object.
  10. System = new SystemClass;
  11. if(!System)
  12. {
  13. return 0;
  14. }
  15. // Initialize and run the system object.
  16. result = System->Initialize();
  17. if(result)
  18. {
  19. System->Run();
  20. }
  21. // Shutdown and release the system object.
  22. System->Shutdown();
  23. delete System;
  24. System = 0;
  25. return 0;
  26. }

正如您所见,我们保持了 WinMain 函数的简单性。我们创建 SystemClass,然后初始化它。如果初始化没有问题,那么我们调用 SystemClassRun 函数。

Run 函数将运行自己的循环并执行所有应用程序代码,直到完成所有逻辑。

Run 函数完成后,我们关闭 SystemClass 并对资源进行清理。

因此,我们保持了它的简单性,并将整个应用程序封装在系统类中。

现在让我们来看一下 SystemClass 头文件:

Systemclass.h

  1. ////////////////////////////////////////////////////////////////////////////////
  2. // Filename: systemclass.h
  3. ////////////////////////////////////////////////////////////////////////////////
  4. #ifndef _SYSTEMCLASS_H_
  5. #define _SYSTEMCLASS_H_

这里我们定义 WIN32_LEAN_AND_MEAN。我们这样做是为了加快构建过程:这个宏将通过排除一些较少使用的 API 来减少 Win32 头文件的大小。

  1. ///////////////////////////////
  2. // PRE-PROCESSING DIRECTIVES //
  3. ///////////////////////////////
  4. #define WIN32_LEAN_AND_MEAN

此时,我们已经在框架中包含了其他两个类的头文件,因此我们可以在 SystemClass 中使用它们。

  1. ///////////////////////
  2. // MY CLASS INCLUDES //
  3. ///////////////////////
  4. #include "inputclass.h"
  5. #include "graphicsclass.h"

Windows.h 包含在内,我们才可以通过调用函数来创建或者销毁窗口,并能够使用其他有用的 Win32 函数。

  1. //////////////
  2. // INCLUDES //
  3. //////////////
  4. #include <windows.h>

这个类的定义相当简单,我们可以看到,WinMain 中调用的 InitializeShutdownRun 函数都是在这里定义的。

除此之外,还有一些私有函数将在这些函数中调用。

我们还在类中添加了 MessageHandler 函数来处理 Windows 系统的消息,这些消息将在应用程序运行时发送到应用程序。

最后,我们定义连个私有变量:m_Inputm_Graphics,它们将分别指向处理输入和图形的两个系统对象。

  1. ////////////////////////////////////////////////////////////////////////////////
  2. // Class name: SystemClass
  3. ////////////////////////////////////////////////////////////////////////////////
  4. class SystemClass
  5. {
  6. public:
  7. SystemClass();
  8. SystemClass(const SystemClass&);
  9. ~SystemClass();
  10. bool Initialize();
  11. void Shutdown();
  12. void Run();
  13. LRESULT CALLBACK MessageHandler(HWND, UINT, WPARAM, LPARAM);
  14. private:
  15. bool Frame();
  16. void InitializeWindows(int&, int&);
  17. void ShutdownWindows();
  18. private:
  19. LPCWSTR m_applicationName;
  20. HINSTANCE m_hinstance;
  21. HWND m_hwnd;
  22. InputClass* m_Input;
  23. GraphicsClass* m_Graphics;
  24. };
  25. /////////////////////////
  26. // FUNCTION PROTOTYPES //
  27. /////////////////////////
  28. static LRESULT CALLBACK WndProc(HWND, UINT, WPARAM, LPARAM);
  29. /////////////
  30. // GLOBALS //
  31. /////////////
  32. static SystemClass* ApplicationHandle = 0;
  33. #endif

WndProc 函数和 ApplicationHandle 指针也包含在这个类文件中,因此我们可以在 SystemClass 中将 Windows 系统的消息重定向到我们自己的 MessageHandler 函数中。

现在让我们看看 SystemClass 源文件:

Systemclass.cpp

  1. ////////////////////////////////////////////////////////////////////////////////
  2. // Filename: systemclass.cpp
  3. ////////////////////////////////////////////////////////////////////////////////
  4. #include "systemclass.h"

在类构造函数中,我将成员对象指针初始化为空,这一点很重要,因为如果这些对象的初始化失败,那么接下来的 Shutdown 函数将尝试清理这些对象。

如果对象不为空,那么它会假定它们是被有效创建的对象,并且他们需要被清理。

将应用程序中的所有指针和变量初始化为空也是一种很好的习惯,如果不这样做,某些发布版本可能将生成失败。

  1. SystemClass::SystemClass()
  2. {
  3. m_Input = 0;
  4. m_Graphics = 0;
  5. }

在这里,我创建了一个空的拷贝构造函数和空的析构函数,在这个类中,我不需要它们,但如果没有定义,一些编译器将为您生成它们,既然这样的话,我更希望它们是空的。

你也会注意到我没有在析构函数中进行任何对象的清理工作,相反,我会在 Shutdown 函数中清理所有对象,关于这些内容你会在下面看到。

我这样做的原因是我不信任它总是会被调用:某些 Windows 函数(如 ExitThread())不调用类析构函数,这会导致内存泄漏。当然,现在的你可以调用这些函数的安全版本,但我只是在 Windows 上编程时更加小心。

  1. SystemClass::SystemClass(const SystemClass& other)
  2. {
  3. }
  4. SystemClass::~SystemClass()
  5. {
  6. }

下面的初始化函数将完成应用程序的所有设置:首先调用 InitializeWindows 函数,它将创建应用程序要使用的窗口,然后创建和初始化应用程序将用于处理用户输入的 input 对象和向屏幕输出图形的 graphics 对象。

  1. bool SystemClass::Initialize()
  2. {
  3. int screenWidth, screenHeight;
  4. bool result;
  5. // Initialize the width and height of the screen to zero before sending the variables into the function.
  6. screenWidth = 0;
  7. screenHeight = 0;
  8. // Initialize the windows api.
  9. InitializeWindows(screenWidth, screenHeight);
  10. // Create the input object. This object will be used to handle reading the keyboard input from the user.
  11. m_Input = new InputClass;
  12. if(!m_Input)
  13. {
  14. return false;
  15. }
  16. // Initialize the input object.
  17. m_Input->Initialize();
  18. // Create the graphics object. This object will handle rendering all the graphics for this application.
  19. m_Graphics = new GraphicsClass;
  20. if(!m_Graphics)
  21. {
  22. return false;
  23. }
  24. // Initialize the graphics object.
  25. result = m_Graphics->Initialize(screenWidth, screenHeight, m_hwnd);
  26. if(!result)
  27. {
  28. return false;
  29. }
  30. return true;
  31. }

Shutdown 函数进行清理工作:它会关闭并释放与图形和输入对象相关的所有内容。此外,它还关闭窗口并清理与之相关的句柄。

  1. void SystemClass::Shutdown()
  2. {
  3. // Release the graphics object.
  4. if(m_Graphics)
  5. {
  6. m_Graphics->Shutdown();
  7. delete m_Graphics;
  8. m_Graphics = 0;
  9. }
  10. // Release the input object.
  11. if(m_Input)
  12. {
  13. delete m_Input;
  14. m_Input = 0;
  15. }
  16. // Shutdown the window.
  17. ShutdownWindows();
  18. return;
  19. }

Run 函数是应用程序循环并执行所有应用程序逻辑的地方,直到我们决定退出。

应用程序逻辑在每次循环都被调用的 Frame 函数中完成。这是一个需要理解的重要概念,因为在编写应用程序的其余部分时,必须牢记这一点。

伪代码如下所示:

  1. while not 程序退出
  2. 检查 Windows 系统消息
  3. 处理系统消息
  4. 执行应用程序主循环
  5. 检查用户是否想要退出程序
  1. void SystemClass::Run()
  2. {
  3. MSG msg;
  4. bool done, result;
  5. // Initialize the message structure.
  6. ZeroMemory(&msg, sizeof(MSG));
  7. // Loop until there is a quit message from the window or the user.
  8. done = false;
  9. while(!done)
  10. {
  11. // Handle the windows messages.
  12. if(PeekMessage(&msg, NULL, 0, 0, PM_REMOVE))
  13. {
  14. TranslateMessage(&msg);
  15. DispatchMessage(&msg);
  16. }
  17. // If windows signals to end the application then exit out.
  18. if(msg.message == WM_QUIT)
  19. {
  20. done = true;
  21. }
  22. else
  23. {
  24. // Otherwise do the frame processing.
  25. result = Frame();
  26. if(!result)
  27. {
  28. done = true;
  29. }
  30. }
  31. }
  32. return;
  33. }

下面的 Frame 函数是完成应用程序所有逻辑的地方。

到目前为止,它相当简单,我们检查 input 对象,看看用户是否按下了 escape 键并想退出;如果他们不想退出,那么我们调用 graphics 对象来为该帧渲染图形。

随着应用程序的逐渐复杂,我们将在这里放置更多代码。

  1. bool SystemClass::Frame()
  2. {
  3. bool result;
  4. // Check if the user pressed escape and wants to exit the application.
  5. if(m_Input->IsKeyDown(VK_ESCAPE))
  6. {
  7. return false;
  8. }
  9. // Do the frame processing for the graphics object.
  10. result = m_Graphics->Frame();
  11. if(!result)
  12. {
  13. return false;
  14. }
  15. return true;
  16. }

MessageHandler 函数是我们处理 Windows 系统消息的地,我们可以监听我们感兴趣的信息。

目前,我们只需读取某个键是否被按下或被释放,然后将该信息传递给 input 对象,所有的其他消息我没将传递给 Windows 默认的消息处理函数来处理。

  1. LRESULT CALLBACK SystemClass::MessageHandler(HWND hwnd, UINT umsg, WPARAM wparam, LPARAM lparam)
  2. {
  3. switch(umsg)
  4. {
  5. // Check if a key has been pressed on the keyboard.
  6. case WM_KEYDOWN:
  7. {
  8. // If a key is pressed send it to the input object so it can record that state.
  9. m_Input->KeyDown((unsigned int)wparam);
  10. return 0;
  11. }
  12. // Check if a key has been released on the keyboard.
  13. case WM_KEYUP:
  14. {
  15. // If a key is released then send it to the input object so it can unset the state for that key.
  16. m_Input->KeyUp((unsigned int)wparam);
  17. return 0;
  18. }
  19. // Any other messages send to the default message handler as our application won't make use of them.
  20. default:
  21. {
  22. return DefWindowProc(hwnd, umsg, wparam, lparam);
  23. }
  24. }
  25. }

InitializeWindows 函数内我们编写用于构建渲染窗口的代码。它将 screenWidthscreenHeight 返回给调用函数,以便我们可以在整个应用程序中使用它们。

我们使用一些默认设置来创建并初始化窗口一个纯黑的无边框窗口。

该函数将根据名为 FULL_SCREEN 的全局变量来决定是生成一个小窗口还是一个全屏窗口。如果设置为 true,那么我们将使屏幕覆盖整个用户桌面窗口;如果设置为 false,我们只需在屏幕中间创建一个 800×600 的窗口。我将 FULL_SCREEN 全局变量放到了 graphicsclass.h 文件的顶部以便修改。

稍后你将理解为什么我将这个全局变量放在该文件内,而不是该文件所包含的其他头文件中。

  1. void SystemClass::InitializeWindows(int& screenWidth, int& screenHeight)
  2. {
  3. WNDCLASSEX wc;
  4. DEVMODE dmScreenSettings;
  5. int posX, posY;
  6. // Get an external pointer to this object.
  7. ApplicationHandle = this;
  8. // Get the instance of this application.
  9. m_hinstance = GetModuleHandle(NULL);
  10. // Give the application a name.
  11. m_applicationName = L"Engine";
  12. // Setup the windows class with default settings.
  13. wc.style = CS_HREDRAW | CS_VREDRAW | CS_OWNDC;
  14. wc.lpfnWndProc = WndProc;
  15. wc.cbClsExtra = 0;
  16. wc.cbWndExtra = 0;
  17. wc.hInstance = m_hinstance;
  18. wc.hIcon = LoadIcon(NULL, IDI_WINLOGO);
  19. wc.hIconSm = wc.hIcon;
  20. wc.hCursor = LoadCursor(NULL, IDC_ARROW);
  21. wc.hbrBackground = (HBRUSH)GetStockObject(BLACK_BRUSH);
  22. wc.lpszMenuName = NULL;
  23. wc.lpszClassName = m_applicationName;
  24. wc.cbSize = sizeof(WNDCLASSEX);
  25. // Register the window class.
  26. RegisterClassEx(&wc);
  27. // Determine the resolution of the clients desktop screen.
  28. screenWidth = GetSystemMetrics(SM_CXSCREEN);
  29. screenHeight = GetSystemMetrics(SM_CYSCREEN);
  30. // Setup the screen settings depending on whether it is running in full screen or in windowed mode.
  31. if(FULL_SCREEN)
  32. {
  33. // If full screen set the screen to maximum size of the users desktop and 32bit.
  34. memset(&dmScreenSettings, 0, sizeof(dmScreenSettings));
  35. dmScreenSettings.dmSize = sizeof(dmScreenSettings);
  36. dmScreenSettings.dmPelsWidth = (unsigned long)screenWidth;
  37. dmScreenSettings.dmPelsHeight = (unsigned long)screenHeight;
  38. dmScreenSettings.dmBitsPerPel = 32;
  39. dmScreenSettings.dmFields = DM_BITSPERPEL | DM_PELSWIDTH | DM_PELSHEIGHT;
  40. // Change the display settings to full screen.
  41. ChangeDisplaySettings(&dmScreenSettings, CDS_FULLSCREEN);
  42. // Set the position of the window to the top left corner.
  43. posX = posY = 0;
  44. }
  45. else
  46. {
  47. // If windowed then set it to 800x600 resolution.
  48. screenWidth = 800;
  49. screenHeight = 600;
  50. // Place the window in the middle of the screen.
  51. posX = (GetSystemMetrics(SM_CXSCREEN) - screenWidth) / 2;
  52. posY = (GetSystemMetrics(SM_CYSCREEN) - screenHeight) / 2;
  53. }
  54. // Create the window with the screen settings and get the handle to it.
  55. m_hwnd = CreateWindowEx(WS_EX_APPWINDOW, m_applicationName, m_applicationName,
  56. WS_CLIPSIBLINGS | WS_CLIPCHILDREN | WS_POPUP,
  57. posX, posY, screenWidth, screenHeight, NULL, NULL, m_hinstance, NULL);
  58. // Bring the window up on the screen and set it as main focus.
  59. ShowWindow(m_hwnd, SW_SHOW);
  60. SetForegroundWindow(m_hwnd);
  61. SetFocus(m_hwnd);
  62. // Hide the mouse cursor.
  63. ShowCursor(false);
  64. return;
  65. }

ShutdownWindows 函数仅仅是将屏幕设置恢复正常,并释放窗口及其相关的句柄。

  1. void SystemClass::ShutdownWindows()
  2. {
  3. // Show the mouse cursor.
  4. ShowCursor(true);
  5. // Fix the display settings if leaving full screen mode.
  6. if(FULL_SCREEN)
  7. {
  8. ChangeDisplaySettings(NULL, 0);
  9. }
  10. // Remove the window.
  11. DestroyWindow(m_hwnd);
  12. m_hwnd = NULL;
  13. // Remove the application instance.
  14. UnregisterClass(m_applicationName, m_hinstance);
  15. m_hinstance = NULL;
  16. // Release the pointer to this class.
  17. ApplicationHandle = NULL;
  18. return;
  19. }

WndProc 函数是 Windows 系统发送消息的目的地,你可以注意到,当我们在 InitializeWindows 函数顶部使用 wc.lpfnWndProc = WndProc 初始化窗口类的时候,就已经告诉了 Windows 系统回调函数的名字。我将其包含在这个类文件中,因为我们让它将所有消息发送到 SystemClass 中定义的 MessageHandler 函数,从而将其直接绑定到 system 类中。这使我们能够将消息传递功能直接挂接到类中,并保持代码整洁。

  1. LRESULT CALLBACK WndProc(HWND hwnd, UINT umessage, WPARAM wparam, LPARAM lparam)
  2. {
  3. switch(umessage)
  4. {
  5. // Check if the window is being destroyed.
  6. case WM_DESTROY:
  7. {
  8. PostQuitMessage(0);
  9. return 0;
  10. }
  11. // Check if the window is being closed.
  12. case WM_CLOSE:
  13. {
  14. PostQuitMessage(0);
  15. return 0;
  16. }
  17. // All other messages pass to the message handler in the system class.
  18. default:
  19. {
  20. return ApplicationHandle->MessageHandler(hwnd, umessage, wparam, lparam);
  21. }
  22. }
  23. }

Inputclass.h

为了保持教程的简单,我暂时使用 Windows 系统自带的消息输入系统,直到我完成 DirectInput 的教程(DirectInput 是更好的选择)。

input 类用来处理来自键盘的用户输入,此类由 SystemClass::MessageHandler 函数提供输入。

输入对象将在键盘数组中存储每个键的状态,当被查询时,它会告诉调用者某个键是否被按下。

以下是头文件内容:

  1. ////////////////////////////////////////////////////////////////////////////////
  2. // Filename: inputclass.h
  3. ////////////////////////////////////////////////////////////////////////////////
  4. #ifndef _INPUTCLASS_H_
  5. #define _INPUTCLASS_H_
  6. ////////////////////////////////////////////////////////////////////////////////
  7. // Class name: InputClass
  8. ////////////////////////////////////////////////////////////////////////////////
  9. class InputClass
  10. {
  11. public:
  12. InputClass();
  13. InputClass(const InputClass&);
  14. ~InputClass();
  15. void Initialize();
  16. void KeyDown(unsigned int);
  17. void KeyUp(unsigned int);
  18. bool IsKeyDown(unsigned int);
  19. private:
  20. bool m_keys[256];
  21. };
  22. #endif

Inputclass.cpp

  1. ////////////////////////////////////////////////////////////////////////////////
  2. // Filename: inputclass.cpp
  3. ////////////////////////////////////////////////////////////////////////////////
  4. #include "inputclass.h"
  5. InputClass::InputClass()
  6. {
  7. }
  8. InputClass::InputClass(const InputClass& other)
  9. {
  10. }
  11. InputClass::~InputClass()
  12. {
  13. }
  14. void InputClass::Initialize()
  15. {
  16. int i;
  17. // Initialize all the keys to being released and not pressed.
  18. for(i=0; i<256; i++)
  19. {
  20. m_keys[i] = false;
  21. }
  22. return;
  23. }
  24. void InputClass::KeyDown(unsigned int input)
  25. {
  26. // If a key is pressed then save that state in the key array.
  27. m_keys[input] = true;
  28. return;
  29. }
  30. void InputClass::KeyUp(unsigned int input)
  31. {
  32. // If a key is released then clear that state in the key array.
  33. m_keys[input] = false;
  34. return;
  35. }
  36. bool InputClass::IsKeyDown(unsigned int key)
  37. {
  38. // Return what state the key is in (pressed/not pressed).
  39. return m_keys[key];
  40. }

Graphicsclass.h

graphics 类是由 system 类创建的另一个对象,应用程序中的用到的所有图形功能都将封装在这个类中。

我们还将在这个文件中使用所有与我们可能希望改变的全局图形设置(如全屏或窗口模式)相关的头文件,目前该类是空的,但在未来的教程中,它将包含所有与图形相关的对象。

  1. ////////////////////////////////////////////////////////////////////////////////
  2. // Filename: graphicsclass.h
  3. ////////////////////////////////////////////////////////////////////////////////
  4. #ifndef _GRAPHICSCLASS_H_
  5. #define _GRAPHICSCLASS_H_
  6. //////////////
  7. // INCLUDES //
  8. //////////////
  9. #include <windows.h>
  10. /////////////
  11. // GLOBALS //
  12. /////////////
  13. const bool FULL_SCREEN = false;
  14. const bool VSYNC_ENABLED = true;
  15. const float SCREEN_DEPTH = 1000.0f;
  16. const float SCREEN_NEAR = 0.1f;

我们将使用这四个全局常量来开始。

  1. ////////////////////////////////////////////////////////////////////////////////
  2. // Class name: GraphicsClass
  3. ////////////////////////////////////////////////////////////////////////////////
  4. class GraphicsClass
  5. {
  6. public:
  7. GraphicsClass();
  8. GraphicsClass(const GraphicsClass&);
  9. ~GraphicsClass();
  10. bool Initialize(int, int, HWND);
  11. void Shutdown();
  12. bool Frame();
  13. private:
  14. bool Render();
  15. private:
  16. };
  17. #endif

Graphicsclass.cpp

由于我们现在仅仅是为本教程构建框架,所以我暂时将这个类保持为空。

  1. ////////////////////////////////////////////////////////////////////////////////
  2. // Filename: graphicsclass.cpp
  3. ////////////////////////////////////////////////////////////////////////////////
  4. #include "graphicsclass.h"
  5. GraphicsClass::GraphicsClass()
  6. {
  7. }
  8. GraphicsClass::GraphicsClass(const GraphicsClass& other)
  9. {
  10. }
  11. GraphicsClass::~GraphicsClass()
  12. {
  13. }
  14. bool GraphicsClass::Initialize(int screenWidth, int screenHeight, HWND hwnd)
  15. {
  16. return true;
  17. }
  18. void GraphicsClass::Shutdown()
  19. {
  20. return;
  21. }
  22. bool GraphicsClass::Frame()
  23. {
  24. return true;
  25. }
  26. bool GraphicsClass::Render()
  27. {
  28. return true;
  29. }

总结

到现在为止,我们有了一个框架和一个可以在屏幕上显示的窗口。

这个框架现在将是所有未来教程的基础,所以理解这个框架是相当重要的。

在继续下一个教程之前,请尝试进行练习部分,以确保代码可以被正确编译。

如果你不理解这个框架,你仍然可以继续学习其他教程,随着框架的内容被逐渐填充,它们各部分的含义将更容易被理解。

练习

graphicsclass.h 中将 FULL_SCREEN 参数更改为 true,然后重新编译该头文件;窗口显示后,按 escape 键可以退出。