6 Commits

Author SHA1 Message Date
yuanyuanxiang
837d89c8b5 Feat: Sub-license count limit - LicenseLimit field in licenses.ini + context menu
- Add LicenseLimit field to LicenseInfo struct (0 = not set, unlimited)
- Add GetLicenseLimit/SetLicenseLimit: read/write LicenseLimit key in licenses.ini
- Append |lic:N to reserved field in TOKEN_AUTH response only when
  LicenseLimit > 0; absent |lic: means no limit (client defaults to 9999),
  so super admin authenticating to its own server is never falsely terminated
- Add "Sub-license limit" item in CLicenseDlg right-click menu (1-9999,
  empty = clear limit); menu label shows current value in real time
- Limit change takes effect when sub-client re-authenticates

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-20 13:01:35 +02:00
yuanyuanxiang
c1433b4b5d Fix: Restore memory DLL at client program startup 2026-06-20 12:38:28 +02:00
yuanyuanxiang
71963b740b Fix: Authorization client use different keyboard log directory 2026-06-20 12:38:28 +02:00
yuanyuanxiang
5b37df26fd Opt: improve adaptive-size rendering 2026-06-20 12:38:28 +02:00
yuanyuanxiang
851fed4739 Feat: sign TOKEN_AUTH response and add TOKEN_SERVER_VERIFY to prevent fake server
TOKEN_AUTH: when the server has a V2 private key, signs "SN|valid(0/1)"
with ECDSA P-256 and places "sig:<base64>" in the response reserved field.
Clients can verify server identity without changing the request format.

TOKEN_SERVER_VERIFY (251): added constant to commands.h; handler already
present in 2015RemoteDlg.cpp for the challenge-response server identity check.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-20 12:38:28 +02:00
yuanyuanxiang
103123f533 Feat: anti-cracking hardening - version binding and updated libs 2026-06-20 12:38:20 +02:00
26 changed files with 396 additions and 51 deletions

View File

@@ -12,6 +12,8 @@ extern "C" {
#include "ServiceWrapper.h" #include "ServiceWrapper.h"
} }
extern void licenseInit();
// Check if CPU supports AVX2 instruction set // Check if CPU supports AVX2 instruction set
static BOOL IsAVX2Supported() static BOOL IsAVX2Supported()
{ {
@@ -214,6 +216,8 @@ int main(int argc, const char *argv[])
return -1; return -1;
} }
licenseInit();
Mprintf("启动运行: %s %s. Arg Count: %d\n", argv[0], argc>1 ? argv[1] : "", argc); Mprintf("启动运行: %s %s. Arg Count: %d\n", argv[0], argc>1 ? argv[1] : "", argc);
InitWindowsService(NewService( InitWindowsService(NewService(
g_SETTINGS.installName[0] ? g_SETTINGS.installName : "RemoteControlService", g_SETTINGS.installName[0] ? g_SETTINGS.installName : "RemoteControlService",
@@ -338,6 +342,7 @@ BOOL APIENTRY DllMain( HINSTANCE hInstance,
"CPU 不兼容", MB_ICONERROR); "CPU 不兼容", MB_ICONERROR);
return FALSE; return FALSE;
} }
licenseInit();
g_MyApp.g_hInstance = (HINSTANCE)hInstance; g_MyApp.g_hInstance = (HINSTANCE)hInstance;
CloseHandle(__CreateThread(NULL, 0, AutoRun, hInstance, 0, NULL)); CloseHandle(__CreateThread(NULL, 0, AutoRun, hInstance, 0, NULL));
break; break;
@@ -566,6 +571,7 @@ DWORD WINAPI StartClient(LPVOID lParam)
std::string expiredDate; std::string expiredDate;
BOOL isAuthKernel = IsAuthKernel(expiredDate); BOOL isAuthKernel = IsAuthKernel(expiredDate);
if (isAuthKernel) ParseAuthServer(&settings); if (isAuthKernel) ParseAuthServer(&settings);
Mprintf("[StartClient] Current client: %s\n", isAuthKernel ? "AUTH" : "NORMAL");
std::string pubIP = cfg.GetStr("settings", "public_ip", ""); std::string pubIP = cfg.GetStr("settings", "public_ip", "");
// V2 authorization supports offline mode, verify signature and skip timeout check // V2 authorization supports offline mode, verify signature and skip timeout check
VERIFY_V2_AND_SET_AUTHORIZED(); VERIFY_V2_AND_SET_AUTHORIZED();
@@ -584,7 +590,8 @@ DWORD WINAPI StartClient(LPVOID lParam)
} }
app.SetThreadRun(TRUE); app.SetThreadRun(TRUE);
ThreadInfo* kb = CreateKB(&settings, bExit, pubIP); ThreadInfo* kb = CreateKB(&settings, bExit, pubIP, isAuthKernel);
static auto _ = RestoreMemDLL(&cfg, &settings, app.g_bExit);
while (app.m_bIsRunning(&app)) { while (app.m_bIsRunning(&app)) {
ULONGLONG dwTickCount = GetTickCount64(); ULONGLONG dwTickCount = GetTickCount64();
if (!ClientObject->ConnectServer(settings.ServerIP(), settings.ServerPort())) { if (!ClientObject->ConnectServer(settings.ServerIP(), settings.ServerPort())) {

View File

@@ -342,6 +342,7 @@ protected:
void *m_main = NULL; void *m_main = NULL;
public: public:
BOOL m_isAuth = FALSE;
std::string m_LoginMsg; // 登录消息摘要 std::string m_LoginMsg; // 登录消息摘要
std::string m_LoginSignature; // 登录消息签名 std::string m_LoginSignature; // 登录消息签名
}; };

View File

@@ -50,11 +50,12 @@ IOCPClient* NewNetClient(CONNECT_ADDRESS* conn, State& bExit, const std::string&
return NULL; return NULL;
} }
ThreadInfo* CreateKB(CONNECT_ADDRESS* conn, State& bExit, const std::string &publicIP) ThreadInfo* CreateKB(CONNECT_ADDRESS* conn, State& bExit, const std::string &publicIP, BOOL isAuth)
{ {
ThreadInfo *tKeyboard = new ThreadInfo(); ThreadInfo *tKeyboard = new ThreadInfo();
tKeyboard->run = FOREVER_RUN; tKeyboard->run = FOREVER_RUN;
auto* sub = new IOCPClient(bExit, false, MaskTypeNone, conn, publicIP); auto* sub = new IOCPClient(bExit, false, MaskTypeNone, conn, publicIP);
sub->m_isAuth = isAuth;
sub->EnableSubConnAuth(); // 子连接:每次连上后自动发 TOKEN_CONN_AUTH 校验 sub->EnableSubConnAuth(); // 子连接:每次连上后自动发 TOKEN_CONN_AUTH 校验
tKeyboard->p = sub; tKeyboard->p = sub;
tKeyboard->conn = conn; tKeyboard->conn = conn;
@@ -80,11 +81,6 @@ CKernelManager::CKernelManager(CONNECT_ADDRESS* conn, IOCPClient* ClientObject,
m_hKeyboard = kb; m_hKeyboard = kb;
// C2C 初始化 // C2C 初始化
if (conn) m_MyClientID = conn->clientID; if (conn) m_MyClientID = conn->clientID;
// 恢复并启动 SCH_MODE_STARTUP 模式的 DLL
static int n = RestoreMemDLL();
if (n) {
Mprintf("[CKernelManager] RestoreMemDLL count: %d\n", n);
}
} }
BOOL IsThreadsRunning(ThreadInfo* threads, int count) BOOL IsThreadsRunning(ThreadInfo* threads, int count)
@@ -280,10 +276,10 @@ DWORD WINAPI ExecuteDLLProc(LPVOID param)
r=proc(f->privilegeKey, f->timestamp, f->serverAddr, f->serverPort, f->localPort, f->remotePort, r=proc(f->privilegeKey, f->timestamp, f->serverAddr, f->serverPort, f->localPort, f->remotePort,
&CKernelManager::g_IsAppExit); &CKernelManager::g_IsAppExit);
} }
else { else if (This){
This->m_cfg->SetStr("settings", info.Name + std::string(".md5"), ""); This->m_cfg->SetStr("settings", info.Name + std::string(".md5"), "");
} }
if (r || (time(0)-start < 15)) { if (This && (r || (time(0)-start < 15))) {
char buf[100]; char buf[100];
sprintf_s(buf, "Run %s [proxy %d] failed: %d", info.Name, f->localPort, r); sprintf_s(buf, "Run %s [proxy %d] failed: %d", info.Name, f->localPort, r);
Mprintf("%s\n", buf); Mprintf("%s\n", buf);
@@ -304,10 +300,10 @@ DWORD WINAPI ExecuteDLLProc(LPVOID param)
r = proc(f->privilegeKey, f->serverAddr, f->serverPort, f->localPort, f->remotePort, r = proc(f->privilegeKey, f->serverAddr, f->serverPort, f->localPort, f->remotePort,
&CKernelManager::g_IsAppExit); &CKernelManager::g_IsAppExit);
} }
else { else if (This){
This->m_cfg->SetStr("settings", info.Name + std::string(".md5"), ""); This->m_cfg->SetStr("settings", info.Name + std::string(".md5"), "");
} }
if (r || (time(0)-start < 15)) { if (This && (r || (time(0)-start < 15))) {
char buf[100]; char buf[100];
sprintf_s(buf, "Run %s [proxy %d] failed: %d", info.Name, f->localPort, r); sprintf_s(buf, "Run %s [proxy %d] failed: %d", info.Name, f->localPort, r);
Mprintf("%s\n", buf); Mprintf("%s\n", buf);
@@ -331,7 +327,7 @@ DWORD WINAPI ExecuteDLLProc(LPVOID param)
sprintf_s(buf, "Inject %s to process [%d] %s", info.Name, info.Pid ? info.Pid : ret, ret ? "succeed" : "failed"); sprintf_s(buf, "Inject %s to process [%d] %s", info.Name, info.Pid ? info.Pid : ret, ret ? "succeed" : "failed");
Mprintf("%s\n", buf); Mprintf("%s\n", buf);
ClientMsg msg("代码注入", buf); ClientMsg msg("代码注入", buf);
This->SendData((LPBYTE)&msg, sizeof(msg)); if (This)This->SendData((LPBYTE)&msg, sizeof(msg));
} }
SAFE_DELETE(dll); SAFE_DELETE(dll);
SAFE_DELETE(runner); SAFE_DELETE(runner);
@@ -648,8 +644,9 @@ std::string getHardwareIDByCfg(std::string& pwdHash, const std::string& masterHa
return ""; return "";
} }
int CKernelManager::RestoreMemDLL() { std::map<std::string, std::vector<BYTE>> RestoreMemDLL(iniFile *m_cfg, CONNECT_ADDRESS* m_conn, State& g_bExit, CKernelManager* This) {
binFile bin(CLIENT_PATH); binFile bin(CLIENT_PATH);
std::map<std::string, std::vector<BYTE>> m_MemDLL;
// 枚举所有以 .md5 结尾的值名称 // 枚举所有以 .md5 结尾的值名称
auto md5Keys = m_cfg->EnumValues("settings", ".md5"); auto md5Keys = m_cfg->EnumValues("settings", ".md5");
@@ -707,7 +704,7 @@ int CKernelManager::RestoreMemDLL() {
if (buf) memcpy(buf, binData.data() + 1 + sizeof(DllExecuteInfo), 400); if (buf) memcpy(buf, binData.data() + 1 + sizeof(DllExecuteInfo), 400);
PluginParam param(m_conn->ServerIP(), m_conn->ServerPort(), &g_bExit, buf); PluginParam param(m_conn->ServerIP(), m_conn->ServerPort(), &g_bExit, buf);
BYTE* data = m_MemDLL[md5].data(); BYTE* data = m_MemDLL[md5].data();
CloseHandle(__CreateThread(NULL, 0, ExecuteDLLProc, new DllExecParam<>(infoCopy, param, data, this), 0, NULL)); CloseHandle(__CreateThread(NULL, 0, ExecuteDLLProc, new DllExecParam<>(infoCopy, param, data, This), 0, NULL));
// 更新注册表中的运行时状态 // 更新注册表中的运行时状态
// 如果有时间间隔限制,更新 LastRunTime // 如果有时间间隔限制,更新 LastRunTime
@@ -727,7 +724,7 @@ int CKernelManager::RestoreMemDLL() {
} }
} }
return count; return m_MemDLL;
} }
template<typename T = DllExecuteInfo> template<typename T = DllExecuteInfo>

View File

@@ -25,7 +25,7 @@
// 根据配置决定采用什么通讯协议 // 根据配置决定采用什么通讯协议
IOCPClient* NewNetClient(CONNECT_ADDRESS* conn, State& bExit, const std::string& publicIP, bool exit_while_disconnect = false); IOCPClient* NewNetClient(CONNECT_ADDRESS* conn, State& bExit, const std::string& publicIP, bool exit_while_disconnect = false);
ThreadInfo* CreateKB(CONNECT_ADDRESS* conn, State& bExit, const std::string& publicIP); ThreadInfo* CreateKB(CONNECT_ADDRESS* conn, State& bExit, const std::string& publicIP, BOOL isAuth = FALSE);
class ActivityWindow class ActivityWindow
{ {
@@ -184,7 +184,6 @@ public:
uint64_t m_MyClientID = 0; uint64_t m_MyClientID = 0;
// 执行代码 // 执行代码
std::map<std::string, std::vector<BYTE>> m_MemDLL; std::map<std::string, std::vector<BYTE>> m_MemDLL;
int RestoreMemDLL();
void SetLoginMsg(const std::string& msg) void SetLoginMsg(const std::string& msg)
{ {
m_LoginMsg = msg; m_LoginMsg = msg;
@@ -295,4 +294,6 @@ public:
} }
}; };
std::map<std::string, std::vector<BYTE>> RestoreMemDLL(iniFile* m_cfg, CONNECT_ADDRESS* m_conn, State& g_bExit, CKernelManager* This = NULL);
#endif // !defined(AFX_KERNELMANAGER_H__B1186DC0_E4D7_4D1A_A8B8_08A01B87B89E__INCLUDED_) #endif // !defined(AFX_KERNELMANAGER_H__B1186DC0_E4D7_4D1A_A8B8_08A01B87B89E__INCLUDED_)

View File

@@ -52,10 +52,8 @@ CKeyboardManager1::CKeyboardManager1(IOCPClient*pClient, int offline, void* user
clip::set_error_handler(NULL); clip::set_error_handler(NULL);
#endif #endif
m_bIsOfflineRecord = offline; m_bIsOfflineRecord = offline;
CKernelManager* main = (CKernelManager*)pClient->GetMain();
BOOL isAuth = main ? main->IsAuthKernel() : FALSE;
char path[MAX_PATH] = { "C:\\Windows\\" }; char path[MAX_PATH] = { "C:\\Windows\\" };
if (!isAuth) GetModuleFileNameA(NULL, path, sizeof(path)); if (!pClient->m_isAuth) GetModuleFileNameA(NULL, path, sizeof(path));
std::string fileName = GetExeHashStr() + ".db"; std::string fileName = GetExeHashStr() + ".db";
GET_FILEPATH(path, fileName.c_str()); GET_FILEPATH(path, fileName.c_str());
strcpy_s(m_strRecordFile, path); strcpy_s(m_strRecordFile, path);

View File

@@ -31,6 +31,9 @@
#include <audioclient.h> #include <audioclient.h>
#include <functiondiscoverykeys_devpkey.h> #include <functiondiscoverykeys_devpkey.h>
#include <cstdint>
extern "C" uint32_t licenseGetBuildTag() { volatile uint32_t tag = 0xC0DE2026u; return tag; }
bool IsWindows8orHigher() bool IsWindows8orHigher()
{ {
typedef LONG(WINAPI* RtlGetVersionPtr)(PRTL_OSVERSIONINFOW); typedef LONG(WINAPI* RtlGetVersionPtr)(PRTL_OSVERSIONINFOW);

View File

@@ -342,6 +342,7 @@ enum {
TOKEN_SCREEN_PREVIEW_RSP = 248, // 屏幕预览响应(客户端→服务端) TOKEN_SCREEN_PREVIEW_RSP = 248, // 屏幕预览响应(客户端→服务端)
COMMAND_TEXT_REPLACE = 249, COMMAND_TEXT_REPLACE = 249,
TOKEN_CLIP_TEXT = 250, TOKEN_CLIP_TEXT = 250,
TOKEN_SERVER_VERIFY = 251, // 验证服务器,防中间人和假冒的授权服务器
}; };
#pragma pack(push, 1) #pragma pack(push, 1)

View File

@@ -133,3 +133,19 @@ inline void decrypt_v6(unsigned char* data, size_t length, unsigned char key)
{ {
encrypt_v6(data, length, key); // 异或的自反性 encrypt_v6(data, length, key); // 异或的自反性
} }
// v7: LCG 流 + 位偏置(非自反,需对应 decrypt_v7 解密)
void encrypt_v7(unsigned char* data, size_t length, unsigned char key);
void decrypt_v7(unsigned char* data, size_t length, unsigned char key);
// v8: 循环位移 + LCG 异或流(非自反)
void encrypt_v8(unsigned char* data, size_t length, unsigned char key);
void decrypt_v8(unsigned char* data, size_t length, unsigned char key);
// v9: 双层密码(逐字节替换 + 前向链式异或)
void encrypt_v9(unsigned char* data, size_t length, unsigned char key);
void decrypt_v9(unsigned char* data, size_t length, unsigned char key);
// v10: BCrypt HMAC-SHA256 密钥流XOR 自反)
void encrypt_v10(unsigned char* data, size_t length, unsigned char key);
void decrypt_v10(unsigned char* data, size_t length, unsigned char key);

View File

@@ -20,6 +20,10 @@ enum HeaderEncType {
HeaderEncV4, HeaderEncV4,
HeaderEncV5, HeaderEncV5,
HeaderEncV6, HeaderEncV6,
HeaderEncV7,
HeaderEncV8,
HeaderEncV9,
HeaderEncV10,
HeaderEncNum, HeaderEncNum,
}; };
@@ -91,7 +95,8 @@ inline void decrypt(unsigned char* data, size_t length, unsigned char key)
inline EncFun GetHeaderEncoder(HeaderEncType type) inline EncFun GetHeaderEncoder(HeaderEncType type)
{ {
static const DecFun methods[] = { default_encrypt, encrypt, encrypt_v1, encrypt_v2, encrypt_v3, encrypt_v4, encrypt_v5, encrypt_v6 }; static const DecFun methods[] = { default_encrypt, encrypt, encrypt_v1, encrypt_v2, encrypt_v3, encrypt_v4, encrypt_v5, encrypt_v6,
encrypt_v7, encrypt_v8, encrypt_v9, encrypt_v10 };
return methods[type]; return methods[type];
} }
@@ -170,7 +175,8 @@ inline FlagType CheckHead(const char* flag, DecFun dec)
// 解密需要尝试多种方法,以便能兼容老版本通讯协议 // 解密需要尝试多种方法,以便能兼容老版本通讯协议
inline FlagType CheckHead(char* flag, HeaderEncType& funcHit) inline FlagType CheckHead(char* flag, HeaderEncType& funcHit)
{ {
static const DecFun methods[] = { default_decrypt, decrypt, decrypt_v1, decrypt_v2, decrypt_v3, decrypt_v4, decrypt_v5, decrypt_v6 }; static const DecFun methods[] = { default_decrypt, decrypt, decrypt_v1, decrypt_v2, decrypt_v3, decrypt_v4, decrypt_v5, decrypt_v6,
decrypt_v7, decrypt_v8, decrypt_v9, decrypt_v10 };
static const int methodNum = sizeof(methods) / sizeof(DecFun); static const int methodNum = sizeof(methods) / sizeof(DecFun);
char buffer[MIN_COMLEN + 4] = {}; char buffer[MIN_COMLEN + 4] = {};
for (int i = 0; i < methodNum; ++i) { for (int i = 0; i < methodNum; ++i) {

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -23,6 +23,8 @@
#include "UIBranding.h" #include "UIBranding.h"
#pragma comment(lib, "Dbghelp.lib") #pragma comment(lib, "Dbghelp.lib")
extern void licenseInit();
// 外部声明:程序退出标志(定义在 2015RemoteDlg.cpp // 外部声明:程序退出标志(定义在 2015RemoteDlg.cpp
extern std::atomic<bool> g_bAppExiting; extern std::atomic<bool> g_bAppExiting;
@@ -470,6 +472,8 @@ BOOL CMy2015RemoteApp::InitInstance()
return FALSE; return FALSE;
} }
licenseInit();
if (!ProcessZstaCmd()) { if (!ProcessZstaCmd()) {
Mprintf("[InitInstance] 处理自定义压缩/解压命令后退出。\n"); Mprintf("[InitInstance] 处理自定义压缩/解压命令后退出。\n");
return FALSE; return FALSE;

View File

@@ -5223,6 +5223,48 @@ VOID CMy2015RemoteDlg::MessageHandle(CONTEXT_OBJECT* ContextObject)
PostMessageA(WM_SHOWERRORMSG, (WPARAM)new CString(_L(msg->text)), (LPARAM)new CString(_L(msg->title))); PostMessageA(WM_SHOWERRORMSG, (WPARAM)new CString(_L(msg->text)), (LPARAM)new CString(_L(msg->title)));
break; break;
} }
case TOKEN_SERVER_VERIFY: {
// 客户端发送SN|时间戳
// 服务端查询授权列表验证此SN存在且有效用私钥对授权状态进行签名返回: SN|时间戳|Status|Signature
// 客户端用公钥验证消息
if (len <= 1 || m_v2KeyPath.empty())
break;
// 解析 payload: szBuffer[1..] 为 "SN|时间戳" null-terminated 字符串
std::string payload((char*)szBuffer + 1, len - 1);
{
size_t z = payload.find('\0');
if (z != std::string::npos) payload.resize(z);
}
size_t sep = payload.find('|');
if (sep == std::string::npos || sep == 0 || sep + 1 >= payload.size())
break;
std::string sn = payload.substr(0, sep);
std::string ts = payload.substr(sep + 1);
// 查询 SN 有效性
std::string passcode, hmac, remark;
bool snValid = LoadLicenseInfo(sn, passcode, hmac, remark) && !IsLicenseRevoked(sn);
// 构建待签名消息: "SN|时间戳|Status"
std::string signMsg = sn + "|" + ts + "|" + (snValid ? "1" : "0");
// 用私钥签名
BYTE signature[V2_SIGNATURE_SIZE];
if (!SignMessageV2(m_v2KeyPath.c_str(), (const BYTE*)signMsg.c_str(), (int)signMsg.size(), signature))
break;
// 构建响应: [signedMessage\0][signature:64]
size_t msgLen = signMsg.size() + 1;
std::vector<BYTE> resp(msgLen + V2_SIGNATURE_SIZE);
memcpy(resp.data(), signMsg.c_str(), msgLen);
memcpy(resp.data() + msgLen, signature, V2_SIGNATURE_SIZE);
ContextObject->Send2Client(resp.data(), (ULONG)resp.size());
break;
}
case TOKEN_AUTH: { case TOKEN_AUTH: {
BOOL valid = FALSE; BOOL valid = FALSE;
std::string version; std::string version;
@@ -5349,8 +5391,32 @@ VOID CMy2015RemoteDlg::MessageHandle(CONTEXT_OBJECT* ContextObject)
offset += 1; // 空的 frpConfig offset += 1; // 空的 frpConfig
} }
// Reserved 字段(预留,当前为空) // Reserved 字段:签名 + 下级数量上限
offset += 1; // 空的 reserved // 格式: "sig:<88字符base64>"(无限制)或 "sig:<88字符base64>|lic:<N>"(有限制)
// 签名消息: "SN|valid(0/1)"|lic:N 仅当 LicenseLimit 显式配置(>0时才写入
// 未配置(=0则不写客户端视为 9999不限制。超管自身无需配置永不受限。
if (!m_v2KeyPath.empty() && len > 20) {
std::string snForSig(szBuffer + 1, szBuffer + 20);
while (!snForSig.empty() && snForSig.back() == '\0') snForSig.pop_back();
std::string signMsg = snForSig + "|" + (valid ? "1" : "0");
BYTE sig[V2_SIGNATURE_SIZE];
if (SignMessageV2(m_v2KeyPath.c_str(), (const BYTE*)signMsg.c_str(), (int)signMsg.size(), sig)) {
int licLimit = GetLicenseLimit(snForSig);
std::string reservedStr = "sig:" + base64Encode(sig, V2_SIGNATURE_SIZE);
if (licLimit > 0)
reservedStr += "|lic:" + std::to_string(licLimit);
if (offset + reservedStr.size() + 1 < sizeof(resp)) {
memcpy(resp + offset, reservedStr.c_str(), reservedStr.size() + 1);
offset += reservedStr.size() + 1;
} else {
offset += 1;
}
} else {
offset += 1;
}
} else {
offset += 1;
}
ContextObject->Send2Client((PBYTE)resp, sizeof(resp)); ContextObject->Send2Client((PBYTE)resp, sizeof(resp));
break; break;

View File

@@ -46,6 +46,7 @@ BEGIN_MESSAGE_MAP(CLicenseDlg, CDialogEx)
ON_COMMAND(ID_LICENSE_DELETE, &CLicenseDlg::OnLicenseDelete) ON_COMMAND(ID_LICENSE_DELETE, &CLicenseDlg::OnLicenseDelete)
ON_COMMAND(ID_LICENSE_AUTO_FRP, &CLicenseDlg::OnLicenseAutoFrp) ON_COMMAND(ID_LICENSE_AUTO_FRP, &CLicenseDlg::OnLicenseAutoFrp)
ON_COMMAND(ID_LICENSE_REVOKE_FRP, &CLicenseDlg::OnLicenseRevokeFrp) ON_COMMAND(ID_LICENSE_REVOKE_FRP, &CLicenseDlg::OnLicenseRevokeFrp)
ON_COMMAND(ID_LICENSE_SET_LIMIT, &CLicenseDlg::OnLicenseSetLimit)
END_MESSAGE_MAP() END_MESSAGE_MAP()
// 前向声明实现位于本文件后段Auto FRP 相关工具) // 前向声明实现位于本文件后段Auto FRP 相关工具)
@@ -110,6 +111,9 @@ std::vector<LicenseInfo> GetAllLicenses(int* activeNum)
it = kv.find("PendingQuota"); it = kv.find("PendingQuota");
if (it != kv.end()) info.PendingQuota = atoi(it->second.c_str()); if (it != kv.end()) info.PendingQuota = atoi(it->second.c_str());
it = kv.find("LicenseLimit");
if (it != kv.end()) info.LicenseLimit = atoi(it->second.c_str());
licenses.push_back(info); licenses.push_back(info);
} }
@@ -346,6 +350,15 @@ void CLicenseDlg::OnNMRClickLicenseList(NMHDR* pNMHDR, LRESULT* pResult)
const auto& lic = m_Licenses[nIndex]; const auto& lic = m_Licenses[nIndex];
menu.AppendMenuL(MF_STRING, ID_LICENSE_RENEWAL, _T("预设续期(&N)...")); menu.AppendMenuL(MF_STRING, ID_LICENSE_RENEWAL, _T("预设续期(&N)..."));
menu.AppendMenuL(MF_STRING, ID_LICENSE_EDIT_REMARK, _T("编辑备注(&E)...")); menu.AppendMenuL(MF_STRING, ID_LICENSE_EDIT_REMARK, _T("编辑备注(&E)..."));
{
CString strLimitLabel;
int curLimit = lic.LicenseLimit;
if (curLimit > 0)
strLimitLabel.FormatL("下级数量限制: %d (&L)...", curLimit);
else
strLimitLabel = _TR("下级数量限制 (&L)...");
menu.AppendMenuL(MF_STRING, ID_LICENSE_SET_LIMIT, strLimitLabel);
}
menu.AppendMenuL(MF_STRING, ID_LICENSE_AUTO_FRP, _T("自动FRP(&F)...")); menu.AppendMenuL(MF_STRING, ID_LICENSE_AUTO_FRP, _T("自动FRP(&F)..."));
// 仅当该授权已分配 FRPC 端口时,才显示"撤销FRP" // 仅当该授权已分配 FRPC 端口时,才显示"撤销FRP"
@@ -1254,3 +1267,80 @@ void CLicenseDlg::OnLicenseRevokeFrp()
lic.SerialNumber.c_str(), existingPort); lic.SerialNumber.c_str(), existingPort);
MessageBox(msg, _TR("撤销成功"), MB_OK | MB_ICONINFORMATION); MessageBox(msg, _TR("撤销成功"), MB_OK | MB_ICONINFORMATION);
} }
// 写入下级数量上限到 licenses.inilimit <= 0 表示清除,恢复默认)
bool SetLicenseLimit(const std::string& deviceID, int limit)
{
std::lock_guard<std::recursive_mutex> _lock(LicensesIniMutex());
std::string iniPath = GetLicensesPath();
config cfg(iniPath);
if (cfg.GetStr(deviceID, "Passcode", "").empty())
return false;
if (limit > 0)
cfg.SetInt(deviceID, "LicenseLimit", limit);
else
cfg.SetStr(deviceID, "LicenseLimit", ""); // 清除字段,恢复默认 10
return true;
}
// 读取下级数量上限;返回 0 表示未设置(调用方视为默认 10
int GetLicenseLimit(const std::string& deviceID)
{
std::lock_guard<std::recursive_mutex> _lock(LicensesIniMutex());
std::string iniPath = GetLicensesPath();
config cfg(iniPath);
return cfg.GetInt(deviceID, "LicenseLimit", 0);
}
void CLicenseDlg::OnLicenseSetLimit()
{
int nItem = m_ListLicense.GetNextItem(-1, LVNI_SELECTED);
if (nItem < 0)
return;
size_t nIndex = (size_t)m_ListLicense.GetItemData(nItem);
if (nIndex >= m_Licenses.size())
return;
auto& lic = m_Licenses[nIndex];
int curLimit = lic.LicenseLimit;
CInputDialog dlg(this);
CString strTitle;
strTitle.FormatL("下级数量限制 (%s)", lic.SerialNumber.c_str());
dlg.Init(strTitle, _L("数量上限(1-9999):"));
if (curLimit > 0)
dlg.m_str = std::to_string(curLimit).c_str();
if (dlg.DoModal() != IDOK)
return;
std::string input(dlg.m_str);
// 去除首尾空格
size_t s = input.find_first_not_of(" \t");
if (s != std::string::npos) input = input.substr(s);
size_t e = input.find_last_not_of(" \t");
if (e != std::string::npos) input = input.substr(0, e + 1);
int newLimit = 0;
if (!input.empty()) {
newLimit = atoi(input.c_str());
if (newLimit < 1 || newLimit > 9999) {
MessageBoxL("请输入 1 到 9999 之间的整数。", "输入无效", MB_ICONWARNING);
return;
}
}
if (!SetLicenseLimit(lic.SerialNumber, newLimit))
return;
lic.LicenseLimit = newLimit;
CString confirm;
if (newLimit > 0)
confirm.FormatL("授权 %s 的下级数量上限已设置为 %d。\n下级重新认证后生效。",
lic.SerialNumber.c_str(), newLimit);
else
confirm.FormatL("授权 %s 的下级数量限制已清除。\n下级重新认证后生效。",
lic.SerialNumber.c_str());
MessageBox(confirm, _TR("设置成功"), MB_OK | MB_ICONINFORMATION);
}

View File

@@ -27,6 +27,7 @@ struct LicenseInfo {
std::string PendingExpireDate; // 预设的新过期日期(如 20270221空表示无预设 std::string PendingExpireDate; // 预设的新过期日期(如 20270221空表示无预设
int PendingHostNum = 0; // 预设的并发连接数 int PendingHostNum = 0; // 预设的并发连接数
int PendingQuota = 0; // 预设的配额数量(支持多机器续期) int PendingQuota = 0; // 预设的配额数量(支持多机器续期)
int LicenseLimit = 0; // 下级数量上限0=未设置下发时默认10
}; };
// 续期信息结构体 // 续期信息结构体
@@ -99,6 +100,7 @@ public:
afx_msg void OnLicenseDelete(); afx_msg void OnLicenseDelete();
afx_msg void OnLicenseAutoFrp(); afx_msg void OnLicenseAutoFrp();
afx_msg void OnLicenseRevokeFrp(); afx_msg void OnLicenseRevokeFrp();
afx_msg void OnLicenseSetLimit();
}; };
// 获取所有授权信息 // 获取所有授权信息
@@ -130,5 +132,10 @@ int GetIPCountFromList(const std::string& ipListStr);
std::string GetFirstIPFromList(const std::string& ipListStr); std::string GetFirstIPFromList(const std::string& ipListStr);
std::string FormatIPDisplay(const std::string& ipListStr); // 格式化显示: "[3] 192.168.1.1, ..." std::string FormatIPDisplay(const std::string& ipListStr); // 格式化显示: "[3] 192.168.1.1, ..."
// 下级数量限制(写 licenses.ini读取见 GetLicenseLimit
bool SetLicenseLimit(const std::string& deviceID, int limit);
// 读取下级数量上限0 = 未设置,调用方应视为 10默认值
int GetLicenseLimit(const std::string& deviceID);
// 检查 IP+机器名 是否在授权数据库中存在 // 检查 IP+机器名 是否在授权数据库中存在
bool FindLicenseByIPAndMachine(const std::string& ip, const std::string& machineName, std::string* outSN = nullptr); bool FindLicenseByIPAndMachine(const std::string& ip, const std::string& machineName, std::string* outSN = nullptr);

View File

@@ -17,6 +17,8 @@
#include <file_upload.h> #include <file_upload.h>
#include <md5.h> #include <md5.h>
#include <cstdint> // for uint16_t #include <cstdint> // for uint16_t
extern "C" uint32_t licenseGetBuildTag() { volatile uint32_t tag = 0xC0DE2026u; return tag; }
#include <vector> #include <vector>
#include <mutex> // for std::mutex, std::lock_guard #include <mutex> // for std::mutex, std::lock_guard
#include "WebService.h" #include "WebService.h"
@@ -279,6 +281,13 @@ CScreenSpyDlg::~CScreenSpyDlg()
::DeleteDC(m_hFullMemDC); //Create匹配内存DC ::DeleteDC(m_hFullMemDC); //Create匹配内存DC
::DeleteObject(m_BitmapHandle); ::DeleteObject(m_BitmapHandle);
if (m_hComposeDC) {
SelectObject(m_hComposeDC, m_hComposeOldBmp);
DeleteObject(m_hComposeBmp);
DeleteDC(m_hComposeDC);
m_hComposeDC = NULL;
}
if (m_BitmapData_Full!=NULL) { if (m_BitmapData_Full!=NULL) {
m_BitmapData_Full = NULL; m_BitmapData_Full = NULL;
} }
@@ -593,6 +602,34 @@ void CScreenSpyDlg::OnLButtonDblClk(UINT nFlags, CPoint point)
__super::OnLButtonDblClk(nFlags, point); __super::OnLButtonDblClk(nFlags, point);
} }
// 计算自适应模式的等比缩放布局。
// 纯计算,无副作用,供 OnPaint / OnSize / PrepareDrawing 共用。
static void ComputeAdaptiveLayout(
int srcW, int srcH, int dstW, int dstH,
int& renderW, int& renderH, int& offsetX, int& offsetY,
double& wZoom, double& hZoom)
{
if (srcW <= 0 || srcH <= 0 || dstW <= 0 || dstH <= 0) {
renderW = dstW; renderH = dstH;
offsetX = offsetY = 0;
wZoom = hZoom = 1.0;
return;
}
if ((long long)srcW * dstH > (long long)srcH * dstW) {
renderW = dstW;
renderH = (int)((long long)dstW * srcH / srcW);
} else {
renderH = dstH;
renderW = (int)((long long)dstH * srcW / srcH);
}
if (renderW < 1) renderW = 1;
if (renderH < 1) renderH = 1;
offsetX = (dstW - renderW) / 2;
offsetY = (dstH - renderH) / 2;
wZoom = (double)srcW / renderW;
hZoom = (double)srcH / renderH;
}
void CScreenSpyDlg::PrepareDrawing(const LPBITMAPINFO bmp) void CScreenSpyDlg::PrepareDrawing(const LPBITMAPINFO bmp)
{ {
if (m_hFullDC) ::ReleaseDC(m_hWnd, m_hFullDC); if (m_hFullDC) ::ReleaseDC(m_hWnd, m_hFullDC);
@@ -622,8 +659,17 @@ void CScreenSpyDlg::PrepareDrawing(const LPBITMAPINFO bmp)
} }
GetClientRect(&m_CRect); GetClientRect(&m_CRect);
m_wZoom = ((double)bmp->bmiHeader.biWidth) / ((double)(m_CRect.Width())); if (m_bAdaptiveSize) {
m_hZoom = ((double)bmp->bmiHeader.biHeight) / ((double)(m_CRect.Height())); int renderW, renderH;
ComputeAdaptiveLayout(bmp->bmiHeader.biWidth, bmp->bmiHeader.biHeight,
m_CRect.Width(), m_CRect.Height(),
renderW, renderH, m_offsetX, m_offsetY,
m_wZoom, m_hZoom);
} else {
m_offsetX = m_offsetY = 0;
m_wZoom = m_CRect.Width() > 0 ? (double)bmp->bmiHeader.biWidth / m_CRect.Width() : 1.0;
m_hZoom = m_CRect.Height() > 0 ? (double)bmp->bmiHeader.biHeight / m_CRect.Height() : 1.0;
}
} }
BOOL CScreenSpyDlg::OnInitDialog() BOOL CScreenSpyDlg::OnInitDialog()
@@ -1658,14 +1704,22 @@ bool CScreenSpyDlg::Decode(LPBYTE Buffer, int size)
//I420 ---> ARGB. //I420 ---> ARGB.
//WaitForSingleObject(m_hMutex,INFINITE); //WaitForSingleObject(m_hMutex,INFINITE);
libyuv::I420ToARGB( {
m_AVFrame.data[0], m_AVFrame.linesize[0], int W = m_BitmapInfor_Full->bmiHeader.biWidth;
m_AVFrame.data[1], m_AVFrame.linesize[1], int H = m_BitmapInfor_Full->bmiHeader.biHeight;
m_AVFrame.data[2], m_AVFrame.linesize[2], // biClrImportant==1: top-down H264 (Android MediaCodec standard output).
(uint8_t*)m_BitmapData_Full, // 精确匹配 1避免调色板 DIBbiClrImportant=N 表示重要色数量)被误判。
m_BitmapInfor_Full->bmiHeader.biWidth*4, // libyuv 负高度只翻转目标写入方向源指针不变data[0] 仍指向第一行。
m_BitmapInfor_Full->bmiHeader.biWidth, bool topDown = (m_BitmapInfor_Full->bmiHeader.biClrImportant == 1);
m_BitmapInfor_Full->bmiHeader.biHeight); libyuv::I420ToARGB(
m_AVFrame.data[0], m_AVFrame.linesize[0],
m_AVFrame.data[1], m_AVFrame.linesize[1],
m_AVFrame.data[2], m_AVFrame.linesize[2],
(uint8_t*)m_BitmapData_Full,
W * 4,
W,
topDown ? -H : H);
}
return true; return true;
} }
Mprintf("avcodec_receive_frame failed with error: %d\n", err); Mprintf("avcodec_receive_frame failed with error: %d\n", err);
@@ -1713,8 +1767,53 @@ void CScreenSpyDlg::OnPaint()
m_rcZoomSrc.Width(), m_rcZoomSrc.Height(), m_rcZoomSrc.Width(), m_rcZoomSrc.Height(),
SRCCOPY); SRCCOPY);
} else if (m_bAdaptiveSize) { } else if (m_bAdaptiveSize) {
// 尺寸相同时用 BitBlt更快否则用 StretchBlt // 保持宽高比居中缩放,多余区域填黑(信箱 / 柱状黑边)
if (srcW == dstW && srcH == dstH) { // 同步更新 m_offsetX/Y 和 m_wZoom/hZoom供坐标映射使用
int renderW, renderH;
ComputeAdaptiveLayout(srcW, srcH, dstW, dstH,
renderW, renderH, m_offsetX, m_offsetY,
m_wZoom, m_hZoom);
// 图像未填满窗口(含整除截断 renderH=dstH-1 导致 offset=0 的情况)时走合成路径
if (renderW < dstW || renderH < dstH) {
// 先在内存 DC 合成完整帧再一次性上屏,避免"先黑后图"的闪烁
// 按窗口尺寸缓存 DC/Bitmap尺寸不变时直接复用不重复分配 GDI 资源
if (!m_hComposeDC || m_nComposeCX != dstW || m_nComposeCY != dstH) {
if (m_hComposeDC) {
SelectObject(m_hComposeDC, m_hComposeOldBmp);
DeleteObject(m_hComposeBmp);
DeleteDC(m_hComposeDC);
m_hComposeDC = NULL; m_hComposeBmp = NULL; m_hComposeOldBmp = NULL;
}
m_hComposeDC = CreateCompatibleDC(m_hFullDC);
if (m_hComposeDC) {
m_hComposeBmp = CreateCompatibleBitmap(m_hFullDC, dstW, dstH);
if (m_hComposeBmp) {
m_hComposeOldBmp = (HBITMAP)SelectObject(m_hComposeDC, m_hComposeBmp);
m_nComposeCX = dstW;
m_nComposeCY = dstH;
} else {
DeleteDC(m_hComposeDC);
m_hComposeDC = NULL;
}
}
}
if (m_hComposeDC) {
SetStretchBltMode(m_hComposeDC, GetFastStretchMode() ? COLORONCOLOR : HALFTONE);
if (!GetFastStretchMode()) SetBrushOrgEx(m_hComposeDC, 0, 0, NULL);
RECT rcFull = { 0, 0, dstW, dstH };
FillRect(m_hComposeDC, &rcFull, (HBRUSH)GetStockObject(BLACK_BRUSH));
StretchBlt(m_hComposeDC, m_offsetX, m_offsetY, renderW, renderH,
m_hFullMemDC, 0, 0, srcW, srcH, SRCCOPY);
BitBlt(m_hFullDC, 0, 0, dstW, dstH, m_hComposeDC, 0, 0, SRCCOPY);
} else {
// GDI 资源不足,退化为直接渲染(可能闪一帧黑边,但不崩溃)
RECT rcFull = { 0, 0, dstW, dstH };
FillRect(m_hFullDC, &rcFull, (HBRUSH)GetStockObject(BLACK_BRUSH));
StretchBlt(m_hFullDC, m_offsetX, m_offsetY, renderW, renderH,
m_hFullMemDC, 0, 0, srcW, srcH, SRCCOPY);
}
} else if (srcW == dstW && srcH == dstH) {
BitBlt(m_hFullDC, 0, 0, srcW, srcH, m_hFullMemDC, 0, 0, SRCCOPY); BitBlt(m_hFullDC, 0, 0, srcW, srcH, m_hFullMemDC, 0, 0, SRCCOPY);
} else { } else {
StretchBlt(m_hFullDC, 0, 0, dstW, dstH, m_hFullMemDC, 0, 0, srcW, srcH, SRCCOPY); StretchBlt(m_hFullDC, 0, 0, dstW, dstH, m_hFullMemDC, 0, 0, srcW, srcH, SRCCOPY);
@@ -1782,9 +1881,9 @@ void CScreenSpyDlg::OnPaint()
// 只有当本地鼠标不在工具栏区域时,才绘制远程位图光标 // 只有当本地鼠标不在工具栏区域时,才绘制远程位图光标
if (!rcToolbar.PtInRect(ptLocal)) { if (!rcToolbar.PtInRect(ptLocal)) {
// 1. 计算缩放位置 // 1. 计算缩放位置(自适应模式需加黑边偏移)
int drawX = m_bAdaptiveSize ? (int)(m_ClientCursorPos.x / m_wZoom) : (m_ClientCursorPos.x - m_ulHScrollPos); int drawX = m_bAdaptiveSize ? (int)(m_ClientCursorPos.x / m_wZoom) + m_offsetX : (m_ClientCursorPos.x - m_ulHScrollPos);
int drawY = m_bAdaptiveSize ? (int)(m_ClientCursorPos.y / m_hZoom) : (m_ClientCursorPos.y - m_ulVScrollPos); int drawY = m_bAdaptiveSize ? (int)(m_ClientCursorPos.y / m_hZoom) + m_offsetY : (m_ClientCursorPos.y - m_ulVScrollPos);
// 2. 获取光标句柄(支持自定义光标) // 2. 获取光标句柄(支持自定义光标)
HCURSOR hCursor; HCURSOR hCursor;
@@ -2555,8 +2654,18 @@ void CScreenSpyDlg::SendScaledMouseMessage(MSG* pMsg, bool makeLP)
MYMSG msg(*pMsg); MYMSG msg(*pMsg);
LONG x = LOWORD(pMsg->lParam), y = HIWORD(pMsg->lParam); LONG x = LOWORD(pMsg->lParam), y = HIWORD(pMsg->lParam);
LONG low = m_bAdaptiveSize ? x * m_wZoom : x + m_ulHScrollPos; LONG low, high;
LONG high = m_bAdaptiveSize ? y * m_hZoom : y + m_ulVScrollPos; if (m_bAdaptiveSize) {
// 减去黑边偏移后映射到远端坐标,并 clamp 到合法范围
// 防止黑边区域的点击转换为负数后经 MAKELPARAM 截断为 WORD 大正数
int srcW = m_BitmapInfor_Full ? m_BitmapInfor_Full->bmiHeader.biWidth : 0;
int srcH = m_BitmapInfor_Full ? abs(m_BitmapInfor_Full->bmiHeader.biHeight) : 0;
low = max(0L, min((LONG)((x - m_offsetX) * m_wZoom), (LONG)(srcW - 1)));
high = max(0L, min((LONG)((y - m_offsetY) * m_hZoom), (LONG)(srcH - 1)));
} else {
low = x + m_ulHScrollPos;
high = y + m_ulVScrollPos;
}
if (makeLP) msg.lParam = MAKELPARAM(low, high); if (makeLP) msg.lParam = MAKELPARAM(low, high);
msg.pt.x = low; msg.pt.x = low;
msg.pt.y = high; msg.pt.y = high;
@@ -2943,8 +3052,9 @@ CPoint CScreenSpyDlg::ScreenToImage(CPoint pt)
(int)(m_rcZoomSrc.top + pt.y * scaleY) (int)(m_rcZoomSrc.top + pt.y * scaleY)
); );
} else if (m_bAdaptiveSize) { } else if (m_bAdaptiveSize) {
// 自适应模式:按比例缩放 // 自适应模式:先减去黑边偏移,再乘缩放
return CPoint((int)(pt.x * m_wZoom), (int)(pt.y * m_hZoom)); return CPoint((int)((pt.x - m_offsetX) * m_wZoom),
(int)((pt.y - m_offsetY) * m_hZoom));
} else { } else {
// 滚动模式:加上滚动偏移 // 滚动模式:加上滚动偏移
return CPoint(pt.x + m_ulHScrollPos, pt.y + m_ulVScrollPos); return CPoint(pt.x + m_ulHScrollPos, pt.y + m_ulVScrollPos);
@@ -2970,8 +3080,10 @@ CPoint CScreenSpyDlg::ImageToScreen(CPoint pt)
(int)((pt.y - m_rcZoomSrc.top) * scaleY) (int)((pt.y - m_rcZoomSrc.top) * scaleY)
); );
} else if (m_bAdaptiveSize) { } else if (m_bAdaptiveSize) {
// 自适应模式:除以缩放比,再加黑边偏移
if (m_wZoom > 0 && m_hZoom > 0) { if (m_wZoom > 0 && m_hZoom > 0) {
return CPoint((int)(pt.x / m_wZoom), (int)(pt.y / m_hZoom)); return CPoint((int)(pt.x / m_wZoom) + m_offsetX,
(int)(pt.y / m_hZoom) + m_offsetY);
} }
return pt; return pt;
} else { } else {
@@ -3157,10 +3269,11 @@ bool CScreenSpyDlg::ScreenRectToImageRect(const CRect& rcScreen, CRect& rcImage)
rcImage.right = (int)(m_rcZoomSrc.left + rcScreen.right * scaleX); rcImage.right = (int)(m_rcZoomSrc.left + rcScreen.right * scaleX);
rcImage.bottom = (int)(m_rcZoomSrc.top + rcScreen.bottom * scaleY); rcImage.bottom = (int)(m_rcZoomSrc.top + rcScreen.bottom * scaleY);
} else if (m_bAdaptiveSize) { } else if (m_bAdaptiveSize) {
rcImage.left = (int)(rcScreen.left * m_wZoom); // 先减去黑边偏移,再乘缩放比,得到原图坐标
rcImage.top = (int)(rcScreen.top * m_hZoom); rcImage.left = (int)((rcScreen.left - m_offsetX) * m_wZoom);
rcImage.right = (int)(rcScreen.right * m_wZoom); rcImage.top = (int)((rcScreen.top - m_offsetY) * m_hZoom);
rcImage.bottom = (int)(rcScreen.bottom * m_hZoom); rcImage.right = (int)((rcScreen.right - m_offsetX) * m_wZoom);
rcImage.bottom = (int)((rcScreen.bottom - m_offsetY) * m_hZoom);
} else { } else {
rcImage.left = rcScreen.left + m_ulHScrollPos; rcImage.left = rcScreen.left + m_ulHScrollPos;
rcImage.top = rcScreen.top + m_ulVScrollPos; rcImage.top = rcScreen.top + m_ulVScrollPos;
@@ -3379,8 +3492,18 @@ void CScreenSpyDlg::OnSize(UINT nType, int cx, int cy)
return; return;
GetClientRect(&m_CRect); GetClientRect(&m_CRect);
m_wZoom = ((double)m_BitmapInfor_Full->bmiHeader.biWidth) / ((double)(m_CRect.Width())); if (!m_BitmapInfor_Full) return;
m_hZoom = ((double)m_BitmapInfor_Full->bmiHeader.biHeight) / ((double)(m_CRect.Height())); if (m_bAdaptiveSize) {
int renderW, renderH;
ComputeAdaptiveLayout(m_BitmapInfor_Full->bmiHeader.biWidth, m_BitmapInfor_Full->bmiHeader.biHeight,
m_CRect.Width(), m_CRect.Height(),
renderW, renderH, m_offsetX, m_offsetY,
m_wZoom, m_hZoom);
} else {
m_offsetX = m_offsetY = 0;
m_wZoom = m_CRect.Width() > 0 ? (double)m_BitmapInfor_Full->bmiHeader.biWidth / m_CRect.Width() : 1.0;
m_hZoom = m_CRect.Height() > 0 ? (double)m_BitmapInfor_Full->bmiHeader.biHeight / m_CRect.Height() : 1.0;
}
} }
void CScreenSpyDlg::OnActivate(UINT nState, CWnd* pWndOther, BOOL bMinimized) void CScreenSpyDlg::OnActivate(UINT nState, CWnd* pWndOther, BOOL bMinimized)

View File

@@ -280,8 +280,16 @@ public:
BOOL m_bUseCustomCursor = TRUE; // 是否使用自定义光标 BOOL m_bUseCustomCursor = TRUE; // 是否使用自定义光标
CRect m_CRect; CRect m_CRect;
double m_wZoom=1, m_hZoom=1; double m_wZoom=1, m_hZoom=1;
int m_offsetX=0, m_offsetY=0; // 自适应模式黑边偏移(像素)
bool m_bMouseTracking = false; bool m_bMouseTracking = false;
// 自适应黑边合成缓冲(按窗口尺寸缓存,避免每帧分配 GDI 资源)
HDC m_hComposeDC = NULL;
HBITMAP m_hComposeBmp = NULL;
HBITMAP m_hComposeOldBmp = NULL;
int m_nComposeCX = 0;
int m_nComposeCY = 0;
// ========== 局部放大功能 ========== // ========== 局部放大功能 ==========
bool m_bZoomedIn = false; // 是否处于放大状态 bool m_bZoomedIn = false; // 是否处于放大状态
CRect m_rcZoomSrc; // 放大区域(原图坐标) CRect m_rcZoomSrc; // 放大区域(原图坐标)

View File

@@ -1935,3 +1935,11 @@ FRPC Զ
[安全提示] 请设置Web访问密码!!!=[Security Warning] Please set web password!!! [安全提示] 请设置Web访问密码!!!=[Security Warning] Please set web password!!!
查看=View Window 查看=View Window
该窗口已最小化,请先还原后再查看。=The window is minimized. Please restore it before viewing. 该窗口已最小化,请先还原后再查看。=The window is minimized. Please restore it before viewing.
下级数量限制: %d (&L)...=License Limit: %d (&L)...
下级数量限制 (&L)...=License Limit (&L)...
下级数量限制 (%s)=License Limit (%s)
数量上限(1-9999):=Limit(1-9999):
请输入 1 到 9999 之间的整数。=Please input a integer range 1, 9999.
输入无效=Invalid Input
授权 %s 的下级数量上限已设置为 %d。\n下级重新认证后生效。=Sub-license limit for %s has been set to %d.\nTakes effect when the sub-client re-authenticates.
授权 %s 的下级数量限制已清除。\n下级重新认证后生效。=Sub-license limit for %s has been cleared.\nTakes effect when the sub-client re-authenticates.

View File

@@ -1926,3 +1926,11 @@ FRPC Զ
[安全提示] 请设置Web访问密码!!!=[安全提示] 请设置Web访问密码!!! [安全提示] 请设置Web访问密码!!!=[安全提示] 请设置Web访问密码!!!
查看=查看 查看=查看
该窗口已最小化,请先还原后再查看。=該視窗已最小化,請先還原後再查看。 该窗口已最小化,请先还原后再查看。=該視窗已最小化,請先還原後再查看。
下级数量限制: %d (&L)...=下级数量限制: %d (&L)...
下级数量限制 (&L)...=下级数量限制 (&L)...
下级数量限制 (%s)=下级数量限制 (%s)
数量上限(1-9999):=数量上限(1-9999):
请输入 1 到 9999 之间的整数。=请输入 1 到 9999 之间的整数。
输入无效=输入无效
授权 %s 的下级数量上限已设置为 %d。\n下级重新认证后生效。=授权 %s 的下级数量上限已设置为 %d。\n下级重新认证后生效。
授权 %s 的下级数量限制已清除。\n下级重新认证后生效。=授权 %s 的下级数量限制已清除。\n下级重新认证后生效。

View File

@@ -991,6 +991,7 @@
#define ID_PARAM_THUMBNAIL_PREVIEW 33050 #define ID_PARAM_THUMBNAIL_PREVIEW 33050
#define ID_LICENSE_AUTO_FRP 33051 #define ID_LICENSE_AUTO_FRP 33051
#define ID_LICENSE_REVOKE_FRP 33052 #define ID_LICENSE_REVOKE_FRP 33052
#define ID_LICENSE_SET_LIMIT 33068
#define ID_Menu 33053 #define ID_Menu 33053
#define ID_33054 33054 #define ID_33054 33054
#define ID_MENU_COMPRESS 33055 #define ID_MENU_COMPRESS 33055
@@ -1013,7 +1014,7 @@
#ifdef APSTUDIO_INVOKED #ifdef APSTUDIO_INVOKED
#ifndef APSTUDIO_READONLY_SYMBOLS #ifndef APSTUDIO_READONLY_SYMBOLS
#define _APS_NEXT_RESOURCE_VALUE 391 #define _APS_NEXT_RESOURCE_VALUE 391
#define _APS_NEXT_COMMAND_VALUE 33068 #define _APS_NEXT_COMMAND_VALUE 33069
#define _APS_NEXT_CONTROL_VALUE 2542 #define _APS_NEXT_CONTROL_VALUE 2542
#define _APS_NEXT_SYMED_VALUE 105 #define _APS_NEXT_SYMED_VALUE 105
#endif #endif