基于tauri+vue3.x多开窗口|Tauri创建多窗体实践

news2025/3/12 9:58:05

最近一种在捣鼓 Tauri 集成 Vue3 技术开发桌面端应用实践,tauri 实现创建多窗口,窗口之间通讯功能。

开始正文之前,先来了解下 tauri 结合 vue3.js 快速创建项目。

tauri 在 github 上star高达53K+,而且呈快速增长趋势。相比electron构建应用更具优势。

分别用 Tauri 和 Electron 打包测试一个 todo list 程序。

Electron打包体积  69  M,Tauri打包体积才只有  7.5 M。

Tauri 构建的桌面应用体积远远比 Electron 构建的小得多。因为它放弃了体积庞大的 Chromium 内核和Nodejs,tauri前端集成了 webview,后端使用 Rust。而且 Tauri 构建应用还提供了诸多初始化程序模板,比如原生 JavaScript、Vue2/3、React、Svelte.js、SvelteKit 等。

准备工作

首先您需要安装 Rust 及其他系统依赖。

  • "C++ 生成工具" 和 Windows 10 SDK。
  • Tauri 需要 WebView2 才能在 Windows 上呈现网页内容,所以您必须先安装 WebView2。
  • Rust

具体操作,请前往 预先准备 | Tauri Apps 来按步骤操作。

  • 创建 tauri 初始化项目

具体的前端框架模板,大家根据实际情况选择。

 npm create tauri-app 

  • 开发/构建打包

 tauri dev  tauri build 

非常简单的几步就能快速搭建 vue3+tauri 桌面端模板。接下来就能顺利的开发了。

tauri 也提供了如下几种常用创建多窗口的方法。

  • tauri.conf.json
{
  "tauri": {
    "windows": [
      {
        "label": "external",
        "title": "Tauri App",
        "url": "https://tauri.app"
      },
      {
        "label": "local",
        "title": "Tauri",
        "url": "home.html"
      }
    ]
  }
}
  • src-tauri/src/main.rs
tauri::Builder::default()
  .setup(|app| {
    let docs_window = tauri::WindowBuilder::new(
      app,
      "external", /* the unique window label */
      tauri::WindowUrl::External("https://tauri.app/".parse().unwrap())
    ).build()?;
    let local_window = tauri::WindowBuilder::new(
      app,
      "local",
      tauri::WindowUrl::App("index.html".into())
    ).build()?;
    Ok(())
})
  • 通过前端 JS 创建窗口。
import { WebviewWindow } from '@tauri-apps/api/window'
const webview = new WebviewWindow('main_win', {
  url: '/home',
})

webview.once('tauri://created', function () {
  // webview window successfully created
})
webview.once('tauri://error', function (e) {
  // an error happened creating the webview window
})

具体详细的介绍,大家可以去官网查看,文档都有非常详细的讲解。

Multiwindow | Tauri Apps

上面介绍的方法比较适用于一些简单的窗口,对于一些复杂多开窗口,还得封装一个窗口创建器,直接通过传入参数快速生成窗体。

createWin({
    label: 'Home',
    title: '主页',
    url: '/home',
    width: 800,
    height: 600,
})

新建一个 windows 文件夹,用来封装窗口及调用窗口。

/**
 * @desc    窗口容器
 * @author: YXY  Q:282310962
 * @time    2022.10
 */

import { WebviewWindow, appWindow, getAll, getCurrent } from '@tauri-apps/api/window'
import { relaunch, exit } from '@tauri-apps/api/process'
import { emit, listen } from '@tauri-apps/api/event'

import { setWin } from './actions'

// 系统参数配置
export const windowConfig = {
    label: null,            // 窗口唯一label
    title: '',              // 窗口标题
    url: '',                // 路由地址url
    width: 900,             // 窗口宽度
    height: 640,            // 窗口高度
    minWidth: null,         // 窗口最小宽度
    minHeight: null,        // 窗口最小高度
    x: null,                // 窗口相对于屏幕左侧坐标
    y: null,                // 窗口相对于屏幕顶端坐标
    center: true,           // 窗口居中显示
    resizable: true,        // 是否支持缩放
    maximized: false,       // 最大化窗口
    decorations: false,     // 窗口是否无边框及导航条
    alwaysOnTop: false,     // 置顶窗口
}

class Windows {
    constructor() {
        this.mainWin = null
    }

    // 获取窗口
    getWin(label) {
        return WebviewWindow.getByLabel(label)
    }

    // 获取全部窗口
    getAllWin() {
        return getAll()
    }

    // 创建新窗口
    async createWin(options) {
        const args = Object.assign({}, windowConfig, options)

        // 判断窗口是否存在
        const existWin = getAll().find(w => w.label == args.label)
        if(existWin) {
            if(existWin.label.indexOf('main') == -1) {
                await existWin?.unminimize()
                await existWin?.setFocus()
                return
            }
            await existWin?.close()
        }

        // 创建窗口对象
        let win = new WebviewWindow(args.label, args)
        
        // 是否最大化
        if(args.maximized && args.resizable) {
            win.maximize()
        }

        // 窗口创建完毕/失败
        win.once('tauri://created', async() => {
            console.log('window create success!')
            ...
        })

        win.once('tauri://error', async() => {
            console.log('window create error!')
        })
    }

    // 开启主进程监听事件
    async listen() {
        // 创建新窗体
        await listen('win-create', (event) => {
            console.log(event)
            this.createWin(JSON.parse(event.payload))
        })

        // 显示窗体
        await listen('win-show', async(event) => {
            if(appWindow.label.indexOf('main') == -1) return
            await appWindow.show()
            await appWindow.unminimize()
            await appWindow.setFocus()
        })

        // 隐藏窗体
        await listen('win-hide', async(event) => {
            if(appWindow.label.indexOf('main') == -1) return
            await appWindow.hide()
        })

        // 退出应用
        await listen('win-exit', async(event) => {
            setWin('logout')
            await exit()
        })

        // 重启应用
        await listen('win-relaunch', async(event) => {
            await relaunch()
        })

        // 主/渲染进程传参
        await listen('win-setdata', async(event) => {
            await emit('win-postdata', JSON.parse(event.payload))
        })
    }
}

export default Windows

actions.js进行一些调用处理。

/**
 * 处理渲染器进程到主进程的异步通信
 */

import { WebviewWindow } from '@tauri-apps/api/window'
import { emit } from '@tauri-apps/api/event'

/**
 * @desc 创建新窗口
 */
export async function createWin(args) {
    await emit('win-create', args)
}

/**
 * @desc 获取窗口
 * @param args {string}
 */
export async function getWin(label) {
    return await WebviewWindow.getByLabel(label)
}

/**
 * @desc 设置窗口
 * @param type {string} 'show'|'hide'|'close'|'min'|'max'|'max2min'|'exit'|'relaunch'
 */
export async function setWin(type) {
    await emit('win-' + type)
}

/**
 * @desc 登录窗口
 */
export async function loginWin() {
    await createWin({
        label: 'Login',
        title: '登录',
        url: '/login',
        width: 320,
        height: 420,
        resizable: false,
        alwaysOnTop: true,
    })
}

// ...

在需要调用创建窗口的.vue页面,引入actions.js文件。

 import { loginWin, createWin } from '@/windows/actions' 

const createManageWin = async() => {
    createWin({
        label: 'Manage',
        title: '管理页面',
        url: '/manage',
        width: 600,
        height: 450,
        minWidth: 300,
        minHeight: 200
    })
}

const createAboutWin = async() => {
    createWin({
        label: 'About',
        title: '关于页面',
        url: '/about',
        width: 500,
        height: 500,
        resizable: false,
        alwaysOnTop: true
    })
}

一些注意点

  • 创建系统托盘图标

use tauri::{
    AppHandle, Manager, 
    CustomMenuItem, SystemTray, SystemTrayEvent, SystemTrayMenu, SystemTrayMenuItem, SystemTraySubmenu
};

// 托盘菜单
pub fn menu() -> SystemTray {
    let quit = CustomMenuItem::new("quit".to_string(), "Quit");
    let show = CustomMenuItem::new("show".to_string(), "Show");
    let hide = CustomMenuItem::new("hide".to_string(), "Hide");
    let change_ico = CustomMenuItem::new("change_ico".to_string(), "Change Icon");
    let tray_menu = SystemTrayMenu::new()
        .add_submenu(SystemTraySubmenu::new(
            "Language", // 语言菜单
            SystemTrayMenu::new()
                .add_item(CustomMenuItem::new("lang_english".to_string(), "English"))
                .add_item(CustomMenuItem::new("lang_zh_CN".to_string(), "简体中文"))
                .add_item(CustomMenuItem::new("lang_zh_HK".to_string(), "繁体中文")),
        ))
        .add_native_item(SystemTrayMenuItem::Separator) // 分割线
        .add_item(change_ico)
        .add_native_item(SystemTrayMenuItem::Separator)
        .add_item(hide)
        .add_item(show)
        .add_native_item(SystemTrayMenuItem::Separator)
        .add_item(quit);

    SystemTray::new().with_menu(tray_menu)
}

// 托盘事件
pub fn handler(app: &AppHandle, event: SystemTrayEvent) {
    match event {
        SystemTrayEvent::LeftClick {
            position: _,
            size: _,
            ..
        } => {
            println!("点击左键");
        }
        SystemTrayEvent::RightClick {
            position: _,
            size: _,
            ..
        } => {
            println!("点击右键");
        }
        SystemTrayEvent::DoubleClick {
            position: _,
            size: _,
            ..
        } => {
            println!("双击");
        }
        SystemTrayEvent::MenuItemClick { id, .. } => match id.as_str() {
            "change_ico" => { // 更新托盘图标
                app.tray_handle()
                    .set_icon(tauri::Icon::Raw(
                        include_bytes!("../icons/new.png").to_vec()
                    ))
                    .unwrap();
            }
            lang if lang.contains("lang_") => { // 选择语言,匹配 id 前缀包含 `lang_` 的事件
                Lang::new(
                    app,
                    id, // 点击菜单的 id
                    vec![
                        Lang {
                            name: "English",
                            id: "lang_english",
                        },
                        Lang {
                            name: "繁体中文",
                            id: "lang_zh_HK",
                        },
                        Lang {
                            name: "简体中文",
                            id: "lang_zh_CN",
                        },
                    ],
                );
            }
            "hide" => {
                // let window = app.get_window("main").unwrap();
                // window.show().unwrap();
                println!("点击隐藏");
            }
            "show" => {
                println!("点击显示");
            }
            "quit" => {
                println!("点击退出");
                std::process::exit(0);
            }
            _ => {}
        },
        _ => {}
    }
}

struct Lang<'a> {
    name: &'a str,
    id: &'a str,
}

impl Lang<'static> {
    fn new(app: &AppHandle, id: String, langs: Vec<Lang>) {
        // 获取点击的菜单项
        langs.iter().for_each(|lang| {
            let handle = app.tray_handle().get_item(lang.id);
            if lang.id.to_string() == id.as_str() {
                // 设置菜单名称
                handle.set_title(format!("  {}", lang.name)).unwrap();
                // 还可以使用 `set_selected`、`set_enabled` 和 `set_native_image`(仅限 macOS)
                handle.set_selected(true).unwrap();
            } else {
                handle.set_title(lang.name).unwrap();
                handle.set_selected(false).unwrap();
            }
        });
    }
}

创建托盘图标,默认图标文件在src-tauri/icons目录下。如果想使用自定义的.ico图标,可通过tauri.cong.json文件配置。

"systemTray": {
    "iconPath": "icons/tray.ico",
    "iconAsTemplate": true,
    "menuOnLeftClick": false
}

如果setIcon报错,则需要在 src-tauri/src/Cargo.toml 中配置 icon-ico 或 icon-png

  • tauri 配置自定义拖拽区域。

当创建窗口的时候配置了 decorations: false  则会不显示窗口边框及顶部导航栏。

此时在需要拖动元素上加一个  data-tauri-drag-region 属性,即可实现自定义区域拖动窗口功能。这个功能有些类似 electron 中自定义拖拽 -webkit-app-region: drag

不过点击窗口右键,会出现系统菜单。这样显得应用不够原生,可以简单的通过禁用右键菜单来屏蔽功能。

export function disableWinMenu() {
    document.addEventListener('contextmenu', e => e.preventDefault())
}
disableWinMenu()

好了,基于 tauri+vue3 构建多窗口桌面应用就分享到这里。希望对大家有丢丢帮助哈~~ 😙

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/171.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

DDoS报告团伙规模

攻击资源活跃度分析 在攻击源活时间的监测中发现&#xff0c;和 2019 年趋势一致&#xff0c;存活时间大于 10 天的攻击资源占比 11%。像这种能够长期被控制的肉鸡大部分都是物联网 设备&#xff0c;物联网设备大都存在设备系统老&#xff0c;人员维 护少&#xff0c;更新慢等…

vue当中的事件处理

1.绑定监听v-on 最简单的一个绑定监听的事件 <body><div id"root"><h1>my name is {{name}}</h1><button v-on:click"showInfo">click me</button></div><script type"text/javascript">Vue.…

HotSpot 虚拟机对象探秘-对象的创建、内存布局、访问定位

目录对象的创建检查类的符号引用&#xff0c;是否执行过类的加载过程分配内存指针碰撞&#xff1a;空闲列表&#xff1a;线程安全的问题&#xff0c;对分配内存空间的动作进行同步处理——TLAB初始化虚拟机对对象进行必要的设置&#xff0c;执行构造方法对象的内存布局对象头包…

Spring、MySQL、日期、BigDecimal、集合、反射、序列化中的坑与使用指南

文章目录MySQL中的坑MySQL断开连接Mysql表字段设置为not null如何解决网络瓶颈核心流程的性能查看Spring中的坑与使用注意springboot的配置文件先后顺序定时任务不进行lombok的不适用场景Spring的Bean默认名称生成规则new出来的对象不被Spring所管理SpringBean相关的注解Spring…

Java 类和对象 详解+通俗易懂

文章目录类和对象1. 面对对象的初步认识1.1 什么是面向过程&#xff1f;什么又是面向对象&#xff1f;1.2 对象、成员变量和成员方法的关系和理解2. 类的定义和使用2.1 简单认识类2.2 类的定义格式2.3 小试身手3. 类的实例化3.1 什么是实例化3.2 类和对象的说明4. this 引用4.1…

k8s上部署seata-server集群并注册到nacos上

部署前准备 第一步&#xff1a; 创建seata-server需要的表,有现成的阿里云RDS&#xff0c;就直接在RDS上创建数据库了&#xff0c;方便后面统一管理。 具体的 SQL 参考script/server/db &#xff0c;这里使用的是 MySQL 的脚本&#xff0c;数据库名称为 seata&#xff0c;还需…

对外 API 接口,请把握这3 条原则,16 个小点

对外API接口设计 安全性 1、创建appid,appkey和appsecret 2、Token&#xff1a;令牌&#xff08;过期失效&#xff09; 3、Post请求 4、客户端IP白名单 &#xff08;可选&#xff09; 5、单个接口针对IP限流&#xff08;令牌桶限流&#xff0c;漏桶限流&#xff0c;计数器…

git如何回滚,返回到之前的记录

文章目录1.建立一个reset的测试文件&#xff0c;并连续提交。2.进行回滚测试。2.1测试,回滚到第二次提交2.1.1首先使用git log命令查看commit记录2.1.2查看结果.2.1.3回滚结果提交到远程2.2.你发现自己回滚的多了3.1撤销测试3.1建立文件&#xff0c;多次填写内容上传到git3.2撤…

顺序栈和链栈的定义和使用C语言实现(附有完整代码)

栈的基本内容&#xff1a; 无论是我们接下来要讲的栈还是后面要讲到的队列&#xff0c;他们虽然在名字上不同于我们之前的顺序表或者单链表&#xff0c;但是它们本质也是线性表&#xff0c;只是在基本操作上没有表那么“自由”。比如&#xff1a;栈只能从栈顶进行插入和删除&a…

【Linux】Linux文件权限的理解

&#x1f4ac;推荐一款模拟面试、刷题神器 、从基础到大厂面试题&#xff1a;&#x1f449;点击跳转刷题网站进行注册学习 目录 一、Shell是什么&#xff1f; 1、Shell承担用户和内核间的翻译工作 2、拒绝用户非法请求&#xff0c;保护内核 3、派生子进程执行用户指令 二…

SpringCloud

SpringCloud 三 本章知识点 3.1 项目架构演变 3.1.1 单体应用架构 部署到一个war里 部署到一个web容器里&#xff08;如tomcat&#xff09; 公用一个DB 优点: 容易测试 容易部署缺点&#xff1a; 开发效率低 代码维护难 部署不灵活&#xff08;如构建时间特别长&#xff0…

人工智能轨道交通行业周刊-第19期(2022.10.17-10.23)

本期关键词&#xff1a;首都智慧地铁、AI四小龙、文本生成视频、低光照目标检测、天窗、电务人员 1 整理涉及公众号名单 1.1 行业类 RT轨道交通中关村轨道交通产业服务平台人民铁道世界轨道交通资讯网铁路信号技术交流北京铁路轨道交通网上榜铁路视点ITS World轨道交通联盟V…

四非到保研厦大,我们还有多少路要走----技术人的保研之路

前言&#xff1a; “Hello&#xff0c;大家好我叫是Dream呀&#xff01;”在1024程序员节到来之际&#xff0c;我想和大家一起分享一下技术人生的故事----我的学长2022年从四非保研至厦大的经验和经历&#xff0c;希望大家可以耐心看完&#xff0c;收获技术力量&#xff0c;更加…

Java面试题

1、JDK 、JRE和JVM 的区别&#xff1f; JDK&#xff0c;java development kit java开发工具包&#xff0c;为java程序提供开发和运行环境JRE&#xff0c; java runtime environment java运行环境&#xff0c;为java程序提供必须的运行环境JVM&#xff0c; java virtual mach…

RayVentory以改进IT的分析,RayVentory原始数据之间轻松切换

使用KeyCloak进行SSO(单点登录)-使用单点登录解决方案&#xff0c;并直接与KeyClok交互。 特定于公司的数据丰富—使用产品所有者、价格或许可证信息等信息轻松丰富您的数据&#xff0c;以获得所需数据的综合视图。 用于更深入数据管理的新连接器-添加了许多新连接器&#xff0…

Vue3中 响应式 API ( readonly、shallowReadonly、toRaw、markRaw ) 详解

传送门&#xff1a;Vue3中 响应式 API ( reactive、ref、toRef、toRefs ) 详解 传送门&#xff1a;Vue3中 响应式 API&#xff08; shallowReactive、shallowRef、triggerRef 、customRef &#xff09;详解 1. readonly 函数 接受一个对象 (不论是响应式还是普通的) 或是一个…

深度学习visio作图技巧

目录 1.不显示跨线 2.调节连接线拐弯 3.方框与连接线反应 4.设计一个卷积块/特征图 5.设计一个特征图方块 1.不显示跨线 不显示跨线&#xff1a;设计→连接线&#xff0c;取消显示 跨线的勾选 2.调节连接线拐弯 连接线拐弯&#xff1a;按住shift拖动线条即可控制连接线拐…

高数(下) 第十二章:无穷级数

文章目录Ch12. 无穷级数(一) 常数项级数正项级数交错级数任意项级数4个特殊的常数项级数收敛级数的性质&#xff08;针对任意项级数&#xff09;常数项级数的审敛法1.正项级数审敛法(判别法)(1)比较判别法(2)比较审敛法极限形式(3)比值法(4)根值法(5)收敛的充要条件(6)绝对收敛…

Spring Boot 篇四: Spring Data JPA使用SQL Server

本文目录SQL Server Express的安装或直接DockerSQL Server JDBC DriverTCP/IP 链接端口Integrated SecuritySSL总结本篇介绍篇一至篇三中用到的JPA链接SQL Server的具体情况以及实战过程中可能遇到的问题。 SQL Server Express的安装或直接Docker 具体的下载和安装教程&#…

sanic:通过dockerfile部署

简介&#xff1a;长期以来&#xff0c;环境一直是部署的难题。如果您的项目中有冲突的配置&#xff0c;您将不得不花费大量时间来解决它们。幸运的是&#xff0c;虚拟化为我们提供了一个很好的解决思路。Docker 就是其中之一。 历史攻略&#xff1a; sanic框架&#xff1a;简…