写点什么

基于 electron+vue+element 构建项目模板之【自定义标题栏 & 右键菜单项篇】

作者:Java-fenn
  • 2022 年 9 月 25 日
    湖南
  • 本文字数:8356 字

    阅读完需:约 27 分钟

lectron Vue1、概述


开发平台 OS:windows


开发平台 IDE:vs code


本篇章将介绍自定义标题栏和右键菜单项,基于 electron 现有版本安全性的建议,此次的改造中主进程和渲染进程彼此语境隔离,通过预加载(preload.js)和进程间通信(ipc)的方式来完成。


2、窗口最大化


一些应用在实际情况中,希望启动的时候就以窗口最大化的方式呈现, BrowserWindow 对象提供了窗口最大化的方法: win.maximize() ,具体如下所示:


const win = new BrowserWindow({//窗体宽度(像素),默认 800 像素 width: 800,//窗体高度(像素),默认 600 像素 height: 600,//窗口标题,如果在加载的 HTML 文件中定义了 HTML 标签 <title>,则该属性将被忽略。title: ${process.env.VUE_APP_NAME}(${process.env.VUE_APP_VERSION}),webPreferences: {// Use pluginOptions.nodeIntegration, leave this alone// See nklayman.github.io/vue-cli-plugin-electron-builder/guide/security.html#node-integration for more infonodeIntegration: process.env.ELECTRON_NODE_INTEGRATION,contextIsolation: !process.env.ELECTRON_NODE_INTEGRATION,},});//窗体最大化 win.maximize();


点击查看代码通过设置后,启动应用就会发现,最大化的过程中会出现黑底闪屏,这样会给用户造成困扰。


造成这个现象的原因是实例化窗体的时候,默认显示了窗口,然后再最大化,从默认窗口大小到最大化窗口大小的这个过程中窗体还没绘制好,就会出现黑色背景直至最大化完成后,现在稍加改造就可以解决这个问题: 实例化的时候不显示窗体,最大化后再显示窗体。


const win = new BrowserWindow({//窗体宽度(像素),默认 800 像素 width: 800,//窗体高度(像素),默认 600 像素 height: 600,//窗口标题,如果在加载的 HTML 文件中定义了 HTML 标签 <title>,则该属性将被忽略。title: ${process.env.VUE_APP_NAME}(${process.env.VUE_APP_VERSION}),//不显示窗体 show: false,webPreferences: {// Use pluginOptions.nodeIntegration, leave this alone// See nklayman.github.io/vue-cli-plugin-electron-builder/guide/security.html#node-integration for more infonodeIntegration: process.env.ELECTRON_NODE_INTEGRATION,contextIsolation: !process.env.ELECTRON_NODE_INTEGRATION,},});//窗体最大化 win.maximize();//显示窗体 win.show();


点击查看代码 3、自定义标题栏为什么要自定义标题栏?electron 应用自带的标题栏不能满足日益复杂的功能需求时,就只能自定义了。自定义标题除了实现基本的窗口功能外,它还能方便的快速的扩展其他功能需求。


自定义标题栏使用的是 css3-flex+scss 来实现布局和样式的编写,其主体划分为两个区域:标题栏区域和功能区域,如下图所示:


为了使用 scss 语言来编写样式,我们需要安装 sass-loader 插件,在终端输入命令: npm install sass-loader@^10 sass --save-dev 指定版本尤为重要,高版本对于 webpack 版本也有要求


3.1、iconfront 图标添加


功能区域处的功能按钮需要图标,此块是在 iconfront 官网上找了合适的图标加入购物车后以下载代码的方式下载资源,然后通过下载的 demo 中第二种方式集成在项目中。


3.2、编写标题栏页面


在 src/renderer/App.vue 修改其内容以完成标题栏的改造,主要是通过 css3-flex 来完成的布局,包含了标题栏原有的基本功能,改造后效果(gif 有失真效果)以及改造的代码如下所示:


<template><div id="app"><header><div class="titleArea"><img :src="winIcon" /><span>{{ winTitle }}</span></div><div class="featureArea"><div title="扩展"><span class="iconfont icon-xiakuozhanjiantou"></span></div><div title="最小化"><span class="iconfont icon-minimum"></span></div><div :title="maximizeTitle"><span:class="{iconfont: true,'icon-zuidahua': isMaximized,'icon-window-max_line': !isMaximized,}"></span></div><div title="关闭"><span class="iconfont icon-guanbi"></span></div></div></header><main>我是主体</main></div></template>


<script>export default {data: () => ({winIcon: ${process.env.BASE_URL}favicon.ico,winTitle: process.env.VUE_APP_NAME,isMaximized: true,}),


computed: {maximizeTitle() {return this.isMaximized ? "向下还原" : "最大化";},},};</script>


<style lang="scss">titleHeight: 40px;body {margin: 0px;}#app {font-family: "微软雅黑";color: #2c3e50;display: flex;flex-direction: column;header {background: #16407b;color: #8c8663;height: titleHeight;width: 100%;display: flex;


.titleArea {  flex-grow: 10;  padding-left: 5px;  display: flex;  align-items: center;  img {    width: 24px;    height: 24px;  }  span {    padding-left: 5px;  }}
.featureArea { flex-grow: 1; display: flex; justify-content: flex-end;
div { width: 30px; height: 30px; line-height: 30px; text-align: center; }
/* 最小化 最大化悬浮效果 */ div:hover { background: #6fa8ff; } /* 关闭悬浮效果 */ div:last-child:hover { background: red; }}
复制代码


}


// 主体区域铺满剩余的整个宽、高度 main {background: #e8eaed;width: 100%;height: calc(100vh - $titleHeight);}}</style>


点击查看代码


3.3、标题栏页面添加交互从 electron 机制上来说,BrowserWindow 是属于主进程模块,要想实现在页面中(渲染进程)调用主进程窗口的功能,这涉及到渲染进程与主进程的通信和安全性,在这通过预加载(preload.js)和 ipc 来实现该需求。


src/main 目录下添加 preload.js 文件,具体内容如下所示:


import { contextBridge, ipcRenderer } from "electron";


//窗体操作 apicontextBridge.exposeInMainWorld("windowApi", {//最小化 minimize: () => {ipcRenderer.send("window-min");},//向下还原|最大化 maximize: () => {ipcRenderer.send("window-max");},//关闭 close: () => {ipcRenderer.send("window-close");},/**


  • 窗口重置大小

  • @param {重置大小后的回调函数} callback*/resize: (callback) => {ipcRenderer.on("window-resize", callback);},});


点击查看代码 src/main/index.js 添加窗体最大化、最小化、关闭、重置大小监听、预先加载指定脚本等功能,具体内容如下所示:


"use strict";


import { app, protocol, BrowserWindow, ipcMain } from "electron";import { createProtocol } from "vue-cli-plugin-electron-builder/lib";import path from "path";// 取消安装 devtools 后,则不需要用到此对象,可以注释掉// import installExtension, { VUEJS_DEVTOOLS } from "electron-devtools-installer";const isDevelopment = process.env.NODE_ENV !== "production";


// Scheme must be registered before the app is readyprotocol.registerSchemesAsPrivileged([{ scheme: "app", privileges: { secure: true, standard: true } },]);


//创建应用主窗口 const createWindow = async () => {const win = new BrowserWindow({//窗体宽度(像素),默认 800 像素 width: 800,//窗体高度(像素),默认 600 像素 height: 600,//窗口标题,如果在加载的 HTML 文件中定义了 HTML 标签 <title>,则该属性将被忽略。title: ${process.env.VUE_APP_NAME}(${process.env.VUE_APP_VERSION}),//不显示窗体 show: false,webPreferences: {// Use pluginOptions.nodeIntegration, leave this alone// See nklayman.github.io/vue-cli-plugin-electron-builder/guide/security.html#node-integration for more info// 是否开启 node 集成,默认 falsenodeIntegration: false,// 否在独立 JavaScript 环境中运行 Electron API 和指定的 preload 脚本. 默认为 truecontextIsolation: true,//在页面运行其他脚本之前预先加载指定的脚本 preload: path.join(__dirname, "preload.js"),},//fasle:无框窗体(没有标题栏、菜单栏)frame: false,});//窗体最大化 win.maximize();//显示窗体 win.show();


if (process.env.WEBPACK_DEV_SERVER_URL) {// Load the url of the dev server if in development modeawait win.loadURL(process.env.WEBPACK_DEV_SERVER_URL);if (!process.env.IS_TEST) win.webContents.openDevTools();} else {createProtocol("app");// Load the index.html when not in developmentawait win.loadURL("app://./index.html");}


//监听窗口重置大小后事件,若触发则给渲染进程发送消息 win.on("resize", () => {win.webContents.send("window-resize", win.isMaximized());});};


// Quit when all windows are closed.app.on("window-all-closed", () => {// On macOS it is common for applications and their menu bar// to stay active until the user quits explicitly with Cmd + Qif (process.platform !== "darwin") {app.quit();}});


app.on("activate", () => {// On macOS it's common to re-create a window in the app when the// dock icon is clicked and there are no other windows open.if (BrowserWindow.getAllWindows().length === 0) createWindow();});


// 只有在 app 模组的 ready 事件能触发后才能创建 BrowserWindows 实例。 您可以借助 app.whenReady() API 来等待此事件// 通常我们使用触发器的 .on 函数来监听 Node.js 事件。// 但是 Electron 暴露了 app.whenReady() 方法,作为其 ready 事件的专用监听器,这样可以避免直接监听 .on 事件带来的一些问题。 参见 https://github.com/electron/electron/pull/21972。app.whenReady().then(() => {createWindow();


//窗口最小化 ipcMain.on("window-min", function (event) {const win = BrowserWindow.fromId(event.sender.id);win.minimize();});//窗口向下还原|最大化 ipcMain.on("window-max", function (event) {const win = BrowserWindow.fromId(event.sender.id);const isMaximized = win.isMaximized();if (isMaximized) {win.unmaximize();} else {win.maximize();}});//窗口关闭 ipcMain.on("window-close", function (event) {const win = BrowserWindow.fromId(event.sender.id);win.destroy();});});// 注释了此种方式改用官方推荐的专用方法来实现事件的监听// app.on("ready", async () => {// //启动慢的原因在此,注释掉它后能换来极致的快感// // if (isDevelopment && !process.env.IS_TEST) {// // // Install Vue Devtools// // try {// // await installExtension(VUEJS_DEVTOOLS);// // } catch (e) {// // console.error("Vue Devtools failed to install:", e.toString());// // }// // }// createWindow();// });


// Exit cleanly on request from parent process in development mode.if (isDevelopment) {if (process.platform === "win32") {process.on("message", (data) => {if (data === "graceful-exit") {app.quit();}});} else {process.on("SIGTERM", () => {app.quit();});}}


点击查看代码完成上述两个步骤后启用应用,控制面板中提示有错误消息,如下图所示:


解决办法:根目录下 vue.config.js 文件 pluginOptions.electronBuilder 节点添加内容 preload: "src/main/preload.js" ,具体内容如下所示:


pluginOptions: {electronBuilder: {mainProcessFile: "src/main/index.js", // 主进程入口文件 mainProcessWatch: ["src/main"], // 检测主进程文件在更改时将重新编译主进程并重新启动 preload: "src/main/preload.js", // 预加载 js},},


点击查看代码 src/renderer/App.vue 在功能区域为功能按钮绑定点击事件及处理,具体内容如下所示:


<template><div id="app"><header><div class="titleArea"><img :src="winIcon" /><span>{{ winTitle }}</span></div><div class="featureArea"><div title="扩展" @click="expand"><span class="iconfont icon-xiakuozhanjiantou"></span></div><div title="最小化" @click="minimize"><span class="iconfont icon-minimum"></span></div><div :title="maximizeTitle" @click="maximize"><span:class="{iconfont: true,'icon-zuidahua': isMaximized,'icon-window-max_line': !isMaximized,}"></span></div><div title="关闭" @click="close"><span class="iconfont icon-guanbi"></span></div></div></header><main>我是主体</main></div></template>


<script>export default {data: () => ({winIcon: ${process.env.BASE_URL}favicon.ico,winTitle: process.env.VUE_APP_NAME,isMaximized: true,}),


mounted() {window.windowApi.resize(this.resize);},


computed: {maximizeTitle() {return this.isMaximized ? "向下还原" : "最大化";},},


methods: {//扩展 expand() {this.$message({type: "success",message: "我点击了扩展",});},//最小化 minimize() {window.windowApi.minimize();},//向下还原|最大化 maximize() {window.windowApi.maximize();},// 窗口关闭 close() {window.windowApi.close();},/*** 重置窗体大小后的回调函数* @param {事件源对象} event* @param {参数} args*/resize(event, args) {this.isMaximized = args;},},};</script>


<style lang="scss">iconSize: 35px;body {margin: 0px;}#app {font-family: "微软雅黑";color: #2c3e50;display: flex;flex-direction: column;header {background: #16407b;color: #8c8663;height: $titleHeight;width: 100%;display: flex;


.titleArea {  flex-grow: 10;  padding-left: 5px;  display: flex;  align-items: center;  img {    width: 24px;    height: 24px;  }  span {    padding-left: 5px;  }}
.featureArea { flex-grow: 1; display: flex; justify-content: flex-end; color: white;
div { width: $iconSize; height: $iconSize; line-height: $iconSize; text-align: center; }
/* 最小化 最大化悬浮效果 */ div:hover { background: #6fa8ff; } /* 关闭悬浮效果 */ div:last-child:hover { background: red; }}
复制代码


}


// 主体区域铺满剩余的整个宽、高度 main {background: #e8eaed;width: 100%;height: calc(100vh - $titleHeight);}}</style>


点击查看代码现在还差最后一步,在拖拽标题栏的时候,也需要能改变窗体位置和大小,具体内容如下所示:


标题栏最终的交互效果,如下图所示:


4、自定义右键菜单项


当前在开发模式下启动应用后也会自启动调试工具(devtools)便于技术人员分析并定位问题,如果关闭调试工具后就没有渠道再次启用调试工具了。还有场景就是在非开发模式下默认是不启用调试工具的,应用出现问题后也需要启用调试工具来分析定位问题。这个时候呢,参考浏览器鼠标右键功能,给应用添加右键菜单项功能包含有:重新加载、调试工具等。右键菜单项在主进程中 src/main/index.js 管理,通过给 BrowserWindow 对象 webContents 属性绑定鼠标右键处理监听处理,具体内容如下所示:


"use strict";


import { app, protocol, BrowserWindow, ipcMain, Menu } from "electron";import { createProtocol } from "vue-cli-plugin-electron-builder/lib";import path from "path";// 取消安装 devtools 后,则不需要用到此对象,可以注释掉// import installExtension, { VUEJS_DEVTOOLS } from "electron-devtools-installer";const isDevelopment = process.env.NODE_ENV !== "production";


// Scheme must be registered before the app is readyprotocol.registerSchemesAsPrivileged([{ scheme: "app", privileges: { secure: true, standard: true } },]);


//创建应用主窗口 const createWindow = async () => {const win = new BrowserWindow({//窗体宽度(像素),默认 800 像素 width: 800,//窗体高度(像素),默认 600 像素 height: 600,//窗口标题,如果在加载的 HTML 文件中定义了 HTML 标签 <title>,则该属性将被忽略。title: ${process.env.VUE_APP_NAME}(${process.env.VUE_APP_VERSION}),//不显示窗体 show: false,webPreferences: {// Use pluginOptions.nodeIntegration, leave this alone// See nklayman.github.io/vue-cli-plugin-electron-builder/guide/security.html#node-integration for more info// 是否开启 node 集成,默认 falsenodeIntegration: false,// 否在独立 JavaScript 环境中运行 Electron API 和指定的 preload 脚本. 默认为 truecontextIsolation: true,//在页面运行其他脚本之前预先加载指定的脚本 preload: path.join(__dirname, "preload.js"),},//fasle:无框窗体(没有标题栏、菜单栏)frame: false,});//窗体最大化 win.maximize();//显示窗体 win.show();


if (process.env.WEBPACK_DEV_SERVER_URL) {// Load the url of the dev server if in development modeawait win.loadURL(process.env.WEBPACK_DEV_SERVER_URL);if (!process.env.IS_TEST) win.webContents.openDevTools();} else {createProtocol("app");// Load the index.html when not in developmentawait win.loadURL("app://./index.html");}


//监听窗口重置大小后事件,若触发则给渲染进程发送消息 win.on("resize", () => {win.webContents.send("window-resize", win.isMaximized());});


//添加右键菜单项 createContextMenu(win);};


//给指定窗体创建右键菜单项 const createContextMenu = (win) => {//自定义右键菜单 const template = [{label: "重新加载",accelerator: "ctrl+r", //快捷键 click: function () {win.reload();},},{label: "调试工具",click: function () {const isDevToolsOpened = win.webContents.isDevToolsOpened();if (isDevToolsOpened) {win.webContents.closeDevTools();} else {win.webContents.openDevTools();}},},];const contextMenu = Menu.buildFromTemplate(template);win.webContents.on("context-menu", () => {contextMenu.popup({ window: win });});};


// Quit when all windows are closed.app.on("window-all-closed", () => {// On macOS it is common for applications and their menu bar// to stay active until the user quits explicitly with Cmd + Qif (process.platform !== "darwin") {app.quit();}});


app.on("activate", () => {// On macOS it's common to re-create a window in the app when the// dock icon is clicked and there are no other windows open.if (BrowserWindow.getAllWindows().length === 0) createWindow();});


// 只有在 app 模组的 ready 事件能触发后才能创建 BrowserWindows 实例。 您可以借助 app.whenReady() API 来等待此事件// 通常我们使用触发器的 .on 函数来监听 Node.js 事件。// 但是 Electron 暴露了 app.whenReady() 方法,作为其 ready 事件的专用监听器,这样可以避免直接监听 .on 事件带来的一些问题。 参见 https://github.com/electron/electron/pull/21972。app.whenReady().then(() => {createWindow();


//窗口最小化 ipcMain.on("window-min", function (event) {const win = BrowserWindow.fromId(event.sender.id);win.minimize();});//窗口向下还原|最大化 ipcMain.on("window-max", function (event) {const win = BrowserWindow.fromId(event.sender.id);const isMaximized = win.isMaximized();if (isMaximized) {win.unmaximize();} else {win.maximize();}});//窗口关闭 ipcMain.on("window-close", function (event) {const win = BrowserWindow.fromId(event.sender.id);win.destroy();});});// 注释了此种方式改用官方推荐的专用方法来实现事件的监听// app.on("ready", async () => {// //启动慢的原因在此,注释掉它后能换来极致的快感// // if (isDevelopment && !process.env.IS_TEST) {// // // Install Vue Devtools// // try {// // await installExtension(VUEJS_DEVTOOLS);// // } catch (e) {// // console.error("Vue Devtools failed to install:", e.toString());// // }// // }// createWindow();// });


// Exit cleanly on request from parent process in development mode.if (isDevelopment) {if (process.platform === "win32") {process.on("message", (data) => {if (data === "graceful-exit") {app.quit();}});} else {process.on("SIGTERM", () => {app.quit();});}}


点击查看代码

用户头像

Java-fenn

关注

需要Java资料或者咨询可加我v : Jimbye 2022.08.16 加入

还未添加个人简介

评论

发布
暂无评论
基于electron+vue+element构建项目模板之【自定义标题栏&右键菜单项篇】_Java_Java-fenn_InfoQ写作社区