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 自定义菜单栏的实现)

[4.1.1 菜单显示](#4.1.1 菜单显示)

[4.1.2 公共窗口创建](#4.1.2 公共窗口创建)

[4.1.3 功能实现](#4.1.3 功能实现)

[4.2 窗口进阶,使某个窗口加载指定 Vue Component](#4.2 窗口进阶,使某个窗口加载指定 Vue Component)

[4.2.1 安装并配置Vue路由](#4.2.1 安装并配置Vue路由)

[4.2.2 布局组件与子窗口](#4.2.2 布局组件与子窗口)

[4.3 页面、功能窗口的实现](#4.3 页面、功能窗口的实现)

[4.3.1 渲染进程与主进程通信](#4.3.1 渲染进程与主进程通信)

[4.3.2 工具页面点击创建窗口实现](#4.3.2 工具页面点击创建窗口实现)

[4.4 窗口图标与托盘显示](#4.4 窗口图标与托盘显示)

[4.4.1 主窗口图标设置](#4.4.1 主窗口图标设置)

[4.4.2 子窗口设置图标](#4.4.2 子窗口设置图标)

[4.4.3 托盘设置](#4.4.3 托盘设置)

五、打包

[5.1 打包Electron程序](#5.1 打包Electron程序)

[5.2 使用inno setup 打包为安装程序](#5.2 使用inno setup 打包为安装程序)

六、安装测试

七、项目源码

结语


零、项目源码

hyf/utils-hub-demo - 码云 - 开源中国https://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

javascript 复制代码
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

javascript 复制代码
//引入自定义菜单
require('./menu')

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

4.1.2 公共窗口创建

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

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

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

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

src/windowManager.js

javascript 复制代码
/** 窗口管理器 */
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

javascript 复制代码
console.log('预加载脚本执行')

打印测试。

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

配置 vue.config.js

vue.config.js

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

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

javascript 复制代码
npm run build

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

4.1.3 功能实现

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

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

src/menu.js

javascript 复制代码
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方法中的一段代码。

javascript 复制代码
win.loadURL(`${winURL}` + "/#" + param.url);

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

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

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

4.2.1 安装并配置Vue路由

javascript 复制代码
//安装适用于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> 标签

javascript 复制代码
  <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

javascript 复制代码
<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

javascript 复制代码
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

javascript 复制代码
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 = false

new Vue({
  router,
  render: h => h(App),
}).$mount('#app')

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

src/menu.js

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

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

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

src/menu.js

javascript 复制代码
url: 'about-win'

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

src/App.vue

javascript 复制代码
<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

javascript 复制代码
<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

javascript 复制代码
<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

javascript 复制代码
<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属性,用于窗口标识。

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

javascript 复制代码
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

javascript 复制代码
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

javascript 复制代码
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

javascript 复制代码
/** 主进程监听与处理 */
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

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

启动测试。

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

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

src/listener.js

javascript 复制代码
/** 主进程监听与处理 */
import {ipcMain} from 'electron'

import commonCreateWindow from '@/windowManager'

ipcMain.on('create-utils-window', (e, flag) => {
    // console.log('主进程方法被调用了')
    //创建窗口
    let windowObject = {}
    //判断flag
    switch (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

javascript 复制代码
<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-input
        class="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-input
        class="common"
        type="textarea"
        :rows="6"
        readonly
        placeholder="Console"
        v-model="destText">
    </el-input>
  </div>
</template>

<style scoped>
.common {
  margin-top: 20px;
}
</style>

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

添加路由

javascript 复制代码
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

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

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

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

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

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

src/background.js

javascript 复制代码
import * as iconManger from '@/iconManager'


icon:path.resolve(__dirname,iconManger.ICON_PATHS.APP_ICON)

启动测试。

4.4.2 子窗口设置图标

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

打开

src/listener.js

javascript 复制代码
import * as iconManger from '@/iconManager'

//TODO ICON稍后处理
windowObject.iconName = iconManger.ICON_PATHS.TRANSFER_WIN_ICON;

启动测试。

4.4.3 托盘设置

悬浮提示文字

右键菜单

新建文件

src/tray.js

javascript 复制代码
// 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

javascript 复制代码
import { app, protocol, BrowserWindow,nativeImage } from 'electron'

import createTray from '@/tray'

createTray(app,win,nativeImage);

启动测试。

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

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

修改

src/background.js 的逻辑

首先将 win 从 const 改为 let

src/background.js

javascript 复制代码
// 部分代码
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

javascript 复制代码
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次中)

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

  • 关于窗口没有图标,忘设置了。
javascript 复制代码
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

加入以下内容

javascript 复制代码
      <!-- 添加此行 -->
    <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 - 码云 - 开源中国https://gitee.com/fan-hongyu/utils-hub-demo

结语

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

非常感谢您能看到最后!

希望可以帮到你!

最后,感谢开源!

相关推荐
NoneCoder32 分钟前
JavaScript系列(38)-- WebRTC技术详解
开发语言·javascript·webrtc
python算法(魔法师版)40 分钟前
html,css,js的粒子效果
javascript·css·html
小彭努力中1 小时前
16.在Vue3中使用Echarts实现词云图
前端·javascript·vue.js·echarts
flying robot1 小时前
React的响应式
前端·javascript·react.js
来一碗刘肉面1 小时前
Vue - ref( ) 和 reactive( ) 响应式数据的使用
前端·javascript·vue.js
guhy fighting2 小时前
原生toFixed的bug
前端·javascript·bug
约定Da于配置7 小时前
uniapp封装websocket
前端·javascript·vue.js·websocket·网络协议·学习·uni-app
大叔_爱编程7 小时前
wx030基于springboot+vue+uniapp的养老院系统小程序
vue.js·spring boot·小程序·uni-app·毕业设计·源码·课程设计
计算机学姐9 小时前
基于微信小程序的驾校预约小程序
java·vue.js·spring boot·后端·spring·微信小程序·小程序
村口蹲点的阿三9 小时前
Spark SQL 中对 Map 类型的操作函数
javascript·数据库·hive·sql·spark