深入理解文档/视图框架体系_九宫格项目开发感悟

1.项目起源以及采用文档/视图框架体系缘由

我们整天都在说:文档保存项目的数据并处理项目上的数据;视图通过关联获得文档的数据并进行可视化;主窗口框架接收外部事件消息并进行消息分配。这句口头禅感觉接触过MFC的新手都能背得一字不差,然而,这到底讲了一件啥事?我想用程序的语言进行变相的概括一下:数据变量定义在文档类并进行保护公用成员函数也被定义在文档类,公用函数是文档类与视图类沟通的唯一桥梁

这个项目挺有意思,一方面关联导师的项目。另一方法姐姐家的小孩3岁了,这是一个很好的方法练习小孩的辨色力以及反应能力。
软件的视图如下所示:


具体功能是:自动变色,调节时间变色,色差分析等。其实,利用对话框也能进行设计,不过数据交流太不方便,最终还是定下来采用文档/视图结构。

2.一步一步接触项目的核心


文档文件中,做数据存储功能实现
1.给文档添加成员变量m_clrGrid和m_clrCurrentColor,并在OnNewDocument()中实现初始化。
<span style="font-size:18px;">protected:  //文档类中进行数据成员保护,视图中索引靠接口函数
	COLORREF m_clrCurrentColor;
	COLORREF m_clrGrid[4][4];</span>

<span style="font-size:18px;">BOOL CChildGridDoc::OnNewDocument()
{
	if (!CDocument::OnNewDocument())
		return FALSE;
	//初始化代码
	for (int i = 0; i < 4; i++)
		for (int j = 0; j < 4; j++)
			m_clrGrid[i][j] = RGB(255, 255, 255);//4*4格子初始化为白色

	m_clrCurrentColor = RGB(255, 0, 0);//初始化当前保持色为红色
	return TRUE;
}</span>

注意:在这里我之所以采用在OnNewDocument中而不是在构造函数中进行初始化,就是考虑到了界面可以在新文档创建时可以被重新设置。如果在文档构造函数中进行初始化,那我们的程序仅仅进行一次初始化(只有在程序启动的时候调用),再新建文档时会残余上一次结果,造成干扰。

2.给文档类添加成员函数GetCurrentColor(),GetSquare()以及SetSquare。使他们作为文档类的公用函数成员,以方便视图类通过他们访问文档数据。
<span style="font-size:18px;">//获取当前控制设备的颜色
COLORREF CChildGridDoc::GetCurrentColor()
{
	return m_clrCurrentColor;
}
//按位置进行颜色索引
COLORREF CChildGridDoc::GetSquare(int x, int y)
{
	ASSERT(x>=0&&x<=3 && y>=0&&y<=3);

	return m_clrGrid[x][y];
}
//在视图中进行设置
void CChildGridDoc::SetSquare(int x, int y, COLORREF color)
{
	ASSERT(x >= 0 && x <= 3 && y >= 0 && y <= 3);
	m_clrGrid[x][y] = color;
	SetModifiedFlag(TRUE);
	UpdateAllViews(NULL);
}</span>

这里需要注意的是,在赋给方格颜色之后,SetSquare将调用文档的SetModifiedFlag把文档标记为已修改,并调用UpdateAllView重绘视图来显示更新后的网络。
GetCurrentColor、GetSquare、SetSquare作为文档与视图之间的桥梁,由于文档的数据成员为保护类型,所以视图不能直接访问他们,只能通过这些桥梁(类间接口函数)实现对文档数据的访问。

2.文档存储和读取操作。

<span style="font-size:18px;">void CChildGridDoc::Serialize(CArchive& ar)
{
	if (ar.IsStoring())
	{//数据存储
		for (int i = 0; i < 4; i++)
			for (int j = 0; j < 4; j++)
				ar << m_clrGrid[i][j];

		ar << m_clrCurrentColor;
	}
	else
	{//数据读取
		for (int i = 0; i < 4; i++)
			for (int j = 0; j < 4; j++)
				ar >> m_clrGrid[i][j];
		ar >> m_clrCurrentColor;
	}
}</span>

当我们将ChildGrid文档保存在磁盘或从磁盘中读取时,MFC就会调用文档的Serialize函数。在文档被保存时,CChildGrid::Serialize通过将m_clrGrid和m_clrCurrentColor串行化输出给文档来做响应,再打开文档时从档案中串行化输入。例如,显示Ope n和Save As对话框,打开文件以供读写等。这也能解释,为什么在文档/视图应用程序中对保存和装载文档的处理,工作量比在传统的应用程序中少许多。

3.颜色界面添加与消息映射图构建


通过在菜单上做颜色选择,更新程序段代码使用CCmdUI::SetRadio来设置当前颜色,这可是一个消息响应的过程,我们需要将命令处理程序将该颜色值赋给m_clrCurrentColor。假如,这是假设,我能操控MFC的ON_COMMAND_RANGE和ON_UPDATE_COMMAND_UI_RANGE宏为六个下拉颜色菜单提供一个公用的命令处理程序和更新处理程序,那就太简单了;我们完全没有必要编写6个独立的命令处理程序以及6个更新程序了!!但是很不幸,类向导并没有任何提供输出RANGE宏的方法,所以也只能手动添加。

CChildGridDoc.h 声明颜色命令的消息处理函数:
<span style="font-size:18px;">protected://手动添加颜色命令的消息处理程序
	afx_msg void OnColorRed();
	afx_msg void OnColorYellow();
	afx_msg void OnColorGreen();
	afx_msg void OnColorCyan();
	afx_msg void OnColorBlue();
	afx_msg void OnColorWhite();
	afx_msg void OnUpdateColorRed(CCmdUI* pCmdUI);
	afx_msg void OnUpdateColorYellow(CCmdUI* pCmdUI);
	afx_msg void OnUpdateColorGreen(CCmdUI* pCmdUI);
	afx_msg void OnUpdateColorCyan(CCmdUI* pCmdUI);
	afx_msg void OnUpdateColorBlue(CCmdUI* pCmdUI);
	afx_msg void OnUpdateColorWhite(CCmdUI* pCmdUI);</span>
在CChildGrid.cpp中定义“命令/消息”响应关联:
<span style="font-size:18px;">BEGIN_MESSAGE_MAP(CChildGridDoc, CDocument)

	ON_COMMAND(ID_COLOR_RED, OnColorRed)
	ON_COMMAND(ID_COLOR_YELLOW, OnColorYellow)
	ON_COMMAND(ID_COLOR_GREEN, OnColorGreen)
	ON_COMMAND(ID_COLOR_CYAN, OnColorCyan)
	ON_COMMAND(ID_COLOR_BLUE, OnColorBlue)
	ON_COMMAND(ID_COLOR_WHITE, OnColorWhite)
	ON_UPDATE_COMMAND_UI(ID_COLOR_RED, OnUpdateColorRed)
	ON_UPDATE_COMMAND_UI(ID_COLOR_YELLOW, OnUpdateColorYellow)
	ON_UPDATE_COMMAND_UI(ID_COLOR_GREEN, OnUpdateColorGreen)
	ON_UPDATE_COMMAND_UI(ID_COLOR_CYAN, OnUpdateColorCyan)
	ON_UPDATE_COMMAND_UI(ID_COLOR_BLUE, OnUpdateColorBlue)
	ON_UPDATE_COMMAND_UI(ID_COLOR_WHITE, OnUpdateColorWhite)

END_MESSAGE_MAP()</span>
设置响应颜色命令的消息响应程序以及和更新程序:
<span style="font-size:18px;">//设置颜色命令的消息响应程序和更新处理程序
void CChildGridDoc::OnColorRed() {
	m_clrCurrentColor = RGB(255, 0, 0);
}
void CChildGridDoc::OnColorYellow() {
	m_clrCurrentColor = RGB(255, 255, 0);
}
void CChildGridDoc::OnColorGreen() {
	m_clrCurrentColor = RGB(0, 255, 0);
}
void CChildGridDoc::OnColorCyan() {
	m_clrCurrentColor = RGB(0, 255, 255);
}
void CChildGridDoc::OnColorBlue() {
	m_clrCurrentColor = RGB(0, 0, 255);
}
void CChildGridDoc::OnColorWhite() {
	m_clrCurrentColor = RGB(255, 255, 255);
}
void CChildGridDoc::OnUpdateColorRed(CCmdUI* pCmdUI) {
	pCmdUI->SetRadio(m_clrCurrentColor == RGB(255, 0, 0));
}
void CChildGridDoc::OnUpdateColorYellow(CCmdUI* pCmdUI) {
	pCmdUI->SetRadio(m_clrCurrentColor == RGB(255, 255, 0));
}
void CChildGridDoc::OnUpdateColorGreen(CCmdUI* pCmdUI) {
	pCmdUI->SetRadio(m_clrCurrentColor == RGB(0, 255, 0));
}
void CChildGridDoc::OnUpdateColorCyan(CCmdUI* pCmdUI) {
	pCmdUI->SetRadio(m_clrCurrentColor == RGB(0, 255, 255));
}
void CChildGridDoc::OnUpdateColorBlue(CCmdUI* pCmdUI) {
	pCmdUI->SetRadio(m_clrCurrentColor == RGB(0, 0, 255));
}
void CChildGridDoc::OnUpdateColorWhite(CCmdUI* pCmdUI) {
	pCmdUI->SetRadio(m_clrCurrentColor == RGB(255, 255, 255));
}</span>


视图文件中,进行绘画视图设计
1.实现视图的OnDraw()函数
<span style="font-size:18px;">void CChildGridView::OnDraw(CDC* pDC)
{
	CChildGridDoc* pDoc = GetDocument();
	ASSERT_VALID(pDoc);
	if (!pDoc)
		return;
//绘图代码
    //设备环境映射方式,定义将逻辑单位转换为设备单位,并定义了的X、Y轴方向。
	pDC->SetMapMode( MM_LOENGLISH );
	for (int i = 0; i < 4; i++) {
		for (int j = 0; j < 4; j++) {
			COLORREF color = pDoc->GetSquare(i, j);
			CBrush brush(color);
			int x1 = (j * 100) + 50;
			int y1 = (i * -100) - 50;
			int x2 = x1 + 100;
			int y2 = y1 - 100;
			CRect rect(x1, y1, x2, y2);
			pDC->FillRect(rect, &brush);
		}
	}
	//画网格线
	for (int x = 50; x <= 450; x += 100) {
		pDC->MoveTo(x, -50);
		pDC->LineTo(x, -450);
	}
	for (int y = -50; y >= -450; y -= 100) {
		pDC->MoveTo(50, y);
		pDC->LineTo(450, y);
	}
}</span>

如图所示:

2.添加鼠标单击左键消息响应代码
此时,我们已经具备了基本的功能,但是我们并不能进行操作;那是因为我们没有定义触发重新绘图的消息。下面以单击鼠标左键消息为例,进行设计:
<span style="font-size:18px;">void CChildGridView::OnLButtonDown(UINT nFlags, CPoint point)
{
	CView::OnLButtonDown(nFlags, point);

	CClientDC dc(this);//定义当前窗口客户区的设备描述表
	dc.SetMapMode(MM_LOENGLISH);//设定客户区的坐标系
	CPoint pos = point;//设备坐标系下,鼠标单击的位置坐标
	//鼠标单击坐标由设备坐标系变换到逻辑坐标系中,依赖于设备的图形模式
	dc.DPtoLP(&pos);

	if (pos.x >= 50 && pos.x <= 450 && pos.y <= -50 && pos.y >= -450) {
		int i = (-pos.y - 50) / 100;
		int j = (pos.x - 50) / 100;
		CChildGridDoc* pDoc = GetDocument();
		COLORREF clrCurrentColor = pDoc->GetCurrentColor();
		pDoc->SetSquare(i, j, clrCurrentColor);
	}
}</span>

这里只需要关心一件事,就是坐标系统一的问题。函数获得的坐标是在设备坐标系下,我们应该把他统一在逻辑坐标系中。所以对于鼠标响应消息以及画图部分,还是建议将所有工作都放在逻辑坐标系的模式下进行。

定时以及轮盘赌算法采用的是开源类包,这里不再赘述。

3.项目开发中的感悟与体会

1.公用接口函数是文档和视图沟通的唯一桥梁,强烈反对友元处理。
2.文档中的数据是保护类型,类间访问只能通过接口函数,此外对数据的处理也是在文档中进行。

你可能感兴趣的:(深入理解文档/视图框架体系_九宫格项目开发感悟)