优化光标区域裁剪 (#947)

* feat: 减少 ClipCursor 调用,以及支持源窗口限制鼠标

* fix: 正确还原原始光标裁剪区域,以及添加注释

* fix: 避免直接调用 ClipCursor

* chore: 更新注释
This commit is contained in:
Xu 2024-06-16 17:27:37 +08:00 committed by GitHub
commit e6c1907eff
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 95 additions and 44 deletions

View file

@ -81,51 +81,63 @@ static POINT ScalingToSrc(POINT pt) noexcept {
// 避免了并发问题。如果设置不成功则多次尝试。这里旨在尽最大努力,我怀疑是否有完美
// 的解决方案。
static void ReliableSetCursorPos(POINT pos) noexcept {
const int screenWidth = GetSystemMetrics(SM_CXVIRTUALSCREEN);
const int screenHeight = GetSystemMetrics(SM_CYVIRTUALSCREEN);
// 检查光标的限制区域,如果要设置的位置不在限制区域内则回落到 SetCursorPos
RECT clipRect;
GetClipCursor(&clipRect);
INPUT input{
.type = INPUT_MOUSE,
.mi{
.dx = (pos.x * 65535) / (screenWidth - 1),
.dy = (pos.y * 65535) / (screenHeight - 1),
.dwFlags = MOUSEEVENTF_MOVE | MOUSEEVENTF_ABSOLUTE | MOUSEEVENTF_VIRTUALDESK
}
};
// 如果设置不成功则多次尝试
for (int i = 0; i < 10; ++i) {
if (!SendInput(1, &input, sizeof(input))) {
Logger::Get().Win32Error("SendInput 失败");
break;
}
// 等待系统处理
Sleep(0);
POINT curCursorPos;
if (!GetCursorPos(&curCursorPos)) {
Logger::Get().Win32Error("GetCursorPos 失败");
break;
}
if (curCursorPos == pos) {
// 已成功,但保险起见再设置一次
SendInput(1, &input, sizeof(input));
return;
if (PtInRect(&clipRect, pos)) {
const int screenWidth = GetSystemMetrics(SM_CXVIRTUALSCREEN);
const int screenHeight = GetSystemMetrics(SM_CYVIRTUALSCREEN);
INPUT input{
.type = INPUT_MOUSE,
.mi{
.dx = (pos.x * 65535) / (screenWidth - 1),
.dy = (pos.y * 65535) / (screenHeight - 1),
.dwFlags = MOUSEEVENTF_MOVE | MOUSEEVENTF_ABSOLUTE | MOUSEEVENTF_VIRTUALDESK
}
};
// 如果设置不成功则多次尝试
for (int i = 0; i < 10; ++i) {
if (!SendInput(1, &input, sizeof(input))) {
Logger::Get().Win32Error("SendInput 失败");
break;
}
// 等待系统处理
Sleep(0);
POINT curCursorPos;
if (!GetCursorPos(&curCursorPos)) {
Logger::Get().Win32Error("GetCursorPos 失败");
break;
}
if (curCursorPos == pos) {
// 已成功,但保险起见再设置一次
SendInput(1, &input, sizeof(input));
return;
}
}
// 多次不成功回落到 SetCursorPos
}
// 回落到 SetCursorPos
SetCursorPos(pos.x, pos.y);
}
CursorManager::~CursorManager() noexcept {
if (ScalingWindow::Get().Options().IsDebugMode()) {
return;
}
_ShowSystemCursor(true, true);
ClipCursor(nullptr);
if (_lastClip.left != std::numeric_limits<LONG>::max()) {
_RestoreClipCursor();
}
if (_isUnderCapture && !ScalingWindow::Get().Options().IsDebugMode()) {
if (_isUnderCapture) {
POINT cursorPos;
if (!GetCursorPos(&cursorPos)) {
Logger::Get().Win32Error("GetCursorPos 失败");
@ -413,25 +425,18 @@ void CursorManager::_UpdateCursorClip() noexcept {
// 3. 常规: 根据多屏幕限制光标,捕获/取消捕获,支持 UI 和多屏幕
if (options.IsDebugMode()) {
if (_isCapturedOnOverlay) {
// 光标被叠加层捕获时将光标限制在输出区域内
ClipCursor(&destRect);
} else {
ClipCursor(nullptr);
}
return;
}
if (options.Is3DGameMode()) {
// 开启“在 3D 游戏中限制光标”则每帧都限制一次光标
ClipCursor(&srcRect);
_SetClipCursor(srcRect, true);
return;
}
if (_isCapturedOnOverlay) {
// 光标被叠加层捕获时将光标限制在输出区域内
ClipCursor(&destRect);
_SetClipCursor(destRect);
return;
}
@ -445,7 +450,7 @@ void CursorManager::_UpdateCursorClip() noexcept {
// 如果光标不在缩放窗口内不应限制光标
if (_isUnderCapture) {
ClipCursor(&srcRect);
_SetClipCursor(srcRect);
}
// 当光标被前台窗口捕获时我们除了限制光标外什么也不做,即光标
@ -650,9 +655,10 @@ void CursorManager::_UpdateCursorClip() noexcept {
clips.bottom = _isUnderCapture ? srcRect.bottom : destRect.bottom;
}
ClipCursor(&clips);
} else {
ClipCursor(nullptr);
_SetClipCursor(clips);
} else if (_lastClip.left != std::numeric_limits<LONG>::max()) {
_RestoreClipCursor();
_lastClip = { std::numeric_limits<LONG>::max() };
}
// SetCursorPos 应在 ClipCursor 之后,否则会受到上一次 ClipCursor 的影响
@ -736,4 +742,42 @@ bool CursorManager::_StopCapture(POINT& cursorPos, bool onDestroy) noexcept {
}
}
void CursorManager::_SetClipCursor(const RECT& clipRect, bool is3DGameMode) noexcept {
RECT curClip;
GetClipCursor(&curClip);
// 如果当前光标裁剪区域与我们之前设置的不同,肯定是其他程序更改了,记录此原始裁剪区域
// 用于后续计算和还原。
// 如果我们没有裁剪光标区域,则 _lastClip.left == std::numeric_limits<LONG>::max()
// curClip != _lastClip 始终为真。
// 但如果其他程序恰好设置了和我们相同的裁剪区域怎么办?
if (curClip != _lastClip) {
_originClip = curClip;
}
// 为了尊重原始裁剪区域,计算和我们的裁剪区域的交集。除非两个区域不相交或光标在叠加层上
RECT targetClip;
if (_isOnOverlay || !IntersectRect(&targetClip, &_originClip, &clipRect)) {
// 不相交则不再尊重原始裁剪区域,这不太可能发生
targetClip = clipRect;
}
// 裁剪区域变化了才调用 ClipCursor。每次调用 ClipCursor 都会向前台窗口发送 WM_MOUSEMOVE
// 消息,一些程序无法正确处理,如 GH#920 和 GH#927
if (targetClip != _lastClip || is3DGameMode) {
ClipCursor(&targetClip);
_lastClip = targetClip;
}
}
void CursorManager::_RestoreClipCursor() const noexcept {
RECT curClip;
GetClipCursor(&curClip);
// 如果 curClip != _lastClip则其他程序已经更改光标裁剪区域我们放弃更改
if (curClip == _lastClip && curClip != _originClip) {
ClipCursor(&_originClip);
}
}
}

View file

@ -48,9 +48,16 @@ private:
bool _StopCapture(POINT& cursorPos, bool onDestroy = false) noexcept;
void _SetClipCursor(const RECT& clipRect, bool is3DGameMode = false) noexcept;
void _RestoreClipCursor() const noexcept;
HCURSOR _hCursor = NULL;
POINT _cursorPos { std::numeric_limits<LONG>::max(),std::numeric_limits<LONG>::max() };
RECT _originClip{ std::numeric_limits<LONG>::max() };
RECT _lastClip{ std::numeric_limits<LONG>::max() };
int _originCursorSpeed = 0;
bool _isUnderCapture = false;