背景
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 handlerbridge.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.user32gdi32 = ctypes.windll.gdi32dc = 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 $R1lbl_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_donelbl_skip: # 用 CharNextA 失去下一个字符的地址 (可正确处理双字节字符)。 System::Call "user32::CharNextA(pR2)p.R2" Goto lbl_looplbl_done: System::Free $R1 Pop $R4 Pop $R3 Pop $R2 Pop $R1 Exch $R0FunctionEndFunction .onVerifyInstDir Push $INSTDIR Call PathIsDBCS_A Pop $R0 IntCmp $R0 0 lbl_done Abortlbl_done: FunctionEnd