Skip to content

Commit b7587a4

Browse files
authored
Merge pull request #477 from lanxiuyun/support-copy-file
windows 支持复制文件到剪贴板
2 parents 6bf6130 + f5f3f03 commit b7587a4

File tree

2 files changed

+178
-6
lines changed

2 files changed

+178
-6
lines changed

src/main/common/api.ts

Lines changed: 45 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,30 @@ import DBInstance from './db';
2727
import getWinPosition from './getWinPosition';
2828
import path from 'path';
2929
import commonConst from '@/common/utils/commonConst';
30+
import { copyFilesToWindowsClipboard } from './windowsClipboard';
31+
32+
/**
33+
* sanitize input files 剪贴板文件合法性校验
34+
* @param input
35+
* @returns
36+
*/
37+
const sanitizeInputFiles = (input: unknown): string[] => {
38+
const candidates = Array.isArray(input)
39+
? input
40+
: typeof input === 'string'
41+
? [input]
42+
: [];
43+
return candidates
44+
.map((filePath) => (typeof filePath === 'string' ? filePath.trim() : ''))
45+
.filter((filePath) => {
46+
if (!filePath) return false;
47+
try {
48+
return fs.existsSync(filePath);
49+
} catch {
50+
return false;
51+
}
52+
});
53+
};
3054

3155
const runnerInstance = runner();
3256
const detachInstance = detach();
@@ -230,13 +254,28 @@ class API extends DBInstance {
230254
}
231255

232256
public copyFile({ data }) {
233-
if (data.file && fs.existsSync(data.file)) {
234-
clipboard.writeBuffer(
235-
'NSFilenamesPboardType',
236-
Buffer.from(plist.build([data.file]))
237-
);
238-
return true;
257+
const targetFiles = sanitizeInputFiles(data?.file);
258+
259+
if (!targetFiles.length) {
260+
return false;
239261
}
262+
263+
if (process.platform === 'darwin') {
264+
try {
265+
clipboard.writeBuffer(
266+
'NSFilenamesPboardType',
267+
Buffer.from(plist.build(targetFiles))
268+
);
269+
return true;
270+
} catch {
271+
return false;
272+
}
273+
}
274+
275+
if (process.platform === 'win32') {
276+
return copyFilesToWindowsClipboard(targetFiles);
277+
}
278+
240279
return false;
241280
}
242281

Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
import { clipboard } from 'electron';
2+
import path from 'path';
3+
4+
// 仅在 Windows 平台辅助操作剪贴板多文件格式。
5+
type ClipboardExModule = typeof import('electron-clipboard-ex');
6+
7+
const DROPFILES_HEADER_SIZE = 20;
8+
9+
let clipboardExModule: ClipboardExModule | null = null;
10+
11+
/**
12+
* Windows 平台专用:尝试加载第三方库 electron-clipboard-ex。
13+
* 这个库能够调用系统底层接口写入“文件复制”数据,成功率更高。
14+
* 其他系统无需加载它,因此这里做了“按需加载”的处理。
15+
*/
16+
const ensureClipboardEx = (): ClipboardExModule | null => {
17+
if (process.platform !== 'win32') return null;
18+
if (clipboardExModule) return clipboardExModule;
19+
try {
20+
// eslint-disable-next-line global-require, @typescript-eslint/no-var-requires
21+
clipboardExModule = require('electron-clipboard-ex');
22+
} catch {
23+
clipboardExModule = null;
24+
}
25+
return clipboardExModule;
26+
};
27+
28+
/**
29+
* 把一组文件路径变成 Windows 规定的文本格式。
30+
* 要求:每个路径之间用单个空字符分隔,最后再额外放两个空字符,表示列表结束。
31+
* Windows 资源管理器会按这个格式解析我们复制到剪贴板的文件。
32+
*/
33+
const buildWindowsFileListPayload = (files: string[]): Buffer =>
34+
Buffer.from(`${files.join('\0')}\0\0`, 'utf16le');
35+
36+
/**
37+
* 构造 CF_HDROP 专用的二进制数据。
38+
* 这是 Windows 复制文件时的底层格式,前 20 字节是固定的结构头,
39+
* 后面紧跟着具体的文件路径(由 buildWindowsFileListPayload 生成)。
40+
* 只要把这个内容写入剪贴板,任何支持粘贴文件的程序都能理解。
41+
*/
42+
const buildWindowsFileDropBuffer = (files: string[]): Buffer => {
43+
const payload = buildWindowsFileListPayload(files);
44+
const header = Buffer.alloc(DROPFILES_HEADER_SIZE);
45+
header.writeUInt32LE(DROPFILES_HEADER_SIZE, 0);
46+
header.writeInt32LE(0, 4);
47+
header.writeInt32LE(0, 8);
48+
header.writeUInt32LE(0, 12);
49+
header.writeUInt32LE(1, 16);
50+
51+
const result = Buffer.alloc(header.length + payload.length);
52+
for (let i = 0; i < header.length; i += 1) {
53+
result[i] = header[i];
54+
}
55+
for (let i = 0; i < payload.length; i += 1) {
56+
result[header.length + i] = payload[i];
57+
}
58+
return result;
59+
};
60+
61+
/**
62+
* 复制/移动/创建快捷方式 等不同操作在 Windows 中对应不同的“意图”值。
63+
* Preferred DropEffect 告诉系统:当前剪贴板数据应该以何种方式处理。
64+
* 我们默认写入“copy”,相当于普通的复制粘贴。
65+
*/
66+
const buildDropEffectBuffer = (effect: 'copy' | 'move' | 'link' = 'copy') => {
67+
const effectMap = {
68+
copy: 1,
69+
move: 2,
70+
link: 4,
71+
} as const;
72+
const buffer = Buffer.alloc(4);
73+
buffer.writeUInt32LE(effectMap[effect], 0);
74+
return buffer;
75+
};
76+
77+
/**
78+
* 直接使用 Electron 内置 API 写入多种剪贴板格式。
79+
* 步骤:
80+
* 1. 写入二进制的 CF_HDROP(含头部与路径列表)
81+
* 2. 写入纯文本形式的 FileNameW(备选格式)
82+
* 3. 写入 Preferred DropEffect(告诉系统“这是复制”)
83+
* 全部成功后,读取一次 CF_HDROP 的长度,确认剪贴板里确实有内容。
84+
*/
85+
const writeWindowsBuffers = (files: string[]): boolean => {
86+
try {
87+
clipboard.writeBuffer('CF_HDROP', buildWindowsFileDropBuffer(files));
88+
clipboard.writeBuffer('FileNameW', buildWindowsFileListPayload(files));
89+
clipboard.writeBuffer('Preferred DropEffect', buildDropEffectBuffer('copy'));
90+
return clipboard.readBuffer('CF_HDROP').length > 0;
91+
} catch {
92+
return false;
93+
}
94+
};
95+
96+
/**
97+
* 如果项目中安装了 electron-clipboard-ex,我们优先使用它。
98+
* 理由:该库通过原生方式与系统交互,兼容性往往优于 Electron 的 JS 层写入。
99+
* 调用成功后,必要时读回文件列表做一次数量校验,确保复制的文件数量正确。
100+
*/
101+
const writeWithClipboardEx = (files: string[]): boolean => {
102+
const clipboardEx = ensureClipboardEx();
103+
if (!clipboardEx) return false;
104+
try {
105+
clipboardEx.writeFilePaths(files);
106+
if (typeof clipboardEx.readFilePaths === 'function') {
107+
const result = clipboardEx.readFilePaths();
108+
return Array.isArray(result) && result.length === files.length;
109+
}
110+
return true;
111+
} catch {
112+
return false;
113+
}
114+
};
115+
116+
/**
117+
* 对外暴露的唯一入口。
118+
* 1. 先把所有路径换成 Windows 可识别的标准形式(path.normalize)。
119+
* 2. 尝试使用 electron-clipboard-ex 写入,如果成功就结束。
120+
* 3. 若第三方库不可用或失败,再退回 Electron 原生写入流程。
121+
* 这一层屏蔽了所有细节,外部调用者只需传入字符串数组即可。
122+
*/
123+
export const copyFilesToWindowsClipboard = (files: string[]): boolean => {
124+
const normalizedFiles = files
125+
.map((filePath) => path.normalize(filePath))
126+
.filter(Boolean);
127+
if (!normalizedFiles.length) return false;
128+
if (writeWithClipboardEx(normalizedFiles)) {
129+
return true;
130+
}
131+
return writeWindowsBuffers(normalizedFiles);
132+
};
133+

0 commit comments

Comments
 (0)