Sep 08, 2025
3 min read
Tauri,
Rust,
macOS,

Tauri 截图应用开发进阶:macOS全屏处理与剪贴板操作

深入探讨使用 Tauri 开发截图应用时在 macOS 平台上的特殊处理,包括全屏显示问题、剪贴板操作异常及解决方案,以及快捷键和预览功能优化等关键技术细节。

macos 上的全屏

如果你在 windows 上或者 linux 上使用 "fullscreen": true,或许没什么问题。然而不出意外的话,在截图这个功能上,macOS 是要出意外的。

之前我们讲过,截图软件本质上就是一个透明的置顶的全屏的应用。 置顶在 macOS 上不会有问题。 透明在 macOS 上需要开启 "macOSPrivateApi": true 属性并在 crate 上要开启以下 features:

tauri = { version = "2", features = ["macos-private-api"] }

而全屏也不能使用 "fullscreen": true属性。因为 macOS 上的全屏,会单独地自动在第二个桌面空间进行全屏。

而是要使用"maximized": true。我们应该在应用加载完后,把应用设置最大化:

  // 页面加载后将窗口最大化
  useEffect(() => {
    const maximizeWindow = async () => {
      const currentWindow = getCurrentWebviewWindow();
      await currentWindow.maximize();
    };
    
    maximizeWindow();
  }, []);

复制截图到剪切板

首先,前端是有复制图片到剪切板功能的,这个 api 基本现代浏览器应该都支持,api 如下:

const blob = new Blob([new Uint8Array(image).buffer], { type: "image/png" });
const clipboardItem = new ClipboardItem({ 'image/png': blob })
await navigator.clipboard.write([clipboardItem])

如果你真的使用上面的 api,则会得到下面的错误:

NotAllowedError: The request is not allowed by the user agent or the platform in the current context, possibly because the user denied permission

原因是前端的 API 需要在安全上下文(HTTPS或localhost)中,并且调用时需要明确获得用户的许可。

第二种方法是通过剪切板插件clipboard-manager:

pnpm tauri add clipboard-manager

import { Image as TauriImage } from "@tauri-apps/api/image";
const image: Uint8Array = await invoke("capture", {
	x: selectionRect.x,
	y: selectionRect.y,
	width: selectionRect.width,
	height: selectionRect.height
  });
// 写入剪贴板
await writeImage(image);

但是,在 macOS 上,你也有可能会得到一个 bug:

thread 'tokio-runtime-worker' panicked at /Users/xxx/.cargo/registry/src/rsproxy.cn-e3de039b2554c837/arboard-3.6.1/src/platform/osx.rs:88:6:
called `Option::unwrap()` on a `None` value
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace

clipboard-manager 依赖 arboard 这个 crate 实现剪切版功能,而 arboard 这个 crate 在 macOS 平台上调用的是 cocoa 框架的 API。不幸的是, arboard 在这里直接来了个 unwrap:

	let cg_image = unsafe {
		CGImageCreate(
			width,
			height,
			8,
			32,
			4 * width,
			Some(&colorspace),
			CGBitmapInfo::ByteOrderDefault | CGBitmapInfo(CGImageAlphaInfo::Last.0),
			Some(&provider),
			ptr::null_mut(),
			false,
			CGColorRenderingIntent::RenderingIntentDefault,
		)
	}
	.unwrap();

目前 arboard 已经修改了这行代码,改为返回Error了.

那如何知道这个 bug 具体的原因是什么? 答案是 console.app 这个应用(中文又叫控制台)。通过打开控制台,点击开始记录日志,再复现出现 bug 的操作,我们就能得到上面的错误日志了。

如果你直接把二进制图片写进剪切板:

// image 是二进制文件数据
const img_data = await TauriImage.new(image, selectionRect.width, selectionRect.height)
 await writeImage(img_data);
 // 或者直接 writeImage(image);

大概会在控制台里找到这么一个错误日志:

CGImageCreate: invalid image data size: 165 (height) x 836 (bytesPerRow) data provider size 27295

在 windows 系统上,这个错误会直接显示出来。

按官方的示例,我们还可能碰到这个错误:

Unhandled Promise Rejection: expected RGBA image data, found raw bytes

事实上,绕这么一大圈,这仅仅是官方文档的问题。它在插件项里没有提到,只是在 javascript api 文档里提到了,tauri 需要添加image-ico 或 image-png features:

[dependencies]
tauri = { version = "...", features = ["...", "image-png"] }

保存截图

没什么可说的,直接使用 dialog 调出保存到文件拿到文件路径,再传到 rust 层保存即可。

import { save } from '@tauri-apps/plugin-dialog';

const path = await save({
      filters: [
        {
          name: 'screenshot',
          extensions: ['png', 'jpeg'],
        },
      ],
    });
    
    try {

      await invoke("capture", {
        x: selectionRect.x,
        y: selectionRect.y,
        width: selectionRect.width,
        height: selectionRect.height,
        savePath: path,
      });

      const currentWindow = getCurrentWebviewWindow();
      await currentWindow.close();
    } catch (error) {
      console.error("关闭窗口时出错:", error);
    }

快捷键

截图应用是不应该一直顶置的,所以我们需要截图后把主窗口隐藏掉,当需要时再调用出来,这时候需要实现快捷键唤醒功能。

pnpm tauri add global-shortcut

需要注意的是,上面的命令会自动在 lib.rs添加这么一行代码:

.plugin(tauri_plugin_global_shortcut::Builder::new().build())

由于我们需要在 rust 层实现快捷键唤醒,所以需要把这行去掉,重新实现。 比如我们要实现一个ctrl + shift + s (mac: command + shift + s)截图功能,在响应回调里实现显示主窗口。

.setup(|app| {
            #[cfg(desktop)]
            {
                use tauri::Manager;
                use tauri_plugin_global_shortcut::{Code, GlobalShortcutExt, Modifiers, Shortcut};
                let ctrl_shift_s_shortcut =
                    Shortcut::new(Some(Modifiers::SUPER | Modifiers::SHIFT), Code::KeyS);

                app.handle().plugin(
                    tauri_plugin_global_shortcut::Builder::new()
                        .with_handler(move |app, shortcut, _event| {
                            let app_handle = app.clone(); // 克隆 handle 用于闭包
                            println!("{:?}", shortcut);
                            if shortcut == &ctrl_shift_s_shortcut {
                                println!("Ctrl-Shift-S Detected!");
                                app_handle
                                    .get_webview_window("main")
                                    .unwrap()
                                    .show()
                                    .unwrap();
                            }
                        })
                        .build(),
                )?;

                app.global_shortcut().register(ctrl_shift_s_shortcut)?;
            }
            Ok(())
        })

其他

为什么截图预览在 tauri 上会闪?

这是由于我们使用定时器去调用 rust 层对鼠标预览区进行截图来实现的,定时器的间隔越大,这个预览功能更闪。如果我们能把这个功能的整体耗时优化到 20ms 以内一帧,就大体上不会看到闪的现象了。

据我观察,在我的设备上, 100x100 大小的预览截图大概在 5ms-15ms 之间。这个截图耗时加上传输的延迟耗时应该是够用了。这也是为什么说Tauri 写截图应用其实不太占优势的原因之一,最好还是原生框架效果高一些。

感兴趣的话,完整代码在这里:点我直达