與踩坑全記錄)
原文項目鐘毓英語衡水體字帖生成器重構前后PyQt6 v1.1.1 → Electron v2.0.x跨平臺 Windows / macOS / Linux關鍵詞electron-vite、Canvas 2D、tesseract.js OCR、electron-builder、pickle 兼容、Bottles 交叉打包前言筆者手上有一款用 PyQt6 開發(fā)的英語衡水體字帖生成器功能挺全乎四種生成模式描紅/抄寫/描紅抄寫/字帖、兩種線格、多標簽頁、PDF 導出還有個自定義的.zyecb工程文件格式。原版在 Windows 上跑得好好的可架不住用戶問Linux 有嗎、“mac 能裝嗎”——Python 桌面應用的分發(fā)短板一下就暴露了PyInstaller 體積大、跨平臺得各開一臺機器、系統庫依賴分分鐘給你整出兼容玄學。得那就用Electron徹底重構吧。本文不整為什么選 Electron這種正確的廢話直接上干貨構建思路怎么定的、關鍵決策怎么做的、以及踩過的那些真實坑和最終怎么爬出來的。正在折騰桌面應用重構的朋友希望能幫你少走點彎路。一、整體架構與技術選型1.1 技術棧層面選型說明構建工具electron-vite 2 Vite 5主/預加載/渲染進程統一構建HMR 香得很運行時Electron 33.4.11直接上 33別問問就是被 Node 20.14 坑過見第六節(jié)坑 4渲染原生 JavaScript Canvas 2D無框架直接復刻 QPainter 自繪邏輯打包electron-builder 24.13.3NSIS / AppImage / deb / rpm / dmg / zip 一網打盡OCRtesseract.js v7截圖識別語言數據內置離線可用1.2 目錄與多入口設計原版就是個單窗口 QMainWindow 套 QTabWidget。到了 Electron 這邊我把全屏自繪的場景拆成了獨立 HTML 入口各司其職src/ ├── main/ 主進程IPC、窗口、菜單、打印、OCR ├── preload/ contextBridge 橋 └── renderer/ ├── index.html 主界面標簽頁 控件 預覽 Canvas ├── print.html 隱藏窗口渲染 A4 頁面供 PDF 導出 / 打印 ├── sel.html 截圖選區(qū)窗口每顯示器一個全屏無邊框 └── preview.html 打印預覽窗口四個入口在electron.vite.config.mjs的rollupOptions.input里注冊。這種按全屏場景拆入口的設計比在單個 BrowserWindow 里切視圖清爽多了——打印和預覽窗口本來就不需要主界面那一堆控件。1.3 逐像素對齊原版別瞎優(yōu)化重構桌面應用最忌諱的就是順手優(yōu)化一下結果用戶一打開感覺這味兒不對啊。我的策略很簡單頁面坐標系、字號、顏色、行高全部一比一復刻。頁面尺寸 800×1131px邊距 (20, 20, 760, 1091)頭部高 100四線三格行高 40、組距 40線位 y0/13/26/39單橫線行高 30、組距 0QFont 的 pt 字號按 96 DPI 換算px pt × 4/3描紅色#ff6464、網格線#c0c0c0、頁碼#a0a0a0。排版引擎集中在src/renderer/src/engine/copybook.js的buildPages一次排版分頁。順帶還修了原版一個潛伏 bug單橫線模式下原版按四線三格行高估算頁容量跨頁時單詞會重復出現重構版按實際行高算就好了——算是重構附贈的彩蛋。二、工程文件雙向兼容pickle 這個老頑童原版的.zyecb工程文件是Python picklePy3 默認協議 4。老用戶遷移是剛需新版必須能讀要讓用戶在新舊版本間自由切換新版存的文件原版最好也能打開。2.1 讀取手寫協議 0~4 子集解析器pickle 本質是個基于棧的虛擬機字節(jié)碼。我吭哧吭哧手寫了一個解析器把常用 opcode 都覆蓋了PROTO、STOP、MARK、EMPTY_LIST/DICT/TUPLE、APPEND、SETITEM、BINUNICODE、SHORT_BINUNICODE、GLOBAL、REDUCE等等協議 4 的FRAME、MEMOIZE也沒落下。這活兒不復雜但碎建議邊對照pickletools.dis()的輸出邊寫事半功倍。2.2 寫入用協議 0但小心\uXXXX輸出我選了協議 0純 ASCII任何 Python 版本都能讀出問題了肉眼也能 debug。這里有個能把人逼瘋的坑協議 0 里字符串 opcodeV后面跟一行以\n結尾的 raw-unicode-escape 字符串。Python 的 raw-unicode-escape只認\uXXXX形式的轉義不認\n、\\這種簡寫。所以字符串里的換行、反斜杠、控制符必須全部編碼成\u000a、\u005c等形式否則原版pickle.load輕則UnicodeDecodeError重則讀到一堆亂碼。2.3 回歸測試雙向跑一遍原版保存一批典型工程特殊字符、空內容、長文本都來點→ 新版打開新版保存 → 原版打開斷言渲染結果一字不差。最終實現了真正的雙向兼容用戶雙擊.zyecb就能用新版打開通過 electron-builder 的fileAssociations注冊文件關聯。三、打印與打印預覽一條管線走天下原版導出 PDF和打印走的是 QPrinter。到了 Electron我把這兩條路合并成一條渲染管線再額外加了個打印預覽窗口——畢竟都 2026 年了沒預覽的打印是不完整的。3.1 三路復用的渲染窗口主進程ipc.js抽出createRenderWindow(data)建一個隱藏的 BrowserWindow加載print.htmlIPC 把排版數據塞過去渲染進程用 Canvas 2D 按 A4 尺寸逐頁畫畫完了 IPC 回報主進程按 sender id 過濾多窗口串消息這種事防一手設個 30s 超時兜底根據調用場景分流pdf:export→webContents.printToPDFprint:direct→webContents.print彈系統打印對話框print:preview→ 復用單例預覽窗口顯示。打印參數統一為{ printBackground: true, pageSize: A4, margins: { marginType: none } }。劃重點新版 Electron 用 margins 對象舊的 marginsType 已經被打入冷宮。3.2 高清渲染2 倍 DPRA4 頁面按2 倍 DPR192 DPI繪制794×1123 96dpi 的 2 倍打出來的字邊緣那叫一個銳利。打印和 PDF 共用同一份 Canvas 數據效果完全一致用戶再也不會說PDF 看著挺好打出來糊了。3.3 打印預覽窗口的那些小細節(jié)單例復用重復打開就 focus 重渲染別傻乎乎每次新建窗口渲染完再 show()不然用戶看到白屏閃爍體驗分驟降setMenu(null)非 macOS 下附加窗口默認帶應用菜單欄必須手動清掉不然預覽窗口頂個文件編輯幫助菜單怪尷尬的CSSzoom縮放別用transform: scalezoom 是參與 Chromium 布局計算的滾動條和頁碼定位才對得上適應頁寬算法zoom (clientWidth - 兩側留白) / 794794 是 A4 寬 96dpi 的像素值頁碼跟隨滾動用getBoundingClientRect找最后一個 top ≤ 視口 35% 的頁當當前頁。別問我為什么知道——初版用current寫了個差一 bug翻第一頁顯示第 2/2 頁當場社死。3.4 Linux 下驗證的小坑在 deepin 上做自動化驗證時發(fā)現一個有意思的現象GTK 打印對話框打開期間父窗口渲染進程的 JS 被模態(tài)阻塞了CDPRuntime.evaluate直接超時對話框一關立馬恢復。所以測試時系統對話框那塊選打印機、點確認得交給 xdotool應用內交互點預覽按鈕、拖縮放才能用 CDP。另外 xdotool 在 GTK 保存對話框里type路徑會被輸入法劫持斜杠還會被吃掉解決方案是測試導出時先接受默認文件名事后再改回去。四、截圖識別desktopCapturer tesseract.js光有手動輸入哪夠必須上個截圖識別——看到屏幕上的英文直接框一下就進字帖多香。4.1 完整流程走一遍主窗口先藏起來desktopCapturer.getSources({ types: [screen] })截屏thumbnailSize設成顯示器物理尺寸 × scaleFactorHiDPI 下才不會糊每個顯示器彈一個全屏無邊框alwaysOnTop選區(qū)窗口sel.html復用主 preload用戶拖框選寬高 8px 當誤觸處理Enter 或雙擊確認、Esc 取消裁出的 dataURL 經 IPC 扔給主進程的 tesseract.js識別結果用document.execCommand(insertText, false, text)回填輸入框——這樣能保留原生撤銷棧還能自動觸發(fā) input 事件聯動預覽。4.2 雙擊確認的交互坑寫的時候踩了個挺隱蔽的 bug拖出選區(qū)后雙擊居然確認不了。一通 debug 發(fā)現雙擊的第一次mousedown命中了已有選區(qū)代碼把選區(qū)重置成了一個點到dblclick時選區(qū)寬高已經是 0 了自然啥也確認不了。修復很簡單mousedown時如果點落在已有選區(qū)內不重置選區(qū)只記個起始點把確認的機會留給dblclick。改完 Enter 確認、雙擊確認、Esc 取消就各司其職了。4.3 HiDPI 坐標換算screenAPI 返回的是 DIP 尺寸125% 縮放下 1536×864但截圖是物理像素1920×1080。選區(qū)坐標到圖像坐標的換算就一句sx image.naturalWidth / window.innerWidth也就是 scaleFactor裁剪時x * sx、y * sy就行。4.4 tesseract.js 在主進程跑createWorker(lang, 1, { langPath, cachePath, logger: () {} })worker 按語言 Map 緩存別每次識別都新建輸入用 BufferdataURL 先轉 Buffer語言數據用tessdata_fast的.gz丟resources/ocr-data里通過extraResources內置打包后路徑是process.resourcesPath/ocr-data必須在package.json的dependencies里externalizeDepsPlugin 會把主進程依賴外部化運行時從 node_modules require重依賴懶加載別在主進程入口頂層import首次調用時再await import(tesseract.js)——這是第六節(jié)四個致命坑之一白屏閃退的元兇。官方tessdata.projectnaptha.com直連會重置連接換cdn.jsdelivr.net/gh/tesseract-ocr/tessdata_fast下 plain 文件再本地 gzip 即可。五、多平臺打包electron-builder 全攻略5.1 基礎配置{win:{target:nsis,icon:resources/app_icon.ico},nsis:{oneClick:false,allowToChangeInstallationDirectory:true,createDesktopShortcut:true},mac:{target:[dmg,zip],icon:resources/app_icon.icns,artifactName:${name}-${version}-${arch}.${ext}},linux:{target:[AppImage,deb,rpm],icon:resources/icons,maintainer:Your Name emailexample.com,artifactName:${name}-${version}-${arch}.${ext}}}幾個要點maintainer必須帶 email不然 deb 構建給你報個莫名其妙的錯artifactName用${name}保持 ASCII 文件名Windows 除外默認按 productName 中文命名linux.icon 指向多尺寸目錄而不是單張 PNG原因見坑 3。5.2 架構支持矩陣平臺x64arm64riscv64Windows NSIS??合并包?Linux AppImage/deb/rpm??交叉?macOS dmg/zip???Linux 交叉打 arm64npx electron-builder --linux AppImage deb rpm --arm64Windows NSIS 不指定--x64時默認打x64arm64 雙架構合并安裝包安裝時讓用戶自選架構riscv64 沒官方 Electron 二進制死心吧。5.3 Linux 上交叉打 Windows NSIS無系統 wine用 flatpak Bottles本機裝不了系統級 wine退而求其次用 flatpak 的 Bottlesflatpak install flathub com.usebottles.bottles建 bottlebottles-cli new --bottle-name builder --environment application --arch win64沙箱網絡是個大坑bottles 組件從 github 下沙箱默認不走 host socks5 代理Python requests 還缺 SOCKS 支持 →pip download PySocks純 py wheel解到 bottles 能訪問的目錄建 bottle 時加--envPYTHONPATH... --envhttps_proxysocks5h://x.x.x.x:port新 winesoda runner 11.xwow64 合并了只有 wine 沒有 wine64而且 standalone 是個 bash 腳本不是二進制寫個 wine 包裝器wine 和 wine64 都軟鏈到它exportLD_LIBRARY_PATHrunner/lib:runner/lib/wine/x86_64-unix:runner/lib/wine/i386-unixexportWINEPREFIXbottle路徑exportPATHrunner/bin:$PATHexecrunner/bin/wine$PATH/tmp/eb-wine:$PATH npx electron-builder --win nsis --publish never實測驗證rceditexe 版本信息能塞中文產品名、makensis 都正常還能在 bottle 里Setup.exe /S靜默安裝走一遍流程。5.4 Linux 上出 macOS zipdmg-license 是 macOS 專屬可選依賴Linux 上裝不上原生庫 invalid ELF header。繞法npm i --no-save dmg-license --force然后把它的index.js替換成module.exports {}樁模塊zip 目標只 require 不調用。dmg 必須在 macOS 上構建要 hdiutilLinux 上沒辦法。完事npm prune清掉。5.5 rpm 4.20 環(huán)境的特殊處理本機 rpm 4.20 和 electron-builder 內置的 fpm 1.9.3 八字不合fpm 傳的--define buildroot X被 rpm 4.20 當空氣結果就是 “File not found”。解法寫個 rpmbuild 包裝器把--define buildroot X翻譯成 4.20 還認的--buildroot X構建時 PATH 前置。另外非 root 跑需要用戶級 rpmdbrpm --initdb初始化~/.cache/rpmdb在~/.rpmmacros寫%_dbpath /home/user/.cache/rpmdb不然會報/var/lib/rpm/rpmdb.sqlite打不開。六、四個致命的打包坑真實用戶機器上栽過的跟頭這節(jié)是全文最有含金量的部分——這四個問題本地開發(fā)壓根測不出來都是打包后扔到真實用戶環(huán)境才現原形的。坑 1Windows 安裝后白屏閃退現象安裝一切正常雙擊快捷方式窗口一閃而過連個錯誤日志都不給你留。根因src/main/ocr.js頂層寫了import { createWorker } from tesseract.js構建產物在主進程入口變成require(tesseract.js)某些打包/系統環(huán)境下初始化失敗直接閃退白屏。修復改成函數內await import(tesseract.js)懶加載首次用到 OCR 時才加載。教訓主進程頂層永遠別 import 重依賴這條記住能救命???2deepin/UOS 裝 deb 報 EXDEV 硬鏈接錯誤現象dpkg: 錯誤新建硬鏈接 ... 無效的跨設備鏈接 (EXDEV)。根因app_icon.png既當extraResources裝到 /opt/…/resources/又當linux.icon裝到 /usr/share/icons/hicolor/electron-builder staging 階段用 hardlink 復制fpm 1.9.3 把硬鏈接寫進 tar條目類型 h。deepin/UOS 這類不可變系統 /opt 和 /usr 是不同掛載點dpkg 解包時link()跨設備直接失敗。冷知識fpm 1.9.3沒有--deb-no-hardlinks選項1.10 才有配deb.fpm會直接構建失敗別在這上面浪費時間。根治圖標別放 extraResources打進 app.asarfiles 里加resources/app_icon.png打包后join(__dirname, ../../resources/app_icon.png)nativeImage 支持 asar 路徑三平臺通吃。這樣 hicolor 成了唯一物理文件硬鏈接條目數直接歸零。驗證方法debar x pkg.deb tar tvf data.tar.* | grep -c ^hrpmrpm2archive pkg.rpm | tar tv | grep -c ^h坑 3Linux 圖標顯示未知類型占位圖現象deb 裝上了啟動器里應用圖標是個灰色的未知類型占位圖丑得離譜。根因linux.icon給單張 PNG 時electron-builder 只把它扔到/usr/share/icons/hicolor/0x0/apps/。0x0 不符合 hicolor-icon-theme 規(guī)范deepin/UOS 的啟動器直接不索引。修復弄個多尺寸圖標目錄resources/icons/塞進去 16/24/32/48/64/128/256/512 的NxN.png用 PIL LANCZOS 從 256px 源圖生成512 是放大的linux.icon指向這個目錄構建后就裝到各hicolor/size/apps/了。驗證模擬 XDG 數據目錄后用 GTKGtk.IconTheme.get_default().lookup_icon(name, 256, 0)能解析出來。注意 offscreen 下 PyQt 的QIcon.fromTheme連系統圖標都查不到別拿它驗證純屬浪費時間???4Windows 12 代 Intel 大小核 CPU 上 Node 初始化崩潰現象Windows 11 真機裝完打不開退出碼 0但同一個安裝包扔 VirtualBox Win10 里跑得歡VS Code 等其他 Electron 應用在這臺機器上也正常。根因Electron 31 內置的 Node 20.14 在 Intel 大小核混合 CPU12 代及以后上調GetLogicalProcessorInformationEx枚舉處理器組時緩沖區(qū)越界直接 fatal。修復升級到 Electron 33.4.11Node 20.18修了這 bug。少數處理器組信息異常的機器還得檢查 BIOS 或 Windows 啟動參數bcdedit里的groupsize/maxgroup/numproc/usegroup。教訓新項目直接 Electron ≥33別給自己找麻煩。七、菜單助記符的跨平臺玄學這是個小坑但煩人的很。Electron 的菜單在 Linux GTK 和 Windows 上對(X)的處理還不一樣頂層菜單(F)會被整體從顯示文本剝離只注冊 Alt 助記符——所以頂層得雙寫文件(F)(F)才能既顯示(F)又有 AltF子菜單項只剝符號本身新建(N)顯示帶下劃線的 N原生寫法就行→ 字面不注冊助記符_在 GTK 不當下劃線語法原樣顯示。封裝兩個 helper 一勞永逸consttopMenuLabel(text,key)${text}(${key})(${key})constitemLabel(text,key,suffix)${text}(${key})${suffix}八、沒有商業(yè) UI 測試工具這套組合拳頂用deepin 上做驗證沒有付費測試工具全靠開源湊CDP--remote-debugging-port9223臨時裝個ws包Node 20 沒全局 WebSocket寫腳本Runtime.evaluate讀 DOM/canvas 像素、Input.dispatchKeyEvent驅動應用內按鍵xdotool負責 GTK 原生對話框菜單導航key --delay 200 altf不加 delay 菜單丟鍵PillowImageGrab.grab(xdisplay:0)截圖物理分辨率坐標按像素算tkinter造 OCR 測試素材——overrideredirect topmost置頂窗顯示已知文本setsid nohup啟動防 shell 退出連帶殺進程文字四周留足 padding不然貼邊字母識別錯pkill 技巧pkill -f的模式如果匹配到自身命令行會自殺用[x]括號技巧比如pkill -f [e]lectron .。九、總結從 PyQt6 到 Electron 的重構最大的收獲是跨平臺分發(fā)能力的質變一套代碼產出 Windows NSIS、Linux AppImage/deb/rpm、macOS zip/dmgOCR、打印、文件關聯這些原生能力也都齊活。代價嘛就是得重新適應瀏覽器環(huán)境下的渲染、IPC、打包模型。幾個扎心的經驗逐像素對齊原版是桌面應用重構的第一原則別擅自優(yōu)化用戶已經習慣的視覺文件格式雙向兼容是遷移的生命線pickle 協議 0 寫入的\uXXXX坑一定要繞開主進程頂層不 import 重依賴一律懶加載白屏閃退能防一大半打包坑全在真實環(huán)境暴露EXDEV 硬鏈接、hicolor 0x0 圖標、Intel 大小核崩潰——這些本地開發(fā)永遠測不出來必須上目標系統驗打印預覽要單例、渲染完再 show、記得清菜單細節(jié)決定體驗。重構這趟下來感覺 Electron 做桌面應用其實沒網上傳的那么不堪關鍵是別把它當網頁寫得有桌面應用的意識——窗口生命周期、系統對話框、原生菜單、文件關聯一個都不能少。希望這篇踩坑記錄能幫到正在做類似重構的你。有問題歡迎評論區(qū)開麥 相關資源項目倉庫GitHubv2.0.2 Release