Windows消息机制
基本概念解释
1) SDK和API
SDK: 软件开发工具包(Software Development Kit),一般都是一些被软件工程师用于为特定的软件包、软件框架、硬件平台、操作系统等建立应用软件的开发工具的集合。
API函数: Windows操作系统提供给应用程序编程的接口(Application Programming Interface)。
Windows应用程序API函数是通过C语言实现的,所有主要的 Windows 函数都在 Windows.h 头文件中进行了声明。Windows 操作系统提供了 1000 多种 API函数。
2) 窗口和句柄
窗口是 Windows 应用程序中一个非常重要的元素,一个 Windows 应用程序至少要有一个窗口,称为主窗口。
窗口是屏幕上的一块矩形区域,是 Windows 应用程序与用户进行交互的接口。利用窗口可以接收用户的输入、以及显示输出。
一个应用程序窗口通常都包含标题栏、菜单栏、系统菜单、最小化框、最大化框、 可调边框,有的还有滚动条。如下图:
窗口可以分为客户区和非客户区, 如上图。 客户区是窗口的一部分, 应用程序通常在客户区中显示文字或者绘制图形。
标题栏、 菜单栏、 系统菜单、 最小化框和最大化框、 可调边框统称为窗口的非客户区, 它们由 Windows 系统来管理, 而应用程序则主要管理客户区的外观及操作。
窗口可以有一个父窗口, 有父窗口的窗口称为子窗口。除了上图所示类型的窗口外, 对话框和消息框也是一种窗口。 在对话框上通常还包含许多子窗口, 这些子窗口的形式有按钮、 单选按钮、 复选框、 组框、 文本编辑框等。
在 Windows 应用程序中, 窗口是通过窗口句柄( HWND) 来标识的。 我们要对某个窗口进行操作, 首先就要得到这个窗口的句柄。
句柄( HANDLE) 是 Windows 程序中一个重要的概念, 使用也非常频繁。 在 Windows 程序中, 有各种各样的资源( 窗口、 图标、光标,画刷等), 系统在创建这些资源时会为它们分配内存, 并返回标识这些资源的标识号, 即句柄。
在后面的内容中我们还会看到图标句柄( HICON)、 光标句柄( HCURSOR) 和画刷句柄( HBRUSH)。
3) 消息与消息队列
Windows 程序设计是一种完全不同于传统的 DOS 方式的程序设计方法。它是一种事件驱动方式的程序设计模式,主要是基于消息的。
每一个 Windows 应用程序开始执行后, 系统都会为该程序创建一个消息队列, 这个消息队列用来存放该程序创建的窗口的消息。
例如,当用户在窗口中画图的时候,按下鼠标左键,此时,操作系统会感知到这一事件,于是将这个事件包装成一个消息,投递到应用程序的消息队列中,等待应用程序的处理。
然后应用程序通过一个消息循环不断地从消息队列中取出消息,并进行响应。
在这个处理过程中,操作系统也会给应用程序“ 发送消息”。所谓“ 发送消息”,实际上是操作系统调用程序中一个专门负责处理消息的函数,这个函数称为窗口过程。
鼠标操作窗口时,操作系统先捕获到消息,把消息放到消息队列中,应用程序捕获消息GetMessage(),然后DispatchMessage()分发消息给操作系统,操作系统调用回调函数----窗口过程
4) WinMain函数
当Windows操作系统启动一个程序时,它调用的就是该程序的WinMain函数( 实际是由插入到可执行文件中的启动代码调用的)。
WinMain是Windows程序的入口点函数,与DOS程序的入口点函数main的作用相同,当WinMain 函数结束或返回时,Windows应用程序结束。
Windows 编程模型
一个完整的Win32程序(#include <windows.h>),该程序实现的功能是创建一个窗口,并在该窗口中响应键盘及鼠标消息,程序的实现步骤为:
1) WinMain函数的定义
2) 创建一个窗口
3) 进行消息循环
4) 编写窗口过程函数
1) WinMain函数的定义
int WINAPI WinMain(
HINSTANCE hInstance, //应用程序实例
HINSTANCE hPrevInstance, //上一个应用程序实例
LPSTR lpCmdLine, //命令行参数
int nShowCmd); //窗口显示的样式
);
WINAPI:是一个宏,它代表的是__stdcall(注意是两个下划线)
表示的是参数传递的顺序:从右往左依次入栈,同时在函数返回前自动清空堆栈
hInstance:表示该程序当前运行的实例的句柄,这是一个数值。当程序在Windows下运行时,它唯一标识运行中的实例(注意,只有运行中的程序实例, 才有实例句柄)。一个应用程序可以运行多个实例,每运行一个实例,系统都会给该实例分配一个句柄值,并通过hInstance参数传递给 WinMain 函数。
hPrevInstance:表示当前实例的前一个实例的句柄。在Win32环境下,这个参数总是NULL,即在Win32环境下,这个参数不再起作用。
lpCmdLine:是一个以空终止的字符串, 指定传递给应用程序的命令行参数,相当于C或C++中的main函数中的参数char *argv[]。
nShowCmd:表示一个窗口的显示,表示它是要最大化显示、最小化显示、正常大小显示还是隐藏显示。
2) 创建一个窗口
创建一个完整的窗口,需要经过下面几个步骤:
1. 设计一个窗口类
一个完整的窗口具有许多特征, 包括光标(鼠标进入该窗口时的形状)、图标、背景色等。窗口的创建过程类似于汽车的制造过程。
我们在生产一个型号的汽车之前, 首先要对该型号的汽车进行设计, 在图纸上画出汽车的结构图, 设计各个零部件, 同时还要给该型号的汽车取一个响亮的名字, 例如“宝马 x6”。
类似地, 在创建一个窗口前, 也必须对该类型的窗口进行设计, 指定窗口的特征。
在Windows中,窗口的特征就是由WNDCLASS结构体来定义的,我们只需给WNDCLASS结构体对应的成员赋值,即可完成窗口类的设计。
WNDCLASS结构体的定义如下:
typedef struct _WNDCLASS{ UINT style; WNDPROC lpfnWndProc; int cbClsExtra; int cbWndExtra; HINSTANCE hInstance; HICON hIcon; HCURSOR hCursor; HBRUSH hbrBackground; LPCWSTR lpszMenuName; LPCWSTR lpszClassName; } WNDCLASS;
style:指定窗口的样式(风格),常用的样式如下:
类型 |
含义 |
CS_HREDRAW |
当窗口水平方向上的宽度发生变化时, 将重新绘制整个窗口。 当窗口发生重绘时, 窗口中的文字和图形将被擦除。如果没有指定这一样式,那么在水平方向上调整窗口宽度时,将不会重绘窗口。 |
CS_VREDRAW |
当窗口垂直方向上的高度发生变化时,将重新绘制整个窗口。如果没有指定这一样式,那么在垂直方向上调整窗口高度时,将不会重绘窗口。 |
CS_NOCLOSE |
禁用系统菜单的 Close 命令,这将导致窗口没有关闭按钮。 |
CS_DBLCLKS |
当用户在窗口中双击鼠标时,向窗口过程发送鼠标双击消息。 |
lpfnWndProc:指定一个窗口回调函数,是一个函数的指针。
当应用程序收到给某一窗口的消息时,就应该调用某一函数来处理这条消息。这一调用过程不用应用程序自己来实施,而由操作系统来完成,但是,回调函数本身的代码必须由应用程序自己完成。
对于一条消息,操作系统调用的是接受消息的窗口所属的类型中的lpfnWndProc成员指定的函数。每一种不同类型的窗口都有自己专用的回调函数,该函数就是通过lpfnWndProc成员指定的。
回调函数的定义形式如下:
LRESULT CALLBACK WindowProc( HWND hWnd, //信息所属的窗口句柄 UINT uMsg, //消息类型 WPARAM wParam, //附加信息(如键盘哪个键按下) LPARAM lParam //附加信息(如鼠标点击坐标) );
cbClsExtra:类的附加内存,通常数情况下为0。
cbWndExtra:窗口附加内存,通常情况下为0。
hInstance:当前实例句柄,用WinMain中的形参hInstance为其赋值。
hIcon:指定窗口类的图标句柄,设置为NULL,则使用默认图标,也可用如下函数进行赋值:
HICON LoadIcon(HINSTANCE hInstance, LPCTSTR lpIconName); 如:LoadIcon(NULL, IDI_WARNING); //第一个参数为NULL,加载系统默认图标
hCursor:指定窗口类的光标句柄,设置为NULL,则使用默认图标,也可用如下函数进行赋值:
HCURSOR LoadCursor(HINSTANCE hInstance, LPCTSTR lpCursorName); 如:LoadCursor(NULL, IDC_HELP); //第一个参数为NULL,加载系统默认光标
hbrBackground:指示窗口的背景颜色,可用如下函数进行赋值:
HGDIOBJ GetStockObject(int fnObject); 如:GetStockObject(WHITE_BRUSH);
lpszMenuName:指定菜单资源的名字。如果设置为NULL,那么基于这个窗口类创建的窗口将没有默认菜单。
lpszClassName:指定窗口类的名字。
2. 注册窗口类
设计完窗口类(WNDCLASS)后, 需要调用RegisterClass函数对其进行注册,注册成功后,才可以创建该类型的窗口。
注册函数的原型声明如下:
ATOM RegisterClass(CONST WNDCLASS *lpWndClass); 使用示例:RegisterClass(&wc);
3. 创建窗口
设计好窗口类并且将其成功注册之后, 即可用CreateWindow函数产生这种类型的窗口了。
CreateWindow函数的原型声明如下:
HWND CreateWindow( LPCTSTR lpClassName, LPCTSTR lpWindowName, DWORD dwStyle, int x, int y, int nWidth, int nHeight, HWND hWndParent, HMENU hMenu, HINSTANCE hInstance, LPVOID lpParam );
参数说明:
lpClassName:指定窗口类的名称,此名字必须和WNDCLASS的lpszClassName成员指定的名称一样。
lpWindowName:指定窗口的名字,即窗口的标题。
dwStyle:指定创建的窗口的样式,常指定为指WS_OVERLAPPEDWINDOW类型,这是一种多种窗口类型的组合类型。
x, y:指定窗口左上角的x,y坐标。如果参数x被设为CW_USEDEFAULT,那么系统为窗口选择默认的左上角坐标并忽略y参数。
nWidth,nHeight:指定窗口窗口的宽度,高度。如果参数nWidth被设为 CW_USEDEFAULT,那么系统为窗口选择默认的宽度和高度,参数nHeight被忽略。
hWndParent:指定被创建窗口的父窗口句柄,没有父窗口,则设置NULL。
hMenu:指定窗口菜单的句柄,没有,则设置为NULL。
hInstance:窗口所属的应用程序实例的句柄,用WinMain中的形参hInstance为其赋值。
lpParam:作为WM_CREATE消息的附加参数lParam传入的数据指针。通常设置为NULL。
返回值说明:如果窗口创建成功,CreateWindow函数将返回系统为该窗口分配的句柄,否则,返回NULL。
示例代码:
HWND hWnd = CreateWindow( TEXT("MyWin"), //窗口类名字 TEXT("测试"), //窗口标题 WS_OVERLAPPEDWINDOW, //窗口风格 CW_USEDEFAULT, CW_USEDEFAULT, //窗口x,y坐标,使用默认值 CW_USEDEFAULT, CW_USEDEFAULT, //窗口宽度,高度,使用默认值 NULL, //无父窗口 NULL, //无菜单 hInstance, //应用程序实例句柄,为WinMain第1个形参 NULL); //附件信息,通常设置为NULL
4. 显示及更新窗口
显示窗口函数原型:
BOOL ShowWindow(HWND hWnd, int nCmdShow);
更新窗口函数原型:
BOOL UpdateWindow(HWND hWnd);
示例代码:
ShowWindow(hWnd, SW_SHOWNORMAL); //SW_SHOWNORMAL为普通模式 UpdateWindow(hWnd);
5. 通过循环取消息
6. 处理消息(窗口过程)
在完成上述步骤后,剩下的工作就是编写一个窗口过程函数,用于处理发送给窗口的消息。
窗口过程函数的名字可以随便取, 如WinProc, 但函数定义的形式必须和下面声明的形式相同:
//CALLBACK __stdcall 参数的传递顺序,从右到左,依次入栈,并且在函数返回前清空堆栈 LRESULT CALLBACK WindowProc( HWND hwnd,//消息所属的窗口句柄 UINT uMsg,//具体的消息名称 WM__XXX消息名 WPARAM wParam,//键盘的附加消息 LPARAM lParam//鼠标的附加消息 )
DefWindowProc函数:DefWindowProc函数调用默认的窗口过程,对应用程序没有处理的其他消息提供默认处理。
WM_CLOSE:对WM_CLOSE消息的响应并不是必须的,如果应用程序没有对该消息进行响应,系统将把这条消息传给DefWindowProc函数而 DefWindowProc函数则调用DestroyWindow函数来响应这条WM_CLOSE消息。
WM_DESTROY:DestroyWindow函数在销毁窗口后,会给窗口过程发送 WM_DESTROY消息,我们在该消息的响应代码中调用PostQuitMessage函数。
PostQuitMessage函数向应用程序的消息队列中投递一条WM_QUIT消息并返回。
WinMain函数中,GetMessage 函数只有在收到WM_QUIT消息时才返回0,此时消息循环才结束,程序退出。传递给 PostQuitMessage函数的参数值将作为WM_QUIT消息的wParam参数,这个值通常用做WinMain函数的返回值。
//处理窗口过程 //CALLBACK __stdcall 参数的传递顺序,从右到左,依次入栈,并且在函数返回前清空堆栈 LRESULT CALLBACK WindowProc( HWND hwnd,//消息所属的窗口句柄 UINT uMsg,//具体的消息名称 WM__XXX消息名 WPARAM wParam,//键盘的附加消息 LPARAM lParam//鼠标的附加消息 ) { switch(uMsg) { case WM_CLOSE:
//所有xxxWindow为结尾的方法,都不会进入消息队列中,而是直接执行 DestroyWindow(hwnd);//DestroyWindow发送另一个消息 WM_DESTROY break; case WM_DESTROY: PostQuitMessage(0); break; case WM_LBUTTONDOWN://鼠标左键按下 { int xPos=LOWORD(lParam); int yPos=HIWORD(lParam); char buf[1024]; wsprintf((LPWSTR)buf,TEXT("x=%d,y=%d"),xPos,yPos); MessageBox(hwnd,buf,TEXT("点击"),MB_OK); break; } case WM_KEYDOWN://键盘 MessageBox(hwnd,TEXT("键盘按下"),TEXT("键盘按下"),MB_OK); break; case WM_PAINT://绘图 { PAINTSTRUCT ps;//绘图结构体 HDC hdc=BeginPaint(hwnd,&ps); TextOut(hdc,100,100,TEXT("hello world!"),strlen("hello world!")); EndPaint(hwnd,&ps); } break; } //返回值用默认处理方式 return DefWindowProc(hwnd,uMsg,wParam,lParam); }
#include<Windows.h>//底层实现窗口的头文件 int WINAPI WinMain( HINSTANCE hInstance,//应用程序实例句柄 HINSTANCE hPrevInstance,//上一个应用程序句柄,在win32环境下,参数一般为NULL,不起作用了 LPSTR lpCmdLine, int nShowCmd ) { //1. 设计窗口 WNDCLASS wc; HWND hwnd; MSG msg; wc.cbClsExtra=0;//类的额外的内存 wc.cbWndExtra=0;//窗口的额外内存 wc.hbrBackground=(HBRUSH)GetStockObject(BLACK_BRUSH);//设置背景 wc.hCursor=LoadCursor(NULL,IDC_HAND);//设置光标,如果第一个参数为NULL,代表使用系统提供的光标 wc.hIcon=LoadIcon(NULL,IDI_INFORMATION);//图标 wc.hInstance=hInstance;//应用程序的实际句柄。传入WinMain中的形参即可 //wc.lpfnWndProc=WindowProc;//回调函数,窗口过程 wc.lpszClassName=TEXT("WIN");//指定窗口类名称 wc.lpszMenuName=NULL;//菜单名称 wc.style=0;//显示风格,0代表默认风格 //2. 注册窗口类 RegisterClass(&wc); //3. 创建窗口 /* lpClassName:类名 lpWindowName:标题名 dwStyle:风格 x:显示坐标 y:显示坐标CW_USEDEFAULT默认值 nWidth:宽高 nHeight hWndParent:父窗口 hMenu:菜单NULL hInstance:实例句柄 lpParam :附加值,鼠标附加值 */ hwnd = CreateWindow(wc.lpszClassName, TEXT("WINDOWS"), WS_OVERLAPPEDWINDOW, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, NULL, NULL, hInstance, NULL); //4.显示和更新 ShowWindow(hwnd,SW_SHOWNORMAL); UpdateWindow(hwnd); //5. 通过循环取消息 /* HWND hwnd;主窗口句柄 UINT message;具体的消息名称 WPARAM wParam;附加消息,键盘消息 LPARAM lParam;附加消息,鼠标消息 DWORD time;消息产生的时间 POINT pt;附加消息,鼠标消息 x y */ while(1) { /* __out LPMSG lpMsg,消息 __in_opt HWND hWnd,捕获窗口,填NULL代表捕获所有的窗口 __in UINT wMsgFilterMin,最小和最大的过滤消息,一般填0 __in UINT wMsgFilterMax 填0代表捕获所有消息 */ if(GetMessage(&msg,NULL,0,0)==FALSE) { break; } //翻译消息 TranslateMessage(&msg); //不为false //分发消息 DispatchMessage(&msg); } return 0; }
注意:生成解决方案的时候出现“error C2275: “xxxxx”: 将此类型用作表达式非法”
错误是由于:c的编译器要求将变量的声明放在一个函数块的头部,而c++没有这样的要求造成的;解决的办法就是把变量的声明全部放在变量的生存块的开始。
解决方法:将所有变量定义在代码段的开头
MFC入门
MFC是什么?
微软基础类库(英语:Microsoft Foundation Classes,简称MFC)是一个微软公司提供的类库(class libraries),以C++类的形式封装了Windows API,并且包含一个应用程序框架,以减少应用程序开发人员的工作量。其中包含的类包含大量Windows句柄封装类和很多Windows的内建控件和组件的封装类。
MFC把Windows SDK API函数包装成了几百个类,MFC给Windows操作系统提供了面向对象的接口,支持可重用性、自包含性以及其他OPP原则。MFC通过编写类来封装窗口、对话框以及其他对象,引入某些关键的虚函数(覆盖这些虚函数可以改变派生类的功能)来完成,并且MFC设计者使类库带来的总开销降到了最低。
编写第一个MFC应用程序
1) 代码的编写
项目的创建和之前一样,只是此次的源文件后缀为.cpp,因为MFC是由C++编写的,编写MFC程序需要包含#include <afxwin.h>头文件
2) 程序执行流程
① 程序开始时,先实例化应用程序对象(有且只有一个)
② 执行程序的入口函数InitInstance()
③ 给框架类MyFrame对象动态分配空间(自动调用它的构造函数),在其构造函数内部,通过CWnd::Create创建窗口
④ 框架类对象显示窗口CWnd::ShowWindow
⑤ 框架类对象更新窗口CWnd::UpdateWindow
⑥ 保存框架类对象指针CWinThread::m_pMainWnd
自定义类 继承与CWinApp应用程序类 MyApp app 应用程序对象,有且仅有一个
#include "mfc.h" MyApp app;//全局应用程序对象,有且仅有一个 BOOL MyApp::InitInstance() { //创建窗口 MyFrame * frame=new MyFrame; //显示和更新 frame->ShowWindow(SW_SHOWNORMAL); frame->UpdateWindow(); m_pMainWnd=frame;//保存指向应用程序的主窗口的指针 return TRUE;//返回正常初始化 } MyFrame::MyFrame() { Create(NULL,TEXT("mfc")); }
#include <afxwin.h> //mfc头文件 class MyApp:public CWinApp///应用程序类 { public: //程序入口 virtual BOOL InitInstance(); }; class MyFrame:public CFrameWnd//窗口框架类 { public:MyFrame(); };
消息映射
消息映射是一个将消息和成员函数相互关联的表
比如,框架窗口接收到一个鼠标左击消息,MFC将搜索该窗口的消息映射,如果存在一个处理WM_LBUTTONDOWN消息的处理程序,然后就调用OnLButtonDown
下面是是将消息映射添加到一个类中所做的全部工作:
1) 所操作类中,声明消息映射宏。
2) 通过放置标识消息的宏来执行消息映射,相应的类将在对BEGIN_MESSAGE_MAP和END_MESSAGE_MAP的调用之间处理消息。
3) 对应消息处理函数分别在类中声明,类外定义:
声明宏 写到h中
分界宏 写到.cpp中
找到消息宏,写到分界宏之间
把函数原型声明写到.h 中
函数的实现写到.cpp中
鼠标,键盘,绘图
#include "mfc.h" MyApp app;//全局应用程序对象,有且仅有一个 BOOL MyApp::InitInstance() { //创建窗口 MyFrame * frame=new MyFrame; //显示和更新 frame->ShowWindow(SW_SHOWNORMAL); frame->UpdateWindow(); m_pMainWnd=frame;//保存指向应用程序的主窗口的指针 return TRUE;//返回正常初始化 } //分界宏 BEGIN_MESSAGE_MAP(MyFrame,CFrameWnd) ON_WM_LBUTTONDOWN( )//鼠标左键按下 ON_WM_CHAR( )//键盘 END_MESSAGE_MAP() MyFrame::MyFrame() { Create(NULL,TEXT("mfc")); } void MyFrame::OnLButtonDown( UINT nFlags, CPoint point ) { /*TCHAR buf[1024]; wsprintf(buf,TEXT("x=%d,y=%d"),point.x,point.y); MessageBox(buf);*/ //mfc中的字符串 CString CString str; str.Format(TEXT("x=%d,,,,y=%d"),point.x,point.y); MessageBox(str); } void MyFrame::OnChar( UINT nChar, UINT nRepCnt, UINT nFlags ) { CString str; str.Format(TEXT("按下了%c 键"),nChar); MessageBox(str); }
#include <afxwin.h> //mfc头文件 class MyApp:public CWinApp///应用程序类 { public: //程序入口 virtual BOOL InitInstance(); }; class MyFrame:public CFrameWnd//窗口框架类 { public:MyFrame(); //声明宏 提供消息映射机制 DECLARE_MESSAGE_MAP() afx_msg void OnLButtonDown( UINT nFlags, CPoint point ); afx_msg void OnChar( UINT nChar, UINT nRepCnt, UINT nFlags ); };
Windows字符集
1. 多字节 字符串 转宽字节 L
2. 声明 宽字节字符串 wchar_t
3. 统计宽字节字符串 wcslen
4. TEXT做了自适应编码的转换
5. char * CString 之间转换
char*->CString
char * p3='ccc';
CString str=Cstring(p3);
CString->char *
CString tmp;
tmp=str;
char *pp=tmp.GetBuffer();
用向导生成一个MFC应用程序
MFC框架中一些重要的函数
1) InitInstance函数
应用程序类的一个虚函数,MFC应用程序的入口。
2) PreCreateWindow函数
当框架调用CreateEx函数创建窗口时,会首先调用PreCreateWindow函数。
通过修改传递给PreCreateWindow的结构体类型参数CREATESTRUCT,应用程序可以更改用于创建窗口的属性。
在产生窗口之前让程序员有机会修改窗口的外观。
最后再调用CreateWindowEx函数完成窗口的创建。
3) OnCreate函数
OnCreate是一个消息响应函数,是响应WM_CREATE消息的一个函数,而WM_CREATE消息是由Create函数调用的。一个窗口创建(Create)之后,会向操作系统发送WM_CREATE消息,OnCreate()函数主要是用来响应此消息的。
onCreate与Create的区别:
1. Create()负责注册并产生窗口,像动态创建控件中的Create()一样,窗口创建之后会向操作系统发送WM_CREATE消息。
2. OnCreate()不产生窗口,只是在窗口显示前设置窗口的属性如风格、位置等。
3. OnCreate()是消息WM_CREATE的消息响应函数。
4) OnDraw和OnPaint
OnPaint是WM_PAINT消息的消息处理函数,在OnPaint中调用OnDraw,一般来说,用户自己的绘图代码应放在OnDraw中。
1. OnPaint()是CWnd的类成员,负责响应WM_PAINT消息。
2. OnDraw()是CView的成员函数,没有响应消息的功能。
当视图变得无效时(包括大小的改变,移动,被遮盖等等),Windows发送WM_PAINT消息。该视图的OnPaint 处理函数通过创建CPaintDC类的DC对象来响应该消息并调用视图的OnDraw成员函数。OnPaint最后也要调用OnDraw,因此一般在OnDraw函数中进行绘制。
通常我们不必编写OnPaint处理函数。当在View类里添加了消息处理OnPaint()时,OnPaint()就会覆盖掉OnDraw()。
如果有了OnDraw,不要再有OnPaint了,同时存在的话,OnPaint会把OnDraw覆盖掉
转自:https://www.cnblogs.com/yangyuqing/p/10283641.html