剪贴板 Clipboard
本教程共 42 篇 · 第 26 篇 · 更新于 2026-08-03
26. 剪贴板 Clipboard
本节目标
- 说清 Wails 剪贴板运行时的能力边界:它只管纯文本
- 会在 Go 侧用
ClipboardGetText/ClipboardSetText读写剪贴板 - 会在 React 前端用
window.runtime调同一组能力,并补上 TypeScript 类型 - 知道什么时候该走 Go、什么时候直接走前端
- 遇到图片、富文本这类需求时,知道有哪些替代路子
26-1 剪贴板运行时能做什么
「剪贴板」(Clipboard)就是系统里那块公共的临时存储区。你在记事本里按 Ctrl+C,内容进去;在别的程序里按 Ctrl+V,内容出来。它是跨进程的,所有应用共用同一块。
Wails 把这块能力包在运行时(Runtime)里,一共就两个方法:一个读、一个写。
ClipboardGetText(ctx context.Context) (string, error)
ClipboardSetText(ctx context.Context, text string) error
前端那侧是对应的两个 Promise 版本:
ClipboardGetText(): Promise<string>
ClipboardSetText(text: string): Promise<boolean>
先把丑话说前面:当前实现只处理文本。你没法用它塞一张图片进剪贴板,也没法读出别人复制的富文本格式。想干这些事得另想办法,26-5 会讲。
再留意两边返回值不一样。Go 侧的写入返回 error,前端侧返回的是 boolean——写成功是 true,失败是 false。写前端逻辑时别把它当 void 用,失败了你会一脸懵。
Note剪贴板是空的时候,
ClipboardGetText不会报错,它老老实实返回空字符串""。所以判断「有没有内容」要看字符串长度,不要指望捕获异常。
TipmacOS 上写中文进剪贴板曾经会变乱码,原因是底层调
pbcopy/pbpaste时没设LANG环境变量。这个问题在 Wails v2.12.0 已经修掉(issue #5012),v2.13.0 自然也带着修复。如果你手上还是更早的版本、又正好遇到中文乱码,升级版本就能解决,不用自己去折腾编码转换。
26-2 Go 侧读写:先把 ctx 存下来
所有 Wails 运行时方法的第一个参数都是 context.Context。这个 ctx 不是你随便 context.Background() 造一个就行的,它是 Wails 启动时塞给你的那一个,里面带着窗口和运行时的句柄。
拿到它的标准姿势是在 OnStartup 生命周期里存一份:
package main
import (
"context"
"github.com/wailsapp/wails/v2/pkg/runtime"
)
type App struct {
ctx context.Context
}
func NewApp() *App {
return &App{}
}
// startup 由 Wails 在应用启动时调用,把运行时上下文交给我们
func (a *App) startup(ctx context.Context) {
a.ctx = ctx
}
存好之后,读写就是两行代码的事:
// CopyToClipboard 把一段文本写进系统剪贴板
func (a *App) CopyToClipboard(text string) error {
return runtime.ClipboardSetText(a.ctx, text)
}
// ReadClipboard 读出剪贴板里的文本
func (a *App) ReadClipboard() (string, error) {
return runtime.ClipboardGetText(a.ctx)
}
别忘了在 main.go 里把 startup 挂上、把 app 绑定出去,不然前端调不到:
func main() {
app := NewApp()
err := wails.Run(&options.App{
Title: "clipboard-demo",
Width: 1024,
Height: 768,
AssetServer: &assetserver.Options{Assets: assets},
BackgroundColour: &options.RGBA{R: 27, G: 38, B: 54, A: 1},
OnStartup: app.startup,
Bind: []interface{}{
app,
},
})
if err != nil {
println("Error:", err.Error())
}
}
前端这样调:
import { CopyToClipboard, ReadClipboard } from "../wailsjs/go/main/App";
await CopyToClipboard("码上学");
const text = await ReadClipboard();
绕一圈走 Go 有什么好处?好处是你可以在写入前做加工。比如复制一段日志,你想自动加上应用版本号和时间戳,这段拼接逻辑放 Go 里比放前端干净。
26-3 前端直调:window.runtime 与类型声明
如果只是「点按钮复制一串文本」,专门为它绑一个 Go 方法就有点重了。Wails 已经把运行时挂在 window.runtime 上,前端可以直接用。
// 写入
const ok = await window.runtime.ClipboardSetText("https://wails.io");
// 读取
const text = await window.runtime.ClipboardGetText();
TypeScript 项目里直接这么写会报错,因为 Window 类型上没有 runtime 这个属性。补一个全局声明就好,在 frontend/src 下新建 wails-runtime.d.ts:
export {};
declare global {
interface Window {
runtime: {
ClipboardGetText(): Promise<string>;
ClipboardSetText(text: string): Promise<boolean>;
};
}
}
Tipreact-ts 模板在
frontend/wailsjs/runtime/下已经生成了一份带类型的运行时模块。想省事也可以import { ClipboardSetText } from "../wailsjs/runtime/runtime",代码提示更全。两种写法调的是同一套东西,选一种在项目里统一即可。
包成一个 React 组件,顺手加上「已复制」的反馈:
import { useState } from "react";
export function CopyButton({ text }: { text: string }) {
const [copied, setCopied] = useState(false);
async function handleCopy() {
const ok = await window.runtime.ClipboardSetText(text);
if (!ok) {
console.warn("写入剪贴板失败");
return;
}
setCopied(true);
setTimeout(() => setCopied(false), 1500);
}
return (
<button onClick={handleCopy}>{copied ? "已复制" : "复制"}</button>
);
}
读取的场景常见于「粘贴导入」。比如用户从群里复制了一串配置,进应用点一下「从剪贴板粘贴」:
async function handlePaste() {
const raw = await window.runtime.ClipboardGetText();
if (!raw.trim()) {
// 剪贴板为空,给个提示就行,不用报错
return;
}
applyConfig(raw.trim());
}
26-4 走 Go 还是走前端
两条路能力完全一样,区别只在数据在哪。给你一个判断依据:
| 场景 | 建议走哪边 | 原因 |
|---|---|---|
| 复制界面上已有的文本 | 前端 | 数据本来就在前端,绕一圈没意义 |
| 复制 Go 侧算出来的结果 | Go | 省一次 IPC 往返 |
| 写入前要拼接、脱敏、加签 | Go | 逻辑集中在后端好维护 |
| 定时把某个值同步到剪贴板 | Go | 后台任务本来就跑在 Go 里 |
| 读剪贴板内容再交给 Go 处理 | Go | 一次调用搞定,别读完再传回去 |
最后一条容易被忽略。有人会在前端 ClipboardGetText() 拿到字符串,再调一个 Go 方法把字符串传过去解析。其实 Go 侧自己就能读,一步到位。
26-5 图片和富文本怎么办
只支持文本这件事,在做截图工具、Markdown 编辑器的时候会很难受。目前有几条路可以走。
第一条,把二进制编码成文本。 图片转 Base64 字符串塞进剪贴板,自家应用之间能互传。缺点也明显:粘到微信、Word 里就是一堆乱码,别人不认。只适合应用内部的复制粘贴。
第二条,用浏览器的异步剪贴板 API。 WebView 里跑的毕竟是标准 Web 环境,navigator.clipboard.write() 在部分平台可用:
async function copyImage(blob: Blob) {
try {
await navigator.clipboard.write([
new ClipboardItem({ [blob.type]: blob }),
]);
} catch (e) {
console.warn("当前环境不支持写入图片剪贴板", e);
}
}
Warning
navigator.clipboard的表现取决于底层 WebView 实现,Windows 的 WebView2、macOS 的 WKWebView、Linux 的 WebKitGTK 支持程度并不一致。真要用,三个平台都得实测,并且一定要写好降级分支。
第三条,在 Go 侧引第三方库。 社区有专门处理系统剪贴板的包,能读写图片。比较常见的是 golang.design/x/clipboard,它按 PNG 格式读写图片,用法是 clipboard.Write(clipboard.FmtImage, pngBytes)。要注意的是这类库对构建环境有额外要求,不同版本对 CGO 和 Linux 上 X11 开发包的依赖不一样,引入前先看清你要用的那个版本的依赖说明。上之前先问自己:这个功能值不值得牺牲「一条命令编三个平台」的便利。
我的建议是先想清楚需求。很多所谓「复制图片」的需求,本质是「把图片存到某处再告诉用户路径」,走保存对话框比硬啃剪贴板省事得多。
常见误区
误区一:在 main() 里直接调运行时方法。 这时候 ctx 还没生成,程序会 panic。所有运行时调用都得在 OnStartup 之后。
误区二:把 ctx 当成普通参数到处传新的。 有人图省事写 runtime.ClipboardSetText(context.Background(), text),编译能过,运行必炸。Wails 靠 ctx 里携带的内部数据找到窗口实例,换一个空 ctx 就找不到了。
误区三:以为写入一定成功。 剪贴板是系统级共享资源,别的程序可能正锁着它。前端记得看返回的 boolean,Go 侧记得判 error。
误区四:在 wails dev 打开的浏览器标签页里测试。 dev 模式会在 http://localhost:34115 起一个网页版。浏览器对剪贴板有安全限制,行为和真实窗口不一样。测剪贴板请以应用窗口里的表现为准。
小结
Wails 的剪贴板 API 简单到没什么可记的:读用 ClipboardGetText,写用 ClipboardSetText,Go 侧和前端各有一份,能力等价。
真正需要拿捏的是两点。一是 ctx 必须来自 OnStartup,这条几乎适用于所有运行时方法,后面章节还会反复遇到。二是只支持文本这个边界,遇到图片需求时别硬凑,先看看能不能换个交互方式解决。
下一章讲另外两个和系统打交道的运行时能力:用系统浏览器打开链接,以及读取屏幕分辨率信息。