Electron-Vite (四)优雅的管理子窗口

160 阅读4分钟

 在Electron应用开发中,我们有时候会接到这样的需求:单独开发一个子窗口,这个子窗口要独立于主窗口,虽然有点类似于网页端里面的弹窗,但是从功能方面来说,这个独立的子窗口要远胜于弹窗

那接下来,我们就来看怎么去创建子窗口以及管理这个子窗口

在这里我们需要先引入一个外部依赖 vue-router

官网地址:入门 | Vue Router

一、创建子窗口父级vue

首先我们在渲染进程里面创建我们的子窗口页面 ChildWin.vue

ChildWin.vue 的内容如下

<template>
  <div ref="childWinRef" class="child-win">
    <div class="title">
      <span class="title-text">{{ title }}</span>
      <div class="electron-title-icon">
        <div class="icon-item" @click="handle('hide')">
          <svg-icon name="funHide" size="0.8em"></svg-icon>
        </div>
        <div class="icon-close" @click="handle('close')">
          <svg-icon name="funClose" size="0.8em" hover-color="#fff"></svg-icon>
        </div>
      </div>
    </div>

    <template v-if="key === 'test'">
      <test></test>
    </template>
  </div>
</template>

<script setup lang="ts">
import { onMounted, ref } from 'vue'
import Test from './components/Test.vue'

const winId = ref<number | null>(null)
const key = ref<string>('')
const title = ref<string>('子窗口标题')
const data = ref<any>(null)

onMounted(() => {
  //监听主进程传过来的初始化数据
  window.electron.ipcRenderer.on('initData', (event, params) => {
    winId.value = params.id
    title.value = params.title
    data.value = params?.data
    key.value = params.key
  })
})

const handle = (params: string): void => {
  const data = {
    id: winId.value,
    key: key.value,
    type: params
  }
  window.electron.ipcRenderer.send('childWinSize', data)
}

const childWinRef = ref<HTMLDivElement | null>(null)
</script>

<style scoped lang="scss">
.child-win {
  width: calc(100% - 2px);
  height: calc(100vh - 2px);
  position: relative;
  font-size: 14px;
  --btnHoverColor: #eee;

  .title {
    width: 100%;
    height: 30px;
    background-color: #eee;
    -webkit-app-region: drag; /* 允许拖拽 */
    display: flex;
    justify-content: space-between;
    align-items: center;

    .title-text {
      margin-left: 10px;
    }
    .electron-title-icon {
      display: flex;
      align-items: center;
      gap: 5px;
      -webkit-app-region: no-drag; /* 禁止拖拽 */
      .icon-item {
        cursor: pointer;
        width: 40px;
        height: 30px;
        cursor: pointer;
        &:hover {
          background-color: var(--btnHoverColor);
        }
      }
      .icon-close {
        cursor: pointer;
        height: 30px;
        display: flex;
        align-items: center;
        justify-content: center;
        width: 30px;
        &:hover {
          background-color: rgb(243, 88, 88);
        }
      }
    }
  }
}
</style>

ChildWin.vue是所有子窗口的父级页面,可以很好去管理所有的子窗口

如果没有ChildWin.vue,那假如我们现在有5个窗口进行管理,我想要改变他们的标题栏样式,那我们可能就需要一个一个去修改,这样的维护过程对我们来说极其繁琐

二、创建test窗口页面

接下来我们来写一个子窗口的页面 Test.vue

文件结构如下

​编辑

内容如下

<template>
  <div>
    <span>我是子窗口Test页面</span>
  </div>
</template>

<script setup lang="ts"></script>

<style scoped></style>

然后我们将这个页面挂载到 ChildWin.vue 里面

三、创建router路由文件

内容如下

import App from '@renderer/App.vue'
import { createRouter, createWebHistory } from 'vue-router'
import ChildWin from '@renderer/components/childWindows/ChildWin.vue'

const routes = [
  {
    path: '/',
    name: 'Home',
    component: App
  },
  {
    path: '/childWindows',
    name: 'childWindows',
    component: ChildWin
  }
]

export const router = createRouter({
  history: createWebHistory(),
  routes
})

将这个路由挂载到main.ts里面

main.ts修改内容如下

import { createApp } from 'vue'
import App from './App.vue'
import { router } from './router'

createApp(App).use(router).mount('#app')

三、创建控制器

我们回到主进程中,创建一个 windowsId.ts 的文件,文件结构如下,外层需要有一个store文件夹

文件内容如下

import { BrowserWindow, shell } from 'electron'
import { join } from 'path'

export const allChildWindowsId = {
  test: null as number | null //test窗口id
}

//设置窗口id
export const setChildWindowsId = (id: number | null, key: string): boolean => {
  allChildWindowsId[key] = id
  return true
}

//查询窗口id
export const getChildWindowsId = (key: string): number => {
  return allChildWindowsId[key]
}

//开启一个全新的子窗口
export const openChildWindow = (data: any) => {
  const childWindow = new BrowserWindow({
    width: data?.width || 800,
    height: data?.height || 600,
    title: data.title,
    show: false,
    autoHideMenuBar: true,
    titleBarStyle: 'hidden',
    webPreferences: {
      preload: join(__dirname, '../preload/index.js'),
      sandbox: false
    }
  })

  childWindow.on('ready-to-show', () => {
    childWindow?.show()

    //发送一些初始数据
    childWindow.webContents.send('initData', {
      id: childWindow.id,
      key: data.key,
      title: data.title,
      data: data?.data
    })
  })

  childWindow.webContents.on('did-fail-load', (event, code, desc) => {
    console.error('Failed to load:', desc)
  })
  childWindow.webContents.setWindowOpenHandler((details) => {
    shell.openExternal(details.url)
    return { action: 'deny' }
  })

  //监听关闭
  childWindow.on('closed', () => {
    setChildWindowsId(null, data.key)
  })

  if (process.env['ELECTRON_RENDERER_URL']) {
    childWindow.loadURL(`${process.env['ELECTRON_RENDERER_URL']}/childWindows`)
  } else {
    childWindow.loadFile(join(__dirname, '../renderer/index.html'), {
      hash: '/childWindows'
    })
  }

  setChildWindowsId(childWindow.id, data.key)
}

然后回到 monitorEvent.ts 文件里面,修改内容如下,在这里面新增加一个监听事件

import { BrowserWindow, ipcMain } from 'electron'
import { mainWindow } from '.'
import { getChildWindowsId, openChildWindow } from './store/windowsId'

export const monitorEvent = (): void => {
  ipcMain.on('toMain', (e, data) => {
    console.log(data)
  })

  ipcMain.on('winAction', (e, data) => {
    const type = data.type
    if (type === 'hide') {
      //最小化窗口
      mainWindow.minimize()
    } else if (type === 'min') {
      //取消最大化
      mainWindow.unmaximize()
    } else if (type === 'max') {
      //最大化
      mainWindow.maximize()
    } else if (type === 'close') {
      //关闭窗口
      mainWindow.close()
    }
  })

  //打开一个子窗口
  ipcMain.on('openChild', (e, data) => {
    //判断是否已经打开过该窗口
    const winId = getChildWindowsId(data.key)
    if (winId !== null) {
      //窗口已经存在,则发送一些初始数据
      const childWindow = BrowserWindow.fromId(winId)
      if (childWindow === null) {
        return
      }

      childWindow.webContents.send('initData', {
        id: childWindow.id,
        key: data.key,
        title: data.title,
        data: data?.data
      })

      //将窗口显示出来
      childWindow.show()
    } else {
      openChildWindow(data)
    }
  })
}

四、开启一个子窗口

我们需要再次创建一个Home.vue的文件,App.vue作为我们的总入口文件

​编辑

Home.vue文件内容如下

<template>
  <div>
    <button @click="openTest">开启test</button>
  </div>
</template>

<script setup lang="ts">
const openTest = () => {
  const obj = {} as any
  obj.key = 'test'
  obj.title = '测试窗口'
  window.electron.ipcRenderer.send('openChild', obj)
}
</script>

<style scoped lang="scss"></style>

我们回到我们的App.vue文件里面

进行如下的修改

<template>
  <div>
    <button @click="openTest">开启test</button>
  </div>
</template>

<script setup lang="ts">
const openTest = () => {
  const obj = {} as any
  obj.key = 'test'
  obj.title = '测试窗口'
  window.electron.ipcRenderer.send('openChild', obj)
}
</script>

<style scoped lang="scss"></style>

然后我们再次返回到我们的router里面的index.ts文件里面

修改内容如下

import { createRouter, createWebHistory } from 'vue-router'
import ChildWin from '@renderer/components/childWindows/ChildWin.vue'
import Home from '@renderer/page/Home.vue'

const routes = [
  {
    path: '/',
    name: 'Home',
    component: Home
  },
  {
    path: '/childWindows',
    name: 'childWindows',
    component: ChildWin
  }
]

export const router = createRouter({
  history: createWebHistory(),
  routes
})

五、验证效果

运行我们的项目

点击开启test按钮

可以看到我们的test子窗口是独立于主窗口的

并且后面想要对这个主窗口进行修改,或者关闭显示隐藏,都可以在主进程里面做到