FE.CLI-基於pywebview搭建企業級桌面端

seasonley發表於2023-04-16

背景

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

相關文章