首页 / Wails 入门教程 / 剪贴板 Clipboard

Wails 入门教程

剪贴板 Clipboard

本教程共 42 篇 · 第 26 篇 · 更新于 2026-08-03

Wails桌面开发ClipboardRuntimeReact

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 不会报错,它老老实实返回空字符串 ""。所以判断「有没有内容」要看字符串长度,不要指望捕获异常。

Tip

macOS 上写中文进剪贴板曾经会变乱码,原因是底层调 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>;
    };
  }
}
Tip

react-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,这条几乎适用于所有运行时方法,后面章节还会反复遇到。二是只支持文本这个边界,遇到图片需求时别硬凑,先看看能不能换个交互方式解决。

下一章讲另外两个和系统打交道的运行时能力:用系统浏览器打开链接,以及读取屏幕分辨率信息。