diff --git a/macos/ScreenHandler.mm b/macos/ScreenHandler.mm index 079ebc7..5f73e0d 100644 --- a/macos/ScreenHandler.mm +++ b/macos/ScreenHandler.mm @@ -88,14 +88,16 @@ bool ScreenHandler::init() m_height = (int)CGDisplayPixelsHigh(m_displayID); } - // Get logical point dimensions (what CGEvent uses) - // NSScreen provides logical dimensions - NSScreen* mainScreen = [NSScreen mainScreen]; - if (mainScreen) { - NSRect frame = [mainScreen frame]; - m_logicalWidth = (int)frame.size.width; - m_logicalHeight = (int)frame.size.height; - } else { + // 逻辑尺寸必须取自「实际抓屏的那块屏」(CGMainDisplayID)。CGEvent 的鼠标坐标是 + // 全局显示坐标(points),而我们发给服务端的是这块屏的编码像素。 + // [NSScreen mainScreen] 取的是菜单栏/焦点窗口所在屏:合盖外接显示器时它仍可能是 + // 已停用的内置屏(例如 13" MacBook Air 的 1470x956),与抓屏的外置屏(1280x1024) + // 不是同一块屏。用错尺寸会让每个注入点变成 p * (logical / encode),表现为从左上角 + // 起线性增长、且 x/y 各轴独立的鼠标偏移(见 ScreenHandler::OnReceive 的换算)。 + CGRect bounds = CGDisplayBounds(m_displayID); + m_logicalWidth = (int)(bounds.size.width + 0.5); + m_logicalHeight = (int)(bounds.size.height + 0.5); + if (m_logicalWidth <= 0 || m_logicalHeight <= 0) { // Fallback: use physical dimensions m_logicalWidth = m_width; m_logicalHeight = m_height; diff --git a/macos/main.mm b/macos/main.mm index 81c64d1..aa4c833 100644 --- a/macos/main.mm +++ b/macos/main.mm @@ -271,11 +271,14 @@ static std::string normalizeExePathLower(const std::string& path) // Get screen resolution static std::string getScreenResolution() { - NSScreen* mainScreen = [NSScreen mainScreen]; - if (mainScreen) { - NSRect frame = [mainScreen frame]; + // 用主显示器的全局显示坐标(points),与 CGEvent 注入坐标同一空间,也和 + // ScreenHandler 抓屏所用的 CGMainDisplayID 保持一致。 + // [NSScreen mainScreen] 在合盖接外置显示器时仍可能返回内置屏(如 1470x956), + // 上报后会让服务端以为主机分辨率是内置屏尺寸。 + CGRect bounds = CGDisplayBounds(CGMainDisplayID()); + if (bounds.size.width > 0 && bounds.size.height > 0) { return [NSString stringWithFormat:@"1:%dx%d", - (int)frame.size.width, (int)frame.size.height].UTF8String; + (int)(bounds.size.width + 0.5), (int)(bounds.size.height + 0.5)].UTF8String; } return "0:0x0"; } diff --git a/server/2015Remote/ScreenSpyDlg.cpp b/server/2015Remote/ScreenSpyDlg.cpp index a1b0b73..f98e08b 100644 --- a/server/2015Remote/ScreenSpyDlg.cpp +++ b/server/2015Remote/ScreenSpyDlg.cpp @@ -13,6 +13,7 @@ #include "CGridDialog.h" #include "2015RemoteDlg.h" #include "CPasswordDlg.h" +#include "InputDlg.h" #include "CDlgFileSend.h" #include #include @@ -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) diff --git a/server/2015Remote/ScreenSpyDlg.h b/server/2015Remote/ScreenSpyDlg.h index 289eb23..9e75470 100644 --- a/server/2015Remote/ScreenSpyDlg.h +++ b/server/2015Remote/ScreenSpyDlg.h @@ -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); diff --git a/server/2015Remote/lang/de_DE.ini b/server/2015Remote/lang/de_DE.ini index 80d342d..bcf42ae 100644 --- a/server/2015Remote/lang/de_DE.ini +++ b/server/2015Remote/lang/de_DE.ini @@ -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 diff --git a/server/2015Remote/lang/en_US.ini b/server/2015Remote/lang/en_US.ini index 1ebcacf..6bce336 100644 --- a/server/2015Remote/lang/en_US.ini +++ b/server/2015Remote/lang/en_US.ini @@ -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 diff --git a/server/2015Remote/lang/zh_TW.ini b/server/2015Remote/lang/zh_TW.ini index 4b32300..c90a732 100644 --- a/server/2015Remote/lang/zh_TW.ini +++ b/server/2015Remote/lang/zh_TW.ini @@ -2110,3 +2110,12 @@ V2 (ÒѹýÆڣ¬ÐøÆڴӽñÌìÆðËã)= (ÒÑß^Æڣ¬ÀmÆڏĽñÌìÆðËã) £¨ԭÊÚȨÒѹýÆڣ¬ÐøÆڴӽñÌìÆðË㣩=£¨ԭÊڙàÒÑß^Æڣ¬ÀmÆڏĽñÌìÆðË㣩 ×ÓĿ¼=×ÓĿ¼ + +; Mouse coordinate calibration (per-host click offset workaround) +Êó±ê×ø±êУ׼(&M)=»¬Êó×ù˜ËУÕý(&M) +Êó±ê×ø±êУ׼=»¬Êó×ù˜ËУÕý +X ÖáËõ·ÅϵÊý=X ÝS¿s·łS”µ +Y ÖáËõ·ÅϵÊý=Y ÝS¿s·łS”µ +»­Ãæ %dx%d£»Éϱ¨ %s£»ϵÊýΪÕæʵ³ýÒÔÉϱ¨=®‹Ãæ %dx%d£»Éψó %s£»‚S”µžéÕ挍³ýÒÔÉψó +Ëõ·ÅϵÊýÐèÔÚ 0.2 µ½ 5.0 ֮¼䡣=¿s·łS”µÐèÔÚ 0.2 µ½ 5.0 ֮ég¡£ +ÒѰ´ »­Ãæ/Éϱ¨ ×Զ¯Ìîд£¬¿ÉÐ޸Ä=ÒÑÒÀ ®‹Ãæ/Éψó ×ԄÓÌîÈ룬¿ÉÐ޸Ä