普通视图

发现新文章,点击刷新页面。
昨天 — 2025年1月17日首页

Tauri(五)——实现托盘菜单和图标切换功能

2025年1月17日 22:47

前言

在桌面端应用中,托盘图标是常见的功能,本文将以 Tauri V2 框架为例,展示如何实现托盘菜单以及根据主题切换托盘图标的功能。以下是效果截图和详细实现步骤和代码说明。

image.png

1. 修改 Cargo.toml 添加依赖

首先,在 src-tauri/Cargo.toml 文件中添加如下依赖:

[dependencies]
tauri = { version = "2.0.6", features = ["tray-icon", "image-png"] }
  • tray-icon: 启用托盘图标功能。
  • image-png: 支持自定义 PNG 图标。

2. 实现托盘菜单功能

在 Rust 中,我们创建一个 enable_tray 函数,用于初始化托盘菜单及其事件。

enable_tray 函数

fn enable_tray(app: &mut tauri::App) {
    use tauri::{
        image::Image,
        menu::{MenuBuilder, MenuItem},
        tray::TrayIconBuilder,
    };
    
    // 退出按钮
    let quit_i = MenuItem::with_id(app, "quit", "Quit Coco", true, None::<&str>).unwrap();
    // 设置按钮
    let settings_i = MenuItem::with_id(app, "settings", "Settings...", true, None::<&str>).unwrap();
    // 打开按钮
    let open_i = MenuItem::with_id(app, "open", "Open Coco", true, None::<&str>).unwrap();
    // 关于按钮
    let about_i = MenuItem::with_id(app, "about", "About Coco", true, None::<&str>).unwrap();
    // 隐藏按钮
    let hide_i = MenuItem::with_id(app, "hide", "Hide Coco", true, None::<&str>).unwrap();
    // ......

    // 按照一定顺序 把按钮 放到 菜单里
    let menu = MenuBuilder::new(app)
        .item(&open_i)
        .separator() // 分割线
        .item(&hide_i)
        .item(&about_i)
        .item(&settings_i)
        .separator() // 分割线
        .item(&quit_i)
        .build()
        .unwrap();

    let _tray = TrayIconBuilder::with_id("tray")
        // .icon(app.default_window_icon().unwrap().clone()) // 默认的图片
        .icon(Image::from_bytes(include_bytes!("../icons/light@2x.png")).expect("REASON")) // 自定义的图片
        .menu(&menu)
        .on_menu_event(|app, event| match event.id.as_ref() {
            "open" => {
                handle_open_coco(app);  // 打开事件
            }
            "hide" => {
                handle_hide_coco(app);
            }
            "about" => {
                let _ = app.emit("open_settings", "about");
            }
            "settings" => {
                // windows failed to open second window, issue: https://github.com/tauri-apps/tauri/issues/11144 https://github.com/tauri-apps/tauri/issues/8196
                //#[cfg(windows)]
                let _ = app.emit("open_settings", "");

                // #[cfg(not(windows))]
                // open_settings(&app);
            }
            "quit" => {
                println!("quit menu item was clicked");
                app.exit(0);
            }
            _ => {
                println!("menu item {:?} not handled", event.id);
            }
        })
        .build(app)
        .unwrap();
}

功能说明

  • 菜单项创建:使用 MenuItem::with_id 方法创建菜单项并设置唯一 ID 和显示文本。
  • 菜单构建:通过 MenuBuilder 组合菜单项并添加分隔符。
  • 托盘图标构建:通过 TrayIconBuilder 设置图标、菜单及点击事件。
  • 事件监听:在 on_menu_event 中根据菜单项 ID 处理对应事件。

3. 注册托盘菜单

在 Tauri 应用启动时,调用 enable_tray 注册托盘菜单。

#[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() {
    let mut ctx = tauri::generate_context!();

    tauri::Builder::default()
        .invoke_handler(tauri::generate_handler![
            switch_tray_icon, // 切换托盘图标
        ])
        .setup(|app| {
            init(app.app_handle());

            enable_tray(app); // 注册事件

            Ok(())
        })
        .run(ctx)
        .expect("error while running tauri application");
}

4. 实现托盘图标切换

为了根据主题切换托盘图标,我们需要创建一个 switch_tray_icon 命令。

switch_tray_icon 命令

#[tauri::command]
fn switch_tray_icon(app: tauri::AppHandle, is_dark_mode: bool) {
    let app_handle = app.app_handle();

    println!("is_dark_mode: {}", is_dark_mode);

    const DARK_ICON_PATH: &[u8] = include_bytes!("../icons/dark@2x.png");
    const LIGHT_ICON_PATH: &[u8] = include_bytes!("../icons/light@2x.png");

    // 根据 app 的主题切换 图标
    let icon_path: &[u8] = if is_dark_mode {
        DARK_ICON_PATH
    } else {
        LIGHT_ICON_PATH
    };

    // 获取托盘
    let tray = match app_handle.tray_by_id("tray") {
        Some(tray) => tray,
        None => {
            eprintln!("Tray with ID 'tray' not found");
            return;
        }
    };

    // 设置图标
    if let Err(e) = tray.set_icon(Some(
        tauri::image::Image::from_bytes(icon_path)
            .unwrap_or_else(|e| panic!("Failed to load icon from bytes: {}", e)),
    )) {
        eprintln!("Failed to set tray icon: {}", e);
    }
}

代码说明

  • 动态加载图标:根据 is_dark_mode 参数决定使用亮色或暗色图标。
  • 更新托盘图标:通过 set_icon 方法更新图标。
  • 错误处理:在托盘实例不存在或图标加载失败时记录错误日志。

5. 前端调用 Rust 命令

前端可以通过 Tauri 的 invoke API 调用 switch_tray_icon 命令。

示例代码

import { invoke } from "@tauri-apps/api/core";

async function switchTrayIcon(value: "dark" | "light") {
    try {
      // invoke  switch_tray_icon 事件名 isDarkMode 参数名
      await invoke("switch_tray_icon", { isDarkMode: value === "dark" });
    } catch (err) {
      console.error("Failed to switch tray icon:", err);
    }
  }

在主题切换时调用 switchTrayIcon 即可实现图标动态切换。

小结

通过本文的实现,我们完成了以下功能:

  1. 创建自定义托盘菜单。(更丰富的菜单内容可以自行扩展了)
  2. 响应托盘菜单事件。
  3. 根据主题动态切换托盘图标。(不仅仅可以主题切换图标,还可以依据 app 行为修改对应的图标)

这种方式为 Tauri 应用提供了更加友好的用户体验。如果有其他需求,可以在菜单事件中扩展更多功能。

参考

  1. v2.tauri.app/learn/syste…
  2. github.com/infinilabs/…

开源

最近,我正在基于 Tauri 开发一款项目,名为 Coco。目前已开源,项目仍在不断完善中,欢迎大家前往支持并为项目点亮免费的 star 🌟!

作为个人的第一个 Tauri 项目,开发过程中也是边探索边学习。希望能与志同道合的朋友一起交流经验、共同成长!

代码中如有问题或不足之处,期待小伙伴们的宝贵建议和指导!

非常感谢您的支持与关注!

Electron发送数据到后端(15)

作者 关山月
2025年1月17日 21:18

Electron应用程序有前端进程(称为“renderer”)和后端进程(称为“main”)。在这两者之间还有一些桥接代码(“preload”),可以访问后端API。

到目前为止,我们完全忽略了后端进程,在前端和预加载中完成了所有工作。

让我们看看后端和前端如何通信。

启动新应用程序

让我们做点什么。创建一个新的项目:

$ npm init -y
$ npm install --save-dev electron

index.html

让我们从一个简单的index.html开始。

<!DOCTYPE html>
<html>
  <body>
    <style>
      body {
        background-color: #444;
        color: #ccc;
        font-family: monospace;
        font-size: 24px;
      }
      form {
        display: flex;
      }
      input {
        background-color: inherit;
        color: inherit;
        font-family: inherit;
        border: none;
        font-size: inherit;
        flex: 1;
      }
    </style>
    <h1>Print to terminal</h1>
    <form>
      <input type="text" autofocus />
    </form>
    <div id="responses"></div>
    <script src="app.js"></script>
  </body>
</html>

后端代码index.js

我们可以像以前一样。稍后我们将在这个文件中添加一个额外的东西。现在,让我们打开index.html并赋予它完全的权限:

let { app, BrowserWindow } = require("electron")

function createWindow() {
  let win = new BrowserWindow({
    webPreferences: {
      nodeIntegration: true,
      contextIsolation: false,
    }
  })
  win.loadFile("index.html")
}

app.on("ready", createWindow)

app.on("window-all-closed", () => {
  app.quit()
})

前端代码app.js

在前端,我们需要一个事件处理程序。我们获取提交的数据,将其发送到后端,获取其响应,并将其附加到#responses

let form = document.querySelector("form")
let input = document.querySelector("input")
let responses = document.querySelector("#responses")

form.addEventListener("submit", async (e) => {
  e.preventDefault()
  let line = input.value
  input.value = ""
  let responseText = // what do we do here?
  let response = document.createElement("div")
  response.textContent = responseText
  responses.appendChild(response)
})

我们如何将数据发送到后端?具体操作如下:

let { ipcRenderer } = require("electron")

let form = document.querySelector("form")
let input = document.querySelector("input")
let responses = document.querySelector("#responses")

form.addEventListener("submit", async (e) => {
  e.preventDefault()
  let line = input.value
  input.value = ""
  let responseText = await ipcRenderer.invoke("console", line)
  let response = document.createElement("div")
  response.textContent = responseText
  responses.appendChild(response)
})

IPC是“进程间通信”,或者说是不同进程之间进行通信的一种方式。

后端处理程序

let { ipcMain } = require("electron")

ipcMain.handle("console", (event, line) => {
  console.log(`Received from frontend: ${line}`)
  return `Backend confirms it received: ${line}`
})

结果

运行:

npx electron .  

代码:github.com/fengjutian/…

原文:dev.to/taw/electro…

昨天以前首页

深入浅出 Electron 进程间通信:从入门到精通

作者 烛阴
2025年1月15日 23:29

Electron 框架允许我们使用 Web 技术(HTML, CSS, JavaScript)构建跨平台的桌面应用程序。然而,Electron 应用并非简单的网页,它由多个进程组成,这些进程之间的有效通信是构建复杂、高性能应用的关键。本文将深入探讨 Electron 中进程间通信(IPC)的各种方法,并提供最佳实践,助你打造卓越的桌面应用。

Electron 的多进程架构

在深入了解 IPC 之前,我们先来回顾一下 Electron 的多进程架构:

  • 主进程 (Main Process): 负责创建和管理应用窗口,处理系统事件,以及与操作系统交互。它运行在 Node.js 环境中,拥有完整的 Node.js API 访问权限。
  • 渲染进程 (Renderer Process): 负责渲染用户界面,运行 Web 页面代码。每个应用窗口都有一个独立的渲染进程。它运行在 Chromium 环境中,拥有 Web API 的访问权限。

由于安全性和隔离性的考虑,主进程和渲染进程之间不能直接共享内存或调用函数。因此,我们需要使用 IPC 机制来实现它们之间的通信。

Electron 中常用的 IPC 方法

Electron 提供了多种 IPC 方法,每种方法都有其适用场景:

  1. ipcMainipcRenderer 模块:

    • 原理: 这是 Electron 最基础的 IPC 机制。主进程使用 ipcMain 模块监听来自渲染进程的消息,渲染进程使用 ipcRenderer 模块发送消息给主进程。

    • 特点:

      • 简单易用: API 直观,易于上手。
      • 单向通信: 消息只能从渲染进程发送到主进程,或从主进程发送到渲染进程。
      • 异步通信: 消息发送和接收都是异步的,不会阻塞进程。
    • 适用场景: 简单的消息传递,例如:

      • 渲染进程请求主进程执行某些操作(如打开文件、保存数据)。
      • 主进程向渲染进程发送更新通知(如数据更新、状态改变)。
    • 代码示例:

      // 主进程 (main.js)
      const { app, BrowserWindow, ipcMain } = require('electron');
      
      function createWindow() {
        const win = new BrowserWindow({
          webPreferences: {
            nodeIntegration: true,
            contextIsolation: false,
          },
        });
        win.loadFile('index.html');
      
        ipcMain.on('message-from-renderer', (event, message) => {
          console.log('主进程收到消息:', message);
          event.reply('reply-from-main', 'Hello from main process!');
        });
      }
      
      app.whenReady().then(createWindow);
      
      // 渲染进程 (index.js)
      const { ipcRenderer } = require('electron');
      
      ipcRenderer.send('message-from-renderer', 'Hello from renderer process!');
      
      ipcRenderer.on('reply-from-main', (event, message) => {
        console.log('渲染进程收到回复:', message);
      });
      
  2. contextBridge 模块:

    • 原理: 在渲染进程和主进程之间创建一个安全的桥梁,允许渲染进程访问主进程中预先暴露的 API。

    • 特点:

      • 安全可靠: 避免了 remote 模块的安全风险。
      • 可控性强: 可以精确控制渲染进程可以访问的 API。
      • 代码稍复杂: 需要定义桥梁 API。
    • 适用场景: 需要安全地暴露主进程 API 给渲染进程,例如:

      • 访问文件系统。
      • 执行系统命令。
      • 与数据库交互。
    • 代码示例:

      // 主进程 (main.js)
      const { app, BrowserWindow, contextBridge, ipcMain } = require('electron');
      
      function createWindow() {
        const win = new BrowserWindow({
          webPreferences: {
            preload: path.join(__dirname, 'preload.js'),
            contextIsolation: true,
          },
        });
        win.loadFile('index.html');
        
        ipcMain.on('message-from-renderer', (event, message) => {
          console.log('主进程收到消息:', message);
          event.reply('reply-from-main', 'Hello from main process!');
        });
      }
      
      app.whenReady().then(createWindow);
      
      // preload.js
      const { contextBridge, ipcRenderer } = require('electron');
      
      contextBridge.exposeInMainWorld('api', {
        sendMessage: (message) => ipcRenderer.send('message-from-renderer', message),
        onReply: (callback) => ipcRenderer.on('reply-from-main', callback),
      });
      
      // 渲染进程 (index.js)
      window.api.sendMessage('Hello from renderer process!');
      window.api.onReply((event, message) => {
        console.log('渲染进程收到回复:', message);
      });
      
  3. webContents.sendwebContents.on

    • 原理: 允许主进程向特定的渲染进程发送消息,渲染进程使用 ipcRenderer 模块接收消息。

    • 特点:

      • 定向通信: 可以精确控制消息发送的目标渲染进程。
      • 异步通信: 消息发送和接收都是异步的。
    • 适用场景: 主进程需要向特定窗口发送消息,例如:

      • 更新特定窗口的数据。
      • 控制特定窗口的行为。
    • 代码示例:

      // 主进程 (main.js)
      const { app, BrowserWindow, ipcMain } = require('electron');
      
      let win;
      function createWindow() {
        win = new BrowserWindow({
          webPreferences: {
            nodeIntegration: true,
            contextIsolation: false,
          },
        });
        win.loadFile('index.html');
      
        win.webContents.on('did-finish-load', () => {
          win.webContents.send('message-from-main', 'Hello from main process!');
        });
      }
      
      app.whenReady().then(createWindow);
      
      // 渲染进程 (index.js)
      const { ipcRenderer } = require('electron');
      
      ipcRenderer.on('message-from-main', (event, message) => {
        console.log('渲染进程收到消息:', message);
      });
      

总结

Electron 的进程间通信是构建复杂桌面应用的关键。理解各种 IPC 方法的原理和适用场景,并遵循最佳实践,可以帮助你构建高性能、安全可靠的 Electron 应用。希望本文能为你提供有价值的参考,助你在 Electron 开发的道路上更进一步!

❌
❌