在CodeBolcks+Windows API下的C++面向对象的编程教程——用面向对象的方法改写用向导创建一个Windows GUI项目(oTetris)

0.前言

我想通过编写一个完整的游戏程序方式引导读者体验程序设计的全过程。我将采用多种方式编写具有相同效果的应用程序,并通过不同方式形成的代码和实现方法的对比来理解程序开发更深层的知识。

了解我编写教程的思路,请参阅体现我最初想法的那篇文章中的“1.编程计划”和“2.已经编写完成的文章(目录)”:

学习编程从游戏开始——编程计划(目录) - lexyao - 博客园

了解不同方式实现同样效果的差异,请阅读以下文章:

在这篇文章里,我主要讲述以下几个方面的内容:

  1. 采用面向对象编程的好处
  2. 关于面向对象的编程的文章推荐
  3. 采用面向对象的方法构建一个Windows GUI项目(oTetris)
  4. 向面向对象的Windows GUI项目添加菜单(构建主窗口前)
  5. 向面向对象的Windows GUI项目添加图标(构建构建主窗口前、后)
  6. 向面向对象的Windows GUI项目添加状态栏(构建构建主窗口后)
  7. 结束语

1.采用面向对象编程的好处

记得我第一次知道面向对象的编程技术是在王府井书店里。那时在王府井的书店里每天都挤满了人,买书的很少,大部分是看书的。开放式的书店里你可以不花钱随意看书,看一天也没人过问,只要不破坏书就行。
那个时候与计算机相关的书都很贵,我每年至少要花两个月的工资来买书,有这种不花钱看书的机会当然不会放过。
书店里计算机的书很多,但是内容很多都是重复的,我在每一本书里只看发现的新内容,回到宿舍之后凭记忆把有用的知识写下来。
在那里,我唯一完全看完的一本书就叫《面向对象的程序设计》。
那个时候面向对象的编程只是一种理念,还没有真正地用于计算机软件的编写,但已经有很多人在尝试。后来有了类(class),这才是真正的把面向对象的的技术用于程序编写。
面向对象的的编程技术的出现,让代码的重用变得很容易,程序的维护也变得容易了。当然,容易了不等于简单了,这是因为类的概念出现之后,程序的接口要求保持一致,为了维护一致性,在策划阶段需要花费的时间要增加很多,但这种花费是值得的,这是因为多花费的时间会在后来的编写程序的过程中节省出来。
面向对象编程的好处很多,最主要的是便于维护、利于重用,你以前做的工作很容易在后来借用。养成面向对象的编程习惯对于一个程序员来说很重要。
在这里我不想说很多,更多的东西你可以到网上搜索一下,看一看大家的评论。 

2.关于面向对象的编程的文章推荐

在网上有大量关于面向对象编程的文章。由于面向对象是一种编程理念,任何程序语言都可以使用,所以这方面的文章我们要有选择地看。现在的程序中有了现成的例子,比我当年只是看理论要容易理解得多了。
我们在这里要说的是C++编程,那么我就推荐一个关于C++编程的资料的网址,里面讲的是C++面向对象编程的具体方法。你可以不用过分地去理解,只要记住做法就行了。

3.采用面向对象的方法构建一个Windows GUI项目(oTetris)

采用面向对象的方法构建一个Windows GUI项目的操作步骤如下:

  1. 用向导新建一个Windows GUI项目,项目名称为oTetris,项目的主代码文件main.cpp改名为oTetrisMainRaw.cpp
  2. 复制Microsoft面向对象的方法的构建Windows GUI项目主界面的示例的代码保存在oTetrisMainMic.cpp文件中
  3. 修改oTetrisMainMic.cpp中的代码生成能够在CodeBlocks的中使用的代码保存在oTetrisMainOOP.cpp文件中
  4. 优化oTetrisMainOOP.cpp文件中的代码生成最终的面向对象的Windows GUI项目主框架的代码保存在oTetrisMainOur.cpp文件中
  5. 修改oTetrisMainOur.cpp文件中的代码生成与oTetrisMainRaw.cpp有一致的视觉效果的代码保存在oTetrisMain.cpp文件中

修改到最后一步,确定用oTetrisMain.cpp文件作为oTetris项目的主文件,而其他的文件将作为过程资料保存,不在项目中使用,仅作参考。

具体的文件创建和修改过程请看下面一篇文章:

在CodeBolcks+Windows API下的C++面向对象的编程教程——采用面向对象的方法构建一个Windows GUI项目的主框架 - lexyao - 博客园

以下是oTetrisMain.cpp文件的最终代码,今后的修改和添加功能将依次为基础进行修改:

#if !defined(UNICODE)
    #define UNICODE
#endif

#if defined(UNICODE) && !defined(_UNICODE)
    #define _UNICODE
#elif defined(_UNICODE) && !defined(UNICODE)
    #define UNICODE
#endif

#include <tchar.h>
#include <windows.h>
#include "aTetris.h"


template <class DERIVED_TYPE>
class BaseWindow
{
public:
    static LRESULT CALLBACK WindowProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam);

    BaseWindow() : m_hwnd(NULL) { }

    BOOL Create(
        PCWSTR lpWindowName,
        DWORD dwStyle,
        DWORD dwExStyle = 0,
        int x = CW_USEDEFAULT,
        int y = CW_USEDEFAULT,
        int nWidth = 544,
        int nHeight = 375,
        HWND hWndParent = HWND_DESKTOP,
        HMENU hMenu = 0
    );

    HWND WindowHwnd() const
    {
        return m_hwnd;
    }

protected:

    virtual PCWSTR  ClassName() const = 0;
    virtual LRESULT HandleMessage(UINT uMsg, WPARAM wParam, LPARAM lParam) = 0;
    virtual void WindowClass(PWNDCLASS wc) = 0;
    virtual void WindowClassEx(PWNDCLASSEX wc) = 0;
private:
    HWND m_hwnd;
};

template <class DERIVED_TYPE>
LRESULT CALLBACK BaseWindow<DERIVED_TYPE>::WindowProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
{
    DERIVED_TYPE *pThis = NULL;

    if (uMsg == WM_NCCREATE)
    {
        CREATESTRUCT* pCreate = (CREATESTRUCT*)lParam;
        pThis = (DERIVED_TYPE*)pCreate->lpCreateParams;
        SetWindowLongPtr(hwnd, GWLP_USERDATA, (LONG_PTR)pThis);

        pThis->m_hwnd = hwnd;
    }
    else
    {
        pThis = (DERIVED_TYPE*)GetWindowLongPtr(hwnd, GWLP_USERDATA);
    }

    if (pThis)
    {
        return pThis->HandleMessage(uMsg, wParam, lParam);
    }
    else
    {
        return DefWindowProc(hwnd, uMsg, wParam, lParam);
    }
}

template <class DERIVED_TYPE>
BOOL BaseWindow<DERIVED_TYPE>::Create(
    PCWSTR lpWindowName,
    DWORD dwStyle,
    DWORD dwExStyle,
    int x ,
    int y,
    int nWidth ,
    int nHeight,
    HWND hWndParent,
    HMENU hMenu
)
{
#define USE_EX
#if defined(USE_EX)
    WNDCLASSEX wc = {0};

    wc.cbSize = sizeof (WNDCLASSEX);
#else
    WNDCLASS wc = {0};
#endif // defined

    wc.lpfnWndProc   = DERIVED_TYPE::WindowProc;
    wc.hInstance     = GetModuleHandle(NULL);
    wc.lpszClassName = ClassName();


#if defined(USE_EX)
    WindowClassEx(&wc);
    if (!RegisterClassEx(&wc))
        return 0;
#else
    WindowClass(&wc);
    if (!RegisterClass(&wc))
        return 0;
#endif // defined


    m_hwnd = CreateWindowEx(
                 dwExStyle, ClassName(), lpWindowName, dwStyle, x, y,
                 nWidth, nHeight, hWndParent, hMenu, GetModuleHandle(NULL), this
             );

    return (m_hwnd ? TRUE : FALSE);
}


class MainWindow : public BaseWindow<MainWindow>
{
public:
    PCWSTR  ClassName() const { return _T("oTetrisApp"); }
    LRESULT HandleMessage(UINT uMsg, WPARAM wParam, LPARAM lParam);
protected:
    void WindowClass(PWNDCLASS wc) override;
    void WindowClassEx(PWNDCLASSEX wc) override;
};


LRESULT MainWindow::HandleMessage(UINT uMsg, WPARAM wParam, LPARAM lParam)
{
    switch (uMsg)           /* 处理消息 handle the messages */
    {

    case WM_COMMAND:
        switch (LOWORD(wParam))
        {
        case IDM_EXIT:
            SendMessage(WindowHwnd(), WM_CLOSE, 0, 0);
            break;
        case IDM_ABOUT:
            MessageBox(WindowHwnd(), _T("This is my aTetris by Win32 GUI\n")
                       _T("Copyright(c) lexyao,2024"),
                       ClassName(),MB_ICONINFORMATION | MB_OK);
            break;

        }
        break;
    case WM_PAINT:
        {
            PAINTSTRUCT ps;
            HDC hdc = BeginPaint(WindowHwnd(), &ps);
            //FillRect(hdc, &ps.rcPaint, (HBRUSH) (COLOR_WINDOW+1));
            EndPaint(WindowHwnd(), &ps);
        }
        break;

    case WM_DESTROY:
        PostQuitMessage(0);  /* 将 WM_QUIT 发送到消息队列 send a WM_QUIT to the message queue */
        break;

    default:                /* 对于我们不处理的消息 for messages that we don't deal with */
        return DefWindowProc(WindowHwnd(), uMsg, wParam, lParam);
    }
    return 0;
}


void MainWindow::WindowClass(PWNDCLASS wc)
{
    wc->hbrBackground = (HBRUSH) COLOR_BACKGROUND;
}

void MainWindow::WindowClassEx(PWNDCLASSEX wc)
{
    wc->hbrBackground = (HBRUSH) COLOR_BACKGROUND;
}


//int WINAPI wWinMain(HINSTANCE hInstance, HINSTANCE, PWSTR pCmdLine, int nCmdShow)
int WINAPI WinMain (HINSTANCE hThisInstance,
                     HINSTANCE hPrevInstance,
                     LPSTR lpszArgument,
                     int nCmdShow)
{
    /* 类已注册,让我们创建程序 The class is registered, let's create the program*/
    MainWindow win;

    if (!win.Create(_T("多彩俄罗斯方块xTetris"), WS_OVERLAPPEDWINDOW))
    {
        return 0;
    }

    /* 使窗口在屏幕上可见 Make the window visible on the screen */
    ShowWindow(win.WindowHwnd(), nCmdShow);

    /* 运行消息循环。它将一直运行,直到 GetMessage()返回 0 Run the message loop. It will run until GetMessage() returns 0 */
    MSG msg = { };
    while (GetMessage(&msg, NULL, 0, 0))
    {
        TranslateMessage(&msg);
        DispatchMessage(&msg);
    }

    /* 程序返回值为 0 - PostQuitMessage()给出的值 The program return-value is 0 - The value that PostQuitMessage() gave */
    return msg.wParam;
}

  

4.向面向对象的Windows GUI项目添加菜单(构建主窗口前)

 在oTetris项目中添加菜单和响应菜单事件的方法跟在aTetris项目中是一样的,可以直接使用aTetris项目中的头文件、资源文件、代码。区别只是添加代码的方式不同罢了。

以下是介绍aTetris项目中添加菜单的文章:

在CodeBolcks+Windows API下的C++编程教程——给你的项目中添加头文件和菜单 - lexyao - 博客园

以下是介绍oTetris项目中添加菜单的文章:

在CodeBolcks+Windows API下的C++面向对象的编程教程——给你的项目中添加头文件和菜单 - lexyao - 博客园

可以通过对比两篇文章中介绍的方法和生成的代码了解两种方法的差别。在简单的事情上,面向对象的的编程没有体现出优势。可能你在以后程序变得更大的时候才会有不同的体验吧。

以下是给窗口添加菜单的代码:

void MainWindow::WindowClassEx(PWNDCLASSEX wc)
{
    wc->lpszMenuName = _T("aTetris");
}

以下是响应菜单事件的代码:

LRESULT MainWindow::HandleMessage(UINT uMsg, WPARAM wParam, LPARAM lParam)
{
    switch (uMsg)           /* 处理消息 handle the messages */
    {

    case WM_COMMAND:
        switch (LOWORD(wParam))
        {
        case IDM_EXIT:   //菜单:File->Exit
            SendMessage(WindowHwnd(), WM_CLOSE, 0, 0);
            break;
        case IDM_ABOUT:  //菜单:Help->About...
            MessageBox(WindowHwnd(), _T("This is my aTetris by Win32 GUI\n")
                       _T("Copyright(c) lexyao,2024"),
                       ClassName(),MB_ICONINFORMATION | MB_OK);
            break;

        }
        break;
    ......
    }
    return 0;
}

 

5.向面向对象的Windows GUI项目添加图标(构建构建主窗口前、后)

在oTetris项目中添加图标的方法跟在aTetris项目中是一样的,可以直接使用aTetris项目中的资源文件、代码。区别只是添加代码的方式不同罢了。

以下是介绍aTetris项目中添加图标的文章:

在CodeBolcks+Windows API下的C++编程教程——给你的项目中添加资源文件和图标 - lexyao - 博客园

以下是介绍oTetris项目中添加图标的文章:

在CodeBolcks+Windows API下的C++面向对象的编程教程——给你的项目中添加图标 - lexyao - 博客园

可以通过对比两篇文章中介绍的方法和生成的代码了解两种方法的差别。在简单的事情上,面向对象的的编程没有体现出优势。可能你在以后程序变得更大的时候才会有不同的体验吧。

以下是给窗口添加图标的代码:

void MainWindow::WindowClassEx(PWNDCLASSEX wc)
{
    wc->hIcon = LoadIcon (wc->hInstance, _T("aaaa"));
    wc->hIconSm = LoadIcon (wc->hInstance, _T("aaaa"));
}

 

6.向面向对象的Windows GUI项目添加状态栏(构建构建主窗口后)

在oTetris项目中添加状态栏的方法跟在aTetris项目中是一样的,可以直接使用aTetris项目中的代码用同样的方式添加状态栏。当然,这里我们还有另一种方法:采用面向对象的方法给oTetris添加状态栏,这个方法也可以用到aTetris项目中。

以下是介绍aTetris项目中添加状态栏的文章:

在CodeBolcks+Windows API下的C++编程教程——给你的项目中添加状态栏(通用公共控件) - lexyao - 博客园

以下是介绍oTetris项目中添加状态栏的文章:

在CodeBolcks+Windows API下的C++面向对象的编程教程——给你的项目中添加状态栏(通用公共控件) - lexyao - 博客园

可以通过对比两篇文章中介绍的方法和生成的代码了解两种方法的差别。在简单的事情上,面向对象的的编程没有体现出优势。可能你在以后程序变得更大的时候才会有不同的体验吧。

给oTetris项目添加状态栏的代码分为三部分:两个文件和一段代码。

以下是构建状态栏的类的定义文件oTetrisCommCtrls.h的内容:

#ifndef OTETRISCOMMCTRLS_H_INCLUDED
#define OTETRISCOMMCTRLS_H_INCLUDED

#if !defined(UNICODE)
    #define UNICODE
#endif

#if defined(UNICODE) && !defined(_UNICODE)
    #define _UNICODE
#elif defined(_UNICODE) && !defined(UNICODE)
    #define UNICODE
#endif

#include <tchar.h>
#include <windows.h>
#include <commctrl.h>


template <class DERIVED_TYPE>
class CommWindow
{
public:
    static LRESULT CALLBACK WindowProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam);

    CommWindow() : m_hwnd(NULL) { }

    BOOL Create(
        HWND hWndParent,
        HMENU idCtrl,
        PCWSTR lpWndText,
        DWORD dwStyle,
        DWORD dwExStyle,
        int x = CW_USEDEFAULT,
        int y = CW_USEDEFAULT,
        int nWidth = CW_USEDEFAULT,
        int nHeight = CW_USEDEFAULT
    );


    inline HWND WindowHwnd() const
    {
        return m_hwnd;
    }

protected:

    virtual PCWSTR  ClassName() const = 0;
    virtual LRESULT HandleMessage(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam){return DefWindowProc(hwnd, uMsg, wParam, lParam);};
private:
    HWND m_hwnd;
};

class cxStatusBar : public CommWindow<cxStatusBar>
{
public:
    PCWSTR  ClassName() const { return STATUSCLASSNAME; }
    BOOL Create(
        HWND hWndParent,
        HMENU idCtrl = NULL,
        DWORD dwStyle = SBARS_SIZEGRIP,
        DWORD dwExStyle = 0
    );
    HRESULT SetStatusParts(int iParts, int* aPartWidth = NULL);
    HRESULT SetStatusText(PCWSTR pszStatusText, int iParts);
};

#endif // OTETRISCOMMCTRLS_H_INCLUDED

以下是构建状态栏的类的代码文件oTetrisCommCtrls.cpp的内容:

#if !defined(UNICODE)
    #define UNICODE
#endif

#if defined(UNICODE) && !defined(_UNICODE)
    #define _UNICODE
#elif defined(_UNICODE) && !defined(UNICODE)
    #define UNICODE
#endif

#include <tchar.h>
#include <windows.h>
#include "oTetrisCommCtrls.h"

template <class DERIVED_TYPE>
BOOL CommWindow<DERIVED_TYPE>::Create(
    HWND hwndParent,
    HMENU idCtrl,
    PCWSTR lpWndText,
    DWORD dwStyle,
    DWORD dwExStyle,
    int x,
    int y,
    int nWidth,
    int nHeight
)
{
    // Ensure that the common control DLL is loaded.
    InitCommonControls();

    // Create the status bar.
    m_hwnd = CreateWindowEx(
        dwExStyle,                       // no extended styles
        ClassName(),         // name of status bar class
        (PCTSTR)lpWndText,            // no text when first created
        dwStyle |         // includes a sizing grip
        WS_CHILD | WS_VISIBLE,   // creates a visible child window
        x, y, nWidth, nHeight,              // ignores size and position
        hwndParent,              // handle to parent window
        (HMENU) idCtrl,       // child window identifier
        GetModuleHandle(NULL),                   // handle to application instance
        this);                   // no window creation data

    return (m_hwnd ? TRUE : FALSE);
}

template <class DERIVED_TYPE>
LRESULT CALLBACK CommWindow<DERIVED_TYPE>::WindowProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
{
    DERIVED_TYPE *pThis = NULL;

    if (uMsg == WM_NCCREATE)
    {
        CREATESTRUCT* pCreate = (CREATESTRUCT*)lParam;
        pThis = (DERIVED_TYPE*)pCreate->lpCreateParams;
        SetWindowLongPtr(hwnd, GWLP_USERDATA, (LONG_PTR)pThis);

        pThis->m_hwnd = hwnd;
    }
    else
    {
        pThis = (DERIVED_TYPE*)GetWindowLongPtr(hwnd, GWLP_USERDATA);
    }

    if (pThis)
    {
        return pThis->HandleMessage(hwnd, uMsg, wParam, lParam);
    }
    else
    {
        return DefWindowProc(hwnd, uMsg, wParam, lParam);
    }
}


BOOL cxStatusBar::Create(HWND hWndParent, HMENU idCtrl, DWORD dwStyle, DWORD dwExStyle)
{
    return CommWindow::Create(hWndParent, idCtrl, (PCWSTR)NULL, dwStyle, dwExStyle);
}

HRESULT cxStatusBar::SetStatusParts(int iParts, int*  aPartWidth)
{
    HWND hwndParent;
    HWND hwndStatus;
    RECT rcClient;
    HLOCAL hloc;
    int i, nWidth;
    HRESULT hResult;

    hwndStatus = this->WindowHwnd();

    if(!aPartWidth)
    {
        // 获得主窗口客户区坐标
        hwndParent = GetParent(this->WindowHwnd());
        GetClientRect(hwndParent, &rcClient);

        // 为设置状态栏分格右边界坐标分配内存
        hloc = LocalAlloc(LHND, sizeof(int) * iParts);
        aPartWidth = (PINT) LocalLock(hloc);

        // 计算状态栏分格右边界坐标
        nWidth = rcClient.right / iParts;
        int rightEdge = nWidth;
        for(i = 0; i < iParts - 1; i++)
        {
            aPartWidth[i] = rightEdge;
            rightEdge += nWidth;
        }
        //最后一格设置为-1可以自动扩展到主主窗口右边界
        aPartWidth[i] = -1;
    }

    hResult = SendMessage(this->WindowHwnd(), SB_SETPARTS, (WPARAM)iParts, (LPARAM)aPartWidth);

    // 释放内存
    LocalUnlock(hloc);
    LocalFree(hloc);
    return hResult;
}

HRESULT cxStatusBar::SetStatusText(PCWSTR pszStatusText, int iParts)
{
    return SendMessage(this->WindowHwnd(), SB_SETTEXT, iParts, (LPARAM)(pszStatusText));
}

以下是oTetrisMain.cpp文件中给窗口添加状态栏的代码:

LRESULT MainWindow::HandleMessage(UINT uMsg, WPARAM wParam, LPARAM lParam)
{
    switch (uMsg)           /* 处理消息 handle the messages */
    {
        cxStatusBar* stBar;

    case WM_CREATE:
        stBar = new cxStatusBar();
        stBar->Create(WindowHwnd());
        stBar->SetStatusParts(2);
        stBar->SetStatusText(_T("欢迎使用多彩俄罗斯方块!"),0);
        stBar->SetStatusText(_T("oTetris"),1);
        break;
    ......
    }
    return 0;
}

 

7.结束语

在这篇文章里我们通过改写想到提供的代码的方式采用面向对象的方法实现了与面向过程的方法相同的结果。虽然这篇文章中编写的代码还没有体现出面向对象的方法的优势,但我们了解了面向对象的方法。
在今后的文章中我们将通过更多的例子让大家体会到面向对象的方法的好处。
在添加新的功能的时候,只需要在原有的类的基础上通过继承编写很少的代码形成一个子类,这样就完成了要做的工作。是不是有些期待了?

posted @ 2024-12-17 10:37  lexyao  阅读(8)  评论(0编辑  收藏  举报