背景
web在桌面端的表現不斷在演變,從nw 到electron,到現如今有很多現成客戶端框架。
大都架構是web核心+服務端語言。
例如:
只要找到適合當前業務的框架即可。
簡介
本文主要介紹在window環境下pywebview 3.x 使用的一些注意事項。
pywbview 官方文件
安裝
.NET>4.0
- (一個和windows資源相關的呼叫庫)
- 如果沒有.NET>4.0的需要安裝。一般win10都自帶
pythonnet
- (一個python調.NET的庫)
- 需要先安裝 pythonnet 庫
WebView2
- (一個Edge的核心)
- 下載
至於如何檢查和讓使用者安裝,下面會說明。
編碼與踩坑
配置
pywebview支援很多web核心。如果不指定web核心,pywebview會自動選擇,比如啥瀏覽器都沒裝的可能會用IE11渲染。所以我們指定核心(WebView2)如下
webview.start(gui='edgechromium',private_mode=True)
其中private_mode=True
則開啟瀏覽器快取(localStorage等)
自實現拖拽拉伸窗體
拖拽:
//js mousemove handler bridge.move(e.screenX, e.screenY) //實際呼叫 window.pywebview?.api.move(left,top);
拉伸:
- 不推薦,未解決windows縮放解析度時窗體右邊有一條縫隙。
綜上,儘可能用自帶的。
使用者環境檢查webview2與安裝
參考了tkwebview2
def have_runtime():#檢測是否含有webview2 runtime
from webview.platforms.winforms import _is_chromium
return _is_chromium()
def install_runtime():#安裝webview2 runtime
#https://go.microsoft.com/fwlink/p/?LinkId=2124703
from urllib import request
import subprocess
import os
url=r'https://go.microsoft.com/fwlink/p/?LinkId=2124703'
path=os.getcwd()+'\\webview2runtimesetup.exe'
unit=request.urlopen(url).read()
with open(path,mode='wb') as uf:
uf.write(unit)
cmd=path
p=subprocess.Popen(cmd,shell=True)
return_code=p.wait()#等待子程式結束
os.remove(path)
return return_code
管理員與登錄檔
解決以下相容問題:
- 解決Renderer Code Integrity造成Chrome瀏覽器崩潰
- 解決content type編碼問題導致html無法被瀏覽器解析,頁面載入不出
解決非管理員許可權開啟時,以管理員許可權重啟自身
def check_reg(): try: # https://zhuanlan.zhihu.com/p/400960997 ok=winreg.CreateKeyEx(winreg.HKEY_LOCAL_MACHINE,r'SOFTWARE\Policies\Microsoft\Edge\WebView2',reserved=0,access=winreg.KEY_WRITE) winreg.SetValueEx(ok,'RendererCodeIntegrityEnabled',0,winreg.REG_DWORD,0) winreg.CloseKey(ok) # https://blog.csdn.net/weixin_46099269/article/details/113185882 ok=winreg.CreateKeyEx(winreg.HKEY_CLASSES_ROOT,r'.js',reserved=0,access=winreg.KEY_WRITE) winreg.SetValueEx(ok,'Content Type',0,winreg.REG_SZ,'text/javascript') winreg.CloseKey(ok) except PermissionError: ctypes.windll.shell32.ShellExecuteW(None, "runas", sys.executable, __file__, None, 1) exit()
埠占用
解決上一個flask服務一直佔用埠
def kill_process(port): r = os.popen("netstat -ano | findstr "+str(port)) text = r.read() arr=text.split("\n") print("程式個數為:",len(arr)-1) for text0 in arr: arr2=text0.split(" ") if len(arr2)>1: pid=arr2[len(arr2)-1] if pid!="0": os.system("taskkill /PID "+pid+" /T /F") print(pid) r.close()
也可直接用隨機埠
服務端
效能
- flask自帶的server在請求時是一個個資源返回的,所以要使用 waitress代替flask自帶的server。它會起多個執行緒來監聽埠。
阻塞
- 服務端單獨開執行緒
def start(): global PORT,DEBUG PORT=5000 if DEBUG else randint(3333,9999) kill_process(PORT) check_reg() if not have_runtime():#不存在webview2 runtime或版本過低 install_runtime()#下載並安裝runtime server_thread = Process(target=start_server, daemon=True,kwargs={'port':PORT,'debug':DEBUG}) server_thread.start() time.sleep(1) start_gui(port=PORT,debug=DEBUG)
其他
- 常見的跨域等安全問題flask都有成熟的解決方案,這裡不展開。
dpi 顯示設定
windows高分屏的顯示設定可能>100%,此時介面可能模糊,或者計算畫素有問題
user32 = ctypes.windll.user32
gdi32 = ctypes.windll.gdi32
dc = user32.GetDC(None)
width = gdi32.GetDeviceCaps(dc, 118) # 原始解析度的寬度
# 最終寬高
dpi_width=int(width*0.6)
dpi_height=int(dpi_width/1202*802)
打包
打包流程
使用nuitka先打包成解壓後的資料夾
python -m nuitka --enable-console --standalone --windows-icon-from-ico=public/logo.ico --include-data-dir=backend/www=www --include-data-file=assets/*.dll=assets/ --follow-imports main.py
使用NSIS打包成安裝包
{ "build:setup.exe": "\"C:\\Program Files (x86)\\NSIS\\makensis.exe\" setup.nsi" }
注意事項
路徑問題
用nuitka將資料夾打包進去使用include-data-dir命令。dll不會被包含,需要用include-data-file命令。
此時 python 獲取相對路徑如下:dir=os.path.join(os.path.dirname(__file__), 'assets')
殺死當前程式並安裝
nsi配置
nsExec::Exec "taskkill /im main.exe /f"
檢查安裝路徑是否有中文
參考了原文
Function PathIsDBCS_A Exch $R0 Push $R1 Push $R2 Push $R3 Push $R4 System::Call "*(&m${NSIS_MAX_STRLEN}R0)p.R1" StrCpy $R0 0 StrCpy $R2 $R1 lbl_loop: # ANSI 版取 1 個位元組長度的字元,字串遇到 0 字元表示結束了。 System::Call "*$R2(&i1.R3)" IntCmp $R3 0 lbl_done # ANSI 字元用 IsDBCSLeadByte 判斷是否雙位元組字元的前導位元組。 System::Call "kernel32::IsDBCSLeadByte(iR3)i.R4" IntCmp $R4 0 lbl_skip IntOp $R0 $R0 ! Goto lbl_done lbl_skip: # 用 CharNextA 得到下一個字元的地址 (可正確處理雙位元組字元)。 System::Call "user32::CharNextA(pR2)p.R2" Goto lbl_loop lbl_done: System::Free $R1 Pop $R4 Pop $R3 Pop $R2 Pop $R1 Exch $R0 FunctionEnd Function .onVerifyInstDir Push $INSTDIR Call PathIsDBCS_A Pop $R0 IntCmp $R0 0 lbl_done Abort lbl_done: FunctionEnd