关于python:FECLI基于pywebview搭建企业级桌面端

5次阅读

共计 4028 个字符,预计需要花费 11 分钟才能阅读完成。

背景

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
正文完
 0