Initial commit: MouseHighlighter project

A lightweight Windows mouse highlight overlay tool with halo circle
and click ripple animations, built with C++17 and Win32 API.
This commit is contained in:
2026-05-27 16:46:07 +08:00
commit 326306d76a
12 changed files with 3578 additions and 0 deletions

109
include/Config.h Normal file
View File

@@ -0,0 +1,109 @@
#pragma once
#include <algorithm>
#include <cstdint>
#include <array>
/**
* @brief 应用全局配置
*
* 从 INI 文件加载,可通过托盘菜单修改
*/
struct Config {
// ========== 光晕圆形参数 ==========
struct Halo {
uint32_t colorARGB = 0x660099FF; // Alpha(102/255≈0.4) + 蓝紫色 ARGB
float radius = 30.0f; // 半径 (像素)
float thickness = 1.5f; // 线条厚度
uint16_t drawSteps = 128; // 多边形近似圆的段数
bool filled = false; // 是否填充 (true=实心圆, false=空心圆)
uint8_t qualityLevel = 2; // 光晕画质: 1=普通, 2=高清, 3=超清
} halo;
// ========== 波纹动画参数 ==========
struct Ripple {
bool enabled = true; // 是否启用波纹效果
uint32_t colorARGB = 0x3300FF99; // Alpha(51/255≈0.2) + 青绿色
float maxRadius = 120.0f; // 最大扩散半径
uint32_t durationMS = 240; // 动画持续时间 (毫秒)
float thickness = 2.5f; // 波纹线条厚度
uint8_t maxConcurrent = 12; // 最多并发波纹数
float alphaExponent = 3.0f; // 衰减阶次: alpha = (1-t)^n
float radiusExponent = 1.0f; // 扩展阶次: 线性 (1.0)
} ripple;
// ========== 坐标平滑参数 ==========
struct Smoothing {
float alpha = 0.0f; // EMA 系数 (0.0=不平滑, 推荐 0.25)
uint32_t minUpdateDistPx = 1; // 最小更新距离 (像素)
} smoothing;
// ========== 渲染节拍参数 ==========
struct Timing {
uint32_t targetFPS = 60; // 目标帧率
uint32_t updateIntervalMS = 17; // 更新间隔 (1000/60 ≈ 16.67ms, 上舍入)
uint32_t maxFrameTimeMS = 20; // 硬超时告警阈值
} timing;
// ========== 性能监控参数 ==========
struct Monitoring {
uint32_t checkIntervalMS = 5000; // 监控检查间隔
uint32_t memoryThresholdMB = 100; // 内存告警阈值
uint32_t gdiHandleThreshold = 3000; // GDI 对象告警
bool logToFile = false; // 是否输出调试日志
} monitoring;
// ========== 系统集成参数 ==========
struct System {
bool enableTrayIcon = true; // 启用托盘图标
bool autoStartup = false; // 开机自启 (Windows 注册表)
uint32_t memoryCheckIntervalMS = 5000; // 内存检查间隔
} system;
// ========== 方法 ==========
/**
* @brief 从 INI 文件加载配置
* @param path 文件路径 (宽字符)
* @return true 成功false 失败或文件不存在
*/
bool LoadFromINI(const wchar_t* path) noexcept;
/**
* @brief 保存配置到 INI 文件
* @param path 文件路径 (宽字符)
* @return true 成功false 失败
*/
bool SaveToINI(const wchar_t* path) const noexcept;
/**
* @brief 验证所有参数的有效范围,必要时钳制
*/
void Validate() noexcept {
// 光晕
halo.radius = std::min(100.0f, std::max(10.0f, halo.radius));
halo.thickness = std::min(5.0f, std::max(0.5f, halo.thickness));
halo.drawSteps = std::min<uint16_t>(256, std::max<uint16_t>(32, halo.drawSteps));
halo.qualityLevel = static_cast<uint8_t>(std::min(3u, std::max(1u, static_cast<unsigned>(halo.qualityLevel))));
// 波纹
ripple.maxRadius = std::min(320.0f, std::max(50.0f, ripple.maxRadius));
ripple.durationMS = std::min(3000u, std::max(100u, ripple.durationMS));
ripple.thickness = std::min(5.0f, std::max(0.5f, ripple.thickness));
ripple.maxConcurrent = std::min(32u, std::max(4u, (uint32_t)ripple.maxConcurrent));
// 平滑
smoothing.alpha = std::min(0.5f, std::max(0.0f, smoothing.alpha));
// 定时
timing.targetFPS = std::min(120u, std::max(30u, timing.targetFPS));
timing.updateIntervalMS = std::max(8u, (uint32_t)(1000 / timing.targetFPS));
}
};
// 获取默认配置
inline Config GetDefaultConfig() {
Config cfg;
cfg.Validate();
return cfg;
}

288
include/DataStructures.h Normal file
View File

@@ -0,0 +1,288 @@
#pragma once
#include <cstdint>
#include <wingdi.h>
#include <array>
#include <cmath>
/**
* @brief DIB (Device Independent Bitmap) 双缓冲管理
*
* 维护 ARGB32 位图,用于 UpdateLayeredWindow 输出
*/
class DIBBuffer {
public:
DIBBuffer() = default;
// 内存 DC & 位图
HDC hdcMem = nullptr;
HBITMAP hbmDIB = nullptr;
uint32_t* pBits = nullptr;
uint32_t width = 0;
uint32_t height = 0;
/**
* @brief 创建或重建 DIB 缓冲
* @param w 宽度 (像素)
* @param h 高度 (像素)
* @param hdcScreen 参考设备上下文,通常来自 GetDC(nullptr)
* @return true 成功false 失败
*/
bool Create(uint32_t w, uint32_t h, HDC hdcScreen) noexcept {
Release();
width = w;
height = h;
// 创建兼容 DC
hdcMem = CreateCompatibleDC(hdcScreen);
if (!hdcMem) {
return false;
}
// 创建 ARGB32 DIB
BITMAPINFO bmi{};
bmi.bmiHeader.biSize = sizeof(BITMAPINFOHEADER);
bmi.bmiHeader.biWidth = static_cast<LONG>(w);
bmi.bmiHeader.biHeight = -static_cast<LONG>(h); // 负数: 顶部对齐
bmi.bmiHeader.biPlanes = 1;
bmi.bmiHeader.biBitCount = 32; // ARGB
bmi.bmiHeader.biCompression = BI_RGB;
hbmDIB = CreateDIBSection(hdcScreen, &bmi, DIB_RGB_COLORS,
reinterpret_cast<void**>(&pBits), nullptr, 0);
if (!hbmDIB || !pBits) {
DeleteDC(hdcMem);
hdcMem = nullptr;
return false;
}
// 选择 DIB 到 DC
HBITMAP hbmOld = (HBITMAP)SelectObject(hdcMem, hbmDIB);
if (hbmOld) {
// 如果旧位图非空,需跟踪以便后续删除 (此处可忽略,因为创建时通常为空)
}
return true;
}
/**
* @brief 释放所有资源
*/
void Release() noexcept {
if (hbmDIB) {
DeleteObject(hbmDIB);
hbmDIB = nullptr;
}
if (hdcMem) {
DeleteDC(hdcMem);
hdcMem = nullptr;
}
pBits = nullptr;
width = height = 0;
}
/**
* @brief 清除矩形区域为透明 (所有像素 alpha = 0)
*/
void ClearRect(const RECT& rc) noexcept {
if (!pBits || width == 0 || height == 0) return;
int left = std::max(0L, rc.left);
int top = std::max(0L, rc.top);
int right = std::min((LONG)width, rc.right);
int bottom = std::min((LONG)height, rc.bottom);
for (int y = top; y < bottom; ++y) {
for (int x = left; x < right; ++x) {
pBits[y * width + x] = 0; // ARGB: alpha=0 (完全透明)
}
}
}
/**
* @brief 获取指针到特定像素
*/
inline uint32_t* GetPixel(int x, int y) noexcept {
if (x < 0 || x >= (int)width || y < 0 || y >= (int)height) {
return nullptr;
}
return &pBits[y * width + x];
}
~DIBBuffer() {
Release();
}
// 禁用拷贝
DIBBuffer(const DIBBuffer&) = delete;
DIBBuffer& operator=(const DIBBuffer&) = delete;
};
/**
* @brief 单个波纹动画状态
*/
struct RippleState {
int32_t centerX = 0;
int32_t centerY = 0;
uint64_t startTime = 0; // QueryPerformanceCounter() 返回值
/**
* @brief 计算当前半径
* @param nowQPC 当前 QueryPerformanceCounter() 返回值
* @param freq QueryPerformanceFrequency() 返回值
* @return 半径 (像素),如果波纹已死亡返回 -1.0f
*/
float GetCurrentRadius(uint64_t nowQPC,
uint64_t freq,
float maxRadius,
uint32_t durationMS,
float radiusExponent = 1.0f) const noexcept {
if (freq == 0) return -1.0f;
uint64_t elapsedMS = (nowQPC - startTime) * 1000 / freq;
if (elapsedMS > durationMS) {
return -1.0f; // 标记已死亡
}
float t = elapsedMS / static_cast<float>(durationMS);
float exp = std::max(0.1f, radiusExponent);
return maxRadius * std::pow(t, exp);
}
/**
* @brief 计算当前 Alpha 值 (0-255)
* 使用立方衰减: alpha = (1 - t)^3
*/
uint8_t GetCurrentAlpha(uint64_t nowQPC,
uint64_t freq,
uint32_t durationMS,
float alphaExponent = 3.0f) const noexcept {
if (freq == 0) return 0;
uint64_t elapsedMS = (nowQPC - startTime) * 1000 / freq;
if (elapsedMS > durationMS) {
return 0;
}
float t = elapsedMS / static_cast<float>(durationMS);
float exp = std::max(0.1f, alphaExponent);
float alpha_norm = std::pow((1.0f - t), exp);
return static_cast<uint8_t>(255.0f * alpha_norm);
}
/**
* @brief 检查波纹是否已鼓励 (可回收)
*/
bool IsAlive(uint64_t nowQPC, uint64_t freq, uint32_t durationMS) const noexcept {
if (freq == 0) return false;
uint64_t elapsedMS = (nowQPC - startTime) * 1000 / freq;
return elapsedMS <= durationMS;
}
};
/**
* @brief 波纹对象池
*/
struct RipplePool {
static constexpr size_t MAX_RIPPLES = 12;
std::array<RippleState, MAX_RIPPLES> ripples{};
uint8_t activeCount = 0; // 当前活跃波纹数 (0 到 MAX_RIPPLES)
/**
* @brief 添加新波纹
* @return true 成功false 池已满
*/
bool AddRipple(int32_t x, int32_t y, uint64_t nowQPC) noexcept {
if (activeCount >= MAX_RIPPLES) {
return false;
}
ripples[activeCount].centerX = x;
ripples[activeCount].centerY = y;
ripples[activeCount].startTime = nowQPC;
activeCount++;
return true;
}
/**
* @brief 清理并压缩死亡波纹,更新 activeCount
*/
void CompactDeadRipples(uint64_t nowQPC, uint64_t freq, uint32_t durationMS) noexcept {
uint8_t writeIdx = 0;
for (uint8_t i = 0; i < activeCount; ++i) {
if (ripples[i].IsAlive(nowQPC, freq, durationMS)) {
ripples[writeIdx++] = ripples[i];
}
}
activeCount = writeIdx;
}
};
/**
* @brief 脏矩形追踪器
*
* 记录"上一帧圆圈的包围盒 + 当前帧圆圈的包围盒"
* 计算联合区域以确定必须重绘的区域
*/
class DirtyRectTracker {
public:
RECT currentDirty{0, 0, 0, 0}; // 当前帧脏区
RECT prevDirty{0, 0, 0, 0}; // 上一帧脏区
uint32_t screenWidth = 1920;
uint32_t screenHeight = 1080;
/**
* @brief 记下一个圆形的包围盒到脏区
*/
void UnionCircle(int x, int y, float radius) noexcept {
int l = std::max(0, static_cast<int>(x - radius - 2.0f));
int t = std::max(0, static_cast<int>(y - radius - 2.0f));
int r = std::min(static_cast<int>(screenWidth),
static_cast<int>(x + radius + 2.0f));
int b = std::min(static_cast<int>(screenHeight),
static_cast<int>(y + radius + 2.0f));
if (l >= r || t >= b) return; // 无效矩形
if (currentDirty.left == 0 && currentDirty.right == 0) {
currentDirty = {l, t, r, b};
} else {
currentDirty.left = std::min(currentDirty.left, (LONG)l);
currentDirty.top = std::min(currentDirty.top, (LONG)t);
currentDirty.right = std::max(currentDirty.right, (LONG)r);
currentDirty.bottom = std::max(currentDirty.bottom, (LONG)b);
}
}
/**
* @brief 获取需要更新的矩形 (当前 + 前一帧的并集)
*/
RECT GetUpdateRect() const noexcept {
RECT result = currentDirty;
result.left = std::min(result.left, prevDirty.left);
result.top = std::min(result.top, prevDirty.top);
result.right = std::max(result.right, prevDirty.right);
result.bottom = std::max(result.bottom, prevDirty.bottom);
return result;
}
/**
* @brief 循环开始 - 交换脏区,准备新一帧
*/
void BeginFrame() noexcept {
prevDirty = currentDirty;
currentDirty = {0, 0, 0, 0};
}
/**
* @brief 检查此帧是否有脏内容
*/
bool HasDirtyRects() const noexcept {
return !IsRectEmpty(&currentDirty);
}
};

271
include/MouseHighlighter.h Normal file
View File

@@ -0,0 +1,271 @@
#pragma once
#include <windows.h>
#include <memory>
#include <cstdint>
#include "SharedState.h"
#include "DataStructures.h"
#include "Config.h"
/**
* @brief 鼠标高亮工具应用主类
*
* 职责:
* 1. 创建并管理全屏透明分层窗口
* 2. 注册全局低级鼠标钩子WH_MOUSE_LL
* 3. 启动渲染线程与监控线程
* 4. 处理退出信号与资源清理
*/
class MouseHighlighter {
public:
/**
* @brief 构造函数 - 初始化默认值
*/
MouseHighlighter() = default;
/**
* @brief 析构函数 - 清理资源 (若未主动调用 Cleanup)
*/
~MouseHighlighter();
/**
* @brief 初始化应用
*
* 执行步骤:
* 1. 加载配置文件 (config.ini)
* 2. 创建透明分层窗口
* 3. 初始化 DIB 双缓冲
* 4. 注册全局鼠标钩子
* 5. 启动渲染线程 & 监控线程
* 6. 创建系统托盘图标
*
* @return true 初始化成功false 失败
*/
bool Initialize();
/**
* @brief 运行消息循环 (阻塞至退出信号)
*
* @return 消息循环的退出代码
*/
int Run();
/**
* @brief 请求干净退出
*
* 这会设置 exitEvent触发消息循环结束、线程停止、资源清理
*/
void RequestShutdown();
/**
* @brief 手动清理所有资源
*
* 可在 WM_DESTROY 或程序结束时调用
* 设计为幂等的(可多次调用)
*/
void Cleanup();
/**
* @brief 获取窗口句柄
*/
HWND GetHWND() const { return hLayeredWindow; }
/**
* @brief 获取配置对象 (const)
*/
const Config& GetConfig() const { return config; }
/**
* @brief 获取配置对象 (mutable)
*/
Config& GetMutableConfig() { return config; }
/**
* @brief 应用新配置 (例如颜色变更、圆形大小变更)
*
* @param newConfig 新的配置
* @return true 成功应用false 失败
*/
bool ApplyConfig(const Config& newConfig);
// 禁用拷贝与移动
MouseHighlighter(const MouseHighlighter&) = delete;
MouseHighlighter& operator=(const MouseHighlighter&) = delete;
MouseHighlighter(MouseHighlighter&&) = delete;
MouseHighlighter& operator=(MouseHighlighter&&) = delete;
private:
// ===== 窗口 & 显示相关 =====
HWND hLayeredWindow = nullptr; // 透明分层窗口
HINSTANCE hInstance = nullptr; // 应用实例句柄
// ===== 配置 =====
Config config;
wchar_t configFilePath[MAX_PATH] = {}; // config.ini 路径
// ===== 图形渲染相关 =====
DIBBuffer dibBuffer; // DIB 双缓冲
DirtyRectTracker dirtyRectTracker; // 脏矩形追踪
LARGE_INTEGER qpcFrequency{}; // QueryPerformanceCounter 频率
// ===== 共享状态 =====
SharedMouseState sharedState; // 钩子 <-> 渲染线程间的共享状态
RipplePool ripplePool; // 波纹对象池
// ===== 线程管理 =====
HANDLE hRenderThread = nullptr; // 渲染节拍线程
HANDLE hMonitorThread = nullptr; // 性能监控线程
HANDLE hExitEvent = nullptr; // 应用退出信号
// ===== 钩子 =====
HHOOK hMouseHook = nullptr; // 全局鼠标钩子
// ===== 托盘相关 =====
NOTIFYICONDATA nid{}; // 托盘图标数据
UINT wmTaskbarCreated = 0; // 任务栏重创建消息 ID
// ===== 平滑坐标 (EMA) =====
float smoothedX = 0.0f;
float smoothedY = 0.0f;
// ===== 窗口类名 =====
static constexpr wchar_t WINDOW_CLASS_NAME[] = L"MouseHighlightWindow";
static constexpr wchar_t APP_NAME[] = L"MouseHighlighter";
// ===== 内部方法 =====
/**
* @brief 创建透明分层窗口
*/
bool CreateLayeredWindow();
/**
* @brief 销毁窗口与相关资源
*/
void DestroyLayeredWindow();
/**
* @brief 初始化 DIB 缓冲
*/
bool InitializeDIBBuffer();
/**
* @brief 注册全局鼠标钩子
*/
bool RegisterMouseHook();
/**
* @brief 卸载鼠标钩子
*/
bool UnregisterMouseHook();
/**
* @brief 启动渲染线程
*/
bool StartRenderThread();
/**
* @brief 启动监控线程
*/
bool StartMonitorThread();
/**
* @brief 等待所有线程结束
*/
void WaitForAllThreads();
/**
* @brief 加载配置文件
*/
bool LoadConfigFile();
/**
* @brief 保存配置文件
*/
bool SaveConfigFile();
/**
* @brief 更新 EMA 平滑坐标
*/
void UpdateSmoothedCursor();
/**
* @brief 执行单次渲染帧
*/
void RenderFrame();
/**
* @brief 绘制鼠标圆圈光晕到 DIB
*/
void DrawHalo(int centerX, int centerY);
/**
* @brief 绘制所有活跃波纹到 DIB
*/
void DrawRipples();
/**
* @brief 更新分层窗口 (输出脏矩形)
*/
void UpdateLayeredWindowOutput(const RECT& updateRect);
/**
* @brief 创建系统托盘图标与菜单
*/
bool SetupTrayIcon();
/**
* @brief 销毁托盘图标
*/
void RemoveTrayIcon();
/**
* @brief 显示托盘右键菜单
*/
void ShowTrayMenu();
// ===== 消息处理 =====
/**
* @brief 窗口消息处理函数
*/
LRESULT HandleWindowMessage(HWND hWnd, UINT msg, WPARAM wParam, LPARAM lParam);
/**
* @brief 处理托盘消息
*/
void HandleTrayMessage(UINT msg, int iconID);
// ===== 全局钩子回调 (静态) =====
/**
* @brief 全局鼠标钩子回调 (低级)
*
* 由 Windows 直接调用,必须快 (< 50μs)
*/
static LRESULT CALLBACK MouseHookProc(int nCode, WPARAM wParam, LPARAM lParam);
// ===== 线程函数 (静态) =====
/**
* @brief 渲染节拍线程
*/
static DWORD WINAPI RenderTickThreadProc(LPVOID lpParam);
/**
* @brief 监控线程
*/
static DWORD WINAPI MonitorThreadProc(LPVOID lpParam);
// ===== 窗口过程 (静态) =====
/**
* @brief 窗口过程回调
*/
static LRESULT CALLBACK WindowProcStatic(HWND hWnd, UINT msg,
WPARAM wParam, LPARAM lParam);
// ===== 全局实例指针 (用于静态回调) =====
static MouseHighlighter* s_pInstance;
};

96
include/SharedState.h Normal file
View File

@@ -0,0 +1,96 @@
#pragma once
#include <atomic>
#include <array>
#include <cstdint>
#include <cstring>
#include <windows.h>
// 对齐至缓存行 (64 字节) 避免伪共享
#pragma pack(push, 8)
/**
* @brief 点击事件结构 - 无锁环形队列中的单个元素
*/
struct ClickEvent {
int32_t x, y; // 屏幕坐标
uint32_t timestamp; // GetTickCount64() 毫秒戳
uint8_t button; // 0=Left, 1=Right, 2=Middle
};
/**
* @brief 钩子线程与渲染线程间的共享状态
*
* - 钩子线程写入cursorX, cursorY, isDirty, clickQueue
* - 渲染线程读取:同上
* - 所有操作无锁,仅原子变量
*/
struct alignas(64) SharedMouseState {
// 鼠标实时坐标 - 钩子线程写,渲染线程读
std::atomic<int32_t> cursorX{0};
std::atomic<int32_t> cursorY{0};
// 脏标记 - 通知渲染线程需要更新
std::atomic<bool> isDirty{false};
// 点击事件环形队列
static constexpr size_t MAX_CLICK_EVENTS = 8;
std::array<ClickEvent, MAX_CLICK_EVENTS> clickQueue{};
// 环形队列指针 - 只能单调递增
// 注意: 不追回溯,无需 CAS仅需 load/store 序列化
std::atomic<uint8_t> clickHead{0}; // 写指针 (钩子线程写)
std::atomic<uint8_t> clickTail{0}; // 读指针 (渲染线程读)
/**
* @brief 钩子线程调用 - 尝试入队一个点击事件
* @return true 成功入队false 队列满
*/
bool TryEnqueueClick(int32_t x, int32_t y, uint8_t button) noexcept {
uint8_t head = clickHead.load(std::memory_order_relaxed);
uint8_t tail = clickTail.load(std::memory_order_acquire);
// 检查队列是否满
uint8_t nextHead = (head + 1) % MAX_CLICK_EVENTS;
if (nextHead == tail) {
return false; // 队列满,丢弃
}
// 写入事件
clickQueue[head].x = x;
clickQueue[head].y = y;
clickQueue[head].timestamp = GetTickCount();
clickQueue[head].button = button;
// 推进写指针
clickHead.store(nextHead, std::memory_order_release);
isDirty.store(true, std::memory_order_release);
return true;
}
/**
* @brief 渲染线程调用 - 尝试出队一个点击事件
* @return true 成功出队到 eventfalse 队列空
*/
bool TryDequeueClick(ClickEvent& event) noexcept {
uint8_t head = clickHead.load(std::memory_order_acquire);
uint8_t tail = clickTail.load(std::memory_order_relaxed);
if (tail == head) {
return false; // 队列空
}
// 读取事件
event = clickQueue[tail];
// 推进读指针
clickTail.store((tail + 1) % MAX_CLICK_EVENTS, std::memory_order_release);
return true;
}
};
#pragma pack(pop)
// 编译期检查
static_assert(sizeof(SharedMouseState) <= 256, "SharedMouseState should not exceed 256 bytes");
static_assert(alignof(SharedMouseState) == 64, "SharedMouseState must be cache-line aligned");