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

发布于:2024-12-06 ⋅ 阅读:(28) ⋅ 点赞:(0)

前言

想做一个桌面应用程序,一直没有找到简单快速可上手的框架。刚好有点前端的底子,就发现了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({
    // 禁用eslint
    lintOnSave: 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 = false

new 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 = {}
    //判断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

<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>

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

添加路由

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',
    //右下角角标 打开 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

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

结语

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

非常感谢您能看到最后!

希望可以帮到你!

最后,感谢开源!


网站公告

今日签到

点亮在社区的每一天
去签到