diff --git a/README.md b/README.md
index 953ed82..a04a171 100644
--- a/README.md
+++ b/README.md
@@ -11,6 +11,10 @@ macOS 和 Windows 原生 API 的 Node.js 封装,使用 Swift + Win32 API + Nod
5. **键盘模拟** - 模拟键盘按键和快捷键(支持修饰键)
6. **粘贴模拟** - 模拟 Cmd+V (macOS) / Ctrl+V (Windows)
7. **区域截图** - 选区截图并自动保存到剪贴板(Windows)
+8. **获取选中内容** - 获取当前选中的文本、文件或图像(支持 Cursor/VS Code 等编辑器)
+9. **鼠标监控** - 实时监听鼠标移动、点击事件
+10. **鼠标模拟** - 模拟鼠标移动、点击操作
+11. **取色器** - 全屏取色工具
## 🔧 系统要求
@@ -106,6 +110,25 @@ ScreenCapture.start((result) => {
}
});
// 操作:拖拽选择区域后释放鼠标,或按 ESC 取消
+
+// 8. 获取选中内容(支持文本、文件、图像)
+const { getSelectedContent } = require('ztools-native-api');
+
+// 在任意应用中选中内容后调用
+const contents = getSelectedContent();
+contents.forEach(item => {
+ switch (item.type) {
+ case 'text':
+ console.log('选中的文本:', item.data);
+ break;
+ case 'file':
+ console.log('选中的文件:', item.data);
+ break;
+ case 'image':
+ console.log('选中的图像 (base64):', item.data.substring(0, 50) + '...');
+ break;
+ }
+});
```
### 跨平台兼容示例
@@ -290,6 +313,66 @@ ScreenCapture.start((result) => {
});
```
+---
+
+### `getSelectedContent()`
+
+#### `getSelectedContent()`
+获取当前选中的内容(支持文本、文件、图像)
+- **返回值**: `Array<{type: string, data: any}>` - 选中内容数组
+ - `type`: 'text' | 'file' | 'image'
+ - `data`: 根据类型不同:
+ - text: 字符串
+ - file: 文件路径字符串数组
+ - image: base64 编码的 PNG 图像(带 format 和 encoding 字段)
+- **平台**: ✅ Windows 和 macOS
+
+**功能说明**:
+- **Windows**: 优先使用 UI Automation API,回退到剪贴板方法
+ - 适用于标准 Windows 控件和 Electron/Chromium 应用(Cursor、VS Code 等)
+- **macOS**: 使用模拟复制方法(Cmd+C)
+- 自动暂停内部的 clipboardMonitor,防止误触发监听自身发起的事件
+- 操作后会恢复原剪贴板内容
+
+**示例**:
+```javascript
+const { getSelectedContent } = require('ztools-native-api');
+
+// 在用户选中内容后调用
+const contents = getSelectedContent();
+
+contents.forEach((item, index) => {
+ console.log(`[${index + 1}] 类型: ${item.type}`);
+
+ switch (item.type) {
+ case 'text':
+ console.log('文本内容:', item.data);
+ console.log('文本长度:', item.data.length);
+ break;
+
+ case 'file':
+ console.log('文件列表:');
+ item.data.forEach((path, i) => {
+ console.log(` ${i + 1}. ${path}`);
+ });
+ break;
+
+ case 'image':
+ console.log('图像格式:', item.format); // 'png'
+ console.log('编码方式:', item.encoding); // 'base64'
+ console.log('数据长度:', item.data.length);
+ // 可以直接用于
+ break;
+ }
+});
+```
+
+**支持的应用**:
+- ✅ Windows: 记事本、Word、Excel、Edge、Chrome、Firefox、VS Code、Notepad++、**Cursor**、**任何 Electron 应用**
+- ✅ macOS: 所有支持标准复制快捷键(Cmd+C)的应用
+- ✅ 支持文件资源管理器/Finder 中选中的文件
+- ✅ 支持图像编辑器中选中的图像区域
+
## 🧪 测试
@@ -299,6 +382,7 @@ npm test
# 或运行特定测试
node test/test-keyboard.js # 完整键盘测试
node test/test-keyboard-simple.js # 简单键盘测试
+node test/test-selected-content.js # 获取选中内容测试
```
## ⚠️ 平台差异
@@ -310,6 +394,7 @@ node test/test-keyboard-simple.js # 简单键盘测试
| **剪贴板监控** | 轮询 `changeCount` | 消息循环 + `WM_CLIPBOARDUPDATE` |
| **键盘模拟** | ✅ 需要辅助功能权限 | ✅ 无需特殊权限 |
| **区域截图** | ❌ 暂不支持 | ✅ 支持(分层窗口 + GDI) |
+| **获取选中内容** | ✅ 支持(模拟复制) | ✅ 支持(UI Automation + 剪贴板回退) |
| **权限要求** | 辅助功能权限(键盘模拟) | 无特殊要求 |
## 📝 注意事项
diff --git a/index.js b/index.js
index 4d37108..8d0d6b9 100644
--- a/index.js
+++ b/index.js
@@ -46,6 +46,26 @@ class ClipboardMonitor {
this._callback = null;
}
+ /**
+ * 暂停剪贴板监控(不触发回调,但保持监控线程运行)
+ */
+ pause() {
+ if (!this._isMonitoring) {
+ return;
+ }
+ addon.pauseMonitor();
+ }
+
+ /**
+ * 恢复剪贴板监控
+ */
+ resume() {
+ if (!this._isMonitoring) {
+ return;
+ }
+ addon.resumeMonitor();
+ }
+
/**
* 是否正在监控
*/
@@ -504,6 +524,42 @@ class MuiResolver {
}
}
+/**
+ * 获取当前选中的内容(支持文本、文件、图像)
+ *
+ * 实现方式:
+ * - Windows: 优先使用 UI Automation API,回退到剪贴板方法(适用于 Cursor/VS Code 等编辑器)
+ * - macOS: 使用模拟复制方法(Cmd+C)
+ *
+ * 在模拟复制时会自动暂停内部的 clipboardMonitor,防止误触发监听自身发起的事件
+ *
+ * @returns {Array<{type: string, data: any}>} 选中内容数组
+ * - type: 'text' | 'file' | 'image'
+ * - data: 根据类型不同:
+ * - text: 字符串
+ * - file: 文件路径字符串数组
+ * - image: base64 编码的 PNG 图像(带 format 和 encoding 字段)
+ *
+ * @example
+ * const contents = getSelectedContent();
+ * contents.forEach(item => {
+ * switch (item.type) {
+ * case 'text':
+ * console.log('Selected text:', item.data);
+ * break;
+ * case 'file':
+ * console.log('Selected files:', item.data);
+ * break;
+ * case 'image':
+ * console.log('Selected image (base64):', item.data.substring(0, 50) + '...');
+ * break;
+ * }
+ * });
+ */
+function getSelectedContent() {
+ return addon.getSelectedContent();
+}
+
// 导出所有类
module.exports = {
ClipboardMonitor,
@@ -514,7 +570,8 @@ module.exports = {
ColorPicker,
IconExtractor,
UwpManager,
- MuiResolver
+ MuiResolver,
+ getSelectedContent
};
// 为了向后兼容,默认导出 ClipboardMonitor
diff --git a/src/binding_mac.cpp b/src/binding_mac.cpp
index 105ab76..88a6c3d 100644
--- a/src/binding_mac.cpp
+++ b/src/binding_mac.cpp
@@ -3,6 +3,7 @@
#include
#include
#include
+#include // For usleep
// Swift 动态库函数类型定义
typedef void (*ClipboardCallback)(); // 无参数回调
@@ -58,6 +59,7 @@ static napi_threadsafe_function colorPickerTsfn = nullptr;
static StartColorPickerFunc startColorPickerFunc = nullptr;
static StopColorPickerFunc stopColorPickerFunc = nullptr;
static FetchFileIconFunc fetchFileIconFunc = nullptr;
+static bool g_isPaused = false; // 剪贴板监控暂停状态
// 在主线程调用 JS 回调
void CallJs(napi_env env, napi_value js_callback, void *context, void *data) {
@@ -71,7 +73,7 @@ void CallJs(napi_env env, napi_value js_callback, void *context, void *data) {
// Swift 回调 -> 推送到线程安全队列
void OnClipboardChanged() {
- if (tsfn != nullptr) {
+ if (tsfn != nullptr && !g_isPaused) {
// 不需要传递数据
napi_call_threadsafe_function(tsfn, nullptr, napi_tsfn_nonblocking);
}
@@ -359,9 +361,256 @@ Napi::Value StopMonitor(const Napi::CallbackInfo &info) {
tsfn = nullptr;
}
+ g_isPaused = false; // 重置暂停状态
+
+ return env.Undefined();
+}
+
+// 暂停剪贴板监控
+Napi::Value PauseMonitor(const Napi::CallbackInfo &info) {
+ Napi::Env env = info.Env();
+ g_isPaused = true;
+ return env.Undefined();
+}
+
+// 恢复剪贴板监控
+Napi::Value ResumeMonitor(const Napi::CallbackInfo &info) {
+ Napi::Env env = info.Env();
+ g_isPaused = false;
return env.Undefined();
}
+// ==================== 获取选中内容(Mac 实现)====================
+
+// 注意:以下函数使用 popen 调用外部命令(pbpaste, osascript)来操作剪贴板
+//
+// 性能考虑:
+// - 这些是低频操作(用户主动触发 getSelectedContent 时才调用)
+// - 相比高频的剪贴板监控(已由 Swift 库处理),性能影响可接受
+//
+// 未来优化方向:
+// - 将此文件重命名为 .mm 并使用 Objective-C++ 直接调用 NSPasteboard API
+// - 或在 Swift 库中实现这些功能并通过 FFI 调用
+//
+// 当前实现的优点:
+// - 简单可维护,无需额外的 Objective-C++ 编译配置
+// - 与 Swift 库保持清晰的职责分离
+
+// 获取剪贴板文本内容
+std::string GetPasteboardText() {
+ FILE* pipe = popen("pbpaste", "r");
+ if (!pipe) return "";
+
+ std::string result;
+ char buffer[256];
+ while (fgets(buffer, sizeof(buffer), pipe)) {
+ result += buffer;
+ }
+ pclose(pipe);
+ return result;
+}
+
+// 获取剪贴板文件列表
+std::vector GetPasteboardFiles() {
+ std::vector result;
+
+ // 使用 osascript 获取文件列表
+ FILE* pipe = popen("osascript -e 'try' -e 'set theList to (the clipboard as «class furl») as list' -e 'set output to \"\"' -e 'repeat with aFile in theList' -e 'set output to output & POSIX path of aFile & linefeed' -e 'end repeat' -e 'return output' -e 'end try'", "r");
+ if (!pipe) return result;
+
+ char buffer[1024];
+ while (fgets(buffer, sizeof(buffer), pipe)) {
+ std::string line = buffer;
+ // 移除换行符
+ if (!line.empty() && line[line.length() - 1] == '\n') {
+ line.erase(line.length() - 1);
+ }
+ if (!line.empty()) {
+ result.push_back(line);
+ }
+ }
+ pclose(pipe);
+ return result;
+}
+
+// 获取剪贴板图像(base64 PNG)
+std::string GetPasteboardImage() {
+ // 使用临时文件保存图像
+ std::string tmpFile = "/tmp/ztools_clipboard_image.png";
+
+ // 使用 osascript 保存剪贴板图像为 PNG
+ std::string cmd = "osascript -e 'try' -e 'set imgData to the clipboard as «class PNGf»' -e 'set outFile to open for access POSIX file \"" + tmpFile + "\" with write permission' -e 'set eof outFile to 0' -e 'write imgData to outFile' -e 'close access outFile' -e 'end try'";
+ int ret = system(cmd.c_str());
+
+ if (ret != 0) return "";
+
+ // 读取文件并转换为 base64
+ FILE* file = fopen(tmpFile.c_str(), "rb");
+ if (!file) return "";
+
+ fseek(file, 0, SEEK_END);
+ long size = ftell(file);
+ fseek(file, 0, SEEK_SET);
+
+ if (size <= 0) {
+ fclose(file);
+ unlink(tmpFile.c_str());
+ return "";
+ }
+
+ std::vector buffer(size);
+ fread(buffer.data(), 1, size, file);
+ fclose(file);
+ unlink(tmpFile.c_str());
+
+ // Base64 编码
+ const char* base64_chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
+ std::string result;
+ int idx = 0;
+ unsigned char char_array_3[3];
+ unsigned char char_array_4[4];
+
+ for (unsigned char c : buffer) {
+ char_array_3[idx++] = c;
+ if (idx == 3) {
+ char_array_4[0] = (char_array_3[0] & 0xfc) >> 2;
+ char_array_4[1] = ((char_array_3[0] & 0x03) << 4) + ((char_array_3[1] & 0xf0) >> 4);
+ char_array_4[2] = ((char_array_3[1] & 0x0f) << 2) + ((char_array_3[2] & 0xc0) >> 6);
+ char_array_4[3] = char_array_3[2] & 0x3f;
+
+ for (int k = 0; k < 4; k++) {
+ result += base64_chars[char_array_4[k]];
+ }
+ idx = 0;
+ }
+ }
+
+ if (idx) {
+ for (int j = idx; j < 3; j++) {
+ char_array_3[j] = '\0';
+ }
+
+ char_array_4[0] = (char_array_3[0] & 0xfc) >> 2;
+ char_array_4[1] = ((char_array_3[0] & 0x03) << 4) + ((char_array_3[1] & 0xf0) >> 4);
+ char_array_4[2] = ((char_array_3[1] & 0x0f) << 2) + ((char_array_3[2] & 0xc0) >> 6);
+
+ for (int j = 0; j < idx + 1; j++) {
+ result += base64_chars[char_array_4[j]];
+ }
+
+ while (idx++ < 3) {
+ result += '=';
+ }
+ }
+
+ return result;
+}
+
+// 获取选中内容(Mac 实现 - 使用模拟复制)
+Napi::Value GetSelectedContent(const Napi::CallbackInfo &info) {
+ Napi::Env env = info.Env();
+ Napi::Array result = Napi::Array::New(env);
+
+ if (!LoadSwiftLibrary(env)) {
+ return result;
+ }
+
+ // 暂停监控以防止触发自身事件
+ bool wasMonitoring = (tsfn != nullptr && !g_isPaused);
+ if (wasMonitoring) {
+ g_isPaused = true;
+ }
+
+ // 保存原剪贴板内容
+ std::string originalText = GetPasteboardText();
+ std::vector originalFiles = GetPasteboardFiles();
+ std::string originalImage = GetPasteboardImage();
+
+ // 清空剪贴板
+ system("pbcopy < /dev/null");
+
+ // 模拟 Cmd+C(使用 simulateKeyboardTap)
+ if (simulateKeyboardTapFunc != nullptr) {
+ simulateKeyboardTapFunc("c", "meta");
+
+ // 等待剪贴板更新
+ usleep(100000); // 100ms
+
+ // 读取新的剪贴板内容
+ std::string newText = GetPasteboardText();
+ std::vector newFiles = GetPasteboardFiles();
+ std::string newImage = GetPasteboardImage();
+
+ uint32_t index = 0;
+
+ // 检查文本
+ if (!newText.empty() && newText != originalText) {
+ Napi::Object item = Napi::Object::New(env);
+ item.Set("type", "text");
+ item.Set("data", newText);
+ result.Set(index++, item);
+ }
+
+ // 检查文件
+ if (!newFiles.empty()) {
+ bool isDifferent = (newFiles.size() != originalFiles.size());
+ if (!isDifferent) {
+ for (size_t i = 0; i < newFiles.size(); i++) {
+ if (newFiles[i] != originalFiles[i]) {
+ isDifferent = true;
+ break;
+ }
+ }
+ }
+
+ if (isDifferent) {
+ Napi::Object item = Napi::Object::New(env);
+ item.Set("type", "file");
+ Napi::Array fileArray = Napi::Array::New(env);
+ for (size_t i = 0; i < newFiles.size(); i++) {
+ fileArray.Set(uint32_t(i), newFiles[i]);
+ }
+ item.Set("data", fileArray);
+ result.Set(index++, item);
+ }
+ }
+
+ // 检查图像
+ if (!newImage.empty() && newImage != originalImage) {
+ Napi::Object item = Napi::Object::New(env);
+ item.Set("type", "image");
+ item.Set("data", newImage);
+ item.Set("format", "png");
+ item.Set("encoding", "base64");
+ result.Set(index++, item);
+ }
+ }
+
+ // 恢复原剪贴板内容
+ if (!originalText.empty()) {
+ // 转义单引号
+ std::string escapedText = originalText;
+ size_t pos = 0;
+ while ((pos = escapedText.find("'", pos)) != std::string::npos) {
+ escapedText.replace(pos, 1, "'\\''");
+ pos += 4;
+ }
+ std::string cmd = "printf '%s' '" + escapedText + "' | pbcopy";
+ system(cmd.c_str());
+ } else if (!originalFiles.empty()) {
+ // 恢复文件列表比较复杂,这里简化处理
+ // 实际应用中可能需要更完善的实现
+ }
+
+ // 恢复监控状态
+ if (wasMonitoring) {
+ usleep(50000); // 50ms 延迟
+ g_isPaused = false;
+ }
+
+ return result;
+}
+
// 获取当前激活窗口
Napi::Value GetActiveWindow(const Napi::CallbackInfo &info) {
Napi::Env env = info.Env();
@@ -1063,6 +1312,8 @@ Napi::Value StopColorPicker(const Napi::CallbackInfo &info) {
Napi::Object Init(Napi::Env env, Napi::Object exports) {
exports.Set("startMonitor", Napi::Function::New(env, StartMonitor));
exports.Set("stopMonitor", Napi::Function::New(env, StopMonitor));
+ exports.Set("pauseMonitor", Napi::Function::New(env, PauseMonitor));
+ exports.Set("resumeMonitor", Napi::Function::New(env, ResumeMonitor));
exports.Set("startWindowMonitor",
Napi::Function::New(env, StartWindowMonitor));
exports.Set("stopWindowMonitor", Napi::Function::New(env, StopWindowMonitor));
@@ -1088,6 +1339,7 @@ Napi::Object Init(Napi::Env env, Napi::Object exports) {
exports.Set("unicodeType", Napi::Function::New(env, UnicodeType));
exports.Set("setClipboardFiles", Napi::Function::New(env, SetClipboardFiles));
exports.Set("getFileIcon", Napi::Function::New(env, GetFileIcon));
+ exports.Set("getSelectedContent", Napi::Function::New(env, GetSelectedContent));
return exports;
}
diff --git a/src/binding_windows.cpp b/src/binding_windows.cpp
index 5fbef34..85bba23 100644
--- a/src/binding_windows.cpp
+++ b/src/binding_windows.cpp
@@ -50,6 +50,7 @@ namespace Gdiplus {
static HWND g_hwnd = NULL;
static std::thread g_messageThread;
static std::atomic g_isMonitoring(false);
+static std::atomic g_isPaused(false); // 新增:暂停状态
static napi_threadsafe_function g_tsfn = nullptr;
// 剪贴板防抖:Edge 等浏览器复制时会分多次写入不同格式,
@@ -110,7 +111,8 @@ LRESULT CALLBACK ClipboardWndProc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lPa
case WM_TIMER:
if (wParam == CLIPBOARD_DEBOUNCE_TIMER_ID) {
KillTimer(hwnd, CLIPBOARD_DEBOUNCE_TIMER_ID);
- if (g_tsfn != nullptr) {
+ // 仅在未暂停时触发回调
+ if (g_tsfn != nullptr && !g_isPaused) {
napi_call_threadsafe_function(g_tsfn, nullptr, napi_tsfn_nonblocking);
}
}
@@ -227,6 +229,7 @@ Napi::Value StopMonitor(const Napi::CallbackInfo& info) {
Napi::Env env = info.Env();
g_isMonitoring = false;
+ g_isPaused = false; // 重置暂停状态
if (g_hwnd != NULL) {
PostMessageW(g_hwnd, WM_QUIT, 0, 0);
@@ -244,6 +247,20 @@ Napi::Value StopMonitor(const Napi::CallbackInfo& info) {
return env.Undefined();
}
+// 暂停剪贴板监控(不触发回调,但保持监控线程运行)
+Napi::Value PauseMonitor(const Napi::CallbackInfo& info) {
+ Napi::Env env = info.Env();
+ g_isPaused = true;
+ return env.Undefined();
+}
+
+// 恢复剪贴板监控
+Napi::Value ResumeMonitor(const Napi::CallbackInfo& info) {
+ Napi::Env env = info.Env();
+ g_isPaused = false;
+ return env.Undefined();
+}
+
// ==================== 窗口监控功能 ====================
// 窗口信息结构(用于线程安全传递)
@@ -2705,6 +2722,424 @@ WORD GetVirtualKeyCode(const std::string& key) {
return 0; // 未知键
}
+// ==================== 获取选中内容功能 ====================
+
+// UI Automation 接口(延迟加载)
+#include
+#pragma comment(lib, "oleaut32.lib")
+#pragma comment(lib, "crypt32.lib") // For CryptBinaryToStringA
+
+// ==================== 剪贴板内容读取辅助函数 ====================
+
+// 读取剪贴板文本内容
+std::string GetClipboardTextContent() {
+ std::string result;
+
+ if (!OpenClipboard(NULL)) {
+ return result;
+ }
+
+ // 尝试读取 Unicode 文本
+ if (IsClipboardFormatAvailable(CF_UNICODETEXT)) {
+ HANDLE hData = GetClipboardData(CF_UNICODETEXT);
+ if (hData != NULL) {
+ wchar_t* pszText = static_cast(GlobalLock(hData));
+ if (pszText != NULL) {
+ // 使用 wcslen 获取实际长度,避免越界写入
+ int wideLen = static_cast(wcslen(pszText));
+ int utf8Size = WideCharToMultiByte(CP_UTF8, 0, pszText, wideLen, nullptr, 0, nullptr, nullptr);
+ if (utf8Size > 0) {
+ result.resize(utf8Size);
+ WideCharToMultiByte(CP_UTF8, 0, pszText, wideLen, &result[0], utf8Size, nullptr, nullptr);
+ }
+ GlobalUnlock(hData);
+ }
+ }
+ }
+ // 回退到 ANSI 文本
+ else if (IsClipboardFormatAvailable(CF_TEXT)) {
+ HANDLE hData = GetClipboardData(CF_TEXT);
+ if (hData != NULL) {
+ char* pszText = static_cast(GlobalLock(hData));
+ if (pszText != NULL) {
+ result = pszText;
+ GlobalUnlock(hData);
+ }
+ }
+ }
+
+ CloseClipboard();
+ return result;
+}
+
+// 读取剪贴板图像内容(返回 base64 编码的 PNG)
+std::string GetClipboardImageContent() {
+ std::string result;
+
+ if (!OpenClipboard(NULL)) {
+ return result;
+ }
+
+ HBITMAP hBitmap = NULL;
+ bool mustDeleteBitmap = false; // 标记是否需要删除 hBitmap
+
+ // 尝试获取位图
+ if (IsClipboardFormatAvailable(CF_BITMAP)) {
+ hBitmap = static_cast(GetClipboardData(CF_BITMAP));
+ } else if (IsClipboardFormatAvailable(CF_DIB)) {
+ HANDLE hDIB = GetClipboardData(CF_DIB);
+ if (hDIB != NULL) {
+ BITMAPINFO* pBMI = static_cast(GlobalLock(hDIB));
+ if (pBMI != NULL) {
+ HDC hDC = GetDC(NULL);
+ void* pBits = reinterpret_cast(pBMI) + pBMI->bmiHeader.biSize;
+ hBitmap = CreateDIBitmap(hDC, &pBMI->bmiHeader, CBM_INIT, pBits, pBMI, DIB_RGB_COLORS);
+ ReleaseDC(NULL, hDC);
+ GlobalUnlock(hDIB);
+ mustDeleteBitmap = true; // 标记需要删除
+ }
+ }
+ }
+
+ if (hBitmap != NULL) {
+ // 使用 GDI+ 将位图转换为 PNG base64
+ Gdiplus::Bitmap* bitmap = Gdiplus::Bitmap::FromHBITMAP(hBitmap, NULL);
+ if (bitmap != NULL) {
+ IStream* pStream = NULL;
+ if (CreateStreamOnHGlobal(NULL, TRUE, &pStream) == S_OK) {
+ CLSID pngClsid;
+ CLSIDFromString(L"{557CF406-1A04-11D3-9A73-0000F81EF32E}", &pngClsid);
+
+ if (bitmap->Save(pStream, &pngClsid, NULL) == Gdiplus::Ok) {
+ HGLOBAL hGlobal = NULL;
+ if (GetHGlobalFromStream(pStream, &hGlobal) == S_OK) {
+ SIZE_T size = GlobalSize(hGlobal);
+ void* pData = GlobalLock(hGlobal);
+ if (pData != NULL) {
+ // Base64 编码
+ DWORD base64Size = 0;
+ CryptBinaryToStringA(static_cast(pData), size, CRYPT_STRING_BASE64 | CRYPT_STRING_NOCRLF, NULL, &base64Size);
+ if (base64Size > 0) {
+ result.resize(base64Size);
+ CryptBinaryToStringA(static_cast(pData), size, CRYPT_STRING_BASE64 | CRYPT_STRING_NOCRLF, &result[0], &base64Size);
+ result.resize(base64Size - 1); // 移除 null terminator
+ }
+ GlobalUnlock(hGlobal);
+ }
+ }
+ }
+ pStream->Release();
+ }
+ delete bitmap;
+ }
+
+ // 根据标记决定是否删除 hBitmap
+ if (mustDeleteBitmap) {
+ DeleteObject(hBitmap);
+ }
+ }
+
+ CloseClipboard();
+ return result;
+}
+
+// 读取剪贴板文件列表
+std::vector GetClipboardFilesList() {
+ std::vector result;
+
+ if (!OpenClipboard(NULL)) {
+ return result;
+ }
+
+ if (IsClipboardFormatAvailable(CF_HDROP)) {
+ HDROP hDrop = static_cast(GetClipboardData(CF_HDROP));
+ if (hDrop != NULL) {
+ UINT fileCount = DragQueryFileW(hDrop, 0xFFFFFFFF, NULL, 0);
+ for (UINT i = 0; i < fileCount; i++) {
+ UINT pathLen = DragQueryFileW(hDrop, i, NULL, 0);
+ if (pathLen > 0) {
+ std::wstring wPath(pathLen, L'\0');
+ DragQueryFileW(hDrop, i, &wPath[0], pathLen + 1);
+
+ // 使用实际长度进行精确转换,避免越界写入
+ int wideLen = static_cast(wPath.length());
+ int utf8Size = WideCharToMultiByte(CP_UTF8, 0, wPath.c_str(), wideLen, nullptr, 0, nullptr, nullptr);
+ if (utf8Size > 0) {
+ std::string utf8Path(utf8Size, '\0');
+ WideCharToMultiByte(CP_UTF8, 0, wPath.c_str(), wideLen, &utf8Path[0], utf8Size, nullptr, nullptr);
+ result.push_back(utf8Path);
+ }
+ }
+ }
+ }
+ }
+
+ CloseClipboard();
+ return result;
+}
+
+// 模拟复制操作(Ctrl + C)
+bool SimulateCopyOperation() {
+ INPUT inputs[4] = {};
+
+ // 1. 按下 Ctrl 键
+ inputs[0].type = INPUT_KEYBOARD;
+ inputs[0].ki.wVk = VK_CONTROL;
+ inputs[0].ki.dwFlags = 0;
+
+ // 2. 按下 C 键
+ inputs[1].type = INPUT_KEYBOARD;
+ inputs[1].ki.wVk = 'C';
+ inputs[1].ki.dwFlags = 0;
+
+ // 3. 释放 C 键
+ inputs[2].type = INPUT_KEYBOARD;
+ inputs[2].ki.wVk = 'C';
+ inputs[2].ki.dwFlags = KEYEVENTF_KEYUP;
+
+ // 4. 释放 Ctrl 键
+ inputs[3].type = INPUT_KEYBOARD;
+ inputs[3].ki.wVk = VK_CONTROL;
+ inputs[3].ki.dwFlags = KEYEVENTF_KEYUP;
+
+ UINT result = SendInput(4, inputs, sizeof(INPUT));
+ return result == 4;
+}
+
+// ==================== 获取选中内容(增强版)====================
+
+// 尝试使用 UI Automation 获取选中文本
+std::string TryGetSelectedTextViaUIAutomation() {
+ HRESULT hr = CoInitializeEx(nullptr, COINIT_APARTMENTTHREADED);
+ bool comInitialized = SUCCEEDED(hr);
+
+ IUIAutomation* pAutomation = nullptr;
+ IUIAutomationElement* pFocusedElement = nullptr;
+ IUIAutomationTextPattern* pTextPattern = nullptr;
+ IUIAutomationTextRangeArray* pSelection = nullptr;
+ std::string selectedText;
+
+ do {
+ hr = CoCreateInstance(__uuidof(CUIAutomation), nullptr, CLSCTX_INPROC_SERVER,
+ __uuidof(IUIAutomation), (void**)&pAutomation);
+ if (FAILED(hr) || !pAutomation) break;
+
+ hr = pAutomation->GetFocusedElement(&pFocusedElement);
+ if (FAILED(hr) || !pFocusedElement) break;
+
+ IUnknown* pPatternUnk = nullptr;
+ hr = pFocusedElement->GetCurrentPatternAs(UIA_TextPatternId, __uuidof(IUIAutomationTextPattern),
+ (void**)&pPatternUnk);
+ if (FAILED(hr) || !pPatternUnk) {
+ hr = pFocusedElement->GetCurrentPatternAs(UIA_TextPattern2Id, __uuidof(IUIAutomationTextPattern),
+ (void**)&pPatternUnk);
+ if (FAILED(hr) || !pPatternUnk) break;
+ }
+
+ pTextPattern = static_cast(pPatternUnk);
+
+ hr = pTextPattern->GetSelection(&pSelection);
+ if (FAILED(hr) || !pSelection) break;
+
+ int selectionCount = 0;
+ hr = pSelection->get_Length(&selectionCount);
+ if (FAILED(hr) || selectionCount == 0) break;
+
+ for (int i = 0; i < selectionCount; i++) {
+ IUIAutomationTextRange* pRange = nullptr;
+ hr = pSelection->GetElement(i, &pRange);
+ if (SUCCEEDED(hr) && pRange) {
+ BSTR bstrText = nullptr;
+ hr = pRange->GetText(-1, &bstrText);
+ if (SUCCEEDED(hr) && bstrText) {
+ // 使用 SysStringLen 获取实际长度,避免越界写入
+ int wideLen = static_cast(SysStringLen(bstrText));
+ int utf8Size = WideCharToMultiByte(CP_UTF8, 0, bstrText, wideLen, nullptr, 0, nullptr, nullptr);
+ if (utf8Size > 0) {
+ std::string utf8Text(utf8Size, '\0');
+ WideCharToMultiByte(CP_UTF8, 0, bstrText, wideLen, &utf8Text[0], utf8Size, nullptr, nullptr);
+ if (!selectedText.empty()) selectedText += "\n";
+ selectedText += utf8Text;
+ }
+ SysFreeString(bstrText);
+ }
+ pRange->Release();
+ }
+ }
+ } while (false);
+
+ if (pSelection) pSelection->Release();
+ if (pTextPattern) pTextPattern->Release();
+ if (pFocusedElement) pFocusedElement->Release();
+ if (pAutomation) pAutomation->Release();
+ if (comInitialized) CoUninitialize();
+
+ return selectedText;
+}
+
+// 获取选中内容(Windows 实现)
+Napi::Value GetSelectedContent(const Napi::CallbackInfo& info) {
+ Napi::Env env = info.Env();
+ Napi::Array result = Napi::Array::New(env);
+
+ // 方法1:尝试 UI Automation(适用于标准 Windows 控件)
+ std::string uiaText = TryGetSelectedTextViaUIAutomation();
+ if (!uiaText.empty()) {
+ Napi::Object item = Napi::Object::New(env);
+ item.Set("type", "text");
+ item.Set("data", uiaText);
+ result.Set(uint32_t(0), item);
+ return result;
+ }
+
+ // 方法2:回退到剪贴板方法(适用于 Electron/Chromium 应用)
+ // 暂停监控以防止触发自身事件
+ bool wasMonitoring = g_isMonitoring && !g_isPaused;
+ if (wasMonitoring) {
+ g_isPaused = true;
+ }
+
+ // 保存原剪贴板内容
+ std::string originalText = GetClipboardTextContent();
+ std::string originalImage = GetClipboardImageContent();
+ std::vector originalFiles = GetClipboardFilesList();
+
+ // 清空剪贴板
+ if (OpenClipboard(NULL)) {
+ EmptyClipboard();
+ CloseClipboard();
+ }
+
+ // 模拟 Ctrl+C
+ if (SimulateCopyOperation()) {
+ // 等待剪贴板更新
+ Sleep(100);
+
+ // 读取新的剪贴板内容
+ std::string newText = GetClipboardTextContent();
+ std::string newImage = GetClipboardImageContent();
+ std::vector newFiles = GetClipboardFilesList();
+
+ uint32_t index = 0;
+
+ // 检查文本
+ if (!newText.empty() && newText != originalText) {
+ Napi::Object item = Napi::Object::New(env);
+ item.Set("type", "text");
+ item.Set("data", newText);
+ result.Set(index++, item);
+ }
+
+ // 检查文件
+ if (!newFiles.empty()) {
+ bool isDifferent = (newFiles.size() != originalFiles.size());
+ if (!isDifferent) {
+ for (size_t i = 0; i < newFiles.size(); i++) {
+ if (newFiles[i] != originalFiles[i]) {
+ isDifferent = true;
+ break;
+ }
+ }
+ }
+
+ if (isDifferent) {
+ Napi::Object item = Napi::Object::New(env);
+ item.Set("type", "file");
+ Napi::Array fileArray = Napi::Array::New(env);
+ for (size_t i = 0; i < newFiles.size(); i++) {
+ fileArray.Set(uint32_t(i), newFiles[i]);
+ }
+ item.Set("data", fileArray);
+ result.Set(index++, item);
+ }
+ }
+
+ // 检查图像
+ if (!newImage.empty() && newImage != originalImage) {
+ Napi::Object item = Napi::Object::New(env);
+ item.Set("type", "image");
+ item.Set("data", newImage);
+ item.Set("format", "png");
+ item.Set("encoding", "base64");
+ result.Set(index++, item);
+ }
+ }
+
+ // 恢复原剪贴板内容
+ if (OpenClipboard(NULL)) {
+ EmptyClipboard();
+
+ // 恢复文本
+ if (!originalText.empty()) {
+ int wideSize = MultiByteToWideChar(CP_UTF8, 0, originalText.c_str(), -1, nullptr, 0);
+ if (wideSize > 0) {
+ HGLOBAL hGlobal = GlobalAlloc(GMEM_MOVEABLE, wideSize * sizeof(wchar_t));
+ if (hGlobal != NULL) {
+ wchar_t* pData = static_cast(GlobalLock(hGlobal));
+ if (pData != NULL) {
+ MultiByteToWideChar(CP_UTF8, 0, originalText.c_str(), -1, pData, wideSize);
+ GlobalUnlock(hGlobal);
+ if (SetClipboardData(CF_UNICODETEXT, hGlobal) == NULL) {
+ GlobalFree(hGlobal); // 失败时释放内存
+ }
+ } else {
+ GlobalFree(hGlobal); // GlobalLock 失败时释放内存
+ }
+ }
+ }
+ }
+
+ // 恢复文件列表
+ if (!originalFiles.empty()) {
+ size_t totalSize = sizeof(DROPFILES);
+ for (const auto& file : originalFiles) {
+ int wideSize = MultiByteToWideChar(CP_UTF8, 0, file.c_str(), -1, nullptr, 0);
+ totalSize += wideSize * sizeof(wchar_t);
+ }
+ totalSize += sizeof(wchar_t); // 额外的 null terminator
+
+ HGLOBAL hGlobal = GlobalAlloc(GMEM_MOVEABLE, totalSize);
+ if (hGlobal != NULL) {
+ DROPFILES* pDropFiles = static_cast(GlobalLock(hGlobal));
+ if (pDropFiles != NULL) {
+ pDropFiles->pFiles = sizeof(DROPFILES);
+ pDropFiles->pt.x = 0;
+ pDropFiles->pt.y = 0;
+ pDropFiles->fNC = FALSE;
+ pDropFiles->fWide = TRUE;
+
+ wchar_t* pData = reinterpret_cast(reinterpret_cast(pDropFiles) + sizeof(DROPFILES));
+ size_t remainingChars = (totalSize - sizeof(DROPFILES)) / sizeof(wchar_t);
+ for (const auto& file : originalFiles) {
+ int wideSize = MultiByteToWideChar(CP_UTF8, 0, file.c_str(), -1, pData, static_cast(remainingChars));
+ pData += wideSize;
+ remainingChars -= wideSize;
+ }
+ *pData = L'\0';
+
+ GlobalUnlock(hGlobal);
+ if (SetClipboardData(CF_HDROP, hGlobal) == NULL) {
+ GlobalFree(hGlobal); // 失败时释放内存
+ }
+ } else {
+ GlobalFree(hGlobal); // GlobalLock 失败时释放内存
+ }
+ }
+ }
+
+ CloseClipboard();
+ }
+
+ // 恢复监控状态
+ if (wasMonitoring) {
+ // 延迟恢复,避免立即触发监听
+ Sleep(50);
+ g_isPaused = false;
+ }
+
+ return result;
+}
+
// 模拟粘贴操作(Ctrl + V)
Napi::Value SimulatePaste(const Napi::CallbackInfo& info) {
Napi::Env env = info.Env();
@@ -4989,6 +5424,8 @@ Napi::Value ReadBrowserWindowUrl(const Napi::CallbackInfo& info) {
Napi::Object Init(Napi::Env env, Napi::Object exports) {
exports.Set("startMonitor", Napi::Function::New(env, StartMonitor));
exports.Set("stopMonitor", Napi::Function::New(env, StopMonitor));
+ exports.Set("pauseMonitor", Napi::Function::New(env, PauseMonitor));
+ exports.Set("resumeMonitor", Napi::Function::New(env, ResumeMonitor));
exports.Set("startWindowMonitor", Napi::Function::New(env, StartWindowMonitor));
exports.Set("stopWindowMonitor", Napi::Function::New(env, StopWindowMonitor));
exports.Set("getActiveWindow", Napi::Function::New(env, GetActiveWindowInfo));
@@ -5015,6 +5452,7 @@ Napi::Object Init(Napi::Env env, Napi::Object exports) {
exports.Set("getExplorerFolderPath", Napi::Function::New(env, GetExplorerFolderPath));
// 读取指定浏览器窗口的当前 URL
exports.Set("readBrowserWindowUrl", Napi::Function::New(env, ReadBrowserWindowUrl));
+ exports.Set("getSelectedContent", Napi::Function::New(env, GetSelectedContent));
return exports;
}
diff --git a/test/test-selected-content.js b/test/test-selected-content.js
new file mode 100644
index 0000000..589a20f
--- /dev/null
+++ b/test/test-selected-content.js
@@ -0,0 +1,69 @@
+const { getSelectedContent } = require('../index.js');
+
+console.log('=== 测试获取选中内容功能 ===\n');
+console.log('支持文本、文件、图像三种类型');
+console.log('请在任意应用中选中一些内容(文本/文件/图像)...');
+console.log('将在 3 秒后自动获取选中的内容\n');
+
+// 倒计时
+let countdown = 3;
+const timer = setInterval(() => {
+ console.log(`${countdown}...`);
+ countdown--;
+
+ if (countdown === 0) {
+ clearInterval(timer);
+ console.log('\n正在获取选中的内容...\n');
+
+ try {
+ const contents = getSelectedContent();
+
+ if (contents && contents.length > 0) {
+ console.log(`✓ 成功获取 ${contents.length} 项内容:\n`);
+
+ contents.forEach((item, index) => {
+ console.log(`[${index + 1}] 类型: ${item.type}`);
+ console.log('----------------------------------------');
+
+ switch (item.type) {
+ case 'text':
+ console.log('文本内容:');
+ console.log(item.data);
+ console.log(`文本长度: ${item.data.length} 字符`);
+ break;
+
+ case 'file':
+ console.log('文件列表:');
+ item.data.forEach((file, i) => {
+ console.log(` ${i + 1}. ${file}`);
+ });
+ console.log(`文件数量: ${item.data.length}`);
+ break;
+
+ case 'image':
+ console.log('图像信息:');
+ console.log(` 格式: ${item.format}`);
+ console.log(` 编码: ${item.encoding}`);
+ console.log(` 数据长度: ${item.data.length} 字符 (base64)`);
+ console.log(` 预览: ${item.data.substring(0, 50)}...`);
+ break;
+
+ default:
+ console.log('未知类型:', item);
+ }
+
+ console.log('----------------------------------------\n');
+ });
+ } else {
+ console.log('✗ 当前没有选中内容');
+ console.log('提示: 请在任意应用中选中文本、文件或图像后再运行此测试');
+ }
+ } catch (error) {
+ console.error('✗ 发生错误:', error.message);
+ console.error(error.stack);
+ }
+
+ console.log('测试完成!');
+ process.exit(0);
+ }
+}, 1000);