Python之点到为止: 给你的GUI换个feel

2020年04月12日 7303点热度 1人点赞 0条评论

上篇文章讲了GUI的故事, nodejs有强大的electron, python有没有类似的? 能不能用前端代码写Python GUI程序?

是否需要

还是那句话, 看你是否真正需要. 因为每一个新模块都代表着一段新知识. 学习是需要成本的, 不要感觉不错用这个库然后没几天又发现别的也不错换别的. 这样浪费时间不说最后也有可能连个Hello World都没写出来. 可能这个库更适合适合Python全栈? ?

相关库

  • cefpython3 —— 开源项目CEF与Python结合

这东西就是个浏览器, 所以浏览器能干的东西他都行. 浏览器不行的就对接Python. 你甚至可以用它来做爬虫, 但是没必要headless就够用. 至于爬虫方面等以后慢慢讲.

实操

本文会提供一个完全可以移植Demo,开箱即用, 但是写文章的时候Python最新版本为3.8.2, cefpython3只支持Python3.7版本及以前, 最近一次更新在2018年8月21日基于Chromium 66.0.3359.181编译的. 所以就目前来看不是特别推荐学习, 至少我现在是在用electron, nodejs学习成本对于后端全栈来说不是太高的. 前端全栈更别说了,nodejs这玩意必须会的东西.

先看看成品什么样, 演示视频中前端代码放在服务器上, 所以加载会有延迟. 实际编写推荐放在本地.

提醒看代码别劝退, 因为很好理解. 实在不理解的话也不用去理解, 因为只用一次. 窗口创建部分都来源自官方Demo: https://github.com/cztomczak/cefpython/blob/master/examples/pywin32.py

from cefpython3 import cefpython as cef

import math
import os
import sys

import win32api
import win32con
import win32gui

DEFAUTL_URL = 'https://api.virace.cc/jgah/cef/'
DEFAULT_USERNAME = 'root'
DEFAULT_PASSWORD = 'root'
DEFAULT_WINDOW_TITLE = '处女座之最 - 演示程序'

# Globals
WindowUtils = cef.WindowUtils()
# 全局窗口句柄
g_windows_handle = None
# 多线程
g_multi_threaded = False


class BindFunction:
    @staticmethod
    def get_title(callback):
        callback.Call(DEFAULT_WINDOW_TITLE)

    @staticmethod
    def login(data, callback):
        if 'username' not in data or 'password' not in data:
            callback.Call(False, '提交格式不正确')
        elif data['username'] == '' or data['password'] == '':
            callback.Call(False, '用户名密码不能为空')
        elif data['username'] == DEFAULT_USERNAME and data['password'] == DEFAULT_PASSWORD:
            callback.Call(True)
        else:
            callback.Call(False, '用户名或密码错误.')

    @staticmethod
    def min():
        win32gui.PostMessage(g_windows_handle, win32con.WM_SYSCOMMAND, win32con.SC_MINIMIZE)

    @staticmethod
    def max():
        # 判断窗口状态
        if win32gui.GetWindowPlacement(g_windows_handle)[1] == win32con.SW_SHOWMAXIMIZED:
            win32gui.PostMessage(g_windows_handle, win32con.WM_SYSCOMMAND, win32con.SC_RESTORE)
        else:
            win32gui.PostMessage(g_windows_handle, win32con.WM_SYSCOMMAND, win32con.SC_MAXIMIZE)

    @staticmethod
    def close():
        win32gui.PostMessage(g_windows_handle, win32con.WM_CLOSE)

    @staticmethod
    def move():
        # 捕获鼠标
        win32gui.ReleaseCapture()
        # 移动
        win32gui.SendMessage(g_windows_handle, win32con.WM_SYSCOMMAND, win32con.SC_MOVE + win32con.HTCAPTION, 0)
        pass


def main():
    sys.excepthook = cef.ExceptHook  # To shutdown all CEF processes on error

    settings = {
        "multi_threaded_message_loop": g_multi_threaded,
    }
    cef.Initialize(settings=settings)

    window_proc = {
        win32con.WM_CLOSE: close_window,
        win32con.WM_DESTROY: exit_app,
        win32con.WM_SIZE: WindowUtils.OnSize,
        win32con.WM_SETFOCUS: WindowUtils.OnSetFocus,
        win32con.WM_ERASEBKGND: WindowUtils.OnEraseBackground
    }
    global g_windows_handle
    g_windows_handle = create_window(title=DEFAULT_WINDOW_TITLE,
                                     class_name=DEFAULT_WINDOW_TITLE,
                                     width=1100,
                                     height=730,
                                     window_proc=window_proc,
                                     icon="resources/chromium.ico")

    window_info = cef.WindowInfo()
    window_info.SetAsChild(g_windows_handle)

    if g_multi_threaded:
        # When using multi-threaded message loop, CEF's UI thread
        # is no more application's main thread. In such case browser
        # must be created using cef.PostTask function and CEF message
        # loop must not be run explicitilly.
        cef.PostTask(cef.TID_UI,
                     create_browser,
                     window_info,
                     {},
                     DEFAUTL_URL)
        win32gui.PumpMessages()

    else:
        create_browser(window_info=window_info,
                       settings={},
                       url=DEFAUTL_URL)
        cef.MessageLoop()

    cef.Shutdown()


def create_browser(window_info, settings, url):
    assert (cef.IsThread(cef.TID_UI))
    bind_js(cef.CreateBrowserSync(window_info=window_info,
                                  settings=settings,
                                  url=url))


def bind_js(browser):
    """
    绑定js事件, 也可以用LoadHandler调用
    :param browser:
    :return:
    """
    bindings = cef.JavascriptBindings()
    bindings.SetFunction("py_title", BindFunction.get_title)
    bindings.SetFunction("py_login", BindFunction.login)
    bindings.SetFunction("py_move", BindFunction.move)
    bindings.SetFunction("py_windows_min", BindFunction.min)
    bindings.SetFunction("py_windows_max", BindFunction.max)
    bindings.SetFunction("py_windows_close", BindFunction.close)
    browser.SetJavascriptBindings(bindings)


def create_window(title, class_name, width, height, window_proc, icon):
    # Register window class
    wndclass = win32gui.WNDCLASS()
    wndclass.hInstance = win32api.GetModuleHandle(None)
    wndclass.lpszClassName = class_name
    wndclass.style = win32con.CS_VREDRAW | win32con.CS_HREDRAW
    wndclass.hbrBackground = win32con.COLOR_WINDOW
    wndclass.hCursor = win32gui.LoadCursor(0, win32con.IDC_ARROW)
    wndclass.lpfnWndProc = window_proc
    atom_class = win32gui.RegisterClass(wndclass)
    assert (atom_class != 0)

    # Center window on screen.
    screenx = win32api.GetSystemMetrics(win32con.SM_CXSCREEN)
    screeny = win32api.GetSystemMetrics(win32con.SM_CYSCREEN)
    xpos = int(math.floor((screenx - width) / 2))
    ypos = int(math.floor((screeny - height) / 2))
    if xpos < 0:
        xpos = 0
    if ypos < 0:
        ypos = 0

    # Create window
    window_style = (win32con.WS_POPUP | win32con.WS_CLIPCHILDREN
                    | win32con.WS_VISIBLE)
    window_handle = win32gui.CreateWindow(class_name, title, window_style,
                                          xpos, ypos, width, height,
                                          0, 0, wndclass.hInstance, None)
    assert (window_handle != 0)

    # Window icon
    icon = os.path.abspath(icon)
    if not os.path.isfile(icon):
        icon = None
    if icon:
        # Load small and big icon.
        # WNDCLASSEX (along with hIconSm) is not supported by pywin32,
        # we need to use WM_SETICON message after window creation.
        # Ref:
        # 1. http://stackoverflow.com/questions/2234988
        # 2. http://blog.barthe.ph/2009/07/17/wmseticon/
        bigx = win32api.GetSystemMetrics(win32con.SM_CXICON)
        bigy = win32api.GetSystemMetrics(win32con.SM_CYICON)
        big_icon = win32gui.LoadImage(0, icon, win32con.IMAGE_ICON,
                                      bigx, bigy,
                                      win32con.LR_LOADFROMFILE)
        smallx = win32api.GetSystemMetrics(win32con.SM_CXSMICON)
        smally = win32api.GetSystemMetrics(win32con.SM_CYSMICON)
        small_icon = win32gui.LoadImage(0, icon, win32con.IMAGE_ICON,
                                        smallx, smally,
                                        win32con.LR_LOADFROMFILE)
        win32api.SendMessage(window_handle, win32con.WM_SETICON,
                             win32con.ICON_BIG, big_icon)
        win32api.SendMessage(window_handle, win32con.WM_SETICON,
                             win32con.ICON_SMALL, small_icon)

    return window_handle


def close_window(window_handle, message, wparam, lparam):
    browser = cef.GetBrowserByWindowHandle(window_handle)
    browser.CloseBrowser(True)
    # OFF: win32gui.DestroyWindow(window_handle)
    return win32gui.DefWindowProc(window_handle, message, wparam, lparam)


def exit_app(*_):
    win32gui.PostQuitMessage(0)
    return 0


if __name__ == '__main__':
    main()

先说说这个代码的特点:

  • pywin32, 纯Windows API创建窗口
  • 无边框窗口, 无标题栏.
  • 解决无边框窗口拖动问题
  • 最大最小化关闭功能完善

这段代码看起来很多, 其实大部分都是创建窗口. 然而cefpython可以配合绝大部分GUI模块一起配合使用 https://github.com/cztomczak/cefpython/blob/master/examples/README-examples.md#gui-frameworks QT、tkinter、wxpython这几个常用跨平台的都可以的哦, 如果需要多窗口的时候这些, 如果但窗口就够用上面代码复制拿走直接加上自己的功能就行了.

其实整个代码只有BindFunction类和绑定js代码是后打的. 剩下什么创建窗口都是官方例子里的, 拿过来就用能看懂就看懂看不懂咱会用不就完了. 但是也没用几个API 哈哈??

撸了一个前端界面, 就是一个普普通通的登录界面, 登录成功后跳转的页面就改了个标题而已. https://api.virace.cc/jgah/cef/

上面的代码是经过绑定JS的方式交互, 说实话如果方法少的话还是挺爽的. 还有一种前后端分离写法, 懂后端的应该已经懂了. 就是前端只干前端显示的活, Python在本地启动一个http服务器, 比如Flask、django(有点大). 再配合上Vue, 嗬~~ 所有操作都通过服务端设置的API操作, 这样后续维护比较方便. 所以说这个框架适合Python全栈来用.

然后上面提供的登录页面, 大部分库用的都是cdn, 源码都在Github对应库分享了. 只是写个例子, 因为网页在本地访问就可以放心大胆的加各种特效, 也不会因为网络问题导致打开速度慢等问题.

总结

虽然听长时间不更新了, 但是你说用Python写这个东西还是挺爽的. 我最早一次用也两年以前了, 为了码文章又看文档跑了一遍. 现在的程序如果你们打开的网页想放在服务器中, 就会引文网络问题刚打开会有白屏的问题. 给你们个思路, 可以将窗口设置透明, 然后利用Animate.css上几个动画. 就会缓和好多. 但还是推荐在本地打开.

那么我就点到为止了
Just give a hint.

相关资料:
登录界面: https://api.virace.cc/jgah/cef/
cefpython3: https://github.com/cztomczak/cefpython/
本期文件: https://github.com/Virace/python-jgah/tree/master/Main/2154
ps: 官方文档例子超多

另外多说一句: 转载请注明出处, 点到为止系列为博主原创文章.

文章评论