事件传播机制 与 责任链模式
1、基本概念
责任链模式(Chain of Responsibility Pattern)是一种行为型设计模式,将请求沿着处理链传递,直到有一个对象能够处理为止。
2、实现的模块有:
Handler(处理者):定义一个处理请求的接口。
ConcreteHandler(具体处理者):实现了处理者接口,判断自己是否能够处理请求,如果不能将请求传递给下一个处理者。
Request(请求):封装了请求的信息,通常作为处理者方法的参数传递。
3、使用场景
当需要将请求的发送者和接收者进行解耦时。
当有多个对象可以处理同一请求,但不确定哪个对象应该处理时。
当需要动态指定处理请求的顺序时。
4、责任链模式的C++实现:
#include <iostream>
#include <memory>
#include <string> // 抽象处理器类
class Handler {
public: virtual ~Handler() {} // 处理请求的方法 virtual void HandleRequest(const std::string& request) { if (successor_) { successor_->HandleRequest(request); } } // 设置后继处理器 void setSuccessor(std::shared_ptr<Handler> successor) { successor_ = successor; } protected: std::shared_ptr<Handler> successor_;
}; // 具体处理器类A
class ConcreteHandlerA : public Handler {
public: void HandleRequest(const std::string& request) override { if (request == "A") { std::cout << "ConcreteHandlerA handles the request: " << request << std::endl; } else { Handler::HandleRequest(request); } }
}; // 具体处理器类B
class ConcreteHandlerB : public Handler {
public: void HandleRequest(const std::string& request) override { if (request == "B") { std::cout << "ConcreteHandlerB handles the request: " << request << std::endl; } else { Handler::HandleRequest(request); } }
}; // 具体处理器类C
class ConcreteHandlerC : public Handler {
public: void HandleRequest(const std::string& request) override { if (request == "C") { std::cout << "ConcreteHandlerC handles the request: " << request << std::endl; } else { Handler::HandleRequest(request); } }
}; int main() { std::shared_ptr<Handler> handlerA = std::make_shared<ConcreteHandlerA>(); std::shared_ptr<Handler> handlerB = std::make_shared<ConcreteHandlerB>(); std::shared_ptr<Handler> handlerC = std::make_shared<ConcreteHandlerC>(); // 设置责任链 handlerA->setSuccessor(handlerB); handlerB->setSuccessor(handlerC); // 发送请求 handlerA->HandleRequest("A"); handlerA->HandleRequest("B"); handlerA->HandleRequest("C"); handlerA->HandleRequest("D"); return 0;
}
在上述示例中,Handler是抽象处理器类,定义了处理请求的方法。ConcreteHandlerA、ConcreteHandlerB和ConcreteHandlerC是具体处理器类,分别处理请求A、B和C。在创建这些处理器对象时,按照责任链的顺序将它们连接起来。在main函数中,程序创建了一个责任链,将请求依次发送给处理器A、B、C,如果没有任何处理器能够处理请求,则请求不会被处理。
总之,责任链模式是一种非常有用的设计模式,它可以将请求和处理请求的对象解耦,从而提高系统的灵活性和可扩展性。在实际应用中,它可以帮助我们解决很多复杂的问题,提高系统的处理能力和吞吐量,同时也提高了代码的可维护性和可读性。
5、Qt事件传播机制
QT源码:事件系统
QT的事件处理系统同样用到了事件处理系统,其中事件通过事件队列发送到对应的对象,每个对象都可以处理该事件,如果该对象无法处理,将会发给下一个对象。
以下是 QAppliaction 发送鼠标事件给 QWidget 的部分源码:
//接收鼠标事件的对象w
QWidget* w = static_cast<QWidget *>(receiver);
//鼠标事件e
QMouseEvent* mouse = static_cast<QMouseEvent*>(e);......while (w) {//创建一个新的鼠标事件对象,用于在对象树中传播鼠标事件QMouseEvent me(mouse->type(), relpos, mouse->windowPos(), mouse->globalPos(),mouse->button(), mouse->buttons(), mouse->modifiers(), mouse->source());......//如果鼠标事件被接受,打破循环eventAccepted = (w == receiver ? mouse : &me)->isAccepted();if (res && eventAccepted)break;......//如果鼠标事件未被接受,将w设置为w的父组件,继续循环w = w->parentWidget();
}
可以看出, QApplication 将鼠标事件沿着对象树传递,直到有一个对象能够处理为止,符合责任链模式的思想,其中:
QObject:Handler(处理者),定义一个处理鼠标事件的接口。
QWidgt:ConcreteHandler(具体处理者),实现了 QObject 的接口,判断自己是否能够处理鼠标事件,如果不能将请求传递给父类 QWidget。
QMouseEvent:Request(请求),封装了鼠标事件的信息。
QAppliaction:客户端,是请求的发起者。
以上即可展现责任链模式在QT中应用。如果在意细节,可以看下方第6条,否则,后面不用看!
6、Qt事件传播机制 具体细节
事件传播机制和对象树机制共同构成了Qt中对象的一种管理方式和事件的一种传播方式。通过在对象之间建立处理请求的责任链,可以使请求的处理与请求的发起者解耦,提高代码的可扩展性和可维护性。另外,Qt还提供了QObject::installEventFilter
方法,即可以安装一个事件过滤器来处理事件。事件过滤器是一个单独的对象,它可以拦截并处理一个或多个对象的事件。因此,事件过滤器也可以看作责任链模式中的一环。
下面给出Qt源码中如何实现事件传播机制,并且体现出责任链模式的一个示例。注意,这个示例省略了比较多的细节,但作为示例去描述Qt源码中如何体现责任链模式,应该是勉强足够的。
#include <iostream>enum EventType { UnknownEvent, MouseButtonPressEvent };class QEvent {
public:explicit QEvent(EventType type) : _type(type), _accepted(false) {}virtual ~QEvent() {}EventType type() const { return _type; }void accept() { _accepted = true; }void ignore() { _accepted = false; }bool isAccepted() const { return _accepted; }private:EventType _type;bool _accepted;
};class QObject {
public:virtual ~QObject() {}virtual bool event(QEvent *event) {// 默认实现,不处理任何事件return false;}virtual bool eventFilter(QObject *, QEvent *) {// 默认实现,不处理任何事件return false;}
};class QWidget : public QObject {
public:virtual ~QWidget() {}bool event(QEvent *event) override {switch (event->type()) {case MouseButtonPressEvent:mousePressEvent(event);return true;default:return QObject::event(event);}}virtual void mousePressEvent(QEvent *event) {std::cout << "QWidget: Mouse button press event\n";event->accept();}
};
通过上面的示例,可以看到,QObject
,QWidget
和QEvent
三个类的设计和实现,体现了责任链模式。
QWidget
,继承自QObject
,重写了event()
方法,专门处理用户界面事件。例如,当按键事件发生,我们可以获取按下的键码,然后调用事件的accept()
方法表示处理完成,从而阻止事件继续向上传递。如果QWidget
无法处理事件,它会将事件传递给父对象。
QEvent
是所有事件的基类,每个事件有独特的类型标识符。QEvent
类还提供了isAccepted()
、accept()
和ignore()
方法,标记事件是否被接受和处理。一旦事件被处理,我们可以通过调用accept()
方法停止事件的传递,避免重复处理。
作为所有Qt对象的基类,QObject
通过提供event()
和eventFilter()
两个虚函数,构建了事件处理的基础。其中event()
负责处理自身接收到的事件,eventFilter()
则处理需要过滤的事件。如果事件未被处理,它会被传递给父对象,形成责任链。那么传递给父对象的逻辑,在哪里呢?往QWidget::event()
方法里打断点调试就会发现,调用栈会经过QApplication::notify(QObject *receiver, QEvent *e)
,也就是说,事件的分发回经过里面QApplication::notify
。切换到这个方法的调用点查看源代码,可以看到这么一段逻辑:
bool QApplication::notify(QObject* receiver, QEvent* e)
{//其他逻辑//...bool res = false;switch (e->type()) {//处理其他事件类型//...case QEvent::MouseButtonPress:case QEvent::MouseButtonRelease:case QEvent::MouseButtonDblClick:case QEvent::MouseMove:{QWidget* w = static_cast<QWidget*>(receiver);QMouseEvent* mouse = static_cast<QMouseEvent*>(e);QPoint relpos = mouse->pos();if (e->spontaneous()) {if (e->type() != QEvent::MouseMove)QApplicationPrivate::giveFocusAccordingToFocusPolicy(w, e, relpos);// ### Qt 5 These dynamic tool tips should be an OPT-IN feature. Some platforms// like OS X (probably others too), can optimize their views by not// dispatching mouse move events. We have attributes to control hover,// and mouse tracking, but as long as we are deciding to implement this// feature without choice of opting-in or out, you ALWAYS have to have// tracking enabled. Therefore, the other properties give a false sense of// performance enhancement.if (e->type() == QEvent::MouseMove && mouse->buttons() == 0&& w->rect().contains(relpos)) { // Outside due to mouse grab?d->toolTipWidget = w;d->toolTipPos = relpos;d->toolTipGlobalPos = mouse->globalPos();QStyle* s = d->toolTipWidget->style();int wakeDelay = s->styleHint(QStyle::SH_ToolTip_WakeUpDelay, 0, d->toolTipWidget, 0);d->toolTipWakeUp.start(d->toolTipFallAsleep.isActive() ? 20 : wakeDelay, this);}}bool eventAccepted = mouse->isAccepted();QPointer<QWidget> pw = w;while (w) {QMouseEvent me(mouse->type(), relpos, mouse->windowPos(), mouse->globalPos(),mouse->button(), mouse->buttons(), mouse->modifiers(), mouse->source());me.spont = mouse->spontaneous();me.setTimestamp(mouse->timestamp());QGuiApplicationPrivate::setMouseEventFlags(&me, mouse->flags());// throw away any mouse-tracking-only mouse eventsif (!w->hasMouseTracking()&& mouse->type() == QEvent::MouseMove && mouse->buttons() == 0) {// but still send them through all application event filters (normally done by notify_helper)d->sendThroughApplicationEventFilters(w, w == receiver ? mouse : &me);res = true;}else {w->setAttribute(Qt::WA_NoMouseReplay, false);res = d->notify_helper(w, w == receiver ? mouse : &me);e->spont = false;}eventAccepted = (w == receiver ? mouse : &me)->isAccepted();if (res && eventAccepted)break;if (w->isWindow() || w->testAttribute(Qt::WA_NoMousePropagation))break;relpos += w->pos();w = w->parentWidget();}mouse->setAccepted(eventAccepted);if (e->type() == QEvent::MouseMove) {if (!pw)break;w = static_cast<QWidget*>(receiver);relpos = mouse->pos();QPoint diff = relpos - w->mapFromGlobal(d->hoverGlobalPos);while (w) {if (w->testAttribute(Qt::WA_Hover) &&(!QApplication::activePopupWidget() || QApplication::activePopupWidget() == w->window())) {QHoverEvent he(QEvent::HoverMove, relpos, relpos - diff, mouse->modifiers());d->notify_helper(w, &he);}if (w->isWindow() || w->testAttribute(Qt::WA_NoMousePropagation))break;relpos += w->pos();w = w->parentWidget();}}d->hoverGlobalPos = mouse->globalPos();}break;//处理其他事件类型//...}return res;
}
我们在这里只关心鼠标事件(QEvent::MouseButtonPress
,QEvent::MouseButtonRelease
,QEvent::MouseButtonDblClick
,QEvent::MouseMove:
)的处理。下面分析一下关键的程序段:
1. 在处理鼠标事件时,首先获取当前QWidget
对象w
。
QWidget* w = static_cast<QWidget *>(receiver);
2. 把事件循环遍历至最高级父窗口或直到一个处理了事件的窗口。在此过程中,对于没有成功处理鼠标事件的窗口,事件会沿着窗口的父链继续传递。
while (w) {// 将事件发送给QWidget对象w,尝试让其处理// ...// 当事件已经被接收并得到处理,跳出循环if (res && eventAccepted)break;// 当到达顶层窗口或当前窗口标记为Qt::WA_NoMousePropagation (无鼠标事件传递)时,跳出循环if (w->isWindow() || w->testAttribute(Qt::WA_NoMousePropagation))break;// 获取父窗口,在while循环下一轮尝试让父窗口去处理w = w->parentWidget();
}
在这个while循环中,就会不断地去获取父对象,尝试让其处理。事件得到处理,或者当到达顶层窗口,或者当前窗口标记为Qt::WA_NoMousePropagation
(无鼠标事件传递)时,跳出循环。这里就体现出了事件处理的传递链。
可以继续解析上面的程序示例中,"将事件发送给QWidget对
象w,尝试让其处理"部分没有展示出的代码:
1. 根据收到的原始鼠标事件(mouse
)创建一个新的鼠标事件(me
),并使用正确的相对位置(relpos
)更新它。同时设置一些其他属性,如是否自发、时间戳和鼠标事件标志。
QMouseEvent me(mouse->type(), relpos, mouse->windowPos(), mouse->globalPos(),mouse->button(), mouse->buttons(), mouse->modifiers(), mouse->source());
me.spont = mouse->spontaneous();
me.setTimestamp(mouse->timestamp());
QGuiApplicationPrivate::setMouseEventFlags(&me, mouse->flags());
2. 对于没有鼠标跟踪并且是无按钮按下的鼠标移动事件(仅用于鼠标跟踪的事件),忽略这个事件,并将其传递给所有应用程序事件过滤器。设置结果变量res = true
表示事件已被处理。
if (!w->hasMouseTracking()&& mouse->type() == QEvent::MouseMove && mouse->buttons() == 0) {// but still send them through all application event filters (normally done by notify_helper)d->sendThroughApplicationEventFilters(w, w == receiver ? mouse : &me);res = true;
}
3. 否则,尝试将mouse
或me
事件传递给QWidget
对象(视情况而定)。更新事件的自发属性为false
。
else {w->setAttribute(Qt::WA_NoMouseReplay, false);res = d->notify_helper(w, w == receiver ? mouse : &me);e->spont = false;
}
4. 检查事件是否已被接受。这个步骤会影响后续窗口的事件传递。
eventAccepted = (w == receiver ? mouse : &me)->isAccepted();
综上,QObject
作为事件处理接口,QWidget
实现具体的处理逻辑,而QEvent
则扮演了请求的角色,QApplication
参与了事件的分发和向上传递。这几个类的协作为Qt提供了一种高效、灵活且可扩展的事件处理机制,帮助开发者轻松处理各种事件和场景。
总结
至此,我们已经解析了Qt的对象树机制,构建在对象树机制之上的事件传递机制,以及它们背后的设计思想。在后续的文章中,我们可能还会继续解析Qt源码中事件机制相关的设计模式。事件机制确实是Qt中的核心机制之一,我们能够深挖的东西,恐怕还有很多。
原文链接:https://blog.csdn.net/buhuiCyvyan/article/details/138793057
原文链接:https://zhuanlan.zhihu.com/p/631330647
相关文章:
事件传播机制 与 责任链模式
1、基本概念 责任链模式(Chain of Responsibility Pattern)是一种行为型设计模式,将请求沿着处理链传递,直到有一个对象能够处理为止。 2、实现的模块有: Handler(处理者):定义一个…...

uniapp 展示地图,并获取当前位置信息(精确位置)
使用uniapp 提供的map标签 <map :keymapIndex class"container" :latitude"latitude" :longitude"longitude" ></map> 页面初始化的时候,获取当前的位置信息 created() {let that thisuni.getLocation({type: gcj02…...
Autosar实践——诊断配置(DaVinci Configuration)
文章目录 一、制作诊断数据库文件(cdd文件)二、导入诊断数据库文件并修复模块生成的问题三、创建SWC CS接口Service Ports四、创建Service Runnable五、关联SWC和DCM/DEM模块六、RTE代码编写22服务2E服务31服务DTC Set/Get关联文章列表: Autosar-软件架构 Autosar诊断-简介和…...

植物大战僵尸杂交版全新版v2.1解决全屏问题
文章目录 🚋一、植物大战僵尸杂交版❤️1. 游戏介绍💥2. 如何下载《植物大战僵尸杂交版》 🚀二、解决最新2.1版的全屏问题🌈三、画质增强以及减少闪退 🚋一、植物大战僵尸杂交版 《植物大战僵尸杂交版》是一款在原版《…...

【code-server】Code-Server 安装部署
Code-Server 安装部署 1.环境准备 可以参考 https://coder.com/docs/code-server/install code-server的安装流程进行安装,主机环境是 Centos7 建议使用 docker 方式进行安装,可能会出现如下报错,需要升级 GNC 的版本,由于影响较…...
博客摘录「 YOLOv5模型剪枝压缩」2024年5月11日
添加L1正则来约束BN层系数 语义边缘检测和语义分割的关系调研结果为,语义信息可以用来增强语义分割的效果,也有一定的优点和采用理由,但此类论文的数量并不是很多,语义分割的多数方法还是使用深度学习直接做像素分类。在对比两者…...
HttpSecurity
这是Spring Security提供的配置类, 用户保护基于HTTP的请求 ,通过HttpSecurity可以设置各种安全设置{认证,授权,CSRF保护,会话管理,异常处理} 主要功能和配置: 1.认证配置: 配置登录和登出功能,指定登录页面、登录处理 URL、成功和失败处理器等。配置认证方式,如表单登录、…...
Mysql union语句
开源项目SDK:https://github.com/mingyang66/spring-parent 个人文档:https://mingyang66.github.io/raccoon-docs/#/ mysql union操作符用于连接两个以上的select语句的结果组合到一个结果集,并去除重复的行,每个select语句的雷叔…...

MySQL之高级特性(四)
高级特性 查询缓存 什么情况下查询缓存能发挥作用 并不是什么情况下查询缓存都会提高系统性能的。缓存和失效都会带来额外的消耗,所以只有当缓存带来的资源节约大于本身的资源消耗时才会给系统带来性能提升。这跟具体的服务器压力模型有关。理论上,可…...
roles安装wordpress
debug模块 1.如何查看ansible-playbook执行过程中产生的具体信息 vim test3.yaml --- - hosts: allremote_user: roottasks:- name: lsshell: ls /rootregister: var_stdout # register:将var_stdout注册为变量- name: debugdebug:var: var_stdout # 查看所有的输出信息#var…...
【Python高级编程】饼状图中autopct和startangle用来做什么的
autopct 设置饼状图中每个扇区的百分比标签。接受一个格式字符串,用于指定如何格式化标签。默认值为 %.1f%%,表示保留一位小数的百分比格式。可以设置为 None 以禁用百分比标签。 startangle 设置饼状图中第一个扇区的起始角度。角度以顺时针方向从 3…...

【ARM Coresight Debug 系列 -- ARMv8/v9 Watchpoint 软件实现地址监控详细介绍】
请阅读【嵌入式开发学习必备专栏 】 文章目录 ARMv8/v9 Watchpoint exceptionsWatchpoint 配置信息读取Execution conditionsWatchpoint data address comparisonsSize of the data accessWatchpoint 软件配置流程Watchpoint Type 使用介绍WT, Bit [20]: Watchpoint TypeLBN, B…...
jvm工具-jps、jstat、jmap、jstack
一、jps jps -v 【输出进程启动参数】 [rootVM-8-2-centos ~]# jps -v 12401 Jps -Dapplication.home/usr/local/jdk1.8.0_241 -Xms8m 16964 jar 其他参考 Java八股文必看,入门到深入理解jvm虚拟机之基础故障指令【jps,jstate...】-CSDN博客 二、j…...

LVS负载均衡群集+NAT部署
目录 一、企业群集应用概述 1.1 群集的含义 1.2 企业群集分类 二、负载均衡群集架构和工作模式 2.1负载均衡的结构 2.2负载均衡群集工作模式分析 三、LVS虚拟服务器 3.1Linux Virtual Server 3.2LVS必要的工具 3.3LVS的负载调度算法 一、企业群集应用概述 1.1 群集的…...

使用 Oracle SQL Developer 导入数据
使用 Oracle SQL Developer 导入数据 1. 导入过程 1. 导入过程 选择要导入数据的表, 然后单击右键,选择"导入数据", 浏览本地文件,选择正确的工作表, 按默认, 按默认, 根据情况修改&…...
品质主管的面试题目
在品质主管的面试中,面试官可能会提出一系列问题来评估应聘者的经验、技能和专业知识。以下是一些常见的品质主管面试题,你可以提前准备,以更好地展示自己的能力和适应性。 一、自我介绍与背景了解 请简单介绍一下自己,包括教育背景、工作经验等。你在过去的工作经历中,主…...

算法专题总结链接地址
刷力扣的时候会遇到一些总结类型的题解,在此记录,方便自己以后找 前缀和 前缀和https://leetcode.cn/problems/unique-substrings-in-wraparound-string/solutions/432752/xi-fa-dai-ni-xue-suan-fa-yi-ci-gao-ding-qian-zhui-/ 单调栈 单调栈https:…...

Oracle--存储结构
总览 一、逻辑存储结构 二、物理存储结构 1.数据文件 2.控制文件 3.日志文件 4.服务器参数文件 5.密码文件 总览 一、逻辑存储结构 数据块是Oracle逻辑存储结构中的最小的逻辑单位,一个数据库块对应一个或者多个物理块,大小由参数DB_BLOCK_SIZE决…...

【计算机毕业设计】259基于微信小程序的医院综合服务平台
🙊作者简介:拥有多年开发工作经验,分享技术代码帮助学生学习,独立完成自己的项目或者毕业设计。 代码可以私聊博主获取。🌹赠送计算机毕业设计600个选题excel文件,帮助大学选题。赠送开题报告模板ÿ…...

HP惠普暗影精灵10 OMEN Gaming Laptop 16-wf1xxx原厂Win11系统镜像下载
惠普hp暗影精灵10笔记本电脑16-wf1000TX原装出厂Windows11,恢复开箱状态oem预装系统安装包,带恢复重置还原 适用型号:16-wf1xxx 16-wf1000TX,16-wf1023TX,16-wf1024TX,16-wf1025TX, 16-wf1026TX,16-wf1027TX,16-wf1028TX,16-wf1029TX, 16-wf1030TX,16-…...
什么?连接服务器也能可视化显示界面?:基于X11 Forwarding + CentOS + MobaXterm实战指南
文章目录 什么是X11?环境准备实战步骤1️⃣ 服务器端配置(CentOS)2️⃣ 客户端配置(MobaXterm)3️⃣ 验证X11 Forwarding4️⃣ 运行自定义GUI程序(Python示例)5️⃣ 成功效果是Android应用内存占用的“头号杀手”。一张1080P(1920x1080)的图片以ARGB_8888格式加载时,内存占用高达8MB(192010804字节)。据统计,超过60%的应用OOM崩溃与Bitm…...

selenium学习实战【Python爬虫】
selenium学习实战【Python爬虫】 文章目录 selenium学习实战【Python爬虫】一、声明二、学习目标三、安装依赖3.1 安装selenium库3.2 安装浏览器驱动3.2.1 查看Edge版本3.2.2 驱动安装 四、代码讲解4.1 配置浏览器4.2 加载更多4.3 寻找内容4.4 完整代码 五、报告文件爬取5.1 提…...

2025季度云服务器排行榜
在全球云服务器市场,各厂商的排名和地位并非一成不变,而是由其独特的优势、战略布局和市场适应性共同决定的。以下是根据2025年市场趋势,对主要云服务器厂商在排行榜中占据重要位置的原因和优势进行深度分析: 一、全球“三巨头”…...

C++使用 new 来创建动态数组
问题: 不能使用变量定义数组大小 原因: 这是因为数组在内存中是连续存储的,编译器需要在编译阶段就确定数组的大小,以便正确地分配内存空间。如果允许使用变量来定义数组的大小,那么编译器就无法在编译时确定数组的大…...
QT3D学习笔记——圆台、圆锥
类名作用Qt3DWindow3D渲染窗口容器QEntity场景中的实体(对象或容器)QCamera控制观察视角QPointLight点光源QConeMesh圆锥几何网格QTransform控制实体的位置/旋转/缩放QPhongMaterialPhong光照材质(定义颜色、反光等)QFirstPersonC…...
JavaScript 数据类型详解
JavaScript 数据类型详解 JavaScript 数据类型分为 原始类型(Primitive) 和 对象类型(Object) 两大类,共 8 种(ES11): 一、原始类型(7种) 1. undefined 定…...

windows系统MySQL安装文档
概览:本文讨论了MySQL的安装、使用过程中涉及的解压、配置、初始化、注册服务、启动、修改密码、登录、退出以及卸载等相关内容,为学习者提供全面的操作指导。关键要点包括: 解压 :下载完成后解压压缩包,得到MySQL 8.…...

【Linux】自动化构建-Make/Makefile
前言 上文我们讲到了Linux中的编译器gcc/g 【Linux】编译器gcc/g及其库的详细介绍-CSDN博客 本来我们将一个对于编译来说很重要的工具:make/makfile 1.背景 在一个工程中源文件不计其数,其按类型、功能、模块分别放在若干个目录中,mak…...

AI语音助手的Python实现
引言 语音助手(如小爱同学、Siri)通过语音识别、自然语言处理(NLP)和语音合成技术,为用户提供直观、高效的交互体验。随着人工智能的普及,Python开发者可以利用开源库和AI模型,快速构建自定义语音助手。本文由浅入深,详细介绍如何使用Python开发AI语音助手,涵盖基础功…...