WPF 應用遷移到 Electron 框架過程記錄

he55發表於2024-03-05

前一段時間我用 WPF 開發了一個檢視 emoji 表情的小工具 https://github.com/he55/EmojiViewer ,由於最近我使用 macOS 系統比較多,我想能在 macOS 系統上也能使用這個工具。於是我嘗試將 WPF 應用遷移到 Electron 框架,感覺這個框架很強大,在這裡記錄一下應用遷移的過程。

安裝 Electron 環境

  • 安裝 nodejs。到官網 https://nodejs.org/en 下載最新的 nodejs,然後安裝
  • 開啟命令列輸入 git clone https://github.com/electron/electron-quick-start.git 命令克隆 Electron 模板專案,使用模板可以快速搭建應用。
  • 然後使用 cd electron-quick-start 目錄進入到目錄,接著執行 npm install 命令還原專案。
  • 使用 vscode 開啟資料夾,專案檔案如下

編寫程式碼

  • Electron 分為主程序和渲染程序,對檔案、系統和視窗的操作需要在主執行緒,介面渲染在渲染程序。建立視窗屬於主程序的工作,需要到 main.js 檔案編寫程式碼。建立視窗使用 BrowserWindow 物件,widthheight 分別設定視窗寬度和高度,autoHideMenuBar 設定是否隱藏選單,最後使用 loadFile 載入頁面檔案並顯示視窗。
function createWindow() {
  const mainWindow = new BrowserWindow({
    width: 915,
    height: 560,
    autoHideMenuBar: true,
    webPreferences: {
      preload: path.join(__dirname, 'preload.js')
    }
  })

  mainWindow.loadFile('index.html')
}
  • 監聽 whenReady 事件,等待應用初始化完成後顯示視窗
app.whenReady().then(() => {
  createWindow()

  app.on('activate', function () {
    if (BrowserWindow.getAllWindows().length === 0) createWindow()
  })
})
  • 修改 index.html 檔案,介面部分使用了 vue 進行渲染
<!DOCTYPE html>
<html>

<head>
  <meta charset="UTF-8">
  <script src="vue.global.js"></script>
  <link href="./styles.css" rel="stylesheet">
  <title>EmojiViewer</title>
</head>

<body>
  <div id="app" class="container">
    <ul class="left">
      <li v-for="(item, key) in categories" :class="{active: item.isActive}" @click="catetoryItemClick(item)">{{ key }}</li>
    </ul>
    <ul class="main" ref="mainElement">
      <li v-for="emoji in emojis" :class="{active: emoji.isActive}" @click="emojiItemClick(emoji)">
        <img :src="emoji.previewImage" alt="">
        <p>{{emoji.name}}</p>
      </li>
    </ul>
    <div class="right">
      <img :src="selectedEmoji.previewImage">
      <p>{{ selectedEmoji.name }}</p>
      <button @click="copyEmoji(selectedEmoji)" type="button">Copy Emoji</button>
      <button @click="copyImage(selectedEmoji)" type="button">Copy Image</button>
      <button @click="openFile(selectedEmoji)" type="button">Open File</button>
    </div>
  </div>

  <script src="./renderer.js"></script>
</body>

</html>
  • renderer.js 檔案中編寫頁面處理程式碼
window.addEventListener('DOMContentLoaded', async () => {
    const { createApp, ref, onMounted } = Vue
    let emojiData = await ipc.getData()

    createApp({
        setup() {
            const mainElement = ref(null)

            const categories = ref(emojiData)
            const emojis = ref([])
            const selectedEmoji = ref({})

            function copyEmoji(emoji) {
                ipc.ipc('writeText', emoji.metadata.glyph)
            }
            function copyImage(emoji) {
                ipc.ipc('writeImage', emoji.previewImage)
            }
            function openFile(emoji) {
                ipc.ipc('showItemInFolder', emoji.previewImage)
            }

            let lastSelectedEmojis
            function catetoryItemClick(items) {
                if (lastSelectedEmojis) {
                    lastSelectedEmojis.isActive = false
                }

                items.isActive = true
                lastSelectedEmojis = items

                // const main = document.querySelector('.main')
                mainElement.value.scrollTop = 0
                emojis.value = items
            }

            function emojiItemClick(emoji) {
                if (selectedEmoji.value) {
                    selectedEmoji.value.isActive = false
                }

                emoji.isActive = true
                selectedEmoji.value = emoji
            }

            onMounted(() => {
                catetoryItemClick(emojiData['Activities'])
                emojiItemClick(emojiData['Activities'][0])
            })

            return {
                mainElement,
                categories,
                emojis,
                selectedEmoji,
                catetoryItemClick,
                emojiItemClick,
                copyEmoji,
                copyImage,
                openFile,
            }
        }
    }).mount('#app')
})
  • 讀取檔案,node 提供了檔案操作相關的 api 可以很方便的操作檔案系統。
function loadData(assetPath) {
  const dirs = fs.readdirSync(assetPath)
  const data = []
  const groupData = {}
  for (const dir of dirs) {
    const fullPath = path.resolve(assetPath, dir)
    const metadata = require(path.resolve(fullPath, 'metadata.json'))
    let previewImage

    let imagePaths = [path.resolve(fullPath, '3D'), path.resolve(fullPath, 'Default', '3D')]
    for (const imagePath of imagePaths) {
      if (fs.existsSync(imagePath)) {
        let files = fs.readdirSync(imagePath)
        if (files.length === 0)
          return
        previewImage = path.resolve(imagePath, files[0])
      }
    }

    const { unicode, group } = metadata
    const obj = {
      metadata,
      id: unicode,
      name: dir,
      previewImage,
    }
    data.push(obj)

    if (!groupData[group])
      groupData[group] = []
    groupData[group].push(obj)
  }
  return groupData
}

完整程式碼(WPF 版本) https://github.com/he55/EmojiViewer
完整程式碼(vue 版本) https://github.com/he55/web-learn/tree/main/9.electron-emoji-viewer(vue)
完整程式碼(js 原生版本) https://github.com/he55/web-learn/tree/main/6.electron-emoji-viewer

相關文章