C/C++ 从零实现一个windows窗口(非常详细)


C/C++教程目录(专栏文章列表,欢迎订阅,持续更新...) https://blog.csdn.net/weixin_50964512/article/details/125710864

前言

学习完C/C++之后,最郁闷的可能就是感觉啥也干不了,怎么才能写出平常我们所使用的软件模样?

而对于大部分人来说,最常使用的就是windows系统,所以这就需要了解windows编程,熟悉一个windows窗口的建立流程

建立一个windows窗口的过程是非常繁琐的,真正做开发的时候,为了追求效率,一般会选用现场的框架,比如大型的QtMFC,或者小型灵巧的ATL模板库,可大大简化C/C++程序员的程序开发。

而本文旨在了解实现一个基本windows窗口,我们需要做哪些必要步骤?以及明白当我们用别人的框架短短几行代码实现一个窗口的背后,框架为我们做了哪些工作?

学习方法

Windows API编程对新手来说是一件非常痛苦的事情,因为它全部用的自定义变量类型,和一些宏,以及很多函数的参数非常多,有的可能多达10个

但这些你不需要过分关注,在VS中,你可以复制下代码后,右键具体的变量类型或函数,如WORD,然后点击速览定义,就可以看到它的实际类型,即C/C++中的如int,short char等基本数据类型
在这里插入图片描述

或者对于复杂的函数,如winmain等,只需要将鼠标点击到该函数名,按F1即可跳转到官方文档

注意,这些完全不用专门去记住,因为WIndows自定义的类型太多了,你只需要知道怎样快速的查到信息即可,需要的时候直接查官方文档就可以了

一、建立一个窗口的基本步骤

  1. 向系统注册一个窗体类
  2. 根据窗体类创建窗口
  3. 进入消息循环

看起来很简单的,不是吗?也就三个步骤而已

但真正让新手痛苦的是这三个步骤中所使用的大量Win API函数

接下来就开始详细介绍具体各个步骤

二、具体流程

1.创建一个空的windows窗口项目

选择windows桌面向导
在这里插入图片描述
随便取一个名字,根据我的经验,尽量用字母英文,别用汉字,否则以后可能会出现奇奇怪怪的错误
在这里插入图片描述
点击创建后,选择如下图所示的内容
在这里插入图片描述
点击确认即可
在这里插入图片描述
右键源文件,添加,新建项
在这里插入图片描述
创建一个.cpp文件即可,名字可自取,我习惯于用英文,防止出现不必要的意外

这样,一个空的带窗口程序的空项目就建立完成了,但现在才刚刚开始!

2.WinMain函数

学到这里,相信至少对C/C++基础应该是相当熟悉的,一个最简单的C/C++程序就必须要写上main函数,否则无法编译成功,因为这是控制台程序的入口函数

这里也一样,当你想写带界面的窗口程序,你就需要将入口函数改为WinMain

函数原型:

int WINAPI WinMain(
  [in] HINSTANCE hInstance, //当前应用程序的实例
  [in] HINSTANCE hPrevInstance, //这个不用管,想了解的可参考官方文档
  [in] LPSTR     lpCmdLine, //传入的命令行参数
  [in] int       nShowCmd //控制当前应用窗口如何显示
);

官方文档点这里

前面的【in】等只是让你知道这个参数是输入还是输出的,可以直接删除掉,改下格式后,代码如下:
在这里插入图片描述
再次强调,不用试图去记忆,这会加重你的学习负载,直接复制即可,各个参数的含义,会在后面具体的代码中用到,用的多了,就自然而然的记住了

3.注册窗口类

这里主要用到API函数RegisterClass

函数原型:

ATOM RegisterClassA(
  [in] const WNDCLASSA *lpWndClass //填入要注册的窗口结构
);

窗口结构WNDCLASSA

typedef struct tagWNDCLASSA {
  UINT      style;  //设置窗口格式,可参考官方文档,一般设置为水平重画与垂直重画:CS_HREDRAW | CS_VREDRAW
  WNDPROC   lpfnWndProc; //窗口的回调函数,也就是窗口接收到消息后,交给哪个函数处理
  int       cbClsExtra; //为类额外分配内存,一般为0
  int       cbWndExtra; //为窗口额外分配内存,一般为0
  HINSTANCE hInstance; //程序实例,这里就用到了WinMain函数的第一个参数hInstance
  HICON     hIcon; //设置程序图标
  HCURSOR   hCursor; //设置程序光标
  HBRUSH    hbrBackground; //设置程序背景色
  LPCSTR    lpszMenuName; //设置菜单名称
  LPCSTR    lpszClassName; //设置类名称
} WNDCLASSA, *PWNDCLASSA, *NPWNDCLASSA, *LPWNDCLASSA;

使用:

#include<Windows.h>
int WINAPI WinMain(HINSTANCE hInstance,HINSTANCE hPrevInstance,LPSTR lpCmdLine,int nShowCmd) {
	WNDCLASS wndcls; //创建一个窗体类
	wndcls.cbClsExtra = 0;//类的额外内存,默认为0即可
	wndcls.cbWndExtra = 0;//窗口的额外内存,默认为0即可
	wndcls.hbrBackground = (HBRUSH)GetStockObject(WHITE_BRUSH);//获取画刷句柄(将返回的HGDIOBJ进行强制类型转换)
	wndcls.hCursor = LoadCursor(NULL, IDC_CROSS);//设置光标
	wndcls.hIcon = LoadIcon(NULL, IDI_ERROR);//设置窗体左上角的图标
	wndcls.hInstance = hInstance;//设置窗体所属的应用程序实例
	wndcls.lpfnWndProc = NULL;//设置窗体的回调函数,暂时没写,先设置为NULL,后面补上
	wndcls.lpszClassName = L"test";//设置窗体的类名
	wndcls.lpszMenuName = NULL;//设置窗体的菜单,没有,填NULL
	wndcls.style = CS_HREDRAW | CS_VREDRAW;//设置窗体风格为水平重画和垂直重画
	RegisterClass(&wndcls);//向操作系统注册窗体
}

在这里插入图片描述

在上述代码中,还用到了三个额外的API,

GetStockObject:获取电脑自带的画刷,笔等
LoadCursor:加载光标,也就是我们电脑屏幕上常见的箭头,十字,转圈等图形
LoadIcon:加载图标资源

这三个函数很简单,因为参数很少,不好理解的可能是这些参数应该怎能填?从哪里来?

最稳妥的方法依旧是查看官方文档,虽然是英文,但现在的翻译也很强大了,相信难不倒诸位对吧。

比如这里用GetStockObject函数举例,进入官网,就可以看到可填参数有很多:
在这里插入图片描述
后面两个函数还多一个程序实例的参数,我都填的NULL,意思就是从系统默认中加载即可,毕竟我也没有自己做的图标啊……

4.创建窗口

这里用到的函数就可以说是一个超级函数了,参数多达11个!

CreateWindow:创建一个窗口
函数原型:

void CreateWindowA(
  [in, optional]  lpClassName,//窗口类名,也就是上面注册窗口类时填的类名
  [in, optional]  lpWindowName, //窗口名,也就是窗口显示时,左上角显示的名称
  [in]            dwStyle, //窗口风格,一般填WS_OVERLAPPEDWINDOW即可,这样创建的窗口就有最大化,最小化等等等等属性。。。
  [in]            x, //窗口最开始被创建在哪里?这里填X坐标,一般默认CW_USEDEFAULT即可
  [in]            y, //窗口最开始被创建在哪里?这里填Y坐标,一般默认CW_USEDEFAULT即可
  [in]            nWidth, //窗口创建的宽度,一般默认CW_USEDEFAULT即可
  [in]            nHeight, //窗口创建的高度,一般默认CW_USEDEFAULT即可
  [in, optional]  hWndParent,//父窗口的句柄,没有,填NULL
  [in, optional]  hMenu, //菜单句柄,没有,填NULL
  [in, optional]  hInstance, //应用程序实例,这里就又用到了WinMain的第一个参数
  [in, optional]  lpParam //额外的数据,挺复杂的,咱们也不需要,填NULL就行了
);

虽然这个函数的参数巨多,但很多参数都是填默认的就可以了,所以有没有一种把它封装一下的冲动呢?

这样以后就可以填两三个参数就行了,这里我就先不谈了,可以自己尝试一下

使用:

	//产生一个窗体,并返回该窗体的句柄,第一个参数必须为要创建的窗体的类名,第二个参数为窗体标题名
	HWND hwnd = CreateWindow(L"test", L"我的第一个窗口",
		WS_OVERLAPPEDWINDOW,CW_USEDEFAULT,
		CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT,
		NULL, NULL, hInstance, NULL);

此时代码截图:
在这里插入图片描述
可能长的有点怪,但没办法,参数太多,一行放不下……

此时还没有结束,因为窗口创建后默认隐藏的,所以我们还得让它显示出来:

	ShowWindow(hwnd, SW_SHOWNORMAL);//把窗体显示出来
	UpdateWindow(hwnd);//更新窗体

这两个API函数也很简单,就一两个参数,稍微看一下文档就能明白
ShowWindow :显示窗口的方式,我填的SW_SHOWNORMAL,即正常显示,更多可选参数看官方文档就完事了,而前面那个hwnd就是上面创建窗口返回的窗口句柄
UpdateWindow:就一个参数,上面创建窗口时返回的窗口句柄,作用就是给窗口发个消息,命令它马上重新画一下窗口 !

此时的代码截图:
在这里插入图片描述

5.消息循环

消息循环对于一个带界面的程序来说可以是最重要的一环了

如何理解消息呢?举个简单的例子,就是当你用鼠标拖动一个窗口时,窗口为啥能跟着你的鼠标运动?就是依靠的消息。

当你的鼠标点击窗口时,就会向对应的窗口发送鼠标点击消息,当你鼠标左键点击并移动时,就会向窗体发送鼠标点击和移动的消息

所谓的消息其实也就是一个结构体,其中包含了消息的类型和其它相关的信息,比如鼠标点击的屏幕坐标等,这个还是老话,得参考官方文档!

下面就开始正式介绍消息循环

消息循环中主要用到了三个API函数:
GetMessage:拿到消息
TranslateMessage:翻译消息
DispatchMessage:派发消息

函数原型:

BOOL GetMessage(
  [out]          LPMSG lpMsg, //拿到的消息
  [in, optional] HWND  hWnd, //拿哪个窗口的消息,一般填NULL,即拿属于该线程所有窗口的消息
  [in]           UINT  wMsgFilterMin, //过滤消息,最小值,一般填0
  [in]           UINT  wMsgFilterMax //过滤消息,最大值,一般填0,当两个均为0,则不进行过滤
);
BOOL TranslateMessage(
  [in] const MSG *lpMsg //翻译消息,如将WMWM_KEYDOWN和WM_KEYUP翻译成一个WM_CHAR消息
);

该函数官方文档的内容中,说道如果使用虚拟键码的话,就可不用该函数,所以还是得多看官方文档啊,总能有一些意外之喜
在这里插入图片描述
这里又说道了虚拟键码,真是越学东西越多啊。

所谓虚拟键码,就是一个数值而已,某一个确定的数值代表你按下的某个键,比如当你按下A键,程序就会收到一个包含数值0x41的消息,这个数字其实就是字母A的ASCII码表的值,所以代码中可以直接写为’A’就可以了

ASCII码表应该就不用过多解释了吧…

如果不理解,还是看下面的程序吧!至少得把基本步骤记住啊!

LRESULT DispatchMessage(
  [in] const MSG *lpMsg
);

该函数就是把消息派发给对应的窗口,因为一个程序可以有很多窗口的吧,总得让消息发送给对应的窗口,否则我点这个窗口,要么没反映----没有收到消息,要么另外一个窗口动了----发错窗口了…

6.回调函数

回调函数可就很重要了,因为你在窗口上所做的一切,都会以消息的形式发送给窗口,而窗口就是通过回调函数来处理这些消息的!

回调函数格式:

LRESULT CALLBACK WinSunProc(
	HWND hwnd,      // 窗口句柄
	UINT uMsg,      // 消息识别符
	WPARAM wParam,  // 第一个消息参数
	LPARAM lParam   // 第二个消息参数
)

这个函数的参数由系统传入,特别是后两个参数,其含义取决于收到的消息类型!

就可以写下代码:

LRESULT CALLBACK WinSunProc(HWND hwnd,UINT uMsg,WPARAM wParam,LPARAM lParam)
{
	switch (uMsg)//通过判断消息进行消息响应
	{
	default:
		return DefWindowProc(hwnd, uMsg, wParam, lParam);//对不感兴趣的消息进行缺省处理,必须有该代码,否则程序有问题
	}
	return 0;

此函数中写个switch结构是基本操作,目的就是不同消息进行不同处理

具体系统有哪些消息呢?点这里查看

需要注意的是DefWindowProc函数,参数就是回调函数所传入的参数,目的就是对我们不感兴趣的消息进行默认处理

此时就可以为我们前面窗口类的回调函数参数填上此函数的名称

此时程序结构如下:
在这里插入图片描述
现在,你就已经可以运行程序了!
在这里插入图片描述
成功出现窗口!

左上角那个X就是前面LoadIcon加载的资源,光标是个十字就是LoadCursor函数加载的资源,可自行更改

可能你会注意到,当你点击右上角的X时,窗口没了,但程序仍然在运行,这是因为我们没有处理窗口关闭消息

此时应该添加对WM_CLOSE和WM_DESTROY消息的处理

	switch (uMsg)//通过判断消息进行消息响应
	{
	case WM_CLOSE:
		DestroyWindow(hwnd);//销毁窗口并发送WM_DESTROY消息,但是程序没有退出
		break;
	case WM_DESTROY:
		PostQuitMessage(0);//发出WM_QUIT消息,结束消息循环
		break;
	default:
		return DefWindowProc(hwnd, uMsg, wParam, lParam);//对不感兴趣的消息进行缺省处理,必须有该代码,否则程序有问题
	}
	return 0;

当你点击关闭时,会发送WM_CLOSE消息到该函数,这里再WM_COLSE的消息处理中调用了DestroyWindow函数,该函数的作用就是彻底摧毁该窗口和与该窗口相关的一切东西,并向自己发送WM_WM_DESTROY消息

所以我们就需要处理WM_DESTROY消息,这里调用了PostQuitMessage函数,目的是发送一个退出程序的消息 WM_QUIT ,当消息循环中的GetMessage函数收到该消息时,就会返回0,继而退出while循环,进而程序结束

此时运行程序,就可以正常退出了!

至此,通过重重困难,你终于从0写出了一个窗口程序了!

此时你也应该明白哪些MFC框架、Qt框架为我们做了多少事情吧!

最后再把完整代码贴出来供大家借鉴

三、完整代码

#include<Windows.h>
LRESULT CALLBACK WinSunProc(HWND hwnd,UINT uMsg,WPARAM wParam,LPARAM lParam)
{
	switch (uMsg)//通过判断消息进行消息响应
	{
	case WM_CLOSE:
		DestroyWindow(hwnd);//销毁窗口并发送WM_DESTROY消息,但是程序没有退出
		break;
	case WM_DESTROY:
		PostQuitMessage(0);//发出WM_QUIT消息,结束消息循环
		break;
	default:
		return DefWindowProc(hwnd, uMsg, wParam, lParam);//对不感兴趣的消息进行缺省处理,必须有该代码,否则程序有问题
	}
	return 0;
}

int WINAPI WinMain(HINSTANCE hInstance,HINSTANCE hPrevInstance,LPSTR lpCmdLine,int nShowCmd) {
	WNDCLASS wndcls; //创建一个窗体类
	wndcls.cbClsExtra = 0;//类的额外内存,默认为0即可
	wndcls.cbWndExtra = 0;//窗口的额外内存,默认为0即可
	wndcls.hbrBackground = (HBRUSH)GetStockObject(WHITE_BRUSH);//获取画刷句柄(将返回的HGDIOBJ进行强制类型转换)
	wndcls.hCursor = LoadCursor(NULL, IDC_CROSS);//设置光标
	wndcls.hIcon = LoadIcon(NULL, IDI_ERROR);//设置窗体左上角的图标
	wndcls.hInstance = hInstance;//设置窗体所属的应用程序实例
	wndcls.lpfnWndProc = WinSunProc;//设置窗体的回调函数,暂时没写,先设置为NULL,后面补上
	wndcls.lpszClassName = L"test";//设置窗体的类名
	wndcls.lpszMenuName = NULL;//设置窗体的菜单,没有,填NULL
	wndcls.style = CS_HREDRAW | CS_VREDRAW;//设置窗体风格为水平重画和垂直重画
	RegisterClass(&wndcls);//向操作系统注册窗体

	
	//产生一个窗体,并返回该窗体的句柄,第一个参数必须为要创建的窗体的类名,第二个参数为窗体标题名
	HWND hwnd = CreateWindow(L"test", L"我的第一个窗口",
		WS_OVERLAPPEDWINDOW,CW_USEDEFAULT,
		CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT,
		NULL, NULL, hInstance, NULL);

	ShowWindow(hwnd, SW_SHOWNORMAL);//把窗体显示出来
	UpdateWindow(hwnd);//更新窗体

	MSG msg;
	//消息循环
	while (GetMessage(&msg, NULL, 0, 0))//如果消息不是WM_QUIT,返回非零值;如果消息是WM_QUIT,返回零
	{
		TranslateMessage(&msg);//翻译消息,如把WM_KEYDOWN和WM_KEYUP翻译成一个WM_CHAR消息
		DispatchMessage(&msg);//派发消息
	}

}

四、常见错误解答

因为一些同学不按文章的逻辑写代码,就会出现一些问题,这里统一回答。

1.出现链接错误

在这里插入图片描述

window程序常见的一般为两个子系统,一个是控制台子系统,也就是常说的控制台程序。

而另外一个则是GUI程序,就是带界面的子系统。

如果你创建控制台项目,就默认为控制台程序,即入口函数为main

而只有创建窗口子系统,入口函数才为winMain,你才能运行起来代码,所以本文前面选择的windows桌面向导,来创建窗口子系统。
在这里插入图片描述

但其实这是可以直接在项目属性中进行调整的:
在这里插入图片描述

或者更简单的,你可以直接复制下面这段代码,放在你的源码最前面,也是一个效果,作用就是告诉编译器,我们要创建的是窗口子系统:

#pragma comment( linker, "/subsystem:\"windows\" /entry:\"WinMainCRTStartup\"" )

或者也可以为控制台子系统:

#pragma comment( linker, "/subsystem:\"console\" /entry:\"mainCRTStartup\"" )

甚至还可以写一个无窗口的控制台子系统:

#pragma comment(linker, "/subsystem:\"windows\" /entry:\"mainCRTStartup\"")

原理就是让入口点函数为main函数,但子系统又采用窗口子系统,这样就不会出现黑窗口了。

  • 200
    点赞
  • 675
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 36
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 36
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

余识-

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值