electron编写一个macOS风格的桌面应用
基于vue3+vite,看一下最后的效果:
针对原始的electron模板,做了如下几点调整:
- 背景边框进行了圆角处理
- 隐藏了原始的titleBar
- 增加了macOS风格的窗口管理工具,就是交通灯按钮组实现最大化/最小化/还原、关闭,并增加按钮点击事件
一、背景边框圆角处理
electron并不支持对桌面应用进行圆角处理,需要相对较为复杂的逻辑处理,思路如下:
- 主进程背景设置透明
- 渲染进程的入口组件设置背景圆角
- 入口页面隐藏滚动条
主进程中的核心代码:
// 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 -->
<!-- <meta
http-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’,即可
如下:
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向主进程发送触发信号
// 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 } }, // 窗口变化的api minimize: () => 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 = electronAPI window.api = api }
platform: process.platform,是为了在渲染进程中可以获取到操作系统的信息
-
主进程通过ipcMain接收指令,并对窗口进行对应操作
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() } })