背景
web 在桌面端的体现一直在演变,从 nw 到 electron, 到现如今有很多现成客户端框架。
大都架构是 web 内核 + 服务端语言。
例如:
- electron=web 核 +nodejs
- pywebview=web 核 加 python
- tauri=web 核 加 rust
- webview =web 核 加 c++/go
只有找到适宜以后业务的框架即可。
简介
本文次要介绍在 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"
查看装置门路是否有中文
C 参考了原文
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