之前对这个技术只是浅尝辄止,这次花了一周时间深入研究,收获很多,整理成文分享出来。

所有代码文件均已放到蓝奏云网盘,下载解压即可运行!
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函数

int WINAPI WinMain(HINSTANCE hinstance, HINSTANCE hPrevInstance, LPSTR lpCMDline, int nCMDshow)
{
}

你会发现我们需要写这么大一串!用main函数不就更好?
事实上,我们只需要用第一个参数 hinstance , 然后把它改成:

int WINAPI WinMain(HINSTANCE hin,HINSTANCE,LPSTR,int)
{
}

这样就简洁多了,关于WinMain的参数,我们只需要理解第一个参数 hinstance

hinstance应用程序当前实例的句柄。
在这里插入图片描述
我们知道,一个程序光有一个窗口不行,我们不仅要有若干个子窗口来丰富我们的程序,更需要很多图片音乐放进程序里锦上添花,可是如何才能将这些东西统一成一个整体呢?实例句柄(hinstance) 因此就出来了

另外,这个hinstance是系统给我们的,不用我们瞎操心,照着用就行
就好像医院妇产科,系统都会给每一个新生儿一个hinstance,不然没hinstance很容易把别人家孩子搞混。hinstance的存在也正是如此,它可以唯一标识应用程序

2. 让系统知道我们要弄一个窗口——注册窗口类

首先,我们需要让系统知道我们要弄窗口,在WinMain函数写上

// WNDCLASS 窗口类信息,是个结构体
WNDCLASS wc = {};
wc.hInstance = hin;               //实例句柄
wc.lpszClassName = "MY_Window";   //窗口类名
RegisterClass(&wc);               //注册窗口
// RegisterClass() 注册窗口类
// 接受一个 WNDCLASS 的指针
// 必须通过这一步, 窗口才能注册成功

你会发现,我们要向系统传一个叫 “WNDCLASS” 窗口类 的东西,这个东西是什么呢?
我们在上面讲到,Hinstance实例句柄作为指向一个应用程序实例的东西,它不仅包括了窗口,还包括该应用程序的图片音乐等等很多东西,包这么多东西,如果我在外面,只是找里面的窗口岂不是很麻烦?

窗口类(WNDClass) 于是就出来了,你可以理解为 一个程序里面所有窗口的集合

窗口类毕竟是一个集合啊!那我们怎么表示单个窗口?
窗口句柄(HWND) 出来了,实际上是一个窗口在系统的唯一ID

下面是三者的关系:

在这里插入图片描述

之后我们填完WNDCLASS结构体,就可以通过 RegisterClass函数 告诉系统我们要弄窗口了。

最佳实践:

经过多个项目的验证,我总结了几个关键点:1) 做好异常处理 2) 添加详细日志 3) 单元测试覆盖核心逻辑。 这些看似简单,但能避免很多生产环境问题。

3. 创建窗口——CreateWindow函数

我们注册窗口类之后,就可以 真正弄个窗口

// 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的窗口难道只是创建就行了吗?大错特错! 你会发现,窗口会一闪而过!程序也完成任务回家了。 如何才能让窗口一直显示? 这个时候,消息循环 的重要作用就很好的彰显出来了!

//消息循环的构建
MSG msg = {};  //消息结构体
while(GetMessage(&msg,NULL,0,0)>0) // 如果没有发生错误,且收到了任意消息...
{
::TranslateMessage(&msg); // 翻译消息,将消息中的键盘码转换为对应的字符
::DispatchMessage(&msg);  // 派发消息,调用 CallBackFunc 回调函数处理消息
}

借助这个循环,程序就可以一直发出消息,阻止程序自身退出
只要用户不按窗口右上角的退出按钮!程序就会一直运行!

问题是,当你把这个循环打上去之后,你就会发现,窗口连影都没有
那么,我们可以使用 DefWindowProc() 返回一个窗口

但是,如何用呢?我们发现,把 DefWindowProc() 放进while循环里竟然不起作用

原来,是我们没有处理 DefWindowProc 返回的窗口! 那么我们应该如何处理呢?

你会发现,我们少讲了什么…

绑定回调函数

没错,就是 回调函数 (CallBackFunction)
我们可以使用回调函数,对 DefWindowProc 返回的窗口进行处理
第一次看 回调 (CallBack) 这个概念的时候萌新肯定会不理解,其实在生活中回调是触手可及的!

就比如说,你现在肚子很饿,直接下馆子,当你点完餐以后,你就会不停的询问服务员什么时候可以上菜,我快饿死了!!!
这时候,服务员肯定会不停地告诉你上菜时间!

不停的操作与获得更新结果,就是回调的概念
有了回调函数,我们就可以保证窗口一直显示

//回调函数, 返回一个窗口
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消息进行处理
然后再根据消息进行操作或返回窗口

高一的时候我开始学做窗口,就是被这消息循环难住了,导致当时还要死记硬背 (当时没理解,只好死记硬背,现在看来完全没必要,果真编程还是注重思维培养的)

消息循环还是得多看,如果你能理解上面那张流程图,那么这样的框架你也能唾手可得
经历了那么多曲折,现在我们终于可以做一个窗口了。

在这里插入图片描述

完整代码示例

// 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,我们下个教程见