本节是个大难关,一定要多看,多敲代码理解
所有代码文件均已放到蓝奏云网盘,下载解压即可运行!
WIN32API示例 (DEVC++) 密码:dgaf

Win32api是一个很老的Native API,早在Windows95年代(1995) 就有了,微软对它整个库的更新是到2002年 (也就是Windows XP 之后该库基本上不再改动了 ,但是这还不够,微软在XP之后要对WinAPI进行扩充,于是就有了后来的 d2d1.h,d3d12.h,mfplay.h 等等)
Windows.h 囊括了WinAPI几乎所有的函数与宏定义,然而实际上源码只有200来行 ,为什么?

函数都是在这些头文件里声明的!
各位C语言党不要担心,Win32API 是用纯C写的,没有C++的晦涩语法 (毕竟C++党都去MFC与QT了),虽然是老工具,可是放眼现在照样有人用
本系列教程我们不用VC (VC的确方便啊,该链的库直接唰唰唰帮你弄好,不用怕什么链接错误又要修改项目属性之类的) ,使用小熊猫 DevC++ 来完成Windows编程之旅!
0. 关于在DevC++上的一些准备
新建项目
打开小熊猫 DevC++,文件->新建->新建项目->选择空项目


之后如果要打开项目,只需 点击.dev文件 即可

修改项目属性
打开项目管理,修改项目属性:将类型改成 Win32图形界面程序(GUI)


项目的重要性
DevC++是支持单文件编译的 ,有人说,我们可以只用一个.c文件来做啊,为什么要拐弯抹角建一个项目呢?
其实,学到后面你就会发现,如果使用单个.c文件,管理资源很不方便 (例如 头文件,资源文件,regex文件等等) ,要处理这些麻烦问题最后还不是要弄项目?
所以,我想对学C的萌新说一句,早学项目早受益,之后我们还会涉及大量的项目管理
教程
1. 初识WinMain函数
我们需要抛弃陪伴我们萌新很久的main 函数,改成WinMain函数
cpp
int WINAPI WinMain(HINSTANCE hinstance, HINSTANCE hPrevInstance, LPSTR lpCMDline, int nCMDshow)
{
}
你会发现我们需要写这么大一串!用main函数不就更好?
事实上,我们只需要用第一个参数 hinstance , 然后把它改成:
cpp
int WINAPI WinMain(HINSTANCE hin,HINSTANCE,LPSTR,int)
{
}
这样就简洁多了,关于WinMain的参数,我们只需要理解第一个参数 hinstance
hinstance :应用程序当前实例的句柄。

我们知道,一个程序光有一个窗口不行 ,我们不仅要有若干个子窗口 来丰富我们的程序,更需要很多图片音乐 放进程序里锦上添花,可是如何才能将这些东西统一成一个整体呢? 实例句柄(hinstance) 因此就出来了
另外,这个hinstance是系统给我们 的,不用我们瞎操心,照着用就行
就好像医院妇产科,系统都会给每一个新生儿一个hinstance,不然没hinstance很容易把别人家孩子搞混。hinstance的存在也正是如此,它可以唯一标识应用程序
2. 让系统知道我们要弄一个窗口------注册窗口类
首先,我们需要让系统知道我们要弄窗口,在WinMain函数写上
cpp
// WNDCLASS 窗口类信息,是个结构体
WNDCLASS wc = {};
wc.hInstance = hin; //实例句柄
wc.lpszClassName = "MY_Window"; //窗口类名
RegisterClass(&wc); //注册窗口
// RegisterClass() 注册窗口类
// 接受一个 WNDCLASS 的指针
// 必须通过这一步, 窗口才能注册成功
你会发现,我们要向系统传一个叫 "WNDCLASS" 窗口类 的东西,这个东西是什么呢?
我们在上面讲到,Hinstance实例句柄作为指向一个应用程序实例的东西,它不仅包括了窗口 ,还包括该应用程序的图片音乐等等很多东西 ,包这么多东西,如果我在外面,只是找里面的窗口岂不是很麻烦?
窗口类(WNDClass) 于是就出来了,你可以理解为 一个程序里面所有窗口的集合
窗口类毕竟是一个集合啊!那我们怎么表示单个窗口?
窗口句柄(HWND) 出来了,实际上是一个窗口在系统的唯一ID
下面是三者的关系:

之后我们填完WNDCLASS结构体,就可以通过 RegisterClass函数 告诉系统我们要弄窗口了。
3. 创建窗口------CreateWindow函数
我们注册窗口类之后,就可以 真正弄个窗口 了
cpp
// CreateWindow 创建窗口,返回一个窗口句柄
HWND hwnd = CreateWindow(
"M_CLASS", //窗口类名,要和上文的 lpszClassName 相同,否则返回NULL
"我的第一个程序", //窗口标题
WS_OVERLAPPEDWINDOW|WS_VISIBLE, //窗口样式
0, //窗口左上角横坐标
0, //窗口左上角纵坐标
640, //窗口宽度
480, //窗口高度
NULL, //父窗口 (HWND)
NULL, //菜单句柄 (HMENU)
hin, //实例句柄 (HINSTANCE) ,不填会返回NULL
NULL //附加参数
);
可能这是C语言萌新见过的最长的函数,开场11个参数,而且都要填!
但是,我们最常用的也还是那几个参数罢了!
这里说明一下第3个参数窗口样式 ,第三个参数指定了我们窗口是咋样的
WS_OVERLAPPEDWINDOW 这个包括了一个常见窗口所需的所有样式 ,直接记就行了!
WS_VISIBLE 这个指定我们的窗口是否可见
更多样式可以参考这位大神的文章:
tanyufeng_521:Windows窗口风格详细解释
创建完之后,我们的程序就差一步即可大功告成,也是基础中最难的一步:消息循环!
4. 萌新噩梦:消息循环与回调函数
这里先放图

创建消息循环
WinAPI的窗口难道只是创建就行了吗?大错特错! 你会发现,窗口会一闪而过!程序也完成任务回家了。 如何才能让窗口一直显示? 这个时候,消息循环 的重要作用就很好的彰显出来了!
cpp
//消息循环的构建
MSG msg = {}; //消息结构体
while(GetMessage(&msg,NULL,0,0)>0) // 如果没有发生错误,且收到了任意消息...
{
::TranslateMessage(&msg); // 翻译消息,将消息中的键盘码转换为对应的字符
::DispatchMessage(&msg); // 派发消息,调用 CallBackFunc 回调函数处理消息
}
借助这个循环,程序就可以一直发出消息,阻止程序自身退出
只要用户不按窗口右上角的退出按钮!程序就会一直运行!
问题是,当你把这个循环打上去之后,你就会发现,窗口连影都没有
那么,我们可以使用 DefWindowProc() 返回一个窗口
但是,如何用呢?我们发现,把 DefWindowProc() 放进while循环里竟然不起作用!
原来,是我们没有处理 DefWindowProc 返回的窗口! 那么我们应该如何处理呢?
你会发现,我们少讲了什么...
绑定回调函数
没错,就是 回调函数 (CallBackFunction)
我们可以使用回调函数,对 DefWindowProc 返回的窗口进行处理
第一次看 回调 (CallBack) 这个概念的时候萌新肯定会不理解,其实在生活中回调是触手可及的!
就比如说,你现在肚子很饿,直接下馆子,当你点完餐以后,你就会不停的询问服务员什么时候可以上菜,我快饿死了!!!
这时候,服务员肯定会不停地告诉你上菜时间!
不停的操作与获得更新结果 ,就是回调的概念
有了回调函数,我们就可以保证窗口一直显示!
cpp
//回调函数, 返回一个窗口
LRESULT CALLBACK CallBackFunc(HWND hwnd,UINT msg,WPARAM wParam,LPARAM lParam)
{
// hwnd 从主函数获得的窗口句柄
// msg 从主函数的消息循环获得的消息
// wParam 附加参数1
// lParam 附加参数2
// C语言Review:
// switch用于对一个整数进行判断,并跳到相应的case语句块
// 会一直执行直至break或switch语句块的末尾
// 所以写完一个case 随手加break是一个必须养成的习惯
// 不然会执行意想不到的结果
switch(msg) //对msg进行判断,进switch语句块
{
case WM_DESTROY: //如果要退出
PostQuitMessage(0); //传递退出消息,终止主函数的消息循环
break;
// 默认行为(什么都不做,就返回窗口)
// 这个地方,很多萌新都容易漏,导致窗口不显示又退不出循环,注意一下
default: return DefWindowProc(hwnd,msg,wParam,lParam);
}
}
代码中我们使用了switch语句,对回调函数的msg消息进行处理
然后再根据消息进行操作或返回窗口
高一的时候我开始学做窗口,就是被这消息循环难住了,导致当时还要死记硬背 (当时没理解,只好死记硬背,现在看来完全没必要,果真编程还是注重思维培养的)
消息循环还是得多看,如果你能理解上面那张流程图,那么这样的框架你也能唾手可得
经历了那么多曲折,现在我们终于可以做一个窗口了。

完整代码示例
cpp
// main.cpp
#include<windows.h>
//回调函数
LRESULT CALLBACK CallBackFunc(HWND hwnd,UINT msg,WPARAM wParam,LPARAM lParam)
{
// hwnd 从主函数获得的窗口句柄
// msg 从主函数的消息循环获得的消息
// wParam 附加参数1
// lParam 附加参数2
// C语言Review:
// switch用于对一个整数进行判断,并跳到相应的case语句块
// 会一直执行直至break或switch语句块的末尾
// 所以写完一个case 随手加break是一个必须养成的习惯
// 不然会执行意想不到的结果
switch(msg) //对msg进行判断,进switch语句块
{
case WM_DESTROY: //如果要退出
PostQuitMessage(0); //传递退出消息,终止主函数的消息循环
break;
// 默认行为(什么都不做,就返回窗口)
// 这个地方,很多萌新都容易漏,导致窗口不显示又退不出循环,注意一下
default: return DefWindowProc(hwnd,msg,wParam,lParam);
}
return 0;
}
int WINAPI WinMain(HINSTANCE hin,HINSTANCE,LPSTR,int)
{
// WNDCLASS 窗口类信息,是个结构体
// 下面这三个参数是必填的
WNDCLASS wc = {};
wc.hInstance = hin; //实例句柄
wc.lpfnWndProc = CallBackFunc; //要绑定的回调函数,函数名就是函数的地址
wc.lpszClassName = "MY_Window"; //窗口类名
RegisterClass(&wc); //注册窗口
// RegisterClass() 注册窗口类
// 接受一个 WNDCLASS 的指针
// 必须通过这一步, 窗口才能注册成功
// CreateWindow 创建窗口,返回一个窗口句柄
HWND hwnd = CreateWindow(
"MY_Window", //窗口类名,要和上文的 lpszClassName 相同,否则返回NULL
"我的第一个程序", //窗口标题
WS_OVERLAPPEDWINDOW|WS_VISIBLE, //窗口样式
0, //窗口左上角横坐标
0, //窗口左上角纵坐标
640, //窗口宽度
480, //窗口高度
NULL, //父窗口 (HWND)
NULL, //菜单句柄 (HMENU)
hin, //实例句柄 (HINSTANCE) ,不填会返回NULL
NULL //附加参数
);
//消息循环的构建
MSG msg = {}; //消息结构体
while(GetMessage(&msg,NULL,0,0)>0) // 如果没有发生错误,且收到了任意消息...
{
::TranslateMessage(&msg); // 翻译消息,将消息中的键盘码转换为对应的字符
::DispatchMessage(&msg); // 派发消息,调用 CallBackFunc 回调函数处理消息
}
return 0;
}

我是DGAF,我们下个教程见