[MFC]程序示例:三子棋游戏

1. 游戏功能简介:

    1) 3×3棋盘,9宫格,每格可放一个棋子;

    2) 鼠标左键落X右键落O,保证X和O轮流出现防止作弊,并且设定X为先手;

    3) 棋盘是井字形的框,鼠标双击井字框将重置棋局;

    4) 水平、垂直或对角线3连线即赢得棋局;

2. TicTac.h:

// TicTac.h

class CMyApp: public CWinApp
{
public:
	virtual BOOL InitInstance();
};

#define EX		1 // 先手X
#define OH		2 // 后手O

class CMyWindow: public CWnd // 注意!从CWnd继承而不是CFrameWnd继承
{
protected:
	static const CRect m_rcSquares[9]; // 格子的坐标(只需要左上角和右下角坐标即可)
	int m_nGameGrid[9]; // 格子中的内容,是哪个玩家的棋子
	int m_nNextChar; // 下一个落子者,初始化成EX

protected:
	int GetRectID(CPoint point); // 根据点的位置返回点落在哪个方格中
	void DrawBoard(CDC* pDC); // 画整个棋局板,包括方块内的棋子
	void DrawX(CDC* pDC, int nPos); // 在nPos号方格中画X
	void DrawO(CDC* pDC, int nPos); // 在nPos号方格中画O
	void ResetGame(); // 重置棋局
	void CheckForGameOver(); // 检查棋局是否结束(哪方赢或者平局)
	int IsWinner(); // 返回谁是胜者,还没决出就返回0,否则返回EX或者OH
	BOOL IsDraw(); // 检查是否平局

public:
	CMyWindow();

protected:
	virtual void PostNcDestroy(); // 从CWnd继承的窗口必须覆盖该函数,用以销毁窗口非客户区

	afx_msg void OnPaint();
	afx_msg void OnLButtonDown(UINT nFlags, CPoint point); // EX落子
	afx_msg void OnRButtonDown(UINT nFlags, CPoint point); // OH落子
	afx_msg void OnLButtonDblClk(UINT nFlags, CPoint point); // 双击井字线条重新开一局

	DECLARE_MESSAGE_MAP()
};

3. TicTac.cpp的开头部分(包括静态成员变量的定义):

// TicTac.cpp

#include <afxwin.h>

#include "TicTac.h"

CMyApp myApp;

BOOL CMyApp::InitInstance()
{
	m_pMainWnd = new CMyWindow;
	m_pMainWnd->ShowWindow(m_nCmdShow);
	m_pMainWnd->UpdateWindow();

	return TRUE;
}

BEGIN_MESSAGE_MAP(CMyWindow, CWnd)
	ON_WM_PAINT()
	ON_WM_LBUTTONDOWN()
	ON_WM_RBUTTONDOWN()
	ON_WM_LBUTTONDBLCLK()
END_MESSAGE_MAP()

const CRect CMyWindow::m_rcSquares[9] = { // 9个格子在客户区中的坐标
	CRect (	16,  16, 112, 112), // 格子按照从上到下从左到右编号0-8
	CRect (128,  16, 224, 112),
	CRect (240,  16, 336, 112),
	CRect ( 16, 128, 112, 224),
	CRect (128, 128, 224, 224),
	CRect (240, 128, 336, 224),
	CRect ( 16, 240, 112, 336),
	CRect (128, 240, 224, 336),
    CRect (240, 240, 336, 336)
};

4. GetRectID:

     1) 用以判断鼠标击键是否位于9个方格内;

     2) 使用到CRect的成员函数来判断一个点是否位于矩形区域内:BOOL CRect::PtInRect(POINT point) const;

int CMyWindow::GetRectID(CPoint point)
{// 判断点point落在几号格子中
	for (int i = 0; i < 9; i++) {
		if (m_rcSquares[i].PtInRect(point))
			return i;
	}
	return -1; // 落在所有格子的外面
}

5. DrawX:

     1) 使用到了CRect的成员函数用来将缩小矩形:void CRect::DeflateRect(int x, int y);,意义是使矩形左右两边分别向中心靠拢x个单位,上下两边分别向中心靠拢y个单位;

     2) 函数将在一个缩小过的方格内画叉,这样不会使叉和井字棋盘边框重叠,更加美观;

void CMyWindow::DrawX(CDC* pDC, int nPos)
{// 在nPos号格子中画X
	CPen pen(PS_SOLID, 16, RGB(255, 0, 0)); // X是红色16宽的实线
	CPen* pOldPen = pDC->SelectObject(&pen);

	CRect rect = m_rcSquares[nPos]; // 将nPos号格子的坐标下载到本地处理
	rect.DeflateRect(16, 16); // 四周向中心收缩16像素以避免X和格角重合,更加美观

	// 画X
	pDC->MoveTo(rect.left, rect.top);
	pDC->LineTo(rect.right, rect.bottom);
	pDC->MoveTo(rect.left, rect.bottom);
	pDC->LineTo(rect.right, rect.top);

	pDC->SelectObject(pOldPen); // !记得还原默认的画笔
}

6. DrawY:

    1) 在方格中画圆,要求使圆中的填充色为透明,所以需要使用透明画刷;

    2) 之前讲过通过CBrush的构造函数、CreateSolidBrush、CreateHatchBrush只能创建单色或者阴影线型的画刷,但是要指定透明画刷只能通过创建逻辑画刷将参数lbStyle设为BS_NULL或BS_HOLLOW,但是这样做实在非常繁琐,但是还有更加简便指定画刷样式的方式,就是使用SelectStockObject将一个库存中定义过的指定样式的画刷直接选入设备环境,连创建画刷的步骤都不需要:

virtual CGdiObject* CDC::SelectStockObject(int nIndex);

!其中nIndex是系统预定好的GDI对象的ID号,比如NULL_BRUSH,就是我们需要用的透明画刷,BLACK_BRUSH即黑色画刷,BLACK_PEN即黑色画笔,如果需要用到这些简单的预订好的GDI对象则可以直接使用该函数将相关对象选入设备环境;

!该函数可选中的GDI对象有画笔、画刷、字体这三样;

!返回的是被替换掉的相应的GDI对象的指针;

    3) 画圆的矩形框同样需要使用DeflateRect缩小,这样圆不会不和井字框重叠;

void CMyWindow::DrawO(CDC* pDC, int nPos)
{// 在nPos号格子中画O
	CPen pen(PS_SOLID, 16, RGB(0, 0, 255)); // O是蓝色16宽的实现
	CPen* pOldPen = pDC->SelectObject(&pen);

	// 指定透明画刷只能使用逻辑画刷指定
	// 避免麻烦可以直接使用该函数指定透明化刷
	pDC->SelectStockObject(NULL_BRUSH); // 透明画刷,O的填充色伴随客户区底色

	CRect rect = m_rcSquares[nPos];
	rect.DeflateRect(16, 16);
	pDC->Ellipse(&rect);

	pDC->SelectObject(pOldPen);
}

7. DrawBoard:

void CMyWindow::DrawBoard(CDC* pDC)
{// 重画整个棋盘,包括已经下的棋子
	CPen pen(PS_SOLID, 16, RGB(0, 0, 0)); // 井字框是16宽的黑色实线
	CPen* pOldPen = pDC->SelectObject(&pen);

	// 画井字棋盘边框
	pDC->MoveTo(120, 16);
	pDC->LineTo(120, 336);

	pDC->MoveTo(232, 16);
	pDC->LineTo(232, 336);

	pDC->MoveTo(16, 120);
	pDC->LineTo(336, 120);

	pDC->MoveTo(16, 232);
	pDC->LineTo(336, 232);

	for (int i = 0; i < 9; i++) { // 画已经下过的棋子
		if (EX == m_nGameGrid[i]) DrawX(pDC, i);
		else if (OH == m_nGameGrid[i]) DrawO(pDC, i);
	}
	pDC->SelectObject(pOldPen);
}

8. IsWinner:

int CMyWindow::IsWinner()
{// 返回谁是胜者
	static int nPattern[8][3] = { // 总共有8种赢的方式
		0, 1, 2, // 只要特定的3个格子中花色一样就代表一方胜
		3, 4, 5,
		6, 7, 8,
		0, 3, 6,
		1, 4, 7,
		2, 5, 8,
		0, 4, 8,
		2, 4, 6
	};

	for (int i = 0; i < 8; i++) { // 检查是否存在上述8种胜利的模式
		if (EX == m_nGameGrid[nPattern[i][0]] &&
			EX == m_nGameGrid[nPattern[i][1]] &&
			EX == m_nGameGrid[nPattern[i][2]])
			return EX;

		if (OH == m_nGameGrid[nPattern[i][0]] &&
			OH == m_nGameGrid[nPattern[i][1]] &&
			OH == m_nGameGrid[nPattern[i][2]])
			return OH;
	}
	return 0; // 没有任何一方胜就返回0,即NOBODY
}

9. IsDraw:

BOOL CMyWindow::IsDraw()
{// 检查是否和棋,但是此函数只检查棋盘是否满
 // 因此要真正检查和棋必须先判断IsWinner是否为0,如果为0再使用IsDraw检查!
	for (int i = 0; i < 9; i++) {
		if (!m_nGameGrid[i])
			return FALSE;
	}
	return TRUE;
}

10. ResetGame:

      1) 重置棋局最重要的一步就是将棋子情况,因此需要将m_nGameGrid清零;

      2) 清零使用到了全局的Win32 API函数::ZeroMemory:VOID ::ZeroMemory(PVOID Destination, SIZE_T Length);,Destination是起始内存块的地址,Length表示清零多少个字节;

      4) 棋盘清空后必须要重画整个棋盘,此时可以使用CWnd的Invalidate函数使窗口客户区无效,该函数会触发WM_PAINT消息让OnPaint函数进行客户区的重绘:

void CWnd::Invalidate(BOOL bErase = TRUE);,其中bErase指示客户区是否应该被重画,TRUE表示该重画,此时会触发WM_PAINT消息,FALSE表示不该重画,不会触发WM_PAINT消息,从而使客户区保持原样,这也是显示手工产生WM_PAINT消息的方法;

void CMyWindow::ResetGame()
{// 重开一局
	m_nNextChar = EX; // 先手
	::ZeroMemory(m_nGameGrid, sizeof(m_nGameGrid)); // 将棋盘内容清零
	Invalidate(); // 棋盘重画,Invalidate手工产生WM_PAINT消息
}

11. CheckForGameOver:

    1) 当有人赢或者是平局的时候需要弹出一个消息框报告;

    2) 使用CWnd的成员函数MessageBox来弹出消息框: int CWnd::MessageBox(LPCTSTR lpszText, LPCTSTR lpszCaption = NULL, UINT nType = MB_OK);

!注意:CString对常规文本字符串指针进行了重载,因此CString和常规文本字符串两者可以混用;

         i. lpszText是消息框中报告的内容;

         ii. lpszCaption是消息框的标题,如果为NULL则标题栏将显示Error字样;

         iii. nType是消息框的风格,以MB_为前缀,即Message Box的缩写:

首先是图标:

MB_ICONHAND/MB_ICONSTOP/MB_ICONERROR:等价,都显示×型图标

MB_ICONQUESTION:显示问号图标

MB_ICONEXCLAMATION/MB_ICONWARNING:显示感叹号涂炳

MB_ICONASTERISK/MB_ICONINFORMATION:显示一个”i”型图标

其次是按钮:

MB_ABORTRETRYINGNORE:非常著名的”终止,重试,忽略“三按钮

MB_OK:只有一个”是”按钮

MB_OKCANCEL:“是,取消”按钮

MB_RETRYCANCEL:重试和取消双按钮

MB_YESNO:是和否按钮

MB_YESNOCANCEL:是、否、取消三按钮

!以上所有的MB_消息框属性ID都可以使用位操作符|进行组合连接;

         iv. 返回值:返回的是用户按下了哪个按钮,即相应按钮的消息框属性ID,但是前缀编程了ID,比如IDOK表示用户按了OK按钮后退出,当系统内存严重不足无法创建消息框的时候会返回0;

void CMyWindow::CheckForGameOver()
{// 检查是否一局结束,即赢或者平局
	int nWinner;

	if (nWinner = IsWinner()) { // 先看是不是有人赢
		CString strWinner = (EX == nWinner ? _T("X Wins!"): _T("O Wins!"));
		MessageBox(strWinner, _T("Game Over!"), MB_ICONEXCLAMATION | MB_OK);
	}
	else if (IsDraw()) { // 没人赢再看是否和棋
		MessageBox(_T("It's a draw!"), _T("Game Over!"), MB_ICONEXCLAMATION | MB_OK);
	}
	ResetGame();
}

!关于模态框和系统框:

    1) 使用MessageBox弹出的消息框是一种模态框,即用户必须先应答完消息框后才能让应用程序干其它的事情,具体表现在消息框弹出后如果你不处理就想点击应用程序的其它地方都会被阻止,也就是强制要求你先处理消息框;

    2) 应用程序模式和系统模式:模态框还有两种模式,一种是应用程序模式,这也是默认的模式,不需显示指定,在此模式下如果不处理模态框虽然不可以使用应用程序其它功能,但是还是能切换到其它应用程序的窗口,并且切换后其它应用程序的框将覆盖该模态框,而系统模式则相反,虽然也不能使用应用程序其它功能,也能切换到其它应用程序的窗口,但是切换后该模态框还是显示在最上层,也就是说在系统模式下模态框永远都显示在最上层,但虽然显示在最上层,还是可以操作另一个应用程序的窗口(即使用另一个应用程序的功能),而模态框所属的那个应用程序的其它功能永远在模态框被处理之前处于挂起状态;

    3) 指定系统模式直接在MessageBox的nType中添加MB_SYSTEMMODAL即可;

!关于消息框的默认按钮问题:

    1) 经常在一些应用中看到,当一个消息框弹出后,某个按钮就已经被“选中”了,即该按钮的颜色反白或者按钮周围有一圈虚线,可以通过左右方向键改变被选中的按钮;

    2) 被“选中”的按钮就是消息框的默认按钮,可以通过在MessageBox中nType中添加MB_DEFBUTTONx来指定默认的按钮,其中x为数字1-4,分别表示从左到右的4个按钮,比如MB_YESNOCANCEL | MB_DEFBUTTON2,就表示第二个按钮“No”将被设定为默认按钮,其按钮会被一个虚线框选中;

!全局AfxMessageBox函数:

    1) CWnd::MessageBox函数只能在窗口被建立后才能通过窗口对象调用,但是在某些情况下进行全局调用,比如在窗口建立之前的InitInstance中使用MessageBox提示一些错误消息;

    2) 可使用全局的AfxMessageBox函数弹出消息框,该消息框功能和CWnd::MesageBox完全等价,该函数其实就是Win32 API的MessageBox;

12. 左击、右击和双击的消息响应:

void CMyWindow::OnLButtonDown(UINT nFlags, CPoint point)
{// EX落子
	if (m_nNextChar != EX) return ; // EX刚下过,不能连续落子

	int nPos = GetRectID(point);
	if (nPos == -1 || m_nGameGrid[nPos] ) return ; // 落在棋盘外或者落在已经放过棋子的区域

	m_nGameGrid[nPos] = EX; // 将X棋子落下
	m_nNextChar = OH; // 换棋手

	CClientDC dc(this);
	DrawX(&dc, nPos); // 在nPos号格子中画X
	CheckForGameOver(); // 一定要先画上棋子再判断是否结束,这样画面更好
}

void CMyWindow::OnRButtonDown(UINT nFlags, CPoint point)
{// OH落子,和EX落子类同
	if (m_nNextChar != OH) return ;

	int nPos = GetRectID(point);
	if (nPos == -1 || m_nGameGrid[nPos]) return ;

	m_nGameGrid[nPos] = OH;
	m_nNextChar = EX;

	CClientDC dc(this);
	DrawO(&dc, nPos);
	CheckForGameOver();
}

双击井字框将重置棋局,而要检验是否击中了井字框很简单,因为井字框的颜色是黑色的,因此只要检测击中点的像素颜色即可实现判断,用到了CDC的GetPixel函数:

COLORREF CDC::GetPixel(int x, int y) const;

COLORREF CDC::GetPixel(POINT point) const;

!参数都是点的坐标

void CMyWindow::OnLButtonDblClk(UINT nFlags, CPoint point)
{// 双击井字棋盘框重设一局
	CClientDC dc(this);

	if (dc.GetPixel(point) == RGB(0, 0, 0)) // 井字框是黑色的,只需检查击中点的像素是否是黑色即可
		ResetGame();
}

OnPaint:不仅要重画棋盘也要重画棋盘中的棋子

void CMyWindow::OnPaint()
{
	CPaintDC dc(this);
	DrawBoard(&dc);
}

13. 从CWnd派生的窗口的创建——注册窗口类:

    1) 本程序的主窗口是从CWnd派生而不是从CFrameWnd派生,因为该简单的程序不需要用到CFrameWnd提供的更多的功能;

    2) 首先在消息映射条目的BEGIN_MESSAGE_MAP宏的基类必须写CWnd而不是CFrameWnd;

    3) 由于当CFrameWnd的Create函数的第一个参数指定为NULL时就表示创建的窗口资源采用MFC实现预定义好且预注册好的默认的WNDCLASS风格,但是CWnd类派生的窗口想要被创建的话就没那么走运了,需要程序员自己指定WNDCLASS并用AfxRegisterWndClass注册窗口类,并使用更底层的CWnd::CreateEx来创建窗口资源;

    4) 其实CFrameWnd::Create的第一个参数和CWnd::CreateEX的第二个参数都是WNDCLASS中设定的窗口名称,只不过Create更高级,如果该参数为NULL表示使用默认的WNDCLASS,而CreateEx的第二个参数不能接受NULL的窗口名称,因此只能用户自定义WNDCLASS并注册了,用注册后的窗口名称作为CreateEx的第二个参数;

    5) AfxRegisterWndClass:

         i. LPCTSTR AFXAPI AfxRegisterWndClass(UINT nClassStyle, HCURSOR hCursor = 0, HBRUSH hbrBackground = 0, HICON hIcon = 0);

         ii. 与Win32 API不同的是,Win32 API需要现设计WNDCLASS再RegisterClass(&wndclass),而MFC中可以直接将对WNDCLASS的设计放在RegisterWndClass中指定,不同的地方在于无需指定回调函数,因为MFC采用的是消息映射基址,还有一些其它参数也无需指定了,在MFC中需要指定以上4个最重要的参数即可;

!其实并不是其它参数无需指定,而是这些参数都已经被AfxRegisterWndClass隐藏起来,这些参数都被指定成了默认值;

         iii. nClassStyle:指定窗口类的风格,以CS_为前缀,即Class Style的缩写,下面列出几种最常用的窗口类风格

CS_DBLCLKS:窗口可接受鼠标双击消息

CS_NOCLOSE:使菜单栏中关闭命令以及标题栏上的关闭按钮失效

CS_HREDRAW/CS_VREDRAW:指定窗口在水平和垂直方向上缩放时使整个客户区无效(即产生WM_PAINT消息)以使OnPaint重画;

!以上这些风格都可以用|进行组合;

         iv. hCursor:指定客户区内光标的样式(比如箭头、沙漏、十字或是自定义的图形等),要获取光标资源的句柄必须通过CWinApp的LoadCursor或者LoadStandardCursor这两个函数,前者可以获取任何类型光标的句柄(系统预设的以及用户自定义的),后者只能获取系统预设的光标的句柄,这里以LoadStandardCursor为例,LoadCursor的参数和返回值均与其相同,

HCURSOR CWinApp::LoadStandardCursor(LPCTSTR lpszCursorName) const;

lpszCursorName:系统为各个预设的光标定义了字符串名,可以直接用字符串指定,不过就连字符串也用宏代替了,以IDC_作为前缀,即ID Cursor的缩写,有以下几种常用的,

IDC_ARROW:箭头

IDC_CROSS:十字

IDC_WAIT:沙漏

IDC_IBEAM:文本插入光标
!!该参数如果为0将默认使用IDC_ARROW;

         v. hCursor以及后面要讲到的hIcon都需要使用CWinApp的Load系列函数来获取句柄,但是最好不要直接使用在全局范围内创建的唯一的那个myApp对象来调用这些函数(myApp.LoadCursor),因为这样有失通用性,如果哪天改了该变量名,则所有使用这些函数的地方都需要跟着改名,这里可以使用全局函数AfxGetApp来获取当前应用的对象的指针:CWinApp* AfxGetApp();,因此这里可以使用AfxGetApp()->LoadStandardCursor(IDC_ARROW)来指定hCursor参数;

         vi. hIcon:指定应用程序在桌面、任务栏、标题栏及其它地方代表应用程序的图标的地方,后去图标句柄的函数为:HICON AfxGetApp()->LoadStandardIcon(LPCTSTR lpszIcon) const;,该函数用以获取系统预定义的图标的句柄,当然也可以使用LoadIcon来加载自定义的图标,其中lpszIcon有以下几种常用的宏,以IDI_作为前缀,即ID Icon的缩写:

IDI_WINLOGO:Windows标志

IDI_APPLICATION:应用程序标志,就是一个控制台的样子

IDI_HAND/IDI_ERROR:一个手掌

IDI_ASTERISK/IDI_INFORMATION:”i”符号以提示

IDI_WARNING/IDI_EXCLAMATION:感叹号

IDI_QEUSTION:问号

!如果该参数为0将默认使用IDI_WINLOGO;

         v. hbrBackGround:用以定义客户区重绘时的背景画刷,OnPaint处理时调用BeginPaint时将产生一个WM_ERASEBKGND消息,如果用户自己不处理该消息则DefWindowProc江永hbrBackGround覆盖整个客户区,然后再执行BeginPaint之后的客户区绘图工作;

!一般情况下用户无需相应WM_ERASEBKGND消息,但是在一些特殊情况下,如游戏界面,就可以响应该消息并用一些自定义的位图来填充整个窗口的背景;

!这里使用Windows预定义的系统颜色+1的方式得到画刷句柄,比如(HBRUSH)(COLOR_3DFACE + 1),这里的COLOR_3DFACE可以产生3D按钮的效果,有哪些系统颜色可以通过Win32 API的::GetSysColor函数的帮助文档查阅;

!如果该参数为0,则默认使用NULL_BRUSH及透明画刷来填充背景色;

    6) AfxRegisterWndClass的返回值:返回值即注册的窗口类的类名,在MFC中窗口类类名将自动生成,而不是Win32那样可以由用户自由指定,MFC保证自动生成的类名不会彼此重复,该名在Create创建窗口资源时需要用到;

CString strWndClass = AfxRegisterWndClass(
	CS_DBLCLKS,
	AfxGetApp()->LoadStandardCursor(IDC_ARROW),
	(HBRUSH)(COLOR_3DFACE + 1),
	AfxGetApp()->LoadStandardIcon(IDI_WINLOGO)
);

14. 创建CWnd派生的窗口资源:

    1) 使用CWnd的CreateEx创建扩展样式的窗口:

BOOL CWnd::CreateEx(
	DWORD dwExStyle,
	LPCTSTR lpszClassName,
	LPCTSTR lpszWindowName,
	DWORD dwStyle,
	int x, int y,
	int nWidth, int nHeight,
	HWND hwndParent,
	HMENU hIDorHMenu,
	LPVOID lpParam = NULL
);

         i. dwExStyle:指定了扩展窗口的样式,由0个或多个WS_EX_前缀的宏组合而成,即Windows Style Extended的缩写,由于目前用不到,因此直接传0

         ii. lpszClassName:即使用AfxRegisterWndClass注册后的窗口类的类名,直接将该函数返回值作为此参数即可;

         iii. lpszWindowName:窗口标题;

         iv. dwStyle:窗口样式,由WS_为前缀,有以下几种常用的

WS_OVERLAPPED:窗口可重叠

WS_CAPTION:窗口有标题栏

WS_SYSMENU:具有系统菜单,即左击窗口左上角的程序图标可弹出的菜单

WS_THICKFRAME:窗口可活动,即四条边还有角可以用鼠标抓取并放缩调整边框

WS_MINIMIZEBOX:标题栏右上角有最小化按钮

WS_MAXIMIZEBOX:标题栏右上角有最大化按钮

WS_OVERLAPPEDWINDOW:以上6个的组合

WS_MAXIMIZE:一创建就是最大化的

WS_MINIMIZE:一创建就是最小化的

         v. x, y, nWidth, nHeight:分别为窗口初始时的位置和宽高,这里统一使用宏CW_USEDEFAULT设置默认的位置和大小,CW_是Create Window的缩写;

         vi. hwndParent和hIDorHMenu:分别为父窗口句柄和菜单句柄,如果是最底下的主窗口没有任何父窗口则直接填NULL,由于未使用菜单所以菜单句柄也为NULL;

         vii. lpParam:窗口资源的额外数据区,自己传数据区的首地址;

    2) 接着上面的代码创建窗口资源:

CreateEx(0, strWndClass, _T("Tic-Tac-Toe"),
	WS_OVERLAPPED | WS_SYSMENU | WS_CAPTION | WS_MINIMIZEBOX,
	CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT,
	NULL, NULL);

    3) 也可以为框架窗口注册自定义的WNDCLASS,下列WNDCLASS的样式就和默认的框架样式等价:

CString strWndClass = AfxRegisterWndClass(
	CS_DBLCLKS | CS_HREDRAW | CS_VREDRAW,
	0,
	(HBRUSH)(COLOR_WINDOW + 1),
	AfxGetApp()->LoadStandardIcon(IDI_APPLICATION)
);

Create(strWndClass, _T("Default Frame Window"));

!这和只有一句

Create(NULL, _T("Default Frame Window"));

是完全等价的,因此可以看出Create第一个参数的NULL,就隐含着上述的注册语句;

!所以也是可以为框架窗口指定WNDCLASS样式的,步骤就是上述那样;

    4) 还有一个问题是窗口客户区的大小最好刚好跟棋盘的大小差不多,当然在Create的时候可以直接指定nWidth和nHeight,但不过这是整个窗口外边框的大小,由于标题栏、边框的宽度是不知道的,因此很难直接根据此大小间接调整客户区大小使之与棋盘大小匹配,因此还不如先在Create中定义默认的外框大小,然后自己再根据棋盘大小想出一个合理大客户区大小,然后通过某个函数根据你理想的客户区大小自动调整整个窗口的大小,答案是肯定的,就是使用CWnd的CalcWindowRect:

virtual void CWnd::CalcWindowRect(LPRECT lpClientRect, UINT nAdjustType = adjustBorder);

!其中lpClientRect就是你理想中的客户区的大小,nAdjustType有CWnd预定义的两个值,一个是CWnd::adjustBorder = 0,该参数表示调整边框不考虑滚动条,还有一个就是CWnd::adjustOutside = 1,表示调整边框的同时也调整滑块;

!该函数会将调整好的整个外框的大小更新至lpClientRect中;

    5) 调整好外框后可以直接使用CWnd的SetWindowPos使调整好的大小生效:

BOOL CWnd::SetWindowPos(
	const CWnd* pWndInsertAfter,
	int x, int y,
	int cx, int cy,
	UINT nFlags
);

         i. 第一个参数目前不用管,填NULL即可;

         ii. x和y是窗口的新位置;

         iii. cx和cy窗口新的宽和高;

         iv. nFlags是一些关于窗口设置时大小和位置的选项,这里用到的就这几个(前缀为SWP_,即Set Window Position的缩写):

SWP_NOZORDER:该选项表示忽略第一个参数

SWP_NOMOVE:该选项将忽略x和y这两个参数,表示保持Set之前的位置,所以这里x和y填任意值,干脆就写0

SWP_NOREDRAW:该选项表示Set之后客户区不需要重绘,因为窗口才刚创建,位置没变仅仅是客户区缩小了,所以无需重绘客户区了;

CMyWindow::CMyWindow()
{
	m_nNextChar = EX; // EX初始化为先手
	::ZeroMemory(m_nGameGrid, sizeof(m_nGameGrid));

	CString strWndClass = AfxRegisterWndClass(
		CS_DBLCLKS, // 由于设计时不允许窗口大小改变,所以不要H/V的Redraw了
		AfxGetApp()->LoadStandardCursor(IDC_ARROW),
		(HBRUSH)(COLOR_3DFACE + 1),
		AfxGetApp()->LoadStandardIcon(IDI_WINLOGO)
	);

	CreateEx(0, strWndClass, _T("Tic-Tac-Toe"),
		WS_OVERLAPPED | WS_SYSMENU | WS_CAPTION | WS_MINIMIZEBOX, // 没有thickframe,禁止改变窗口大小
		CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT,
		NULL, NULL);

	CRect rect(0, 0, 352, 352);
	CalcWindowRect(&rect);
	SetWindowPos(NULL, 0, 0, rect.Width(), rect.Height(),
		SWP_NOZORDER | SWP_NOMOVE | SWP_NOREDRAW);
}

    原文作者:Lirx_Tech
    原文地址: https://blog.csdn.net/Lirx_Tech/article/details/46350795
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞