现如今,各端的UI框架层出不穷,iOS的SwiftUI、安卓的Composure、还有Flutter、React等,对于声明式UI已经进入一种流行;那么问题来了:
为什么声明式UI这么火热:
发展历程:
Web开发中的发展:
移动开发中的采用:
移动开发领域相对较晚才采用声明式UI,但发展迅速:
- React Native(2015年):将React的声明式理念带入移动开发。
- Flutter(2017年):Google的跨平台框架,采用声明式UI设计。
- SwiftUI(2019年):Apple为iOS生态系统引入的声明式UI框架。
- Jetpack Compose(2021年):Google为Android平台推出的声明式UI工具包。
声明式UI的优点:
1.代码的简洁性:
// 命令式UI(使用UIKit) class ViewController: UIViewController { override func viewDidLoad() { super.viewDidLoad() let label = UILabel() label.text = "Hello, World!" label.textAlignment = .center label.frame = CGRect(x: 0, y: 0, width: 200, height: 50) label.center = view.center view.addSubview(label) } } // 声明式UI(使用SwiftUI) struct ContentView: View { var body: some View { Text("Hello, World!") .frame(width: 200, height: 50) } }
2.可读性强,开发效率高:
- 声明式UI描述了"什么"应该被显示,而不是"如何"显示它。
- 代码结构更接近于最终的UI结构,使得理解和维护变得更容易。
- 很多声明式框架支持实时预览功能,加速了开发过程。
3.状态管理的简化:
- 声明式UI通常提供更简单和直接的状态管理机制。
- 单向数据流
- 例如,SwiftUI的@State和@Binding属性包装器使得状态管理变得非常直观。
4.更少的副作用
- 声明式UI鼓励使用纯函数和不可变状态,这降低了出现副作用和难以追踪的bug的可能性。
5.更容易实现响应式设计:
- 声明式UI天然地支持响应式编程模型,使得UI能够自动响应数据的变化。
6.更好的可测试性,方便单测:
- 由于声明式UI组件通常是纯函数,它们更容易进行单元测试。
7.跨平台开发的便利性:
- 声明式UI的抽象层次更高,更容易在不同平台间共享UI逻辑。
那么命令式UI有什么天生的缺点:
1.代码冗长和复杂性:
class ComplexViewController: UIViewController { var nameLabel: UILabel! var nameTextField: UITextField! var submitButton: UIButton! override func viewDidLoad() { super.viewDidLoad() nameLabel = UILabel() nameLabel.text = "Name:" nameLabel.translatesAutoresizingMaskIntoConstraints = false view.addSubview(nameLabel) nameTextField = UITextField() nameTextField.borderStyle = .roundedRect nameTextField.translatesAutoresizingMaskIntoConstraints = false view.addSubview(nameTextField) submitButton = UIButton(type: .system) submitButton.setTitle("Submit", for: .normal) submitButton.translatesAutoresizingMaskIntoConstraints = false submitButton.addTarget(self, action: #selector(submitTapped), for: .touchUpInside) view.addSubview(submitButton) NSLayoutConstraint.activate([ nameLabel.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 20), nameLabel.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 20), nameTextField.topAnchor.constraint(equalTo: nameLabel.bottomAnchor, constant: 8), nameTextField.leadingAnchor.constraint(equalTo: nameLabel.leadingAnchor), nameTextField.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -20), submitButton.topAnchor.constraint(equalTo: nameTextField.bottomAnchor, constant: 20), submitButton.centerXAnchor.constraint(equalTo: view.centerXAnchor) ]) } @objc func submitTapped() { // 处理提交逻辑 } }
2.状态管理困难:
- 在大型应用中,管理和同步UI状态变得非常复杂。
- 容易出现状态不一致的问题,导致UI显示错误。
3.难以维护和重构:
- 由于UI逻辑和业务逻辑often高度耦合,重构变得困难。
- 修改一个组件可能会影响多个其他组件,增加了维护的复杂性。
4.测试困难:
- UI逻辑和业务逻辑的混合使得单元测试变得困难。
- 需要模拟复杂的UI状态来测试特定功能。
5.性能优化挑战:
- 手动管理UI更新可能导致不必要的重绘,影响性能。
- 优化渲染性能需要开发者手动处理,容易出错。
6.动画实现繁琐:
- 复杂动画的实现通常需要大量的命令式代码,难以维护和调试。
可以看到声明式UI基本上都是完美避开或者解决了命令式UI的痛点;所以他的高度流行除了开发者们的用脚投票,还有其自身的优越性;
那么对于PyQt这种UI框架,他诞生的比较早,当然保留了命令式UI的开发模式,并且大型UI框架积重难返;
但是作为一个有梦想的爱坤,我们可不可以改造当前的开发模式,来搭建一套声明式的框架来实现方便的在PyQt里开发声明式UI;
首先我们应该想清楚要做哪几个点:
- 声明式语法;
- state状态更新;
- Diff算法;
这三个点可以说是声明式框架的重中之重;
1.PyQt6的声明式语法:
声明式语法的特性:组件的嵌套和链式调用
让我们写一个demo:
from PySide6.QtWidgets import ( QHBoxLayout, QVBoxLayout, QWidget, QPushButton, QLabel, QLineEdit, QCheckBox, QRadioButton, QComboBox, QSpinBox, QDoubleSpinBox, QSlider, QProgressBar, QGroupBox, QTabWidget, QScrollArea, QSplitter, QTreeView, QTableView, QListView, QTextEdit, QPlainTextEdit, QMessageBox, QMenuBar, QMenu, QToolBar, QLayout, QFormLayout, QGridLayout, QDateEdit, QTimeEdit, QDateTimeEdit, QGraphicsView, QGraphicsScene, QGraphicsItem, QGraphicsTextItem, QGraphicsRectItem, QGraphicsEllipseItem, QGraphicsPathItem, QGraphicsPolygonItem, QGraphicsLineItem, ) from PySide6.QtGui import QIcon, QPixmap from PySide6.QtCore import Qt, QPoint, QSize, QRect, QMargins, QTimer, QEventLoop, QObject class UIComponent: def __init__(self, widget_class, **props): self.widget_class = widget_class self.props = props self.children = [] self.styles = [] def __call__(self, *children): self.children.extend(children) return self def style(self, *styles): self.styles.extend(styles) return self def build(self, parent=None): if issubclass(self.widget_class, QWidget): widget = self.widget_class(parent) else: widget = self.widget_class() if 'fixedWidth' in self.props: widget.setFixedWidth(self.props['fixedWidth']) if 'fixedHeight' in self.props: widget.setFixedHeight(self.props['fixedHeight']) for key, value in self.props.items(): if hasattr(widget, f'set{key.capitalize()}'): getattr(widget, f'set{key.capitalize()}')(value) elif hasattr(widget, key): setattr(widget, key, value) for child in self.children: if isinstance(child, UIComponent): child_widget = child.build(widget if isinstance(widget, QWidget) else None) if isinstance(widget, QLayout): if isinstance(child_widget, QLayout): # 如果子部件是布局,创建一个新的 QWidget 来包含它 container = QWidget() container.setLayout(child_widget) widget.addWidget(container) else: widget.addWidget(child_widget) elif isinstance(widget, QWidget): if widget.layout() is None: widget.setLayout(QVBoxLayout()) if isinstance(child_widget, QLayout): # 如果子部件是布局,创建一个新的 QWidget 来包含它 container = QWidget() container.setLayout(child_widget) widget.layout().addWidget(container) else: widget.layout().addWidget(child_widget) elif isinstance(child, str): if hasattr(widget, 'setText'): widget.setText(child) elif isinstance(widget, QLayout): label = QLabel(child) widget.addWidget(label) style = '; '.join(self.styles) if style and isinstance(widget, QWidget): widget.setStyleSheet(style) return widget # 链式调用 def bg_color(self, color): self.styles.append(f"background-color: {color}") return self def text_color(self, color): self.styles.append(f"color: {color}") return self def font_size(self, size): self.styles.append(f"font-size: {size}px") return self def font_family(self, family): self.styles.append(f"font-family: {family}") return self def font_weight(self, weight): self.styles.append(f"font-weight: {weight}") return self def font_style(self, style): self.styles.append(f"font-style: {style}") return self def border(self, width, color): self.styles.append(f"border: {width}px solid {color}") return self def border_radius(self, radius): self.styles.append(f"border-radius: {radius}px") return self def padding(self, padding): self.styles.append(f"padding: {padding}px") return self def margin(self, margin): self.styles.append(f"margin: {margin}px") return self def width(self, width): self.props['fixedWidth'] = width return self def height(self, height): self.props['fixedHeight'] = height return self def opacity(self, opacity): self.styles.append(f"opacity: {opacity}") return self def visible(self, visible): self.styles.append(f"visible: {visible}") return self def position(self, x, y): self.styles.append(f"position: absolute; left: {x}px; top: {y}px") return self def z_index(self, index): self.styles.append(f"z-index: {index}") return self def on_click(self, callback): self.props['clicked'] = callback return self # 创建快捷函数 def VBox(*children, **props): return UIComponent(QVBoxLayout, **props)(*children) def HBox(*children, **props): return UIComponent(QHBoxLayout, **props)(*children) def ZBox(*children, **props): return UIComponent(QVBoxLayout, **props)(*children) def Button(text, **props): return UIComponent(QPushButton, **props)(text) def Label(text, **props): return UIComponent(QLabel, **props)(text) def LineEdit(**props): return UIComponent(QLineEdit, **props) # 样式函数 def bg_color(color): return f"background-color: {color}" def text_color(color): return f"color: {color}" def font_size(size): return f"font-size: {size}px" # 添加更多样式函数... def GridLayout(*children, **props): return UIComponent(QGridLayout, **props)(*children) def FormLayout(*children, **props): return UIComponent(QFormLayout, **props)(*children) # 基本控件 def CheckBox(text="", **props): return UIComponent(QCheckBox, **props)(text) def RadioButton(text="", **props): return UIComponent(QRadioButton, **props)(text) def ComboBox(**props): return UIComponent(QComboBox, **props) def SpinBox(**props): return UIComponent(QSpinBox, **props) def DoubleSpinBox(**props): return UIComponent(QDoubleSpinBox, **props) def Slider(orientation=Qt.Horizontal, **props): return UIComponent(QSlider, **props) def ProgressBar(**props): return UIComponent(QProgressBar, **props) # 容器控件 def GroupBox(title="", **props): return UIComponent(QGroupBox, **props) def TabWidget(**props): return UIComponent(QTabWidget, **props) def ScrollArea(**props): return UIComponent(QScrollArea, **props) def SplitterH(*children, **props): splitter = UIComponent(QSplitter, **props) splitter.props['orientation'] = Qt.Horizontal return splitter(*children) def SplitterV(*children, **props): splitter = UIComponent(QSplitter, **props) splitter.props['orientation'] = Qt.Vertical return splitter(*children) # 高级控件 def TreeView(**props): return UIComponent(QTreeView, **props) def TableView(**props): return UIComponent(QTableView, **props) def ListView(**props): return UIComponent(QListView, **props) def TextEdit(**props): return UIComponent(QTextEdit, **props) def PlainTextEdit(**props): return UIComponent(QPlainTextEdit, **props) # 对话框 def MessageBox(text, title="", **props): return UIComponent(QMessageBox, text=text, windowTitle=title, **props) # 菜单和工具栏 def MenuBar(**props): return UIComponent(QMenuBar, **props) def Menu(title, **props): return UIComponent(QMenu, title=title, **props) def ToolBar(**props): return UIComponent(QToolBar, **props) # 日期和时间控件 def DateEdit(**props): return UIComponent(QDateEdit, **props) def TimeEdit(**props): return UIComponent(QTimeEdit, **props) def DateTimeEdit(**props): return UIComponent(QDateTimeEdit, **props) # 图形控件 def GraphicsView(**props): return UIComponent(QGraphicsView, **props)
UIComponent可以作为QuickUI的一个基类,类似于SwiftUI中的View;
借助于基类可以实现VBox、HBox以及其他基本组件的嵌套使用:
def VBox(*children, **props): return UIComponent(QVBoxLayout, **props)(*children) def HBox(*children, **props): return UIComponent(QHBoxLayout, **props)(*children)
链式调用可以使用如下方法实现:
# 链式调用 def bg_color(self, color): self.styles.append(f"background-color: {color}") return self def text_color(self, color): self.styles.append(f"color: {color}") return self
好的,按照这个思路,我们来使用QuickUI来编写我们日常的UI界面:
from PySide6.QtWidgets import ( QApplication, QMainWindow, QWidget ) from QuickUI import * from QuickUI.component import Button, HBox, Label, VBox, ZBox class MainWindow(QMainWindow): def __init__(self): super().__init__() self.setWindowTitle("我的应用") self.central_widget = QWidget() self.setCentralWidget(self.central_widget) # 调用 build() 方法来创建实际的 Qt 布局对象 self.layout = self.build_ui().build() self.central_widget.setLayout(self.layout) def build_ui(self): return VBox( Label("欢迎使用我的应用"), Label("使用声明式UI的魅力"), HBox( Button("点击我1"), Button("点击我2") ), Button("点击我3"), VBox( Button("点击我4") .bg_color("green") .text_color("white") .width(100) .height(100), Button("点击我5") .bg_color("red") .text_color("white") .width(100) .height(50) ) ) # 主程序 if __name__ == "__main__": from PySide6.QtWidgets import QApplication import sys app = QApplication(sys.argv) window = MainWindow() window.show() sys.exit(app.exec())
看到了一颗UITree,是不是有那味儿:
def build_ui(self): return VBox( Label("欢迎使用我的应用"), Label("使用声明式UI的魅力"), HBox( Button("点击我1"), Button("点击我2") ), Button("点击我3"), VBox( Button("点击我4") .bg_color("green") .text_color("white") .width(100) .height(100), Button("点击我5") .bg_color("red") .text_color("white") .width(100) .height(50) ) )
看下页面运行结果:
2.State状态更新:
class Counter(UIComponent): def __init__(self): super().__init__(QWidget) self.state = {'count': 0} def increment(self): self.state['count'] += 1 self.update_ui() def build_ui(self): return VBox( Label(lambda: f"Count: {self.state['count']}"), Button("Increment", clicked=self.increment) ) def build(self, parent=None): self.widget = super().build(parent) self.update_ui() return self.widget def update_ui(self): if hasattr(self, 'widget'): new_widget = self.build_ui().build(self.widget.parent()) layout = self.widget.parent().layout() layout.replaceWidget(self.widget, new_widget) self.widget.deleteLater() self.widget = new_widget
可以看到类似与React中hooks的state更新后,对应的UI更新需要自己手动触发,而无法直接像React那样可以自动更新;
3.Diff算法
def update_ui(self): if hasattr(self, 'widget'): new_widget = self.build_ui().build(self.widget.parent()) layout = self.widget.parent().layout() layout.replaceWidget(self.widget, new_widget) self.widget.deleteLater() self.widget = new_widget
由于现在界面更新都是使用replace方式实现的,那么肯定会造成组件的重复创建和渲染的问题;那么Diff算法就是重中之重了;Diff算法可以参考很多现成的Diff算法,如Fiber等,但是也需要因地制宜式的设计对应的PyQt的Diff逻辑;
缺点:
这样虽然可以近似的模拟出声明式UI的开发体验,但是对于基建UIComponent和其他复杂组件的封装的时间投入是巨大的,如果说你们公司对于PyQt爱不释手,可以长久的做3,4年或者更久,那么你值得去做一层这样的封装,之前在前司内部论坛上有同学进一步封装SwiftUI后,使用SwiftUI开发达到相比使用UIKit+OC的人效比的3.5倍;即:SwiftUI完成同样的工作只需要UIKit所需时间的大约28.6%
所以对于某些无历史负债场景,使用声明式UI框架技术栈毫无异议应该作为第一技术栈;
更进一步:
PyQt:
- PyQt实际上是一个包装器(wrapper),它将Qt的C++接口暴露给Python。
- 当你使用PyQt编写Python代码时,PyQt会将你的Python调用转换为对应的C++ Qt调用。
那么其实还可以抛弃Python这一层:
使用一套更加偏向于现代化的DSL,然后将他们编译成C++代码,来直接调用QT实现,以达到声明式UI来开发跨平台应用的体验:
// DSL Example: /* Window { title: "My App" width: 300 height: 200 VBox { Button { text: "Click me" onClicked: { console.log("Button clicked!") } } TextField { placeholder: "Enter text" onTextChanged: { console.log("Text changed: " + text) } } } } */ // Generated C++ Code: #include <QApplication> #include <QMainWindow> #include <QPushButton> #include <QLineEdit> #include <QVBoxLayout> #include <QDebug> int main(int argc, char *argv[]) { QApplication app(argc, argv); QMainWindow window; window.setWindowTitle("My App"); window.resize(300, 200); QWidget *centralWidget = new QWidget(&window); QVBoxLayout *vboxLayout = new QVBoxLayout(centralWidget); QPushButton *button = new QPushButton("Click me", centralWidget); QObject::connect(button, &QPushButton::clicked, []() { qDebug() << "Button clicked!"; }); vboxLayout->addWidget(button); QLineEdit *textField = new QLineEdit(centralWidget); textField->setPlaceholderText("Enter text"); QObject::connect(textField, &QLineEdit::textChanged, [](const QString &text) { qDebug() << "Text changed:" << text; }); vboxLayout->addWidget(textField); window.setCentralWidget(centralWidget); window.show(); return app.exec(); } // DSL Parser (Pseudocode): class DslParser { public: static std::unique_ptr<QWidget> parse(const std::string& dsl) { // Implement parsing logic here // This would involve tokenizing the DSL, building an AST, // and then generating the corresponding Qt widgets and layout } }; // Code Generator (Pseudocode): class CodeGenerator { public: static std::string generateCpp(const std::unique_ptr<QWidget>& rootWidget) { // Implement code generation logic here // This would involve traversing the widget tree and // generating the corresponding C++ code using Qt } };
最后,开发一套新的DSL来支持声明式QT肯定是最佳的,但是解析器,代码生成器等以及IDE插件都是比较耗时的;如果作为一个使用方来讲,那么从投入产出比来讲,肯定不太值得去做;
但是如果你是一只有梦想的爱坤,那么值得去做!