(2023-06-07) Win32API【1】-- DevC++做一个窗口

本节是个大难关,一定要多看,多敲代码理解

  • [0. 关于在DevC++上的一些准备](#0. 关于在DevC++上的一些准备)
  • 教程
    • [1. 初识WinMain函数](#1. 初识WinMain函数)
    • [2. 让系统知道我们要弄一个窗口------注册窗口类](#2. 让系统知道我们要弄一个窗口——注册窗口类)
    • [3. 创建窗口------CreateWindow函数](#3. 创建窗口——CreateWindow函数)
    • [4. 萌新噩梦:消息循环与回调函数](#4. 萌新噩梦:消息循环与回调函数)
  • 完整代码示例

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

相关推荐
Yu_Lijing2 小时前
基于C++的《Head First设计模式》笔记——组合模式
c++·笔记·设计模式·组合模式
ChoSeitaku2 小时前
28.C++进阶:map和set封装|insert|迭代器|[]
java·c++·算法
钮钴禄·爱因斯晨2 小时前
操作系统第一章:计算机系统概述
linux·windows·ubuntu·系统架构·centos·鸿蒙系统·gnu
jojo_zjx2 小时前
GESP 23年9月2级 小杨的X字矩阵
c++
君生我老2 小时前
C++ list类容器常用操作
开发语言·c++
fpcc2 小时前
跟我学C++中级篇——文件和目录
linux·c++
ouliten2 小时前
C++笔记:std::tuple
c++·笔记
小龙报2 小时前
【初阶数据结构】解锁顺序表潜能:一站式实现高效通讯录系统
c语言·数据结构·c++·程序人生·算法·链表·visual studio
历程里程碑2 小时前
Linux 1 指令(1)入门:6大基础指令详解
linux·运维·服务器·c语言·开发语言·数据结构·c++