#!/usr/bin/python # -*- coding: utf-8 -*- """ MVVM 小框架核心模块。 这个文件实现了一个极简的“发布-订阅”事件通道,并在它之上封装 Model 与 View 的绑定关系。demo 中的界面刷新不是由 ViewModel 直接 调用 View 完成,而是由 Model 发布事件,再由订阅者 View 响应事件。 """ from threading import Thread, Lock from queue import Queue from enum import Enum, unique class Subscriber(object): """ 订阅者。 一个订阅者由两部分组成: 1. topic:它关心的事件主题。 2. callback:主题事件发生时要执行的回调函数。 """ def __init__(self, topic, callback): # 保存订阅主题,例如 "log_to_view"。 self._topic = topic # 保存事件到来时要调用的函数,例如 view.signal_proxy。 self._callback = callback @property def topic(self): """返回订阅者关注的主题。""" return self._topic @property def callback(self): """返回事件触发时要执行的回调函数。""" return self._callback class Publisher(object): """ 发布者/事件对象。 这里的 Publisher 不是一个长期存在的对象,而是一次事件发布时 放进队列里的消息包:topic 表示发给谁,content 表示传什么数据。 """ def __init__(self, topic, content): # 事件主题,决定这条消息会被哪些订阅者消费。 self._topic = topic # 事件内容,也就是回调函数收到的参数。 self._content = content @property def topic(self): """返回事件主题。""" return self._topic @property def content(self): """返回事件内容。""" return self._content # 保留主题:当前 demo 没有使用,可作为以后“不触发实际业务”的占位主题。 KEEP_TOPIC = 'keep' class EventChannel(Thread): """ 事件通道。 它继承 Thread,内部维护一个 Queue。发布事件时先把事件放入队列; 线程运行后不断从队列取事件,再根据 topic 找到对应订阅者并执行回调。 """ def __init__(self): super(EventChannel, self).__init__() # 控制事件循环是否继续运行。 self._working = True # 保护订阅关系表,避免多个线程同时读写造成状态错乱。 self._mutex = Lock() # 存放待处理事件的线程安全队列。 self._queue = Queue() # 订阅关系表:{"topic": [Subscriber, Subscriber, ...]}。 self._resigster_container = {} def __del__(self): # 对象销毁时尝试停止循环。注意:如果线程正阻塞在 queue.get(), # 仅设置 False 不一定能立即退出,这里只作为简单 demo 处理。 self._working = False def run(self) -> None: """线程入口:持续消费事件队列,并通知对应订阅者。""" while self._working: # Queue.get() 会阻塞等待新事件,因此线程不会空转占用 CPU。 puber = self._queue.get() with self._mutex: print('pop event {} : {}'.format(puber.topic, puber.content)) # 找到该 topic 下的所有订阅者;无人订阅时返回空列表。 group = self._resigster_container.get(puber.topic, []) for suber in group: # 将事件内容传给订阅者的回调函数。 suber.callback(puber.content) def publish(self, topic: str, content): """发布事件:把主题和内容包装成 Publisher 后放入队列。""" puber = Publisher(topic, content) self._queue.put(puber) def subscribe(self, topic: str, callback): """订阅事件:把某个 topic 与对应回调函数登记到订阅关系表中。""" suber = Subscriber(topic, callback) with self._mutex: group = self._resigster_container.get(suber.topic, []) group.append(suber) self._resigster_container[suber.topic] = group class BaseModel(object): """ Model 基类。 子类修改自身数据时,一般会通过 fw_proxy.channel.publish 发布事件; 当 View 的事件反向更新 Model 时,则由 signal_proxy 接收新值。 """ def __init__(self, topic: str): # 每个 Model 都有自己的主题,用来向外通知“我变了”。 self._topic = topic @property def topic(self): """返回 Model 对应的发布主题。""" return self._topic def signal_proxy(self, *args): """事件代理函数。子类通常会重写它来更新自身状态。""" print('{} event proxy run'.format(type(self))) class BaseView(object): """ View 基类。 View 负责界面显示。它收到 Model 发布的事件后,在 signal_proxy 中 把数据刷新到具体控件上。 """ def __init__(self, topic: str): # 每个 View 也有自己的主题,做双向绑定时可用来通知 Model。 self._topic = topic @property def topic(self): """返回 View 对应的发布主题。""" return self._topic def signal_proxy(self, *args): """事件代理函数。子类通常会重写它来刷新界面控件。""" print('{} event proxy run'.format(type(self))) @unique class BindingStyle(Enum): """绑定方式枚举。""" DOUBLE_BINDING = 0 # 双向绑定:Model -> View,同时 View -> Model。 MODEL_TO_VIEW = 1 # 单向绑定:Model 改变后刷新 View。 VIEW_TO_MODEL = 2 # 单向绑定:View 改变后更新 Model。 class Framework(object): """框架入口对象,负责启动事件通道并建立绑定关系。""" def __init__(self): # 整个框架共用一个事件通道。 self.channel = EventChannel() def work(self): """启动事件通道线程。""" # 设为守护线程:主程序退出时,该事件线程也会随之结束。 self.channel.daemon = True self.channel.start() def binding(self, model: BaseModel, view: BaseView, style=BindingStyle.DOUBLE_BINDING): """ 建立 Model 与 View 的绑定。 本质是把某一方的 topic 订阅到另一方的 signal_proxy。 """ if style == BindingStyle.DOUBLE_BINDING: # Model 发布变化时,View 响应并刷新界面。 self.channel.subscribe(model.topic, view.signal_proxy) # View 发布变化时,Model 响应并更新数据。 self.channel.subscribe(view.topic, model.signal_proxy) elif style == BindingStyle.MODEL_TO_VIEW: # 模型改变引起视图改变。 self.channel.subscribe(model.topic, view.signal_proxy) elif style == BindingStyle.VIEW_TO_MODEL: # 视图改变引起模型改变。 self.channel.subscribe(view.topic, model.signal_proxy) else: raise RuntimeWarning('binding is wrong') # 框架单例对象:其他模块通过 fw_proxy 使用同一个事件通道。 fw_proxy = Framework()