From 1e4f043cb50504ed62f05b70d18bf4ff81b6ebcd Mon Sep 17 00:00:00 2001 From: SummerRay160 <141414769+SummerRay160@users.noreply.github.com> Date: Tue, 18 Aug 2026 13:57:28 +0800 Subject: [PATCH 1/2] =?UTF-8?q?feat:=20=E6=96=B0=E5=A2=9E=E5=85=A8?= =?UTF-8?q?=E5=B1=80=E5=BF=AB=E6=8D=B7=E9=94=AE=E7=9B=B4=E8=BE=BE=E9=95=BF?= =?UTF-8?q?=E6=88=AA=E5=9B=BE/=E5=BD=95=E5=B1=8F=EF=BC=8C=E7=B2=98?= =?UTF-8?q?=E8=B4=B4=E4=BC=98=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 1. 新增长截图、录屏全局快捷键支持,支持自定义快捷键和清除 2. 通用设置页添加剪贴板临时目录打开按钮 3. 优化剪贴板粘贴逻辑,支持资源管理器直接粘贴截图文件 4. 更新多语言文案,补充快捷键禁用、清除相关文本 5. 重构快捷键注册逻辑,支持多套全局热键 --- Lang/Bahasa Indonesia.id-ID.json | Bin 6054 -> 6514 bytes ...21\201\320\272\320\270\320\271.ru-RU.json" | Bin 6232 -> 6676 bytes "Lang/\347\271\201\344\275\223.zh-TW.json" | Bin 3974 -> 4276 bytes Src/Lang.cpp | 4 +- Src/Setting.cpp | 53 ++++++++++++----- Src/Util.cpp | 54 +++++++++++++++++- Src/Util.h | 4 +- Src/Win/WinCap.cpp | 25 ++++++-- Src/Win/WinCap.h | 6 +- Src/Win/WinSettingCommon.cpp | 30 ++++++++++ Src/Win/WinSettingCommon.h | 1 + Src/Win/WinSettingShortcut.cpp | 31 +++++++++- 12 files changed, 178 insertions(+), 30 deletions(-) diff --git a/Lang/Bahasa Indonesia.id-ID.json b/Lang/Bahasa Indonesia.id-ID.json index b1003c82b7c0516d2df1049da45b6bbb290e0cae..1cd4a2cf57c6dabf19fb26a45bb971a1c5fc8d99 100644 GIT binary patch delta 286 zcmZ3c|H)`WhQQ<$Rv~8>hD?Sc1|Xnr10E*t35Oop_{f~@k=$zXLLcfnM8096zK-Bb+n#pZuPD_8(L CVLihD delta 51 zcmexlv`l|PhQMYW!6+tKFNQ>h93akP$Oe-N42}#1K#@!kfAU#D<;~|rmaqT-ZmtdP diff --git "a/Lang/\320\240\321\203\321\201\321\201\320\272\320\270\320\271.ru-RU.json" "b/Lang/\320\240\321\203\321\201\321\201\320\272\320\270\320\271.ru-RU.json" index 85badbd76c1d7aeb686f06a9b20137f43841401e..87d1292c13f60e476e9b61015cb1ecc117f7a446 100644 GIT binary patch delta 306 zcmca%FvVnpfZ*hLtU}f<44Di?3`z`E3<^L}p2dL0p2Z4CDlizaIIx(q*Z{FD5PGw? z0>vDGEDNBR6^k8<(PVugY4&`E0)|wEyvYZ7gw;{al4EfKs&fG9bYk%V>a~ZO?L1kK zMRK!%U<#9@BGfoLph5)(U$AlmpqK&3Xzd(^e1<%RbhzV0Sge7TLmXp?HPQfZ*m!f+b&BFq_|Vq*d{N+-`_786PZTLzP0%}bfKnS-~E2>|OZ BGA;lB delta 30 mcmdm@*e1WhgnRQY?o*5$@u63P!Xq_GCx74)+3dlW!vp}b!V2{O diff --git a/Src/Lang.cpp b/Src/Lang.cpp index 7a8c9db4..e6086fb7 100644 --- a/Src/Lang.cpp +++ b/Src/Lang.cpp @@ -28,8 +28,8 @@ namespace { } // 内置的两种语言。第三方语言文件里缺键时也拿 en-US 这份兜底(见 Lang::get) - constexpr std::wstring_view langZhCN{ LR"""({"cap":{"mark":"图像标记","long":"截长图","video":"录屏","ocr":"文字识别","qrcode":"二维码识别","qrcodeEmpty":"未识别到二维码","qrcodeCopy":"点击确定将内容写入剪切板"},"tray":{"setting":"设置","exit":"退出"},"tool":{"rect":"矩形","ellipse":"圆形","arrow":"箭头","number":"标号","line":"线条","text":"文本","mosaic":"马赛克","eraser":"橡皮擦","undo":"撤销","redo":"重做","clipboard":"剪切板","save":"保存","close":"关闭","pin":"钉住","rectFill":"矩形填充","ellipseFill":"圆形填充","arrowFill":"箭头填充","numberFill":"标号填充","semiTransparent":"半透明","bold":"粗体","italic":"斜体"},"color":{"red":"红","yellow":"黄","green":"绿","cyan":"青","blue":"蓝","purple":"紫","pink":"粉","black":"黑","white":"白"},"video":{"outputMp4":"输出MP4格式文件","outputGif":"输出GIF格式文件","recordSystem":"录制系统声音","recordMic":"录制麦克风声音","startRecord":"开始录制","exit":"退出","stopClipboard":"停止录制,并保存到剪切板","stopFile":"停止录制,并保存到文件","stopExit":"停止录制,并退出","recordFailed":"屏幕录制失败:请在系统图形设置中为本程序选择\"节能\"后重试"},"setting":{"title":"ScreenCapture设置","common":"通用设置","shortcut":"快捷键","about":"关于","autoStart":"开机自启动:","language":"语言:","getMoreLang":"获取更多语言"},"about":{"version":"版本:","project":"项目:","author":"作者:","wechat":"微信:liulun_007","copySuccess":"复制成功","sysTip":"系统提示"},"shortcut":{"cap":"启动应用:","pressKey":"请按键..."},"long":{"start":"开始","reachedBottom":"已触底,截图停止","tooLong":"图像过长,截图停止"},"util":{"file":"文件"},"update":{"found":"发现新版本","tip":"新版本已经下载完成,点击\"确定\"立即重启,完成升级","noPermission":"程序所在目录没有写入权限,无法自动升级。点击\"确定\"打开下载页面"}})""" }; - constexpr std::wstring_view langEnUS{ LR"""({"cap":{"mark":"Annotate Image","long":"Scrolling Screenshot","video":"Record Screen","ocr":"Text Recognition","qrcode":"QR Code Recognition","qrcodeEmpty":"No QR code found","qrcodeCopy":"Click OK to copy the content to the clipboard"},"tray":{"setting":"Settings","exit":"Exit"},"tool":{"rect":"Rectangle","ellipse":"Ellipse","arrow":"Arrow","number":"Number","line":"Line","text":"Text","mosaic":"Mosaic","eraser":"Eraser","undo":"Undo","redo":"Redo","clipboard":"Clipboard","save":"Save","close":"Close","pin":"Pin","rectFill":"Filled Rectangle","ellipseFill":"Filled Ellipse","arrowFill":"Filled Arrow","numberFill":"Filled Number","semiTransparent":"Semi-transparent","bold":"Bold","italic":"Italic"},"color":{"red":"Red","yellow":"Yellow","green":"Green","cyan":"Cyan","blue":"Blue","purple":"Purple","pink":"Pink","black":"Black","white":"White"},"video":{"outputMp4":"Export as MP4 file","outputGif":"Export as GIF file","recordSystem":"Record system audio","recordMic":"Record microphone","startRecord":"Start Recording","exit":"Exit","stopClipboard":"Stop recording and copy to clipboard","stopFile":"Stop recording and save to file","stopExit":"Stop recording and exit","recordFailed":"Screen recording failed. Please set this app to \"Power saving\" in the system graphics settings and try again."},"setting":{"title":"ScreenCapture Settings","common":"General","shortcut":"Shortcuts","about":"About","autoStart":"Launch at startup:","language":"Language:","getMoreLang":"get more language"},"about":{"version":"Version:","project":"Project:","author":"Author:","wechat":"WeChat: liulun_007","copySuccess":"Copied successfully","sysTip":"Information"},"shortcut":{"cap":"Launch App:","pressKey":"Press keys..."},"long":{"start":"Start","reachedBottom":"Reached the bottom, capture stopped","tooLong":"Image too long, capture stopped"},"util":{"file":"File"},"update":{"found":"New version available","tip":"The new version has been downloaded. Click OK to restart and finish the upgrade.","noPermission":"The program folder is not writable, so it can not update itself. Click OK to open the download page."}})""" }; + constexpr std::wstring_view langZhCN{ LR"""({"cap":{"mark":"图像标记","long":"截长图","video":"录屏","ocr":"文字识别","qrcode":"二维码识别","qrcodeEmpty":"未识别到二维码","qrcodeCopy":"点击确定将内容写入剪切板"},"tray":{"setting":"设置","exit":"退出"},"tool":{"rect":"矩形","ellipse":"圆形","arrow":"箭头","number":"标号","line":"线条","text":"文本","mosaic":"马赛克","eraser":"橡皮擦","undo":"撤销","redo":"重做","clipboard":"剪切板","save":"保存","close":"关闭","pin":"钉住","rectFill":"矩形填充","ellipseFill":"圆形填充","arrowFill":"箭头填充","numberFill":"标号填充","semiTransparent":"半透明","bold":"粗体","italic":"斜体"},"color":{"red":"红","yellow":"黄","green":"绿","cyan":"青","blue":"蓝","purple":"紫","pink":"粉","black":"黑","white":"白"},"video":{"outputMp4":"输出MP4格式文件","outputGif":"输出GIF格式文件","recordSystem":"录制系统声音","recordMic":"录制麦克风声音","startRecord":"开始录制","exit":"退出","stopClipboard":"停止录制,并保存到剪切板","stopFile":"停止录制,并保存到文件","stopExit":"停止录制,并退出","recordFailed":"屏幕录制失败:请在系统图形设置中为本程序选择\"节能\"后重试"},"setting":{"title":"ScreenCapture设置","common":"通用设置","shortcut":"快捷键","about":"关于","autoStart":"开机自启动:","language":"语言:","clipboardDir":"临时截图目录:","openClipboardDir":"打开目录","getMoreLang":"获取更多语言"},"about":{"version":"版本:","project":"项目:","author":"作者:","wechat":"微信:liulun_007","copySuccess":"复制成功","sysTip":"系统提示"},"shortcut":{"cap":"截图:","long":"长截图:","video":"录屏:","disabled":"已禁用","clear":"清除","pressKey":"请按键..."},"long":{"start":"开始","reachedBottom":"已触底,截图停止","tooLong":"图像过长,截图停止"},"util":{"file":"文件"},"update":{"found":"发现新版本","tip":"新版本已经下载完成,点击\"确定\"立即重启,完成升级","noPermission":"程序所在目录没有写入权限,无法自动升级。点击\"确定\"打开下载页面"}})""" }; + constexpr std::wstring_view langEnUS{ LR"""({"cap":{"mark":"Annotate Image","long":"Scrolling Screenshot","video":"Record Screen","ocr":"Text Recognition","qrcode":"QR Code Recognition","qrcodeEmpty":"No QR code found","qrcodeCopy":"Click OK to copy the content to the clipboard"},"tray":{"setting":"Settings","exit":"Exit"},"tool":{"rect":"Rectangle","ellipse":"Ellipse","arrow":"Arrow","number":"Number","line":"Line","text":"Text","mosaic":"Mosaic","eraser":"Eraser","undo":"Undo","redo":"Redo","clipboard":"Clipboard","save":"Save","close":"Close","pin":"Pin","rectFill":"Filled Rectangle","ellipseFill":"Filled Ellipse","arrowFill":"Filled Arrow","numberFill":"Filled Number","semiTransparent":"Semi-transparent","bold":"Bold","italic":"Italic"},"color":{"red":"Red","yellow":"Yellow","green":"Green","cyan":"Cyan","blue":"Blue","purple":"Purple","pink":"Pink","black":"Black","white":"White"},"video":{"outputMp4":"Export as MP4 file","outputGif":"Export as GIF file","recordSystem":"Record system audio","recordMic":"Record microphone","startRecord":"Start Recording","exit":"Exit","stopClipboard":"Stop recording and copy to clipboard","stopFile":"Stop recording and save to file","stopExit":"Stop recording and exit","recordFailed":"Screen recording failed. Please set this app to \"Power saving\" in the system graphics settings and try again."},"setting":{"title":"ScreenCapture Settings","common":"General","shortcut":"Shortcuts","about":"About","autoStart":"Launch at startup:","language":"Language:","clipboardDir":"Temporary screenshot folder:","openClipboardDir":"Open folder","getMoreLang":"get more language"},"about":{"version":"Version:","project":"Project:","author":"Author:","wechat":"WeChat: liulun_007","copySuccess":"Copied successfully","sysTip":"Information"},"shortcut":{"cap":"Screenshot:","long":"Scrolling Screenshot:","video":"Screen Recording:","disabled":"Disabled","clear":"Clear","pressKey":"Press keys..."},"long":{"start":"Start","reachedBottom":"Reached the bottom, capture stopped","tooLong":"Image too long, capture stopped"},"util":{"file":"File"},"update":{"found":"New version available","tip":"The new version has been downloaded. Click OK to restart and finish the upgrade.","noPermission":"The program folder is not writable, so it can not update itself. Click OK to open the download page."}})""" }; } Lang::Lang() diff --git a/Src/Setting.cpp b/Src/Setting.cpp index d6e3bc35..b9bee102 100644 --- a/Src/Setting.cpp +++ b/Src/Setting.cpp @@ -8,10 +8,20 @@ namespace { std::unique_ptr setting; - constexpr int capShortcutMsgId{ 100 }; + constexpr UINT capShortcutMsgId{ 100 }; + constexpr UINT longShortcutMsgId{ 101 }; + constexpr UINT videoShortcutMsgId{ 102 }; // 配置文件的默认内容。空文件、坏 JSON、缺键都拿它兜底,所以这里列出的每一项 // 都是代码里会直接按名字取的(见 getLang / getAutoStart / initShortcutKeys) - constexpr std::wstring_view defaultConfig{ LR"""({"common":{"autoStart":false,"language":"zh-CN"},"shortcutKey":{"cap":"Ctrl+Alt+A"}})""" }; + constexpr std::wstring_view defaultConfig{ LR"""({"common":{"autoStart":false,"language":"zh-CN"},"shortcutKey":{"cap":"Ctrl+Alt+A","long":"","video":""}})""" }; + + UINT shortcutMsgId(const std::wstring& type) + { + if (type == L"cap") return capShortcutMsgId; + if (type == L"long") return longShortcutMsgId; + if (type == L"video") return videoShortcutMsgId; + return 0; + } // 自己读文件而不用 Ling::Util::readFile:那个只认 UTF-16LE,而且是按字节数 resize 的, // 后面会跟着一半长度的 \0。用户拿记事本新建的 config.json 默认是 UTF-8, @@ -92,8 +102,10 @@ bool Setting::ensureDefaults() Setting::~Setting() { - //auto lingApp = Ling::App::get(); - //lingApp->unRegHotKey(capShortcutMsgId); + auto lingApp = Ling::App::get(); + lingApp->unRegHotKey(capShortcutMsgId); + lingApp->unRegHotKey(longShortcutMsgId); + lingApp->unRegHotKey(videoShortcutMsgId); } void Setting::init() @@ -125,16 +137,18 @@ const JsonObject Setting::getConfigObj() void Setting::setShortcutKey(const std::wstring& type, const std::vector& keys) { std::wstring str; - for (size_t i = 0; i < keys.size(); i++) - { - str += L"+" + keys[i]; + for (const auto& key : keys) { + if (!str.empty()) str += L"+"; + str += key; } - str.erase(0,1); auto shortcutKey = configObj.GetNamedObject(L"shortcutKey"); shortcutKey.SetNamedValue(type, JsonValue::CreateStringValue(str)); - auto app = Ling::App::get(); - app->unRegHotKey(capShortcutMsgId); - app->regHotKey(str, capShortcutMsgId); + auto msgId = shortcutMsgId(type); + if (msgId != 0) { + auto app = Ling::App::get(); + app->unRegHotKey(msgId); + if (!str.empty()) app->regHotKey(str, msgId); + } save(); } @@ -290,15 +304,24 @@ void Setting::setUpdateCheckDay(long long day) void Setting::initShortcutKeys() { auto lingApp = Ling::App::get(); - // 取不到就用默认的那个组合:热键注册不上顶多是快捷键不好用,不该让程序起不来 - std::wstring capStr{ getShortcutKey(L"cap") }; - if (capStr.empty()) capStr = L"Ctrl+Alt+A"; - lingApp->regHotKey(capStr, capShortcutMsgId); + auto registerShortcut = [lingApp](const std::wstring& type, UINT msgId) { + auto shortcut = Setting::get()->getShortcutKey(type); + if (!shortcut.empty()) lingApp->regHotKey(shortcut, msgId); + }; + registerShortcut(L"cap", capShortcutMsgId); + registerShortcut(L"long", longShortcutMsgId); + registerShortcut(L"video", videoShortcutMsgId); lingApp->onHotKey.add([this](UINT msg) { if (msg == capShortcutMsgId) { WinCap::init(); } + else if (msg == longShortcutMsgId) { + WinCap::init(L"long"); + } + else if (msg == videoShortcutMsgId) { + WinCap::init(L"video"); + } }); lingApp->onSecondInstance.add([this]() { WinCap::init(); diff --git a/Src/Util.cpp b/Src/Util.cpp index 79377bfc..8b62e1e7 100644 --- a/Src/Util.cpp +++ b/Src/Util.cpp @@ -79,6 +79,11 @@ void Util::saveToClipboard(const int w, const int h, BYTE* data) DWORD rowBytes = (DWORD)w * 4; DWORD imgBytes = rowBytes * (DWORD)h; + // 资源管理器只能从 CF_HDROP 粘贴文件,不能把剪切板里的 DIB/PNG 自动落盘。 + // 文件必须一直留着,直到用户真正粘贴,所以不要放在临时目录里立即删除。 + auto filePath = Setting::get()->getDataPath().append(L"clipboard_" + createFileName(L"png")); + const bool fileReady = saveToFile(filePath.wstring(), w, h, data); + // ---------- 1) PNG 编码到内存流 ---------- ComPtr pngStream; if (FAILED(CreateStreamOnHGlobal(nullptr, TRUE, pngStream.GetAddressOf()))) return; @@ -155,8 +160,34 @@ void Util::saveToClipboard(const int w, const int h, BYTE* data) } GlobalUnlock(hDib); - // ---------- 4) 写入剪切板 ---------- + // ---------- 4) 构造 CF_HDROP(资源管理器粘贴文件) ---------- + HGLOBAL hDrop{ nullptr }; + if (fileReady) { + auto filePathStr = filePath.wstring(); + auto dropSize = sizeof(DROPFILES) + (filePathStr.length() + 3) * sizeof(wchar_t); + + hDrop = GlobalAlloc(GMEM_MOVEABLE, dropSize); + if (hDrop) { + auto drop = static_cast(GlobalLock(hDrop)); + if (drop) { + drop->pFiles = sizeof(DROPFILES); + drop->fWide = TRUE; + auto dropPath = reinterpret_cast(drop + 1); + wcscpy_s(dropPath, filePathStr.length() + 1, filePathStr.c_str()); + dropPath[filePathStr.length() + 1] = L'\0'; + GlobalUnlock(hDrop); + } + else { + GlobalFree(hDrop); + hDrop = nullptr; + } + } + } + + // ---------- 5) 写入剪切板 ---------- + if (!OpenClipboard(nullptr)) { + GlobalFree(hDrop); GlobalFree(hDib); GlobalFree(hDibV5); GlobalFree(hPng); @@ -170,6 +201,25 @@ void Util::saveToClipboard(const int w, const int h, BYTE* data) if (!SetClipboardData(CF_DIB, hDib)) { GlobalFree(hDib); } + if (hDrop && !SetClipboardData(CF_HDROP, hDrop)) { + GlobalFree(hDrop); + } + // 告诉资源管理器这是复制而不是移动,Ctrl+V 时按普通文件复制处理。 + UINT cfPreferredDropEffect = RegisterClipboardFormatW(CFSTR_PREFERREDDROPEFFECT); + if (cfPreferredDropEffect != 0) { + HGLOBAL hDropEffect = GlobalAlloc(GMEM_MOVEABLE, sizeof(DWORD)); + if (hDropEffect) { + auto effect = static_cast(GlobalLock(hDropEffect)); + if (effect) { + *effect = DROPEFFECT_COPY; + GlobalUnlock(hDropEffect); + if (!SetClipboardData(cfPreferredDropEffect, hDropEffect)) GlobalFree(hDropEffect); + } + else { + GlobalFree(hDropEffect); + } + } + } UINT cfPng = RegisterClipboardFormatW(L"PNG"); if (cfPng == 0 || !SetClipboardData(cfPng, hPng)) { GlobalFree(hPng); @@ -261,7 +311,7 @@ void Util::addFileToClipboard(const std::wstring& filePath) if (!OpenClipboard(nullptr)) return; EmptyClipboard(); // DROPFILES 之后紧跟双 \0 结尾的路径列表,这里只放一条 - auto totalSize = sizeof(DROPFILES) + (filePath.length() + 2) * sizeof(wchar_t); + auto totalSize = sizeof(DROPFILES) + (filePath.length() + 3) * sizeof(wchar_t); auto hGlobal = GlobalAlloc(GMEM_MOVEABLE, totalSize); if (!hGlobal) { CloseClipboard(); diff --git a/Src/Util.h b/Src/Util.h index 6728769c..4b74bf3d 100644 --- a/Src/Util.h +++ b/Src/Util.h @@ -7,8 +7,8 @@ class Util { public: - // 同时写入 CF_DIBV5(Office / 微信 / WPS 这类原生程序认)和 "PNG" 注册格式 - //(浏览器 / Electron 程序认),两份都带 alpha + // 同时写入图像格式和 CF_HDROP:图像程序按 PNG/DIB 粘贴,资源管理器按 PNG 文件粘贴。 + // CF_HDROP 对应的缓存文件会保留,直到用户清理数据目录。 static void saveToClipboard(const int w, const int h, BYTE* data); static bool saveToFile(const std::wstring& path, const int w, const int h, BYTE* data); // 弹系统另存为对话框,返回空串表示用户取消 diff --git a/Src/Win/WinCap.cpp b/Src/Win/WinCap.cpp index 8012710f..15cea868 100644 --- a/Src/Win/WinCap.cpp +++ b/Src/Win/WinCap.cpp @@ -53,11 +53,12 @@ WinCap::~WinCap() { } -void WinCap::init() +void WinCap::init(const std::wstring& enter) { // 双击托盘图标会连着来两下,已经开着就不再建第二个 if (winCap) return; auto ptr = new WinCap(); + ptr->pendingEnter = enter; winCap.reset(ptr); ptr->cutMask = std::make_unique(ptr); ptr->createNativeWindow(WS_EX_TOOLWINDOW, WS_POPUP);//WS_EX_TOPMOST @@ -290,9 +291,13 @@ void WinCap::onKey(UINT key) Ling::Util::setTextToClipboard(std::format(L"{},{}", pos.x, pos.y)); close(); } - // 长图与录屏阶段:Ctrl+S 存文件,Ctrl+C 存剪切板,等价于各自工具条上的那两个按钮。 - // 这两个阶段里键盘消息进的往往是工具条,ToolLong / ToolVideo 会把 onKeyDown 转回这里 + // 各阶段的 Ctrl+C / Ctrl+S 等价于工具条上的复制和保存按钮。 + // 工具条获得焦点时也会把键盘消息转回这里。 else if ((key == 'S' || key == 'C') && (GetKeyState(VK_CONTROL) & 0x8000)) { + if (stage == CapStage::Adjust && key == 'C') { + copyToClipboard(); + return; + } const bool toClipboard{ key == 'C' }; if (stage == CapStage::Long && capLong && capLong->hasImage()) { // 与 ToolLong::onClick 同一套规则:存盘被取消了就留在原地,图还没丢 @@ -430,7 +435,7 @@ void WinCap::onUp(POINT pos, bool isRight) if (!cutMask->hasRect()) return; // 命令行指定了直奔某个阶段:它比下面 Ctrl 那条钉图的快捷路径更优先 —— // 参数是用户明确要求的,Ctrl 只是顺手按上的 - if (enterByArg()) return; + if (enterByShortcut() || enterByArg()) return; // 按住 Ctrl 框选:跳过调整和工具条,直接钉到桌面上 if (GetKeyState(VK_CONTROL) & 0x8000) { startPin(); @@ -514,6 +519,18 @@ bool WinCap::enterByArg() return true; } +bool WinCap::enterByShortcut() +{ + if (pendingEnter.empty()) return false; + auto enter = std::exchange(pendingEnter, L""); + stage = CapStage::Adjust; + refresh(); + if (enter == L"long") startLong(); + else if (enter == L"video") startVideo(); + else return false; + return true; +} + void WinCap::relayoutTool() { if (capLong) capLong->layoutTool(); diff --git a/Src/Win/WinCap.h b/Src/Win/WinCap.h index 48d9fec5..36b34bef 100644 --- a/Src/Win/WinCap.h +++ b/Src/Win/WinCap.h @@ -11,7 +11,7 @@ class WinCap:public Ling::WinBase { public: ~WinCap(); - static void init(); + static void init(const std::wstring& enter = L""); static WinCap* get(); // 退出流程里调:窗口对象是文件级静态变量,交给静态析构就在 CoUninitialize 之后了 static void dispose(); @@ -68,10 +68,11 @@ class WinCap:public Ling::WinBase void onUp(POINT pos, bool isRight); void onClosed(); void makeToolCap(); - // 命令行给了 --enter=xxx(long / video / ocr / qr / pin)时,框完选区不出 ToolCap, + // 命令行或全局快捷键指定了直奔某阶段时,框完选区不出 ToolCap, // 直接走对应的那条路 —— 等于替用户点了工具条上的那个按钮。 // 返回是否已经接手;值不认识(拼错了)就返回 false,照常出工具条 bool enterByArg(); + bool enterByShortcut(); // DPI 变了之后重走一遍工具条的摆放规则(哪个阶段就重排哪个工具条) void relayoutTool(); // 进长图 / 录屏阶段的公共动作:收掉底图与工具条,并提到最上层 @@ -93,6 +94,7 @@ class WinCap:public Ling::WinBase Ling::Canvas* canvas{ nullptr }; POINT pixPos; bool isPress{ false }, isClosed{ false }, isMouseTransparent{ false }; + std::wstring pendingEnter; // onDpiChanged 与 onSizeChanged 之间的接力标记,见构造函数里的注释 bool dpiChanged{ false }; // 自己认双击用的上一次按下时间与位置。Ling 的窗口类没带 CS_DBLCLKS, diff --git a/Src/Win/WinSettingCommon.cpp b/Src/Win/WinSettingCommon.cpp index 248e85d4..d97ce516 100644 --- a/Src/Win/WinSettingCommon.cpp +++ b/Src/Win/WinSettingCommon.cpp @@ -8,6 +8,7 @@ WinSettingCommon::WinSettingCommon(Ling::WinBase* parent):Ling::Node(parent) { initAutoStartCtrls(); initLangCtrls(); + initClipboardDirCtrls(); auto weakThis = getWeakThis(); // 这个回调一直挂在窗口上,而本节点可能在窗口关闭之前就被菜单切换换掉了, // 所以先确认自己还活着再去碰成员 @@ -55,6 +56,35 @@ void WinSettingCommon::initAutoStartCtrls() border->setBg(0xE0E0E0FF); } +void WinSettingCommon::initClipboardDirCtrls() +{ + auto box = makeChild(); + box->setHeight(39.f); + box->setFlexDirection(Ling::FlexDirection::Row); + box->setAlignItems(Ling::Align::Center); + + auto label = box->makeChild(); + label->setText(Lang::get(L"setting.clipboardDir")); + label->setHeightPercent(100.f); + label->setJustifyContent(Ling::Justify::Center); + label->setFlexGrow(1.f); + + auto btn = box->makeChild(); + btn->setText(Lang::get(L"setting.openClipboardDir")); + btn->setHeight(28.f); + btn->setWidth(120.f); + btn->setBorder(1.f, 0xE0E0E0FF); + btn->setHoverBg(0xFFFFFFFF); + btn->onClick.add([](Ling::Button* btn) { + auto path = Setting::get()->getDataPath().wstring(); + ShellExecute(nullptr, L"open", path.c_str(), nullptr, nullptr, SW_SHOWNORMAL); + }); + + auto border = makeChild(); + border->setHeight(1.f); + border->setBg(0xE0E0E0FF); +} + void WinSettingCommon::initLangCtrls() { auto box = makeChild(); diff --git a/Src/Win/WinSettingCommon.h b/Src/Win/WinSettingCommon.h index 729bb669..25860316 100644 --- a/Src/Win/WinSettingCommon.h +++ b/Src/Win/WinSettingCommon.h @@ -11,6 +11,7 @@ class WinSettingCommon:public Ling::Node private: void initAutoStartCtrls(); void initLangCtrls(); + void initClipboardDirCtrls(); void setAutoStartBtn(Ling::Button* btn); void showSelectBox(Ling::Button* btn); private: diff --git a/Src/Win/WinSettingShortcut.cpp b/Src/Win/WinSettingShortcut.cpp index 6bc8be5f..ae5114a3 100644 --- a/Src/Win/WinSettingShortcut.cpp +++ b/Src/Win/WinSettingShortcut.cpp @@ -97,7 +97,7 @@ namespace { WinSettingShortcut::WinSettingShortcut(Ling::WinBase* parent):Ling::Node(parent) { - std::vector keys = { L"cap" }; + std::vector keys = { L"cap", L"long", L"video" }; for (auto& key:keys) { auto box = makeChild(); @@ -113,7 +113,8 @@ WinSettingShortcut::WinSettingShortcut(Ling::WinBase* parent):Ling::Node(parent) auto btn = box->makeChild(); btn->setId(key); - btn->setText(Setting::get()->getShortcutKey(key)); + auto shortcut = Setting::get()->getShortcutKey(key); + btn->setText(shortcut.empty() ? Lang::get(L"shortcut.disabled") : shortcut); btn->setHeight(28.f); btn->setWidth(120.f); btn->setBg(0xFFFFFFFF); @@ -122,6 +123,17 @@ WinSettingShortcut::WinSettingShortcut(Ling::WinBase* parent):Ling::Node(parent) btn->onClick.add([this](Ling::Button* btn) {this->onBtnClick(btn);}); btns.push_back(btn); + auto clearBtn = box->makeChild(); + clearBtn->setId(L"clear:" + key); + clearBtn->setText(Lang::get(L"shortcut.clear")); + clearBtn->setHeight(28.f); + clearBtn->setWidth(72.f); + clearBtn->setBg(0xFFFFFFFF); + clearBtn->setHoverBg(0xFFF0F0FF); + clearBtn->setBorder(1.f, 0xE0E0E0FF); + clearBtn->onClick.add([this](Ling::Button* btn) {this->onBtnClick(btn);}); + btns.push_back(clearBtn); + auto border = makeChild(); border->setHeight(1.f); border->setBg(0xE0E0E0FF); @@ -158,6 +170,18 @@ WinSettingShortcut::~WinSettingShortcut() void WinSettingShortcut::onBtnClick(Ling::Button* btn) { + if (btn->id.starts_with(L"clear:")) { + if (!curKey.empty()) endCapture(); + auto key = btn->id.substr(6); + Setting::get()->setShortcutKey(key, {}); + for (auto shortcutBtn : btns) { + if (shortcutBtn->id == key) { + shortcutBtn->setText(Lang::get(L"shortcut.disabled")); + break; + } + } + return; + } // 再次点击当前正在捕获的按钮 → 取消 if (curKey == btn->id) { endCapture(); @@ -183,7 +207,8 @@ void WinSettingShortcut::endCapture() for (auto& btn : btns) { if (btn->id == curKey) { - btn->setText(Setting::get()->getShortcutKey(curKey)); + auto shortcut = Setting::get()->getShortcutKey(curKey); + btn->setText(shortcut.empty() ? Lang::get(L"shortcut.disabled") : shortcut); break; } } From ff9232586e17ca02bb210de2d13e3d71648eef81 Mon Sep 17 00:00:00 2001 From: SummerRay160 <141414769+SummerRay160@users.noreply.github.com> Date: Tue, 18 Aug 2026 14:34:51 +0800 Subject: [PATCH 2/2] =?UTF-8?q?refactor(shortcut,clipboard):=20=E4=BC=98?= =?UTF-8?q?=E5=8C=96=E5=BF=AB=E6=8D=B7=E9=94=AE=E7=AE=A1=E7=90=86=E4=B8=8E?= =?UTF-8?q?=E5=89=AA=E8=B4=B4=E6=9D=BF=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 1. 拆分快捷键按钮容器为allBtns和shortcutBtns,区分所有按钮和快捷键配置按钮 2. 简化命令行参数识别逻辑,合并重复的参数判断分支 3. 重构剪贴板文件处理逻辑,提取通用的DROPFILES生成函数,修复文件清理时机问题 --- Src/Util.cpp | 108 ++++++++++++++++++--------------- Src/Win/WinCap.cpp | 16 ++--- Src/Win/WinSettingShortcut.cpp | 11 ++-- Src/Win/WinSettingShortcut.h | 3 +- 4 files changed, 76 insertions(+), 62 deletions(-) diff --git a/Src/Util.cpp b/Src/Util.cpp index 8b62e1e7..6eeb46cb 100644 --- a/Src/Util.cpp +++ b/Src/Util.cpp @@ -40,6 +40,28 @@ namespace { return SUCCEEDED(encoder->Commit()); } + HGLOBAL makeFileDropHandle(const std::wstring& filePath) + { + if (filePath.empty()) return nullptr; + const size_t maxChars = (SIZE_MAX - sizeof(DROPFILES)) / sizeof(wchar_t); + if (filePath.length() > maxChars - 3) return nullptr; + const size_t charCount = filePath.length() + 3; + auto hDrop = GlobalAlloc(GMEM_MOVEABLE, sizeof(DROPFILES) + charCount * sizeof(wchar_t)); + if (!hDrop) return nullptr; + auto drop = static_cast(GlobalLock(hDrop)); + if (!drop) { + GlobalFree(hDrop); + return nullptr; + } + drop->pFiles = sizeof(DROPFILES); + drop->fWide = TRUE; + auto dropPath = reinterpret_cast(drop + 1); + wcscpy_s(dropPath, charCount, filePath.c_str()); + dropPath[filePath.length() + 1] = L'\0'; + GlobalUnlock(hDrop); + return hDrop; + } + // quirc 交出来的是裸字节流:BYTE 类型的二维码现实中基本都是 UTF-8(微信、支付宝 // 生成的都是),Kanji 类型按 ISO 18004 规定是 Shift-JIS。所以先按 UTF-8 严格解, // 解不通再退回对应的本地代码页,避免把中文变成一堆问号 @@ -79,11 +101,6 @@ void Util::saveToClipboard(const int w, const int h, BYTE* data) DWORD rowBytes = (DWORD)w * 4; DWORD imgBytes = rowBytes * (DWORD)h; - // 资源管理器只能从 CF_HDROP 粘贴文件,不能把剪切板里的 DIB/PNG 自动落盘。 - // 文件必须一直留着,直到用户真正粘贴,所以不要放在临时目录里立即删除。 - auto filePath = Setting::get()->getDataPath().append(L"clipboard_" + createFileName(L"png")); - const bool fileReady = saveToFile(filePath.wstring(), w, h, data); - // ---------- 1) PNG 编码到内存流 ---------- ComPtr pngStream; if (FAILED(CreateStreamOnHGlobal(nullptr, TRUE, pngStream.GetAddressOf()))) return; @@ -161,39 +178,42 @@ void Util::saveToClipboard(const int w, const int h, BYTE* data) GlobalUnlock(hDib); // ---------- 4) 构造 CF_HDROP(资源管理器粘贴文件) ---------- - HGLOBAL hDrop{ nullptr }; - if (fileReady) { - auto filePathStr = filePath.wstring(); - auto dropSize = sizeof(DROPFILES) + (filePathStr.length() + 3) * sizeof(wchar_t); - - hDrop = GlobalAlloc(GMEM_MOVEABLE, dropSize); - if (hDrop) { - auto drop = static_cast(GlobalLock(hDrop)); - if (drop) { - drop->pFiles = sizeof(DROPFILES); - drop->fWide = TRUE; - auto dropPath = reinterpret_cast(drop + 1); - wcscpy_s(dropPath, filePathStr.length() + 1, filePathStr.c_str()); - dropPath[filePathStr.length() + 1] = L'\0'; - GlobalUnlock(hDrop); - } - else { - GlobalFree(hDrop); - hDrop = nullptr; - } - } + // 文件必须留到用户真正粘贴,只有内存格式均已准备好后才落盘,避免前面的失败路径留下垃圾文件。 + auto filePath = Setting::get()->getDataPath().append(L"clipboard_" + createFileName(L"png")); + const bool fileReady = saveToFile(filePath.wstring(), w, h, data); + if (!fileReady) { + std::error_code ec; + std::filesystem::remove(filePath, ec); + } + HGLOBAL hDrop = fileReady ? makeFileDropHandle(filePath.wstring()) : nullptr; + if (fileReady && !hDrop) { + std::error_code ec; + std::filesystem::remove(filePath, ec); } // ---------- 5) 写入剪切板 ---------- - + auto discardFile = [&]() { + if (!fileReady) return; + std::error_code ec; + std::filesystem::remove(filePath, ec); + }; if (!OpenClipboard(nullptr)) { GlobalFree(hDrop); GlobalFree(hDib); GlobalFree(hDibV5); GlobalFree(hPng); + discardFile(); + return; + } + if (!EmptyClipboard()) { + CloseClipboard(); + GlobalFree(hDrop); + GlobalFree(hDib); + GlobalFree(hDibV5); + GlobalFree(hPng); + discardFile(); return; } - EmptyClipboard(); // SetClipboardData 成功后 HGLOBAL 归剪切板所有,不能再 GlobalFree;失败了才要自己释放 if (!SetClipboardData(CF_DIBV5, hDibV5)) { GlobalFree(hDibV5); @@ -201,11 +221,13 @@ void Util::saveToClipboard(const int w, const int h, BYTE* data) if (!SetClipboardData(CF_DIB, hDib)) { GlobalFree(hDib); } - if (hDrop && !SetClipboardData(CF_HDROP, hDrop)) { + const bool dropPublished = hDrop && SetClipboardData(CF_HDROP, hDrop); + if (hDrop && !dropPublished) { GlobalFree(hDrop); + discardFile(); } // 告诉资源管理器这是复制而不是移动,Ctrl+V 时按普通文件复制处理。 - UINT cfPreferredDropEffect = RegisterClipboardFormatW(CFSTR_PREFERREDDROPEFFECT); + UINT cfPreferredDropEffect = dropPublished ? RegisterClipboardFormatW(CFSTR_PREFERREDDROPEFFECT) : 0; if (cfPreferredDropEffect != 0) { HGLOBAL hDropEffect = GlobalAlloc(GMEM_MOVEABLE, sizeof(DWORD)); if (hDropEffect) { @@ -308,31 +330,19 @@ std::vector Util::captureScreen(const int x, const int y, const int w, con void Util::addFileToClipboard(const std::wstring& filePath) { - if (!OpenClipboard(nullptr)) return; - EmptyClipboard(); - // DROPFILES 之后紧跟双 \0 结尾的路径列表,这里只放一条 - auto totalSize = sizeof(DROPFILES) + (filePath.length() + 3) * sizeof(wchar_t); - auto hGlobal = GlobalAlloc(GMEM_MOVEABLE, totalSize); - if (!hGlobal) { - CloseClipboard(); + auto hDrop = makeFileDropHandle(filePath); + if (!hDrop) return; + if (!OpenClipboard(nullptr)) { + GlobalFree(hDrop); return; } - auto pDropFiles = static_cast(GlobalLock(hGlobal)); - if (!pDropFiles) { - GlobalFree(hGlobal); + if (!EmptyClipboard()) { CloseClipboard(); + GlobalFree(hDrop); return; } - pDropFiles->pFiles = sizeof(DROPFILES); - pDropFiles->fWide = TRUE; - auto dest = reinterpret_cast(pDropFiles + 1); - wcscpy_s(dest, filePath.length() + 1, filePath.c_str()); - dest[filePath.length() + 1] = L'\0'; - GlobalUnlock(hGlobal); // 成功后 HGLOBAL 归剪切板所有,只在失败时自己释放 - if (!SetClipboardData(CF_HDROP, hGlobal)) { - GlobalFree(hGlobal); - } + if (!SetClipboardData(CF_HDROP, hDrop)) GlobalFree(hDrop); CloseClipboard(); } diff --git a/Src/Win/WinCap.cpp b/Src/Win/WinCap.cpp index 15cea868..2eb3384d 100644 --- a/Src/Win/WinCap.cpp +++ b/Src/Win/WinCap.cpp @@ -505,6 +505,9 @@ bool WinCap::enterByArg() auto it = args.find(L"--enter"); if (it == args.end()) return false; auto& val = it->second; + const bool recognized = val == L"long" || val == L"video" || val == L"ocr" + || val == L"qr" || val == L"qrcode" || val == L"pin"; + if (!recognized) return false; // 下面这几条路本来都是从 ToolCap 的按钮进的,start* 会检查选区是不是已经定下来了 stage = CapStage::Adjust; refresh(); //收掉放大镜:这几条路都是马上要换阶段或者弹窗,屏幕上不能留着它 @@ -512,22 +515,21 @@ bool WinCap::enterByArg() else if (val == L"video") startVideo(); else if (val == L"ocr") startOcr(); else if (val == L"qr" || val == L"qrcode") startQrcode(); - else if (val == L"pin") startPin(); //等于替用户按住了 Ctrl 框选 - // 值不认识(用户拼错了):当没给这个参数,照常出工具条。上面那两句白做了, - // 但调用方接着也是这两句,重复一遍没有副作用 - else return false; + else startPin(); //等于替用户按住了 Ctrl 框选 return true; } bool WinCap::enterByShortcut() { - if (pendingEnter.empty()) return false; + if (pendingEnter != L"long" && pendingEnter != L"video") { + pendingEnter.clear(); + return false; + } auto enter = std::exchange(pendingEnter, L""); stage = CapStage::Adjust; refresh(); if (enter == L"long") startLong(); - else if (enter == L"video") startVideo(); - else return false; + else startVideo(); return true; } diff --git a/Src/Win/WinSettingShortcut.cpp b/Src/Win/WinSettingShortcut.cpp index ae5114a3..8b0837f0 100644 --- a/Src/Win/WinSettingShortcut.cpp +++ b/Src/Win/WinSettingShortcut.cpp @@ -121,7 +121,8 @@ WinSettingShortcut::WinSettingShortcut(Ling::WinBase* parent):Ling::Node(parent) btn->setHoverBg(0xFFFFFFFF); btn->setBorder(1.f, 0xE0E0E0FF); btn->onClick.add([this](Ling::Button* btn) {this->onBtnClick(btn);}); - btns.push_back(btn); + shortcutBtns.push_back(btn); + allBtns.push_back(btn); auto clearBtn = box->makeChild(); clearBtn->setId(L"clear:" + key); @@ -132,7 +133,7 @@ WinSettingShortcut::WinSettingShortcut(Ling::WinBase* parent):Ling::Node(parent) clearBtn->setHoverBg(0xFFF0F0FF); clearBtn->setBorder(1.f, 0xE0E0E0FF); clearBtn->onClick.add([this](Ling::Button* btn) {this->onBtnClick(btn);}); - btns.push_back(clearBtn); + allBtns.push_back(clearBtn); auto border = makeChild(); border->setHeight(1.f); @@ -153,7 +154,7 @@ WinSettingShortcut::WinSettingShortcut(Ling::WinBase* parent):Ling::Node(parent) onMouseDownToken = win->onMouseDown.add([this,weakThis](POINT pos, bool isRight) { if (!weakThis.lock()) return; if (this->curKey.empty()) return; - for (auto btn: this->btns) + for (auto btn: this->allBtns) { if(btn->isPosIn(pos)) return; } @@ -174,7 +175,7 @@ void WinSettingShortcut::onBtnClick(Ling::Button* btn) if (!curKey.empty()) endCapture(); auto key = btn->id.substr(6); Setting::get()->setShortcutKey(key, {}); - for (auto shortcutBtn : btns) { + for (auto shortcutBtn : shortcutBtns) { if (shortcutBtn->id == key) { shortcutBtn->setText(Lang::get(L"shortcut.disabled")); break; @@ -204,7 +205,7 @@ void WinSettingShortcut::beginCapture(Ling::Button* btn) void WinSettingShortcut::endCapture() { if (curKey.empty()) return; - for (auto& btn : btns) + for (auto& btn : shortcutBtns) { if (btn->id == curKey) { auto shortcut = Setting::get()->getShortcutKey(curKey); diff --git a/Src/Win/WinSettingShortcut.h b/Src/Win/WinSettingShortcut.h index cf9b0e34..48fb9f66 100644 --- a/Src/Win/WinSettingShortcut.h +++ b/Src/Win/WinSettingShortcut.h @@ -13,7 +13,8 @@ class WinSettingShortcut :public Ling::Node void onKeyUp(UINT key); std::wstring keyToStr(UINT vkCode); private: - std::vector btns; + std::vector allBtns; + std::vector shortcutBtns; std::wstring curKey; winrt::event_token onMouseDownToken, onKeyDownToken, onKeyUpToken; std::vector tempKeys;