把 PyQt6 字帖工具迁到 Electron:Canvas、OCR 与跨平台打包的关键决策

2026-10-03 35 预计阅读时间: 1 分钟
来源: oschina.net AI 摘要 Original link

Disclaimer: This article is an AI-assisted summary. Read it together with the original source when precision matters. The summary may omit context, version differences, or edge cases and is not official documentation.

预计阅读时间:11 分钟

将一款已经能用的桌面软件从 PyQt6 v1.1.1 重构到 Electron v2.0.x,难点并不在于把 Python 界面逐页翻译成 HTML。真正需要重新设计的是渲染、文件格式、OCR 生命周期和发布链路。以英语衡水体字帖生成器为例,它既要保留包括描红在内的多种生成模式,又要同时面向 Windows、macOS 和 Linux,因此迁移更像一次产品架构升级,而不是简单换壳。

不要逐个控件翻译,而要重新划分边界

PyQt6 应用通常把窗口、业务状态、文件读写和绘制逻辑放在同一个 Python 进程中。Electron 则天然分成主进程、预加载脚本和渲染进程。如果仍然让页面直接访问文件系统、随意调用 Node.js API,应用虽然能跑,却会失去清晰的安全边界。

可以把字帖工具拆成三层:

  • 主进程:窗口创建、打开与保存对话框、打印、应用生命周期。
  • 预加载脚本:通过有限的 IPC 接口暴露文件导入、导出等能力。
  • 渲染进程:表单、模式切换、Canvas 预览、OCR 结果编辑。

创建窗口时应默认开启上下文隔离,避免为了开发方便而启用完整的 Node.js 能力:

// src/main/index.js
import { app, BrowserWindow } from 'electron'
import { join } from 'node:path'

function createWindow() {
  const win = new BrowserWindow({
    width: 1200,
    height: 820,
    webPreferences: {
      preload: join(__dirname, '../preload/index.js'),
      contextIsolation: true,
      sandbox: true,
      nodeIntegration: false
    }
  })

  if (process.env.ELECTRON_RENDERER_URL) {
    win.loadURL(process.env.ELECTRON_RENDERER_URL)
  } else {
    win.loadFile(join(__dirname, '../renderer/index.html'))
  }
}

app.whenReady().then(createWindow)

目录名称需要根据所用 electron-vite 模板调整。关键不在路径本身,而是不要把 fs、shell 或任意命令执行能力直接挂到页面上。文件保存可以设计成 saveWorkbook(data) 这样的窄接口,并在主进程中校验路径与数据结构。

Canvas 不是画布控件的平替

PyQt 的绘图坐标、字体度量和打印体系与浏览器并不相同。迁移到 Canvas 2D 后,最常见的问题包括预览发虚、导出尺寸变化、字体尚未加载就开始测量,以及高 DPI 屏幕上的线条错位。

一个稳妥的做法是把“纸张逻辑尺寸”和“设备像素尺寸”分开。下面的模块可以直接放进渲染进程,再根据具体字帖规则调整行距、基线和字体:

// src/renderer/src/worksheet.js
export async function renderWorksheet(canvas, options = {}) {
  const {
    text = 'Practice makes progress.',
    fontFamily = 'Hengshui, Arial, sans-serif',
    fontSize = 42,
    rowHeight = 120
  } = options

  await document.fonts.load(`${fontSize}px ${fontFamily}`)

  // 逻辑画布接近 A4 比例;正式导出时可改为目标 DPI 对应的尺寸。
  const width = 1240
  const height = 1754
  const ratio = window.devicePixelRatio || 1

  canvas.width = Math.round(width * ratio)
  canvas.height = Math.round(height * ratio)
  canvas.style.width = `${width}px`
  canvas.style.height = `${height}px`

  const ctx = canvas.getContext('2d')
  ctx.setTransform(ratio, 0, 0, ratio, 0, 0)
  ctx.fillStyle = '#ffffff'
  ctx.fillRect(0, 0, width, height)

  ctx.font = `${fontSize}px ${fontFamily}`
  ctx.textBaseline = 'alphabetic'

  for (let y = 120; y < height - 80; y += rowHeight) {
    ctx.strokeStyle = '#d8d8d8'
    ctx.lineWidth = 1

    for (let offset = -42; offset <= 42; offset += 28) {
      ctx.beginPath()
      ctx.moveTo(70, y + offset)
      ctx.lineTo(width - 70, y + offset)
      ctx.stroke()
    }

    // 描红示例:降低透明度;其他模式可以替换这一层的绘制策略。
    ctx.fillStyle = 'rgba(70, 70, 70, 0.22)'
    ctx.fillText(text, 90, y + 14)
  }
}

调用方式如下:

import { renderWorksheet } from './worksheet.js'

const canvas = document.querySelector('#worksheet')
await renderWorksheet(canvas, {
  text: 'Never stop learning.',
  fontSize: 44
})

如果需要打印级导出,不要简单截取页面截图。应使用固定输出分辨率重新绘制,例如按 A4、300 DPI 生成约 2480 × 3508 像素的 Canvas。与此同时,要确认字体授权允许随应用分发或嵌入输出文件。

多种生成模式也不必复制整套绘图代码。可以把纸张网格、文字布局和模式效果拆成独立步骤:描红模式改变透明度,临摹或空白练习模式替换文字层,排版与分页逻辑继续复用。这样后续修正基线时只需改一处。

OCR 和旧数据都要设置迁移边界

tesseract.js 让 OCR 留在本地完成,适合不希望把练习内容上传服务器的桌面工具。不过 OCR 是耗时任务,不应在每次点击时反复创建 worker。更合理的方式是复用实例,并给界面增加进度、取消和人工校对入口。

下面是一个可改造的最小封装,API 细节需要与项目锁定的 tesseract.js 主版本核对:

// src/renderer/src/ocr.js
import { createWorker } from 'tesseract.js'

let workerPromise

function getWorker() {
  if (!workerPromise) {
    workerPromise = createWorker('eng')
  }
  return workerPromise
}

export async function recognizeEnglish(image) {
  const worker = await getWorker()
  const result = await worker.recognize(image)
  return result.data.text.trim()
}

export async function closeOcr() {
  if (!workerPromise) return
  const worker = await workerPromise
  await worker.terminate()
  workerPromise = undefined
}

OCR 输出不能直接成为最终字帖内容。倾斜照片、手写体、连字符和标点都会影响识别结果,比较安全的流程是“导入图片 → OCR → 文本编辑确认 → 生成字帖”。应用打包后还要专门验证 worker、WASM 和语言数据能否被正确加载,不能只验证开发服务器环境。

旧版 PyQt6 项目若用 pickle 保存设置或模板,Electron 无法自然读取。更重要的是,pickle 不是安全的跨语言交换格式,加载恶意文件可能执行代码。推荐在仍能运行旧版 Python 环境时,将可信文件一次性转换成带版本号的 JSON:

# migrate_pickle.py
import argparse
import json
import pickle
from pathlib import Path

parser = argparse.ArgumentParser()
parser.add_argument('input', type=Path)
parser.add_argument('output', type=Path)
args = parser.parse_args()

# 仅转换你自己生成且确认可信的 pickle 文件。
with args.input.open('rb') as source:
    old_data = pickle.load(source)

payload = {
    'schemaVersion': 1,
    'data': old_data,
}

with args.output.open('w', encoding='utf-8') as target:
    json.dump(payload, target, ensure_ascii=False, indent=2, default=str)

print(f'Converted: {args.input} -> {args.output}')

运行方式:

python migrate_pickle.py old-template.pkl template.json

如果 pickle 中包含旧项目自定义类,转换脚本应放进原 PyQt6 环境运行,否则可能因为模块路径变化而无法反序列化。default=str 只适合过渡:日期、路径或复杂对象最好显式转换,以免静默丢失类型信息。新版本则应定义稳定的数据结构和 schemaVersion,以后通过迁移函数逐版升级。

打包成功不等于发布成功

使用 electron-builder 时,可以把平台目标集中放入 electron-builder.yml:

appId: com.example.hengshuiworkbook
productName: Hengshui Workbook

directories:
  output: release

files:
  - out/**
  - resources/**

extraResources:
  - from: resources/fonts
    to: fonts

win:
  target:
    - nsis

mac:
  target:
    - dmg

linux:
  target:
    - AppImage
    - deb

构建命令可以这样执行:

npm run build
npx electron-builder --win
npx electron-builder --mac
npx electron-builder --linux

这些命令并不意味着任意宿主系统都能可靠生成全部安装包。macOS 签名、公证和部分原生依赖通常应在 macOS 构建机上完成;Windows 安装器可以借助 Wine 环境生成,Bottles 能用于隔离这类环境,但它更适合作为工程上的兼容手段,而不是替代持续集成和真机测试。

如果项目包含原生 Node 模块,跨平台构建会进一步受到 CPU 架构、ABI 和工具链影响。纯 JavaScript 的 Canvas 与 tesseract.js 方案能减少一部分障碍,但仍需要分别检查:

  • 安装、升级和卸载是否正常;
  • 自定义字体、OCR 语言数据和 WASM 文件是否进入安装包;
  • Windows 路径、Linux 文件权限和 macOS 沙箱行为是否一致;
  • 打印结果与屏幕预览是否保持相同分页;
  • x64 与 ARM64 产物是否使用了正确依赖;
  • 签名、公证和自动更新策略是否匹配发布渠道。

一条更稳妥的迁移顺序

不要在同一个版本里同时重写界面、文件格式、OCR 和所有生成模式。更安全的节奏是先冻结旧版输出样例,再建立 Electron 的最小渲染闭环,随后逐项迁移模式,最后处理旧数据导入与平台安装包。

每次迁移都可以使用同一组文本、字体、纸张尺寸和 OCR 图片做回归测试。对于字帖工具,像素截图比较还不够,必须打印或导出后核对基线、行距、分页和字体替换。完成这些验证后,从 PyQt6 到 Electron 的变化才真正带来跨平台收益,而不是把原有问题搬进三个平台。


相关推荐