分类
联系方式
  1. 新浪微博
  2. E-mail

Edifice:Python 声明式 UI 库

介绍

最近喜欢用 Python 写 PyQt 程序。但是有一点不太爽,PyQt 采用传统命令式编程,开发起来太费劲。了解前端的小伙伴都知道,现在流行声明式编程,开发效率和体验都大幅提升,用过就回不去了。

Python 的生态丰富度,在所有编程语言中是位列第一梯队的。只要有一个想法,定能找到好几个开源库。这不,搜索 Python 响应式 UI 库,就找到了本文的主角 Edifice。

Edifice 是一个 Python 下构建响应式界面的 UI 库。来自前端生态的启发(比如 React)。特色功能:

  • 来自前端实践的现代化范式,更加容易地创建 UI
  • 支持 Hot Reload 热重载
  • 与 Python 生态无缝衔接
  • 原生桌面程序,没有浏览器套壳那么重

Edifice 使用 Qt(PySide/PyQt5)作为后端。

Getting Started

安装包:pip install pyedifice

Hello World:

import edifice
from edifice import Label

edifice.App(Label("Hello World!")).start()

热更新

开启方式:

python -m edifice tutorial.py MyApp

Component Inspector

Edifice 还带有一个组件检查器,能够辅助调试组件树以及状态变化。开方式:

python -m edifice --inspect tutorial.py MyApp

官方 Examples

地址

项目 备注 截图
Calculator 100 lines of code
Financial Charting 响应式,自动更新

200 lines of code

结合绘图功能

Forms 基于 Form 组件高效开发表单
Harmonic Oscillator 响应式图表

自定义动画 30fps 运行

Qt Backend

Edifice 对 PySide2 和 PyQt5 都支持。Edifice 默认使用 PySide2。

如果希望使用 PyQt5,分为两步。

首先,通过以下方式安装 pyedifice 包,可以不装 PySide2:

# A dependency required for dynamic reloads
pip install watchdog
# A dependency required for asyncio integration
pip install qasync  
pip install --no-dependencies pyedifice

然后通过一个环境变量选择使用那种后端:

export EDIFICE_QT_VERSION=PyQt5

组件

Edifice 应用由 Component 和 App 组成。

Component 是构成应用的基本单元,状态 + UI。Edifice 是一个组件化框架,整个应用由组件构成。

组件分为两类:

  • native components:Qt View、Button、Text
  • composite components:对 Component 组合后的组件

在 Edifice 中,组件的声明方式如下:

import edifice as ed
from edifice import Label, TextInput, View

# Declare your own component, MyApp. 
# The render function describes how your custom component is to be rendered
class MyApp(ed.Component):
    def render(self):
        return View(layout="row")(
            Label("Measurement in meters:"),
            TextInput(""),
            Label("Measurement in feet:"),
        )

if __name__ == "__main__":
    ed.App(MyApp()).start()

组件的状态分为两类,一类是内部状态 State,一类是外部属性 Props。

Props

Props 是由外界传入的属性,声明方式:

class Foo(Component):
    # don't worry about this for now
    @edifice.register_props  
    def __init__(self, a, b, c):
        # a, b, c are the props
        pass

# Create a Foo with props a=1, b=2, c=3.
Foo(a=1, b=2, c=3)

其中:

  • @edifice.register_props  是一个注解,用于自动保存 props
  • props 被存入 self.props 字典
  • 组件内获取 props 两种方式:self.props["value"] 或者 self.props.value
  • 在组件内,props 是只读的,不可更改

State

State 表示组件的内部状态。

状态在构造函数中初始化。跟通常的创建类成员无异。

组件内部可以通过 set_state 方法刷新状态。之后会触发 render 方法 UI 刷新。

注:通过覆写 should_update 方法,可以控制是否拦截 UI 刷新。

状态更新示例:

self.timer = edifice.Timer(
    lambda: self.set_state(seconds=self.seconds+1))

render

render 是组件的核心方法,用于根据状态生成对应的 UI。示例:

def render(self):
    return View(layout="column")(
        View(layout="row")(
            Label("Username: "),
            TextInput()
        ),
        View(layout="row")(
            Label("Email: "),
            TextInput()
        ),
    )

Stateful Component

有状态组件,能够维护初始状态,保存状态属性。组件内部可以使用 set_state 方法更新状态。组件会通过 render 方法重新绘制:

import edifice

class Timer(edifice.Component):
    def __init__(self):
        super().__init__()
        self.seconds = 0
        self.timer = edifice.Timer(lambda: self.set_state(seconds=self.seconds+1))

    def did_mount(self):
        self.timer.start(1000)

    def render(self):
        return edifice.Label(self.seconds, style={"width": 80, "height": 30, "font-size": 20})

edifice.App(Timer()).start()

Stateless Componet

Edifice 也是支持无状态组件的,或称为 React 中的函数式组件。

通过 make_component 注解,能够将一个函数转换为组件,适合于没有内部状态的组件。

状态管理器

状态管理是应用开发的难点,状态管理器是目前前端领域常用的最佳实践。Edifice 也提供了一个状态管理器。

Edifice 中组件像 React 一样,将状态从上向下传递。这里有一个问题,如果父组件状态变化,所有子组件都要更新,导致性能浪费

Edifice 提供了 StateValue 和 StateManager 进行状态管理。

StateValue

StateValue 是 Edifice 可跟踪的状态,用于响应式订阅,核心方法是 set 和 subscribe。

subscribe 只能在 render 中调用,或者在 mount 之后调用。

如果状态改变,将会重绘。使用方法:

def render(self)::
    # Assume USER is a module-level variable
    user = USER.subscribe(self)
    # Assume balance is passed from a parent
    balance = self.props.balance.subscribe(self)
    return Label(f"{user}: {balance}")

其中:

  • 包含两个用例:USER 是模块级别的,balance 从父组件传入的

状态更新:

def on_click(self):
    USER.set(self.text_input_value)

StateManager

StateManager 的概念跟 StateValue 类似,可以将它理解为管理多状态的 Key-Value 存储。

StateManagers 允许将相关联的状态存储到一起,并且进行批量更新。

组件订阅的是 StateManagers 中单独的 key,与 key 绑定的还是 StateValue。

方法:

  • subscribe:将组件与某个 Key 相绑定
  • set:更新 key 值
  • keys:获取所有的 Key

参见:State Values and State Managers

Window

在窗口中展示子组件。

示例 效果
class MyApp(Component):

    def render(self):
        return View()(
            Window(title="Hello")(
                Label("Hello")
            )
        )

App(MyApp()).start()

注:官方文档的 Demo 有误。

窗口和视图的区别?窗口支持一系列参数:

  • title:窗口标题
  • icon:窗口图标
  • menu:菜单栏
  • on_close:窗口关闭时的回调函数

View

基础布局组件。越界的内容将会被截断。如果需要滚动,使用 ScrollView。

属性:layout(str):

  • 取值 row、column、none
  • none 表示绝对布局,子组件可以通过 left、top 调整绝对位置

TabView

Tab 分页组件。

示例 效果
class MyApp(Component):

    def render(self):
        return View()(
            TabView(labels=['Label1', 'Label2'])(
                View()(Label('I am tab1')),
                View()(Label('I am tab2'))
            )
        )

App(MyApp()).start()

属性 labels:标签分页属性。

CustomWidget

edifice 自带的视图控件很少,但是提供了 CustomWidget 组件,供开发者自己封装。示例:

class MyWidgetComponent(CustomWidget):
    def create_widget(self):
        # This function should return the new widget
        # (with parent set to None; Edifice will handle parenting)
        return QtWidgets.FooWidget()

    def paint(self, widget, newprops):
        # This function should update the widget
        for prop in newprops:
            if prop == "text":
                widget.setText(newprops[prop])
            elif prop == "value":
                widget.setValue(newprops[prop])

覆写了两个方法:

  • create_widget:创建一个 Qt Widget
  • paint:解析 newprops,对 widget 进行一顿设置

布局

Row 横向布局

import edifice as ed
from edifice import Label, TextInput, View

window = View(layout="row")(  # Layout children in a row
    Label("Measurement in meters:"),
    TextInput(""),
    Label("Measurement in feet:"),
)

if __name__ == "__main__":
    ed.App(window).start()

UI 控件

IconButton

图标按钮。Edifice 捆绑了 FontAwesome,能够直接使用图标库里面图标,图标列表

属性:

  • name:FontAwesome 图标名称
  • size: int 大小
  • collection:图标集,当前只支持 FontAwesome
  • sub_collection:图标子集,如 regular 或者 solid
  • color:颜色,一个 tuple (R, G, B, A)
  • rotation:旋转角度

TextInput 输入框

监听输入内容变化:

TextInput(meters, style=input_style,
          on_change=lambda text: self.set_state(meters=text)),

props:

  • text:输入内容
  • on_change:Callable[[str], None] 内容变更回调
  • on_edit_finish:Callable[[], None]
  • completer:文本补全

网络资源

文档首页