初始化

This commit is contained in:
rxy
2026-05-18 11:33:59 +08:00
commit b22e46dd60
20 changed files with 1440 additions and 0 deletions

2
demo/.idea/.gitignore generated vendored Normal file
View File

@@ -0,0 +1,2 @@
# Default ignored files
/workspace.xml

8
demo/.idea/demo.iml generated Normal file
View File

@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="PYTHON_MODULE" version="4">
<component name="NewModuleRootManager">
<content url="file://$MODULE_DIR$" />
<orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" />
</component>
</module>

View File

@@ -0,0 +1,6 @@
<component name="InspectionProjectProfileManager">
<settings>
<option name="USE_PROJECT_PROFILE" value="false" />
<version value="1.0" />
</settings>
</component>

4
demo/.idea/misc.xml generated Normal file
View File

@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectRootManager" version="2" project-jdk-name="Python 3.6 (audio_project_py36) (2)" project-jdk-type="Python SDK" />
</project>

8
demo/.idea/modules.xml generated Normal file
View File

@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectModuleManager">
<modules>
<module fileurl="file://$PROJECT_DIR$/.idea/demo.iml" filepath="$PROJECT_DIR$/.idea/demo.iml" />
</modules>
</component>
</project>

6
demo/.idea/vcs.xml generated Normal file
View File

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="$PROJECT_DIR$/.." vcs="Git" />
</component>
</project>

1
demo/design/__init__.py Normal file
View File

@@ -0,0 +1 @@
"""Qt Designer 生成界面代码所在包。"""

87
demo/design/main_win.py Normal file
View File

@@ -0,0 +1,87 @@
# -*- coding: utf-8 -*-
# Form implementation generated from reading ui file 'main_win.ui'
#
# Created by: PyQt5 UI code generator 5.13.0
#
# WARNING! All changes made in this file will be lost!
#
# 学习说明:
# 这个文件由 Qt Designer 的 .ui 文件生成,真实项目中通常不手动维护。
# 这里添加中文注释是为了帮助理解界面树、布局和控件名称。
from PyQt5 import QtCore, QtGui, QtWidgets
class Ui_MainWindow(object):
"""Qt Designer 生成的主窗口界面类。"""
def setupUi(self, MainWindow):
"""
创建控件并设置布局。
MainWinView 会调用 self.setupUi(self),因此这里创建出来的控件
会成为 MainWinView 的成员,例如 self.LogBrowser、self.YesBtn。
"""
# 设置主窗口对象名Qt 的样式和自动连接机制可能会用到它。
MainWindow.setObjectName("MainWindow")
# 设置主窗口初始大小。
MainWindow.resize(475, 571)
# 创建中心控件。QMainWindow 必须通过 setCentralWidget 设置主区域。
self.centralwidget = QtWidgets.QWidget(MainWindow)
self.centralwidget.setObjectName("centralwidget")
# 创建垂直布局:上方放日志框,下方放按钮区域。
self.verticalLayout = QtWidgets.QVBoxLayout(self.centralwidget)
self.verticalLayout.setObjectName("verticalLayout")
# 创建日志显示控件View 层会把它包装成 LogView。
self.LogBrowser = QtWidgets.QTextBrowser(self.centralwidget)
self.LogBrowser.setObjectName("LogBrowser")
self.verticalLayout.addWidget(self.LogBrowser)
# 创建按钮区域 frame用来承载水平布局。
self.frame = QtWidgets.QFrame(self.centralwidget)
self.frame.setFrameShape(QtWidgets.QFrame.StyledPanel)
self.frame.setFrameShadow(QtWidgets.QFrame.Raised)
self.frame.setObjectName("frame")
# 创建水平布局:左右 spacer 负责把两个按钮推到中间。
self.horizontalLayout = QtWidgets.QHBoxLayout(self.frame)
self.horizontalLayout.setObjectName("horizontalLayout")
spacerItem = QtWidgets.QSpacerItem(132, 20, QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Minimum)
self.horizontalLayout.addItem(spacerItem)
# 创建 Yes 按钮。binder.py 中会绑定它的 clicked 信号。
self.YesBtn = QtWidgets.QPushButton(self.frame)
self.YesBtn.setObjectName("YesBtn")
self.horizontalLayout.addWidget(self.YesBtn)
# 创建 No 按钮。binder.py 中会绑定它的 clicked 信号。
self.NoBtn = QtWidgets.QPushButton(self.frame)
self.NoBtn.setObjectName("NoBtn")
self.horizontalLayout.addWidget(self.NoBtn)
# 右侧弹性空白,与左侧 spacer 一起让按钮组居中。
spacerItem1 = QtWidgets.QSpacerItem(131, 20, QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Minimum)
self.horizontalLayout.addItem(spacerItem1)
self.verticalLayout.addWidget(self.frame)
# 设置拉伸比例:日志区域占 10 份,按钮区域占 1 份。
self.verticalLayout.setStretch(0, 10)
self.verticalLayout.setStretch(1, 1)
MainWindow.setCentralWidget(self.centralwidget)
# 设置可翻译文本,例如窗口标题和按钮文字。
self.retranslateUi(MainWindow)
# Qt 自动槽连接机制,本 demo 没有依赖它,但生成代码会保留。
QtCore.QMetaObject.connectSlotsByName(MainWindow)
def retranslateUi(self, MainWindow):
"""设置界面上的文字,便于后续做国际化翻译。"""
_translate = QtCore.QCoreApplication.translate
MainWindow.setWindowTitle(_translate("MainWindow", "MainWindow"))
self.YesBtn.setText(_translate("MainWindow", "yes"))
self.NoBtn.setText(_translate("MainWindow", "no"))

78
demo/design/main_win.ui Normal file
View File

@@ -0,0 +1,78 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>MainWindow</class>
<widget class="QMainWindow" name="MainWindow">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>611</width>
<height>571</height>
</rect>
</property>
<property name="windowTitle">
<string>MainWindow</string>
</property>
<widget class="QWidget" name="centralwidget">
<layout class="QVBoxLayout" name="verticalLayout" stretch="0,0">
<item>
<widget class="QTextBrowser" name="LogBrowser"/>
</item>
<item>
<widget class="QFrame" name="frame">
<property name="frameShape">
<enum>QFrame::StyledPanel</enum>
</property>
<property name="frameShadow">
<enum>QFrame::Raised</enum>
</property>
<layout class="QHBoxLayout" name="horizontalLayout">
<item>
<spacer name="horizontalSpacer">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>132</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
<item>
<widget class="QPushButton" name="YesBtn">
<property name="text">
<string>yes</string>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="NoBtn">
<property name="text">
<string>no</string>
</property>
</widget>
</item>
<item>
<spacer name="horizontalSpacer_2">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>131</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
</layout>
</widget>
</item>
</layout>
</widget>
</widget>
<resources/>
<connections/>
</ui>

1
demo/mvvm/__init__.py Normal file
View File

@@ -0,0 +1 @@
"""MVVM demo 包:包含框架、模型、视图、视图模型和绑定逻辑。"""

24
demo/mvvm/binder.py Normal file
View File

@@ -0,0 +1,24 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
"""
绑定层。
binder 的职责是把 View 和 ViewModel 连接起来:
1. 把界面控件信号连接到 ViewModel 的事件函数。
2. 把 Model 与 View 交给框架建立数据绑定。
"""
from mvvm.view_model import *
from mvvm.view import *
def binding_main_ui(ui: MainWinView, vm: MainWinVM):
"""绑定主窗口 UI 与主窗口 ViewModel。"""
# 绑定 PyQt 控件事件:按钮点击后调用 ViewModel 中的业务方法。
ui.YesBtn.clicked.connect(vm.event_log_yes)
ui.NoBtn.clicked.connect(vm.event_log_no)
# 建立双向绑定。
# 当前 demo 实际使用的是 Model -> Viewmodel_log 改变后刷新 view_log。
# 因为 view_log 没有主动发布事件,所以 View -> Model 方向暂时不会触发。
fw_proxy.binding(vm.model_log, ui.view_log)

210
demo/mvvm/framework.py Normal file
View File

@@ -0,0 +1,210 @@
#!/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()

45
demo/mvvm/model.py Normal file
View File

@@ -0,0 +1,45 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
"""
Model 层。
Model 负责保存业务数据。这个 demo 只有一个字符串模型 StringModel
它保存一条日志文本;当日志文本被修改时,会向事件通道发布通知。
"""
from mvvm.framework import fw_proxy, BaseModel
class StringModel(BaseModel):
"""字符串数据模型:保存一个字符串,并在值改变时发布事件。"""
def __init__(self, topic: str, value: str):
# topic 用于告诉框架:这个 Model 的变化应该发布到哪个主题。
super().__init__(topic)
# 真正保存字符串值的私有变量。
self._string = value
@property
def value(self):
"""读取当前字符串值。"""
return self._string
@value.setter
def value(self, value: str):
"""
修改字符串值。
这是 MVVM 自动刷新的关键位置:只要业务代码给 model.value 赋值,
Model 就会发布事件View 会在订阅到事件后自动刷新。
"""
self._string = value
fw_proxy.channel.publish(self._topic, value)
def signal_proxy(self, value: str):
"""
接收 View 反向传来的值。
在双向绑定场景中,如果 View 发布了自己的变化,框架会调用这里,
从而把 View 的新值同步回 Model。
"""
super().signal_proxy()
self._string = value

49
demo/mvvm/view.py Normal file
View File

@@ -0,0 +1,49 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
"""
View 层。
View 负责界面控件的创建、显示和刷新。这里使用 PyQt5 的 QMainWindow
作为主窗口,并把 QTextBrowser 包装成一个 LogView方便接入 MVVM 绑定。
"""
from PyQt5.QtWidgets import *
from mvvm.framework import *
from design import main_win
class LogView(BaseView):
"""日志视图:把收到的日志文本追加显示到 QTextBrowser 中。"""
def __init__(self, topic: str, browser: QTextBrowser):
# topic 表示这个 View 自己发布变化时使用的主题。
super().__init__(topic)
# 保存真正负责显示日志的 PyQt 控件。
self.browser = browser
def signal_proxy(self, log: str):
"""
接收 Model 发来的日志文本,并刷新界面。
在本 demo 中StringModel.value 被修改后会发布 log 文本,
框架事件通道最终会调用这个函数。
"""
super().signal_proxy()
# 将新日志追加到文本浏览器。
self.browser.append(log)
# 文本框显示到底部,让用户总能看到最新日志。
self.browser.moveCursor(self.browser.textCursor().End)
class MainWinView(QMainWindow, main_win.Ui_MainWindow):
"""主窗口 View继承 Qt 主窗口和 Qt Designer 生成的界面类。"""
def __init__(self, parent=None):
super(MainWinView, self).__init__(parent)
# setupUi 会创建 LogBrowser、YesBtn、NoBtn 等控件。
self.setupUi(self)
# 设置窗口标题。
self.setWindowTitle("log演示demo")
# 包装日志控件,让它成为可绑定的 View 对象。
self.view_log = LogView('view_to_log', self.LogBrowser)

26
demo/mvvm/view_model.py Normal file
View File

@@ -0,0 +1,26 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
"""
ViewModel 层。
ViewModel 连接界面操作与业务数据:按钮点击后不直接操作界面,
而是修改 ModelModel 再通过事件通道通知 View 刷新。
"""
from mvvm.model import *
class MainWinVM(object):
"""主窗口 ViewModel保存主窗口需要的数据模型和按钮事件逻辑。"""
def __init__(self):
# 创建日志内容 Model。
# 主题 "log_to_view" 表示:这个 Model 的变化要通知日志 View。
self.model_log = StringModel('log_to_view', '启动日志')
def event_log_yes(self):
"""Yes 按钮点击事件:写入一条 yes 日志。"""
self.model_log.value = 'log: yes!!!'
def event_log_no(self):
"""No 按钮点击事件:写入一条 no 日志。"""
self.model_log.value = 'log: no~~~'

33
demo/start_up.py Normal file
View File

@@ -0,0 +1,33 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
"""
程序入口。
运行这个文件会启动 PyQt5 应用,创建 View 和 ViewModel完成绑定
最后启动框架事件通道并进入 Qt 主事件循环。
"""
import sys
from mvvm.binder import *
if __name__ == "__main__":
# 创建 Qt 应用对象。所有 PyQt 界面程序都需要 QApplication。
app = QApplication(sys.argv)
# 创建主窗口 View负责界面控件与显示。
ui_main_win = MainWinView()
# 创建主窗口 ViewModel负责界面事件背后的逻辑。
vm_main_win = MainWinVM()
# 绑定 View 与 ViewModel。
binding_main_ui(ui_main_win, vm_main_win)
# 显示主窗口。
ui_main_win.show()
# 启动 MVVM 框架的事件循环线程。
fw_proxy.work()
# 进入 Qt 主事件循环,直到窗口关闭。
sys.exit(app.exec_())