Electron全局搜索框实战:快捷键调起+实时高亮+多窗口支持

在现代化桌面应用中,快速搜索功能是提升用户体验的关键因素之一。本文将详细介绍如何在 Electron 应用中实现一个优雅的全局搜索框,支持快捷键调起、实时搜索和高亮导航。

功能预览

实现效果:

  • 使用 Ctrl+F(Windows/Linux)或 Cmd+F(Mac)快捷键调起搜索框
  • 搜索框悬浮在应用右上角,不占用主内容区域
  • 实时搜索并高亮匹配结果
  • 支持上下导航匹配项
  • 显示当前匹配位置和总匹配数
  • 支持键盘快捷键操作(Enter、Shift+Enter、Esc、方向键)

样式可以自行于search.html内修改,这里只是简单实现了一下

实现步骤

1. 搜索框页面 (search.html)

首先创建搜索框的 HTML 页面,包含输入框、导航按钮和关闭按钮:

html 复制代码
<!doctype html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>搜索</title>
    <style>
      body {
        margin: 0;
        padding: 0;
        overflow: hidden;
        font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
      }
      .search-wrap {
        padding: 10px;
        background: #fff;
        box-shadow: 0 0 10px rgba(0, 0, 0, 0.5);
        border-radius: 4px;
        display: flex;
        align-items: center;
      }
      .input {
        flex: 1;
        width: 200px;
        height: 30px;
        padding-right: 10px;
        border: 0;
        outline: none;
        border-radius: 5px;
      }
      .button {
        display: inline-block;
        background: transparent;
        border: 0;
        cursor: pointer;
      }
      .icon {
        width: 26px;
        height: 26px;
        font-size: 26px;
        margin: 0;
        padding: 0;
        border-radius: 50%;
        position: relative;
        display: inline-block;
        vertical-align: middle;
        line-height: 1;
        flex-shrink: 0;
      }
      .icon:hover {
        background-color: #e5e2e2;
      }
      .icon.next:hover::after {
        border-color: #e5e2e2 transparent transparent transparent;
      }
      .icon.prev:hover::after {
        border-color: transparent transparent #e5e2e2 transparent;
      }
      .prev::before,
      .next::before,
      .prev::after,
      .next::after {
        content: '';
        position: absolute;
        width: 0;
        height: 0;
        top: 50%;
        left: 50%;
        transform: translate(-50%, -50%);
        border-style: solid;
      }
      .prev::before {
        border-width: 0 6px 6px 6px;
        border-color: transparent transparent #000 transparent;
      }
      .prev::after {
        border-width: 0 6px 6px 6px;
        border-color: transparent transparent #fff transparent;
        transform: translate(-50%, calc(-50% + 2px));
      }
      .next::before {
        border-width: 6px 6px 0 6px;
        border-color: #000 transparent transparent transparent;
      }
      .next::after {
        border-width: 6px 6px 0 6px;
        border-color: #fff transparent transparent transparent;
        transform: translate(-50%, calc(-50% - 2px));
      }
      .count {
        display: inline-block;
        font-size: 12px;
        margin-right: 6px;
        flex-shrink: 0;
        max-width: 50px;
      }
    </style>
  </head>
  <body>
    <div class="search-wrap">
      <input id="search" class="input" placeholder="搜索..." />
      <div class="count" id="count">0/0</div>
      <button class="button icon prev" id="prev" title="上一个 (Shift+Enter)"></button>
      <button class="button icon next" id="next" title="下一个 (Enter)"></button>
      <button class="button icon" id="close" title="关闭 (Esc)">&times;</button>
    </div>
    <script>
      window.onload = () => {
        // 移除加载动画(如果有)
        postMessage({ payload: 'removeLoading' }, '*')
        
        // 自动聚焦到搜索框
        document.getElementById('search').focus()

        // 输入事件处理
        document.getElementById('search').oninput = () => {
          const value = document.getElementById('search').value
          if (!value) {
            // 清空搜索
            electronAPI.sendSearchPage(JSON.stringify({ value: '', start: true }))
            document.getElementById('count').innerText = '0/0'
            return
          }
          electronAPI.sendSearchPage(JSON.stringify({ value, start: true }))
        }

        // 上一个匹配项
        document.getElementById('prev').onclick = () => {
          const value = document.getElementById('search').value
          if (!value) return
          electronAPI.sendSearchPage(JSON.stringify({ value, next: false }))
        }

        // 下一个匹配项
        document.getElementById('next').onclick = () => {
          const value = document.getElementById('search').value
          if (!value) return
          electronAPI.sendSearchPage(JSON.stringify({ value, next: true }))
        }

        // 关闭搜索框
        document.getElementById('close').onclick = () => {
          electronAPI.closeSearchPage()
        }

        // 键盘事件处理
        window.addEventListener('keyup', (e) => {
          if (e.key === 'Escape') {
            electronAPI.closeSearchPage()
          } else if (e.key === 'Enter') {
            if(e.shiftKey) {
              document.getElementById('prev').click()
            } else {
              document.getElementById('next').click()
            }
          } else if (e.key === 'ArrowRight') {
            document.getElementById('next').click()
          } else if (e.key === 'ArrowLeft') {
            document.getElementById('prev').click()
          }
        })

        // 监听搜索结果
        setTimeout(() => {
          electronAPI.foundInPage((arg) => {
            document.getElementById('count').innerText = `${arg.activeMatchOrdinal}/${arg.matches}`
          })
        }, 0);
      }
    </script>
  </body>
</html>

2. 预加载脚本 (preload.js)

在预加载脚本中定义与主进程通信的 API:

javascript 复制代码
const { contextBridge, ipcRenderer } = require('electron')

contextBridge.exposeInMainWorld('electronAPI', {
  openSearchPage: (info) => ipcRenderer.send('open-search-page', info),
  sendSearchPage: (info) => ipcRenderer.send('search-page', info),
  closeSearchPage: (info) => ipcRenderer.send('close-search-page', info),
  foundInPage: (callback) => {
    ipcRenderer.on('found-in-page', (event, progress) => {
      callback(progress)
    })
  }
})

3. 主进程搜索功能 (searchView.js)

创建搜索功能的主进程模块:

javascript 复制代码
const { ipcMain, BrowserView } = require('electron')
const path = require('path')

let searchView = null
let searchUrl = path.join(__dirname, '../dist/search.html')

function initCodeWin(options) {
  const { win, NODE_ENV } = options

  // 注册打开搜索页面的IPC监听
  ipcMain.on('open-search-page', (event, arg) => {
    // 如果已经存在搜索视图,则销毁之前的
    if (searchView) {
      closeSearchView(win)
    }
    
    // 创建一个新的BrowserView
    searchView = new BrowserView({
      webPreferences: {
        webviewTag: true,
        devTools: NODE_ENV === 'development',
        contextIsolation: true,
        nodeIntegrationInWorker: false,
        nodeIntegration: false,
        preload: path.join(__dirname, '../preload.js')
      }
    })
    
    updateSearchViewBounds(win)

    // 将搜索视图添加到主窗口中
    win?.addBrowserView(searchView)

    // 加载搜索页面
    searchView.webContents.loadFile(searchUrl)
  })

  // 处理搜索请求
  ipcMain.on('search-page', (event, arg) => {
    const searchObj = JSON.parse(arg)
    
    // 如果搜索内容为空,清除搜索高亮
    if (!searchObj.value) {
      win?.webContents.stopFindInPage('clearSelection')
      return
    }
    
    // 执行搜索
    win?.webContents.findInPage(searchObj.value, {
      findNext: searchObj.start,
      forward: searchObj.next
    })
  })

  // 停止搜索
  ipcMain.on('stop-search', (event, arg) => {
    win?.webContents.stopFindInPage('clearSelection')
  })

  // 关闭搜索页面
  ipcMain.on('close-search-page', (event, arg) => {
    closeSearchView(win)
  })

  // 当主窗口失去焦点时关闭搜索框
  win.on('blur', () => {
    closeSearchView(win)
  })

  // 窗口大小变化时调整搜索框位置
  win.on('resize', () => {
    if (searchView) {
      updateSearchViewBounds(win)
    }
  })

  // 监听搜索结果
  win.webContents.on('found-in-page', (event, arg) => {
    // 向搜索视图发送找到的内容
    searchView?.webContents.send('found-in-page', arg)
  })

  // 关闭搜索视图的函数
  function closeSearchView(win) {
    try {
      win?.webContents.stopFindInPage('clearSelection')
      if (searchView) {
        win?.removeBrowserView(searchView)
        searchView = null
      }
    } catch (error) {
      console.log('关闭搜索视图时出错:', error)
    }
  }

  // 更新搜索视图位置和大小的函数
  function updateSearchViewBounds(win) {
    if (!win || !searchView) return
    
    try {
      const winBounds = win.getBounds()
      const x = winBounds.width - 360 - 60 // 计算新的x坐标
      const y = 30
      searchView.setBounds({ x, y, width: 360, height: 70 })
    } catch (error) {
      console.log('更新搜索视图位置时出错:', error)
    }
  }
}

module.exports = { initCodeWin }

4. 渲染进程调用

在 Vue 组件中注册快捷键:

javascript 复制代码
import { useCombinationKey } from "@/hooks/useKeyboard"

// 注册 Ctrl+F / Cmd+F 快捷键
useCombinationKey("CommandOrControl+f", () => {
  window.electronAPI.openSearchPage()
})

注意:useCombinationKey 是一个自定义的快捷键组合 Hook,可以参考之前的文章Electron/Vue 3快捷键方案:useCombinationKey Hook设计与实现详解了解实现细节。

5. 主窗口初始化

在主窗口创建后初始化搜索功能:

javascript 复制代码
const { app, BrowserWindow } = require('electron')
const { initCodeWin } = require('./searchView')

app.whenReady().then(() => {
  const win = new BrowserWindow({
    width: 1200,
    height: 800,
    webPreferences: {
      preload: path.join(__dirname, 'preload.js'),
      contextIsolation: true,
      nodeIntegration: false
    }
  })

  // 加载应用内容
  win.loadFile('index.html')

  // 初始化搜索功能
  initCodeWin({ win, NODE_ENV: process.env.NODE_ENV })
})

多窗口支持

如果你的应用需要支持多个窗口,每个窗口都需要独立的搜索功能,可以使用以下修改版本:

javascript 复制代码
const { globalShortcut, ipcMain, BrowserView } = require("electron")
const path = require("path")

// 为每个窗口保存搜索视图
const searchViews = new Map()

function initCodeWin(options) {
  const { win, NODE_ENV } = options
  
  // 当主窗口获得焦点时
  win.on("focus", () => {
    // 注册全局快捷键,打开搜索视图
    globalShortcut.register("CommandOrControl+F", function () {
      // 如果已经存在搜索视图,则销毁之前的
      if (searchViews.has(win)) {
        closeSearchView(win)
      }
      
      // 创建一个新的BrowserView
      const searchView = new BrowserView({
        webPreferences: {
          webviewTag: true,
          devTools: NODE_ENV === 'development',
          contextIsolation: true,
          nodeIntegrationInWorker: false,
          nodeIntegration: false,
          preload: path.join(__dirname, "../preload.js")
        }
      })
      
      // 保存搜索视图
      searchViews.set(win, searchView)
      updateSearchViewBounds(win)

      // 将搜索视图添加到主窗口中
      win?.addBrowserView(searchView)

      // 加载搜索页面
      searchView.webContents.loadFile(path.join(__dirname, "./search.html"))
    })

    // 处理搜索请求
    ipcMain.on("search-page", (event, arg) => {
      const searchObj = JSON.parse(arg)
      win?.webContents.findInPage(searchObj.value, {
        findNext: searchObj.start,
        forward: searchObj.next
      })
    })

    // 关闭搜索页面
    ipcMain.on("close-search-page", (event, arg) => {
      closeSearchView(win)
    })
  })
  
  // 当主窗口失去焦点时
  win.on("blur", () => {
    closeSearchView(win)
    // 取消注册全局快捷键
    globalShortcut.unregister("CommandOrControl+F")
  })

  // 窗口关闭时清理资源
  win.on("closed", () => {
    closeSearchView(win)
    searchViews.delete(win)
  })

  // 窗口大小变化时调整搜索框位置
  win.on("resize", () => {
    if (searchViews.has(win)) {
      updateSearchViewBounds(win)
    }
  })

  // 监听搜索结果
  win.webContents.on("found-in-page", (event, arg) => {
    // 向搜索视图发送找到的内容
    const searchView = searchViews.get(win)
    searchView?.webContents.send("found-in-page", arg)
  })

  // 关闭搜索视图的函数
  function closeSearchView(win) {
    try {
      win?.webContents.stopFindInPage("clearSelection")
      const searchView = searchViews.get(win)
      if (searchView) {
        win?.removeBrowserView(searchView)
        searchViews.delete(win)
      }
    } catch (error) {
      console.log("关闭搜索视图时出错:", error)
    }
  }

  // 更新搜索视图位置和大小的函数
  function updateSearchViewBounds(win) {
    const searchView = searchViews.get(win)
    if (!win || !searchView) return
    
    try {
      const winBounds = win.getBounds()
      const x = winBounds.width - 360 - 60
      const y = 30
      searchView.setBounds({ x, y, width: 360, height: 70 })
    } catch (error) {
      console.log("更新搜索视图位置时出错:", error)
    }
  }
}

module.exports = { initCodeWin }

使用说明

  1. 将上述代码文件放置到合适的位置
  2. 在主进程初始化时调用 initCodeWin
  3. 确保预加载脚本正确配置
  4. 在前端代码中注册快捷键

结语

通过本文的介绍,我们实现了一个功能完整的 Electron 搜索框,支持快捷键调起、实时搜索、结果导航等特性。这个实现既美观又实用,可以大大提升用户的使用体验。

相关推荐
前端大卫2 小时前
Vue3 + Element-Plus 自定义虚拟表格滚动实现方案【附源码】
前端
却尘2 小时前
Next.js 请求最佳实践 - vercel 2026一月发布指南
前端·react.js·next.js
ccnocare2 小时前
浅浅看一下设计模式
前端
Lee川2 小时前
🎬 从标签到屏幕:揭秘现代网页构建与适配之道
前端·面试
Ticnix3 小时前
ECharts初始化、销毁、resize 适配组件封装(含完整封装代码)
前端·echarts
纯爱掌门人3 小时前
终焉轮回里,藏着 AI 与人类的答案
前端·人工智能·aigc
twl3 小时前
OpenClaw 深度技术解析
前端
崔庆才丨静觅3 小时前
比官方便宜一半以上!Grok API 申请及使用
前端
星光不问赶路人3 小时前
vue3使用jsx语法详解
前端·vue.js
天蓝色的鱼鱼3 小时前
shadcn/ui,给你一个真正可控的UI组件库
前端