首頁>技術>

本文為《Electron實戰》中文版第7章內容,私聊得樣本電子書。

本章包括

使用Electron的Menu和MenuItem模組建立選單通過模版構建選單為目標作業系統自定義選單為選單項分配通用作業系統角色使選單項具有自定義的、特定於應用程式的功能為介面不同部分建立自定義的上下文選單

在基於瀏覽器的應用程式中,開發人員只能訪問應用程式視窗的可見區域,不能向瀏覽器的工具欄或選單欄新增控制元件,應用程式的整個功能介面必須位於視窗內。開發人員同樣在視窗內面臨一些限制,不能修改右鍵單擊介面時出現的上下文選單,為每個選項和命令找到一個合適位置可能是一個挑戰。而Electron則使開發人員能夠在瀏覽器視窗之外新增功能,例如當用戶右鍵單擊介面元件時出現的自定義應用程式和上下文選單。

在本章中,我們將探討如何在Fire Sale中建立和配置這些選單。我們將用自己的選單替換Electron提供的預設選單,並在選單中展示常見的作業系統功能。我們將給選單項分配快捷方式,以便在應用程式的任何位置都能觸發它們。在實現了基本的選單功能之後,我們將新增我們自己應用程式所特定的選單項——特別是,能夠從檔案系統中開啟Markdown檔案,在介面左側窗格中顯示其內容,並在右側窗格中將其內容呈現為HTML的功能。最後,當用戶在左側窗格中單擊右鍵時,我們建立一個自定義上下文選單,其中包含常見的文字操作任務(剪下、複製和貼上,如圖7.1所示)。

在前幾章中,我們已經在Fire Sale中實現了一個選單。那麼為什麼現在還要定製呢?開發人員可以覆蓋Electron的預設選單,但是他們需要從頭開始構建。在本章,我們會還原大多數桌面應用程式常見的基本功能。在打下基礎之後,我們再使用自定義功能對其進行擴充套件。例如,使用者可以通過選單儲存當前活動的檔案,以及將HTML匯出到單獨的檔案。除了能夠從應用程式選單中訪問此功能外,使用者還可以使用快捷鍵來觸發選單項。在本章,我們會構建一個Fire Sale選單,其結構如圖7.2所示。

圖7.2 Fire Sale選單結構。

7.1. 仿製並替換預設選單

首先建立一個名為./app/application.js的新檔案,到本章結束時,這個檔案將會變得很大,因此我們現在就把它分拆成獨立的檔案。我們先從複製、貼上選單開始。

程式碼清單7.1 :建立帶有複製和貼上功能的編輯選單:./app/application-menu.js

const { app, BrowserWindow, Menu, shell } = require('electron');const mainProcess = require('./main');const template = [    {        label: 'Edit',        submenu: [            {                label: 'Copy',                accelerator: 'CommandOrControl+C',                role: 'copy',            },            {                label: 'Paste',                accelerator: 'CommandOrControl+V',                role: 'paste',            },        ]    }];module.exports = Menu.buildFromTemplate(template); 

下一步,當應用程式觸發ready事件時設定選單。

程式碼清單7.2 :從應用程式檔案中載入選單:./app/main.js

const { app, BrowserWindow, dialog, Menu } = require('electron');const applicationMenu = require('./application-menu');const fs = require('fs');const windows = new Set();const openFiles = new Map();app.on('ready', () => {    Menu.setApplicationMenu(applicationMenu);    createWindow();});// ... Additional methods below ...

Electron有Menu和MenuItem模組可用於構建選單。理論上,我們可以只用MenuItem就可以構建一個選單,但這種方法可能很繁瑣,且容易出錯。為了方便起見,Menu模組提供了buildFromTemplate()方法,該方法接受一個常規的JavaScript物件陣列。Electron會在內部根據你所提供的陣列來建立選單項。

7.1.1. macOS與缺少編輯選單的情況

如果在Windows中啟動應用程式,應該會看到一個“編輯”選單,不出意外的話,其中會有兩個選單項:複製和貼上。但如果你在macOS上測試應用程式,結果會有些不同,如圖7.3所示。

在macOS中,選單名會是“Electron”而不是“編輯”,因為macOS上的第一個選單總是“應用程式”選單。要在Electron中解決這個問題,我們需要將”編輯”選單以及後續所有選單項都向後移動一個位置(如清單7.3和圖7.4所示),以便為本章後續要實現的應用程式選單騰出空間。

圖7.4 通過將所有菜單向後移動一個位置,“編輯”選單就能正常顯示了。很快,我們就會實現一個與原生macOS應用程式行為類似的“應用程式”選單

程式碼清單7.3 :將macOS原有選單項前移:./app/application-menu.js

const { app, BrowserWindow, Menu, shell } = require('electron');const mainProcess = require('./main');const template = [    // ... Menu template from the last section. ...];if (process.platform === 'darwin') {    const name = 'Fire Sale';    template.unshift({ label: name });}module.exports = Menu.buildFromTemplate(template);

使用Electron構建應用程式的好處之一是,開發人員可以針對macOS、Windows和Linux使用同一套程式碼。需要注意的是,開發人員在編寫程式碼時應該考慮每種受支援作業系統的特性。幸運的是,Node提供了process物件,該物件提供了一些能對應用程式執行環境進行自我檢測的屬性、方法和事件。

process.platform返回當前應用程式執行的平臺名稱,在寫這篇文章的時候,process.platform支援這五個平臺:darwin、freebsd、linux、sunos或win32,其中Darwin是UNIX作業系統,macOS正是基於該系統開發的。我們可以在執行時通過檢測process.platform是否等於darwin來調整選單。如果是,那麼應用程式正執行在macOS上,所有選單項應該向右移動一個位置。

為了以正確順序顯示選單而做的一切額外工作,所得到的效果,正是圖7.4所示。另外我們不需要通過“Edit”選單來特別實現,即可獲得對“聽寫”和“表情&符號”的支援。

7.1.2. 替換Electron預設選單的隱性成本

Electron提供了一個預設選單,但這是一個要麼都有或要麼全無的功能,當我們要替換選單時,我們就要放棄它所有原始功能。我們不僅會丟失一些選單項,而且還同時會丟失快捷鍵。比如在macOS上使用Command-X,或者在Windows和Linux上使用Control-X快捷鍵,從左側窗格中剪下文字的。在macOS或Windows上分別使用Command-A或Control-A命令選擇所有文字的,以及Command-Z或Control-Z命令執行撤銷的,都將失效。如果你使用的是macOS,請嘗試使用Command-Q退出應用程式,如果使用兩次都無效,那麼我們就失去了在macOS中隱藏此應用程式或其他應用程式的功能。在所有作業系統上,我們失去了“撤消”和“重做”功能、“最小化”和“關閉”視窗以及選擇文字的能力,剩下的就只有複製和貼上的功能,如圖7.5所示,這是因為為我們認為的將其添加回了自定義選單中。

圖7.5 在Electron的內建選單中實現的編輯和視窗選單。

是否將這些功能添加回應用程式取決於開發人員,如果他們想在應用程式中省略這些功能,他們完全可以這樣做。你的最初想法可能是,重新實現這些功能就像重新造輪子。幸運的是,Electron可以很容易地建立可以執行常用作業系統任務的選單項。建立新選單項時,可以設定一些選項。到目前為止,我們已經接觸了label和type選項,你可能注意到我們這前面每個清單中的第三個選單項的type選項都設定成了separator。

7.1.3. 實現編輯和視窗選單

為了構建Electron選單,我們先從實現“編輯”和“視窗”選單開始,實現方式類似於在Electron預設選單中定義它們一樣,如圖7.5所示。

程式碼清單7.4 :“編輯”選單模版:./app/application-menu.js

const template = [ { label: 'Edit', submenu: [ { label: 'Undo', accelerator: 'CommandOrControl+Z', role: 'undo', }, { label: 'Redo', accelerator: 'Shift+CommandOrControl+Z', role: 'redo', }, { type: 'separator' }, { label: 'Cut', accelerator: 'CommandOrControl+X', role: 'cut', }, { label: 'Copy', accelerator: 'CommandOrControl+C', role: 'copy', }, { label: 'Paste', accelerator: 'CommandOrControl+V', role: 'paste', }, { label: 'Select All', accelerator: 'CommandOrControl+A', role: 'selectall', }, ], }, { label: 'Window', submenu: [ { label: 'Minimize', accelerator: 'CommandOrControl+M', role: 'minimize', }, { label: 'Close', accelerator: 'CommandOrControl+W', role: 'close', }, ], },];if (process.platform === 'darwin') { const name = app.getName(); template.unshift({ label: name });}module.exports = Menu.buildFromTemplate(template);7.1.4. 定義選單項的角色和快捷鍵

你可能已經注意到,到目前為止新增的所有選單項都有一個特殊的role屬性。這個設定很重要,因為像複製和貼上這樣的功能很難手工實現。選單項可以具有一個role屬性,它與作業系統嚮應用程式提供的內建功能相關聯。在Windows、Linux和macOS上,選單項的role屬性可以設定為以下任意一種:

undoredocutcopypasteselectallminimizeclose

這些角色與我們在用自己的選單替換預設選單時所丟失的許多功能重疊。新增具有這些角色的選單項可以將這些功能恢復到選單中,但是卻不會恢復許多使用者已經習慣的快捷鍵。

Electron提供了一個名為accelerator的額外屬性,用於定義可以觸發選單項功能的快捷鍵。在建立選單項時,可以將accelerator屬性設定為符合Electron約定的字串。清單7.5是一個新增複製功能的選單項。

碼清單7.5 :使用role和accelerator屬性:./app/application-menu.js

const { app, BrowserWindow, Menu, MenuItem, shell } = require('electron');const copyMenuItem = new MenuItem({ label: 'Copy', accelerator: 'CommandOrControl+C', role: 'copy'});

在Windows和Linux上,通常在快捷鍵前面加上Control鍵。在macOS上,常使用Command鍵來達到類似目的。一般情況下,Command鍵在Linux和Windows上也是不可用的,不需要在選單項中依賴process.platform來做條件判斷,Electron提供了更方便的CommandOrControl,它在macOS上,會將快捷鍵繫結到Command鍵,在Windows和Linux上,Electron會使用Control鍵。另外,Electron還提供了Command、Control和CommandOrControl的簡寫別名,分別是Cmd、Ctrl和CmdOrCtrl。

7.1.5. macOS上還原應用程式選單

當Electron執行時,它將模板編譯成MenuItems集合,並相應地將其設定為應用程式的選單。還原了複製和貼上等常用操作的快捷鍵,應用程式在Windows和Linux中的行為也達到了預期。然而,在macOS中,應用程式仍然缺少一些重要功能,至少缺少退出應用程式的功能。macOS中的標準應用程式選單的結構如圖7.6所示。

當運行於macOS上時,Electron提供了一組額外的角色,可以輕鬆還原大多數Mac應用程式的常用選單。這些額外的角色有:

abouthidehideothersunhidefrontwindowhelpservices

Electron提供的預設應用程式選單的一些選單項,包括應用程式的“關於”面板、macOS提供的公開服務、隱藏應用程式、隱藏其他所有應用程式以及退出應用程式,如圖7.7所示。

實現應用程式選單類似於實現“編輯”和“視窗”選單。定義快捷方式時,Command比CommandOrControl更可取,因為該菜單隻出現在macOS上。此外,我們使用模板字串來給“關於”、“隱藏”和“退出”選單加上應用程式名稱,因為習慣上在這些選單項中應該包含應用程式的名稱。

程式碼清單7.6 :macOS的應用程式選單:./app/application-menu.js

if (process.platform === 'darwin') { const name = 'Fire Sale'; template.unshift({ label: name, submenu: [ { label: `About ${name}`, role: 'about', }, { type: 'separator' }, { label: 'Services', role: 'services', submenu: [], }, { type: 'separator' }, { label: `Hide ${name}`, accelerator: 'Command+H', role: 'hide', }, { label: 'Hide Others', accelerator: 'Command+Alt+H', role: 'hideothers', }, { label: 'Show All', role: 'unhide', }, { type: 'separator' }, { label: `Quit ${name}`, accelerator: 'Command+Q', click() { app.quit(); }, }, ], });}我們的應用程式現在幾乎擁有macOS上原生應用程式的所有功能,但是我們仍然需要處理一些細微的差異。在macOS上,視窗選單有一些額外的選單項——最明顯的就是“Bring All to Front”選單項,它可以將當前應用程式所有視窗移到最前面。此外,macOS特有的window角色還具有從“視窗”選單中關閉和最小化當前視窗的功能,還列出了應用程式所有視窗,以及可以將它們顯示在前面的功能。在不支援此角色的平臺上將會忽略此角色。程式碼清單7.7 :合併應用程式“編輯”、“視窗”選單:./app/application-menu.jsconst template = [ { label: 'Edit', submenu: [ // "Edit" menu shown in Listing 7.4 ], }, { label: 'Window', role: 'window', submenu: [ // "Window" menu shown in Listing 7.4 ], },];if (process.platform === 'darwin') { const name = app.getName(); template.unshift({ label: name, submenu: [ // #Application menu shown in Listing 7.6 ], }); const windowMenu = template.find(item => item.label === 'Window'); windowMenu.role = 'window'; windowMenu.submenu.push( { type: 'separator' }, { label: 'Bring All to Front', role: 'front', } );}

圖7.8 macOS中的“視窗”選單可以讓你檢視應用程式當前開啟的所有視窗。

7.1.8. 新增幫助選單

無論什麼平臺,新增“幫助”選單都是一個很好的選擇,尤其在macOS上這樣做還有一個額外的好處。即使你的應用程式還沒有任何文件或技術支援提供時,內建的“幫助”選單可以讓使用者搜尋應用程式的選單項,如圖7.9所示。這在大多數macOS應用程式中都是有效的,對於快速檢索那些巢狀很深的選單來說非常有用。你可以隨時按下Command-Shift-?組合鍵來訪問選單的搜尋功能。

要將“幫助”選單新增到應用程式中(如圖7.10所示的結構),請新增一個具有help角色的額外選單和一個包含該選單項的子選單。你必須提供一個數組作為子選單,如清單7.8所示,即便它是空的。現在我們還可以新增啟用開發者工具的功能,當然有些應用程式可能希望在釋出之前刪除此功能。但像Atom、Nylas Mail和Visual Studio Code這樣的流行應用程式選擇了保留這個功能。

圖7.9 在macOS中,“幫助”選單可以讓使用者搜尋應用程式的選單項。

程式碼清單7.8 :建立“幫助”選單:./app/application-menu.js

const template = [ // "Edit" and "Window" menus defined in Listing 7.7 { label: 'Help', role: 'help', submenu: [ { label: 'Visit Website', click() { /* To be implemented */ } }, { label: 'Toggle Developer Tools', click(item, focusedWindow) { if (focusedWindow) focusedWindow.webContents.toggleDevTools(); } } ], }];

click()方法有三個可選引數:選單項本身、當前處於焦點的BrowserWindow例項和事件物件。在清單7.8中,我們使用了第二個引數——當前焦點視窗——來確定哪個視窗應該切換開發者工具。

圖7.11 本章,我們會新增一個本應用程式的“檔案”選單功能。

當用戶單擊“開啟檔案”選單項或按下相應的快捷鍵時,選單項會觸發主程序中的openFile()函式,與從介面上按下按鈕觸發的函式相同。單擊“新建檔案”將呼叫主程序的createWindow()函式。我們先在模板中新增“檔案”選單開始,將圖7.11中所示的每個功能都作為選單項新增到模板的submenu陣列中。

然而,針對儲存或匯出檔案功能,我們分別需要Markdown窗格和HTML窗格的當前內容。如果當前有開啟的檔案,我們還需要該檔案的名稱,由於主程序無法訪問該資訊,因此需要我們向當前焦點視窗傳送一條訊息,該視窗應該為我們收集這些資訊,並觸發與使用者單擊介面按鈕時相同的功能。

程式碼清單7.9 :自定義選單功能:./app/application-menu.js

const template = [    {        label: 'File',        submenu: [            {                label: 'New File',                accelerator: 'CommandOrControl+N',                click() {                    mainProcess.createWindow();                }            },            {                label: 'Open File',                accelerator: 'CommandOrControl+O',                click(item, focusedWindow) {                    mainProcess.getFileFromUser(focusedWindow);                },            },            {                label: 'Save File',                accelerator: 'CommandOrControl+S',                click(item, focusedWindow) {                    focusedWindow.webContents.send('save-markdown');                },            },            {                label: 'Export HTML',                accelerator: 'Shift+CommandOrControl+S',                click(item, focusedWindow) {                    focusedWindow.webContents.send('save-html');                },            },        ],    },    // "Edit", "Window", and "Help" menus are defined here as well.];

向焦點視窗傳送訊息是成功的一半,我們仍然需要配置渲染程序來監聽這些訊息並進行相應地操作。我們先設定一個IPC監聽器來負責接收這些訊息,並在接收到訊息時呼叫現有的儲存和匯出功能。

程式碼清單7.10 :給渲染程序新增IPC監聽器:./app/renderer.js

ipcRenderer.on('save-markdown', () => {    mainProcess.saveMarkdown(currentWindow, filePath, markdownView.value);});ipcRenderer.on('save-html', () => {    mainProcess.saveHtml(currentWindow, filePath, markdownView.value);});
7.2.1. 處理沒有焦點視窗的情況

在Windows和Linux中,當所有視窗都關閉時,應用程式就退出。在macOS上,即使所有視窗都關閉了,應用程式仍然還在執行,單擊圖示時會開啟一個新視窗。但在某些情況下,使用者可能會選擇我們剛剛實現的三個選單項中的一個,而此時的焦點視窗是undefined。在第9章,我們會介紹如何啟用和禁用選單項。現在,我們採用一種更簡單的方法:如果使用者選擇“開啟檔案”,則開啟一個新視窗;如果沒有內容要儲存或匯出的話,則顯示一條錯誤訊息。

要在使用者試圖儲存或匯出不存在的檔案時顯示錯誤訊息,我們可以使用dialog.showErrorBox()函式,它類似於dialog.showMessageBox(),但是它是專門用來顯示錯誤訊息的,並且沒有那麼多配置選項。

程式碼清單7.11 :試圖儲存或匯出不存在的檔案時給出錯誤提示:./app/application-menu.js

const { app, dialog, Menu, MenuItem shell } = require('electron');const mainProcess = require('./main');const template = [    {        label: 'File',        submenu: [            {                label: 'New File',                accelerator: 'CommandOrControl+N',                click() {                    mainProcess.createWindow();                }            },            {                label: 'Open File',                accelerator: 'CommandOrControl+O',                click(item, focusedWindow) {                    mainProcess.getFileFromUser(focusedWindow);                },            },            {                label: 'Save File',                accelerator: 'CommandOrControl+S',                click(item, focusedWindow) {                    if (!focusedWindow) {                        return dialog.showErrorBox(                            'Cannot Save or Export',                            'There is currently no active document to save or export.'                            );                    }                    focusedWindow.webContents.send('save-markdown');                },            },            {                label: 'Export HTML',                accelerator: 'Shift+CommandOrControl+S',                click(item, focusedWindow) {                    if (!focusedWindow) {                        return dialog.showErrorBox(                            'Cannot Save or Export',                            'There is currently no active document to save or export.'                            );                }                focusedWindow.webContents.send('save-html');            },        },    ], },

如果使用者選擇的是“開啟檔案”,並且也沒有視窗來接收該命令的話,那麼問題就簡單了。我們只需建立一個新視窗,等視窗顯示出來就觸發開啟檔案對話方塊,就像視窗一直存在一樣。

程式碼清單7.12 :當沒有視窗用於顯示使用者開啟的新檔案時就建立一個新視窗:./app/application-menu.js

const template = [    {        label: 'File',        submenu: [            {                label: 'Open File',                accelerator: 'CommandOrControl+O',                click(item, focusedWindow) {                    if (focusedWindow) {                        return mainProcess.getFileFromUser(focusedWindow);                    }                    const newWindow = mainProcess.createWindow();                                        newWindow.on('show', () => {                        mainProcess.getFileFromUser(newWindow);                    });                },            }, // "Save File" and "Export HTML" menus are defined here.        ],    }, // "Edit", "Window", and "Help" menus are defined here.];

首先,我們檢查focusedWindow是否存在。如果有,我們希望觸發之前實現的功能,並儘早從函式返回。如果沒有焦點視窗,我們需要建立一個。幸運的是,在第5章中就建立了這個函式來幫助我們完成這個過程。當新視窗完成初始化後,就可以像使用現有視窗一樣使用它。我們的程式碼現在已經可以匹配這種情況,可以繼續實現其他功能。

7.3. 構建上下文選單

在上一節,我們定義了一個選單,並在app模組觸發“ready”事件時將其設定為主程序中的應用程式選單。應用程式一次只能有一個應用程式選單,但是,我們可以在渲染程序中定義額外的選單,如圖7.12所示,當用戶右鍵單擊(或在某些計算機上用兩個手指單擊)介面的某一部位,這些選單就會彈出。

下一步,我們在左邊markdown窗格中監聽contextmenu事件。

程式碼清單7.13 :監聽contextmenu事件:./app/renderer.js

markdownView.addEventListener('contextmenu', (event) => {    event.preventDefault();    alert('One day, a context menu will go here.');});

注意,除非使用者右鍵單擊左側窗格,否則警告不會觸發。如果你想要從應用程式中的任何位置觸發上下文選單,請在window物件上而不是在DOM節點上監聽。Menu模組在渲染程序中是不可用的,但是可以使用remote模組從主程序的上下文中訪問它,如下面的清單所示。匯入該模組,我們就可以使用Menu.buildFromTemplate()來構造一個選單,如清單7.15所示。

程式碼清單7.14 :建立上下文選單:./app/renderer.js

const { remote, ipcRenderer } = require('electron');const { Menu } = remote;const path = require('path');const mainProcess = remote.require('./main.js');const currentWindow = remote.getCurrentWindow();// Our existing renderer code…const markdownContextMenu = Menu.buildFromTemplate([    { label: 'Open File', click() { mainProcess.getFileFromUser(); } },    { type: 'separator' },    { label: 'Cut', role: 'cut' },    { label: 'Copy', role: 'copy' },    { label: 'Paste', role: 'paste' },    { label: 'Select All', role: 'selectall' },]);

要觸發此選單,要使用一個新函式來替換contextmenu事件監聽器,該函式將呼叫新建立的選單上的popup()方法,如下所示。

程式碼清單7.15 :觸發上下文選單:./app/renderer.js

markdownView.addEventListener('contextmenu', (event) => {    event.preventDefault();    markdownContextMenu.popup();});

popup()方法有四個引數:BrowserWindow、x、y和positioningItem。所有這些引數都是可選的,如果省略了它們,那麼上下文選單將直接顯示在當前瀏覽器視窗中滑鼠游標下,這正是我們所期望的。有了這些程式碼,我們就可以在Markdown窗格中觸發一個上下文選單。在嚮應用程式新增更多功能時,我們就向上下文選單以及其他上下文選單新增功能即可。本章的完整程式碼可以在https://github.com/electron-in-action/firesale/tree/chapter-7或附錄中找到。或者,你可以從https://github.com/electron-in-action/firesale.git的GitHub程式碼庫中進行克隆,然後切到chapter-7分支,並執行npm install來看實際效果。

總結Electron可以讓開發人員定製應用程式選單和上下文選單。Electron提供了Menu和MenuItem模組用於構建選單。Menu.buildFromTemplate()函式讓開發人員可以從JavaScript物件陣列中構建選單,而不必使用MenuItem建構函式。Electron提供了一個內建的應用程式選單,其中充滿了合理的預設設定。覆蓋這個選單意味著我們必須替換內建的功能。process.platform可以讓開發人員檢測他們應用程式當前所執行的作業系統。macOS的第一個選單項是一個特殊的應用程式選單。Electron為MenuItem提供了角色,可以讓開發人員輕鬆實現常用的作業系統級別的功能。MenuItem有一個click()方法,用於定義使用者單擊時的行為。MenuItem支援一個快捷方式屬性,可以讓開發人員定義一個快捷鍵來觸發它的動作。Electron支援渲染程序中的contextmenu事件,該事件在使用者右鍵單擊DOM時觸發。

《Electron實戰》中文版!

最新評論
  • BSA-TRITC(10mg/ml) TRITC-BSA 牛血清白蛋白改性標記羅丹明
  • 《Electron實戰》中文版:Electron實現UI介面