一、概述
- 编译工具:x32,x64位编译器、CLR(公共语言运行库)
- Visual C++ 库:标准C++、活动模板库(ATL)、Microsoft基础类库(MFC)。这些库由iostream库、标准模板库(STL)、C运行时库(CRT)组成。STL/CLR 库为托管代码开发人员引入STLK
- 开发环境
每个MFC 程序都要一个继承自CWinApp(或CWinAppEx) 的应用类,比如CTestApp 。应用类的生命期从程序开始执行到执行结束。应用类必须重载CWin 的虚函数InitInstance ,程序的初始化操作通常放在这个函数里。这里我们让程序刚运行就跳出一个消息框,因此把AfxMessageBox 放在InitInstance 里。_T 的作用与TEXT 相同,都是为了让字符串同时支持Unicode 字符和多字符环境,AfxMessageBox 是一个全局函数。
二、字符集
1、C运行时库对Unicode的支持
- 字符类型
C/C++ 定义新的名为“宽字符”的数据类型,以提供对Unicode 的支持。这种数据类型为wchar_t 定义如下:
typedef unsigned short wchar_t;
该类型为无符号短整型,占用2字节,原来的char 类型仍然可以用,一个字符占用一个字节相对于wchar_t ,char 通常被称为窄字符类型
wchar_t 可以用来定义字符,也可用来定义字符数组
wchar_t arr[] = L"Style";
L 要求编译器将其后字符串按Unicode 保存,即字符串中的每个字符占用 2 字节
为了统一处理窄字类型和宽字类型,系统头文件tchar.h 中定义字符数据类型TCHAR 为char,定义大致如下:
#ifdef _UNICODE
#define wchar_t
#else
typedef
#endif
该处用到了预处理器,意思是如果定义了_UNICODE 宏,则TCHAR 定义为wchar_t ,否则定义为char
TCHAR ch = 'A'; //该字符是窄字符还是宽字符取决于前面有没有定义_UNICODE
- 字符串处理
2、C++标准库对Unicode的支持
在C++ 标准库中的string 也有对应的宽字符版本wstring ,但未提供统一的函数形式,类似的还有fstream/wfstream,ofstream/wofstream
3、Windows API 对Unicode 的支持
两个版本,以A为结束的函数形式,针对多字节字符集,另外一个以W 为结束的函数形式,针对Unicode 字符集。
4、Visual C++ 2017对Unicode 的支持
可在工程属性中选择本工程所使用的字符集
三、SDK编程基础
1、消息的定义
一个消息是系统定义的一个32位的整数值,并用宏表示。它向Windows 发出一个通知,告诉应用程序某个事情发生了。例:单击鼠标,改变窗口尺寸等等。这会使Windows 发送消息给应用程序。产生消息的来源有3个:
- 由操作系统产生
- 由用户触发事件转换而来
- 由另一个消息产生
消息分为两种:
- 系统预定义消息
- 用户自定义消息
其中用户自定义消息用一个宏来表示。在头文件winuser.h 中这些消息宏被定义为32位整数。例如:
#define WM_CREATE //窗口创建消息
#define BM_CLICK //按钮单击消息
消息宏分为两部分:
- 前缀:表示处理该消息的窗口类别
- 后缀:描述了该消息的目的
hwnd 是32位的窗口句柄(类指针),Win32 能够维护大多数可视对象的句柄(窗口、对话框、按钮、编辑框);message 是消息号用来存放消息常量值(消息宏)
2、预定义消息
1、窗口消息(Windows message)
窗口消息用于窗口的内部运作,窗口消息可用于一般窗口,也可以是对话框、控件,因为对话框和控件也是窗口
2、命令消息(Command Message)
Command Message 用于处理用户请求,如用户单击菜单项或工具栏按钮或标准控件时就会产生命令消息。命令消息的格式是WM_COMMAND ,消息参数wParam 的低字节,即LOWORD(wParam)表示菜单项或工具栏。
WM_COMMAND 除了作为命令消息外,还能作为标准控件的控件通知消息。区别是消息参数lParam 是否为NULL :如果是NULL ,则该消息是命令消息,如果lParam 是控件句柄值,则该消息是标准控件通知消息。通用控件有情况向父窗口发送消息时,并不通过WM_COMMAND,而是通过控件通知消息WM_NOTIFY 。
- 标准控件:按钮(Button)、静态文本控件(Static)、编辑框(Edit)、组合框(ComboBox)、列表框(ListBox)、滚动块(ScrollBar)
3、控件通知消息
控件有情况需要通知其父窗口,该消息有三种形式:
(1)作为窗口消息的子集
WM_XXXX 的格式
- 控件窗口在创建或销毁前,会发送WM_PARENTNOTIFY 消息给它的父窗口
- 控件窗口绘制自身窗口消息,如WM_CTLCOLOR 等等
- 由滚动条控件发送,通知其父窗口滚动的消息,如WM_VSCROLL 和 WM_HSCROLL
(2)WM_COMMAND形式
WM_COMMAND 除了作为命令消息外,还能作为标准控件的控件通知消息。区别是消息参数lParam 是否为NULL :如果是NULL ,则该消息是命令消息,如果lParam 是控件句柄值,则该消息是标准控件通知消息。通用控件有情况向父窗口发送消息时,并不通过WM_COMMAND,而是通过控件通知消息WM_NOTIFY 。
- 标准控件:按钮(Button)、静态文本控件(Static)、编辑框(Edit)、组合框(ComboBox)、列表框(ListBox)、滚动块(ScrollBar)
lParam 用来区分是命令消息还是控件通知消息,如果为NULL ,则是命令消息,否则lParam 里面放的必然是控件的句柄,是一个控件通知消息,消息参数wParam 的低字节表示控件的ID,高字节即HIWORD(wParam)表示控件通知码,通知码标记了控件所发生的各种事件,常见标准控件的通知码和含义:
(3)WM_NOTIFY形式
通用控件(如树型视图,列表视图)消息的消息参数wParam 和lParam 分别表示控件ID 和指向结构体NMHDR 的指针,NMHDR(Notify Message Handler) 包含控件通知消息函数的内容比如控件通知码。
WM_NOTIFY解决了不断需要增加新消息和消息参数所带信息容量不够的问题。目前Windows 中通用控件的通知方式都是采用该消息的形式。
3、自定义消息
开发者可以自己定义消息以达到特殊功能的目的,注意不要和预定义消息的宏冲突。系统保留的消息标识符在0x0000 到0x03ff (WM_USER - 1)范围内。开发者不能使用这些值定义自己的消息。可以从WM_USER 开始,例如:
#define MY_MSG WM_USER + 1
4、消息(Message)和事件(Event)
消息是描述某个事件发生的信息,而事件是用户操作应用程序产生的动作。事件是原因,消息是结果,事件产生消息,消息对应事件。
消息可能是由一个事件转换而来(或者由操作系统产生),一个消息可能会产生另一个消息。事件只能由用户触发。总而言之,事件只能由用户通过外设的输入产生。
5、消息和窗口
Windows 的消息机制就是“以消息为基础,以事件为驱动”,也就是说:程序不断等待消息的形式进入系统后放入相应的队列,然后调用API 函数GetMessage 取得相应的消息并做出相应的处理。窗口就是用来接受并处理消息的,每个窗口都对应一个函数来处理消息,该函数是窗口函数(Windows Procedure 窗口过程函数)
Win32应用程序(SDK)的实现主要分为以下步骤:
- WinMain 函数
- MSG 结构体
- 注册窗口
- 创建窗口
- 显示窗口
- 刷新窗口
- 消息循环
- WindowsProc 窗口函数
该函数是一个回调函数(Callback)就是系统无需用户调用,只需要窗口函数的函数名赋值给窗口类WNDCLASS 中的成员lpfnWndProc ,系统就知道了,利用Switch/Case 方式来判断消息种类,然后进入各分支针对某个具体消息进行处理。
例:可以从基本的Win32 程序来理解,创建Windows 桌面应用程序,会自动生成一个Win32 的框架
// WindowsProject1.cpp : 定义应用程序的入口点。
//
#include "framework.h"
#include "WindowsProject1.h"
#define MAX_LOADSTRING 100
// 全局变量:
HINSTANCE hInst; // 当前实例
WCHAR szTitle[MAX_LOADSTRING]; // 标题栏文本
WCHAR szWindowClass[MAX_LOADSTRING]; // 主窗口类名
// 此代码模块中包含的函数的前向声明:
ATOM MyRegisterClass(HINSTANCE hInstance);
BOOL InitInstance(HINSTANCE, int);
LRESULT CALLBACK WndProc(HWND, UINT, WPARAM, LPARAM);
INT_PTR CALLBACK About(HWND, UINT, WPARAM, LPARAM);
int APIENTRY wWinMain(_In_ HINSTANCE hInstance,
_In_opt_ HINSTANCE hPrevInstance,
_In_ LPWSTR lpCmdLine,
_In_ int nCmdShow)
{
UNREFERENCED_PARAMETER(hPrevInstance);
UNREFERENCED_PARAMETER(lpCmdLine);
// TODO: 在此处放置代码。
// 初始化全局字符串
LoadStringW(hInstance, IDS_APP_TITLE, szTitle, MAX_LOADSTRING);
LoadStringW(hInstance, IDC_WINDOWSPROJECT1, szWindowClass, MAX_LOADSTRING);
MyRegisterClass(hInstance);
// 执行应用程序初始化:
if (!InitInstance (hInstance, nCmdShow))
{
return FALSE;
}
HACCEL hAccelTable = LoadAccelerators(hInstance, MAKEINTRESOURCE(IDC_WINDOWSPROJECT1));
MSG msg;
// 主消息循环:
while (GetMessage(&msg, nullptr, 0, 0))
{
if (!TranslateAccelerator(msg.hwnd, hAccelTable, &msg))
{
TranslateMessage(&msg);
DispatchMessage(&msg);
}
}
return (int) msg.wParam;
}
//
// 函数: MyRegisterClass()
//
// 目标: 注册窗口类。
//
ATOM MyRegisterClass(HINSTANCE hInstance)
{
WNDCLASSEXW wcex;
wcex.cbSize = sizeof(WNDCLASSEX);
wcex.style = CS_HREDRAW | CS_VREDRAW;
wcex.lpfnWndProc = WndProc;
wcex.cbClsExtra = 0;
wcex.cbWndExtra = 0;
wcex.hInstance = hInstance;
wcex.hIcon = LoadIcon(hInstance, MAKEINTRESOURCE(IDI_WINDOWSPROJECT1));
wcex.hCursor = LoadCursor(nullptr, IDC_ARROW);
wcex.hbrBackground = (HBRUSH)(COLOR_WINDOW+1);
wcex.lpszMenuName = MAKEINTRESOURCEW(IDC_WINDOWSPROJECT1);
wcex.lpszClassName = szWindowClass;
wcex.hIconSm = LoadIcon(wcex.hInstance, MAKEINTRESOURCE(IDI_SMALL));
return RegisterClassExW(&wcex);
}
//
// 函数: InitInstance(HINSTANCE, int)
//
// 目标: 保存实例句柄并创建主窗口
//
// 注释:
//
// 在此函数中,我们在全局变量中保存实例句柄并
// 创建和显示主程序窗口。
//
BOOL InitInstance(HINSTANCE hInstance, int nCmdShow)
{
hInst = hInstance; // 将实例句柄存储在全局变量中
HWND hWnd = CreateWindowW(szWindowClass, szTitle, WS_OVERLAPPEDWINDOW,
CW_USEDEFAULT, 0, CW_USEDEFAULT, 0, nullptr, nullptr, hInstance, nullptr);
if (!hWnd)
{
return FALSE;
}
ShowWindow(hWnd, nCmdShow);
UpdateWindow(hWnd);
return TRUE;
}
//
// 函数: WndProc(HWND, UINT, WPARAM, LPARAM)
//
// 目标: 处理主窗口的消息。
//
// WM_COMMAND - 处理应用程序菜单
// WM_PAINT - 绘制主窗口
// WM_DESTROY - 发送退出消息并返回
//
//
LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
{
switch (message)
{
case WM_COMMAND:
{
int wmId = LOWORD(wParam);
// 分析菜单选择:
switch (wmId)
{
case IDM_ABOUT:
DialogBox(hInst, MAKEINTRESOURCE(IDD_ABOUTBOX), hWnd, About);
break;
case IDM_EXIT:
DestroyWindow(hWnd);
break;
default:
return DefWindowProc(hWnd, message, wParam, lParam);
}
}
break;
case WM_PAINT:
{
PAINTSTRUCT ps;
HDC hdc = BeginPaint(hWnd, &ps);
// TODO: 在此处添加使用 hdc 的任何绘图代码...
EndPaint(hWnd, &ps);
}
break;
case WM_DESTROY:
PostQuitMessage(0);
break;
default:
return DefWindowProc(hWnd, message, wParam, lParam);
}
return 0;
}
// “关于”框的消息处理程序。
INT_PTR CALLBACK About(HWND hDlg, UINT message, WPARAM wParam, LPARAM lParam)
{
UNREFERENCED_PARAMETER(lParam);
switch (message)
{
case WM_INITDIALOG:
return (INT_PTR)TRUE;
case WM_COMMAND:
if (LOWORD(wParam) == IDOK || LOWORD(wParam) == IDCANCEL)
{
EndDialog(hDlg, LOWORD(wParam));
return (INT_PTR)TRUE;
}
break;
}
return (INT_PTR)FALSE;
}
定义一个窗口类
在上面的程序中操作系统要求在调用API 函数CreateWindow 创建窗口之前,要求开发者必须定义一个窗口类(不是C++ 传统意义上的类)来规定所要创建窗口的各种信息,包括函数处理名、窗口风格、图标、鼠标、菜单等,通过一个结构体WNDCLASSEXW (末尾的W 表示是Unicode 下的版本,如果是多字节下的版本为WNDCLASSEXW ,关于该名称的解释在MFC 常见变量解析中有所提及)来定义窗口类,定义如下:
创建窗口
Create Window 函数声明如下:
显示窗口、刷新窗口
窗口创建完毕后就要显示窗口,使用Show Window 来显示窗口,再使用Update Window 来刷新窗口。所谓刷新窗口就是发送一个WM_PAINT 消息给窗口,在窗口函数WndProc 中会做WM_PAINT 处理。
消息循环
其中,GetMessage 从主线程消息队列中获取一个消息并将它复制到MSG 结构中。
GetMessage 函数将等待一个消息的到来以后才返回。
MSDN中介绍 MSG 定义如下:
typedef struct tagMSG {
HWND hwnd; //1、hwnd表示消息所属的窗口。我们通常开发的程序都是窗口应用程序,一个消息一般都是与某个窗口相关联的。
UINT message; //2、指定了消息的标识符
WPARAM wParam; //3、用于指定消息的附加信息
LPARAM lParam; //4、用于指定消息的附加信息
DWORD time; //5、消息投递到消息队列中的时间
POINT pt; //6、鼠标的当前位置
} MSG;
参考微软官方文档Microsoft MSG structure
窗口函数
WIN32 编程中最重要的函数,窗口(过程)函数,WndProc 函数和 About 函数和都是窗口过程函数,分别处理各自窗口的消息。
窗口函数的声明(该函数为回调函数):
LRESULT CALLBACK WndProc(HWND hwnd,UINT message,WPARAM wParam,LPARAM lParam);
//1、LRESULT 表示会返回多种long 型值
//2、HWND 窗口句柄,一个整形值
//3、UINT unsigned int
//4、typedef UINT_PTR WPARAM
//就是unsigned int,因为typedef unsigned int UINT_PTR
//5、typedef LONG_PTR LPARAM
//就是unsigned LONG,因为typedef unsigned LONG ULONG_PTR
窗口过程是一个用于处理所有发送到这个窗口的消息的函数。任何一个窗口类都有一个窗口过程。同一个类(窗口类名相同)使用相同的窗口过程来响应消息
画出字符串
- 函数GetClientRect 是获取窗口客户区(暂时认为空白区域)的坐标和大小,存入Rect 类型变量中。Rect 是一个结构体,定义如下:
typedef struct tagRECT{
LONG left;
LONG top;
LONG right;
LONG bottom;
}RECT;
- 用SetTextColor 函数设置要画的字符串颜色,SetBkColor 设置背景色,最后用DrawText 画出文本字符串,声明如下:
int DrawText(
HDC hDC, // 设备描述表句柄
LPCTSTR lpString, // 将要绘制的字符串
int nCount, // 字符串的长度
LPRECT lpRect, // 指向矩形结构RECT的指针
UINT uFormat // 正文的绘制选项
);
Win32 编程最重要的概念和要点(窗口和消息)
需要理解的要点如下:
- 消息的组成:一个消息由消息名称message(UINT)
和两个参数(WPARAM,LPARAM)组成。当用户输入或窗口状态发生改变时系统都会发送消息到某一窗口。用户可以自定义消息名称(自定义消息的宏),也可以利用自定义消息来发送通知和传送数据。 - 消息的接收者:一个消息必须由一个窗口接收。在窗口的过程(WNDPROC)中可以对消息进行分析,对自己感兴趣的消息进行处理。
- 消息的默认处理:如果用户没有对某个消息进行处理,通常窗口会对消息进行默认处理。微软为窗口编写了默认窗口处理过程。该窗口负责处理我们未处理的消息。因此我们可以不用去理睬我们未处理的消息,例如,窗口拖动会产生大量的消息。
- 窗口句柄:系统通过窗口句柄在整个系统中唯一标识一个窗口。发送消息时必须指定一个窗口句柄表明该消息由哪个窗口接收。而每个窗口有自己的窗口过程,所以用户的输入就会被正确的处理。
6、工程目录结构
- 工程、项目(project)
- 每个Project 都存在于Solution(解决方案)中
- Text.rc 是资源文件
- .vcxproj 是工程文件
- small.ico 和Test.ico 是两个图标文件
7、Win32 控件编程
控件名称 | 预定义窗口类名 | 功能 |
---|---|---|
按钮控件 | Button | 提供单击和选择 |
静态文本控件 | Static | 显示文本字符串 |
编辑框控件 | Edit | 显示和输入文本字符串 |
列表框 | Listbox | 列表显示字符串 |
组合框 | combobox | 供用户选择或编辑字符串 |
- 创建窗口
控件也是窗口,因此创建窗控件和创建窗口一样,使用CreateWindow 或CreateWindowEx函数,并在窗口样式上使用WS_CHILD 这个样式(或WS_CHILDWINDOW 效果一样),表示控件放在其他窗口上,作为其他窗口的子窗口。创建控件的地方一般放在父窗口WM_CREATE消息处理中
- WS_VISIBLE 表示创建按钮的同时显示出来,如果没有该风格,则创建成功后不会显示。
- 每个控件必须有唯一ID(其实是一个整数值)
- 控件和父窗口之间的信息交互
用户对某窗口的控件进行操作时,操作产生的事件会被控件转换为消息,然后发送给父窗口,消息的名字系统预定义为WM_COMMAND ,消息参数wParam 的低字节部分LOWORD(wParam)表示控件ID ,wParam 的高字节部分HIWORD(wParam)表示通知码,通知码表示某种操作的宏,定义在winuser.h中;lParam 表示控件句柄。WM_COMMAND 只是告诉父窗口控件产生消息了,但具体是什么操作由通知码来获得,不同的控件通知码不同。 - 控件大小、位置、使能、可见性和销毁
控件也是窗口,调整窗口大小和位置函数是MoveWindow 或SetWindowPos ,函数也可用于控件。如果要让控件不可用可以使用EnableWindow 。销毁控件可以使用DestroyWindow,但一般主窗口销毁的时候,其拥有的子窗口(包括控件)都会自动销毁,所以不用显式地去调用DestroyWindow
1)按钮控件
常用按钮通知码,表2为常见按钮通知消息
_tcscpy_s 是strcpy 的Unicode 和非Unicode 的通用版本,结尾加了_s是表示该函数的安全版本。同样的还有_stprintf_s
四、MFC编程基础
1、MFC类库概述
MFC类库的类别层次结构:
重要类之间的继承关系:
详细参考:Microsoft Docs MFC Hierarchy Chart
几个重要的类:
- 类CObject
大多数MFC类的基类,主要提供四方面的功能:串行化数据、运行时提供类的信息、对象诊断输出和收集类兼容 - 类CCmdTarget
“大内总管”,消息映射的一个基类。消息映射把命令或消息引导给用户为之编写的响应函数。 - 类CWinApp
主要封装程序初始化、运行和结束等功能。如其中含有一个InitInstance ,在这个函数中实现程序主窗口的创建。在其中可以加入自己的代码,在程序刚运行时执行某些功能。如果需要在程序结束时加入一些功能,可在ExitInstance中加入一些功能 - 类CWnd
举足轻重,几乎所有的窗口都从它派生而来。通常的三种MFC程序:单个文档程序、多个文档程序和基于对话框的程序。这3种程序的主窗口由CFrameWnd、CMDIFrameWnd、CDialog来实现,这三个类都由该类直接或间接派生 - 类CFrameWnd
框架窗口(构造应用程序或部分程序的窗口),通常包含视图窗口(通常和文档数据打交道,用来显示文档中的数据,是框架窗口的客户区,用来显示文档中的数据,是框架窗口的客户区,用来显示数据的子窗口)、工具栏、状态栏等元素的窗口。
创建框架窗口三种方法:直接通过CFrameWnd 的成员函数Create 来创建、通过成员函数LoadFrame 来创建、通过文档模板来间接构造 - 类CMDIFrameWnd
继承于CFrameWnd ,主要用来实现多文档程序(MDI)的框架窗口 - 类CDialog
用来实现程序中各种对话框的功能,包括模态对话框和非模态对话框 - CCommonDialog
各通用对话框的父类,通用对话框:颜色选取对话框(CColorDialog)、字体设置对话框(CFontDialog)、文件打开对话框(CFileDialog)、搜索替换对话框(CFindReplaceDialog)、打印对话框(CPrintDialog)等。Windows 标准对话框类(CCommonDialog)继承自CDIalog - 类CView
视图类,也是一种窗口,是框架窗口的客户区(显示数据的子窗口)。作用是为文档数据提供一个视图。视图就是显示数据、接收用户输入、编辑或选择数据的窗口。CView 派生出很多不同显示数据方式的视图子类,如让视图以列表显示的CLIstView 、让数据以树型方式显示的CTreeView 、支持用户编辑功能的视图CEditView、让数据以超文本方式显示的CHtmlView - 类CDocTemplate
文档模板类主要用来整合文档、视图和框架窗口的创建 - 类CDocument
管理程序中的数据
以上为程序大框架方面的几个类。其他的是针对某些功能的,如CSocket 是针对网络功能的实现、CMenu 是针对菜单功能的实现、CException 是针对异常功能的实现、CFile 是针对文件功能的实现等等。
2、MFC应用程序类型
- 单文档程序就是一个程序中只有一个视图窗口和一个文档对象
- 多文档程序就是一个程序中有多个视图窗口和多个文档对象
- 对话框程序没有视图窗口和文档对象等概念,通常是在对话框上放置控件,然后通过控件的操作和用户交互。
3、窗口客户区
在Win32 程序中,窗口客户区比较简单,通常指除去菜单栏、滚动条后的中间区域部分。
MFC程序的窗口客户区,文档程序主窗口的客户区和视图窗口的客户区是不同的,通常画图程序是在视图窗口的客户区上进行。
单文档程序的工具栏和状态栏是显示在客户区内的,即主窗口客户区是包括工具栏和状态栏区域的白色区域是视图窗口的客户区。
CRect 是一个MFC 类,用于表示一个矩形的大小,常见成员函数:
五、键盘
键盘的硬件原理是用户每次按下或释放某个键,键盘都会产生扫描码,以确定相应的键,并且按下的时候,扫描码最高位是0,释放的时候最高位是1,因此每个键能产生两个不同的扫描码。
物理键盘产生扫描码,不同键盘产生的扫描码不同。为屏蔽不同物理键盘差异,Windows 系统通过虚拟键盘来提供与设备无关的键盘操作。上层应用只需要和虚拟键盘打交道
虚拟键盘上的键码通过宏定义来表示,它们在winuser.h 中定义: