问题标题: win32窗口程序基本教程 ①~③

3
1
已解决
薛乘志
薛乘志
初级启示者
初级启示者

新坑

最近**codingtang.h的win实现,也学会了win32窗口程序的编程方法,发个帖讲讲心得

有空就连载

薛乘志在2022-06-19 13:02:35追加了内容

开始:头文件

先引入win32程序编写必须的头文件

#include <windows.h>

 

① 创建窗口

1. 窗口类

窗口类定义一些窗口可能共有的一些行为,创建窗口前,需要注册该窗口使用的窗口类名

以下代码可在程序任意部分编写

填写 WNDCLASS 结构:

//以下是必填内容
WNDCLASS wc = {};
wc.lpfnWndProc = WindowProc; //消息回调函数(以后介绍)
wc.hInstance = GetModuleHandle(NULL); //程序句柄
wc.lpszClassName = "Win32App"; //窗口类名
//以下是选填内容
wc.hbrBackground = CreateSolidBrush(RGB(0, 0, 0)); //背景颜色
wc.style = CS_DBLCLKS; //开启双击消息获取(以后有用)

注册窗口类:

RegisterClass(&wc); //将填写过的结构wc注册为窗口类

2. 消息回调函数

win32程序是消息驱动的,在运行时会接收到若干消息以处理用户输入,图形显示等

消息回调函数即用于处理消息的函数,通用格式如下:

static LRESULT CALLBACK WindowProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam) {
    //处理消息
}

其中:static表示该函数为静态的;LRESULT表示返回值,是win32定义的数据类型;CALLBACK表示这是一个供win32调用的回调函数;参数中的hwnd表示接收该消息的窗口句柄;uMsg表示该消息的类型;wParam、lParam用于描述该消息的具体内容

处理消息部分的一般代码:

//WindowProc函数内:
switch (uMsg) {
	case WM_DESTROY: //窗口关闭消息
		PostQuitMessage(0); //关闭窗口
		break;
	default: //默认情况,调用**函数处理窗口消息
		DefWindowProc(hwnd, uMsg, wParam, lParam);
}
return 0;

3. 创建窗口

终于到了我们最激动人心的时刻!

调用CreateWindowEx创建新窗口:

HWND hwnd = CreateWindowEx(0, //不必管它
	                       "Win32App", //窗口类名
	                       "New App", //窗口标题
	                       WS_VISIBLE | WS_OVERLAPPEDWINDOW, //窗口样式(以后会详细讲)
	                       CW_USEDEFAULT, //窗口位置,CW_USEDEFAULT表示默认,下同
	                       CW_USEDEFAULT, //窗口位置
	                       CW_USEDEFAULT, //窗口大小
	                       CW_USEDEFAULT, //窗口大小
	                       NULL, NULL, GetModuleHandle(NULL), NULL //不必管它
	                      );

4. 消息循环

没错,创建完窗口,还有重要的一步,消息循环!!!

前面说了要有一个处理消息的函数,但是我们还没有接收和处理消息啊

处理消息的循环:

MSG msg;
while (GetMessage(&msg, NULL, 0, 0) > 0 /* 如果窗口未关闭 */) {
	TranslateMessage(&msg); //不必管它
	DispatchMessage(&msg); //调用注册的消息处理函数
}

于是一个空白的窗口就诞生了!

5. 本节完整代码:

// ① 创建窗口

#include <windows.h>

static LRESULT CALLBACK WindowProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam) {
	switch (uMsg) {
		case WM_DESTROY: //窗口关闭消息
			PostQuitMessage(0); //关闭窗口
			break;
		default: //默认情况,调用**函数处理窗口消息
			return DefWindowProc(hwnd, uMsg, wParam, lParam);
	}
	return 0;
}

int main() {
	WNDCLASS wc = {};
	wc.lpfnWndProc = WindowProc; //消息回调函数(以后介绍)
	wc.hInstance = GetModuleHandle(NULL); //程序句柄
	wc.lpszClassName = "Win32App"; //窗口类名
	RegisterClass(&wc); //将填写过的结构wc注册为窗口类
	HWND hwnd = CreateWindowEx(0, //不必管它
	                           "Win32App", //窗口类名
	                           "New App", //窗口标题
	                           WS_VISIBLE | WS_OVERLAPPEDWINDOW, //窗口样式(以后会详细讲)
	                           CW_USEDEFAULT, //窗口位置,CW_USEDEFAULT表示默认,下同
	                           CW_USEDEFAULT, //窗口位置
	                           CW_USEDEFAULT, //窗口大小
	                           CW_USEDEFAULT, //窗口大小
	                           NULL, NULL, GetModuleHandle(NULL), NULL //不必管它
	                          );
	MSG msg;
	while (GetMessage(&msg, NULL, 0, 0) > 0 /* 如果窗口未关闭 */) {
		TranslateMessage(&msg); //不必管它
		DispatchMessage(&msg); //调用注册的消息处理函数
	}
	return 0;
}

下节内容:绘制窗口(基**)

薛乘志在2022-06-19 13:46:59追加了内容

②:绘制窗口(基本)

1. 处理重绘消息:

在上一节窗口回调函数的switch中,添加如下代码:

case WM_PAINT: //窗口重绘消息
	WindowPrint(hwnd); //调用绘制函数
	break;

其中,WindowPrint是我们定义的一个函数,其唯一的参数是窗口句柄

void WindowPrint(HWND hwnd) {
}

注意:WM_PAINT函数仅在**认为需要重绘时收到,如果想要立即重绘需要手动发送消息(以后会讲)

2. 开始/结束绘制

使用BeginPaint和EndPaint函数开始/结束绘制

PAINTSTRUCT ps;
HDC hdc = BeginPaint(hwnd, &ps); //开始绘制
//绘制代码...
EndPaint(hwnd, &ps); //结束绘制

HDC是win32的绘图设备句柄

3. 绘制一个点

在BeginPaint和EndPaint间,使用SetPixel函数绘制点:

SetPixel(hdc/*句柄*/, 1/*X坐标*/, 1/*Y坐标*/, RGB(255, 0, 0)/*红色*/);

其中,RGB(r,g,b)表示将三原色表示的颜色转化为win32能识别的模式

以下为酷丁平台**文档内容:

学习过色彩的同学应该明白三原色  红、绿、蓝
	r:红
	g:绿
	b:蓝
即:
	红色: { 255 , 0 , 0 };
	绿色: { 0 , 255 , 0 };
	蓝色: { 0 , 0 , 255 };
	黑色: { 0 , 0 , 0 };
    白色: { 255 , 255 , 255 };

运行代码,就会发现在左上角出现了一个小的不能再小的红点

4. 绘制一条线

绘制点有个p用?当然是用于画线!

不过,我们也不用手动实现画线的代码,win32提供了实现好的代码,不过内部也是使用画点实现的

MoveToEx(hdc, 10/*X坐标*/, 1/*Y坐标*/, NULL); //修改线的起始点
LineTo(hdc, 40/*X坐标*/, 1/*Y坐标*/); //从起始点画线

将其加入到刚才的位置,就会发现在红点右边出现了一条细得不能再细的黑直线

5. 绘制一条线plus

为什么是黑线呢?线不能改变颜色吗?答案是当然可以

上面的画线函数调用的是画笔的颜色,所以我们需要修改画笔的颜色

创建一个新的画笔:

HPEN Pen = CreatePen(PS_SOLID/*画笔类型*/, 1/*画笔宽度*/, RGB(0, 255, 0)/*绿色*/);

画笔类型包括:

PS_SOLID:画笔画出的是实线
PS_DASH:画笔画出的是虚线(宽度必须不大于1)
PS_DOT:画笔画出的是点线(宽度必须不大于1)
PS_DASHDOT:画笔画出的是点划线(宽度必须不大于1)
PS_DASHDOTDOT:画笔画出的是点-点-划线(宽度必须不大于1)

将画笔绑定到hdc上:

SelectObject(hdc, Pen);

画线就有颜色了:

MoveToEx(hdc, 10/*X坐标*/, 1/*Y坐标*/, NULL); //修改线的起始点
LineTo(hdc, 40/*X坐标*/, 1/*Y坐标*/); //从起始点画线

最后,画笔是一个对象,创建了就必须释放

DeleteObject(Pen);

运行代码,就会发现红点右边出现了一条绿线

6. 绘制矩形

画线需要画笔,画矩形则需要画刷

HBRUSH hbr = CreateSolidBrush(RGB(0, 0, 255)/*蓝色*/); //创建画刷

然后就可以画矩形了:

RECT rect;
rect.left = 1; //左侧位置
rect.top = 10; //顶部位置
rect.right = 11; //右侧位置
rect.bottom = 20; //底部位置
FillRect(hdc, &rect, hbr); //填充矩形

最后释放画刷:

DeleteObject(hbr); //释放资源

运行代码,就会发现点和线下面出现了一个蓝色小方块

7. 注意事项

HPEN和HBRUSH在创建以后一定要记得释放!!不然会造成内存泄漏的问题!!!

创建HPEN和HBRUSH是一项开销较大的操作,所以在绘制中如果不需要重新创建HPEN和HBRUSH尽量不要重新创建,使用一个就够了

8. 本节完整代码


#include <windows.h>

void WindowPrint(HWND hwnd) { //定义绘制函数,参数为窗口句柄
	PAINTSTRUCT ps;
	//HDC为绘图设备句柄
	HDC hdc = BeginPaint(hwnd, &ps); //开始绘制

	//绘制点:
	SetPixel(hdc/*句柄*/, 1/*X坐标*/, 1/*Y坐标*/, RGB(255, 0, 0)/*红色*/);
	//绘制直线:
	HPEN Pen = CreatePen(PS_SOLID/*画笔类型*/, 1/*宽度*/, RGB(0, 255, 0)/*绿色*/);
	SelectObject(hdc, Pen);
	MoveToEx(hdc, 10/*X坐标*/, 1/*Y坐标*/, NULL); //修改线的起始点
	LineTo(hdc, 40/*X坐标*/, 1/*Y坐标*/); //从起始点画线
	DeleteObject(Pen); //释放资源
	//绘制矩形:
	HBRUSH hbr = CreateSolidBrush(RGB(0, 0, 255)/*蓝色*/); //创建画刷
	RECT rect;
	rect.left = 1; //左侧位置
	rect.top = 10; //顶部位置
	rect.right = 11; //右侧位置
	rect.bottom = 20; //底部位置
	FillRect(hdc, &rect, hbr); //填充矩形
	DeleteObject(hbr); //释放资源
	
	EndPaint(hwnd, &ps); //结束绘制
}

static LRESULT CALLBACK WindowProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam) {
	switch (uMsg) {
		case WM_DESTROY:
			PostQuitMessage(0);
			break;
		case WM_PAINT: //窗口重绘消息
			WindowPrint(hwnd); //调用绘制函数
			break;
		default:
			return DefWindowProc(hwnd, uMsg, wParam, lParam);
	}
	return 0;
}

int main() {
	WNDCLASS wc = {};
	wc.lpfnWndProc = WindowProc;
	wc.hInstance = GetModuleHandle(NULL);
	wc.lpszClassName = "Win32App";
	RegisterClass(&wc);
	HWND hwnd = CreateWindowEx(0, "Win32App", "App",
	                           //有所不同,使用这种窗口样式禁用最大化按钮和用户改变窗口大小
	                           WS_VISIBLE | (WS_OVERLAPPEDWINDOW ^ WS_THICKFRAME ^ WS_MAXIMIZEBOX),
	                           CW_USEDEFAULT, CW_USEDEFAULT, 800, 600, NULL, NULL, GetModuleHandle(NULL), NULL);
	MSG msg;
	while (GetMessage(&msg, NULL, 0, 0) > 0) {
		TranslateMessage(&msg);
		DispatchMessage(&msg);
	}
	return 0;
}

9. 下节内容:绘制进阶(实心圆,文字、图片、双缓冲)

薛乘志在2022-06-26 20:08:28追加了内容

③ 绘图进阶

1. 绘制圆

绘制圆的过程有一点麻烦,新建一个函数处理:

void fillCircle(HDC hdc/*绘图句柄*/, int x/*X坐标*/, int y/*Y坐标*/, int r/*半径*/, COLORREF rgb/*颜色*/)

先通过注册hPen以设置圆边框的颜色:

HPEN hPen = CreatePen(PS_SOLID, 1, rgb);
SelectObject(hdc, hPen);

再通过注册hBrush以设置圆内部的颜色:

HBRUSH hBrush = CreateSolidBrush(rgb);
SelectObject(hdc, hBrush);

然后画圆啦: 

Ellipse(hdc, x - r, y - r, x + r, y + r);

最后别忘了释放资源:

DeleteObject(hPen);
DeleteObject(hBrush);

在WindowPrint函数内调用函数就行了:

fillCircle(hdc, 100/*X坐标*/, 100/*Y坐标*/, 50/*半径*/, RGB(255, 0, 0)/*红色*/);

2. 绘制文本

绘制文本的过程同样复杂,再新建一个函数处理

void drawText(HDC hdc, const char text[], int x, int y, //HDC,文本内容,坐标
              int bkmode = TRANSPARENT, COLORREF bkrgb = RGB(255, 255, 255), //背景模式,背景颜色
              COLORREF fontrgb = RGB(0, 0, 0), int fontsize = 18, const char fontname[] = "微软雅黑") //字体颜色,字体粗细,字体名称

为了方便使用,设置函数参数默认值

先根据字体信息创建字体:

LOGFONT logfont; //创建字体信息
ZeroMemory(&logfont, sizeof(LOGFONT)); //初始化
logfont.lfHeight = fontsize; //设置字体大小
strcpy(logfont.lfFaceName, fontname); //设置字体名称
HFONT hFont = CreateFontIndirect(&logfont); //根据字体信息创建字体
SelectObject(hdc, hFont); //字体绑定到hdc

设置文字的其他选项:

SetTextColor(hdc, fontrgb); //设置字体颜色
SetBkColor(hdc, bkrgb); //设置背景颜色
SetBkMode(hdc, bkmode); //设置背景模式

这个背景模式包括:TRANSPARENT(透明),OPAQUE(根据设置的颜色)

然后输出文字即可:

TextOut(hdc, x, y, text, strlen(text));

释放内存万万不可忘记:

DeleteObject(hFont);

最后在WindowPrint内调用函数即可:

drawText(hdc, "这是一个圆形", 60, 160);

3. 让图形动起来

所以绘制了这么多基本图形,如何让图形动起来呢?

首先我们需要一个键盘输入,在WindowProc的switch内添加:

case WM_KEYDOWN:
	//处理键盘按下消息
	break;

这就处理了键盘按下的消息(注:输入消息以后会详细的讲)

如果按下按键,就让我们的圆和文字右移一格,这应该很好实现,不再赘述

不过,当我们运行时,发现按下键盘后,文字并没有移动,这是因为窗口并没有立即更新

想让窗口立即更新,需要使用一下代码:

InvalidateRect(hwnd, NULL, 1); //设置无效区
UpdateWindow(hwnd); //更新窗口

然后在运行时就会发现前后的图形重叠在一起,无法识别了

解决方法是在显示图形前先绘制一个和窗口等大的矩形覆盖掉原有的图形,就行了

运行代码,就会发现图形虽然能正常移动了,但是是不是有时闪一下?

这个问题留到下一小节解决

4. 双缓冲

闪屏的原因很简单,就是在清空屏幕时,还没有画出新的图形,就已经显示在了屏幕上,导致了一瞬间的闪烁

解决这个问题就可以先把图形画在一个图片上,再直接绘制图形到屏幕

直接上代码:

RECT rc;
GetClientRect(hwnd, &rc); //获取屏幕大小
HDC hMemDc = CreateCompatibleDC(hdc); //创建临时DC
HBITMAP hBmp = CreateCompatibleBitmap(hdc, rc.right, rc.bottom); //创建位图
SelectObject(hMemDc, hBmp); //将位图绑定到临时DC

//在临时DC上画图
fillRect(hMemDc, 0, 0, 800, 600); 
fillCircle(hMemDc, X, 100, 50, RGB(255, 0, 0));
drawText(hMemDc, "这是一个圆形", X - 40, 160);

BitBlt(hdc, 0, 0, rc.right, rc.bottom, hMemDc, 0, 0, SRCCOPY); //将位图画到屏幕上
DeleteObject(hBmp); //释放资源,下同
DeleteObject(hMemDc);

完美吗?不完美。

在窗口运行时,还有一个消息是专门重绘背景的(由于我们已经手动绘制了背景),就没有必要处理它了,但是它重绘时会影响我们双缓冲的效果,所以需要手动拦截这个消息,不处理

在WindowProc的switch里加上:

case WM_ERASEBKGND:
	break;

完美!

5. 图片部分留到下节再讲吧,本节完整代码:


#include <windows.h>

void fillRect(HDC hdc, int x, int y, int w, int h, COLORREF rgb = RGB(255, 255, 255)) {
	HBRUSH hbr = CreateSolidBrush(rgb);
	RECT rect;
	rect.left = x, rect.top = y;
	rect.right = w + x;
	rect.bottom = h + y;
	FillRect(hdc, &rect, hbr);
	DeleteObject(hbr);
}

void fillCircle(HDC hdc, int x, int y, int r, COLORREF rgb) {
	HPEN hPen = CreatePen(PS_SOLID, 1, rgb);
	SelectObject(hdc, hPen);
	HBRUSH hBrush = CreateSolidBrush(rgb);
	SelectObject(hdc, hBrush);
	Ellipse(hdc, x - r, y - r, x + r, y + r);
	DeleteObject(hPen);
	DeleteObject(hBrush);
}

void drawText(HDC hdc, const char text[], int x, int y,
              int bkmode = TRANSPARENT, COLORREF bkrgb = RGB(255, 255, 255),
              COLORREF fontrgb = RGB(0, 0, 0), int fontsize = 18, const char fontname[] = "微软雅黑") {
	LOGFONT logfont;
	ZeroMemory(&logfont, sizeof(LOGFONT));
	logfont.lfHeight = fontsize;
	strcpy(logfont.lfFaceName, fontname);
	HFONT hFont = CreateFontIndirect(&logfont);
	SelectObject(hdc, hFont);
	SetTextColor(hdc, fontrgb);
	SetBkColor(hdc, bkrgb);
	SetBkMode(hdc, bkmode);
	TextOut(hdc, x, y, text, strlen(text));
	DeleteObject(hFont);
}

int X = 100;

void WindowPrint(HWND hwnd) {
	PAINTSTRUCT ps;
	HDC hdc = BeginPaint(hwnd, &ps);

	RECT rc;
	GetClientRect(hwnd, &rc);
	HDC hMemDc = CreateCompatibleDC(hdc);
	HBITMAP hBmp = CreateCompatibleBitmap(hdc, rc.right, rc.bottom);
	SelectObject(hMemDc, hBmp);

	fillRect(hMemDc, 0, 0, rc.right, rc.bottom);
	fillCircle(hMemDc, X, 100, 50, RGB(255, 0, 0));
	drawText(hMemDc, "这是一个圆形", X - 40, 160);

	BitBlt(hdc, 0, 0, rc.right, rc.bottom, hMemDc, 0, 0, SRCCOPY);
	DeleteObject(hBmp);
	DeleteObject(hMemDc);

	EndPaint(hwnd, &ps);
}

LRESULT CALLBACK WindowProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam) {
	switch (uMsg) {
		case WM_DESTROY:
			PostQuitMessage(0);
			break;
		case WM_PAINT:
			WindowPrint(hwnd);
			break;
		case WM_ERASEBKGND:
			return 0;
		case WM_KEYDOWN:
			X++;
			InvalidateRect(hwnd, NULL, 1);
			UpdateWindow(hwnd);
			break;
		default:
			return DefWindowProc(hwnd, uMsg, wParam, lParam);
	}
	return 0;
}

int main() {
	WNDCLASS wc = {};
	wc.lpfnWndProc = WindowProc;
	wc.hInstance = GetModuleHandle(NULL);
	wc.lpszClassName = "Win32App";
	RegisterClass(&wc);
	HWND hwnd = CreateWindowEx(0, "Win32App", "App", WS_VISIBLE | WS_OVERLAPPEDWINDOW,
	                           CW_USEDEFAULT, CW_USEDEFAULT, 800, 600, NULL, NULL, GetModuleHandle(NULL), NULL);
	MSG msg;
	while (GetMessage(&msg, NULL, 0, 0) > 0) {
		TranslateMessage(&msg);
		DispatchMessage(&msg);
	}
	return 0;
}

 

薛乘志在2022-06-26 20:10:08追加了内容

标题**了,处理下

薛乘志在2022-07-05 14:19:51追加了内容

编译不过的话,在 Dev-C++ -> 工具 -> 编译器选项 -> 编译时加入以下参数 内加上内容:-lgdi32


0
已采纳
被禁言 刘宇航
刘宇航
修练者
修练者

还是不行

刘宇航在2022-07-05 14:44:58追加了内容

OK了,刚才输错了

1
0
陈慕嘉
陈慕嘉
初级光能
初级光能

额,好像DEV和CodeBlocks不行,VC可以

0
0
0
0
0
0
0
王牌工作室官方
王牌工作室官方
新手光能
新手光能

不对,

应该这么写

HDC hdc=GetDC(hwnd)
//Paint
ReleaseDC(hwnd,hdc);

 

0
0
王牌工作室官方
王牌工作室官方
新手光能
新手光能

还有,Win32程序执行入口不是main(int,char**)而是WinMain(HINSTANCE,HINSTANCE,LPCSTR,INT)

0
王牌工作室官方
王牌工作室官方
新手光能
新手光能

看看这个

选段:

BeginPaint() 和EndPaint() 可以删除消息队列中的WM_PAINT消息,并使无效区域有效。 
GetDC()和ReleaseDC()并不删除也不能使无效区域有效,因此当程序跳出 WM_PAINT 时 ,无效区域仍然存在。**就回不断发送WM_PAINT消息,于是程序不断处理WM_PAINT消息。

BeginPaint、EndPaint会告诉GDI内部,这个窗口需要重画的地方已经重画了,这样 WM_PAINT处理完返回给**后,**不会再重发WM_PAINT,而GetDC没有告诉**这个窗口需要重画的地方已经画过,在你把程序返回给** 后,**一直以为通知你的重画命令你还没有乖乖的执行或者执行出错,所以在消息空闲时,它还会不断地发WM_PAINT催促你画,导致程序卡**。

 

王牌工作室官方在2022-06-25 21:04:23追加了内容

@这个

0
王牌工作室官方
王牌工作室官方
新手光能
新手光能

好家伙,绘图进阶那一课也好简单(咦,双缓冲是个甚)

0
0
我要回答