基於electron+vue+element構建項目模板之【自定義標題欄&右鍵菜單項篇】
1、概述
開發平台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 info nodeIntegration: 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 info nodeIntegration: 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"; //窗體操作api contextBridge.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 ready protocol.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集成,默認false nodeIntegration: false, // 否在獨立 JavaScript 環境中運行 Electron API和指定的preload 腳本. 默認為 true contextIsolation: 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 mode await 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 development await 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 + Q if (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 事件帶來的一些問題。 參見 //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"> $titleHeight: 40px; $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 ready protocol.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集成,默認false nodeIntegration: false, // 否在獨立 JavaScript 環境中運行 Electron API和指定的preload 腳本. 默認為 true contextIsolation: 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 mode await 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 development await 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 + Q if (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 事件帶來的一些問題。 參見 //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(); }); } }
點擊查看程式碼
下一篇中將介紹項目打包等事宜
感謝您閱讀本文,如果本文給了您幫助或者啟發,還請三連支援一下,點贊、關注、收藏,作者會持續與大家分享更多乾貨~
源碼地址://gitee.com/libaitianya/electron-vue-element-template