Electron + Vue 简单实现窗口程序(Windows)从零到一

前言

想做一个桌面应用程序,一直没有找到简单快速可上手的框架。刚好有点前端的底子,就发现了Electron。关于Electron的介绍,请移步 https://www.electronjs.org/ 查阅。

简单来说,引用官网的话,Electron是一个使用 JavaScript、HTML 和 CSS 构建桌面应用程序的框架,就是可以用前端代码开发跨平台的桌面应用程序(GUI);又因为有CSS的存在,所以可以引入很多的UI样式库或者手搓样式,让应用美观一些

我相信很多朋友也可能喜欢写点桌面应用程序或者称为小工具,所以本篇适合想用前端来实现功能的伙伴,因此需要至少有html、css、js、Vue2基础。

废话少说,先看下完成图。

很简单的功能,就当作一个Demo吧。

其中关键在于如何组织Vue路由、创建子窗口、在 Vue Component 中如何与主进程通信(渲染进程与主进程)、如何设置图标等内容,东西不多不少,刚好够起步。当把这些配置好了就可以专注于应用功能的开发了。

共19000左右字的Demo教程,准备好纸巾,让我们开始吧!

目录

前言

零、项目源码

二、技术栈(框架)

三、环境搭建

四、分析与开发

4.1 自定义菜单栏的实现

4.1.1 菜单显示

4.1.2 公共窗口创建

4.1.3 功能实现

4.2 窗口进阶,使某个窗口加载指定 Vue Component

4.2.1 安装并配置Vue路由

4.2.2 布局组件与子窗口

4.3 页面、功能窗口的实现

4.3.1 渲染进程与主进程通信

4.3.2 工具页面点击创建窗口实现

4.4 窗口图标与托盘显示

4.4.1 主窗口图标设置

4.4.2 子窗口设置图标

4.4.3 托盘设置

五、打包

5.1 打包Electron程序

5.2 使用inno setup 打包为安装程序

六、安装测试

七、项目源码

结语

 


零、项目源码

hyf/utils-hub-demo - 码云 - 开源中国icon-default.png?t=O83Ahttps://gitee.com/fan-hongyu/utils-hub-demo一、基本环境说明

  • npm版本:8.6.0
  • node版本:v18.0.0
  • Vue/cli版本:@vue/cli 5.0.8
  • IDE:WebStorm

二、技术栈(框架)

  • Vue 2
  • ElementUI
  • Electron 13

三、环境搭建

为避免篇幅冗长(最终还是冗长了),基础环境搭建请移步我的另一篇文章

【桌面应用程序】Vue-Electron 环境构建、打包与测试(Windows)-CSDN博客

环境搭建好了如图

注释掉自动安装 VUEJS_DEVTOOLS,否则可能导致启动缓慢。

src/background.js

启动示例

四、分析与开发

4.1 自定义菜单栏的实现

4.1.1 菜单显示

首先我们来实现菜单栏的显示。

在 src 下新建一个 menu.js 文件,用来实现菜单的逻辑。

src/menu.js

import {Menu} from "electron";const template = [{label: '帮助',submenu: [{label: '关于',accelerator: 'CmdOrCtrl+H'}]}]Menu.setApplicationMenu(Menu.buildFromTemplate(template))

代码解释:从 electron 将 Menu 导入进来;定义一个菜单栏模板,里面包含了一级菜单帮助,他有一个子选项 关于,快捷键为 Mac(Command)/ Windows(Ctrl) + H 。通过 Menu.setApplicationMenu传入一个Menu.buildFromTemplate(模板),即可实现自定义菜单栏实现。

现在菜单就定义完了,如何应用到窗口上呢?

backgroud.js 中引入即可。

src/backgroud.js

//引入自定义菜单
require('./menu')

启动测试,可以看到已经实现了。需要什么,就在 模板数组 中添加即可。

4.1.2 公共窗口创建

为了使点击 “关于” 选项,可以弹出新窗口。那么我们需要一个创建窗口的方法。

思考:应用可能会随着开发功能越来越多,那么每次创建窗口都要写一个单独的方法吗?这样会导致代码冗余,难以维护。

所以要创建一个公共的创建窗口的方法。

在 src 下新建一个 windowManager.js 文件,并填写以下代码。

src/windowManager.js

/** 窗口管理器 */
import {BrowserWindow} from "electron";const path = require('path')/**** @param param 窗口参数对象*/
const winURL = process.env.NODE_ENV === 'development'? 'http://localhost:8080': `file://${__dirname}/index.html`let win;
function commonCreateWindow(param) {let win = new BrowserWindow({width: param.width || 400,height: param.height || 200,autoHideMenuBar: param.isAutoHideMenuBar || false,title: param.title || 'utils-hub',show: param.show || false,minWidth: param.minWidth || 400,minHeight: param.minHeight || 200,//如果没有传icon,那么就使用默认的图标,在 public/下icon: path.join(__dirname, param.iconName || 'app.ico'),minimizable: param.minimizable,maximizable: param.maximizable,resizable: param.resizable,closable: true,webPreferences: {preload: path.resolve(__dirname, './preload.js'),nodeIntegration: false,contextIsolation: true,enableRemoteModule: false, // 禁用 remote 模块以提高安全性}})// win.webContents.on('did-finish-load', () => {//     win.setTitle(param.title); // 设置窗口标题//     win.show();// })win.loadURL(`${winURL}` + "/#/sub-win/" + param.url);// console.log(win)win.once('ready-to-show', () => {if (param.isMax) {win.maximize();}win.setTitle(param.title)win.show();      // 显示窗口});// 打开开发者工具(仅在开发环境中启用)if (process.env.NODE_ENV === 'development') {win.webContents.openDevTools();}win.on('closed', () => {console.log('执行')win = null;});
}export default commonCreateWindow;

让我们来解读以下以上代码:

  • 定义了一个winUrl,判断当前是否为开发环境。如果是,那么窗口就加载 http://localhost:8080(当启动electron时,开发环境会启动本地服务器,从而使页面加载);如果不是,那么通过file协议访问打包后的通过node构建路径(__dirname)来找到你的index.html,并让窗口加载此文件。由于我们的Vue实例是挂载到index.html中,所以可以显示Vue组件内容。(构建的文件将会被自动注入)

  • 定义了一个方法,接受窗口参数并创建窗口。调用 electron 的 new BrowserWindow 来实现一个窗口的创建。
  • 窗口参数解释
属性释义
width窗口宽度
height

窗口高度

autoHideMenuBar自动隐藏菜单栏
title窗口标题
show是否显示窗口
minWidth窗口最小宽度
minHeight窗口最小高度
icon窗口图标,没有则使用 public/favicon.icon
minimizable窗口是否可以被最小化
maximizable窗口是否可以被最大化
webPreferences.preload预加载脚本,实现渲染进程与主进程通信
webPreferences.nodeIntegration禁用 Node.js 集成以提高安全性
webPreferences.contextIsolation启用上下文隔离以进一步增强安全性
webPreferences.enableRemoteModule禁用 remote 模块以提高安全性
  • win.once('ready-to-show',()=>{}),在该方法中调用show方法,用于避免electron启动后白屏或闪屏现象。
  •  param.isMax,自定义的属性。窗口启动后是否立即最大化显示。
  •  win.webContents.openDevTools(); 该上下行代码表示,是否启动开发者工具,建议开发环境开启此选项,用于排查错误。
  • win.loadURL(`${winURL}` + "/#" + param.url); ,用于加载窗口的路径,此处使用Vue 路由,并且 mode 为 hash。参考 electron实现打开子窗口,窗口加载vue路由指定的组件页面_vue electron单独打开子窗口-CSDN博客
  • win.on('closed', () =>{}),监听窗口关闭事件,将win置为null。
  • || xxx 为默认值。

 通过解读以上代码,大家应该了解了窗口的创建过程。下面我们创建预加载脚本

在 src 下新建 preload.js

src/preload.js

console.log('预加载脚本执行')

打印测试。

当然现在还不能启动,因为启动了也会报找不到 preload.js。

配置 vue.config.js

vue.config.js

const {defineConfig} = require('@vue/cli-service')
module.exports = defineConfig({// 禁用eslintlintOnSave: false,transpileDependencies: true,//添加预加载脚本pluginOptions:{electronBuilder:{preload: {preload: 'src/preload.js'  // 确保路径正确},}}
})

这样,就定义好了。然后执行

npm run build

 好了,现在预加载脚本就配置好了。

4.1.3 功能实现

现在创建窗口的方法有了,预加载脚本也有了,是不是能实现点击 “关于” 选项,弹出窗口了呢?

测试一下,让我们回到 menu.js ,并在 关于 中添加 click 。

src/menu.js

import {Menu} from "electron";//导入公共创建窗口的方法
import commonCreateWindow from "@/windowManager";const template = [{label: '帮助',submenu: [{label: '关于',accelerator: 'CmdOrCtrl+H',//添加单击方法click: () => {//构建 关于 窗口参数对象const aboutWindowObject = {//宽高使用默认值//自动隐藏菜单栏isAutoHideMenuBar: true,title: '关于',//默认隐藏//最大宽度和最大高度使用默认值//TODO 对于ICON,最后处理//禁止最大化最小化minimizable: false,maximizable: false,//禁止窗口调整大小resizable:false,//禁止启动后最大化isMax: false,url: 'about-win'}//调用公共创建窗口的方法commonCreateWindow(aboutWindowObject)}}]}]Menu.setApplicationMenu(Menu.buildFromTemplate(template))

启动程序测试。

启动后,发现 dist_electron目录中多了preload.js,这说明已经预加载脚本配置好了。

点击 帮助 -> 关于,或者使用快捷键 Ctrl+h。

此时可以看到,窗口已经成功弹出了,并且预加载脚本也执行了。

4.2 窗口进阶,使某个窗口加载指定 Vue Component

当我们打开了 “关于” 窗口后,发现了一个问题,这个窗口所显示的内容并不是我们想要的。

我需要让这个窗口加载我的Vue页面(About.vue)。

先看下我们的 windowManger.js 中的commonCreateWindow方法中的一段代码。

win.loadURL(`${winURL}` + "/#" + param.url);

我们在上面创建 “关于” 窗口时,aboutWindowObject 对象中没有 url属性。所以我们的窗口访问路径就是 http://localhost:8080/#(开发环境)

所以打开浏览器访问这个地址,显示出的页面和关于页面一致,这是没问题的。

好了,现在我们要加载自己的内容。

4.2.1 安装并配置Vue路由

//安装适用于Vue2的Vue-Router
npm install vue-router@3 --save

在 src 下,新建 router 目录,并且新建 index.js

路由文件创建好了,先不管。

4.2.2 布局组件与子窗口

我们要使用electron创建桌面应用,不仅有窗口,还有页面。为了将窗口与页面文件区分开,我们在 src 下新建 views 目录,并在里面新建 pages 和 windows。

  • src/views
    • pages
    • windows

那就可以直接写功能页面/窗口了吗?不,我们还需要一个布局组件。

在 src 下,我们可以发现有一个 components 目录,就它了。

在 src/components 下,新建 BaseLayout.vue 与 SubWindowLayout.vue。一个用于页面的布局,一个用于窗口的布局。

全部新建为 Options API 组件(只会Vue2)

里面放点啥呢?

简单来说,如果要是 SubWindowLayout.vue 中添加了一个按钮,那么如果你的嵌套路由中以此组件为布局组件后,所有依赖此路由的子路由的窗口都会有这个按钮。

添加 <router-view></router-view> 标签

  <div><router-view></router-view></div>

如需了解更多Vue-Router信息,请移步 Vue Router | The official Router for Vue.js 

接下来,我们创建 AboutWin.vue(为了与页面文件区分,所有窗口采用 xxxWin.vue 的命名方式)

在 src/views/windows/ 新建 AboutWin.vue。如果你喜欢更加清晰的层次,可以再新建一层about目录。

src/views/windowsAboutWin.vue

<script>
export default {name: "AboutWin"
}
</script><template><div class="desc"><p>这是使用Electron开发的一款集成了多个工具的软件。</p><p>版本:v1.0</p></div>
</template><style scoped>
.desc {margin-top: 40px;font-size: 12px;
}
</style>

现在布局组件有了,关于 窗口也有了,那就配置下路由吧。

src/router/index.js

import Vue from 'vue';
import Router from 'vue-router';
/** 引入窗口布局组件 */
import SubWinLayout from "@/components/SubWindowLayout.vue";/** 引入窗口  */
import AboutWin from "@/views/windows/AboutWin.vue";Vue.use(Router)const routes = [{path: '/sub-win',component: SubWinLayout,children: [{path: 'about-win', component: AboutWin}]}
]const router = new Router({mode: 'hash',routes
});
export default router;

简单解释一下:引入布局组件和关于窗口,定义路由规则。其中,mode必须为hash,使用history模式的话会找不到路径。参考

electron实现打开子窗口,窗口加载vue路由指定的组件页面_vue electron单独打开子窗口-CSDN博客

 在 src/main.js 中引入路由。

src/main.js

import Vue from 'vue'
import App from './App.vue'
import ElementUI from 'element-ui'
import 'element-ui/lib/theme-chalk/index.css'
import router from './router/index.js';
Vue.use(ElementUI)
Vue.config.productionTip = falsenew Vue({router,render: h => h(App),
}).$mount('#app')

现在我们要稍微改动一下 src/menu.js 中的一段代码

src/menu.js

// 旧的
win.loadURL(`${winURL}` + "/#" + param.url);
// 新的
win.loadURL(`${winURL}` + "/#/sub-win/" + param.url);

由于我所有的子窗口路由都基于 sub-win 这个父路由,所以可以把这写死。如果有多个父路由,请保持旧有的即可,然后在传参时,传父+子(或者使用逻辑判断)。

现在我们修改 src/menu.js 中的菜单栏 关于 选项的click方法,添加 url 参数。

src/menu.js

url: 'about-win'

修改 src/App.vue,删除其他内容,添加 <router-view></router-view>

src/App.vue

<template><div id="app"><router-view></router-view></div>
</template><script>export default {name: 'App',
}
</script><style>
</style>

启动测试。

4.3 页面、功能窗口的实现

好了,帮助 -> 关于 窗口弹出并显示指定内容已经完成了。

接下来实现一下页面上的功能。目前程序启动之后,页面是空白的,因为App.vue中仅有一个<router-view></router-view>,路由中没有指定根路由,所以什么都没有。

创建根页面

在 src/views/ 下,创建index.vue,用于首页。

src/views/index.vue

<script>
import Dashboard from "@/views/pages/Dashboard.vue";
import CommonUtils from "@/views/pages/CommonUtils.vue";export default {name: "index",components: {CommonUtils, Dashboard},data() {return {activeIndex: 'dashboard',currentDate:''}},methods: {handleSelect(val) {console.log(val)this.activeIndex = val;},getCurrentDate(){const now = new Date();// 使用 toLocaleString 格式化日期和时间this.currentDate = `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, '0')}-${String(now.getDate()).padStart(2, '0')} ${String(now.getHours()).padStart(2, '0')}:${String(now.getMinutes()).padStart(2, '0')}:${String(now.getSeconds()).padStart(2, '0')}`;}},created() {this.getCurrentDate();setInterval(() => {this.getCurrentDate();}, 1000);}
}
</script><template><div><div><h3><i class="el-icon-time">&nbsp;&nbsp;{{currentDate}}</i></h3></div><nav><!--菜单拦内容--><el-menu :default-active="activeIndex" class="el-menu-demo" mode="horizontal" @select="handleSelect"><el-menu-item index="dashboard"> <i class="el-icon-odometer"></i>控制板</el-menu-item><el-menu-item index="common-utils"> <i class="el-icon-suitcase"></i>常用工具</el-menu-item></el-menu></nav><!--  动态显示Component--><br><div><Dashboard v-show="activeIndex==='dashboard'"></Dashboard><CommonUtils v-show="activeIndex==='common-utils'"></CommonUtils></div></div>
</template><style scoped></style>

 简单解释一下:引入了两个自定义的组件,Dashboard.vue 和 CommonUtils.vue ,一个用于显示Dashboard,一个用于显示常用工具。通过ElementUI的  <el-menu></el-menu> 实现切换。

还有一个动态时间,实现每秒刷新。

以下是两个组件的内容

src/views/pages/Dashboard.vue

<script>
export default {name: "Dashboard",data(){return{}}
}
</script><template><div><p>Welcome to Utils-Hub.</p></div>
</template><style></style>

Dashboard.vue 页面很简单,就一个p标签。 

src/views/pages/CommonUtils.vue

<template><div><div v-for="(val) in utilsList"><el-card class="box-card" @click.native="createUtilsWin(val.flag)"><div slot="header" class="clearfix"><h2><i :class="val.icon"></i> {{ val.title }}</h2></div><div class="descCls">{{ val.desc }}</div></el-card></div></div>
</template><script>
export default {name: "CommonUtils",methods: {createUtilsWin(flag) {if(flag === "other"){this.$message.warning('暂无更多需求!')return false;}//创建窗口window.api.createUtilsWindow(flag);}},data() {return {utilsList: [{id: '1',flag: 'transfer',title: '大小写转换',desc: '键入字符,将其转换为大写字符或小写字符',icon: 'el-icon-refresh'},// {id: '2', flag: 'regexp', title: '正则表达式', desc: '使用简单的表达式完成复杂的需求', icon: 'el-icon-cpu'},// {id: '3', flag: 'cronExp', title: 'Cron表达式', desc: '? * * * * *', icon: 'el-icon-cpu'},// {//   id: '4',//   flag: 'TableNameExtract',//   title: '表名提取',//   desc: '仅适用于MySQL。选择标准的.sql格式文件,从中提取表名',//   icon: 'el-icon-top'// },// {//   id: '5',//   flag: 'uuidGenerator',//   title: 'UUID生成器',//   desc: '生成标准的uuid。',//   icon: 'el-icon-s-opportunity'// },{id: '6', flag: 'other', title: 'Other', desc: '需求加载中...', icon: 'el-icon-loading'},]}}
}
</script><style scoped>
.box-card {width: 250px;height: 200px;float: left;margin-left: 43px;margin-top: 40px;
}.box-card:hover {border: 1px solid #409EFF;cursor: pointer;
}.descCls {font-size: 12px;
}
</style>

CommonUtils.vue 页面也很简单,让我们来解读一下:

定义了一个 utilsList 的工具列表数组,里面写的是一些常用的工具对象。使用v-for将其遍历到el-card 上,从而实现一个工具对象对应一个卡片。当点击一个卡片时,弹出对应工具卡片功能的窗口。

其中,需要注意的是,每个工具对象中,有一个 flag属性,用于窗口标识。

整段代码中,最关键的部分就是

window.api.createUtilsWindow(flag);

为便于理解,请看以下图示。

4.3.1 渲染进程与主进程通信

简单来说,就是Vue组件中定义一个函数,用于处理页面的事件;然后此函数再调用由preload.js中暴露出来的方法,通过window对象调用;preload.js中通过ipcRederer与主进程ipcMain通信。

对于ipcRenderer 有两种方式通信方法,对应ipcMain两种接收的方法

  • ipcRenderer.send 发送给主进程消息,不关心处理结果,类似void。主进程通过ipcMain.on来接收,随后处理。
  • ipcRenderer.invoke 发送给主进程调用方法,需要返回值。主进程通过ipcMain.handle来处理,随后返回返回值。

4.3.2 工具页面点击创建窗口实现

现在回过头来看看我们的页面怎么样了。

什么都没有。是的,如果有东西就奇怪了。

添加路由

src/router/index.js

import Vue from 'vue';
import Router from 'vue-router';
/** 引入窗口布局组件 */
import SubWinLayout from "@/components/SubWindowLayout.vue";
import BaseLayout from "@/components/BaseLayout.vue";/** 引入窗口  */
import AboutWin from "@/views/windows/AboutWin.vue";/** 引入页面 */
import index from '@/views/index.vue'
import Dashboard from "@/views/pages/Dashboard.vue";
import CommonUtils from "@/views/pages/CommonUtils.vue";Vue.use(Router)const routes = [{path: "/",component: BaseLayout,children: [{path: '', component: index},{path: 'dashboard', component: Dashboard},{path: 'common-utils', component: CommonUtils},]},{path: '/sub-win',component: SubWinLayout,children: [{path: 'about-win', component: AboutWin}]}
]const router = new Router({mode: 'hash',routes
});
export default router;

启动测试。

直接点击 大小写转换 卡片将提示 createUtilsWindow 是未定义的。

现在打开我们的preload.js,并加入以下内容

src/preload.js

const {contextBridge, ipcRenderer} = require('electron')
contextBridge.exposeInMainWorld('api',{createUtilsWindow:(flag)=>{ipcRenderer.send('create-utils-window',flag)}
})

由于我们只需要创建一个窗口,并不需要返回值,所以使用ipcRenderer.send方法。

现在的情况是,渲染进程(Vue Component)中定义好创建窗口的方法了,预加载脚本(preload.js)也定义好了,那么现在就差主进程了。

项目中肯定不只有一个渲染进程与主进程的通信方法,所以我们把监听逻辑放在一个单独的文件中。

src 下新建 listener.js 。

src/listener.js

/** 主进程监听与处理 */
import {ipcMain} from 'electron'ipcMain.on('create-utils-window',(e,flag)=>{console.log('主进程方法被调用了')
})

顺便提一句,如果觉得会有很多 ipcRenderer.send 和 ipcMain.on,两个文件来回跑怕打错的话,可以将其定义为常量。

伪代码

  • const CREATE_UTILS_WINDOW = 'create-utils-window';
    • ipcRenderer.send(CREATE_UTILS_WINDOW);
    • ipcMain.on(CREATE_UTILS_WINDOW);

继续,在background.js 中引入 listener.js,并且设置preload.js

//引入监听
require('./listener')
//预加载脚本
preload:path.resolve(__dirname,'./preload.js'),

启动测试。

可以看到,成功打印了。主进程在终端打印,渲染进程在控制台打印。

让我们修改一下listener.js监听方法的内容,实现窗口的创建。

src/listener.js

/** 主进程监听与处理 */
import {ipcMain} from 'electron'import commonCreateWindow from '@/windowManager'ipcMain.on('create-utils-window', (e, flag) => {// console.log('主进程方法被调用了')//创建窗口let windowObject = {}//判断flagswitch (flag) {case "transfer":windowObject.width = 800;windowObject.height = 600;windowObject.isAutoHideMenuBar = true;windowObject.title = '大小写转换';windowObject.minWidth = 800;windowObject.minHeight = 600;//TODO ICON稍后处理windowObject.isMax = true;windowObject.url = 'transfer-win';break;default:break;}commonCreateWindow(windowObject)
})

定义好了,现在缺的是路由和窗口页面。

新建

src/views/windows/commonUtils/TransferWin.vue

<script>export default {methods: {/** 复制内容到剪切板 */async copyToClipboard(text) {try {await navigator.clipboard.writeText(text);this.$message.success('内容已复制!')} catch (err) {this.$message.error('复制失败!')}},/** 转大写 */async handleTransferToUpper() {this.destText = this.srcText.toUpperCase();await this.copyToClipboard(this.destText)},/** 转小写 */async handleTransferToLower() {this.destText = this.srcText.toLowerCase();await this.copyToClipboard(this.destText)},/** 清空 */handleClear() {this.destText = '';this.srcText = '';},},name: "TransferWin",data() {return {srcText: '',destText: ''}}
}
</script>
<template><div><el-card class="box-card"><div slot="header" class="clearfix"><h2>大小写转换</h2></div>输入小写或大写字符,将其转换为大写或小写。</el-card><el-inputclass="common"type="textarea":rows="6"placeholder="请输入需要转换的内容"v-model="srcText"></el-input><div class="common"><el-button type="primary" icon="el-icon-refresh-right" @click="handleTransferToUpper">转大写</el-button><el-button type="primary" icon="el-icon-refresh-left" @click="handleTransferToLower">转小写</el-button><el-button type="danger" icon="el-icon-delete-solid" @click="handleClear">清空</el-button></div><el-inputclass="common"type="textarea":rows="6"readonlyplaceholder="Console"v-model="destText"></el-input></div>
</template><style scoped>
.common {margin-top: 20px;
}
</style>

功能实现很简单,就是输入字符,将其转为大写或小写,并自动复制到剪切板,可清空。

添加路由

import TransferWin from "@/views/windows/commonUtils/TransferWin.vue";{path: 'transfer-win', component: TransferWin},

 启动测试。

到这功能基本就完事了,剩下还有一些小细节需要处理一下。

4.4 窗口图标与托盘显示

现在我们每个窗口的图标都是一样的,原因在于

没传图标参数。

4.4.1 主窗口图标设置

主窗口的图标设置会关联到应用的首页图标和任务栏图标。

为了使图标管理起来不那么混乱,我们需要一个文件来存储icon的名称。

在 src 下新建 iconManager.js

src/iconManager.js

/** 图标管理器 */
export const ICON_PATHS = {//应用程序的图标,首页左上角角标、任务栏角标、托盘角标APP_ICON: 'winIcon/app.ico',//关于窗口的图标ABOUT_WIN_ICON:'winIcon/about.ico',//大小写转换窗口左上角角标TRANSFER_WIN_ICON: 'winIcon/transfer.ico',//右下角角标 打开 iconOPEN_ICON:'winIcon/open.ico',//右下角角标 退出 iconEXIT_ICON:'winIcon/exit.ico',
}

此处需要注意,当是开发环境的时候,图标从 dist_electron 目录读取;当打包后,图标是从 public 目录获取。(因为__dirname)

所以需要在 public 和 dist_electron 目录中新建 winIcon 目录

将对应的图标放到这两个文件夹中。

打开 src/background.js,设置应用图标。

  src/background.js

import * as iconManger from '@/iconManager'icon:path.resolve(__dirname,iconManger.ICON_PATHS.APP_ICON)

启动测试。

 

4.4.2 子窗口设置图标

子窗口图标只涉及左上角角标

打开

src/listener.js

import * as iconManger from '@/iconManager'//TODO ICON稍后处理
windowObject.iconName = iconManger.ICON_PATHS.TRANSFER_WIN_ICON;

 启动测试。

4.4.3 托盘设置

悬浮提示文字

右键菜单

新建文件

src/tray.js

// tray.js
const { Tray, Menu } = require('electron');
const path = require('path');
import * as iconSupport from '@/iconManager'let appTray = null;
let win = null; // 你需要从外部传递窗口实例function createTray(app,mainWindow,nativeImage) {win = mainWindow;// 打开图标缩小设置const openResizedIcon = nativeImage.createFromPath(path.join(__dirname, iconSupport.ICON_PATHS.OPEN_ICON)).resize({width: 16,height: 16});// 退出图标缩小设置const exitResizedIcon = nativeImage.createFromPath(path.join(__dirname, iconSupport.ICON_PATHS.EXIT_ICON)).resize({width: 16,height: 16});// 系统托盘右键菜单let trayMenuTemplate = [{label: '打开',icon: openResizedIcon,click: function () {win.show();win.maximize();}},{label: '退出',icon: exitResizedIcon,click: function () {app.quit();app.quit();}}];appTray = new Tray(path.join(__dirname, iconSupport.ICON_PATHS.APP_ICON));const contextMenu = Menu.buildFromTemplate(trayMenuTemplate);appTray.setToolTip('utils-hub');appTray.setContextMenu(contextMenu);// 点击托盘图标时显示窗口appTray.on('click', function () {win.show();win.maximize();});
}export default createTray;

解读一下:

首先将两个图标进行缩放,因为太大了不好看;然后定义了一个托盘菜单模板,有 打开 和 退出功能;最后设置托盘图标,将菜单应用上。当单击菜单时,显示程序。创建托盘的方法需要三个参数,应用实例,窗口以及一个用于处理图片的对象。

我们在 

src/background.js中调用一下这个方法。

src/background.js

import { app, protocol, BrowserWindow,nativeImage } from 'electron'import createTray from '@/tray'createTray(app,win,nativeImage);

启动测试。

托盘就设置好了,但是现在有一个问题就是当我们点击主窗口的关闭时,程序就退出了,一般的应用可能会询问用户是退出还是驻托盘。

咱们这里不搞那么复杂了,就默认关闭按钮是驻托盘,想要退出的话使用托盘右键退出。

修改 

src/background.js 的逻辑

首先将 win 从 const 改为 let

 

src/background.js

// 部分代码
let win = new BrowserWindow({})win.on('close', (e) => {if (win.isMinimized()) {win = null;} else {e.preventDefault();win.minimize();win.hide();}
})

解读一下:监听关闭事件,如果窗口当前是最小化的,那么就置为null;如果不是,那么就最小化并隐藏(不显示窗口)

启动测试。

可以看到窗口虽然已经关闭了,但是托盘区还是有程序存在的。单击或右键打开,都可以使窗口重新显示。

到现在,开发工作完成了。

五、打包

5.1 打包Electron程序

首先进行打包配置

vue.config.js

builderOptions: {appId: 'com.utils.hub',productName: 'UtilsHubDemo',directories: {output: 'build'},win: {// 应用图标,这里要确保图标文件存在且路径正确,一般为.ico 格式icon: 'public/winIcon/app.ico',// 目标架构,可以是 x64、ia32 等,根据实际需求选择target: [{target: 'nsis', // 使用 NSIS 打包arch: ['x64']   // 指定架构}]},
}

执行吗?

我先执行试试(此处不贴图,打包n次中)

一番操作,发现两个问题。

  • 关于窗口没有图标,忘设置了。

import * as iconManager from '@/iconManager'iconName:iconManager.ICON_PATHS.APP_ICON,
  • ElementUI 图标没有了

参考 [已解决]electron-builder vue 打包后iconfont/element-ui字体图标不显示问题_vue打包后element图标没有了-CSDN博客

在项目 public 目录下新建 element-ui 目录

然后打开项目的node_modules目录,搜索element-ui,将其中的theme-chalk目录拷贝到 public/element-ui 下

修改

public/element-ui/theme-chalk/index.css

搜索fonts,在前面都添加  ./

修改前

 修改后

修改 

public/index.html

加入以下内容

      <!-- 添加此行 --><link rel="stylesheet" href="<%= BASE_URL %>element-ui/theme-chalk/index.css">

至此,问题应该都会解决了,重新打包测试。

完成!

5.2 使用inno setup 打包为安装程序

当electron打包完成后,目录下会自动生成一个 Setup.exe,双击即可安装。

但是没法选择目录什么的。(应该是需要配置nsis脚本,奈何不会呀!)

所以我们看见上面还有一个文件夹,顾名思义应该是未打包的(直译)。那么我的理解就是,把这个文件夹拿到哪,里面的程序都能正常运作(Windows平台)。

所以下载 Inno Setup

可以使用图形化的方式进行打包过程。

 下载

默认没有中文(不是软件的中文,是用于你的应用安装时的中文)。

需要下载中文包

选择简体中文下载即可。

保存下载语言包之后(注意后缀名不要是txt),打开inno setup的安装目录,放到language目录下即可。

 开始打包

这里选择前边说的win-unpacked/下面的UtilsHubDemo.exe

此处需要注意,因为仅有这一个exe是跑不起来,它还需要win-unpacked目录中的文件支持。所以我们在其他地方新建一个文件夹 test。

然后将win-unpacked中除 UtilsHubDemo.exe 之外的文件拷贝到 test 文件夹中。

继续打开inno setup操作

选择刚刚的 test 文件夹

该应用没有相关的后缀文件

建议勾选最后一个选项,勾选之后,用户安装的时候会询问

仅为我安装还是为所有用户安装(为所有用户安装需要管理员权限)。

默认即可

我这里选择仅支持简体中文。

输出目录、输出文件名、图标

保存脚本文件

六、安装测试

打开两个虚拟机,Win10和Win7测试效果。

Win10

Win7

七、项目源码

hyf/utils-hub-demo - 码云 - 开源中国icon-default.png?t=O83Ahttps://gitee.com/fan-hongyu/utils-hub-demo

结语

至此,就从开发到打包到安装全都实现啦!由于笔者水平有限,其中涉及性能、安全、高级语法的问题可能未进行过多说明,望包涵!:)

非常感谢您能看到最后!

希望可以帮到你!

最后,感谢开源!

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

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

相关文章

健康养生生活

在快节奏的现代生活中&#xff0c;健康养生愈发成为人们关注的焦点。它不仅是一种生活方式&#xff0c;更是对生命质量的珍视与呵护。 健康养生&#xff0c;饮食为先。合理的膳食结构是维持身体健康的基石。我们应确保每餐营养均衡&#xff0c;增加蔬菜、水果、全谷物以及优质蛋…

如何避免数据丢失:服务器恢复与预防策略

在当今数字时代&#xff0c;数据对于个人和企业来说都至关重要。数据丢失可能会导致严重的财务损失、业务中断甚至法律责任。因此&#xff0c;采取措施防止数据丢失至关重要。本文将讨论服务器数据丢失的常见原因以及如何防止数据丢失的有效策略。 服务器数据丢失的常见原因 服…

网站访问统计A/B测试与数据分析

在网站运营中&#xff0c;访问统计和数据分析是优化用户体验和提高转化率的关键工具。A/B测试作为一种数据驱动的方法&#xff0c;能够帮助网站运营者验证设计和内容的有效性。A/B测试的基本原理是同时展示两个不同的版本&#xff08;A和B&#xff09;&#xff0c;通过比较它们…

芯驰X9SP与汽车麦克风-打造无缝驾驶体验

当今汽车技术的进步不仅提升了驾驶体验&#xff0c;还改变了我们与车辆互动的方式。汽车麦克风作为车内语音控制系统的重要组成部分&#xff0c;正逐渐成为现代汽车的标配。 技术原理 汽车麦克风主要依赖于声音传感技术&#xff0c;通常包括电容式麦克风和动圈式麦克风。这些…

界面控件Syncfusion Essential Studio®现在已完全支持 .NET 9

Syncfusion Essential Studio现在完全支持 .NET 9&#xff0c;可最新版本2024 Volume 3 版本中使用&#xff01;通过此更新&#xff0c;Blazor、.NET MAUI、WPF、WinForms、WinUI和ASP.NET Core 平台中的 Syncfusion 组件以及文档处理库已准备好让您利用 .NET 9 中的最新功能。…

剑指offer(专项突破)---字符串

总目录&#xff1a;剑指offer&#xff08;专项突破&#xff09;---目录-CSDN博客 1.字符串的基本知识 C语言中&#xff1a; 函数名功能描述strcpy(s1, s2)将字符串s2复制到字符串s1中&#xff0c;包括结束符\0&#xff0c;要求s1有足够空间容纳s2的内容。strncpy(s1, s2, n)…

yarn : 无法加载文件 E:\node\node_global\yarn.ps1,因为在此系统上禁止运行脚本

先确保安装了yarn —— npm install -g yarn 终端输入set-ExecutionPolicy RemoteSigned 若要在本地计算机上运行您编写的未签名脚本和来自其他用户的签名脚本&#xff0c;请使用以下命令将计算机上的执行策略更改为RemoteSigned 再去使用yarn okk~

python学opencv|读取视频(二)制作gif

【1】引言 前述已经完成了图像和视频的读取学习&#xff0c;本次课学习制作gif格式动图。 【2】教程 实际上想制作gif格式动图是一个顺理成章的操作&#xff0c;完成了图像和视频的处理&#xff0c;那就自然而然会对gif的处理也产生兴趣。 不过在opencv官网、matplotlib官网…

高校心理教育辅导|基于springBoot的高校心理教育辅导系统设计与实现(附项目源码+论文+数据库)

私信或留言即免费送开题报告和任务书&#xff08;可指定任意题目&#xff09; 目录 一、摘要 二、相关技术 三、系统设计 四、数据库设计 五、核心代码 六、论文参考 七、源码获取 一、摘要 随着Internet技术的发展&#xff0c;心理教育辅导系统应运而生&…

网络安全、Web安全、渗透测试之笔经面经总结(一)

本篇文章总结涉及以下几个方面&#xff1a; 一&#xff1a;对称加密非对称加密&#xff1f; 对称加密&#xff1a;加解密用同一密钥&#xff0c;密钥维护复杂n&#xff08;n-1&#xff09;/2&#xff0c;不适合互联网传输密钥&#xff0c;加解密效率高。应用于加密数据。 非…

Nginx静态资源配置

基本配置原则 明确资源目录&#xff1a;为不同类型的静态资源指定不同的路径&#xff0c;这样可以避免路径冲突&#xff0c;并且便于管理。正确设置文件权限&#xff1a;确保 Nginx 具有读取静态资源的权限。缓存优化&#xff1a;为静态资源设置缓存头&#xff08;如 expires&…

华为NPU服务器昇腾Ascend 910B2部署通义千问Qwen2.5——基于mindie镜像一路试错版(三)

文章目录 前言纯模型推理启动服务后面干什么?这可咋整啊?愁死了!总结前言 这是咱这个系列的第三个文章了。 毕竟,这是我好几天摸索出的经验,能帮助各位在几个小时内领会,我觉得也算是我的功劳一件了。 所以,一是希望大家耐心看下去,耐心操作下去;而是恳请各位多多关…

Pixtral Large开源:Mistral AI的1240亿参数多模态模型超越GPT-4o等竞争对手

Pixtral Large是什么 Pixtral Large是由法国人工智能初创公司Mistral AI开发的超大多模态模型&#xff0c;拥有1240亿参数&#xff0c;2024年11月18日正式对外发布。它基于Mistral Large 2开发而成&#xff0c;具备1230亿参数的多模态解码器和10亿参数的视觉编码器。这个模型能…

阿里云整理(二)

阿里云整理 1. 访问网站2. 专业名词2.1 域名2.2 域名备案2.3 云解析DNS2.4 CDN2.5 WAF 1. 访问网站 用户使用浏览器访问网站大体分为几个过程&#xff1a; 用户在浏览器输入域名URL&#xff0c;例如www.baidu.com。 不过&#xff0c;浏览器并不知道为该域名提供服务的服务器具…

【联表查询】.NET开源 ORM 框架 SqlSugar 系列

.NET开源 ORM 框架 SqlSugar 系列 【开篇】.NET开源 ORM 框架 SqlSugar 系列【入门必看】.NET开源 ORM 框架 SqlSugar 系列【实体配置】.NET开源 ORM 框架 SqlSugar 系列【Db First】.NET开源 ORM 框架 SqlSugar 系列【Code First】.NET开源 ORM 框架 SqlSugar 系列【数据事务…

标贝科技受邀出席2024东湖国际人工智能高峰论坛并入选数据要素合作伙伴名单

近日&#xff0c;备受瞩目的2024东湖国际人工智能高峰论坛在中国光谷科技会展中心隆重召开。会议以“智联世界&#xff0c;共创未来”为主题&#xff0c;省市相关单位、专家学者、产学研各界百余家联合体单位齐聚一堂&#xff0c;共话人工智能领域的最新技术及产业发展趋势。会…

使用AI工具Screenshot to Code将UI设计图翻译成代码

一、获取openAI apikey。 一般有两种方式&#xff0c;一种是到openAI官网注册账号&#xff0c;付费申请GPT4的apikey。另一种是某宝买代理。我这里采用第二种。 二、安装Screenshot to Code 1.到github下载源码。 2.启动&#xff0c;两种方式&#xff1a;源码启动和docker启动…

VMware Workstation Pro安装教程 (全图文保姆级)

一、前言 系统&#xff1a;Windows 11时间&#xff1a;2024/12/04需求&#xff1a;注册&#xff1a;broadcom&#xff08;邮箱&#xff09;难点&#xff1a;在官网找到下载链接 二、说明 建议前往官网&#xff08;https://www.vmware.com&#xff09;下载&#xff0c;可能加…

前端开发 之 15个页面加载特效下【附完整源码】

文章目录 十二&#xff1a;铜钱3D圆环加载特效1.效果展示2.HTML完整代码 十三&#xff1a;扇形百分比加载特效1.效果展示2.HTML完整代码 十四&#xff1a;四色圆环显现加载特效1.效果展示2.HTML完整代码 十五&#xff1a;跷跷板加载特效1.效果展示2.HTML完整代码 十二&#xff…

protobuf实现Hbase数据压缩

目录 前置HBase数据压缩效果获取数据(反序列化) 前置 安装说明 使用说明 HBaseDDL和DML操作 HBase数据压缩 问题 在上文的datain中原文 每次写入数据会写入4个单元格的内容&#xff0c;现在希望能对其进行筛减&#xff0c;合并成1格&#xff0c;减少存储空间&#xff08;序列…