Fix: Align macOS mouse coordinates with the captured display
The macOS client derived its logical screen size from [NSScreen mainScreen] while it captured CGMainDisplayID. In clamshell mode with an external display the former can still report the disabled built-in panel (1470x956) while the captured frame is 1280x1024, so every injected point was scaled by logical/encode and drifted linearly with the distance from the top left corner, independently on each axis. Take the logical size from CGDisplayBounds of the captured display instead, and report the same bounds in the login resolution. For hosts that still run the old client, the remote desktop dialog gains a per-host mouse calibration in its system menu. The factors are stored by client ID together with the reported resolution and the captured frame size, so they stop applying once the client is upgraded or a different display is captured, and come back automatically when the original display returns. When no calibration has been saved the dialog pre-fills the factors from frame/reported, but only for macOS clients, because the other clients derive their conversion from the captured display itself and a mismatching report there would be misleading. Co-Authored-By: deepseek-flash
This commit is contained in:
1 parent
38cd207f5d
commit
3ae6298746
7 files changed
+250
-12
No files matched your search
@@ -13,6 +13,7 @@
|
||||
#include "CGridDialog.h"
|
||||
#include "2015RemoteDlg.h"
|
||||
#include "CPasswordDlg.h"
|
||||
#include "InputDlg.h"
|
||||
#include "CDlgFileSend.h"
|
||||
#include <file_upload.h>
|
||||
#include <md5.h>
|
||||
@@ -707,6 +708,9 @@ BOOL CScreenSpyDlg::OnInitDialog()
|
||||
Mprintf("[ScreenSpy] Dialog created for device %llu, isMfcSession=%d, isWebSession=%d\n",
|
||||
m_ClientID, isMfcSession ? 1 : 0, m_bIsWebSession.load() ? 1 : 0);
|
||||
|
||||
// 逐主机鼠标坐标补偿。必须在用户开始操作前加载;默认 1.0 不影响任何主机。
|
||||
LoadMouseScale();
|
||||
|
||||
// 获取默认 IME 上下文(ImmAssociateContext 返回之前关联的上下文)
|
||||
// 先禁用再恢复,以获取原始上下文句柄
|
||||
m_hOldIMC = ImmAssociateContext(m_hWnd, NULL);
|
||||
@@ -728,6 +732,7 @@ BOOL CScreenSpyDlg::OnInitDialog()
|
||||
SysMenu->AppendMenuL(MF_STRING, IDM_REMOTE_CURSOR, "使用远程光标(&C)");
|
||||
SysMenu->AppendMenuL(MF_STRING, IDM_ADAPTIVE_SIZE, "自适应窗口大小(&A)");
|
||||
SysMenu->AppendMenuL(MF_STRING, IDM_TRACE_CURSOR, "跟踪被控端鼠标(&T)");
|
||||
SysMenu->AppendMenuL(MF_STRING, IDM_MOUSE_CALIB, "鼠标坐标校准(&M)");
|
||||
SysMenu->AppendMenuL(MF_STRING, IDM_BLOCK_INPUT, "锁定被控端鼠标和键盘(&L)");
|
||||
SysMenu->AppendMenuL(MF_STRING, IDM_RESTORE_CONSOLE, "RDP会话归位(&R)");
|
||||
// 只在虚拟桌面模式下显示重置选项
|
||||
@@ -1142,6 +1147,8 @@ VOID CScreenSpyDlg::OnReceiveComplete()
|
||||
m_ClientID = *((uint64_t*)(m_ContextObject->InDeCompressedBuffer.GetBuffer(1 + sizeof(BITMAPINFOHEADER))));
|
||||
}
|
||||
PrepareDrawing(m_BitmapInfor_Full);
|
||||
// 重连或切换显示器后重新校验鼠标补偿(客户端上报分辨率可能已变化)。
|
||||
LoadMouseScale();
|
||||
// 分辨率切换完成,允许解码
|
||||
m_bResolutionChanging = false;
|
||||
// Notify web clients of resolution change (only for Web session dialogs)
|
||||
@@ -2121,6 +2128,10 @@ void CScreenSpyDlg::OnSysCommand(UINT nID, LPARAM lParam)
|
||||
m_ContextObject->Send2Client(cmd, sizeof(cmd));
|
||||
break;
|
||||
}
|
||||
case IDM_MOUSE_CALIB: { // 鼠标坐标校准(逐主机,默认 1.0)
|
||||
ShowMouseCalibration();
|
||||
break;
|
||||
}
|
||||
case IDM_SAVEDIB: { // 快照保存
|
||||
SaveSnapshot();
|
||||
break;
|
||||
@@ -2722,12 +2733,199 @@ void CScreenSpyDlg::SendScaledMouseMessage(MSG* pMsg, bool makeLP)
|
||||
low = x + m_ulHScrollPos;
|
||||
high = y + m_ulVScrollPos;
|
||||
}
|
||||
|
||||
// 逐主机鼠标坐标补偿(系统菜单「鼠标坐标校准」)。默认 1.0,对所有主机无影响。
|
||||
// 客户端若用错误的逻辑分辨率换算坐标(见 macos/ScreenHandler.mm),注入点会随
|
||||
// 离左上角的距离线性偏移;这里先按 真实分辨率 ÷ 上报分辨率 缩放,抵消其换算。
|
||||
// 注意:补偿后坐标可以超出位图尺寸(例如 y 需要 1096 才能落到 1023 行),
|
||||
// 因此不按位图边界裁剪,只做 16 位协议范围裁剪。
|
||||
if (m_mouseScaleX != 1.0)
|
||||
low = (LONG)(low * m_mouseScaleX + 0.5);
|
||||
if (m_mouseScaleY != 1.0)
|
||||
high = (LONG)(high * m_mouseScaleY + 0.5);
|
||||
low = max(0L, min(low, 65535L));
|
||||
high = max(0L, min(high, 65535L));
|
||||
|
||||
if (makeLP) msg.lParam = MAKELPARAM(low, high);
|
||||
msg.pt.x = low;
|
||||
msg.pt.y = high;
|
||||
SendCommand(&msg);
|
||||
}
|
||||
|
||||
// 解析客户端上报的分辨率 "N:WxH"(N=显示器数)。失败返回 false。
|
||||
static bool ParseReportedResolution(const CString& res, int& w, int& h)
|
||||
{
|
||||
int colon = res.Find(':');
|
||||
if (colon < 0) return false;
|
||||
CString dims = res.Mid(colon + 1);
|
||||
int sep = dims.FindOneOf(_T("xX*,"));
|
||||
if (sep <= 0) return false;
|
||||
w = _ttoi(dims.Left(sep));
|
||||
h = _ttoi(dims.Mid(sep + 1));
|
||||
return w > 0 && h > 0;
|
||||
}
|
||||
|
||||
// 自动建议补偿系数。只针对已确诊的情形:Mac 客户端 + 画面与上报分辨率在两轴上
|
||||
// 比例不一致(旧 Mac 客户端用 [NSScreen mainScreen] 的逻辑尺寸做坐标换算)。
|
||||
// 建议值 = 画面尺寸 ÷ 上报尺寸,前提是抓屏显示器为标准 1× 分辨率(非 Retina)且
|
||||
// 画质档位没有压缩画面宽度;两者任一不成立时用户需手工修正。
|
||||
// 两轴比例一致说明客户端换算正常(Retina 下画面与上报同为一个缩放倍数),不补偿。
|
||||
static bool SuggestMouseScale(const CString& clientType, int imgW, int imgH,
|
||||
int repW, int repH, double& sx, double& sy)
|
||||
{
|
||||
if (clientType != _T("MAC"))
|
||||
return false;
|
||||
if (imgW <= 0 || imgH <= 0 || repW <= 0 || repH <= 0)
|
||||
return false;
|
||||
double rx = (double)imgW / repW;
|
||||
double ry = (double)imgH / repH;
|
||||
double lo = min(rx, ry), hi = max(rx, ry);
|
||||
if (lo <= 0.0 || hi / lo <= 1.02) // 两轴一致:正常客户端,无需补偿
|
||||
return false;
|
||||
if (rx < 0.2 || rx > 5.0 || ry < 0.2 || ry > 5.0)
|
||||
return false;
|
||||
sx = rx;
|
||||
sy = ry;
|
||||
return true;
|
||||
}
|
||||
|
||||
void CScreenSpyDlg::LoadMouseScale()
|
||||
{
|
||||
m_mouseScaleX = m_mouseScaleY = 1.0;
|
||||
m_mouseScaleFromConfig = false;
|
||||
|
||||
std::string raw = THIS_CFG.GetStr("mouse_scale", std::to_string(m_ClientID));
|
||||
if (raw.empty()) return;
|
||||
|
||||
// 格式: "sx,sy,refRepW,refRepH[,refImgW,refImgH]"。后两项是校准时的画面尺寸,
|
||||
// 用于识别抓屏显示器是否已切换;旧记录没有后两项,退化为按上报/画面比例判断。
|
||||
double sx = 1.0, sy = 1.0;
|
||||
int refW = 0, refH = 0, refImgW = 0, refImgH = 0;
|
||||
int fields = sscanf_s(raw.c_str(), "%lf,%lf,%d,%d,%d,%d",
|
||||
&sx, &sy, &refW, &refH, &refImgW, &refImgH);
|
||||
if (fields != 4 && fields != 6) {
|
||||
Mprintf("[MouseScale] device %llu: invalid config '%s'\n", m_ClientID, raw.c_str());
|
||||
return;
|
||||
}
|
||||
if (!(sx >= 0.2 && sx <= 5.0 && sy >= 0.2 && sy <= 5.0)) {
|
||||
Mprintf("[MouseScale] device %llu: out of range %.4f,%.4f\n", m_ClientID, sx, sy);
|
||||
return;
|
||||
}
|
||||
// 1.0 等价于“无补偿”(用户复位后留下的记录),按未校准处理,允许重新自动建议。
|
||||
if (sx == 1.0 && sy == 1.0)
|
||||
return;
|
||||
|
||||
// 校准记录绑定当时的「客户端上报分辨率」。客户端修复后上报值会变,旧系数会变成
|
||||
// 二次补偿(反而更偏),因此上报值不一致时自动失效。refW/refH=0 表示上报值不可用,
|
||||
// 无法校验,按配置生效。
|
||||
int curW = 0, curH = 0;
|
||||
bool haveRep = false;
|
||||
context* mainCtx = m_pParent ? m_pParent->FindHost(m_ClientID) : nullptr;
|
||||
if (mainCtx && ParseReportedResolution(mainCtx->GetAdditionalData(RES_RESOLUTION), curW, curH))
|
||||
haveRep = true;
|
||||
if (refW > 0 && refH > 0 && haveRep && (curW != refW || curH != refH)) {
|
||||
Mprintf("[MouseScale] device %llu: reported %dx%d != calibrated %dx%d, override disabled\n",
|
||||
m_ClientID, curW, curH, refW, refH);
|
||||
return;
|
||||
}
|
||||
|
||||
// 校准记录还绑定当时的「画面(抓屏)宽高比」。切换显示器后(例如外接屏切回内置屏)
|
||||
// 画面比例会变,旧系数不能再套用;切回原显示器时比例恢复,系数自动重新生效。
|
||||
// 客户端修复后同一次会话内画面比例不变,但上报值会变,仍由上面的检查兜底。
|
||||
int curImgW = m_BitmapInfor_Full ? m_BitmapInfor_Full->bmiHeader.biWidth : 0;
|
||||
int curImgH = m_BitmapInfor_Full ? abs(m_BitmapInfor_Full->bmiHeader.biHeight) : 0;
|
||||
if (curImgW > 0 && curImgH > 0) {
|
||||
if (refImgW > 0 && refImgH > 0) {
|
||||
double refAspect = (double)refImgW / refImgH;
|
||||
double curAspect = (double)curImgW / curImgH;
|
||||
if (fabs(curAspect / refAspect - 1.0) > 0.02) {
|
||||
Mprintf("[MouseScale] device %llu: frame %dx%d (aspect %.3f) != calibrated aspect %.3f, override disabled\n",
|
||||
m_ClientID, curImgW, curImgH, curAspect, refAspect);
|
||||
return;
|
||||
}
|
||||
} else if (haveRep && curW > 0 && curH > 0) {
|
||||
// 旧格式没记录画面比例:只有当前画面与上报分辨率两轴比例不一致(即客户端
|
||||
// 仍存在换算偏差)时才套用;两轴一致说明换算正常(例如切回内置屏),不补偿。
|
||||
double rx = (double)curImgW / curW;
|
||||
double ry = (double)curImgH / curH;
|
||||
double lo = min(rx, ry), hi = max(rx, ry);
|
||||
if (lo <= 0.0 || hi / lo <= 1.02) {
|
||||
Mprintf("[MouseScale] device %llu: frame %dx%d matches reported %dx%d, override disabled\n",
|
||||
m_ClientID, curImgW, curImgH, curW, curH);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
m_mouseScaleX = sx;
|
||||
m_mouseScaleY = sy;
|
||||
m_mouseScaleFromConfig = true;
|
||||
Mprintf("[MouseScale] device %llu: apply x%.4f y%.4f\n", m_ClientID, sx, sy);
|
||||
}
|
||||
|
||||
void CScreenSpyDlg::ShowMouseCalibration()
|
||||
{
|
||||
int imgW = m_BitmapInfor_Full ? m_BitmapInfor_Full->bmiHeader.biWidth : 0;
|
||||
int imgH = m_BitmapInfor_Full ? abs(m_BitmapInfor_Full->bmiHeader.biHeight) : 0;
|
||||
|
||||
CString reported, clientType;
|
||||
int repW = 0, repH = 0;
|
||||
context* mainCtx = m_pParent ? m_pParent->FindHost(m_ClientID) : nullptr;
|
||||
if (mainCtx) {
|
||||
reported = mainCtx->GetAdditionalData(RES_RESOLUTION);
|
||||
ParseReportedResolution(reported, repW, repH);
|
||||
clientType = mainCtx->GetAdditionalData(RES_CLIENT_TYPE);
|
||||
}
|
||||
|
||||
// 没有保存过系数时,按「画面 ÷ 上报」自动填入建议值(无需手工计算);
|
||||
// 保存过的系数优先展示,避免覆盖用户手工微调的结果。
|
||||
double sx = m_mouseScaleX, sy = m_mouseScaleY;
|
||||
bool autoFilled = !m_mouseScaleFromConfig
|
||||
&& SuggestMouseScale(clientType, imgW, imgH, repW, repH, sx, sy);
|
||||
|
||||
// 提示文本走 CInputDialog 的 m_sTipInfo(DDV 上限 64 字符),保持简短。
|
||||
CString tip;
|
||||
if (autoFilled)
|
||||
tip = _TR("已按 画面/上报 自动填写,可修改");
|
||||
else
|
||||
tip.Format(_TR("画面 %dx%d;上报 %s;系数为真实除以上报"),
|
||||
imgW, imgH, reported.IsEmpty() ? _T("-") : reported.GetString());
|
||||
|
||||
CString xStr, yStr;
|
||||
xStr.Format(_T("%.4f"), sx);
|
||||
yStr.Format(_T("%.4f"), sy);
|
||||
|
||||
CInputDialog dlg(this);
|
||||
dlg.Init(_TR("鼠标坐标校准"), _TR("X 轴缩放系数"));
|
||||
dlg.m_str = xStr;
|
||||
dlg.Init2(_TR("Y 轴缩放系数"), yStr);
|
||||
dlg.m_sTipInfo = tip;
|
||||
if (dlg.DoModal() != IDOK)
|
||||
return;
|
||||
|
||||
sx = _ttof(dlg.m_str);
|
||||
sy = _ttof(dlg.m_sSecondInput);
|
||||
if (!(sx >= 0.2 && sx <= 5.0 && sy >= 0.2 && sy <= 5.0)) {
|
||||
MessageBox(_TR("缩放系数需在 0.2 到 5.0 之间。"), _TR("鼠标坐标校准"), MB_ICONWARNING);
|
||||
return;
|
||||
}
|
||||
|
||||
// 记录上报分辨率 + 画面尺寸,供下次加载时判断系数是否仍然适用(切换显示器后失效)。
|
||||
CString val;
|
||||
if (sx == 1.0 && sy == 1.0) {
|
||||
// 1.0 表示不补偿;写成等价的空记录,下次打开对话框仍会按当前画面自动建议。
|
||||
val = _T("1.0,1.0,0,0");
|
||||
m_mouseScaleFromConfig = false;
|
||||
} else {
|
||||
val.Format(_T("%.6f,%.6f,%d,%d,%d,%d"), sx, sy, repW, repH, imgW, imgH);
|
||||
m_mouseScaleFromConfig = true;
|
||||
}
|
||||
THIS_CFG.SetStr("mouse_scale", std::to_string(m_ClientID), val.GetString());
|
||||
m_mouseScaleX = sx;
|
||||
m_mouseScaleY = sy;
|
||||
Mprintf("[MouseScale] device %llu: saved x%.4f y%.4f (reported %dx%d)\n",
|
||||
m_ClientID, sx, sy, repW, repH);
|
||||
}
|
||||
|
||||
VOID CScreenSpyDlg::SendCommand(const MYMSG* Msg)
|
||||
{
|
||||
if (!m_bIsCtrl)
|
||||
|
||||
@@ -141,6 +141,7 @@ enum {
|
||||
IDM_AUDIO_TOGGLE, // 音频开关
|
||||
IDM_ENABLE_H264_HARD,
|
||||
IDM_ENABLE_AV1_HARD,
|
||||
IDM_MOUSE_CALIB, // 鼠标坐标校准(逐主机补偿客户端坐标换算偏差)
|
||||
};
|
||||
|
||||
// 状态信息窗口 - 全屏时显示帧率/速度/质量
|
||||
@@ -243,6 +244,13 @@ public:
|
||||
CCursorInfo m_CursorInfo; //自定义的一个系统的光标类
|
||||
VOID SendCommand(const MYMSG* Msg);
|
||||
void SendScaledMouseMessage(MSG* pMsg, bool makeLP);
|
||||
// 鼠标坐标补偿:默认 1.0(不补偿)。用于客户端上报的逻辑分辨率与实际显示不一致
|
||||
// (例如合盖后用外接显示器的 Mac)时,抵消客户端在 OnReceive 里的错误换算。
|
||||
double m_mouseScaleX = 1.0;
|
||||
double m_mouseScaleY = 1.0;
|
||||
bool m_mouseScaleFromConfig = false; // 当前系数来自已保存配置(否则对话框可自动建议)
|
||||
void LoadMouseScale(); // 从配置读取本主机的补偿系数(会校验上报分辨率)
|
||||
void ShowMouseCalibration();// 系统菜单「鼠标坐标校准」入口
|
||||
VOID UpdateServerClipboard(char *szBuffer,ULONG ulLength);
|
||||
VOID SendServerClipboard(void);
|
||||
|
||||
|
||||
@@ -2104,3 +2104,12 @@ V2
|
||||
(已过期,续期从今天起算)= (abgelaufen; Verlaengerung ab heute)
|
||||
(原授权已过期,续期从今天起算)=(Urspruengliche Lizenz abgelaufen; Verlaengerung ab heute)
|
||||
子目录=SubDir
|
||||
|
||||
; Mouse coordinate calibration (per-host click offset workaround)
|
||||
鼠标坐标校准(&M)=Mauskoordinaten-Kalibrierung(&M)
|
||||
鼠标坐标校准=Mauskoordinaten-Kalibrierung
|
||||
X 轴缩放系数=Skalierungsfaktor X-Achse
|
||||
Y 轴缩放系数=Skalierungsfaktor Y-Achse
|
||||
画面 %dx%d;上报 %s;系数为真实除以上报=Bild %dx%d; gemeldet %s; Faktor: echt/gemeldet
|
||||
缩放系数需在 0.2 到 5.0 之间。=Skalierungsfaktoren muessen zwischen 0.2 und 5.0 liegen.
|
||||
已按 画面/上报 自动填写,可修改=Automatisch aus Bild/Meldung; aenderbar
|
||||
@@ -2119,3 +2119,12 @@ V2
|
||||
(已过期,续期从今天起算)= (expired; renewal starts today)
|
||||
(原授权已过期,续期从今天起算)=(Original license expired; renewal starts today)
|
||||
子目录=SubDir
|
||||
|
||||
; Mouse coordinate calibration (per-host click offset workaround)
|
||||
鼠标坐标校准(&M)=Mouse Coordinate Calibration(&M)
|
||||
鼠标坐标校准=Mouse Coordinate Calibration
|
||||
X 轴缩放系数=X-axis Scale Factor
|
||||
Y 轴缩放系数=Y-axis Scale Factor
|
||||
画面 %dx%d;上报 %s;系数为真实除以上报=Frame %dx%d; reported %s; factor: real/reported
|
||||
缩放系数需在 0.2 到 5.0 之间。=Scale factors must be between 0.2 and 5.0.
|
||||
已按 画面/上报 自动填写,可修改=Auto-filled from frame/reported; editable
|
||||
@@ -2110,3 +2110,12 @@ V2
|
||||
(已过期,续期从今天起算)= (已過期,續期從今天起算)
|
||||
(原授权已过期,续期从今天起算)=(原授權已過期,續期從今天起算)
|
||||
子目录=子目录
|
||||
|
||||
; Mouse coordinate calibration (per-host click offset workaround)
|
||||
鼠标坐标校准(&M)=滑鼠座標校正(&M)
|
||||
鼠标坐标校准=滑鼠座標校正
|
||||
X 轴缩放系数=X 軸縮放係數
|
||||
Y 轴缩放系数=Y 軸縮放係數
|
||||
画面 %dx%d;上报 %s;系数为真实除以上报=畫面 %dx%d;上報 %s;係數為真實除以上報
|
||||
缩放系数需在 0.2 到 5.0 之间。=縮放係數需在 0.2 到 5.0 之間。
|
||||
已按 画面/上报 自动填写,可修改=已依 畫面/上報 自動填入,可修改
|
||||
Reference in new issue
Block a user