Skip to content

Latest commit

 

History

History
461 lines (324 loc) · 20.9 KB

5-程序的执行流程.md

File metadata and controls

461 lines (324 loc) · 20.9 KB

第五讲(程序的执行流程)

1. 显示对话框

在第四讲中,老师讲解了对话框的创建和使用,本讲将带着大家理解程序的整个执行过程。

首先同学们要理解 消息 这一概念,在Windows中,鼠标的移动,键盘的输入,都是一种消息,应用程序根据这些消息来执行相应的处理程序。

比如说你打开 QQ 的聊天页面,在消息栏中输入一些你想发送的消息,然后点击发送按钮,聊天消息就被发送给了对方。

这里你用键盘输入的文字,和点击发送按钮其实对于 QQ 这个应用程序来说都是消息。

QQ 应用程序根据这些消息来调用相关的函数为你处理事情,或者称之为 事件,比如说你的消息被发送了,消息记录中出现了你发的消息,这都是事情处理的结果。

整个 Windows 系统都是消息驱动,事件处理的,MFC 是基于 Windows 系统的,当然也是有消息产生,有事件处理的。

我们来想一想绘图程序的整个输入输出逻辑,(边讲解边运行程序)当鼠标左键单击程序的,并按下键盘上的 Ctrl 键时,弹出我们的图元属性对话框。

程序对应的区域

用户在往对话框输入值后,程序根据这些值来绘制具体的图形。

(边讲解边在运行的程序上比划)我们画图的区域是由 DrawingView 文件控制的,而整个程序框架上的控件是由 MainFrm 文件控制的。

我们要在画图区域操作,就要在 DrawingView 文件中添加代码,和我们在对话框上的事件处理一样,打开类向导。

类向导处理消息

在消息一项中找到鼠标左键单击的消息名,添加处理程序,这样在消息产生后,程序就可以对这个消息做出相应的操作,比如弹出我们的对话框。

消息都是以 WM 作为前缀开头,因为是 Window Message 窗口消息嘛,鼠标左键按下的消息是 WM_LBUTTONDOWN,添加处理程序。

通过上面的操作,我们来看看类向导为我们做了什么,打开 DrawingView.h 文件,可以看到新增的消息映射函数,afx_msg void OnLButtonDown(UINT nFlags,CPoint point),这个函数来处理我们鼠标左键按下的消息。

所谓映射就是一一对应的关系,一个消息只能对应一个处理函数,而一个处理函数也只能处理一个消息。

自定义的消息映射函数

我们在函数实现中完成弹出对话框的操作。

在使用框架时,很重要的能力是模仿,看看框架中的示例程序,能够更好的帮助我们理解和使用框架。

如果我们把关于对话框的显示弄明白了,显示我们自己创建的对话框也自然没有问题。

打开 Drawing.cpp 文件,找到 OnAppAbout() 函数,这里面的代码就是让关于对话框显示出来的。

关于对话框的显示

仿着这个来写我们对话框的显示,找到刚才的消息处理函数,使用我们的对话框类 AttributeDialog,创建对话框对象,调用 DoModal() 函数就可以让对话框显示出来了。(边讲解边写代码)

// 按下鼠标左键的消息响应函数
void CDrawingView::OnLButtonDown(UINT nFlags, CPoint point)
{
	AttributeDialog dialog;
	dialog.DoModal();
}

我们再来运行程序,在程序中单击鼠标左键,对话框就可以显示出来了。

不过课程设计任务书上还要求按下 Ctrl 键,这个也好办,我们只需要在按下鼠标左键时,判断 Ctrl 键是否也被按下。

消息映射函数中的参数 nFlags 是无符号长整型,无法直接和键盘按键消息 16 进制数对比,需要先与 MK_CONTROL 做相与逻辑运算后再和 MK_CONTROL 比较判断真假。

// 按下鼠标左键的消息响应函数
void CDrawingView::OnLButtonDown(UINT nFlags, CPoint point)
{
	if ((nFlags&MK_CONTROL) == MK_CONTROL)
	{
		AttributeDialog dialog;
		dialog.DoModal();
	}
}

我们的对话框可以显示了,我们下一步是要得到在对话框输入的值。

打开我们创建的对话框类 AttributeDialog 的源文件,(边说边打开 AttributeDialog.cpp 文件),在 DoDataExchange() 函数中,实现对话框界面和变量的绑定。

也就是说,通过界面输入的值会赋值给对应的变量。

我们在前面根据用户的选项来改变对话框界面的显示时,已经添加了一个下拉框对应的 Control 类别的变量 shapeType。(找到 OnCbnSelchangeComboShapeType() 函数)

我们根据这个变量使用 GetCurSel() 函数来获取用户选择的选项序号,而不是直接获取用户选择的值。

对于文本编辑框,我们可以直接使用 Value 类别的变量获取输入的值,还是在类向导中给控件添加变量,比如我们给原点X 后面的文本编辑框添加一个 Value 类别的变量。(演示一下)

给控件添加变量示例

这样用户输入的原点X 的值就会赋给文本编辑框控件对应的 Value 类别的变量,实现了界面值到变量值的传递。

MFC 中 Value 类别的变量类型有 int 类型和 CString 类型,分别和 C++ 中的 int 类型和 String 类型对应。

控件变量类型

根据文本编辑框输入值的类型来决定对应 Value 类别控件变量的类型。

对于文本编辑框来说,我们只需要绑定 Value 类别的控件变量即可,而对于下拉框,列表框,颜色选择框,我们要用 Control 类别的控件变量获取选择的值。

根据我们所需的值,同学们自己添加相应的控件变量。

可以在 AttributeDialog.h 文件中看到我们添加的控件变量。

自定义的控件变量

同学们再想一想,我们什么时候获取控件变量的值呢?

用户输入完毕后,选择图元的类型,在按下图元属性对话框上的确定按钮后,对话框消失,我们就无法知道对话框界面上输入的值了。

所以我们要在对话框消失之前,用户按下确认按钮之后获取对话框界面上的值。

我们就需要编写处理按下确认按钮消息的处理函数,可以在对话框编辑界面双击确定按钮进入按钮点击函数的编写。

处理确定按钮消息

CDialogEx::OnOK();这一句是调用对话框父类 CDialogEx 的 OnOK() 函数,更新对话框界面上的值到对应的控件变量中。

所以我们添加的代码要写到这一句的后面,而不能写到这一句代码之前,否则就无法获得最新的对话框界面的值。

下面我们来编写我们的 OnBnClickedOk() 函数。(边说边写)

我们要获取图形类型的选项,因为我们要靠这个来决定绘制正方形还是矩形还是其他的图形。

我们还要获取变量类型的选项值,填充类型的选项值,边框颜色的值,填充颜色的值。

为了方便记录图形类型的选项,边框类型的选项,填充类型的选项,我们可以在头文件中自定义三个变量 comboxIndex,borderTypeIndex,filltypeIndex分别来记录。

自定义变量记录选项的值

void AttributeDialog::OnBnClickedOk()
{
	// TODO: 在此添加控件通知处理程序代码
	CDialogEx::OnOK();
	comboxIndex = shapeType.GetCurSel(); // 获取图形类型选项值
	borderTypeIndex = borderType.GetCurSel(); // 获取边框类型选项值
	filltypeIndex = fillType.GetCurSel(); // 获取填充类型选项值
	borderColor = borderColorButton.GetColor(); // 获取边框颜色值
	fillColor = fillColorButton.GetColor(); // 获取填充颜色值
}

获取界面上的值之后,我们就可以根据这些值来绘制我们的图形了。

回到 DrawingView.cpp 文件的 OnLButtonDown() 函数,(鼠标指到这里)我们创建对话框对象,并使对话框已经显示了。

这里要获取鼠标的位置,涉及到设备坐标转换成逻辑坐标(DP->LP)的问题,课程设计任务书中已经给出了代码。(鼠标指到这里)

设备坐标转换为逻辑坐标

我们可以在对话框显示之前获取逻辑坐标,然后当对话框显示之后让鼠标的坐标位置显示出来。(边写代码边讲解)

根据对话框的 DoModal() 函数的返回值,我们可以知道对话框界面上的确定按钮是否被按下,进而获取我们想要的值。

我们获取值时不管绘制的是什么图形,要把界面上所有的值都获取到,不同的图形使用的值也不同,各取所用。

根据用户选择的图形类型,我们来绘制相关的函数,我们可以用一个 switch 语句来做。

按照我们设置的图形序号,是 0 就绘制正方形,是 1 就绘制矩形,是 2 就绘制圆形,是 3 就绘制椭圆形,是 4 就绘制正三角形,是 5 就绘制文本。

下拉列表初始化内容

// 按下鼠标左键的消息响应函数
void CDrawingView::OnLButtonDown(UINT nFlags, CPoint point)
{
	// TODO: 在此添加消息处理程序代码和/或调用默认值
	// 设备坐标转换成逻辑坐标(DP->LP)
	CClientDC dc(this);
	CPoint pntLogical = point;
	OnPrepareDC(&dc);
	dc.DPtoLP(&pntLogical);

	// 判断在鼠标左键按下的同时,Ctrl键是否按下
	if ((nFlags&MK_CONTROL) == MK_CONTROL)
	{
		AttributeDialog dialog; // 创建对话框对象
		// 在对话框显示之前获取逻辑坐标
		dialog.orgX = pntLogical.x;
		dialog.orgY = pntLogical.y;
		// 对话框的返回值是否是IDOK
		if(dialog.DoModal() == IDOK)
		{
			//获取图形的属性
			int orgX = dialog.orgX;
			int orgY = dialog.orgY;
			int width = dialog.width;
			int height = dialog.height;
			CString textContent = dialog.textContent;
			int textAngle = dialog.textAngle;
			int borderWidth = dialog.borderWidth;
			int borderType = dialog.borderTypeIndex;
			COLORREF borderColor = dialog.borderColor;
			int fillType = dialog.filltypeIndex;
			int fillColor = dialog.fillColor;
			int comboxIndex = dialog.comboxIndex;
			switch(comboxIndex)
			{
			case 0:
				{
					// 正方形
					WSquare *p = new WSquare(orgX, orgY, width, borderWidth, borderType, borderColor, fillType, fillColor);
					// 绘制正方形
				}
             ...
		}
	} 
}

绘制图形,肯定是调用图形对象中的绘图函数啦,但是我们还要在每个 case 语句中传入设备指针 CDC* pDC。

用心的同学可能已经发现 DrawingView.cpp 文件有一个 OnDraw() 函数,(鼠标指到这里),这个函数是绘制程序的整个 View 界面的,而每个图形中的 Draw() 函数只是这个 OnDraw() 函数绘制的一小部分。

每个图形的绘图函数要受这个 OnDraw() 函数的调控,我们还可以看到这个函数中有 CDrawingDoc 类的对象,这是我们整个程序的数据文档存储对象。

全局的绘图函数

我们应该把我们绘制的图形都保存到这个文档对象中,然后在这个 OnDraw() 函数逐一绘制,刷新界面就可以看到绘制的图形,这就是绘图的整个流程。

现在我们再打开 DrawingDoc.h 和 DrawingDoc.cpp 文件,既然我们要保存绘制的图形,就需要一个集合对象来保存这些图形对象。

这里我们使用数组来保存,也便于图形对象的存取,我们在DrawingDoc的头文件中加上 CObArray shapeArray; // 图元数组

所以在 case 语句中,我们只需要将图形对象保存到文档对象CDrawingDoc的数组中,然后再 OnDraw() 函数中取出,执行绘图函数 Draw(),刷新界面即可。

下面继续 OnLButtonDown() 函数的编写,我们要使用文档对象的数组,就需要获得这个对象,和 OnDraw() 函数中的写法一样 CDrawingDoc *pDoc = GetDocument();。(边讲解边写代码)

所以正方形的代码就是:

case 0:
{
    // 正方形
    WSquare *p = new WSquare(orgX, orgY, width, borderWidth, borderType, borderColor, fillType, fillColor);
    pDoc->shapeArray.Add(p);
    break;
}

其他图形的写法类似,同学们自己完成。

if ((nFlags&MK_CONTROL) == MK_CONTROL)
	{
		AttributeDialog dialog; // 创建对话框对象
		// 在对话框显示之前获取逻辑坐标
		dialog.orgX = pntLogical.x;
		dialog.orgY = pntLogical.y;
		// 对话框的返回值是否是IDOK
		if(dialog.DoModal() == IDOK)
		{
			//获取图形的属性
			int orgX = dialog.orgX;
			int orgY = dialog.orgY;
			int width = dialog.width;
			int height = dialog.height;
			CString textContent = dialog.textContent;
			int textAngle = dialog.textAngle;
			int borderWidth = dialog.borderWidth;
			int borderType = dialog.borderTypeIndex;
			COLORREF borderColor = dialog.borderColor;
			int fillType = dialog.filltypeIndex;
			int fillColor = dialog.fillColor;
			int comboxIndex = dialog.comboxIndex;
			switch(comboxIndex)
			{
			case 0:
				{
					// 正方形
					WSquare *p = new WSquare(orgX, orgY, width, borderWidth, borderType, borderColor, fillType, fillColor);
					pDoc->shapeArray.Add(p);
					break;
				}
             ...
             }
			Invalidate();//刷新窗口
		}
	} 

在 switch 语句之后,我们调用 Invalidate() 函数刷新整个程序窗口,这个刷新函数会调用 OnDraw() 函数,我们接着来编写 OnDraw() 函数。(边讲解边写代码)

我们可以得到图形数组中的对象数量,遍历数组,利用多态性,使用图形基类的父类指针指向数组中的每个图形对象,调用子类中的 Draw() 函数,这样就完成了整个图形的绘制了。

void CDrawingView::OnDraw(CDC* pDC)
{
	CDrawingDoc *pDoc = GetDocument();
	ASSERT_VALID(pDoc);
	if (!pDoc)
		return;

	// TODO: 在此处为本机数据添加绘制代码
	int count = pDoc->shapeArray.GetCount();
	if(count > 0)
	{
		WShape *p = NULL;
		for(int i = 0; i < count; i++)
		{
			p = (WShape*)pDoc->shapeArray[i];
			p->Draw(pDC);
		}
	}
}

每次程序界面刷新,我们创建的图形都会重新绘制一遍,因为计算机的速度很快,我们很难感受到这种图形绘制的过程,感觉像是新图形在原来已经绘制好的图形基础上添加上去的一样。

至此图形绘制的功能就已经完成了,下面我们来完成剩余的功能。

当只按下鼠标左键而没有按下 Ctrl 键时,我们要判断,鼠标是否落在了已经绘制的图形内(运行程序,绘制图形,举例说明)。

判断鼠标是否落在了已有的图形当中,需要使用数组中图形对象的 IsMatched() 函数一一调用。(边讲解边写代码)

如果鼠标落在了某一个图形内,为了更好的用户体验性,我们可以把被选中的图形的参数显示在对话框的相应位置。

和绘制图形一样,在确定按钮按下后,改变图形的属性值,刷新窗口,重新绘制整个界面。

// 判断鼠标的落点是否在图元内,修改操作
else 
{
    //未按下Ctrl键时左击,则逐个比较,看是否命中图元
    WShape *p = NULL;
    int count = pDoc->shapeArray.GetCount();
    for (int i = 0; i < count; i++)
    {
        p = (WShape*)pDoc->shapeArray[i];
        if (p->IsMatched(pntLogical))
        {
            //修改图元属性,从图元属性值里面取值赋值给对话框的变量
            AttributeDialog dialog;
            dialog.orgX = p->OrgX;
            dialog.orgY = p->OrgY;
            dialog.borderColor = p->BorderColor;
            dialog.borderWidth = p->BorderWidth;
            dialog.borderTypeIndex = p->BorderType;

            if (dialog.DoModal() == IDOK)
            {
                //利用改了以后对话框中图元的属性更新到文档图元数组的对象中
                p->OrgX = dialog.orgX;
                p->OrgY = dialog.orgY;

                p->BorderColor = dialog.borderColor;
                p->BorderWidth = dialog.borderWidth;
                p->BorderType = dialog.borderTypeIndex;

                p->FillColor = dialog.fillColor;
                p->FillType = dialog.filltypeIndex;

                p->SetAttribute(p->OrgX, p->OrgY, p->BorderColor, p->BorderType, p->BorderWidth, p->FillColor, p->FillType);
            }
            Invalidate();//刷新窗口
        }
    }
}

图形的删除功能,同学们想一想该如何实现呢?

课程设计任务书上要求鼠标右键双击某个图形,删除某个图形。

图形对象保存在一个数组中,我们只要把要删除的图形对象从数组中移除,然后刷新窗口,重新绘制剩下的图形,给人的印象就是删除了某个图形。

和图形修改功能类似,删除图形也要先判断是哪个图形,也是使用图形对象的 IsMatched() 函数逐个比对。

找到想要删除的图形后,为了做的用户体验性更好一点,可以使用简单消息对话框函数 AfxMessageBox(),让用户再次确认是否要删除图形。

消息对话框函数 AfxMessageBox() 的第一个参数是要显示的文本内容,第二个参数决定对话框上的按钮,比如我们只想有确定和取消按钮,就可以将第二个参数设为 MB_OKCANCEL。

根据对话框函数的返回值,来判断用户是否真的要删除,同学们可在网上搜索这个消息对话框函数的其他参数,推荐大家去微软官方的文档中心查询,网址 https://msdn.microsoft.com/zh-cn/library/。(打开网址)

微软类库API

在搜索栏输入我们要查询的函数,比如就是这个消息对话框函数 AfxMessageBox() 吧,我们可以在这里找到这个函数的详细介绍。(带着同学们浏览一下网页)

AfxMessageBox函数的介绍

下面老师带着大家看一下这个 OnRButtonDblClk() 函数,数组中有删除某个元素的函数 RemoveAt() ,参数为要删除的元素的下标。

(编写代码)

void CDrawingView::OnRButtonDblClk(UINT nFlags, CPoint point)
{
	// TODO: 在此添加消息处理程序代码和/或调用默认值
	CDrawingDoc *pDoc = GetDocument();

	CClientDC dc(this);
	CPoint pntLogical = point;
	OnPrepareDC(&dc);
	dc.DPtoLP(&pntLogical);//DP->LP进行转换 

	WShape* p;
	int count = pDoc->shapeArray.GetCount();
	for (int i = 0; i < count; i++)
	{
		p = (WShape*)pDoc->shapeArray[i];
		if (p->IsMatched(pntLogical))
		{
			int value = AfxMessageBox(L"你是否要删除?", MB_OKCANCEL);
			if(value == IDOK)
				pDoc->shapeArray.RemoveAt(i);
		}
	}
	Invalidate();//刷新窗口
}

图形的保存功能看起来很难,其实只需要很少的代码。

(打开 DrawingDoc.cpp 文件鼠标指到 Serialize() 函数)这个函数是整个程序的序列化函数,整个程序的的序列化操作都在这个函数中完成。

我们直接使用图形数组对象中的 Serialize() 函数,即可完成数组中每个图形对象的序列化,所以代码只要这一句 shapeArray.Serialize(ar);。(边讲解边写上)

CArchive类在前面类的设计时讲过,它是负责向文件中读写的类,使用它来完成保存文件和打开文件等操作。

// CDrawingDoc 序列化
void CDrawingDoc::Serialize(CArchive& ar)
{
	shapeArray.Serialize(ar);
}

到目前为止,我们的程序就算基本完成了。

同学们可以运行一下,看看有没有问题,这里老师就不带着大家运行了。

下面我们总结一下整个程序的执行流程。

我们这是一个绘图程序,程序的主类 CDrawingApp 负责整个绘图程序的启动和初始化,这个类是整个程序的司令官。

CMainFrame 类只是负责程序的主框架窗口,窗口上的工具栏,状态栏等都是该类管理的。

CDrawingView 类负责 View 界面的用户交互,处理界面上的消息,显示程序的数据对象,来完成整个程序的功能。

CDrawingDoc 类负责整个程序的数据存储,序列化操作就是典型的数据存取。

本次课程设计的程序是典型的 MFC 应用程序,程序的各个类各司其职,共同完成整个程序具有的功能。

写一个程序要处理的两大问题:

1.数据对象是什么,也可以说是数据模型,本程序的数据对象是图形对象,数据封装成对象存储。

2.数据对象怎么展示,也就是程序的界面,PC端的程序可以是 Windows 程序界面,也可以是基于浏览器的网页,在手机上可以是安卓界面,界面不同,处理的繁易也有区别,但是处理的逻辑大同小异。

不管再复杂的程序,核心都是解决这两个问题。

下一讲,老师将针对往年同学在课程设计中犯得错误做出总结和讲解,避免你们这一届的同学再犯。