win32-显示窗口、消息循环、消息队列

承接上文:

  1. win32窗口编程
  2. windows 开发基础
  3. win32-注册窗口类、创建窗口

显示窗口 ShowWindow function (winuser.h)

在前文中介绍了创建窗口的过程,注册窗口类,创建窗口并返回一个窗口句柄 hWnd,这个窗口句柄 hWnd 可以在内存中找到一块保存着窗口各种信息的内存(大小、位置等信息),创建完窗口后并不能看见窗口,必须还要在屏幕上进行绘制,这时候需要调用 show(hWnd, SW_SHOW); 函数。第一个参数即窗口的句柄,第二个参数显示窗口的方式,它可以是由 WinMain() 窗口主函数传进来的参数 nCmdShow 的值,也可以是自己指定的值:

  • SW_SHOW :按照创建窗口时的参数原样显示。
  • SW_SHOWNORMAL :显示窗口并激活它,窗口的大小和位置由操作系统确定。
  • SW_SHOWMAXIMIZED :显示窗口并将其最大化。
  • SW_SHOWMINIMIZED :显示窗口并将其最小化。
  • … …

在显示窗口之后最好更新一下窗口 UpdateWindow(hWnd);


Sets the specified window’s show state.

BOOL ShowWindow(
  [in] HWND hWnd,   //A handle to the window.
  [in] int  nCmdShow
    /*
    Controls how the window is to be shown. This parameter is ignored the first time an application calls ShowWindow, 
    if the program that launched the application provides a STARTUPINFO structure. Otherwise, 
    the first time ShowWindow is called, the value should be the value obtained by the WinMain function in 
    its nCmdShow parameter. In subsequent calls, this parameter can be one of the following values.
    
       参数 | 数值 | 解释
    1. SW_HIDE | 0 | Hides the window and activates another window.
    2. SW_SHOWNORMAL 或 SW_NORMAL | 1 | Activates and displays a window. If the window is minimized, maximized, or arranged, the system restores it to its original size and position. An application should specify this flag when displaying the window for the first time. 
    3. SW_SHOWMINIMIZED | 2 | Activates the window and displays it as a minimized window.
    4. SW_SHOWMAXIMIZED 或 SW_MAXIMIZE | 3 | Activates the window and displays it as a maximized window.
    5. SW_SHOWNOACTIVATE | 4 | Displays a window in its most recent size and position. This value is similar to SW_SHOWNORMAL, except that the window is not activated.
    6. SW_SHOW | 5 | Activates the window and displays it in its current size and position.
    7. SW_MINIMIZE | 6 | Minimizes the specified window and activates the next top-level window in the Z order.
    8. SW_SHOWMINNOACTIVE | 7 | Displays the window as a minimized window. This value is similar to SW_SHOWMINIMIZED, except the window is not activated.
    9. SW_SHOWNA | 8 | Displays the window in its current size and position. This value is similar to SW_SHOW, except that the window is not activated.
    10. SW_RESTORE | 9 | Activates and displays the window. If the window is minimized, maximized, or arranged, the system restores it to its original size and position. An application should specify this flag when restoring a minimized window.
    11. SW_SHOWDEFAULT | 10 | Sets the show state based on the SW_ value specified in the STARTUPINFO structure passed to the CreateProcess function by the program that started the application.
    12. SW_FORCEMINIMIZE | 11 | Minimizes a window, even if the thread that owns the window is not responding. This flag should only be used when minimizing windows from a different thread. 
    */
);

消息循环

//消息循环
	MSG msg = { 0 };    //msg 结构体
	while ( GetMessage(&msg, NULL, 0, 0)) {       //GetMessage()专门负责抓取消息(从消息队列中抓取消息),抓到消息后返回值非零,消息被保存在msg结构体中,&msg引用,类似于指针
		TranslateMessage(&msg);     //翻译消息,可以区分大小写字母
		DispatchMessage(&msg);      //派发消息给消息处理函数进行处理,前面定义的 WndProc 函数。
	}

MSG 是一个结构体。
GetMessage() 到消息队列中抓取消息。
TranslateMessage() 翻译消息。
DispatchMessage() 派发消息给窗口的处理函数。

消息基础

消息的组成 (在 Windows 系统上)

  • 窗口句柄 hWnd:有了窗口句柄就可以知道该消息属于哪个窗口。
  • 消息ID:消息的ID用来唯一标识一个消息。
  • 消息的两个参数(两个附带信息):lParam, wParam 附带信息,不同类型的消息附带信息的含义有所不同。
  • 消息产生的时间
  • 消息产生时的鼠标位置

消息的作用:当系统通知窗口工作时,就采用消息的方式派发给窗口(窗口的消息处理函数 WndProc(), 每个窗口都应有一个窗口消息处理函数,默认处理函数 DefWindowProc() 参见前文)。

Windows系统上,消息是一个结构体,用来保存消息的各种信息
在这里插入图片描述

MSG 结构体的定义 MSG structure (winuser.h)

Contains message information from a thread’s message queue.

typedef struct tagMSG {
  HWND   hwnd;      //A handle to the window whose window procedure receives the message. This member is NULL when the message is a thread message.
  UINT   message;      //The message identifier. Applications can only use the low word; the high word is reserved by the system.
  WPARAM wParam;     //Additional information about the message. The exact meaning depends on the value of the message member.
  LPARAM lParam;     //Additional information about the message. The exact meaning depends on the value of the message member.
  DWORD  time;      //The time at which the message was posted.
  POINT  pt;        //The cursor position, in screen coordinates, when the message was posted.
  DWORD  lPrivate;
} MSG, *PMSG, *NPMSG, *LPMSG;

POINT structure 结构体 (windef.h)

在 MSG 结构体中封装了 POINT 结构体成员变量 pt,它用来保存鼠标位置坐标(x, y)。

The POINT structure defines the x- and y-coordinates of a point.

typedef struct tagPOINT {
  LONG x;     //Specifies the x-coordinate of the point.
  LONG y;     //Specifies the y-coordinate of the point.
} POINT, *PPOINT, *NPPOINT, *LPPOINT;


typedef struct _POINTL {
  LONG x;
  LONG y;
} POINTL, *PPOINTL;

The POINT structure is identical完全相同的 to the POINTL structure.

DispatchMessage() 如何将消息派发给窗口的处理函数

在 MSG 结构体中有一个成员变量 hWnd 窗口句柄,通过窗口句柄可以找到一块内存,内存中保存着各种数据(这些数据是窗口类和CreateWindow函数提供的),从这块窗口信息的内存中找到该窗口的消息处理函数 WndProc(),并调用该函数进行消息处理。WndProc() 函数是我们自己写的,所以也就回到了我们自己定义的消息处理代码中。窗口处理函数也可以指定为默认的 DefWindowProc(),它由微软提供,只是对于特定消息,它不能做出我们想要的处理效果。

派发消息伪代码:
在这里插入图片描述
调用 WndProc() 时传入了参数 msg.hWnd 窗口句柄,msg.message 消息ID(微软的消息ID总共有几千个,其本质是一个数字,用宏定义#define定义了别名,用来唯一标识一个消息),并传入了两个附带信息。

窗口处理函数

自定义的消息处理函数必须按照以下格式来:(函数名可以改,参数名可以改)

// 窗口过程函数
LRESULT CALLBACK WindowProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
{
    return DefWindowProc(hwnd, uMsg, wParam, lParam);     //返回默认的消息处理函数,对各种消息做默认的处理,该函数由微软提供
}

对于不同的消息,可以用 switch() 语句来做不同的处理:

switch (uMsg)
{
    case WM_SIZE: // Handle window resizing

    // etc
}

GetMessage() function (winuser.h)

GetMessage() 从消息队列中获取消息。

BOOL GetMessaeg(
	LPMSG lpMsg, //将获取到的消息放入MSG结构体中
	HWND hWnd, //抓取指定窗口句柄的消息
	UINT wMsgFilterMin,   //获取消息ID所在区间的下限
	UINT wMsgFilterMax    //获取消息ID所在区间的上限
	//后两个参数用来指定抓取消息ID的范围,如果两个参数都填 0,那么就是不限定消息ID范围
);

while ( GetMessage(&msg, NULL, 0, 0)) GetMessage() 函数的返回值作为 while 循环是否继续执行下去的条件,如果窗口关闭了但进程仍未结束意味着进程并没有退出 while 循环。注意 GetMessage() 函数只抓取本进程的消息,不会抓取其他进程的消息。

在这里插入图片描述
GetMessage() 函数的返回值:

  • 当抓到的消息不是 WM_QUIT 的时候,返回值为非零。
  • 当抓到的消息是 WM_QUIT 的时候,返回值为零。
  • 出错的时候返回值是 -1。

尽量不要使用 while (GetMessage())... ... 这种形式,用下面的形式:

BOOL bRet;

while ( (bRet = GetMessage(&msg, NULL, 0, 0)) != 0) 
{
	if (bRet == -1) 
	{
		//处理错误或者直接退出程序
	}
	else 
	{
		TranslateMessage(&msg);
		DispatchMessage(&msg);
	}
}

或者:

//message bump
	MSG msg;
	BOOL gResult;         // typedef int BOOL;
	while ((gResult = GetMessage(&msg, nullptr, 0, 0)) > 0)
	{
		TranslateMessage(&msg);
		DispatchMessage(&msg);
	}

	if (gResult == -1)
	{
		return -1;
	}
	else
	{
		return msg.wParam;    //此时 wParam 是 PostQuitMessage()的参数值
	}

之前的文章中提到,当窗口关闭时,进程仍然没有结束,需要添加以下代码:

LRESULT CALLBACK WndProc(HWND hWnd, UINT msgID, WPARAM wParam, LPARAM lParam)
{
	switch (msgID)
	{
	case WM_CLOSE:     //窗口关闭的消息
		PostQuitMessage(0);      //发送一个退出的消息到消息队列中
		//发送的这个消息可以使GetMessage()函数返回零,从而结束while循环,退出程序
		break;
	}
	return DefWindowProc(hWnd, msgID, wParam, lParam);
}

当窗口关闭的消息产生时,调用 PostQuitMessage(0);,它会往消息队列中发送一个 WM_QUIT 消息,当 GetMessage() 函数抓到 WM_QUIT 消息时返回值为0,从而结束 while 循环。

TranslateMessage() function

BOOL TranslateMessage( CONST MSG *lpMsg );

翻译消息,将按键消息,翻译成字符消息。

  • 检查消息是否为按键消息,如果不是按键消息,不做任何处理,继续执行。

TranslateMessage() 函数只翻译可见字符消息,像 a~z 这是可见字符消息,像 上下左右 箭头按键是不可见字符消息。因为字母有大小写之分,所以要 TranslateMessage() 翻译消息。游戏角色的 WASD 移动不需要翻译,打字输入文本需要区分大小写,需要翻译。

GetMessage() 函数抓取的消息非常多,包括鼠标消息等等,只有是键盘消息的情况下才会进行翻译。

常见消息

所有的消息都是使用 #define 进行定义的:
在这里插入图片描述
每个消息都有一个数值,0x 表示十六进制数(消息的数值ID)。

WM_DESTROY

产生时间,窗口被销毁时的消息(不等于窗口被关闭),一般用法(不是必须用法)常用于窗口被销毁前,做相应的善后处理,例如资源、内存等。

WM_CLOSE

窗口关闭按钮被点击时产生。

WM_SYSCOMMAND

产生时间:当点击窗口的最大化、最小化、关闭等时。

附带信息:

  • wParam:具体点击的位置,例如关闭SC_CLOSE等
  • lParam:鼠标光标的位置。lParam 是一个占 4个字节 的长整型数据,低16位保存 x 坐标,高16位保存 y 坐标。
    • LOWORD(lParam); //水平位置
    • HIWORD(lParam); //垂直位置

一般用法:常用在窗口关闭时,提示用户处理。

LRESULT CALLBACK WndProc(HWND hWnd, UINT msgID, WPARAM wParam, LPARAM lParam)
{
	switch (msgID)
	{
	case WM_CLOSE:
		PostQuitMessage(0);
		break;
	case WM_SYSCOMMAND:
		MessageBox(hWnd,"WM_SYSCOMMAND", "Infor", MB_OK);     //弹出提示框  
        break;
	}
	return DefWindowProc(hWnd, msgID, wParam, lParam);
}
case WM_CLOSE:
	PostQuitMessage(0);
	break;
case WM_SYSCOMMAND:
	if (wParam == SC_CLOSE)    //如果点击的是关闭按钮      
	{
		int nRet = MessageBox(hWnd, "是否退出?", "Infor", MB_YESNO);
		if (nRet == IDYES)
		{
			//什么代码都不写,最后 return DefWindowProc(),该函数会默认处理关闭,给到 WM_CLOSE case
		}
		else
			return 0;        //return 0;则不会执行最后的 return DefWindowProc()
	}
}

WM_CREATE

在窗口创建成功还未显示时,产生的消息。

附带信息:

  • wParam :为 0;
  • lParam :为 CREATESTRUCT 类型的指针,通过这个指针可以获取 CreateWindowEx 函数的全部 12 个参数的信息。

一本用法:常用于初始化窗口的参数、资源等等,包括创建子窗口等。

在创建窗口的时候,最后参数加一个附加消息:

char extraMsg[20] = "Extra Message";      
HWND hWnd = CreateWindowEx(
	0, pClassName, "My Window",
	WS_CAPTION | WS_MINIMIZEBOX | WS_SYSMENU | WS_OVERLAPPEDWINDOW,
	200, 200, 640, 480,        //窗口位置(200,200),大小长宽(640,480)。
	nullptr, nullptr, hIns, extraMsg     //传入一个附加消息
);

消息处理函数中加入对 WM_CREATE 消息的处理:

case WM_CREATE:
	OnCreate(hWnd, lParam);    //调用 OnCreate()函数
	break;

OnCreate 函数的内容如下:

void OnCreate(HWND hWnd, LPARAM lParam)
{
	CREATESTRUCT* ptrToStruct = (CREATESTRUCT*)lParam;     //将附加信息强制转换成 CREATESTRUCT 类型的指针
	char* msgContent = (char*)ptrToStruct->lpCreateParams;     //取出附加信息
	MessageBox(NULL, msgContent, "Infor", MB_OK);     //提示框显示附加信息
}

在这里插入图片描述
运行后,在窗口还没有显示之前,弹出了一个提示框,并将创建窗口时最后一个参数的附带信息显示了出来。

WM_SIZE

在窗口的大小发生变化的时候产生。

附加信息:

  • wParam :窗口大小发生变化的原因。
  • lParam :窗口变化后的大小。
    • LOWORD(lParam) //低16位表示变化后的宽度
    • HIWORD(lParam) //高16位表示变化后的高度

一般用法:常用于窗口的大小变化后,调整窗口各个部分的布局。

添加DOS窗口:
图形界面的窗口并不能让我们知道程序在运行过程中发生了什么,为了方便调试,可以为窗口添加一个DOS窗口,通过往DOS窗口中打印信息进行反馈从而知道程序运行过程中发生了什么。

//添加全局变量
HANDLE g_dos_output = 0;   //接收标准输出句柄

//WinMain()窗口主函数中添加以下两行代码:
AllocConsole();    //增加DOS窗口
g_dos_output = GetStdHandle(STD_OUTPUT_HANDLE);   //获得标准输出句柄

在这里插入图片描述
测试 WM_SIZE 消息

//switch语句中添加:
case WM_SIZE:
	OnSize(hWnd, lParam);
	break;

//OnSize()函数:
void OnSize(HWND hWnd, LPARAM lParam) {
	short width = LOWORD(lParam);    //低两个字节窗口宽度
	short hight = HIWORD(lParam);    //高两个字节窗口高度
	char text[256] = { 0 };
	sprintf_s(text, "WM_SIZE: 宽 %d,高 %d\n", width, hight);
	WriteConsole(g_dos_output, text, strlen(text), NULL, NULL);
}

//添加头文件
#include <stdio.h>

每次窗口发生变化的时候在DOS窗口中打印信息:
在这里插入图片描述

WM_QUIT

产生时间:程序员发送。

附带信息:

  • wParam :PostQuitMessage() 函数传递的参数。
  • lParam :0。

一般用法:用于结束消息循环,当 GetMessage() 抓到 WM_QUIT 消息后,返回 False , 结束 while 循环。

消息循环的原理

消息循环的阻塞 : GetMessage()函数 VS PeekMessage() 函数

  • GetMessage() 从系统获取消息,将消息从系统中移除,阻塞函数。当系统无消息的时候会等候消息。当消息队列中没有消息时,GetMessage() 函数会阻塞,也就意味着程序停在 GetMessage() 这里。只有有消息时才会继续执行下去。程序阻塞时不再参与 CPU 的调度,进入睡觉状态。如果消息队列中经常没有消息,意味着程序经常进入阻塞状态,这样的程序效率并不是很高。对于我们人的感觉来说,消息是每时每刻都有的,但对于高速运转的 CPU 来说,消息并不常有,意味着程序经常阻塞(对CPU来说)。
  • PeekMessage() 以查看的方式从系统中获取消息,可以不将消息从系统中移除,非阻塞函数。当系统无消息时,返回 False,继续执行后面的代码。
    • 函数定义:
    BOOL PeekMessage(
    	LPMSG lpMsg,      //MSG结构体指针
    	HWND hWnd,      //窗口句柄 handle to window
    	UINT wMsgFilterMin,      //消息ID的下限
    	UINT wMsgFilterMax,    //消息ID的上限
    	UINT wRemoveMsg,     //移除标识,是否从消息队列中移除消息
    	// PM_REMOVE / PM_NOREMOVE
    );
    

更加高效地抓取消息

... WinMain(...) {
	... ...
	MSG nMsg = { 0 };
	BOOL gResult;
	while (1)     //先进入死循环,循环体中进行判断是否退出循环
	{
		if (PeekMessage(&nMsg, NULL, 0, 0, PM_NOREMOVE))  //PeekMessage侦察兵侦察是否有消息,如果有消息
		{
			if ((gResult = GetMessage(&nMsg, NULL, 0, 0 )) > 0)  //gResult值大于零意味着GetMessage没有抓到 WM_QUIT(返回值为0) 也没有出错(出错返回值为-1)
			{
				TranslateMessage(&nMsg);
				DispatchMessage(&nMsg);
			}
			else      //如果抓到 WM_QUIT 或者 出错 退出while循环
			{
				break;
			}
		}
		else    //如果PeekMessage()没有侦察到消息,空闲处理
		{
			WriteConsole(g_dos_output, "No Message", strlen("No Message"), NULL, NULL);
		}
	}
	
	if (gResult == -1)   //GetMessage()出错返回值 -1
	{
		//错误处理或直接退出程序		
		return -1;
	}
	else   //抓到 WM_QUIT 消息,退出程序,返回 PostQuitMessage()的参数值
	{
		return nMsg.wParam;    //此时 wParam 是 PostQuitMessage()的参数值
	}
}

从DOS窗口打印的结果来看,进程绝大多数时间都处于没有消息的状态:

在这里插入图片描述

发送消息 : SendMessage()函数 VS PostMessage() 函数

消息如何产生的?
在这里插入图片描述
消息的产生:

  • 操作系统进程发送
  • 程序员编写的程序的进程发送

在Windows操作系统上,几乎所有的消息都是以下这两个函数发送的:

  • SendMessage() :发送消息,会等候消息的处理结果。
  • PostMessage() :投递消息,消息发出后立即返回,不等候消息执行结果。
BOOL SendMessage / PostMessage(
	HWND hWnd, //消息发送的目的窗口
	UINT Msg,   //消息ID
	WPARAM wParam,    //消息参数
	LPARAM lParam     //消息参数
);

包括程序退出发出 WM_QUIT 消息时调用的 PostQuitMessage(); 函数内部,也是调用了上面的 PostMessage() 函数。

case WM_CLOSE:    //窗口关闭
	//PostQuitMessage(0);
	PostMessage(hWnd, WM_QUIT, 0, 0);    //程序成功退出
	//SendMessage(hWnd, WM_QUIT, 0, 0);  //程序没有退出
	break;

这两个函数都是 Windows API 提供的函数,用于在应用程序之间或者在同一个应用程序的不同窗口之间发送消息。

  • SendMessage() 函数会同步发送消息,即函数调用会一直等待接收方处理完消息后才返回。这使得消息的发送和接收在时间上是连续的,适用于需要确保消息被及时处理的场景。

  • PostMessage() 函数则是异步发送消息,即函数调用后立即返回,不等待接收方处理完消息。这使得消息的发送和接收在时间上是分离的,适用于不需要立即响应的场景。

在这里插入图片描述
使用 SendMessage() 函数发送 WM_QUIT 消息,程序并没有成功退出,那么该函数把 WM_QUIT 消息发送到哪了呢?参见后文。

消息分类

  • 系统消息:微软官方定制好的消息,在操作系统内部,可以直接使用。ID范围:0 ~ 0x03FF
  • 用户自定义消息:如果系统消息中没有一个消息能够满足程序员自己的需求,那么我们可以自己定义消息。ID范围:0x400 ~ 0x7FFF(31743)。自定义消息宏:WM_USER。自己定义的消息需要自己发送并处理。
//自定义消息,添加宏定义
#define MY_MESSAGE WM_USER + n

n 取值 0 ~ 31743。

在这里插入图片描述

在这里插入图片描述

消息队列

  • 消息队列是用于存放消息的队列(数据结构)。
  • 消息在队列中先进先出(FIFO:First-In-First-Out)。
  • 所有的窗口程序都有消息队列。
  • 程序可以从队列中获取消息。

消息队列分类

  • 系统消息队列:由操作系统进行维护,存放系统产生的消息。系统的消息队列要保存所有进程产生的消息,所以它非常庞大。进程产生的消息首先要进系统的消息队列。
  • 程序消息队列:属于每一个应用程序(主线程)的消息队列。由应用程序(线程)维护。

在操作系统上可能同时运行着很多进程,各个进程的 GetMessage() 函数是到本进程的消息队列中抓取消息。绝大多数消息产生后先进系统消息队列,再由操作系统每隔一段时间转发到各个进程的消息队列中。

操作系统可以根据消息的 窗口句柄 hWnd 找到保存窗口数据的内存,在这块内存中有一个当前程序实例句柄 Instance,而当前程序的实例句柄 Instance 可以找到当前进程所占的一块内存,从而将每一个消息正确地转发给各个进程的消息队列。

消息和消息队列的关系

  • 消息和消息队列的关系
    1. 当鼠标、键盘等产生消息时,会将消息存放到系统消息队列。
    2. 系统会根据存放的消息,找到对应程序的消息队列。
    3. 将消息投递到程序的消息队列中。
  • 根据消息和消息队列之间的使用关系,将消息分类为:
    • 队列消息:消息的发送和获取,都是通过消息队列完成的。
    • 非队列消息:消息的发送和获取,是直接调用消息的窗口处理函数完成的。

在这里插入图片描述

PostMessage() 把消息发送到操作系统的消息队列中,由操作系统派发到进程的消息队列中。

SendMessage() 把消息直接发送给 WndProc() 进行处理并等待返回。

PostMessage() 发送的消息为 队列消息。常见队列消息:WM_PAINT、WM_QUIT(必须进队列)、键盘、鼠标、定时器等。

SendMessage() 发送的消息为 非队列消息。常见消息:WM_CREATE(必须不能进队列)、WM_SIZE(窗口第一次创建到显示时产生不进队列,后面可以进队列)等。

消息本身没有队列或非队列的属性。

深入 GetMessage() 函数

到本进程的消息队列中抓去本进程的消息。GetMessage() 函数做的事情如下:

  • 在程序(线程)消息队列中查找消息,如果队列有消息,检查消息是否满足指定条件(HWND, ID范围),不满足条件就不会取出消息,否则从消息队列中取出消息并返回。
  • 如果程序(线程)队列中没有消息,向操作系统消息队列要本程序的消息。如果系统队列中有消息属于本程序,系统会将消息转发到程序消息队列中。
  • 如果系统的消息队列中也没有属于本程序的消息,则检查当前进程的所有窗口的需要重新绘制的区域,如果发现有需要绘制的区域,产生 WM_PAINT 消息,取得消息返回处理。
  • 如果没有重新绘制的区域,检查定时器如果有到时的定时器,产生 WM_TIMER,返回处理执行。
  • 如果没有到时的定时器,整理程序的资源、内存等。
  • 如果以上条件都不满足,GetMessage() 才会进入阻塞状态。PeekMessage()会做以上同样的事情,但是不会阻塞等待消息,而是直接返回 False。


---------------------------------------------------------------------------- 后续更新,关注不迷路 ---------------------------------------------------------------

  • 18
    点赞
  • 22
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

Jackey_Song_Odd

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

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

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

打赏作者

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

抵扣说明:

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

余额充值