春分-electron自定义titlebar

768 阅读4分钟

春分不仅意味着昼长夜短,更象征着万物复苏。它预示着生机勃勃的季节即将到来,草木欣欣向荣,生长愈发旺盛

惊蛰-electron里面优雅地发起请求已经定制好了请求方式

接下来我们讲讲如何自定义electron的titlebar(兼容win和mac版本)

看看原始的的titlebar:

可以发现不管是mac还是win上面都会有多处一条特殊的割裂的区域显示了title和操作区域,其实是很丑的

macos: image.png

win:

image.png

改造:方案一: 利用api自定义

  1. 我们先改动主进程文件:src/main/index.ts文件,改动createMainWindow方法
   //改动createMainWindow方法,里面增加
   // macos是不支持titleBarOverlay的自定义的
   titleBarStyle: 'hidden',
    ...(process.platform !== 'darwin'
      ? { titleBarOverlay: { color: '#000', height: 35, symbolColor: '#000' } }
      : {}),
     
     //完整是这样:
  mainWindow = new BrowserWindow({
    width: 1100,
    height: 700,
    minWidth: 1100,
    minHeight: 700,
    show: false,
    autoHideMenuBar: true,
    titleBarStyle: 'hidden',
    ...(process.platform !== 'darwin'
      ? { titleBarOverlay: { color: '#eaf4ff', height: 35, symbolColor: '#000' } }
      : {}),
    webPreferences: {
      preload: join(__dirname, '../preload/index.js'),
      sandbox: false
    }
  })

2.当然我们还要改动渲染进程renderer/src/layout/index

注意高度减少35 是需要给titlebar留高度的,titleBarOverlay里面设置了35px

   // 样式增加
   .layout {
      display: flex;
      height: calc(100vh - 35px); // 改动这个
      margin-top: 32px; // 增加这个
      display: flex;

macos效果: image.png

win效果:

image.png

问题所在:macos是无法定制背景颜色跟我们左侧导航颜色一致的(如果是白色是没问题的):参考文档

那么这种其实不适合我们左侧有颜色的,不能统一颜色,其实是不太理想的

改造: 方案二: 用ui完全自定义

进行方案二之前先要撤回方案一改动的代码;我们其实可以完全用渲染进程的ui自定义titilebar,只需要主进程提供类关闭、缩小、放大、方法等等的功能就好了

  1. 改造src/main/index.ts文件,改动createMainWindow
// 改动createMainWindow方法

function createMainWindow(): void {
  mainWindow = new BrowserWindow({
    width: 1100,
    height: 700,
    minWidth: 1100,
    minHeight: 700,
    show: false,
    autoHideMenuBar: true,
    titleBarStyle: 'hidden',    //增加了这个
    frame: process.platform === 'darwin', // 增加了这个,macos显示原生的titlebar
    webPreferences: {
      preload: join(__dirname, '../preload/index.js'),
      sandbox: false
    }
  })

  mainWindow.on('ready-to-show', () => {
    mainWindow.show()
  })

  mainWindow.webContents.setWindowOpenHandler((details) => {
    shell.openExternal(details.url)
    return { action: 'deny' }
  })

  if (is.dev && process.env['ELECTRON_RENDERER_URL']) {
    mainWindow.loadURL(process.env['ELECTRON_RENDERER_URL'])
  } else {
    mainWindow.loadFile(join(__dirname, '../renderer/index.html'))
  }
  // 方便调式
  // is.dev && mainWindow.webContents.openDevTools()

  mainWindow.on('closed', () => {
    mainWindow = null
  })
  
  //增加了这3个ipcMain
  ipcMain.on('window-minimize', () => mainWindow?.minimize()) 
 
  ipcMain.on('window-close', () => mainWindow?.close())
  
  ipcMain.on('window-maximize', () => {
    if (mainWindow) {
      if (mainWindow.isMaximized()) {
        mainWindow.unmaximize() // 如果已经最大化,则恢复
      } else {
        mainWindow.maximize() // 否则最大化窗口
      }
    }
  })
}
 
  1. 改动src/preload/index.ts

传递process.platform让渲染进程知道是macos或者window

   // 传递process.platform让渲染进程知道是macos或者window
   
import { contextBridge } from "electron"
import { electronAPI } from "@electron-toolkit/preload"

// Custom APIs for renderer
const api = {}

const extendedElectronAPI = {
  ...electronAPI,
  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", extendedElectronAPI)
    contextBridge.exposeInMainWorld("api", api)
  } catch (error) {
    console.error(error)
  }
} else {
  // @ts-ignore (define in dts)
  window.electron = extendedElectronAPI
  // @ts-ignore (define in dts)
  window.api = api
}

  1. 改动src/preload/index.d.ts

声明platform


import { ElectronAPI } from "@electron-toolkit/preload"

declare global {
  // 扩展 ElectronAPI 接口
  interface IElectronAPI extends ElectronAPI {
    platform: string  // 增加这个
  }

  interface Window {
    electron: IElectronAPI
    api: unknown
  }
}

  
  1. 增加自定义titlebar

在renderer/src/ 增加titlebar文件夹

 //titlebar/index.vue
<!-- 定制不同系统的头部样式 -->
<template>
  <div class="titlebar">
    <!-- macOS 样式 -->
    <div v-if="isMac" class="macos">
      <el-icon :size="16" class="action"> <Setting /></el-icon>
    </div>
    <!-- Windows 样式 -->
    <div v-else class="win">
      <div class="title"></div>
      <div class="controls">
        <ElSpace size="small">
          <el-icon class="action"><Setting /></el-icon>
          <el-icon :size="16" class="action" @click="handleMinimize"> <Minus /></el-icon>
          <el-icon :size="16" class="action" @click="handleMaximize"><FullScreen /></el-icon>
          <el-icon :size="16" class="action" @click="handleClose"> <Close /></el-icon>
        </ElSpace>
      </div>
    </div>
  </div>
</template>

<script setup lang="ts">
import { computed } from 'vue'

import { ElSpace, ElIcon } from 'element-plus'
import { Minus, Close, Bell, FullScreen, Setting } from '@element-plus/icons-vue'

// 条件判断
const isMac = computed(() => window.electron.process.platform === 'darwin')

// 方法定义
const handleMinimize = () => {
  window.electron.ipcRenderer.send('window-minimize')
}

const handleClose = () => {
  window.electron.ipcRenderer.send('window-close')
}

const handleMaximize = () => {
  window.electron.ipcRenderer.send('window-maximize')
 
}
</script>

<style lang="scss" scoped>
.titlebar {
  .action {
    background: transparent;
    border: none;
    color: #333;
    width: 20px;
    height: 32px;
    cursor: pointer;
  }
  .win {
    height: 32px;
    background-color: #eaf4ff;
    color: white;
    display: flex;
    align-items: center;
    justify-content: space-between;
    padding: 0 10px 0 10px;
    -webkit-app-region: drag; /* 允许拖动窗口 */
    .controls {
      display: flex;
      -webkit-app-region: no-drag; /* 不允许拖动窗口 */
    }
  }

  .macos {
    height: 32px;
    background-color: #eaf4ff;
    color: white;
    display: flex;
    align-items: center;
    justify-content: end;
    padding: 0 10px 0 10px;
    -webkit-app-region: drag; /* 允许拖动窗口 */
  }
}
</style>


  1. 改造渲染进程的renderer/src/layout/index.vue

改变布局,给自定义titlebar留出位置

  <template>
  <div class="layout">
    <TitleBar />
    <div class="body">
      <div class="left-bar">
        <div class="nav-bar">
          <div class="menu">
            <div class="nav-item">
              <el-icon style="padding-right: 6px"><Monitor /></el-icon>主页
            </div>
          </div>
          <div class="system">
            <div class="nav-item" @click="handleLogout">
              <el-icon style="padding-right: 6px"><Monitor /></el-icon>退登
            </div>
          </div>
        </div>
      </div>
      <div class="content">
        <router-view></router-view>
      </div>
    </div>
  </div>
</template>

<script lang="ts" setup>
import { useRouter } from 'vue-router'
import { Monitor } from '@element-plus/icons-vue'
import { ElIcon } from 'element-plus'
import TitleBar from '@renderer/titlebar/index.vue'
const router = useRouter()
const handleLogout = async () => {
  const res = await window.electron.ipcRenderer.invoke('logout')
  if (res) {
    router.push('/login')
  }
}
</script>

<style scoped lang="scss">
.layout {
  display: flex;
  flex-direction: column;
  .body {
    height: calc(100vh - 35px);
    display: flex;
    .left-bar {
      height: 100%;
      flex-shrink: 0;
      width: 120px;
      background-color: #eaf4ff;
      display: flex;
      align-items: center;
      flex-direction: column;

      .menu {
        display: flex;
        justify-content: space-between;
        flex-direction: column;
      }
      .nav-bar {
        flex: 1;
        width: 100px;
        display: flex;
        flex-direction: column;
        justify-content: space-between;
        align-items: center;
        .nav-item {
          margin-bottom: 8px;
          height: 30px;
          width: 100px;
          display: flex;
          font-size: 14px;
          align-items: center;
          justify-content: center;
          cursor: pointer;
          border-radius: 5px;
          color: rgba(0, 0, 0, 0.6);
          &:hover {
            background-color: #fff;
            font-weight: bold;
            color: #000;
          }
        }
        .active {
          background-color: #fff;
          font-weight: bolder;
          color: #000;
        }
      }
    }
    .content {
      flex: 1;
    }
  }
}
</style>

重启应用,效果如下:

macos:

image.png

win:

image.png

好了,这样自定义的titlebar是跟左侧完全融入的了,且兼容多个系统了

到此,自定义titlebar就基本完成了!

最后源码地址:源码