动手学MFC之八——鼠标和键盘

对于程序员来说,懒惰是一种美。

鼠标和键盘是我们计算机的两大输入设备,所以我们必须要掌握他们的用法,你是不是乐了,鼠标和键盘我早就会用了,还用你教?且慢,我说的是获取鼠标和键盘的事件,也就是鼠标点击了,程序能知道它点在那个位置,键盘按下了,程序能知道按下了那个键,不会了吧,现在就来学习吧。

上一节我们建立了一个单文档程序,我说过建立步骤里面是有玄机的,今天我们就来道破其一,我们在建立程序的时候点击next,当点到第四步的时候。


停一下,这里可以配置窗口风格,点击那个Advanced按钮


勾选Maximized,这是窗口最大化的意思,这样我们运行程序的时候窗口就会自动最大化。点击Close,其他默认。

         运行一下试试看吧,是不是直接就是最大化的呢?我可是经常用这个配置做程序。

         下面我们为程序添加一个WM_LBUTTONDOWN的消息响应函数,想必大家都知道怎么做了。调出类向导,将class name选择为CxxxView,然后在下面选择WM_LBUTTONDOWN,Add Function,Edit Code,一切都是这么地熟练。


这个消息是鼠标左键单击的消息,看到了函数原型吧,注意这里面有一个Cpoint point的参数,这个参数就代表了我们鼠标左键按下的位置,CPoint类代表一个点,有两个成员变量x和y,代表点的x坐标和y坐标。我们在里面加一段代码:

CString str;
str.Format("x=%d,y=%d",point.x,point.y);
MessageBox(str);

运行一下吧,是不是我们鼠标点击那里,就弹出那里的坐标呢?关于鼠标我们先就讲这么多,当然MFC里面还有一些其它关于鼠标的消息,WM_LBUTTONUP表示鼠标左键抬起,WM_MOUSEMOVE表示鼠标移动等等,大家可以自己去发掘。

         下面讲一下键盘,我们增加一个WM_CHAR消息的响应函数,这个消息代表一个字符按键被按下,什么叫字符按键呢?就是数字,字母,空格等等,而F1,Ctrl,Shift等等就属于非字符按键。这个函数中有一个nChar的变量,它就代表我们按下的字符,我们在里面增加一段代码:

CString str;
str.Format("%c",nChar);
MessageBox(str);

运行下吧,是不是按键就能弹出一个消息框,告诉我们按下了哪个键呢?

         现在我们的程序能够响应鼠标和键盘的操作了,下面我们来做一个稍微有点意思的程序。我们在程序中画一个长50,宽50的矩形,让它在里面自由地翱翔,然后我们的鼠标控制一个小圆圈,如果它指向了那个矩形,就算是瞄准了,然后我们按下空格键,就可以把那个矩形消灭,然后让矩形再在另一个位置重生。

         如果你有一点美术功底,再学习下贴图的知识,那么这个程序完全可以变成一个射击小游戏了。

         废话不多说,但是我们学习之前得先来回忆一下画图怎么弄来着,哦,OnDraw,希望你还记得这个关键的函数。

我们现在View.h这个文件中的View类里面定义一些变量:

CPoint m_ptSquare;		//矩形中心点
CPoint m_ptCircle;		//圆形中心点

注意这种变量的命名方式,m_xxx,前面的m表示这是一个类的成员变量(member),后面的pt表示这是一个点(point),然后再跟一个有意义的单词,变量命名是一个普通程序员和一个高级程序员的一个很重要的区别,而初学者往往忽略变量命名的重要性,然后它们的代码长大以后就臃肿不堪,所以我希望大家能够从现在开始就养成一种命名规范,对今后绝对大有好处。


然后我们别忘了初始化,在MFC里面,我们要养成在类中定义变量后紧接着就是到构造函数中初始化的良好习惯。

         但是初始化为多少呢?圆形中心点好办,我们可以初始化为(0,0),反正到时候它跟着鼠标,但是矩形中心点呢?我们打算一开始就让它在左上角,所以它的坐标是(25,25)。噢,不,你简直太勤奋了,竟然亲自算了两次小学除法50/2=25,我们应该把这件事情交给计算机去做的。

         我们在类定义上面定义一些宏:

#define SQUARE_WIDTH	50
#define SQUARE_HEIGHT	50
#define CIRCLE_RADIUS	10



然后我们在构造函数中引用这些宏,这里多说一句,定义宏的时候同样用有意义的单词,然后用下划线连接,而且一般都是大写,这些不是必须的,但是作为程序员,这些是约定俗成的,就像我们数学定义一个未知数就用x一样。这样的好处也一目了然,让别人一下就能看懂,而不会说你是个外行。

         下面我们可以进行初始化了:

m_ptSquare = CPoint(SQUARE_WIDTH/2, SQUARE_HEIGHT/2);
m_ptCircle = CPoint(0, 0);



Ok,我们在OnDraw中可以画出来了,这里我们认识一个新的类CRect,它代表一个矩形,之前我们画矩形是直接指定左上角坐标和右下角坐标,而现在我们可以用一个CRect。

在OnDraw中加入如下代码:

CRect square = CRect(m_ptSquare.x - SQUARE_WIDTH/2, m_ptSquare.y - SQUARE_HEIGHT/2,
		m_ptSquare.x + SQUARE_WIDTH/2, m_ptSquare.y + SQUARE_HEIGHT/2);
pDC->Rectangle(square);
CRect circle = CRect(m_ptCircle.x - CIRCLE_RADIUS, m_ptCircle.y - CIRCLE_RADIUS,
		m_ptCircle.x + CIRCLE_RADIUS, m_ptCircle.y + CIRCLE_RADIUS);
pDC->Ellipse(circle);


可能你觉得用CRect还不如用之前的方法,但是后面你就知道CRect的方便了。

         现在我们先来运行一下程序吧。


是不是画出图来了呢?

         下面我们先让圆圈动起来,我们添加一个WM_MOUSEMOVE消息的响应函数,在里面添加代码:

m_ptCircle = point;
Invalidate();



这两句代码大家都能看懂吧,运行下,圆圈是不是跟着鼠标动了呢?

         然后我们让矩形随机地动,我们在程序启动是就设置一个定时器,让矩形的坐标每个100ms就随机改变一下,那么在哪里开启定时器呢?很直观,我们会想到在View的构造函数中添加SetTimer(1,100,NULL);但是这样,你运行下,却发现了这个


遇到这种情况不要害怕,点击中止吧,你的程序出错了,而且带着毫无营养的信息出错了,这是MFC做得比较不好的一点,出错的时候没有营养信息,造成调试起来较困难,所以我们更要编一点代码就调试一下。想象下,如果我们把代码全部编完了,突然发现这样一个信息,我们能找到是这个SetTimer(1,100,NULL);写错地方了吗?恐怕很难了。

         那么为什么在View的构造函数中不能启动定时器呢?这是因为View的构造函数是执行得比较早的一个函数,在这个时候MFC的一些资源啥的还没有初始化好。打个比方,早上8点的时候,你还在被窝里睡觉,这时候你的岳父岳母突然来敲门了,你总不能立马去开门吧,好歹穿上衣服是吧?进来之后你也不能立马谈正事吧?好歹让他们先坐着,你去刷完牙洗完脸再来谈正事。MFC也是一样,在View的构造函数中还没有准备好启动定时器的条件,所以不能在里面启动它,那么我们往下找,有一个PreCreateWindow函数,看名字就知道,它是在创建Window之前调用的,在这里面行不行呢?试了一下,还是不行,看来MFC这时候还没准备好,再往下好像没有什么好的函数了,怎么办呢?别急,打开类向导,我们可以自己添加一个消息响应函数啊,在类向导里,我们找到OnInitialUpdate这个函数,Add Function,Edit Code,这样一来我们就找到了一个函数,看它的描述(下方的Description),它是当第一个视图和文档关联的时候调用。


不管这么多啦,我们在这里面试一下,哇,成功了。这里我们可以得到一个结论,那就是OnInitialUpdate这个函数在PreCreateWindow和View的构造函数之后调用,我们在MFC中初始化一些资源的时候可以在这个函数中写代码。

         下面我们添加WM_TIMER的消息响应,在里面添加:

m_ptSquare.x += rand() % 21 - 10;
m_ptSquare.y += rand() % 21 - 10;
Invalidate();

稍微解释一下,rand()函数会返回一个随机整数,范围从0到RAND_MAX,当然RAND_MAX是一个系统定义的宏,可能在不同的系统不一样,或者不用的版本不一样,我们这里不比深究它是多少,总之是一个比较大的数。然后我们让它对21取余,这样它的范围就变成了0到20,然后再减去10,范围变成-10到10。

         运行一下吧,不好,它怎么跑到屏幕外面去了?啊,这是一个Bug,也许你会说,把矩形初始位置调到中间不就行了吗?但是这样还是可能造成矩形出界,虽然概率很小,但是作为一个程序员,绝对不能抱任何侥幸心理,否则酿成大错后悔都来不及,谁也不知道你这里的一个侥幸心理会对今后的程序造成什么样的影响,今后很可能因为有这样一个出界的隐患导致软件有些时候会die掉,所以我们决不允许这样的事情发生。

         我们要100%从逻辑上保证矩形不会出界,程序员就是玩逻辑的嘛。那么怎么办呢,我们需要获得窗口的大小,这样不就有办法了吗?百度一下,就知道GetClientRect这个函数可以获得窗口大小。我们调整一下代码:

CRect clientRect;
GetClientRect(clientRect);
m_ptSquare.x += rand() % 21 - 10;
m_ptSquare.y += rand() % 21 - 10;
while(!(m_ptSquare.x - SQUARE_WIDTH/2 > clientRect.left && m_ptSquare.x + SQUARE_WIDTH/2 < clientRect.right
	&& m_ptSquare.y - SQUARE_HEIGHT/2 > clientRect.top && m_ptSquare.y + SQUARE_HEIGHT/2 < clientRect.bottom))
{
	m_ptSquare.x += rand() % 21 - 10;
	m_ptSquare.y += rand() % 21 - 10;
}
Invalidate();

我们通过窗口大小来判断矩形是否出界,如果出界则重新生成矩形坐标,以上代码如有不明白的地方欢迎在评论中和我讨论,再运行下,矩形就不会出界了。

         下面我们还要增加一个功能,就是圆圈在矩形中的时候按下空格,要让矩形消失,然后再另一个地方产生一个新矩形,这个功能在那个消息响应中增加呢?稍微想一想就知道在按键消息响应中增加,因为它是按键的时候发生的事情。

         在WM_CHAR的消息响应函数中添加代码:

if(' ' == nChar)
{
	CRect squareRect = CRect(m_ptSquare.x - SQUARE_WIDTH/2, m_ptSquare.y - SQUARE_HEIGHT/2,
		m_ptSquare.x + SQUARE_WIDTH/2, m_ptSquare.y + SQUARE_HEIGHT/2);
	CRect clientRect;
	GetClientRect(clientRect);
	if(squareRect.PtInRect(m_ptCircle))
	{
		m_ptSquare.x = rand() % (clientRect.right - SQUARE_WIDTH) + SQUARE_WIDTH/2;
		m_ptSquare.y = rand() % (clientRect.bottom - SQUARE_HEIGHT) + SQUARE_HEIGHT/2;
		Invalidate();
	}
}

稍微解释一下:首先判断是否按下空格,然后得到矩形的Rect和窗口Rect,判断点是否在矩形中,使用PtInRect函数,注意,它是CRect类的成员函数,所以使用squareRect.PtInRect(m_ptCircle),这里,只有圆心在矩形中才会命中。如果命中,我们让矩形的中心随机为下一个位置,然后invalidate。置于随机化的算法,大家自己琢磨下吧,主要是保证矩形不会出界。

运行一下吧,是不是可以玩了呢?但是有一点不太好,我们移动鼠标的时候不知道到底有没有命中,我们想让命中的时候矩形和圆圈都变成红色。

那么这段代码加在哪里呢?这是绘图相关的操作,当然在OnDraw里面。

将OnDraw中添加的代码改一下:
CRect square = CRect(m_ptSquare.x - SQUARE_WIDTH/2, m_ptSquare.y - SQUARE_HEIGHT/2,
		m_ptSquare.x + SQUARE_WIDTH/2, m_ptSquare.y + SQUARE_HEIGHT/2);
CRect circle = CRect(m_ptCircle.x - CIRCLE_RADIUS, m_ptCircle.y - CIRCLE_RADIUS,
		m_ptCircle.x + CIRCLE_RADIUS, m_ptCircle.y + CIRCLE_RADIUS);
if(square.PtInRect(m_ptCircle))
{
	CPen *oldPen,redPen;
	redPen.CreatePen(PS_SOLID,1,RGB(255,0,0));
	oldPen = pDC->SelectObject(&redPen);
	pDC->Rectangle(square);
	pDC->Ellipse(circle);
	pDC->SelectObject(oldPen);
}
else
{
	pDC->Rectangle(square);
	pDC->Ellipse(circle);
}

如果说圆圈在矩形里面,就让矩形和圆圈都变成红色,那么怎么变红色呢?这里要说一下画笔的概念,我们之所以画出来的矩形和圆圈是黑色,是因为MFC默认使用了黑色的画笔,CPen代表一个画笔,我们创建了一个红色画笔,redPen.CreatePen(PS_SOLID,1,RGB(255,0,0));里面三个参数分别是:PS_SOLID,表示实线(还可以是虚线,点划线等等,大家可以看MSDN自己试试其他Style);笔触大小,1表示1像素,颜色RGB(255,0,0),RGB表示红绿蓝三个分量,大家还记得小学的图画课上老师讲过的吧,红绿蓝三原色,这里红绿蓝都是从0-255,值为0表示没有分量,值为255表示100%的分量,RGB(255,0,0)代表红色100%,绿色0%,蓝色0%,所以是红色,如果要弄一个紫色改怎么办呢,那就是RGB(255,0,255)。

         然后我们有这一句oldPen = pDC->SelectObject(&redPen);,这是让pDC选择我们的红色画笔,还记得pDC吧,它是一个画图助手,原来它拿的黑色画笔,画出来当然是黑色咯,现在我们让它拿红色画笔,画出来是不是应该是红色呢?注意这个函数返回了一个东西,我们把它保存在oldPen中,这是画图助手原来使用的那只笔,在这里它就是黑色画笔,我们在画图完毕之后,还要pDC->SelectObject(oldPen);,这是让系统选回黑色画笔。

         这是因为画笔是系统的,系统还要用它画别的东西,所以我们画完后,要马上复原,让系统画别的东西的时候不受影响。

好了,运行下代码试试吧,是不是我们想要的效果呢?

到这里这个程序的所有功能都实现了,但是别急,如果你真的有天赋的话,你会发现这个程序有一个bug,那就是每次矩形的轨迹都是一样的,不信你仔细看,我们不按空格键消灭矩形,每次新打开这个程序,矩形一开始的轨迹都是一样的!这真是令人惊讶,我们不是给它随机数的吗?啊,这是因为rand()函数并不能模拟真正的随机数,它是用过一种特定算法算出一个伪随机数,这种伪随机数的概率分布与随机数几乎没有差别,所以我们可以用它来代替随机数,那么为什么会出现上述现象呢?这是因为随机数算法也是一个算法呀,是算法就该有输入,但是我们的rand函数没有带参数,没有指定输入,所以算法一直是使用默认输入,每次都是一样的输入,那么输出自然也就都一样咯。

解决办法是在初始化的时候指定随机算法的输入,我们用srand(time(NULL));,这里srand接收一个int型参数,作为rand函数的初始输入,也叫种子(seed,s代表seed),而用什么作为初始值呢,为了使每次运行都不一样,我们用时间作为种子,time会返回从1970年1月1号0点0分0秒至今的秒数,用它作为种子再合适不过啦,关于这里面的细节欢迎大家和我讨论,也可自行看MSDN或者百度,这里不再赘述。

这句话加在哪里呢?当然加在我们之前苦苦寻来的OnInitialUpdate函数中。

好了,这节讲完了,让我们欣赏一下最终的作品。

总结一下:

1、  窗口初始最大化的配置

2、  鼠标事件,WM_LBUTTONDOWN和WM_MOUSEMOVE

3、  键盘事件,WM_CHAR

4、  OnInitialUpdate函数

5、  rand和srand

6、  画笔的选择

7、  CRect和CPoint,还有点在矩形中的判断函数PtInRect

8、  获取窗口大小的函数GetClientRect

这节的例子程序在http://download.csdn.net/detail/yeluoxiang/7098443,欢迎大家下载,也欢迎大家和我讨论。



评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值