news 2026/9/4 7:36:12

Windows GDI编程实战:在屏幕上绘制自定义透明光标

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Windows GDI编程实战:在屏幕上绘制自定义透明光标

你是不是也遇到过这样的需求:在开发屏幕录制软件、远程桌面工具,或者做自动化测试时,需要模拟一个鼠标光标在屏幕上移动?更具体一点,你可能想测试一个“找不同”游戏的点击反馈,或者模拟一个演示视频中的鼠标轨迹,但又不希望真的去移动物理鼠标。

传统的做法可能是截图后PS,或者用复杂的自动化框架。但今天要聊的,是一个更底层、更直接、也更有趣的技术方案:使用 Windows 的 GDI(图形设备接口)直接在屏幕上“画”出一个鼠标光标

这听起来有点“黑客”,但它的原理并不复杂。GDI 作为 Windows 图形显示的核心,允许我们直接与屏幕的像素“对话”。通过它,我们可以在任意窗口、甚至整个桌面的任意坐标,绘制出我们想要的任何图形——当然也包括一个逼真的鼠标箭头。

这篇文章,我们就来彻底搞懂这个技术。我会带你从 GDI 的基础概念入手,一步步拆解如何在屏幕上定位、绘制一个光标,并处理最关键的问题:如何让这个“画”出来的光标,不影响你真正的鼠标操作?最后,我会提供一个完整的、可运行的 C++ 示例代码,以及你可能遇到的所有坑和解决方案。

无论你是对 Windows 图形编程感兴趣,还是有具体的模拟光标需求,这篇文章都能让你从原理到实践,一次掌握。

1. 这篇文章真正要解决的问题

在开始敲代码之前,我们必须先想清楚:为什么不用SetCursorPos这样的 API 来移动真实光标,而非要“画”一个假的?

这背后是几个非常实际的开发痛点:

  1. 无干扰模拟:很多自动化测试或演示场景中,你需要展示光标的移动轨迹,但又不希望这个模拟光标接管用户的真实输入。真实的SetCursorPos会立刻把物理光标跳过去,影响用户当前的操作。
  2. 自定义光标样式:系统光标就那么几种。如果你想画一个红色的圆圈、一个自定义的图标来作为“高亮提示”或“特殊标记”,系统 API 就无能为力了。
  3. 跨窗口与顶层绘制:有时你需要一个始终显示在最前面的光标指示器,比如远程协助时的“激光笔”效果。这需要能在所有窗口之上进行绘制。
  4. 性能与精准控制:直接操作像素,你可以精确控制光标的每一帧出现、消失、淡入淡出,实现更流畅的动画效果,这是单纯移动系统光标做不到的。

所以,“画光标”技术的核心价值,在于它提供了一种与系统光标解耦的、高度可定制的视觉模拟方案。它特别适合屏幕标注、录制软件的光标高亮、自动化测试的可视化反馈以及一些特殊的 UI 交互演示。

2. 基础概念与核心原理

2.1 什么是 GDI?

GDI (Graphics Device Interface),即图形设备接口,是 Windows 操作系统早期用于图形输出的核心子系统。你可以把它理解成 Windows 给程序员提供的一套“画笔”和“画布”。通过 GDI,应用程序可以在屏幕、打印机或其他输出设备上绘制线条、形状、文本和位图。

虽然现代应用更多使用 DirectX 或 GDI+ 来获得更好的性能与效果,但 GDI 因其简单、直接、无需复杂依赖的特性,在完成一些基础的、对性能不敏感的屏幕绘图任务时,依然非常高效和实用。

2.2 关键概念:设备上下文 (DC)

要使用 GDI 绘图,你必须先获取一个设备上下文 (Device Context, DC)。DC 是一个抽象的数据结构,它代表了你可以绘制的目标(如窗口、内存、打印机),并包含了当前绘图的所有状态信息,比如选择的画笔、画刷、字体等。

对于我们的目标——在屏幕上画光标,我们需要获取的是整个屏幕的 DC,这样我们才能在任意位置绘图。

2.3 如何“画”出一个光标?

系统光标本质上就是一个带有透明通道的小位图。我们要模拟它,思路很清晰:

  1. 获取屏幕DC:拿到整块屏幕的“画布”。
  2. 准备光标图像:在内存中创建一个和光标外观一样的位图(比如一个箭头)。
  3. 处理透明效果:光标不是实心的方块,箭头周围是透明的。这需要用到 GDI 的掩码位图技术来实现透明或异或绘制。
  4. 在指定坐标绘制:将准备好的光标图像,“贴”到屏幕DC的指定坐标上。
  5. 管理绘制生命周期:画上去之后,还需要能在适当的时候把它擦除,否则它会永远留在屏幕上。

2.4 与“Kali没有鼠标光标”问题的区别

网络热词“kali没有鼠标光标”通常指的是在 Kali Linux 等虚拟化或特殊显示环境中,由于显卡驱动、桌面环境或虚拟机工具问题,导致的系统光标完全消失的故障。这是一个系统级别的显示问题。

而我们今天讨论的,是在系统光标正常显示的前提下,主动地、编程式地额外绘制一个光标图像。这是应用层的、可控的图形叠加,而非系统故障。两者有本质区别。

3. 环境准备与前置条件

要实践本教程,你需要准备以下环境:

  1. 操作系统:Windows 7 及以上版本(推荐 Windows 10/11)。GDI 是 Windows 核心组件,无需额外安装。
  2. 开发环境
    • 编译器:支持 C++ 的编译器,如 Microsoft Visual Studio 的 MSVC,或 MinGW。
    • IDE (可选但推荐):Visual Studio 2019/2022。社区版免费,对 Windows 开发支持最好。
    • 项目类型:一个简单的Win32 控制台应用程序桌面应用程序即可。我们不需要复杂的 MFC 或 .NET 框架。
  3. 知识准备
    • 基础的 C/C++ 语法。
    • 对 Windows 编程有基本了解(知道什么是HWND,HDC, 消息循环等概念会有帮助,但文中会解释)。
  4. 权限:该程序在运行时,会尝试在屏幕最顶层绘图。某些安全软件或系统设置可能会拦截此类操作。如果遇到问题,可以尝试以管理员身份运行程序,或检查安全软件设置。

4. 核心流程拆解

让我们把“在随机位置画光标”这个目标,拆解成几个可执行的步骤:

4.1 第一步:获取整个屏幕的设备上下文 (DC)

这是所有屏幕绘图操作的起点。没有 DC,我们就没有“画布”。

4.2 第二步:创建或加载光标位图

我们需要一个图像来表示光标。最简单的方法是使用 GDI 函数创建一个简单的箭头形状。更高级的做法是加载一个.cur.ico光标文件,或者一个带透明度的 PNG 位图(需要 GDI+)。

4.3 第三步:实现透明绘制

这是最关键也最容易出错的一步。我们不能简单地把一个白色背景的箭头位图画上去。我们需要:

  1. 创建一个单色掩码位图,用来定义哪些像素是透明的(白色),哪些是不透明的(黑色)。
  2. 使用BitBlt函数配合SRCANDSRCPAINT光栅操作码,分两步将光标与屏幕背景进行合成,从而实现透明效果。

4.4 第四步:在随机坐标进行绘制

生成屏幕范围内的随机坐标,将上一步准备好的光标绘制到该位置。

4.5 第五步:擦除光标

由于我们是直接修改屏幕像素,这个“画”上去的光标不会自动消失。我们必须手动记录上次绘制的位置,并在下一次绘制前,或在程序退出时,将那个区域的屏幕内容恢复原样。

4.6 第六步:循环与退出

为了实现动画(比如让光标随机移动),我们需要一个循环。同时,必须提供清晰的退出机制(如按ESC键),并确保在退出前清理所有资源(DC、位图等)。

5. 完整示例与代码实现

下面是一个完整的、注释详细的 C++ 实现。它创建了一个简单的箭头光标,并在屏幕上随机位置绘制,模拟光标移动的效果。

// 文件名:DrawCursor.cpp // 编译:在Visual Studio中创建Win32控制台项目,或使用 g++ -o DrawCursor.exe DrawCursor.cpp -lgdi32 #include <windows.h> #include <iostream> #include <cstdlib> #include <ctime> // 函数声明 void DrawCursor(HDC hdcScreen, int x, int y, HDC hdcMem, HBITMAP hbmMask, HBITMAP hbmCursor, int width, int height); void CleanupResources(HDC hdcScreen, HDC hdcMem, HBITMAP hbmOld, HBITMAP hbmMask, HBITMAP hbmCursor); int main() { // 初始化随机数种子 std::srand(static_cast<unsigned int>(std::time(nullptr))); // 1. 获取屏幕尺寸和整个屏幕的设备上下文(DC) int screenWidth = GetSystemMetrics(SM_CXSCREEN); int screenHeight = GetSystemMetrics(SM_CYSCREEN); HDC hdcScreen = GetDC(nullptr); // nullptr 表示获取整个屏幕的DC if (!hdcScreen) { std::cerr << "无法获取屏幕设备上下文!" << std::endl; return 1; } // 2. 创建内存DC,用于准备光标图像 HDC hdcMem = CreateCompatibleDC(hdcScreen); if (!hdcMem) { std::cerr << "无法创建内存设备上下文!" << std::endl; ReleaseDC(nullptr, hdcScreen); return 1; } // 定义我们自定义光标的尺寸 const int cursorWidth = 32; const int cursorHeight = 32; // 3. 创建单色掩码位图 (1位深度,黑白) // 掩码位图:黑色部分(0)表示不透明,白色部分(1)表示透明 HBITMAP hbmMask = CreateBitmap(cursorWidth, cursorHeight, 1, 1, nullptr); // 4. 创建彩色光标位图 (24位或32位深度) HBITMAP hbmCursor = CreateCompatibleBitmap(hdcScreen, cursorWidth, cursorHeight); if (!hbmMask || !hbmCursor) { std::cerr << "无法创建位图!" << std::endl; CleanupResources(hdcScreen, hdcMem, nullptr, hbmMask, hbmCursor); return 1; } // 5. 为内存DC选择我们创建的彩色光标位图,以便在上面绘图 HBITMAP hbmOld = (HBITMAP)SelectObject(hdcMem, hbmCursor); // 6. 绘制一个简单的箭头到 hbmCursor (彩色位图) // 使用一个黑色箭头和白色背景(注意,这个白色背景之后会被掩码过滤掉) HBRUSH hBrushWhite = CreateSolidBrush(RGB(255, 255, 255)); // 白色画刷填充背景 HBRUSH hBrushBlack = CreateSolidBrush(RGB(0, 0, 0)); // 黑色画刷绘制箭头 HPEN hPenBlack = CreatePen(PS_SOLID, 2, RGB(0, 0, 0)); // 黑色画笔 SelectObject(hdcMem, hBrushWhite); SelectObject(hdcMem, hPenBlack); // 填充整个位图为白色(将成为透明区域) Rectangle(hdcMem, 0, 0, cursorWidth, cursorHeight); // 绘制一个简单的黑色箭头(不透明部分) SelectObject(hdcMem, hBrushBlack); POINT arrowPoints[] = { {5, 5}, {25, 16}, {10, 16}, {10, 27}, {5, 27} }; Polygon(hdcMem, arrowPoints, 5); // 7. 创建并绘制掩码位图 // 我们需要另一个内存DC来处理掩码 HDC hdcMask = CreateCompatibleDC(hdcScreen); SelectObject(hdcMask, hbmMask); // 将掩码位图全部初始化为白色(1,代表透明) PatBlt(hdcMask, 0, 0, cursorWidth, cursorHeight, WHITENESS); // 然后,在箭头形状对应的区域画上黑色(0,代表不透明) SelectObject(hdcMask, GetStockObject(BLACK_BRUSH)); Polygon(hdcMask, arrowPoints, 5); // 绘制相同的箭头形状到掩码 // 清理临时对象 DeleteDC(hdcMask); DeleteObject(hBrushWhite); DeleteObject(hBrushBlack); DeleteObject(hPenBlack); // 8. 主循环:在随机位置绘制光标 int oldX = -1, oldY = -1; // 记录上一次绘制的位置,用于擦除 std::cout << "正在屏幕上随机绘制光标... 按 ESC 键退出。" << std::endl; while (true) { // 生成新的随机位置(确保光标完全在屏幕内) int newX = std::rand() % (screenWidth - cursorWidth); int newY = std::rand() % (screenHeight - cursorHeight); // 如果这不是第一次绘制,需要先擦除旧光标 // 注意:这是一个简单的演示。复杂的场景需要保存被覆盖的屏幕区域。 // 这里我们只是重新绘制整个桌面(通过系统),但这不是好方法。 // 更优方案是保存绘制前的屏幕块,擦除时恢复。为了简化,此处略过。 // 提示:可以调用 InvalidateRect(nullptr, nullptr, TRUE) 触发桌面重画来“擦除”,但效率低。 // 本示例专注于“绘制”本身,擦除逻辑需根据实际应用设计。 // 调用绘制函数 DrawCursor(hdcScreen, newX, newY, hdcMem, hbmMask, hbmCursor, cursorWidth, cursorHeight); // 更新旧位置 oldX = newX; oldY = newY; // 延迟一段时间,模拟光标移动 Sleep(500); // 暂停500毫秒 // 检查用户是否按下了ESC键 if (GetAsyncKeyState(VK_ESCAPE) & 0x8000) { std::cout << "ESC键按下,退出程序。" << std::endl; break; } } // 9. 程序结束前,清理资源 // 重要:在退出前,最好强制刷新一下绘制区域,或者恢复原貌。 // 由于我们的绘制是持久的,简单的办法是触发一个桌面重绘。 // 生产代码应实现正确的擦除。 InvalidateRect(nullptr, nullptr, TRUE); UpdateWindow(GetDesktopWindow()); CleanupResources(hdcScreen, hdcMem, hbmOld, hbmMask, hbmCursor); return 0; } /** * 在屏幕指定位置绘制透明光标。 * @param hdcScreen 屏幕设备上下文 * @param x, y 绘制位置的左上角坐标 * @param hdcMem 内存设备上下文(已选入彩色光标位图) * @param hbmMask 单色掩码位图句柄 * @param hbmCursor 彩色光标位图句柄 * @param width, height 光标位图的宽高 */ void DrawCursor(HDC hdcScreen, int x, int y, HDC hdcMem, HBITMAP hbmMask, HBITMAP hbmCursor, int width, int height) { // 第一步:使用掩码位图 (AND 操作) // 将屏幕目标区域中,对应掩码黑色(0)的部分置为黑色。 // 这为后续的彩色绘制准备好了“底子”。 HDC hdcMaskTemp = CreateCompatibleDC(hdcScreen); SelectObject(hdcMaskTemp, hbmMask); BitBlt(hdcScreen, x, y, width, height, hdcMaskTemp, 0, 0, SRCAND); DeleteDC(hdcMaskTemp); // 第二步:使用彩色光标位图 (OR 操作) // 将彩色光标图像绘制到刚刚处理过的屏幕区域。 // 因为上一步已将不透明区域变为黑色(0),此时彩色图像与之进行 OR 操作, // 黑色部分会直接显示彩色,而原本屏幕上的内容在透明区域(白色掩码)得以保留。 BitBlt(hdcScreen, x, y, width, height, hdcMem, 0, 0, SRCPAINT); } /** * 清理所有创建的GDI对象,防止资源泄漏。 */ void CleanupResources(HDC hdcScreen, HDC hdcMem, HBITMAP hbmOld, HBITMAP hbmMask, HBITMAP hbmCursor) { if (hdcMem) { if (hbmOld) { SelectObject(hdcMem, hbmOld); // 将旧位图选回DC } DeleteDC(hdcMem); } if (hbmCursor) { DeleteObject(hbmCursor); } if (hbmMask) { DeleteObject(hbmMask); } if (hdcScreen) { ReleaseDC(nullptr, hdcScreen); // 释放屏幕DC } }

关键逻辑解释:

  1. 资源获取与创建:程序首先获取屏幕DC,并创建了兼容的内存DC、一个彩色位图(hbmCursor)和一个单色掩码位图(hbmMask)。
  2. 绘制光标形状:在彩色位图上,我们画了一个白色背景上的黑色箭头。在掩码位图上,我们在箭头对应的区域画了黑色,其余部分是白色。
  3. 透明绘制算法 (DrawCursor函数)
    • SRCAND操作:用掩码图(黑白)与屏幕目标区域进行“与”操作。白色(1)与任何值相“与”结果不变(保留屏幕原色),黑色(0)与任何值相“与”结果为0(变成黑色)。这样,屏幕上的箭头区域变成了黑色背景。
    • SRCPAINT操作:将彩色光标图与上一步处理后的屏幕区域进行“或”操作。黑色(0)与彩色值相“或”,结果就是彩色值本身,从而显示出箭头。而原屏幕区域(对应掩码白色部分)与彩色位图的白色(255,255,255)相“或”,结果仍是原屏幕颜色,实现了透明。
  4. 循环与交互:主循环不断生成随机坐标并调用DrawCursor。通过GetAsyncKeyState检测 ESC 键来退出循环。
  5. 资源清理CleanupResources函数确保所有 GDI 对象都被正确删除,这是 Windows 编程中防止资源泄漏的关键步骤。

6. 运行结果与效果验证

  1. 编译与运行:将上述代码保存为DrawCursor.cpp,在 Visual Studio 中编译运行,或使用命令行cl DrawCursor.cpp user32.lib gdi32.lib(MSVC) 或g++ -o DrawCursor.exe DrawCursor.cpp -lgdi32(MinGW) 进行编译。
  2. 预期效果:程序运行后,你会看到一个黑色的箭头光标(大约32x32像素)每隔0.5秒在屏幕的随机位置闪烁出现。注意:这个光标是“画”在屏幕上的,你的真实鼠标光标仍然可以自由移动,两者互不影响。
  3. 验证成功
    • 观察屏幕,能看到一个独立于真实光标移动的箭头。
    • 移动你的真实鼠标,画上去的箭头不会跟随。
    • 打开一个文本编辑器或其他窗口,画上去的箭头会覆盖在窗口之上。
  4. 退出程序:按下键盘上的ESC键,程序会退出。由于示例中简单的擦除机制(InvalidateRect),画上去的光标痕迹可能会立即消失,也可能需要你最小化/移动一下窗口才会被系统重绘覆盖。

如果运行失败,第一步应该看哪里?

  • 检查编译器是否链接了gdi32.libuser32.lib
  • 如果出现“无法获取屏幕设备上下文”等错误,尝试以管理员身份重新运行程序。
  • 某些安全软件(如某些杀毒软件或屏幕录制保护)可能会阻止程序直接向屏幕绘图,暂时禁用它们再试。

7. 常见问题与排查思路

问题现象可能原因排查方式解决方案
编译错误:undefined reference to ‘BitBlt’没有链接 GDI32 库检查编译命令,确保包含了-lgdi32(GCC) 或gdi32.lib(MSVC)。在编译参数或IDE的项目属性中添加对应的库依赖。
程序运行后无任何显示1. 坐标计算错误,画在了屏幕外。
2. 安全软件拦截。
3. 透明绘制逻辑错误,光标全透明。
1. 打印生成的随机坐标(newX, newY)到控制台,检查是否在屏幕范围内。
2. 暂时关闭安全软件测试。
3. 简化测试:先不用掩码,用SRCCOPY直接绘制彩色位图,看是否有方框出现。
1. 确保坐标计算正确:newX = rand() % (screenWidth - width)
2. 以管理员身份运行,或将程序加入安全软件白名单。
3. 逐步调试DrawCursor函数,检查掩码位图是否创建成功(黑白分明)。
光标有白色背景(不透明)掩码位图创建或绘制失败,导致透明区域未生效。检查hbmMask是否创建成功。检查PatBltPolygon绘制掩码的代码。确保掩码中箭头区域为黑(0),其余为白(1)。使用GetObject检查位图信息。考虑将掩码位图保存为文件进行调试。
光标绘制后留下残影,无法擦除没有实现正确的擦除逻辑。示例中的InvalidateRect是全局重绘,可能不及时。观察残影是永久性的,还是被后续窗口刷新覆盖。实现局部擦除:在绘制新光标前,将旧位置区域的屏幕内容恢复。这需要你在第一次绘制前,用BitBlt将屏幕内容保存到一个位图中,擦除时再BitBlt回去。
程序崩溃或资源泄漏GDI 对象未正确删除。检查CleanupResources函数是否在所有退出路径都被调用。使用任务管理器观察程序运行后GDI对象数量是否持续增长。确保每个CreateCompatibleDC,CreateBitmap,CreateSolidBrush等都有对应的DeleteDCDeleteObject。使用RAII(资源获取即初始化) 思想封装GDI对象是更好的实践。
绘制效率低,闪烁严重在循环中频繁获取/释放DC,或绘制区域过大。分析循环中的代码,看是否有不必要的资源创建/销毁。1. 将GetDC(nullptr)CreateCompatibleDC移到循环外。
2. 使用双缓冲技术:在内存中完成所有绘制,最后一次性BitBlt到屏幕。
3. 只绘制变化的区域。

8. 最佳实践与工程建议

将技术 demo 转化为稳定可用的功能,需要注意以下几点:

  1. 封装与抽象:将光标绘制、擦除、位置管理等功能封装成一个类(如SimulatedCursor)。对外提供Show()Hide()MoveTo(x, y)SetCursorImage()等接口。
  2. 高效的擦除机制:这是核心挑战。推荐的做法是:
    • 保存背景:在首次绘制到位置(x,y)前,使用BitBlt将屏幕对应矩形区域的内容保存到一个独立的HBITMAP中。
    • 恢复背景:需要擦除时,再将保存的位图BitBlt回屏幕的相同位置。
    • 顺序:先擦除旧光标(恢复背景),再在新位置绘制新光标并保存新背景。
  3. 支持多种光标图像:不要硬编码箭头形状。可以设计从文件(.cur,.ico,.png)加载光标图像的函数。对于带Alpha通道的PNG,可以考虑使用GDI+,它原生支持Alpha混合,绘制更简单美观。
    // 伪代码:使用GDI+加载PNG并绘制 #include <gdiplus.h> Gdiplus::Bitmap bitmap(L"cursor.png"); Gdiplus::Graphics graphics(hdcScreen); graphics.DrawImage(&bitmap, x, y); // 自动处理透明度
  4. 线程安全与消息循环:如果你的程序有窗口消息循环(GetMessage/DispatchMessage),绘制操作最好放在WM_PAINT消息处理中,或者使用定时器(SetTimer)驱动。在非UI线程中直接操作屏幕DC需要谨慎,可能引发冲突。
  5. 性能优化
    • 限制绘制频率:对于平滑移动,使用高精度定时器(如timeSetEventCreateTimerQueueTimer)而非Sleep,并控制帧率(如30fps)。
    • 减少绘制区域:只更新光标所在的最小矩形区域。
    • 复用GDI对象:在程序生命周期内创建一次并重复使用,避免在循环内频繁创建/销毁。
  6. 错误处理:每一个 GDI API 调用后都应检查返回值(NULL,0,FALSE等),并给出有意义的错误信息或回退方案。
  7. 兼容性考虑:虽然 GDI 很古老,但在高DPI(缩放>100%)的屏幕上,你的光标可能会显得很小或模糊。可以通过GetDpiForWindowGetDeviceCaps(hdcScreen, LOGPIXELSX)获取DPI缩放比例,并对光标位图的尺寸进行相应缩放。
  8. 提供退出清理:除了ESC键,还应响应窗口关闭事件等,确保程序退出前一定能执行资源清理代码。

9. 总结与后续学习方向

通过本文,我们深入探讨了利用 Windows GDI 在屏幕任意位置绘制模拟光标的技术。从原理上,我们理解了通过屏幕DC内存DC掩码位图以及SRCAND/SRCPAINT光栅操作的配合,来实现透明叠加效果的核心流程。从实践上,我们获得了一个可以直接编译运行的、能够将自定义光标图案绘制在屏幕上的完整代码示例。

这项技术的真正威力,不在于“画一个光标”,而在于它揭示了 Windows 底层图形编程的一种思维模式:直接、精准地控制像素。掌握了它,你就能实现很多有趣的功能,比如屏幕涂鸦、实时标注、非侵入式的视觉反馈等。

然而,示例代码只是一个起点。要将其用于实际项目,你还需要攻克两个主要的工程难题:一是高效、无残留的擦除机制(通过保存与恢复背景实现),二是更丰富、更美观的光标资源加载(如通过 GDI+ 支持 PNG 透明度)。

如果你想继续深入,建议从以下方向拓展:

  • 深入 GDI/GDI+:学习更多关于画笔、画刷、路径、区域(Region)和坐标变换的知识。
  • 探索 DirectX/OpenGL:对于需要高性能、复杂动画(如游戏中的鼠标特效)的场景,这些现代图形API是更专业的选择。
  • 集成到具体应用:尝试将光标绘制功能封装成 DLL 或静态库,集成到你的屏幕录制软件、远程协助工具或自动化测试框架中。
  • 处理多显示器:使用EnumDisplayMonitorsAPI 来应对多显示器环境,确定光标应该绘制在哪块屏幕上。

希望这篇长文能帮你打开 Windows 图形编程的一扇窗。代码和思路已经给你了,剩下的就是动手实验和优化。如果在实践中遇到新的问题,欢迎在 CSDN 社区交流讨论。建议收藏本文,当你在未来需要实现类似“屏幕标记”功能时,这份详细的指南或许能派上用场。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/4 7:36:08

Faster-RCNN在PCB缺陷检测中的工业级应用与实战指南

简介&#xff1a;本资源是面向计算机视觉初学者与工业检测实践者的Faster-RCNN实战项目&#xff0c;聚焦PCB元器件缺陷&#xff08;如缺失、偏斜、短路等&#xff09;的自动识别与定位&#xff0c;适用于课程设计、毕业设计及产线质检算法预研。压缩包共79个文件&#xff0c;含…

作者头像 李华
网站建设 2026/9/4 7:35:41

MySQL 同步 ES 实战,肝到爆!

技术是什么&#xff1f;就是拿来玩的&#xff0c;边玩边学&#xff0c;才能成长得更快。之前已经给大家讲解了 MySQL 同步 ES 的几种方案&#xff0c;下面就教大家如何通过 Canal&#xff0c;将 MySQL 同步到 ES&#xff0c;文章内容绝对妥妥干货&#xff01;本文会先讲解需要用…

作者头像 李华
网站建设 2026/9/4 7:33:37

基于YOLOv8的溺水检测系统:从模型选型到工程落地的完整指南

简介&#xff1a;本资源是一套面向计算机视觉初学者与毕业设计学生的高精度溺水行为智能识别系统&#xff0c;基于YOLOv8深度学习框架构建&#xff0c;解决水域安全监控中人员异常状态实时判别难题&#xff0c;适用于智慧泳池、景区水域、校园人工湖等安防场景。压缩包共681个文…

作者头像 李华
网站建设 2026/9/4 7:33:07

基于FPGA与USB3.0的高速数据采集系统设计实战解析

简介&#xff1a;本资源是一套面向FPGA工程师与高速接口开发者的完整硬件设计与逻辑实现资料包&#xff0c;聚焦Cyclone III FPGA平台上的DDR2内存控制器与USB 3.0&#xff08;CYUSB3014&#xff09;协同开发实践&#xff0c;适用于数据采集、图像传输等高带宽嵌入式系统原型验…

作者头像 李华
网站建设 2026/9/4 7:32:34

STM32与MAX31865实现PT100高精度测温:从电路设计到软件算法的完整指南

简介&#xff1a;本资源是一套面向嵌入式开发工程师与STM32初学者的高精度PT100温度测量实战方案&#xff0c;聚焦工业级测温场景&#xff0c;解决铂电阻传感器信号采集、冷端补偿、线性化转换及MCU驱动集成等核心问题。压缩包共657个文件&#xff0c;涵盖149个.h头文件&#x…

作者头像 李华
网站建设 2026/9/4 7:32:00

从零构建皮肤病检测数据集:VOC格式标注与YOLO模型训练实战

简介&#xff1a;本资源是一份面向计算机视觉初学者与深度学习实践者的脸部皮肤病目标检测数据集&#xff0c;聚焦粉刺、丘疹、结节、脓疱四类常见痤疮病变识别任务&#xff0c;适用于YOLO系列模型训练及PASCAL VOC格式算法验证。压缩包共2000个文件&#xff0c;含1590张清晰原…

作者头像 李华