Kafka 学习之:基于 flask 框架通过具体案例详解生产消费者模型,这一篇文章就够了

news2024/11/29 2:49:43

文章目录

  • 案例信息介绍
    • 后端异步处理请求和后端同步处理请求
      • 同步方式
      • 异步方式
  • 环境文件目录
  • 配置
    • .env
    • requirements.txt
  • 完整代码
    • ext.py
    • app.py
    • kafka_create_user.py
  • 运行方式
    • 本地安装 kafka
    • 运行 app.py
    • 使用 postman 测试
      • 建立 http 长连接,等待后端处理结果
      • 发送 RAW DATA

  • 在看这个文章之前,建议先学习 kafka的工作原理 这个系列视频讲得很好,虽然基于 Java 但是理解原理并不用区分语言。只需要看懂工作原理即可。

案例信息介绍

  • 假设我的网站需要高并发地处理 user 注册这个简单的功能。
  • 前端会发送 {"user_id": xxxx, "psw":xxx} 的信息到后端完成创建
    • 前端用 postman 来模拟
    • 后端用 flask 框架来简单演示
  • 下面我用一张大致的图来表示代码的架构:
    • 前端的原始数据进入后端之后,后端要用 kafka 的架构在有序地处理 user 的请求,在这个任务中所有 user 的请求都是 register,因此我们就创建一个 kafka 的 topic 专门用来处理 user 的这类请求
    • 同时由于 kafka 是通过队列的方式 异步地处理 user 的请求,所以当 kafka 处理完 user 的请求后,我们需要找到这个处理结果并返回给对应的 user

      如果大家对于 异步处理 user 请求和同步处理没有概念,那么下面一章我先给大家讲一下同步处理请求和异步处理的区别

架构图

后端异步处理请求和后端同步处理请求

同步方式

"""
 @file: app.py.py
 @Time    : 2024/3/30
 @Author  : Peinuan qin
"""
import threading
import json
from flask import Flask, jsonify, request
from flask_cors import CORS
from ext import FLASK_HOST, FLASK_PORT

app = Flask(__name__)
CORS(app)

@app.route("/login", methods=['POST'])
def create_user_post():
    data = request.json
	"""
	register user code ....
	"""
    return jsonify({"status": 200, "msg": "success"})



if __name__ == '__main__':
   	app.run(host=FLASK_HOST, port=FLASK_PORT, debug=True)
  • 上述方式可以看到我的 create_user_post 负责接受前端的数据并且即刻处理,处理之后将结果返回前端 jsonify({"status": 200, "msg": "success"}),这个过程是一行接着一行发生的,如果中途出现了很耗时的操作,那么程序会一直等着。
  • 在 Flask 应用中,如果 "register user code ...." 处理需要20秒,这确实会阻塞处理该请求的线程,直到该过程完成。由于 Flask 开发服务器默认是单线程的,这意味着在这20秒内,服务器将无法处理来自其他用户的任何其他请求。
  • 为了允许 Flask 同时处理多个请求,你可以启用多线程模式。这可以通过在 app.run() 中设置 threaded=True 来实现 app.run(host=FLASK_HOST, port=FLASK_PORT, debug=True, threaded=True)。这样,Flask 将能够为每个请求启动一个新的线程,从而允许同时处理多个请求。但这仍然并不是一种很好的方法,因为整个服务器来看,不具备扩展性。 假设我们服务器为每个 user 的请求开一个线程,那么服务器资源是有限的,当服务器宕机,也并不能很快的恢复这就导致扩展性很差。

异步方式

import threading
import json
from flask import Flask, jsonify, request
from flask_cors import CORS
from ext import FLASK_HOST, FLASK_PORT, users_streams, LOGIN_TOPIC, producer, logger, ResponseConsumer
from kafka_create_user import kafka_consumer_task

app = Flask(__name__)
CORS(app)

@app.route("/login", methods=['POST'])
def create_user_post():
    data = request.json
    # 发送数据到Kafka
    producer.produce(LOGIN_TOPIC, key=str(data['user_id']).encode("utf-8"), value=json.dumps(data).encode("utf-8"))
    producer.flush()
    logger.info("send message to consumer")
    return jsonify({"msg": "你好,请求正在处理"})
  • 我们先忽略其他的代码,只看这一部分。
  • 这里相当于我们接受 user 的请求之后,通过 kafka 把处理请求的需要转移到外部的服务器集群上去了。而 kafka 的特性在于非常高的可扩展性。增加 kafka 的节点就可以线性地将任务处理的数量提高。
  • 如果你看我上面给的那张图,kafka 可以通过无限制增加 consumer 的数量来提高数据的处理能力。而后端的服务器需要做的就是把这些数据不断地派发出去,这个步骤相比于直接在后端将所有的请求处理来说可以忽略不计。

环境文件目录

.
├── app.py
├── ext.py
├── kafka_create_user.py
└── requirements.txt

配置

.env

  • 首先构建一个配置文件 .env 来存放基础的配置信息
FLASK_HOST=0.0.0.0
FLASK_PORT=9300
# LOGIN 这个 topic 是用来处理用户注册这个业务的
LOGIN_TOPIC=LOGIN

# RESPONSE_TOPIC 则是用来构建 response 来返回前端成功或者失败
RESPONSE_TOPIC=RESPONSE_TOPIC

requirements.txt

kafka-python==2.0.0
colorlog==6.7.0
configparser==5.3.0
flask==2.3.2
flask_basicauth==0.2.0
Flask-JWT-Extended==4.6.0
Flask-Limiter==3.5.1
Flask-PyMongo==2.3.0
requests==2.31.0
gunicorn==21.0.0
pymongo==4.6.0
pdfminer.six==20231228
flask_cors==4.0.0
python-dotenv
orjson==3.10.0
langchain
langchain-community
langchain_openai
chromadb
  • python=3.10

完整代码

ext.py

"""
 @file: ext.py.py
 @Time    : 2024/3/30
 @Author  : Peinuan qin
 """
import json
import logging
import os
import queue
import threading
import colorlog
from confluent_kafka import Producer, Consumer, KafkaError
from dotenv import load_dotenv
from confluent_kafka.admin import AdminClient, NewTopic

# 加载 .env 中的变量
load_dotenv()

FLASK_HOST = os.environ['FLASK_HOST']
FLASK_PORT = os.environ['FLASK_PORT']
LOGIN_TOPIC = os.environ['LOGIN_TOPIC']
RESPONSE_TOPIC = os.environ['RESPONSE_TOPIC']

TOPICS = [LOGIN_TOPIC, RESPONSE_TOPIC]


def create_topic():
    # Kafka服务器配置
    admin_client = AdminClient({
        "bootstrap.servers": "localhost:9092"
    })

    # 创建新主题的配置
    topic_list = [NewTopic(topic, num_partitions=3, replication_factor=1) for topic in TOPICS]
    # 注意: replication_factor 和 num_partitions 可能需要根据你的Kafka集群配置进行调整

    # 创建主题
    fs = admin_client.create_topics(topic_list)

    # 处理结果
    for topic, f in fs.items():
        try:
            f.result()  # The result itself is None
            logger.info(f"Topic {topic} created")
        except Exception as e:
            logger.error(f"Failed to create topic {topic}: {e}")



# Handler for logging
handler = colorlog.StreamHandler()
formatter = colorlog.ColoredFormatter(
    "%(log_color)s%(asctime)s.%(msecs)03d - %(levelname)s - %(message)s",
    datefmt='%Y-%m-%d %H:%M:%S',
    log_colors={
        'DEBUG': 'cyan',
        'INFO': 'green',
        'WARNING': 'yellow',
        'ERROR': 'red',
        'CRITICAL': 'red,bg_white',
    }
)
handler.setFormatter(formatter)

# Logger
logger = colorlog.getLogger(__name__)
logger.addHandler(handler)
logger.setLevel(logging.INFO)


"""
尝试创建 topic
"""
create_topic()


# 初始化Kafka生产者
producer_config = {
    'bootstrap.servers': 'localhost:9092'
}
producer = Producer(**producer_config)



"""
定义专门用来回复 response 的 consumer
"""

class ResponseConsumer:
    """
    专门用来将各种处理好的结果返回给 user 作为 response; 也就是图中针对 RESPONSE TOPIC 的 consumer
    """
    def __init__(self):
        self.users_streams = {}
        self.config = {
        'bootstrap.servers': 'localhost:9092',
        'group.id': 'user-response',			# 设置 groupid,如果不知道为什么要设置 groupid 可以去先看 kafka 的讲解视频
        'auto.offset.reset': 'earliest'}			# 告诉 Kafka 消费者在找不到初始偏移量(offset)或者偏移量无效时(比如,指定的偏移量已经被删除),应该从哪里开始消费消息。它可以设置为 'earliest' 或 'latest'。设置为 'earliest' 意味着消费者将从主题的开始处开始读取数据,即尽可能不漏掉任何消息;设置为 'latest' 意味着消费者将从新产生的消息开始读取,即只消费自启动之后产生的消息。
        
        self.consumer = Consumer(**self.config)
        logger.info("Create Response Consumer")
        self.consumer.subscribe([RESPONSE_TOPIC])
        logger.info("Subscribe Response Topic")
		
		# 因为可能有多个线程一起操作 consumer,所以通过 lock 来保证线程安全
        self.lock = threading.Lock()

    def get_or_make(self, user_id):
        """
        获取某个 user_id 的 response queue, 如果当前 user_id 的 response queue 不存在就创建一个
        每个 user_id 的 response queue 中都是返回给前端 user 的信息,也就是图中的  RESPONSE MSG
        :param user_id:
        :return:
        """
        with self.lock:
            # 如果当前 user_id 还没有 queue,就构建一个
            q = self.users_streams.get(user_id, queue.Queue())
            self.users_streams[user_id] = q
        return q

    def pop(self, user_id):
        with self.lock:
            self.users_streams.pop(user_id, None)

    def put(self, user_id, msg_dict):
	    """
        当 user_id 的请求处理完,产生的 RESPONSE MSG 放到 user_id 的队列里面
        :param user_id: 
        :param msg_dict: 
        :return: 
        """
        q = self.get_or_make(user_id)
        
        if q:
            with self.lock:
                self.users_streams[user_id].put(msg_dict)
                logger.info(f"put {msg_dict} into {user_id}'s queue")
            return True

        else:
            return False

    def listen_for_response(self):
        """
        不断拉取 RESPONSE TOPIC 的 producer 生成的结果
        :return:
        """

        try:
            while True:
                msg = self.consumer.poll(timeout=1.0)  # 1秒超时
                if msg is None:
                    continue
                if msg.error():
                    if msg.error().code() == KafkaError._PARTITION_EOF:
                        # End of partition event
                        continue
                    else:
                        print(msg.error())
                        break
				"""
				如果拉取到了就放到对应的 user_id 的 queue 里面
				"""
                if msg:
                    logger.info(f"received data: {msg}")

                    msg_data = json.loads(msg.value().decode("utf-8"))
                    user_id = msg.key().decode("utf-8")

                    logger.info(f"msg_data: {msg_data}")
                    logger.info(f"user_id: {user_id}")

                    put_flag = self.put(user_id, msg_data)

                    if not put_flag:
                        logger.error(f"Create RESPONSE MSG for {user_id} failed")

                    else:
                        logger.info(f"create RESPONSE MSG response for {user_id}")

        except Exception as e:
            self.consumer.close()

app.py

"""
 @file: app.py.py
 @Time    : 2024/3/30
 @Author  : Peinuan qin
"""
import threading
import json
from flask import Flask, jsonify, request
from flask_cors import CORS
from ext import FLASK_HOST, FLASK_PORT, users_streams, LOGIN_TOPIC, producer, logger, ResponseConsumer
from kafka_create_user import kafka_consumer_task

app = Flask(__name__)
CORS(app)


response_consumer = ResponseConsumer()

@app.route("/login", methods=['POST'])
def create_user_post():
    data = request.json
    # 发送数据到Kafka
    producer.produce(LOGIN_TOPIC, key=str(data['user_id']).encode("utf-8"), value=json.dumps(data).encode("utf-8"))
    producer.flush()
    logger.info("send message to login consumer")
    return jsonify({"msg": "你好,请求正在处理"})


@app.route('/stream')
def stream():
    user_id = request.args.get('user_id')  # 假设用户ID通过查询参数传入
    logger.info(f"uid: {user_id}")
    logger.info(f"user_streams: {response_consumer.users_streams}")

    def event_stream(user_id):
        # 这里需要一种机制来持续发送数据给特定用户的流
        q = response_consumer.get_or_make(user_id)
        logger.info(f"{user_id} 's queue is: {q}")
        while True:
            if not q.empty():
                message = q.get()
                logger.info(f"message: {message}")
                yield f"data: {json.dumps(message)}\n\n"

    return app.response_class(event_stream(user_id), content_type='text/event-stream')


def run_multi_thread():

    consumer_thread = threading.Thread(target=kafka_consumer_task)
    response_thread = threading.Thread(target=response_consumer.listen_for_response, daemon=True)
    logger.info("Start APP ...")
    consumer_thread.start()
    logger.info("Create User Consumer start ...")
    response_thread.start()
    logger.info("Response Consumer start ...")
    app.run(host=FLASK_HOST, port=FLASK_PORT, debug=True, use_reloader=False)

if __name__ == '__main__':
    run_multi_thread()

kafka_create_user.py

"""
 @file: kafka_create_user.py
 @Time    : 2024/3/30
 @Author  : Peinuan qin
 """
import json
import os
from queue import Queue
import threading

# 初始化全局消息队列
from confluent_kafka import Consumer, KafkaError
from kafka import KafkaConsumer, KafkaProducer
from dotenv import load_dotenv
from ext import logger, LOGIN_TOPIC, RESPONSE_TOPIC, producer



def kafka_consumer_task():
	"""
    这里定义了 LOGIN TOPIC 的 consumer 的行为;也就是对 user_id 传过来的 RAW DATA 如何处理
    :return: 
    """
    
    # Kafka配置
    config = {
        'bootstrap.servers': 'localhost:9092',
        'group.id': 'user-login-group',
        'auto.offset.reset': 'earliest'
    }
    consumer = Consumer(**config)
    consumer.subscribe([LOGIN_TOPIC])

    # 读取数据
    try:
        while True:
            msg = consumer.poll(timeout=1.0)  # 1秒超时
            if msg is None:
                continue
            if msg.error():
                if msg.error().code() == KafkaError._PARTITION_EOF:
                    # End of partition event
                    continue
                else:
                    print(msg.error())
                    break

            if msg:
                data = json.loads(msg.value().decode("utf-8"))
                key = msg.key().decode("utf-8")
                print("key:", key)

				"""
				为了观察,我们将 user 传过来的数据保存到本地
				"""
                with open(f"{key}.json", 'w') as f:
                    json.dump(data, f, ensure_ascii=False, indent=4)
                    logger.info(f"successfully saved the {key}.json")

                """
                完成任务后,通过 RESPONSE TOPIC 的 producer 生成 response,并发送给 RESPONSE TOPIC 等待对应的 consumer 来取,并且返回给前端
                """
                producer.produce(RESPONSE_TOPIC, key=msg.key(), value=json.dumps({"msg": f"successfully create user {key}"}).encode("utf-8"))
                producer.flush()
                logger.info("send processed data to response consumer")


    except KeyboardInterrupt:
        pass
    finally:
        # 清理操作
        consumer.close()
        producer.flush()
        producer.close()

强调一下:

  • 如果你也是基于 Flask 框架,虽然这里的 debug=True 可以保证每次更改代码后对代码进行重载,方便你进行调试。但是关于内存中的一些变量会消失,所以保证我上面的代码能够顺利运行,我设置了 use_reloader=False 否则 response_consumer.users_streams 总是为空,因为重载变量会造成混淆,引发未知的程序错误。
  • app.py 中的 stream 是以 SSE 的方式让服务器可以主动通知 user,本质是 user 向服务器建立长连接,然后 kafka 完成任务后通过这个端口将信息发送给 user

运行方式

本地安装 kafka

  • 不知道如何安装请 参考

运行 app.py

  • 直接用 pycharm 运行就可以

使用 postman 测试

建立 http 长连接,等待后端处理结果

  • + 新建窗口
  • 建立 http 连接,针对 stream 端口,并且是 GET 方法(注意选中 http 协议哦,通过左上角的符号,不要选择其他协议)
  • 同时在 Params 下面的 keyvalue 输入你 user_id 的信息(要和下面的 /login 的一致)
  • 然后点击 send
  • 长连接就会成功建立了
    在这里插入图片描述

发送 RAW DATA

  • 打开另一个新的窗口
  • 输入你本地运行的地址和端口,并且选择 post 方法
  • 选择 bodyraw 选择 json 的格式并在文本框中键入 json 数据
  • 发送
  • 就会收到 阶段性的服务器回复 ,这代表后端已经通过 kafka 来异步处理数据
    在这里插入图片描述
  • 这个时候,很快你应该可以看到在长连接的那个 postman 窗口里面出现 {"msg": "successfully create user peinuan"};并且每次你在 /login send 一次,这里就会成功获得一次结果(前端获得成功的信息)
    在这里插入图片描述

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1559903.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

# 达梦数据库知识点

达梦数据库知识点 测试数据 -- SYSDBA.TABLE_CLASS_TEST definitionCREATE TABLE SYSDBA.TABLE_CLASS_TEST (ID VARCHAR(100) NOT NULL,NAME VARCHAR(100) NULL,CODE VARCHAR(100) NULL,TITLE VARCHAR(100) NULL,CREATETIME TIMESTAMP NULL,COLUMN1 VARCHAR(100) NULL,COLUMN…

ARP原理

解析原理 动态ARP通过广播ARP请求和单播ARP应答这两个过程完成地址解析 当需要通信的两台主机处于同一网段时,如上图中的Host_1和Host_3,Host_1要向Host_3发送数据。 首先,Host_1会查找自己本地缓存的ARP表,确定是否包含Host_3对…

精通Go语言文件上传:深入探讨r.FormFile函数的应用与优化

1. 介绍 1.1 概述 在 Web 开发中,文件上传是一项常见的功能需求,用于允许用户向服务器提交文件,如图像、文档、视频等。Go 语言作为一门强大的服务器端编程语言,提供了方便且高效的方式来处理文件上传操作。其中,r.F…

155 Linux C++ 通讯架构实战10,工具telent 和 wireshark的使用

telnet工具使用介绍 windows 上开启telnet linux 上开始telnet 使用telnet //是一款命令行方式运行的客户端TCP通讯工具,可以连接到服务器端,往服务器端发送数据,也可以接收从服务器端发送过来的信息; //类似nginx5_1_1_clie…

IO流c++

IO流类库 输入输出流 #include <iostream> using namespace std;class InCount { public:InCount(int a 0, int b 0){c1 a;c2 b;}void show(void){cout << "c1" << c1 << "\t" << "c2" << c2 << …

Go 语言学习一篇入门

Goloang的优缺点 Goloang的优点 极其简单的部署方式 可直接编译成机器码不依赖其他库直接运行即可部署 语言层面的并发 天生的基因支持充分的利用多核 强大的标准库 runtime 系统调度机制高效的GC垃圾回收丰富的标准库 Goloang的不足 包管理&#xff0c;大部分包都在g…

MultiPath HTTP:北大与华为合作部署FLEETY

当前的终端基本都能支持蜂窝网络和wifi网络&#xff0c;然而&#xff0c;不同的网络通路都不可避免的会出现信号不好或者其他因素引起的通路性能(吞吐量、时延等)下降。为了能够提升终端业务体验&#xff0c;很多不同的MultiPath方案被提出&#xff0c;其中&#xff0c;包括应用…

(文章复现)考虑分布式电源不确定性的配电网鲁棒动态重构

参考文献&#xff1a; [1]徐俊俊,吴在军,周力,等.考虑分布式电源不确定性的配电网鲁棒动态重构[J].中国电机工程学报,2018,38(16):4715-47254976. 1.摘要 间歇性分布式电源并网使得配电网网络重构过程需要考虑更多的不确定因素。在利用仿射数对分布式电源出力的不确定性进行合…

Decoupled Multimodal Distilling for Emotion Recognition 论文阅读

Decoupled Multimodal Distilling for Emotion Recognition 论文阅读 Abstract1. Introduction2. Related Works2.1. Multimodal emotion recognition2.2. Knowledge distillation3. The Proposed Method3.1. Multimodal feature decoupling3.2. GD with Decoupled Multimodal …

【Gd2O3】Gd2O3栅极电介质增强GaN器件的可靠性

【Effects of Gd2O3 Gate Dielectric on Proton-Irradiated AlGaN/GaN HEMTs】 概括总结&#xff1a; 该研究探讨了质子辐射对使用Gd2O3作为栅极电介质的AlGaN/GaN高电子迁移率晶体管&#xff08;HEMTs&#xff09;的影响。通过对比肖特基栅极HEMTs和MOS-HEMTs在2 MeV质子辐射…

SOC内部集成网络MAC外设+ PHY网络芯片方案:MII/RMII 接口与 MDIO 接口

一. 简介 本文来了解一下常用的一种网络硬件方案&#xff1a;SOC内部集成网络MAC外设 PHY网络芯片方案。 其中涉及的 MII接口&#xff0c;RMII接口&#xff08;MII接口与RMII接口二选一&#xff09;&#xff0c;MDIO接口&#xff0c;RJ45。 二. MII/RMII 接口&#xff0c;M…

数据挖掘|贝叶斯分类器及其Python实现

分类分析|贝叶斯分类器及其Python实现 0. 分类分析概述1. Logistics回归模型2. 贝叶斯分类器2.1 贝叶斯定理2.2 朴素贝叶斯分类器2.2.1 高斯朴素贝叶斯分类器2.2.2 多项式朴素贝叶斯分类器 2.3 朴素贝叶斯分类的主要优点2.4 朴素贝叶斯分类的主要缺点 3. 贝叶斯分类器在生产中的…

随便注【强网杯2019】

大佬的完整wp&#xff1a;buuctf-web-[强网杯 2019]随便注-wp_取材于某次真实环境渗透,只说一句话:开发和安全缺一不可-CSDN博客 知识点&#xff1a; 单引号字符型绕过堆叠注入 可以执行多条语句multi_query()&#xff1a;该函数可能引发堆叠注入handler用法 mysql专属&#…

面试题:JVM 调优

一、JVM 参数设置 1. tomcat 的设置 vm 参数 修改 TOMCAT_HOME/bin/catalina.sh 文件&#xff0c;如下图 JAVA_OPTS"-Xms512m -Xmx1024m" 2. springboot 项目 jar 文件启动 通常在linux系统下直接加参数启动springboot项目 nohup java -Xms512m -Xmx1024m -jar…

动态内存管理-错题合集讲解

空指针的解应用操作&#xff08;错误信息合集&#xff09; 越界访问 首先我们上一个代码&#xff0c;看看这个的代码的问题 这个代码的问题显而易见 &#xff0c;就是在循环里面&#xff0c;产生了越界访问的问题&#xff0c;这里你开辟了10个整形空间&#xff0c;但是从0-1…

谈谈MVCC机制

在MySQL中&#xff0c;MVCC&#xff08;多版本并发控制&#xff09;是InnoDB存储引擎使用的并发控制机制。它提供对数据的并发访问&#xff0c;并确保多用户环境中数据的一致性和隔离性。 InnoDB通过“Undo log”存储每条记录的多个版本&#xff0c;提供历史记录供读取&#x…

Python数据结构与算法——数据结构(链表、哈希表、树)

目录 链表 链表介绍 创建和遍历链表 链表节点插入和删除 双链表 链表总结——复杂度分析 哈希表(散列表) 哈希表介绍 哈希冲突 哈希表实现 哈希表应用 树 树 树的示例——模拟文件系统 二叉树 二叉树的链式存储 二叉树的遍历 二叉搜索树 插入 查询 删除 AVL树 …

后端SpringBoot+Mybatis 查询订单数据库奇怪报错加一

排错过程&#xff1a; 看报错意思是SQL语句存在错误&#xff0c;然后使用图形化工具运行这个SQL语句 其实这里稍微细心想一下就能发现问题&#xff0c;但是当时没深入想&#xff0c;就觉得order表前加了数据库名字影响不大&#xff0c;所以感觉SQL语句是没问题的&#xff0c;然…

Java6升级至Java8常用新特性

目录 Java 8 常用新特性1、Lambda 表达式2、方法引用2.1 静态方法引用2.2 特定对象的实例方法引用2.3 特定类型的任意对象的实例方法引用2.4 构造器引用 3、接口中的默认方法4、函数式接口4.1 自定义函数式接口4.2 内置函数式接口 5、Date/Time API6、Optional 容器类型7、Stre…

【Qt】窗口

目录 一、概述二、菜单栏&#xff08;QMenuBar&#xff09;三、工具栏&#xff08;QToolBar&#xff09;四、状态栏&#xff08;QStatusBar&#xff09;五、浮动窗口六、对话框 一、概述 Qt窗口是通过QMainWindow类来实现的。 QMainWindow是一个为用户提供主窗口程序的类&…