VS2015之博大精深的MFC项目开发(二)

  • 第二章 MFC原理篇
    • 1、MFC06-1:CString类的测试
      • 1.1 operator+函数
      • 1.2 Delete函数
      • 1.3 Find函数
      • 1.4 Insert函数
      • 1.5 切分函数(Mid、Left、Right)
      • 1.6 其它函数
      • 1.7 反向查找函数ReverseFind
    • 2、MFC06-2:开发一个带列表控件的软件
      • 2.1 Trim系列函数
      • 2.2 GetBuffer和ReleaseBuffer函数
      • 2.3 开发一个带列表控件的软件(CListCtrl类)
    • 3、MFC06-2:继续完善带列表控件的软件开发
      • 3.1 设置界面背景和文字背景颜色
      • 3.2 设置列表扩展风格
      • 3.3 单选某行和多选一些行的特性
    • 4、MFC07-1:完善员工信息管理功能项目开发
      • 4.1 设置焦点
      • 4.2 实现保存功能
    • 5、MFC07-2:继续完善员工信息管理项目开发
      • 5.1 实现加载功能
      • 5.2 把日期类型转换为字符串
      • 5.3 实现修改功能(单行)
      • 5.4 实现多行修改
      • 5.5 实现删除
      • 5.6 实现退出功能
    • 6、MFC07-3:MFC类库封装原理
      • 6.1 在C语言中我们是如何使用时间的
      • 6.2 使用MFC类库中的时间族CTime
      • 6.3 用C语言把你的生日存入到一个时间对象里
      • 6.4 再看看MFC类的存入时间的方法
      • 6.5 自己封装一个Time类
    • 7、MFC08-1:开发一个记事本软件
      • 7.1 讲解初始化消息OnInitDialog
      • 7.2 添加菜单资源
      • 7.3 实现菜单退出项的功能
      • 7.4 记事本的文件拖入打开功能
        • 对拖入的每个文件名进行提取
    • 8、MFC08-2:继续开发记事本软件
      • 8.1 对话框窗体的属性(外观、行为)
      • 8.2 编辑控件的风格
      • 8.3 完善编辑框的功能
      • 8.4 添加一下close消息处理
    • 9、MFC08-3:记事本软件功能完善
      • 9.1 粘贴功能
      • 9.2 实现粘贴功能
      • 9.3 全选功能
      • 9.4 插入日期
      • 9.5 简介消息映射
      • 9.6 剖析消息映射机制
        • DECLARE_MESSAGE_MAP、BEGIN_MESSAGE_MAP
    • 10、MFC09-1:读取文本文件的软件开发
      • 10.1 文本编码格式(UTF-8、UTF-16)
      • 10.2 读取UTF-8转换为Unicode
    • 11、MFC09-2:对话框常用的回调函数
      • 11.1 读取Unicode文件
      • 11.2 对话框常用的回调函数
        • 11.2.1 WM_CREATE和WM_INITDIALOG消息
        • 11.2.2 WM_CLOSE和WM_DESTROY消息
        • 11.2.3 测试WM_SYSCOMMAND消息
        • 11.2.4 测试WM_CLOSE消息
        • 11.2.5 OnCancel虚函数
        • 11.2.6 WM_DESTROY消息
    • 12、MFC09-3:消息传递函数
      • 12.1 SendMessage函数
      • 12.2 PostMessage函数
      • 12.3 Win32下测试SendMessage的返回值
      • 12.4 MFC下测试SendMessage的返回值
        • 在MFC中自定义消息应该怎么建立呢?

第二章 MFC原理篇

1、MFC06-1:CString类的测试

1.1 operator+函数

那么两个TCHAR是否能相加呢?

错了,不能想加,我们学C语言的时候都知道,两个指针相减还是有道理的,两个指针相减是两个地址之间的距离,是一个整数;
但是两个指针相加那就没有道理了。
只有第一个是对象,或者第二个是对象,才可以无限的连加,后面再有多少个字符串就都没问题了。

1.2 Delete函数

我们在试试给参数nCount设为-1看看能不能删除到结尾:

我们看到参数2设为-1没有效果。

我故意把参数2的值也得很长,程序也不会崩溃。

1.3 Find函数

我们发现没查找到,这是因为我们查找的那串字符不是个字符串。

如果这个Find函数查不到的话就返回-1。

从第5个位置开始查找的话,字符a在整个字符串的第6个位置上。

大家在每要测试一个新的内容的时候,要新建一个按钮专门测试这个内容。

1.4 Insert函数

每测一个函数你要用心的把它用心的做一个按钮,好好的练一下。

在字符串中间某个位置插入的话:

1.5 切分函数(Mid、Left、Right)

Mid函数是一个功能全面的切分函数,用Mid可以代替Left和Right函数,当你要字符串左边3个或者字符串右边3个的话,用Left和Right比较方便。

如果要求从字符串第3个位置以后的部分,有几种方法可以实现呢?

  • 用Right函数来实现

  • 用Mid函数来实现

第2个参数如果为-1的话不行,返回值为空。

我们发现第2个参数设置为字符串长度也可以,该值超长的话也不出错。

其实按照函数说明,不提供第2个参数的话,会一直到结尾;
单参数版本的Mid函数就是指从第几个位置开始一直到结尾。

所以说,Mid是功能最全面的切分函数,凡是Right能做到的,Left能做到的,Mid都能做到。

要切字符串右边的3个:

或者用单参数版本也行:

Mid函数也可以实现Left的功能:

在我们模拟CString类的时候,Left和Right的功能我们可以用Mid这个函数做出来。

1.6 其它函数

这个nID参数是指资源中字符串表中的字符串,如果资源中没有字符串表,你可以加入一个;
字符串表中可以放一些测试的字符串:

IDS_TEST这个ID将生成到resource.h当中,因为它是一个资源的ID:

我们看到这是一个整数,那么这个整数我们在什么时候使用呢?
当你需要使用字符串列表中的文字的时候:

现在很少使用LoadString函数了,它主要是以前做多国语言版的时候用的,现在有Unicode的话就没必要做多国语言版了。

1.7 反向查找函数ReverseFind

这个反向查找不支持查找一个字符串,只支持查找一个字符。

比方说当我们获得一个目录的时候,我们要从中找到当前的执行文件名:

就得到了程序运行目录,这个是非常好用的,因为在我们程序运行目录下往往还会有一些图片,我们要加载与程序相同目录下的资源、一些文件的时候,这种方法还是非常好用的。

在windows下一般用\r\n更能准确的代表换行,在linux下一般直接就用\n代表换行。

2、MFC06-2:开发一个带列表控件的软件

2.1 Trim系列函数

不带参数的Trim只裁剪字符串两端的空格,中间的空格不裁剪。

中间如果有空格、括号等包含在要裁剪的字符集中的字符仍然不裁,Trim只裁剪左右两端的。

2.2 GetBuffer和ReleaseBuffer函数

这是两个CString类中非常有用的函数,也是最古怪的CString对象的成员函数。

我们经常有一些API的参数是C语言格式的字符串(LPSTR或者LPTSTR),我们想把它直接获取到CString对象里面去(API本来就不支持对象),这样的话以前我们比较笨的方法就是首先定义一个字符缓冲区:

现在可以直接通过CString对象来获取缓冲区:

它会在内部的堆空间上去分配1000个,沿着这1000个去获取,获取完了之后必须要执行ReleaseBuffer函数,它是一个修复函数,必须要用ReleaseBuffer对这个str进行修复,因为在调用GetBuffer之前我们这个str的长度是空的,GetBuffer之后这个str的长度可能还是0,但是你经过ReleaseBuffer对这个str字符串进行修复之后,str将成为真正有用的字符串了,GetLength将会是实际获取到的那个字符串的长度:

我们从上图可以看到,即使是str获取到了系统目录字符串,但是它的长度仍然为0,所以我们要用ReleaseBuffer这个函数对它进行修复一下,修复了之后这个字符串才成为真正可用的字符串:

2.3 开发一个带列表控件的软件(CListCtrl类)

把按回车键默认关闭的代码注释掉,但是按ESC的话可以关闭,所以删除确定按钮,保留取消按钮。

ListBox是一个简易的列表,只有单行效果,现在一般都不用了;
现在凡是多行列表的都是ListControl:

上图这个Edit Labels可以让你实现,点击一个文件名,再点击一下就可以编辑这个文件名:

每一种控件都有自己对应的类,因为不同的控件有不同的操作。

运行起来出错了,一定要记得点重试按钮,再点中断:

双击打开你自己的程序代码,错误应该就是在刚编写的InsertColumn这一行上,没插进去,而且崩溃了。

我们首先看一下CListCtrl这个类的基类是什么:

而CWnd类核心的成员变量就是一个窗口句柄:

我们看到这个句柄是空的;
你创建了一个CListCtrl对象,你要拿这个对象去操作什么,比如说有5个ListControl控件,谁知道你要操作哪一个ListControl控件,你必须将这个CListCtrl对象和某一个控件的ID关联到一起,它没有关联到任何控件上,是不可以操作任何控件的,所以上面这样写是错的。

我们要看CWnd类的该成员函数,因为CWnd类是我们MFC的窗口类。

接着我们点击添加按钮的时候插入行:

消息映射表中就是:一消息(WM_COMMAND)、一ID、一函数。

GetDlgItem函数的返回值说明中,上图所选这句话,是说该函数的返回值是一个临时性指针变量,不要把它储存到成员变量里长期使用;
就是说你每个消息处理函数中都通过GetDlgItem函数获取一下,因为它是一个临时性的,会随时被清理掉。

这个InsertItem函数是插入一行,它只能添加该行第一列的内容,后面列的内容就不支持添加了。

item就是指最左边的列这一项叫item,subitem就是右边剩余的项。

它不但可以设置最左边这一列的文字,也可以通过nSubItem设置其他列的文字。

我们看到最后的日期没有放进来。

3、MFC06-2:继续完善带列表控件的软件开发

通过上节课的学习,我们知道了CListCtrl类的InsertColumn是插入列,InsertItem是插入行,SetItemText可以设置每一行上每列的文字。

把SetItemText函数第2个参数设为0就是给该行第0列添加文字。

我们再插入一列用来添加入职日期:

我们把列宽再调整一下:

现在我们把日期中的星期一这3个字符给它切掉:

3.1 设置界面背景和文字背景颜色

我们还能让列表美化一下:

我们可以通过画图程序得到想要的颜色值,比如下图中所选中这一行的颜色:

按PrtSc SysRq键全屏拷贝,粘贴到画图板程序上,然后我们用提取工具吸一下截图上选中行的颜色:

再点击画图板最右边的编辑颜色就能得到该颜色值:

3.2 设置列表扩展风格

留个作业:

插入的时候,如果要插入的工号已存在就禁止插入,提示已存在!

3.3 单选某行和多选一些行的特性

我们删除功能要完成的是,选中某一行,点击删除按钮删除:

GetSelectionMark这个函数是获得第一个选中行的位置,如上图所示这个虚框,是一个单选标志,在你鼠标第一次点击的位置上,永远不可能有两个同时被选中的这种虚框,只要这个虚框在它就认为被选中了;
虚线总是在一行上,不可能在多行上。

GetSelectedCount函数是多选标志,就是有多少个实际被打上蓝色的:

你仔细看上图可以看到这个虚线(虚框)还在,现在你要删除的话,GetSelectionMask函数有效,但是GetSelectedCount函数无效,它认为一个都没选中,但是这种情况下GetSelectionMask仍然是有效的。

这样的情况下,即使你一行没选中,它有个虚线(虚框)在那里,GetSelectionMask也认为是选中了:

而GetSelectedCount它是多行选择,它是按照蓝色作为选中标志的。

这个函数很显然是多选函数,它可以把第一个被选中的位置返回;它常和GetNextSelectedItem函数配合使用可以遍历选中的多项:

你在程序里也可以用多选的这类函数,因为多选毕竟更准确一些,多选是以蓝色为选中标志,更醒目。

留个作业:
用多选标志之类的函数实现上述删除功能。

你如果不想让它多选,可以设置Single Selection属性为True。

这个属性是非常重要的属性,就是当你焦点离开了这个列表,它还一直是选中的状态。
如下图所示,当你焦点离开,它还有一个灰色的选中标志:

现在我们怎么能够不倒着插入每一行呢?
我们想到的笨的不实用的方法就是定义一个全局变量或者成员变量,每插入一行就i++,如果中间有删除的话i也相应自减,但是这种方法比较麻烦,这个i你要自己维护。

如果这个列表控件自身就带有记录总数的功能就比较好了,也就是说我们不需要定义一个全局变量或者成员变量来维护;
CListCtrl类中有GetItemCount函数能够获取到总数:

这样的话我们就可以正序插入了,不再倒序插入了。

留个作业:
写个循环,判断所插入的工号是否已经存在,禁止重复录入。

我们希望把列表中的数据保存到一个文件中,待下次打开软件时,可以通过加载按钮加载进软件列表里。

编译运行测试后发现可以创建文件。

我们以后再实现循环的把列表中每一行的数据保存在文件里,并且还能从文件中加载数据到列表中。

4、MFC07-1:完善员工信息管理功能项目开发

我们要做一个循环查找要添加的工号是否已有记录。

当点击上图确定后,我们想让焦点在已经清空的工号编辑框里,以便重新输入;
由于添加按钮函数里的代码太多了,所以我们给CWorkerDlg添加一个成员函数:

4.1 设置焦点

给对话框设置焦点,我们测试程序发现焦点并没有设置成功。

我们按ctrl+d看一下tab顺序:

我们用鼠标点击这些编号就能改变顺序:

让程序启动的时候焦点就落在1号上,由于1是不接受焦点的,所以焦点会自动落在2上。
我们可以查看这两个控件的Tabstop属性:

设置变量i的值为当前总行数就可以实现在当前行的下一行上插入:

把CheckNumber的参数改为LPCTSTR类型:

修改头文件中该函数的声明:

我们如果想实现删除选中的多行数据(不连续、间隔的,例如用按着ctrl键用鼠标点击第3、5、7行):


选中项我们要全部删除,怎么样来循环把选中的几项都删掉呢?
这里我们先完成修改函数,因为修改会把选中的几项全部修改成一样的。

4.2 实现保存功能

对了,先等等,我们还是先把保存函数先做好,这样方便其他功能的测试:

这个SInfo结构体是私有的,自己用的,不需要对外边使用,那你就放在类内就可以了。

在退出的时候我们要遍历每一个列表项,把数据存下去。

为什么sName这个成员不使用可以自动增长CString类型呢?用CString的话就不用管它的长度,用多少就使多少,但是需要很复杂的计算,为什么呢?
因为CString里面是一个指针变量,它在结构体中只占4个字节,你把CString里面的指针变量存到文件里了,那么进程退出后,这个指针变量指向的内存肯定是释放了,重新启动这个进程,把文件中该指针的值提取出来,这个指针所指向的位置上根本没有你上次保存的名字那些数据;
所以说,CString不能直接存在文件里,也不能发送给网络程序把这个CString给另一台机器,你要把这个CString实际指向的内容发送出去才对。

要计算数组大小要用sizeof,也就是整个结构体在控件上占用的字节数,而不能用_countof,因为_countof是用来计算数组的,是用总共的空间除以单个元素的空间。

所有的C语言函数在我们的VS开发的时候,你都要看一下帮助,有宽的,有窄的,还有自适应的TCHAR类型的。

为了将入职日期这个字符串转换为CTime类型,我们查看了CTime类的帮助,发现没有合适的成员函数;
我们又看了COleDateTime类:

只要你有年月日时分秒的东西带进来,就给你生成一个COleDateTime对象,这个函数具有将字符串转换成时间的功能,它内部有自动拆分的功能。

用CTime类型的话就很麻烦了,你要想把带进来的字符串拆分成年、月、日、时、分、秒,再把它们存入到CTime对象里来,这是一个很麻烦的过程。

果然,tDate变成了非0的数字,时间部分是空的,只把日期部分(年月日)合成一个数字放在这里,这是一个准确的时间。

这个COleDateTime类的核心数据成员:

double是8个字节;
也就是说COleDateTime这个类的对象占12个字节。

可以看到保存的Worker.dat文件的大小是52个字节。

这是用二进制来看的,前面的4个字节0x000003E9的十进制就是1001,也就是我们输入的工号。

上图选中的8个字节就是COleDateTime对象中的DATA类型的m_dt数据成员,占8个字节(低4字节是时间部分为空);最后的4个字节是该对象中的m_status数据成员。

5、MFC07-2:继续完善员工信息管理项目开发

5.1 实现加载功能

每读取出来一行数据,就把它放到列表中。

从文件中读取一个结构体(SInfo),只要Read的返回值每次都是一个完整的结构体,不是半个结构体,就可以继续循环。

5.2 把日期类型转换为字符串

我们发现这个日期带有星期,我们如果不开心使用这个类的Format,不想使用这种文字日期格式的话,我们就要手工的去Format了,就用CString的Format,它的Format是简易的Format;
这里我们还是用这个类的其他Format重载形式看看。


我们用CString的Format,它的Format是简易的Format,用它的话就比较累些:

让软件一启动的时候自动加载:

再把加载按钮给它删掉:

我们还可以在软件退出的时候,提示用户是否需要保存。

5.3 实现修改功能(单行)

我们在修改的时候一般不改编号,而且我们多数情况下只对第一个选中的进行修改。

我们看到GetSelectedCount返回值是3,这个可以用来判断是否有选中的行。
不用这个方法,我们用GetFirstSelectedItemPosition函数的返回值来判断也是可以的:

我们看到GetFirstSelectedItemPosition函数的返回值就不是空了,它的值是3;接着往下执行的话GetSelectedCount的返回值nSel的值是2。

上面实现的是单选的修改。

5.4 实现多行修改

5.5 实现删除

大家现在如果要删除多行的话,用刚才修改里面的那个循环,保证会出问题,为什么出问题呢?

在你多选的行为1、3、4行的时候,循环中删除了第1行的时候,下面的2行就成了1行,3行成了2行,4行成了3行,就可能删串了;然后你本来该删3行的,结果变成了删的是4行;当你要删4行的时候,这一行成空行了。
整个删除的过程会往上串,如果你没考虑串的这个问题的话,最后你会发现乱了,没有达到你的预期:

第一个删除是对的,但是当你删除了第一个之后,直接就跳到删1005去了,把1004给跳过了:

把1004给漏下了。

现在是要删除1003、1005、1007。

删除1003是对的,但是删完了1003之后1005往上串了,如果你把1006删除了,就会串到1008去了,删错了1008:

漏删了1005和1007:

解决方法:
用两个循环,先用一个循环把要删除的记录到一个数组里,然后再倒着删除比较好,从底下往上删除就不会出现串的问题了。

如果你不想从下往上删除,你就用一个循环,那就需要调整代码,要删除的第一个序号不减,之后你每删除一次就要把得到的pos序号减1,这就需要你好好想想该怎么写这个算法。

5.6 实现退出功能

我们在类中添加一个BOOL类型的成员变量,用来记录数据是否被改动过了:

也可以在OnInitDialog函数这里让它等于FALSE也是可以的,软件启动之后等于FALSE就行了。

在添加、删除(确实删除成功了)、修改(确实修改成功了)函数里面给它赋值为TRUE。

还要记得,如果保存成功了,要把这个标志修改为FALSE:

也就是说,如果你在退出之前,主动点击了保存按钮,已经保存过了(在保存的地方把修改标志变成FALSE),这种情况下你退出的时候就不需要再保存了,你已经保存过这个修改了,然后你在退出时它就不用提示了,直接退出即可。

6、MFC07-3:MFC类库封装原理

6.1 在C语言中我们是如何使用时间的

我们对C语言的Time和MFC的CTime类进行一个对比,在这个基础上我们自己把这个C语言的Time封装成一个类。

在C语言中获取当前时间是很痛苦的:

根据你的操作系统是64位还是32位,这个time_t会自动选择,编译器会根据你的机器自动选择64还是32的,你直接用就可以了。

这串数字你通过肉眼根本是识别不出来年月日时分秒的,这个数字每过一秒会增加1下。

这个time函数既可以从参数返回结果,也能从函数返回值返回结果:

拆分函数是localtime:

struct tm 这个结构体就包含了年月日时分秒。
也可以使用安全版本的函数:

这个tm_year的115,是指从民国时间开始算起的:

6.2 使用MFC类库中的时间族CTime

CTime的核心数据成员就是m_time:

一个用类的获取时间的方法,一个是用C语言函数获取,很显然C语言比较笨拙。

6.3 用C语言把你的生日存入到一个时间对象里

这个mktime函数是localtime函数的反函数,它可以生成你指定的这个time_t结构。

测试mktime函数后,我们就可以获取上图界面中填入的生日,存入tm结构,再通过mktime生成time_t结构;你还可以把数据存入文件中就比较完善了。

6.4 再看看MFC类的存入时间的方法

你如果用C语言来做的话,还得把时间解析出来,就比较笨了。

你可以通过程序界面上的控件,把获取到的年月日时分秒存入到日期对象里:

所以我们这个CTime这个类,无论是存入时间,还是获取当前时间,还是从其中取出年月日时分秒,都非常方便。

6.5 自己封装一个Time类

使用localtime的安全版本localtime_s:

把不好用的东西放在内核里封装起来,做成好用的东西。

测试用我们自己封装的类:

获取当前时间函数,我们直接返回time(NULL)的返回值就可以了,time函数返回的time_t 时间句柄会自动构造CMyTime对象。

7、MFC08-1:开发一个记事本软件

7.1 讲解初始化消息OnInitDialog

我们看到上图的OnInitDialog这本来是一个消息,却没有出现在上面的BEGIN_MESSAGE_MAP位置那里,这是因为这个函数被CDialog类给做成在基类中去截获这个消息,然后再以虚函数的方式反调到派生类来;

  • OnInitDialog
  • OnOK
  • OnCancel
    总共有这3个虚函数,这3个虚函数你都不需要在消息映射中去添加,你要添加的话,你可以在类向导的虚函数中添加就行了:

本来这3个消息,有两个是按钮消息,一个是初始化消息;
我们从上图可以看到,现在这个初始化消息已经是在虚函数中添加了。

对于每一个窗口有两种回调方式,第一种回调就是消息映射,消息映射是最基础的回调方式;
在消息映射的基础上呢,有些函数呢它又通过虚函数回调回来,比如说OnOK,这个本来也是按钮消息,是ID_OK的按钮消息,还有一个是OnCancel也是系统消息,原来这3个函数都是系统消息,双击一下就可以编辑这个消息的处理代码,但是都被基类把消息给提前处理了;
默认按回车键会调用OnOK把该对话框关闭,正常来说按ESC是不会关闭记事本的,所以这里我们把调用基类的OnOK和OnCancel都注释掉,把这两个按钮的缺省动作都去掉:

今后要退出时要从菜单退出,OnOK和OnCancel这两个地方不退。

7.2 添加菜单资源

如果以前的工程中有现成的菜单,我们想把它添加到我们现有的工程中的话,比如以前用VC6做过的一个工程;
或者一个现成的exe的资源,比如系统Windows目录下的notepad.exe:

我们看到这里果然有菜单,把这个菜单给它拿出来那该多好啊,我们试着用VS2015打开这个资源:

我们发现好像不行,如果VS也能像VC6那样把资源提取出来就好了:

我们发现VS没有提取资源的功能。

VC6也无法导出这个资源,我们用VS2015导不进来,也无法从文件、打开来打开这个资源;
经过反复的试验,我们可以先用VC6另建一个对话框工程,把这个有菜单的资源添加进这儿新VC6工程中,然后再从这个工程里把这个资源弄出去:

用VC6打开notepad.exe这个执行文件的资源:

把这个执行文件的菜单从到我们新建的VC6工程里面来:

保存一下。
再用VS2015添加现有项,添加刚才那个VC6新工程的.rc资源文件:

把VC6的菜单资源拖动到我们工程里面来。

然后就可以把上图这个添加的VC6.rc删除了。

这就是将外部资源,尤其是执行文件的资源,拖入到我们的工程中的方法。
这样的话已经有很多资源编辑好了,我们只要把自己的资源删掉,留已经做好的资源,然后就可以在我们对话框中把这个菜单添加进来:

把添加的这个菜单的ID改一下(方便识别而且易用),就是我们自己的菜单了。

再把菜单添加到我们的对话框里:

这个菜单里的ID不好看,我们把菜单中的那些命令的ID修改系统中已有的ID,这样方便识别而且易用,编译运行后:

退出和关于这两个菜单项的系统ID一般是ID_APP_开头的。

7.3 实现菜单退出项的功能

像这种菜单是如何生成一个消息映射函数呢?只要你选择CNotepadDlg这个类,或者选择这个对话框资源,打开类向导:

这个类向导能做几项功能:
当你直接选择消息的时候,这个消息是主窗口消息,包括LBUTTONDOWN、MOUSE_MOVE等;

虚函数也是主对话框的,一般不是控件的回调,都是主对话框的事情。

命令里有很多是按钮、菜单或者说是工具栏;
在windows开发中有3种能够发出命令的东西,一是菜单,二是按钮,三是工具栏快捷键可能也能发出命令。

我们在win32下是由自己去做这种COMMAND消息的截获,在我们MFC中,如上图所示只要选择了左边的命令ID,双击COMMAND就会自动生成一个OnAppExit消息处理函数:

这个EndDialog将IDCANCEL参数代进去的时候,它将从应用程序实例CNotepadApp的InitInstance传出来;
我们把上图中的启动函数清理一下,里面只留一个对话框的弹出:

这里我们干脆把菜单项的ID传进去,DoModal的返回值通过这个EndDialog传入的参数可以知道,你是按什么方式关闭的这个对话框,是按确定,还是取消,还是某个菜单项;
也就是说EndDialog你放入什么,DoModal就传出什么,有时候用于对点击哪个按钮退出的判断。

7.4 记事本的文件拖入打开功能


我们首先修改整个对话框的属性Accept Files,让它支持拖放文件的方式来打开文件,这样的话,整个对话框都能拖入文件了:

我们的对话框支持拖放后,为了实现拖放打开文件,是要编码的,要对它相应的消息映射函数进行处理:

当你把文件拖进去,鼠标不弹起,不叫Drop,鼠标弹起来了,才叫Drop。

这个hDrop句柄可能是不止一个文件的集合,比如说你拖动好几个图片往Photoshop里扔;
通过这个句柄可以取出好几个文件的文件名,或者是一个文件的文件名。

iFile是说,当你代入-1的时候,你是要求当前有多少个文件被拖入了;
比方说总共拖进来5个,如果你代入的是0到4之间这5个数中的一个数,该函数将把对应的文件名拷贝到lpszFile参数里。

对拖入的每个文件名进行提取

这里我们拖入5个文本文件:

虽然真正的记事本软件不需要循环,记事本只能打开一个文件,但是这里我们通过一个循环来熟悉了解DragQueryFile这个函数;如果你做的是多文档编辑的软件,那么你就要用循环。

这里我们再修改代码,不管你拖入几个,我就只管打开第一个就够了,其他的都不管了:

8、MFC08-2:继续开发记事本软件

8.1 对话框窗体的属性(外观、行为)


设置Border为None的话可麻烦了,这是做子窗口用的,它往往放在一个分页窗口里,每个页面都是这样的子对话框;有些软件的关于对话框就是一副图片的样式,None的话就没有标题;
这里我们设置Border为Resizing恢复回去。

8.2 编辑控件的风格

编辑框的Accept Files即使设置为True,编辑框也接收不到消息,因为Accept接收是在主对话框里接收的,消息是送到主对话框的;
那你这里的编辑框的Accept Files要设置成True的话,它的消息要在这个控件类里接收,也就是说你要做一个CEdit的派生类才能接收到这个消息。

这个No Hide Selection的效果如下图所示:

当焦点跑到另外一个对话框的时候,前一个窗体中选中文字仍然处于选中状态,这个选中仍然有效。

我们也创建一个关于对话框:

并给它添加一个关联的类:

我们在主对话框类上打开类向导,并给ID_APP_ABOUT这个菜单命令添加一个消息响应函数:

并把这个关于对话框的属性No Hide Selection设置为True,这个时候里面选中了一些文字的话,当你的焦点跑了之后,选中的文字仍然处于选中状态:

我们一般不用Auto HScroll,这是个单行的控件,它会自动横向滚动,当你输入的内容太多的时候会自动往右滚,只能在一行上输入显示文字(密码风格只能是单行的控件才能使用);
我们一般用Vertical Scroll,可以让控件内容自动换行向下滚动。

8.3 完善编辑框的功能


8.4 添加一下close消息处理

当你点击系统关闭按钮的时候,或者当你按Alt+F4的时候,由于系统的这个对话框的基类CDialogEx::OnClose函数会自动转入到OnCancel,而我们已经设置OnCancel不工作了,是为了防止你按ESC键把我的记事本给关闭,所以这里就把基类CDialogEx::OnClose函数注释掉:

9、MFC08-3:记事本软件功能完善

9.1 粘贴功能

9.2 实现粘贴功能

我们发现鼠标右键可以粘贴,但是菜单里的粘贴无效,并且选中一部分文字后删除也无效。

我们再看看撤销的功能看看能不能实现:

我们测试发现撤销功能可以工作。

那为什么那几个菜单不可用呢?

我们按照上图所示,把那几个菜单的Grayed属性设置为False(为True的话是灰色不可用的)。

9.3 全选功能

测试后发现鼠标点击全选菜单工作正常,但是它的Ctrl+A这种快捷键功能并没有实现,因为快捷键是一个专门的技术,我们必须要在资源中添加快捷键,但是添加了快捷键之后也不见得能够生效,这是一种专门的技术。
这里我们先暂时把快捷键资源添加上来,先放在这里:

即使添加了这个快捷键,它还是不能使用,为什么呢?大家可以自己琢磨搜索LoadAccelarator研究一下,你要加载快捷键,还要在对话框里设置上快捷键,才能够实现这样的功能,这个技术以后大家慢慢研究。

9.4 插入日期

要实现上图的功能,先得取当前时间;
推荐使用COleDateTime,而不推荐CTime:

但是,我们测试后发现,选中一部分文字后,点击日期,会把编辑框中所有文字覆盖掉了,都丢失了,和正规的记事本不一样,说明SetDlgItemText这个函数很显然是不对的。

第2个参数是指,是否要重新恢复原来的文字。

设置一下对话框的字体:

9.5 简介消息映射

其实消息映射机制我们现在也都知道了,就是在BEGIN_MESSAGE_MAP和END_MESSAGE_MAP之间,将一个ID、一个消息(COMMAND、SIZE、CLOSE)与一个函数关联;


或者将一个消息(ON_WM_PAINT)与一个固定的函数相关联,比如上面这个WM_PAINT消息,它就是绘图消息,它就必须和OnPaint关联,它是一消息、一函数;

有些呢是一消息加一ID、再一函数,那个函数的名字就是死的了,因为它必须根据这个ID生成这个函数名,有些是跟那个ID无关的,比如说ON_WM_CLOSE,这个消息在消息映射中它必然是映射到这个死的OnClose函数名,绝对不可以换,你把这个OnClose函数名改变、改成小写的、大小写搞反一个,它都找不到那个函数,这就是消息映射。

总之,你只要知道,一个消息可以关联到一个函数上就可以了。

9.6 剖析消息映射机制


大致的消息映射,就是刚才所说的那些理论,就是一消息、一ID、一函数,或者一消息、一函数。

Win32没有消息映射,Win32是使用消息处理函数,统一把所有的消息都在一个消息处理函数中进行分流;
而我们的MFC呢是系统帮你把内部的内容分流好了。

DECLARE_MESSAGE_MAP、BEGIN_MESSAGE_MAP


像上图头文件中最后一句话DECLARE_MESSAGE_MAP是什么意思呢?

这句话其实是隐藏了很多内容的,我们把它还原一下:


而实现文件中的BEGIN_MESSAGE_MAP这一句话,实际上带了一堆东西:


我们也罢BEGIN_MESSAGE_MAP还原一下:

AFX_MSGMAP_ENTRY这个结构体里面包括了消息、消息ID和函数地址,这就是一个消息对应一个函数地址(函数指针):

就是AFX_MSGMAP_ENTRY这样一个结构体在GetThisMessageMap这函数内定义了一个数组,而且是静态的数组,加上static就代表这个_messageEntries数组的位置放在了全局区,它的有效性是长期的,甚至在程序启动的时候这个数组就可能被初始化了,编译之后把上图这些内容都编译到数组里来了。
所以从上面可以看到,这就是一个消息对应一个函数。
这些结构体初始化好了,也就是填充好了这个AFX_MSGMAP_ENTRY结构体,放在_messageEntries数组里,这个数组里可能是100这种结构体,或者几个结构体。
这样我们就看到了,每个窗口类在全局区定义好了自己的一个结构体数组,这个结构体数组就反应到了,在上层比如基类指针如果获取到了这个数组地址的时候,就会把这个数组里的消息对应的函数地址取出来了。

我们看到GetThieMessageMap这个函数返回的是一个结构体地址,其实它主要返回的就是_messageEntries指针。

messageMap也是一个结构体,AFX_MSGMAP这个结构体里面放了两个指针,这个lpEntries指针只要返回去之后,它就能够沿着这个lpEntries(其实就是_messageEntries数组)找到结构体的地址。

在系统内部有这个窗口的消息循环,它就是在每一次消息循环的时候,把对应的消息在另一个循环里对_messageEntries这个数组进行遍历,找到该消息对应的函数进行调用。

我们刚才手动添加了一个WM_MOUSEMOVE消息映射,但是还没有该消息对应的OnMouseMove函数实现,所我们给窗口类手工添加一个OnMouseMove和这个消息去匹配:

CEdit类成员:

10、MFC09-1:读取文本文件的软件开发

如果发现拖拽功能失效,看看对话框的Accept Files属性是否为True。

我们拖入的文件大小是210字节,因为我们这里是unicode字符集,每个TCHAR占2个字节,sizeof(_T(‘\0’))的值为2,但是从上图监视发现,sFile[nRet] = _T(‘\0’);这里的nRet的值却为210,这是错误的,'\0’的索引应该为105附近才对。
整个的读取过程都是不太正确的。

10.1 文本编码格式(UTF-8、UTF-16)

我们要知道文件一般分为3种,有三种编码格式:

  • ANSI
  • Unicode
  • UTF-8

我们假设拖入的文本文件编码格式为ANSI,修改代码如下:

我们看到这肯定还是文本格式有点问题,我们用VC6以二进制格式来打开拖入的文本文件,看看文本的头会不会是:

我们看到前面明明是等号,但是用VC6以二进制格式打开该文件,开头的3个字节却是EF BB BF,之后才是一堆3D这个等号的ANSI码,那么前面这3个字节到底是什么?

读取文本的过程,我们要分为3种来读取,即我们要区分3种文件,有3种文件要读取,所以我们用3个函数来读取。

通常见到的就是UTF-8和UTF-16这两种格式,UTF-16就是Unicode,我们重点是研究UTF-8和UTF-16格式的文件。

10.2 读取UTF-8转换为Unicode

这个CFile::Seek函数的第二个参数意思就是,你是距离头,还是距离尾,还是距离当前。


其实我们这里只需要把文件转换为Unicode编码格式就够了,不需要转换为UTF-8。

经断点调试发现,前面的中文是对的,然后是一段乱码,接着又是一段正确的中文,然后又是一段乱码,可能是由于我们是一段一段读取的,中断的地方有问题,比如一个中文是两个字节,正好把这两个字节断开了,半个汉字一转码后,显示肯定乱码。。。。。。
所以,如果文件不是特别巨大的话,我们可以把整个文件完整的一次性读取完毕。

记得最后默认读取ANSI的函数那里,要用Seek重新定位到头部,因为ANSI没有编码头。

11、MFC09-2:对话框常用的回调函数

11.1 读取Unicode文件

这里读取到的字符数要算一下,因为这里的TCHAR占2个字节,如果读取到的是10个字节,那么也就相当于是5个字符。

由于CFile::Read的返回值是以字节为单位的,它不是以你的TCHAR为单位的,因为我们当前系统就是Unicode,也就是UTF-16,这个地方的TCHAR是2个字节的,所以Read读取到的长度要除以2:

Windows的记事本软件的另存为,可以选择编码格式,Unicode就是UTF-16,Unicode big endian就是UTF-32了。

11.2 对话框常用的回调函数

11.2.1 WM_CREATE和WM_INITDIALOG消息

为了判断WM_CREATE和WM_INITDIALOG的处理顺序,我们给对话框添加一个CListCtrl控件进行测试,先看看在WM_CREATE消息种是否能够初始化CListCtrl该控件:

WM_CREATE是指我的父窗口已经创建好了,WM_INITDIALOG它不止是父窗口创建好了,而且子控件也创建好了,就等着你去使用它了;
所以你要是想在WM_CREATE对CListCtrl控件进行操作,可能会造成程序崩溃:

很显然,对于子控件的操作,必须在当你的对话框程序里面的初始化这部分消息开始的时候,包括控件已经创建好了,你才能够去调用这些控件。

总之,在WM_INITDIALOG这个消息来的时候,对话框内的资源都已经按照模板中指定的控件都已经创建好了,可以使用了。
对对话框内部控件的初始化操作一般放在WM_INITDIALOG这里。

我们再试试对于主窗口的图标设置放在WM_CREATE处理函数里面是不是可以使用:

我们发现,对主窗口的操作放在这里也没问题。


我们再看看PreSubclassWindow虚函数是在什么时候被执行:

这是一个虚函数回调,我们来看看它发生在OnCreate的过程之前,还是发生在OnInitDialog之后。

我们发现,先调用PreSubclassWindow,然后才去调用OnCreate,这个函数就很强大了。

我们再试试对主窗口设置的函数放在PreSubclassWindow函数里看看是否可以:

说明在这里、在OnCreate、在OnInitDialog设置主窗口图标都是没问题的;
对主窗口的操作,这3个函数都可以,但是对子窗口的操作只有OnInitDialog中执行。

11.2.2 WM_CLOSE和WM_DESTROY消息

接下来我们要看一下关闭过程,首先要介绍的是系统关闭过程。
WM_SYSCOMMAND这个系统命令中包含了:还原、移动、改变大小、最小化、最大化、关闭,就是在下图系统菜单中你能看到的东西,都属于WM_SYSCOMMAND的管辖范围。


WM_CLOSE是系统消息中的CLOSE关闭分支,也就是说WM_CLOSE也是来自于WM_SYSCOMMAND里面总共有6个分支其中的一个分支。
WM_CLOSE最后会进入OnCancel虚函数回调,本来它是个按钮消息,被系统接管过去之后变成一个虚函数回调,当你真正关闭的时候有一个WM_DESTROY消息发生,当WM_DESTROY消息发生的时候就已经无法取消这次关闭了;
当WM_CLOSE消息发生时,或者OnCancel执行时,我们可以取消这次关闭的任务;但是到了WM_DESTROY消息发生时,已经正在摧毁窗口了,是不可以回头必须关闭了。

11.2.3 测试WM_SYSCOMMAND消息

我们做个测试,模仿360或者鲁大师,点击窗口右上角,或者ALT+F4关闭窗口,但其实并没有真正关闭程序,只是隐藏起来了。

第一个参数就告诉你是6种动作中的哪一种,虽然从上图可知不止这6种动作;
如果我们在OnSysCommand函数当中不想关闭的话,我们想把这个窗口隐藏,或者最小化:

在这里设个断点,编译运行程序,当我们ALT+F4的时候进断到这里了,继续运行就将该窗口最小化了;但是当执行最后一行的时候,实际上这个窗口还是关闭了,也就是说走基类的OnSysCommand的话还会执行缺省任务,缺省任务的意思就是如果刚才是关闭任务的话它还继续关闭。
这里实际上我们不想关闭:

因为我们还没有实现托盘功能,如果将该窗口隐藏(SW_HIDE)的话,任务栏看不到该程序,我们就只能通过任务管理器强制关闭该进程。

当你鼠标右击窗口标题栏点击弹出的关闭,或者ALT+F4关闭窗口的时候,这个过程它最先进入的就是OnSysCommand这个消息。

11.2.4 测试WM_CLOSE消息

经过上图修改下断点调试,发现先调用OnSysCommand,然后再去调用OnClose,这样的话呢,我们有的时候就干脆不要这个OnSysCommand消息了,我们可以在类向导种删除OnSysCommand消息:

删掉这个处理程序,它会把头文件中的函数头,实现文件中的函数实现都删除掉,另外还有一个地方被删除掉了,就是消息关联的地方被删除掉了:

我们可以在OnClose中专门去管关闭时的消息,就不需要if分支、判断ID了:

这时候你点右上角的关闭按钮、鼠标右击标题栏点关闭、或者ALT+F4的话,它并没有真正的关闭,而是最小化了,除非你点击取消按钮才是真正的关闭。

11.2.5 OnCancel虚函数

第3个关闭流程,假设你不是没有在OnClose进行最小化处理,你是真正要关闭了,就是还有一个地方可以截获这个关闭消息,就是OnCancel这个虚函数:


你在OnClose和OnCancel中下断点,可以观察到,会从OnClose调用到OnCancel。

11.2.6 WM_DESTROY消息

实际上走到这个消息的时候,窗口的句柄还在,在这个时候你可以执行列表控件的数据保存,并且子窗口都可以使用,唯一的就是它不可以回头了,不能取消关闭了;
而且走到这一步你不用隐藏它了,它自身已经隐藏了,窗口已经处于不可见状态了。

走到这个消息确实是不能再回头了,包括窗口都已经消失了,假设运行该程序后我点击确定按钮,窗口在任务栏中已经没了,只有这个弹窗还能出来。

包括你在OnCancel函数中添加上基类的OnCancel,不管你从哪里执行的关闭对话框的动作,走到了OnDestroy这一步的话是最后一步,整个窗口都没有了:

在这个时候你还可以去执行对列表控件内的数据进行循环保存还是可以的。

因为在这里你可以使用列表进行循环,而真正的析构函数是不能操作这些列表的,因为真正的析构函数,列表的控件都已经被摧毁了。

12、MFC09-3:消息传递函数

12.1 SendMessage函数


现在你在按钮上面双击,是不会建立关联的函数了,那是MFC的功能,MFC双击一个界面中的按钮,它就能建立消息映射函数;
我们在win32下(没有MFC的情况下)必须是在回调函数中去做消息的分支处理。

SendMessage就是模拟系统来发送这种关闭,或者是任何消息都是可以模仿发送出来的。

我们还可以用SendMessage朝一个系统内的其他的进程,只要用FindWindow找到这个句柄,就可以关闭其他软件的窗口,也是可以的。


当这个窗口是阻塞的状态,或者是发送的过程,对方不肯关闭,这个SendMessage是阻塞的,等待你对记事本的操作,所以SendMessage是一个阻塞型的操作,要等你把它处理好它才走,或者等你返回一个结果它才走。

12.2 PostMessage函数

PostMessage是异步的,它是不等的,它告诉你一声就走了,就是说,你要记得把那个窗口关闭啊,它不会像SendMessage那样你要不关闭我就不走,我就等你关闭,什么时候关闭我才结束,它会卡在那里,SendMessage等待消息被处理完了之后才返回;
PostMessage就是把消息放在对方的消息队列里,不管消息处理的结果如何就返回。
SendMessage是必须等你这个消息处理完之后才返回。

一个是阻塞型的,一个不是阻塞型的。
如果是SendMessage,它就要等人家处理,人家不处理它就不干;
PostMessage就不管你那一套,你去处理与不处理,我这里告诉你了,我就完成任务了,后续它就不等待。

12.3 Win32下测试SendMessage的返回值

一般情况下,我们平时开发的时候自定义消息大多数都是加个100或200就够了(WM_USER + 100)。

在这里我们来发一个自定义消息,你要把传进来的两个参数wParam和lParam相加的结果返回:

我看看我在SendMessage这里等待,看它的返回结果是不是UM_ADD分支返回回来的结果,在这两处设置断点;
我们不再往记事本里发送消息了,我们往自己窗口里发送,并且在自己窗口里截获这个UM_ADD消息;由于wParam是一个无符号整数,lParam是有符号整数,所以我们调整代码,把负数放在后面:

理论上SendMessage必须要等待这两个数字送过去之后,在UM_ADD分支那里把两个数字相加的结果要返回来,但是我们发现返回的结果res还是等于0。
在Win32下SendMessage和PostMessage的这种返回值还不好观测;
在Win32下一直截获不到SendMessage的返回结果。

我们去到MFC工程下,再来测SendMessage的返回值,在MFC下是如何截获SendMessage发送来的用户消息或者系统消息。

12.4 MFC下测试SendMessage的返回值

上图function就是Win32 API,而method就是MFC。

在MFC中自定义消息应该怎么建立呢?

ON_MESSAGE是自定义消息的宏。

我们可以看到用SendMessage发送的自定义消息的返回值正确返回回来了。

假设消息处理函数里有一个循环,需要等待2秒才处理完,SendMessage就要等2秒才能返回,是必须等你算出结果后我才走,而PostMessage半秒都不用等直接就走了。

VS2015之博大精深的MFC项目开发(二)相关推荐

  1. Android 实践项目开发二

    在地图开发中项目中,我这周主要完成的任务是和遇到的问题是以下几个方面. 1.在本次的项目中主要是利用百度地图的.jar包实现地图的定位与搜索功能,需要在百度地图开发中心网站取得 密钥,并下载相关.ja ...

  2. 仿掘金社区全栈项目开发(二)-前端工程化

    前端工程化 webpack 核心概念 看官方文档:https://webpack.docschina.org/concepts/ 小demo 参考博客:https://juejin.cn/post/6 ...

  3. Flutter黑马头条项目开发(二.底部切换导航和新闻页面开发)

    底部四个切换导航 它分为首页,问答,视频和我的四大模块 创建lib/home/home.dart首页文件,使用的是bottomNavigationBar组件,官网也有介绍 它有一个onTap函数,这个 ...

  4. NVIDIA Jetson TX1 项目开发二刷机(使用JetPack3.1重装系统)

    上一篇我们谈到了jetson tx1的开箱实验,这一篇博客记录了我刷机的过程. 所需器材及环境 1.tx1板子一套(包括显示器.键鼠等) 2.一台主机(能够运行linux系统,我用的是Ubuntu 1 ...

  5. 视频教程-虚拟现实之汽车模拟仿真项目开发-Unity3D

    虚拟现实之汽车模拟仿真项目开发 二十多年的软件开发与教学经验IT技术布道者,资深软件工程师.具备深厚编程语言经验,在国内上市企业做项目经理.研发经理,熟悉企业大型软件运作管理过程.软件架构设计理论.精 ...

  6. vs2019中如何创建qt项目_在VS2015中创建Qt项目【VS+Qt项目开发系列】(二)

    在VS2015中创建Qt项目[VS+Qt项目开发系列](二) 发布时间:2018-04-20 22:44, 浏览次数:1269 , 标签: VS Qt 在上一篇[VS+Qt项目开发](一)在VS201 ...

  7. 《MFC游戏开发》笔记二 建立工程、调整窗口

    本系列文章由七十一雾央编写,转载请注明出处. http://blog.csdn.net/u011371356/article/details/9300383 作者:七十一雾央 新浪微博:http:// ...

  8. 【Qt+OpenCV项目开发学习】二、图片查看器应用程序开发

    一.前言 本博客将讲解如何用Qt+OpenCV开发一款图片查看器的Windows应用程序,其实不用OpenCV也能开发出这类软件,作者目的是为了学习Qt+OpenCV开发项目,所以会使用OpenCV, ...

  9. 100个vc小项目开发:二、一步一点设计音乐播放器 [I]

    100个vc小项目开发:二.一步一点设计音乐播放器 [源码解读] 文章作者: July 软件来源:开源 ================== 1.有不正之处,恳请指正. 2.本文贴出的是关键实现代码部 ...

最新文章

  1. java stringbuilder 替换字符串_java中的经典问题StringBuilder替换String
  2. 微软出面解释Win11各种大bug,引发网友一顿嘲讽:都是祖传手艺
  3. Serval and Toy Bricks
  4. for循环执行 mybatis_Mybatis中使用循环遍历
  5. java ognl表达式 与struts2标签_Struts2 OGNL表达式实例详解
  6. [凯立德]2015春季版C2739-M7L83-3521JON,已O+带3D+带路况
  7. Team Foundation Server
  8. Python3.5 配置MySql数据库连接
  9. ✨Shell脚本实现Base64 加密解密
  10. Linux学习第一篇之Linux系统安装——系统分区
  11. 判断数字在字符串中的位置 详解(C++)
  12. Deepracer 学了就能云驾驭赛车? Deepracer机器学习入门级干货分享!
  13. 华为HCNA认证---简介及资源
  14. 论文阅读汇总(4)-【篇数:50】
  15. 电脑键盘快捷键和组合键功能使用大全
  16. 【Kotlin学习之旅】Kotlin实现101个C#的LINQ示例,让你领略一下Kotlin代码的魅力
  17. 裸金属服务器性能描述,裸金属服务器性能描述
  18. Java中的自动向量化(SIMD)
  19. 为什么要实施微服务架构?
  20. 在centos7下安装云锁

热门文章

  1. 反种族主义者是错误的共同种族差异问题
  2. vue中手写一个放大镜功能
  3. ABP+AdminLTE+Bootstrap Table权限管理系统第十一节--bootstrap table之用户管理列表
  4. css设置1.5倍行高,css设定行高、绝对定位
  5. 无法启动此程序,因为计算机中丢失api-ms-win-crt-locale-l1-1-0.dll,尝试重新安装此程序以解决此问题
  6. qs框架快速将JSON格式转换为FormData格式
  7. 使用springmvc时报错JSPs only permit GET POST or HEAD
  8. PS如何降低选取内图像的亮度?
  9. 女人的十种养生好食物
  10. 15 个好用的 API 接口管理神器