上次将项目向同事展示了一下,算是electron-vue的第一阶段调研。但是演示的时候却被各种否定,窗体比较大,而且设计不合理,布局方式不合理,等等等。由于在此之前没有做过这种客户端的形式,所以设计也是无从说起。
于是经过一番的讨论,基本上确定了要做一个类似于QQ的客户端的产品,经过一番努力,终于搞得是差不多了,成品图如下:
主要解决的问题
舍弃electron自带的窗体,自定义窗体通过事件监听解决最小化和隐藏托盘的功能无窗体下的拖拽功能实现下面就开始我们的代码之旅。
参考文档 这里是对electron窗口属性的详细说明,可以参考一下。
BrowserWindow是一个EventEmitter。
它创建一个新的BrowserWindow具有原生属性的设置options。其中我们需要注意的是下面这几个重要的属性:
resizable布尔(可选) - 窗口是否可调整大小。默认是true frame布尔(可选) -指定false创建一个无框窗口。默认是true
如果想影藏框架的窗体,可以设置frame:false即可。而且原则上我们的登陆窗口是不允许通过鼠标拖调整窗口的大小,所以需要设置resizable:false。
mainWindow = new BrowserWindow({ height: 350, useContentSize: false, width: 400, resizable: false, frame: false })这样就可以去掉外层窗体,并且不可以进行放大缩小。但是这也导致一个问题,如果你选择不使用electron自带的frame,那么就默认你是放弃了frame的拖拽功能,所以这里需要我们自己去实现拖拽功能。
在开发应用的过程中,想要使得窗口在普通大小可以拖动,经常会设置该css属性:-webkit-app-region: drag; 来实现。
在窗口的最外层,即登录页面的根节点上设置该属性为drag,这样的话就可以实现窗口的拖拽,但是也会产生一个问题,一般会在最外层设置这个属性,那么在窗体的任意位置都可以进行拖拽。那我们的表单怎么办呢?
如果设置在最外层,那当我们的鼠标移动到输入框,按钮等操作事件上时,就会发现,不能获取到焦点,不能输入,也不能点击,这就很麻烦了。
而关于-webkit-app-region的属性设置也可以找到对应的解决办法,那就是在需要事件或者需要输入的地方设置该属性不能拖拽,即-webkit-app-region: no-drag;
于是,我就想到只要给表单一个div或者在表单的最外层设置这个属性为no-drag不就行了么?而事实并非如此,不明白有的表单项却不行,比如按钮,CheckBox,radio等这些,在我这里好像不能用。如果在form表单最外层不起作用,那只能退而求其次,每一项都这么设置。
这么一来,属性设置就显得很冗余了。如果在每一项都能这么设置,那么在行设置会不会起作用呢?
于是尝试了在.ant-row上进行设置,而不是在ant-input上设置。以下就是样式代码:
.login-container { width: 100vw; height: 100vh; font-size: 14px; -webkit-app-region: drag; //无边框下设置窗口可拖拽 .login-top { position: relative; width: 100%; height: 80px; background-color: #179bbb; color: white; .left { float: left; height: 30px; line-height: 30px; padding-left: 10px; } .right { float: right; .window-min, .window-close { width: 30px; height: 30px; line-height: 30px; display: inline-block; text-align: center; -webkit-app-region: no-drag; //事件处可以禁用拖拽区域 } .window-min:hover { background-color: rgb(209, 207, 207); } .window-close:hover { background-color: red; } } .logo { position: absolute; top: 40px; left: 160px; width: 80px; } } .login-form { width: 70vw; height: 150px; position: absolute; left: 15vw; top: 120px; .ant-row { margin-bottom: 0px; -webkit-app-region: no-drag; //事件处可以禁用拖拽区域 } .ant-input-group.ant-input-group-compact > *:first-child { border-right: 1px solid #f0f0f0; } .ant-input { border: none; } .login-button { display: block; width: 100%; } } }在窗口的最外层设置-webkit-app-region: drag;,而在表单中设置-webkit-app-region: no-drag;,这样就可以达到无外层窗体达到自定义的拖拽功能。
缺点:网上好像是说该样式的兼容性不是很好,并不是在window,linxu和macOS中都能够得到很好地支持,所以需要针对不同的系统做样式调整,比如使用最原始的时间监听,即窗口监听鼠标移动事件,获取当前鼠标的位置,从而改变窗口的位置,达到拖拽的目的。
在前一篇文章中讲到如何实现点击关闭的时候从任务栏隐藏到托盘中。而现在弃用了electron自带的窗体frame,就不得不自己去实现相关的功能。
界面的布局我就不详细说了,代码会在最后完全的粘出来,供大家参考。这里只说最小化和关闭时隐藏托盘的实现。
在页面的布局中,主要分为两部分,top顶部,表单。
其中top包含APP名称,最小化图标,关闭图标。而APP名称是左浮动,最小化和关闭图标放到了一个div中并且右浮动,当然你也可以使用flex布局,这里不赘述。
布局实现之后,需要给两个图标添加hover事件,当鼠标移动上去的时候,改变图标的背景色,具体代码:
<div class="login-top"> <div class="left">xxxx客户端</div> <div class="right"> <span class="window-min" @click="windowMin"> <a-icon type="minus" /> </span> <span class="window-close" @click="windowClose"> <a-icon type="close" /> </span> </div> <img src="~@/assets/logo123.png" class="logo" /> </div> .login-top { position: relative; width: 100%; height: 80px; background-color: #179bbb; color: white; .left { float: left; height: 30px; line-height: 30px; padding-left: 10px; } .right { float: right; .window-min, .window-close { width: 30px; height: 30px; line-height: 30px; display: inline-block; text-align: center; -webkit-app-region: no-drag; //事件处可以禁用拖拽区域 } .window-min:hover { background-color: rgb(209, 207, 207); } .window-close:hover { background-color: red; } } .logo { position: absolute; top: 40px; left: 160px; width: 80px; } }我看了一下,QQ的客户端,在登陆的界面右下图的这么个图片,想了想他是怎么卖实现的,用什么css能实现这个功能。最后发现,应该是图片本身是圆的,然后使用了绝对定位的方式,让图片在正中的位置。并且加了阴影。
最小话不能使用electron的功能,只能自己去写,简单的实现应该是给最小化以及关闭隐藏添加监听事件,然后在事件中通过ipcRenderer发送事件给主进程,主进程监听事件,然后将窗口最小化到任务栏或者隐藏到托盘。
login.vue
<span class="window-min" @click="windowMin"> <a-icon type="minus" /> </span> windowMin() { this.$electron.ipcRenderer.send("window-min") },main/index.js
// 监听窗口的关闭 ipcMain.on('window-min', () => { mainWindow.minimize() })login.vue
<span class="window-close" @click="windowClose"> <a-icon type="close" /> </span> windowClose() { this.$electron.ipcRenderer.send("window-close") },main/index.js
// 监听窗口的关闭隐藏到托盘 ipcMain.on('window-close', (event) => { mainWindow.hide(); })完整的代码如下 login.vue
<template> <div id="wrapper"> <login-page></login-page> </div> </template> <script> import LoginPage from "./loginPage/LoginPage"; export default { name: "login", components: { LoginPage }, methods: { open(link) { this.$electron.shell.openExternal(link); }, }, }; </script> <style> @import url("https://fonts.googleapis.com/css?family=Source+Sans+Pro"); * { box-sizing: border-box; margin: 0; padding: 0; } body { font-family: "Source Sans Pro", sans-serif; } #wrapper { height: 100vh; width: 100vw; } </style>main/index.js
import { app, BrowserWindow, ipcMain, Tray, Menu } from 'electron' import log from 'electron-log' const cp = require('child_process') const path = require('path'); // 仅用于开发环境日志输出,开发环境下请注销所有日志输出 log.transports.file.file = "D://agent.log" let startMsg let child let tray = null /** * Set `__static` path to static files in production * https://simulatedgreg.gitbooks.io/electron-vue/content/en/using-static-assets.html */ if (process.env.NODE_ENV !== 'development') { global.__static = require('path').join(__dirname, '/static').replace(/\\/g, '\\\\') } let mainWindow const winURL = process.env.NODE_ENV === 'development' ? `http://localhost:9080` : `file://${__dirname}/index.html` // 创建窗体 function createWindow() { /** * Initial window options */ mainWindow = new BrowserWindow({ height: 350, useContentSize: false, width: 400, resizable: false, frame: false }) // mainWindow.setMenu(null) mainWindow.loadURL(winURL) mainWindow.webContents.openDevTools() mainWindow.on('closed', () => { mainWindow = null }) // 点击关闭时,并没有真正关闭,而是隐藏窗口 mainWindow.on('close', (event) => { mainWindow.hide(); mainWindow.setSkipTaskbar(true); event.preventDefault(); }); mainWindow.on('show', () => { tray.setHighlightMode('always') }) mainWindow.on('hide', () => { tray.setHighlightMode('never') }) //创建系统通知区菜单 tray = new Tray(path.join(__dirname, '../renderer/assets/icon.ico')) //定制退出操作 const contextMenu = Menu.buildFromTemplate([ { label: '退出', click: () => { mainWindow.destroy() } }, ]) tray.setToolTip('蔷薇灵动') tray.setContextMenu(contextMenu) tray.on('click', () => { mainWindow.show() mainWindow.setSkipTaskbar(false) }) } app.on('ready', () => { createWindow(); startExeFile(); }) app.on('window-all-closed', () => { if (process.platform !== 'darwin') { app.quit() } }) app.on('activate', () => { if (mainWindow === null) { createWindow() } }) // 监听窗口的关闭 ipcMain.on('window-min', () => { mainWindow.minimize() }) // 监听窗口的关闭隐藏到托盘 ipcMain.on('window-close', (event) => { mainWindow.hide(); })