Electron自定义菜单栏及Mac最大化无效的问题解决
Electron自定义菜单栏及Mac最大化无效的问题解决
electron的应用打包后会有一个系统标题栏,在win中包含最小化、最大化、关闭、file等其他功能按钮。这个系统标题栏的局限很大,首先就是这个标题栏是固定的像素高度,没办法做到响应式,其次就是这个菜单栏的功能没法拓展,如果需要添加其他功能就比较捉襟见肘了。
针对这个问题我们可以隐藏这个系统标题栏然后在页面中自己去编写一个菜单栏,这一点官方也给出了实例👉自定义标题栏 | Electron,接下来我们就叙述一下如何创建一个自定义菜单栏并解决一些可能出现的问题。
自定义菜单栏
1.隐藏自带菜单栏
要创建自定义菜单栏第一步就是隐藏掉electron自带的系统标题栏。在隐藏之前还是提一下关于调整系统菜单栏的样式:
titleBarOverlay: {color: '#0ff', // 自定义背景色symbolColor: '#fff', // 自定义符号颜色height: 24, // 自定义高度
}
可以看到能做的处理有限,远比不上使用html+css能做到的样式体验,所以我们需要隐藏掉这个菜单栏:
autoHideMenuBar: true, // 是否自动隐藏菜单栏
可以简单展示一下创建窗口的相关参数:
import { BrowserWindow } from 'electron';mainWindow = new BrowserWindow({width: 1400,height: 800,useContentSize: true,autoHideMenuBar: true, // 是否自动隐藏菜单栏resizable: false, // 是否允许用户手动调整大小transparent: false, // 是否开启透明背景maximizable: false, //禁止双击放大frame: false, // 去掉顶部操作栏// titleBarStyle: 'hidden', // 隐藏标题栏// titleBarOverlay: {// color: '#0ff', // 自定义背景色// symbolColor: '#fff', // 自定义符号颜色// height: 24, // 自定义高度// },webPreferences: {preload: path.join(__dirname, 'preload.mjs'),},});
2.封装自定义菜单栏组件
接下来就是封装一个自定义菜单栏去替代系统的菜单栏,这里我只以最小化、全屏、关闭等操作为例。
// SysTitleBar.tsxconst SysTitleBar = (props: SysTitleBarProps) => {return (<div className={styles['system-title-bar']}><div className={styles['title-bar-left']}>{leftContent}</div><div className={styles['title-bar-center']}>{centerContent}</div><div className={styles['title-bar-right']}>{rightContent}<div className={styles['sys-menu-icon-box']}><MinimizeIcon className={styles['sys-menu-icon']} onClick={minimizeHandle} /><FullScreenIconclassName={styles['sys-menu-icon']}onClick={() => {toggleMaximize(true);}}/><CloseIcon className={styles['sys-menu-icon']} /></div></div></div>);
};
创建好了以后我们在入口页面中引入
// app.tsx<div className="app-container"><SysTitleBar /><div className="app-content"><Outlet /></div>
</div>
当然了,如果布局不一样可以自行调整,这一块就按设计稿去决定怎么展示菜单栏就好了。
3.实现系统菜单栏功能
现在我们创建了自定义的菜单栏,但是现在它还是个花架子,只能看不能用,所以我们需要给他补充功能。
3.1 可拖拽
当务之急我们需要完成的第一个功能就是让这个菜单栏可以像系统菜单栏一样鼠标按住拖拽。这里用的是app-region: drag来告诉electron哪些地方时可以拖拽的。
.system-title-bar{app-region: drag;
}
到这里还没有结束,因为你这么设置以后,组件内部的所有元素都是可拖拽的了,这样点击等事件根本没办法触发,所以我们需要将这些元素排除为非可编辑区域。
.title-bar-right{app-region: no-drag;
}
3.2 最小化
最小化的功能依靠于electron提供的minimize方法,所以我们只需要通知主进程调用这个方法即可。得益于我们刚刚将自定义菜单栏的右边设置为非可拖拽区域,所以我们可以给最小化的图标添加一个点击事件并绑定一个minimizeHandle
方法来告诉electron去调用minimize方法
// SysTitleBar.tsxconst minimizeHandle = () => {window.ipcRenderer?.minimize();
};// preload.ts
import { ipcRenderer, contextBridge } from 'electron';contextBridge.exposeInMainWorld('ipcRenderer', {minimize: () => ipcRenderer.send('window-minimize'),
});// main.ts
import { BrowserWindow, ipcMain } from 'electron';ipcMain.on('window-minimize', (event) => {const win = BrowserWindow.fromWebContents(event.sender);win?.minimize();
});
3.3 全屏
这里除了需要将软件全屏外,还有一个恢复原始大小的功能。图标的切换大家自己做处理就好,这里仅展示功能实现。还是按上面最小化的方式去实现,具体的方法及使用的API如下:
//preload.tstoggleMaximize: () => ipcRenderer.send('toggle-window-maximize'),// main.ts
ipcMain.on('toggle-window-maximize', (event) => {const win = BrowserWindow.fromWebContents(event.sender);if (win?.isMaximized()) { // 判断当前是否是全屏状态win.unmaximize(); // 取消全屏} else {win?.maximize(); // 全屏}
});
3.4 关闭软件
方式同上,使用的是创建的window实例上的close方法,具体使用方式如下:
// main.ts
ipcMain.on('window-close', (event) => {const win = BrowserWindow.fromWebContents(event.sender);win?.close();
});
到这里基本的自定义菜单栏就OK了,自定义菜单栏也是好处多多,例如需要再关闭之前做什么操作,我们完全可以在用户点击关闭按钮时先执行我们的操作然后再通知electron去关闭。所以我们在开发应用时基本都是需要自定义菜单栏实现。
Mac的最大化无效
这里说一下我这里的环境,electron的版本是30.0,系统是win11。上述功能在我自己电脑上运行时没有问题,在其他同事电脑上也是正常。但是测试突然给我踢了一个bug说是最大化无效。这就有点难搞了,因为我比较熟悉win所以我没有选Mac,难不成以前兼容ie的噩梦又出现了?
调试
首先还是需要找出问题,于是紧急给旁边同事派了根烟让他休息半小时,我紧急调试一下。根据测试发现是判断当前是否是全屏的方法**win.isMaximized()**一直返回false。去查了下发现是因为Mac和win的全屏逻辑不一样,electron的issue也提到了这个问题,这里我就不贴了感兴趣的自己可以去翻翻看。
我看了下issue给出的建议是全屏的状态有我们去控制不依赖electron的API,然后我试了下,发现没啥用,win?.maximize()这个方法不生效,看来问题不出在这,还得接着找。
于是我又去找了下原因,发现罪魁祸首是创建窗口时的一个属性——resizable。这里我不希望用户能够拖拽改变软件窗口大小,所以这里设置为fasle。但是在Mac环境下如果resizable为false的情况下,Mac没办法做到全屏,因为Mac的全屏就是resize页面到屏幕的四周,所以上面的方法就没用了。
解决
然后我就在想,可不可以在全屏的时候将resizable设置为true,方法调用完毕以后再设置为false
ipcMain.on('toggle-window-maximize', (event, flag: boolean) => {const win = BrowserWindow.fromWebContents(event.sender);if (!win) return;// 临时打开 resizable,否则 macOS 最大化不生效const wasResizable = win.isResizable();// 如果页面传过来的全屏判断值为trueif (flag) {// 开启resizeableif (!wasResizable) win.setResizable(true);// 这里可以不判断直接调用if (!win.isMaximized()) win.maximize();} else {if (win.isMaximized()) win.unmaximize();// 关闭resizeablesetTimeout(() => {win?.setResizable(false);}, 100);}
});
🚩之所以在调用win.maximize()以后不关闭resizeable是因为如果在调用win.maximize()以后关闭resizeable会导致windows环境下调用win.unmaximize()方法无法回到初始比例的现象,应该是会导致electron的页面基准计算错误出现这个问题