electronmacOS_0">electron编写一个macOS风格的桌面应用
基于vue3+vite,看一下最后的效果:
针对原始的electron模板,做了如下几点调整:
- 背景边框进行了圆角处理
- 隐藏了原始的titleBar
- 增加了macOS风格的窗口管理工具,就是交通灯按钮组实现最大化/最小化/还原、关闭,并增加按钮点击事件
一、背景边框圆角处理
electron并不支持对桌面应用进行圆角处理,需要相对较为复杂的逻辑处理,思路如下:
- 主进程背景设置透明
- 渲染进程的入口组件设置背景圆角
- 入口页面隐藏滚动条
主进程中的核心代码:
javascript">// main.js
const { app, BrowserWindow } = require('electron');app.whenReady().then(() => {const mainWindow = new BrowserWindow({width: 800,height: 600,frame: false, // 移除默认框架transparent: true, // 使窗口背景透明webPreferences: {preload: path.join(__dirname, 'preload.js'),contextIsolation: true,},});mainWindow.loadURL('http://localhost:3000'); // 加载Vue应用或HTML文件
});
入口组件中的核心代码:
// App.vue
<template><div class="mainWin"><RouterView /></div>
</template><script setup>
import { RouterView } from 'vue-router';
import { ref } from 'vue';
</script><style lang="scss" scoped>
.mainWin {width: 100%;height: 100vh;background-color: #fff;border-radius: 5px;overflow-y: hidden;display: flex;flex-direction: column;
}</style>
入口页面文件中的核心代码:
<!doctype html>
<html><head><meta charset="UTF-8" /><title>Electron</title><!-- https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP --><!-- <metahttp-equiv="Content-Security-Policy"content="default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data:"/> --><style>body,html {overflow: hidden;border-radius: 5px;}</style></head><body><div id="app"></div><script type="module" src="/src/main.js"></script></body>
</html>
overflow: hidden;是用于隐藏滚动条,经测试,写在index.html文件中才生效,边框圆角写在app组件中生效
二、隐藏原始titleBar,增加交通灯按钮
1、隐藏titleBar
主进程中设置titleBarStyle: ‘hidden’,即可
如下:
javascript">function createWindow() {// Create the browser window.const mainWindow = new BrowserWindow({width: 900,height: 670,show: false,autoHideMenuBar: true,titleBarStyle: 'hidden',transparent: true,frame: process.platform === 'darwin',...(process.platform === 'linux' ? { icon } : {}),webPreferences: {preload: join(__dirname, '../preload/index.js'),sandbox: false}}).....省略其他代码
}
2、交通灯样式
逻辑是在app组件中添加header标签,添加按钮组,并设置样式
// App.vue
<script setup>
import { RouterView } from 'vue-router'
import { ref } from 'vue'// 方法用于最小化、关闭窗口以及切换最大化/还原状态
const isMaximized = ref(false)
const minimizeWindow = () => {if (window.api.platform !== 'darwin') {window.api.minimize()}
}const maximizeWindow = () => {if (isMaximized.value) {window.api.unmaximize()} else {window.api.maximize()}isMaximized.value = !isMaximized.value
}const closeWindow = () => {window.api.close()
}const toggleMaximize = () => {window.api.toggleMaximize()
}
</script><template><div class="mainWin"><header class="window-header" @dblclick="toggleMaximize"><!-- <span class="title">My App</span> --><div class="controls"><!-- 自定义交通灯按钮 --><button@click="minimizeWindow"class="traffic-light":class="'minimize'"title="最小化"></button><button@click="maximizeWindow"class="traffic-light":class="isMaximized ? 'restore' : 'maximize'":title="isMaximized ? '还原' : '最大化'"></button><button @click="closeWindow" class="traffic-light" :class="'close'" title="关闭"></button></div></header><RouterView /></div>
</template><style lang="scss" scoped>
.mainWin {/* 添加背景图片 */background-image: url('./assets/imgs/background.jpg');backdrop-filter: blur(10px); /* 模糊背景 */background-size: cover; /* 自适应填充 */background-position: center; /* 居中显示 */background-repeat: no-repeat; /* 避免重复 */width: 100%;height: 100vh;// margin: 0;// padding: 0;background-color: #fff;border-radius: 5px;// overflow-y: hidden;.window-header {-webkit-app-region: drag;display: flex;justify-content: space-between;align-items: center;padding: 10px;background-color: #f0f0f0;border-bottom: 1px solid #ddd;border-top-left-radius: 5px;border-top-right-radius: 5px;flex-shrink: 0;}.controls {display: flex;align-items: center;}.traffic-light {-webkit-app-region: no-drag;width: 12px;height: 12px;margin-left: 8px;border-radius: 50%;background-color: transparent;border: none;cursor: pointer;transition: all 0.2s ease;&:hover {transform: scale(1.2);}&.minimize {background-color: #ffcc00; // 黄色}&.maximize {background-color: #9fac00; // 绿色}&.restore {background-color: #9fac00; // 恢复按钮也用绿色}&.close {background-color: #ff4f38; // 红色}}.title {-webkit-app-region: drag;font-size: 14px;font-weight: bold;}
}
</style>
3、交通灯按钮组点击响应逻辑
窗口的最大最小化以及还原操作需要调用window底层api,因此需要渲染进程和主进程进行通讯,逻辑如下:
-
渲染进程点击对应的按钮,就是上面代码中的点击事件
-
在预加载进程中分别添加最大化、最小化、还原、关闭以及拖动窗口的api,同时通过ipcRenderer向主进程发送触发信号
javascript">// preload/index.js import { contextBridge, ipcRenderer } from 'electron' import { electronAPI } from '@electron-toolkit/preload'// Custom APIs for renderer const api = {// 添加一个方法来打开新窗口openNewWindow: async () => {try {const response = await ipcRenderer.invoke('open-new-window')return response} catch (error) {console.error('Failed to open new window:', error)throw error}},// 窗口变化的apiminimize: () => ipcRenderer.send('minimize-window'),maximize: () => ipcRenderer.send('maximize-window'),unmaximize: () => ipcRenderer.send('unmaximize-window'),close: () => ipcRenderer.send('close-window'),toggleMaximize: () => ipcRenderer.send('toggle-maximize'),platform: process.platform, }// Use `contextBridge` APIs to expose Electron APIs to // renderer only if context isolation is enabled, otherwise // just add to the DOM global. if (process.contextIsolated) {try {contextBridge.exposeInMainWorld('electron', electronAPI)contextBridge.exposeInMainWorld('api', api)} catch (error) {console.error(error)} } else {window.electron = electronAPIwindow.api = api }
platform: process.platform,是为了在渲染进程中可以获取到操作系统的信息
-
主进程通过ipcMain接收指令,并对窗口进行对应操作
javascript">ipcMain.on('minimize-window', () => {mainWindow.minimize()})ipcMain.on('maximize-window', () => {mainWindow.maximize()})ipcMain.on('unmaximize-window', () => {mainWindow.unmaximize()})ipcMain.on('close-window', () => {mainWindow.close()})ipcMain.on('toggle-maximize', () => {if (mainWindow.isMaximized()) {mainWindow.unmaximize()} else {mainWindow.maximize()}})