c++鼠标特效

使用c++实现一个鼠标点击特效

编写一个在后台获取鼠标点击位置的函数

要实现这个功能,可以注册一个鼠标点击事件的挂钩,使得鼠标点击可以触发程序内的函数。

参考:SetWindowsHookExA 函数 (winuser.h) - Win32 apps | Microsoft Learn

WH_KEYBOARD_LL 安装用于监视低级别键盘输入事件的挂钩过程

使用这个挂钩,可以获取鼠标点击的时机,同时不会影响游戏,任务管理器等不容干扰的程序内的点击。

代码:

image-20250126144540711

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
#include <windows.h>
#include <iostream>
using namespace::std;

POINT mouse_pos;

LRESULT CALLBACK MouseProc(int nCode, WPARAM wParam, LPARAM lParam)
{
if (nCode >= 0 && wParam == WM_LBUTTONDOWN)
{

GetCursorPos(&mouse_pos);
cout << "鼠标点击事件:(" << mouse_pos.x << ", " << mouse_pos.y << endl;
}
return CallNextHookEx(NULL, nCode, wParam, lParam);
}

int main(void)
{
SetWindowsHookEx(WH_MOUSE_LL, MouseProc, NULL, 0);
MSG msg;
while (GetMessage(&msg, nullptr, 0, 0))
{
//TranslateMessage(&msg);
//DispatchMessage(&msg);
Sleep(2000);
}
return 0;
}

创建一个全屏的透明窗口让鼠标穿透过去。

这个红色的区域是覆盖区域,将背景设置为透明键后就看不到了,并且鼠标是穿透的

看不到的时候别乱点!!!通过任务栏将这个窗口关掉,如果给覆盖完了,可以唤出开始菜单来关闭。)

如果已经将程序改为窗口应用而任务栏没有显示,那么需要一点点操作才能关掉:打开任务管理器,通过预览定位搜索框,搜索并结束这个进程。但是重启可以解决一切问题。

参考:CreateWindowExW 函数 (winuser.h) - Win32 apps | Microsoft Learn

创建具有扩展窗口样式的重叠、弹出窗口或子窗口;否则,此函数与 CreateWindow 函数相同。

image-20250126150224263

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
#include <windows.h>
#include <iostream>
using namespace::std;

HWND hwnd;

LRESULT CALLBACK WndProc(HWND hwnd, UINT message, WPARAM wparam, LPARAM lparam)
{
switch (message)
{
default:
return DefWindowProc(hwnd, message, wparam, lparam);
}
return 0;
}

void CreateFullScreenTransparentWindow(const wchar_t* window_name, const wchar_t* class_name)
{
WNDCLASS wc = {}; // 配置一个窗口类
wc.lpfnWndProc = WndProc; // 配置消息处理函数
wc.hInstance = GetModuleHandle(nullptr);
wc.lpszClassName = class_name;
wc.style = CS_HREDRAW | CS_VREDRAW;
wc.hbrBackground = (HBRUSH)CreateSolidBrush(RGB(255, 0, 0)); //设置背景色与透明键相同,实现透明
//wc.hbrBackground = (HBRUSH)CreateSolidBrush(RGB(254, 0, 0)); // 设置为非透明键来检验窗口是否覆盖全屏
if (!RegisterClass(&wc))
{
MessageBox(nullptr, L"窗口类注册失败", L"错误", MB_OK);
return;
}

// 创建窗口, 使用这里的透明是指鼠标事件穿透, 分层窗口,置顶窗口, 工具窗口和弹出窗口实现在任务栏不显示。
// 尺寸为获取屏幕尺寸, 减去一定区域给任务栏露出来,因为任务栏的鼠标悬停显示预览会被阻碍。
hwnd = CreateWindowExW(WS_EX_TRANSPARENT | WS_EX_LAYERED | WS_EX_TOPMOST | WS_EX_TOOLWINDOW, class_name, window_name, WS_POPUP,
0, 0, GetSystemMetrics(SM_CXSCREEN), GetSystemMetrics(SM_CYSCREEN) - 40,
nullptr, nullptr, GetModuleHandle(nullptr), nullptr);
if (hwnd == nullptr)
{
MessageBox(nullptr, L"窗口创建失败", L"错误", MB_OK);
return;
}
// 设置透明键
SetLayeredWindowAttributes(hwnd, RGB(255, 0, 0), 100, LWA_COLORKEY);
ShowWindow(hwnd, SW_SHOW);
UpdateWindow(hwnd);
}

int main(void)
{
CreateFullScreenTransparentWindow(L"透明窗口", L"transparent");

MSG msg;
while (GetMessage(&msg, nullptr, 0, 0))
{
TranslateMessage(&msg);
DispatchMessage(&msg);
Sleep(2000);
}
return 0;
}

绘制图形

可以用GDI+绘图,相对简单。

参考:绘制线条 - Win32 apps | Microsoft Learn

案例中的代码要用VisualStudio创建一个桌面应用来运行,但是桌面应用会创建一堆文件,可以先创建一个空项目,然后再项目属性中将链接器中的子系统选项设置为窗口应用,这样就可以得到一个干干净净的窗口应用环境了。

image-20250126151746509

需要注意的是,这个设置时对于每一个启动配置的,切换启动配置之后要记得修改,否则将会报错main函数未定义。

将案例代码复制,创建一个cpp文件,并粘贴代码,代码还不能运行,因为这个代码用了一个预编译头,而这个什么也没有的项目里没有,所以,直接将预编译头去掉就可以了:删除#include <stdafx.h>

image-20250126152224221

这段代码演示了绘制图形,但经过了不少麻烦事儿,咱只需要简单地话一些触手藤曼或者点点就好了。

绘图的核心就这几行:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// Initialize GDI+. 初始化GDI+ 初始化之后才能绘图。
GdiplusStartup(&gdiplusToken, &gdiplusStartupInput, NULL);
// 获取一个绘图设备上下文
hdc = BeginPaint(hWnd, &ps);
// 创建一个与设备上下文关联的图像类
Graphics graphics(hdc);
// 创建一个笔对象,用于指定颜色,宽度,线形,线帽(端点形状)
Pen pen(Color(255, 0, 0, 255));
// 绘制一根线
graphics.DrawLine(&pen, 0, 0, 200, 100);
// 释放设备
EndPaint(hWnd, &ps);
// 关闭GDI+
GdiplusShutdown(gdiplusToken);

咱不需要那么严谨也不需要设备上下文,再官网graphics类的介绍中可以找到还有一种可以直接关联图形类和窗口的构造函数,这样就可以直接再窗口上绘图了。

image-20250126153517679

我觉得初始化的时候传入的参数基本没什么用,那个用于关闭GDI的token,其实不关闭GDI+好像也没什么毛病,因为进程都结束了,资源会被回收的,windows的稳定性并不差,不至于一个GDI不关就G了。

所以,最终,绘图的步骤就是

1
2
3
4
5
6
7
8
9
10
11
// 初始化(只在程序开始执行一次)
GdiplusStartup(&gdiplusToken, &gdiplusStartupInput, NULL);
// 创建图形类(也只执行一次,创建后就一直用了)
Graphics graphics(hwnd);
// 创建笔(这个为了画各种图形就单独创建好了)
// 颜色的定义时Color(a,r,g,b),不透明度,红色,绿色,蓝色,第一个a通道不写就是普通的不透明颜色
// 但是这里的不透明度没有什么意义,它并不是说透过这个窗口
// 只是在当前程序绘图透明,也就是和前面设置的透明键颜色混在一起了
Pen pen(Color(0, 0, 255)); //所以直接用普通的颜色就好了,半透明效果我还没实现。
// 画线
graphics.DrawLine(&pen, 0, 0, 200, 100);// (0,0)到(200,100),原点在左上角,x+xiang'you

魔改后的绘图

image-20250126163302935

代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
#include <windows.h>
#include <objidl.h>
#include <gdiplus.h>
using namespace Gdiplus;
#pragma comment (lib,"Gdiplus.lib")

void createWindow(int iCmdShow, HINSTANCE hInstance);
LRESULT CALLBACK WndProc(HWND, UINT, WPARAM, LPARAM);

Graphics* graphics;
HWND hWnd;

INT WINAPI WinMain(HINSTANCE hInstance, HINSTANCE, PSTR, INT iCmdShow)
{
createWindow(iCmdShow, hInstance);

// 初始化
GdiplusStartupInput gdiplusStartupInput;
ULONG_PTR gdiplusToken;
GdiplusStartup(&gdiplusToken, &gdiplusStartupInput, NULL);
graphics = new Graphics(hWnd);

MSG msg;
// 创建笔
Pen pen(Color(0, 0, 255));
while (GetMessage(&msg, NULL, 0, 0))
{
TranslateMessage(&msg);
DispatchMessage(&msg);
//绘图
graphics->DrawLine(&pen, 0, 0, 200, 100);
Sleep(1);
}
return msg.wParam;
} // WinMain

LRESULT CALLBACK WndProc(HWND hWnd, UINT message,
WPARAM wParam, LPARAM lParam)
{
switch (message)
{
case WM_DESTROY:
PostQuitMessage(0);
return 0;
default:
return DefWindowProc(hWnd, message, wParam, lParam);
}
} // WndProc

void createWindow(int iCmdShow, HINSTANCE hInstance)
{
WNDCLASS wndClass;
wndClass.style = CS_HREDRAW | CS_VREDRAW;
wndClass.lpfnWndProc = WndProc;
wndClass.cbClsExtra = 0;
wndClass.cbWndExtra = 0;
wndClass.hInstance = hInstance;
wndClass.hIcon = LoadIcon(NULL, IDI_APPLICATION);
wndClass.hCursor = LoadCursor(NULL, IDC_ARROW);
wndClass.hbrBackground = (HBRUSH)GetStockObject(WHITE_BRUSH);
wndClass.lpszMenuName = NULL;
wndClass.lpszClassName = TEXT("GettingStarted");

RegisterClass(&wndClass);

hWnd = CreateWindow(
TEXT("GettingStarted"), // window class name
TEXT("Getting Started"), // window caption
WS_OVERLAPPEDWINDOW, // window style
CW_USEDEFAULT, // initial x position
CW_USEDEFAULT, // initial y position
CW_USEDEFAULT, // initial x size
CW_USEDEFAULT, // initial y size
NULL, // parent window handle
NULL, // window menu handle
hInstance, // program instance handle
NULL); // creation parameters

ShowWindow(hWnd, iCmdShow);
UpdateWindow(hWnd);
}

好了,可以开始画图了

这个是第一代图案,有点子碍眼。

image-20250126163938203

是画完了才开始擦除。这里的擦除其实就是再用透明键颜色把画过的地方覆盖了,为了保证擦干净就用了更粗的画笔。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
void vine(int x1, int y1, float anchor, float turn, int speed, int cost, Color color, int count, int clone)
{
if (speed <= 0) // 速度为零就是结束了
{
Sleep(300);
return;
}
count++; // 计数加一
int x2 = x1 + (speed)*cos(anchor); //计算下一个点的坐标
int y2 = y1 + (speed)*sin(anchor);
Pen penline(color, speed); // 创建和设置笔
penline.SetEndCap(LineCapRound);
penline.SetStartCap(LineCapRound);
mtx.lock(); // 加锁,不加的话会很卡并且线断断续续,因为graphics不能被两个线程同时调用。
graphics->DrawLine(&penline, x1, y1, x2, y2);
mtx.unlock();
Sleep(1);
if (clone > 0 && rand() % 10 == 0) // 克隆一个分支
{
int x_copy = x1;
int y_copy = y1;
float anchor_copy = anchor + rand() % 10 / 26.0; // 随机化下一个分支
float turn_copy = turn * (-1 + rand() % 2 * 2);
int speed_copy = speed + rand() % 3;
int cost_copy = cost + rand() % 3;
thread t(vine, x_copy, y_copy, anchor_copy, turn_copy, speed_copy, cost_copy, Color(rand() % 50, 100 + rand() % 100, 20 + rand() % 50), 0, --clone);
t.detach();
}
if (count % 10 == 0)
speed -= cost;
anchor += turn;
if (count % 3 == 0)
turn += turn / 2;
if (turn > 0.6)
speed = 0;
vine(x2, y2, anchor, turn, speed, cost, color, count, clone); // 递推下一个绘制过程。
Sleep(count / 3); // 全部画完后等待并擦除回去,这样做的效果是成出去再收回来。
Pen erase(Color(255, 0, 0), speed * 2 + 3); // 用透明键擦除。
erase.SetEndCap(LineCapRound);
erase.SetStartCap(LineCapRound);
mtx.lock();
graphics->DrawLine(&erase, x1, y1, x2, y2);
mtx.unlock();
}

我觉得好看的版本(图是过去的参数,代码里的参数画出来效果有点不一样,会大一点点):

代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
#include <windows.h>
#include <gdiplus.h>
#include <vector>
#include <thread>
#include <mutex>
#include <queue>
using namespace Gdiplus;
using namespace std;
#pragma comment (lib,"Gdiplus.lib")

LRESULT CALLBACK MouseProc(int nCode, WPARAM wParam, LPARAM lParam);

Gdiplus::Graphics* graphics;
ULONG_PTR gdiplusToken;
HWND hwnd;
HHOOK hook;
mutex mtx;

LRESULT CALLBACK WndProc(HWND hwnd, UINT message, WPARAM wparam, LPARAM lparam);
void CreateFullScreenTransparentWindow(const wchar_t* window_name, const wchar_t* class_name);
int g_hotkeyId = 1;
typedef struct {
int x1, y1, x2, y2;
int width;
int time;
}VineDraw;
queue<VineDraw> vines;

DWORD WINAPI eraser(LPVOID lpParam);
void vine2(int x1, int y1, float anchor, float turn, int speed, int cost, Color color, int count, int clone);

int WINAPI WinMain(HINSTANCE hThisInstance, HINSTANCE hPrevInstance, LPSTR lpszArgument, int nCmdShow)
{
HANDLE hMutex = CreateMutex(NULL, FALSE, L"cursorAnim");
if (hMutex == NULL) {
return 1;
}
if (GetLastError() == ERROR_ALREADY_EXISTS) {
MessageBox(nullptr, L"已运行", L"错误", MB_OK);
CloseHandle(hMutex);
return 1;
}

CreateFullScreenTransparentWindow(L"鼠标特效", L"cursor_anim");
hook = SetWindowsHookEx(WH_MOUSE_LL, MouseProc, NULL, 0);


// 创建线程
HANDLE hThread = CreateThread(NULL, 0, eraser, NULL, 0, NULL);
if (hThread == NULL) {
MessageBox(nullptr, L"eraser线程启动失败", L"错误", MB_OK);
CloseHandle(hMutex);
return 1;
}

// 设置线程优先级为低
SetThreadPriority(hThread, THREAD_PRIORITY_LOWEST);
SetPriorityClass(GetCurrentProcess(), IDLE_PRIORITY_CLASS);



MSG msg;
while (GetMessage(&msg, nullptr, 0, 0))
{
TranslateMessage(&msg);
DispatchMessage(&msg);
Sleep(3);
}
CloseHandle(hMutex);
// 等待线程结束
WaitForSingleObject(hThread, INFINITE);
// 关闭线程句柄
CloseHandle(hThread);
return 0;
}

void CreateFullScreenTransparentWindow(const wchar_t* window_name, const wchar_t* class_name)
{
WNDCLASS wc = {};
wc.lpfnWndProc = WndProc;
wc.hInstance = GetModuleHandle(nullptr);
wc.lpszClassName = class_name;
wc.style = CS_HREDRAW | CS_VREDRAW;
wc.hbrBackground = (HBRUSH)CreateSolidBrush(RGB(255, 0, 0));
if (!RegisterClass(&wc))
{
MessageBox(nullptr, L"窗口类注册失败", L"错误", MB_OK);
return;
}

hwnd = CreateWindowExW(WS_EX_TRANSPARENT | WS_EX_LAYERED | WS_EX_TOPMOST | WS_EX_TOOLWINDOW, class_name, window_name, WS_POPUP,
0, 0, GetSystemMetrics(SM_CXSCREEN), GetSystemMetrics(SM_CYSCREEN)-40,
nullptr, nullptr, GetModuleHandle(nullptr), nullptr);
if (hwnd == nullptr)
{
MessageBox(nullptr, L"窗口创建失败", L"错误", MB_OK);
return;
}

SetLayeredWindowAttributes(hwnd, RGB(255, 0, 0), 100, LWA_COLORKEY);
ShowWindow(hwnd, SW_SHOW);
UpdateWindow(hwnd);
}


LRESULT CALLBACK WndProc(HWND hwnd, UINT message, WPARAM wparam, LPARAM lparam)
{
switch (message)
{
case WM_CREATE:
{
Gdiplus::GdiplusStartupInput gdiplusStartupInput;
Gdiplus::GdiplusStartup(&gdiplusToken, &gdiplusStartupInput, NULL);
graphics = new Gdiplus::Graphics(hwnd);
break;
}
case WM_TIMER:
break;
case WM_DESTROY:
{
MessageBox(nullptr, L"关闭", L"关闭", MB_OK);
delete graphics;
UnregisterHotKey(hwnd, g_hotkeyId);
Gdiplus::GdiplusShutdown(gdiplusToken);
UnhookWindowsHookEx(hook);
PostQuitMessage(0);
break;
}
default:
return DefWindowProc(hwnd, message, wparam, lparam);
}
return 0;
}

void vine2(int x1, int y1, float anchor, float turn, int speed, int cost, Color color, int count, int clone)
{
if (speed <= 0)
{
Sleep(300);
return;
}
count++;
int x2 = x1 + (speed)*cos(anchor);
int y2 = y1 + (speed)*sin(anchor);
Pen penline(color, speed);
penline.SetEndCap(LineCapRound);
penline.SetStartCap(LineCapRound);
mtx.lock();
graphics->DrawLine(&penline, x1, y1, x2, y2);
vines.push(VineDraw{x1,y1,x2,y2,speed,10}); // 加入擦除队列
if (clone > 0 && rand() % 10 == 0)
{
thread t(vine2, x1, y1, anchor + rand() % 10 / 20.0, turn * (-1 + rand() % 2 * 2), speed + rand() % 2, cost, Color(rand() % 70, 115 + rand() % 3 * 70, 20 + rand() % 20), 0, --clone);
t.detach();
}
if (count % 9 == 0)
speed -= cost;
anchor += turn;
if (count % 3 == 0)
turn += turn / 2;
if (turn > 0.6)
speed = 0;
mtx.unlock();
Sleep(1);
vine2(x2, y2, anchor, turn, speed, cost, color, count, clone);
}

// 擦除线程
DWORD WINAPI eraser(LPVOID lpParam)
{
Pen erase(Color(255, 0, 0));
erase.SetEndCap(LineCapRound);
erase.SetStartCap(LineCapRound);
while(true)
{
if (!vines.empty())
{
vines.front().time--;
if (vines.front().time <= 0)
{
erase.SetWidth(vines.front().width * 2 + 2);
mtx.lock();
graphics->DrawLine(&erase, vines.front().x1, vines.front().y1, vines.front().x2, vines.front().y2);
vines.pop();
mtx.unlock();
}
}
else
{
Sleep(70); // 节省性能
}
}
}

//鼠标事件回调
LRESULT CALLBACK MouseProc(int nCode, WPARAM wParam, LPARAM lParam) {
if (nCode >= 0 && wParam == WM_LBUTTONDOWN) {
// 获取鼠标点击位置
POINT p;
GetCursorPos(&p);

float base = rand() % 10 / 3; // 随机化初始角度
thread t0(vine2, p.x, p.y, 0 + base, 0.02, 5, 2, Color(0, 255, 0), 0, 3);
t0.detach();
if (rand() % 3 != 0) // 随机化生成
{
thread t1(vine2, p.x, p.y, 2 + base, 0.010, 5, 2, Color(0, 255, 0), 0, 3);
t1.detach(); //每一个分支都是一个线程
}
if (rand() % 2 == 0)
{
thread t2(vine2, p.x, p.y, -2 + base, 0.025, 5, 2, Color(0, 255, 0), 0, 3);
t2.detach();
}
else
{
thread t3(vine2, p.x, p.y, -0.5 + base, 0.025, 5, 2, Color(0, 255, 0), 0, 3);
t3.detach();
thread t4(vine2, p.x, p.y, -3 + base, 0.025, 5, 2, Color(0, 255, 0), 0, 3);
t4.detach();
}
}
return CallNextHookEx(NULL, nCode, wParam, lParam);
}

结束