『pyqt5 从0基础开始项目实战』12. 实现多线程循环检测数据的开始和停止(保姆级图文)

news2024/10/7 6:37:43

目录

    • 最终效果
    • 导包和框架代码
  • main.py
    • 避免重复执行
    • 开始与停止事件
    • 表格更新事件
  • scheduler.py
    • 初始化
    • 开始线程
    • 停止线程
    • 在线程列表中删除线程
    • TaskThread新建线程
    • StopThread停止线程
    • 完整代码
      • main.py
      • threads.py
      • scheduler.py
    • 总结


欢迎关注 『pyqt5 从0基础开始项目实战』 专栏,持续更新中
欢迎关注 『pyqt5 从0基础开始项目实战』 专栏,持续更新中

最终效果

  • 开始执行
    在这里插入图片描述
  • 逐渐停止
    在这里插入图片描述
  • 全部停止
    在这里插入图片描述

导包和框架代码

提示!!!! 检查一下你的threads.py有没有导入 import time
如果导入了from datetime import time 会导致闪退!!

请查阅上文获取源码,此处只列举第三方库

import os
import sys
from PyQt5.QtCore import Qt
from PyQt5.QtWidgets import QApplication, QWidget, QDesktopWidget, QHBoxLayout, QVBoxLayout
from PyQt5.QtWidgets import QPushButton, QLineEdit, QTableWidget, QTableWidgetItem, QLabel
from PyQt5.QtWidgets import QMessageBox, QMenu


main.py

避免重复执行

方法很简单,设置一个变量用来记录当前是否已经在执行中。

  • 初始化设置常量
# 设置一些常用的常量
RUNNING = 1
STOPPING = 2
STOP = 3

class MainWindow(QWidget):
    def __init__(self):
		···
        self.switch = STOP #用于记录当前是否在执行中,如果已经在执行中,不允许重复启动
        ···

在开始按钮的事件最前面添加判断

    # 点击开始
    def event_start_click(self):
        if self.switch != STOP:
            QMessageBox.warning(self, "错误", "正在执行获取终止中,请勿重复操作")
            return
        self.switch = RUNNING
  • 在结束按钮的事件前面添加判断
    # 点击停止
    def event_stop_click(self):
        if self.switch != RUNNING:#如果点击停止时不是执行状态
            QMessageBox.warning(self, "错误", "不在执行中,不需要停止")
            return

        self.switch = STOPPING
  • 在我们更新左下角标签的事件中也要修改
    def update_status_message(self, message):
        if message == "已终止":
            self.switch = STOP
        self.label_status.setText(message)
        self.label_status.repaint()

开始与停止事件

  • 开始事件
    # 点击开始
    def event_start_click(self):
        if self.switch != STOP:
            QMessageBox.warning(self, "错误", "正在执行获取终止中,请勿重复操作")
            return
        self.switch = RUNNING

        # 1.为每一行创建一个线程去执行 ( 所有的线程记录)  [x,x,x,x,]
        from utils.scheduler import SCHEDULER

        SCHEDULER.start(
            BASE_DIR,
            self,
            self.task_start_callback,
            self.task_stop_callback,
            self.task_counter_callback,
            self.task_error_counter_callback
        )

        # 2.执行中
        self.update_status_message("执行中")

  • 停止事件
    # 点击停止
    def event_stop_click(self):
        if self.switch != RUNNING:#如果点击停止时不是执行状态
            QMessageBox.warning(self, "错误", "不在执行中,不需要停止")
            return

        self.switch = STOPPING

        # 1.执行中的线程逐一终止
        from utils.scheduler import SCHEDULER

        SCHEDULER.stop()
        # 2.更新状态

表格更新事件

    # 将状态改为执行中
    def task_start_callback(self, row_index):
        # 对表格中的数据进行状态更新
        cell_status = QTableWidgetItem(STATUS_MAPPING[2])#得到对应行的状态文本
        cell_status.setFlags(Qt.ItemIsSelectable | Qt.ItemIsEnabled)#不可选中 不可修改
        self.table_widget.setItem(row_index, 6, cell_status)#修改状态
    
    # 将状态改为待执行
    def task_stop_callback(self, row_index):
        cell_status = QTableWidgetItem(STATUS_MAPPING[1])
        cell_status.setFlags(Qt.ItemIsSelectable | Qt.ItemIsEnabled)
        self.table_widget.setItem(row_index, 6, cell_status)
    # 成功次数增加
    def task_counter_callback(self, row_index):
        # 原有个数+1
        old_count = self.table_widget.item(row_index, 4).text().strip()
        new_count = int(old_count) + 1

        # 表格赋值
        cell_status = QTableWidgetItem(str(new_count))
        cell_status.setFlags(Qt.ItemIsSelectable | Qt.ItemIsEnabled)
        self.table_widget.setItem(row_index, 4, cell_status)

    # 503 次数增加
    def task_error_counter_callback(self, row_index):
        # 原有个数+1
        old_count = self.table_widget.item(row_index, 5).text().strip()
        new_count = int(old_count) + 1

        # 表格赋值
        cell_status = QTableWidgetItem(str(new_count))
        cell_status.setFlags(Qt.ItemIsSelectable | Qt.ItemIsEnabled)
        self.table_widget.setItem(row_index, 5, cell_status)

scheduler.py

初始化

    def __init__(self):
        self.thread_list = []
        self.window = None
        self.terminate = False  # 标志,点击停止,false表示此时可以启动,true表示正在关闭

开始线程

    # 开始线程
    def start(self, base_dir, window, fn_start, fn_stop, fn_counter, fn_error_counter):
        self.window = window#这里的window就是我们main传入的窗体对象,通过这个 .window.控件 我们可以调用窗体对象中的所有控件
        self.terminate = False
        # 1.获取表格中的所有数据,每一行创建一个线程去执行监控
        for row_index in range(window.table_widget.rowCount()):
            # 0/1/2/3
            asin = window.table_widget.item(row_index, 0).text().strip()#得到型号
            status_text = window.table_widget.item(row_index, 6).text().strip()#得到状态文本
            
            # 日志
            import os
            log_folder = os.path.join(base_dir, 'log')
            if not os.path.exists(log_folder):#如果日志文件夹不存在就新建
                os.makedirs(log_folder)
            log_file_path = os.path.join(log_folder, "{}.log".format(asin))# 日志文件

            # 只有是待执行的时,才创建线程去执行
            if status_text != "待执行":
                continue

            # 2.每个线程 执行 & 状态实时的显示在表格中 信号+回调
            from utils.threads import TaskThread
            t = TaskThread(self, log_file_path, row_index, asin, window)
            t.start_signal.connect(fn_start)
            t.counter_signal.connect(fn_counter)
            t.error_counter_signal.connect(fn_error_counter)
            t.stop_signal.connect(fn_stop)
            t.start()

            self.thread_list.append(t)

停止线程

    #停止线程
    def stop(self):
        self.terminate = True#设置标记为true
        # 创建线程,去监测 thread_list 中的数量 + 实时更新的窗体的label中
        # self.window.update_status_message("xxx")
        from utils.threads import StopThread
        t = StopThread(self, self.window)
        t.update_signal.connect(self.window.update_status_message)#窗口更新数据的声明
        t.start()

在线程列表中删除线程

    # 在线程列表中删除线程
    def destroy_thread(self, thread):
        self.thread_list.remove(thread)#关闭线程时要把线程从我们正在执行的线程列表中移除


# 在threads.py中的新建线程与停止线程函数 - 开始,创建线程去执行任务(更新表格数据 + 信号 ) - 结束,表格更新 + 结束

我们的思路是在新建线程的同时把线程放入我们的thread_list,线程停止执行后移除thread_list,根据thread_list是否为空,判断当前是否所有的线程都停止了。

TaskThread新建线程

class TaskThread(QThread):
    start_signal = pyqtSignal(int)#开始信号
    stop_signal = pyqtSignal(int)#结束信号
    counter_signal = pyqtSignal(int)#总数统计信号
    error_counter_signal = pyqtSignal(int)#错误进程信号

    def __init__(self, scheduler, log_file_path, row_index, asin, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.scheduler = scheduler
        self.log_file_path = log_file_path#日志文件
        self.row_index = row_index
        self.asin = asin

    def run(self):
        # 触发start_signal
        self.start_signal.emit(self.row_index)

        import time
        import random
        while True:
            # 停止
            if self.scheduler.terminate:#终止线程的执行。根据操作系统的调度策略,线程可能会立即终止,也可能不会立即终止。可以在终止()之后使用QThread.wait()。
                self.stop_signal.emit(self.row_index)
                # 自己的线程在thread_list中移除掉
                self.scheduler.destroy_thread(self)
                return
            try:
                time.sleep(random.randint(1, 3))#随机休眠
                self.counter_signal.emit(self.row_index)

                # 日志写入,注意用mode=a 表示在原来的文件末尾续写而不是覆盖
                with open(self.log_file_path, mode='a', encoding='utf-8') as f:
                    f.write("日志\n")

                # 监控的动作
                # 1.根据型号访问通过bs4获取数据
                # 2.获取到数据 价格是否小于预期
                # 3.发送报警(邮件)

                time.sleep(5)#检测的5秒延时
            except Exception as e:
                self.error_counter_signal.emit(self.row_index)#如果错误,提交错误的行号

StopThread停止线程


class StopThread(QThread):
    update_signal = pyqtSignal(str)#线程传递数据的信号

    def __init__(self, scheduler, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.scheduler = scheduler

    def run(self):
        # 1.监测线程数量(总线程数)
        while True:#死循环,直到所有的线程都结束了才跳出循环
            running_count = len(self.scheduler.thread_list)#得到存储当前正在运行的线程列表的长度
            self.update_signal.emit("正在终止({})".format(running_count))#正在终止X个还没有结束的线程
            if running_count == 0:#如果执行中的线程数为0,说明所有的线程都结束了,退出循环
                break
            time.sleep(1)

        self.update_signal.emit("已终止")#提交数据,更新左下角状态


完整代码

main.py

import os
import sys
from PyQt5.QtCore import Qt
from PyQt5.QtWidgets import QApplication, QWidget, QDesktopWidget, QHBoxLayout, QVBoxLayout
from PyQt5.QtWidgets import QPushButton, QLineEdit, QTableWidget, QTableWidgetItem, QLabel
from PyQt5.QtWidgets import QMessageBox, QMenu
from utils.dialog import LogDialog

#拿到在另外一台计算机上执行程序代码所存储的文件路径。
BASE_DIR = os.path.dirname(os.path.realpath(sys.argv[0]))

STATUS_MAPPING = {
    0: "初始化中",
    1: "待执行",
    2: "正在执行",
    3: "完成并提醒",
    10: "异常并停止",
    11: "初始化失败",
}

# 设置一些常用的常量
RUNNING = 1
STOPPING = 2
STOP = 3

class MainWindow(QWidget):
    def __init__(self):
        # 用super 继承父类的初始化
        super().__init__()

        self.switch = STOP #用于记录当前是否在执行中,如果已经在执行中,不允许重复启动

        self.txt_asin=None

        # 设置窗口的窗体标题
        self.setWindowTitle('发现你走远了的xx系统')

        # 设置窗体的尺寸
        self.resize(1228, 450)

        # 设置窗体位置
        # 获取整个窗口部分的宽高和左上角坐标信息,返回值是一个QRect类型,(x,y width,height)
        qr = self.frameGeometry()
        cp = QDesktopWidget().availableGeometry().center()  # 得到屏幕中间的位置信息
        qr.moveCenter(cp)  # 让我们的窗体移动到屏幕中间

        # 创建窗口总布局
        layout = QVBoxLayout()

        # 讲调用方法生成的顶部菜单布局添加到总布局
        layout.addLayout(self.init_header())
        layout.addLayout(self.init_form())
        layout.addLayout(self.init_table())
        layout.addLayout(self.init_footer())

        # 给窗体设置元素的排列方式
        self.setLayout(layout)

    def init_header(self):
        # 1.顶部菜单布局
        header_layout = QHBoxLayout()  # 创建顶部菜单布局
        # 1.1 放入按钮
        btn_start = QPushButton("开始")  # 新建一个开始按钮
        btn_start.clicked.connect(self.event_start_click)
        header_layout.addWidget(btn_start)  # 将开始按钮添加到顶部菜单布局

        btn_stop = QPushButton("停止")  # 新建一个开始按钮
        btn_stop.clicked.connect(self.event_stop_click)
        header_layout.addWidget(btn_stop)  # 将开始按钮添加到顶部菜单布局

        # 1.2 加入弹簧
        header_layout.addStretch()

        return header_layout

    def init_form(self):
        # 2.添加内容布局
        form_layout = QHBoxLayout()  # 创建添加内容布局

        # 2.1 输入框
        txt_asin = QLineEdit()  # 新建一个输入框对象
        txt_asin.setText("B07YN82X3B=100")  # 设置默认的form数据
        txt_asin.setPlaceholderText("请输入商品ID和价格,例如:B0818JJQQ8=88")  # 设置灰色的提示信息
        self.txt_asin=txt_asin
        form_layout.addWidget(txt_asin)  # 将输入框加入到布局中

        # 2.2 添加按钮
        btn_add = QPushButton("添加")  # 新建一个添加按钮
        btn_add.clicked.connect(self.event_add_click)#新增一个点击事件
        form_layout.addWidget(btn_add)  # 将添加按钮添加到form布局

        return form_layout

    def init_table(self):
        # 3.表格数据展示布局
        table_layout = QHBoxLayout()
        # 3.1 创建表格
        self.table_widget=table_widget = QTableWidget(0, 8)  # 新建一个0行8列的表格
        # # 修改表格索引名
        # item=QTableWidgetItem()
        # item.setText("标题0")
        # table_widget.setHorizontalHeaderItem(0,item)
        # table_widget.setColumnWidth(0,150)#设置水平单元格0号位置的宽度 150
        #
        # item2=QTableWidgetItem()
        # item2.setText("网址1")
        # table_widget.setHorizontalHeaderItem(1,item2)
        # table_widget.setColumnWidth(1,400)#设置水平单元格1号位置的宽度 400
        #
        # item3=QTableWidgetItem()
        # item3.setText("行索引0")
        # table_widget.setVerticalHeaderItem(0,item3)
        table_header = [
            {"field": "asin", "text": "ASIN", 'width': 120},
            {"field": "title", "text": "标题", 'width': 150},
            {"field": "url", "text": "URL", 'width': 400},
            {"field": "price", "text": "底价", 'width': 100},
            {"field": "success", "text": "成功次数", 'width': 100},
            {"field": "error", "text": "503次数", 'width': 100},
            {"field": "status", "text": "状态", 'width': 100},
            {"field": "frequency", "text": "频率(N秒/次)", 'width': 100},
        ]
        for idx, info in enumerate(table_header):
            item = QTableWidgetItem()
            item.setText(info['text'])
            table_widget.setHorizontalHeaderItem(idx, item)
            table_widget.setColumnWidth(idx, info['width'])

        # 3.2 初始化表格数据
        # 读取数据文件
        import json
        file_path = os.path.join(BASE_DIR, "db", "db.json")
        with open(file_path, mode='r', encoding='utf-8') as f:
            data = f.read()
        data_list = json.loads(data)#读取得到了json数据


        current_row_count = table_widget.rowCount()  # 当前表格有多少行
        for row_list in data_list:#每有一行json数据,我们就需要遍历一轮增加一行数据
            table_widget.insertRow(current_row_count)#增加一行
            # print(row_list)  # ['B08166SLDF', 'AMD er', 'https://www.amazon.', 300.0, 0, 166, 1, 5]
            # 把row_list写入这一行
            for i, ele in enumerate(row_list):#enumerate中 i表示索引id,ele表示数据值
                # 一个多目运算 如果i==6(此时是状态的数据) 如果STATUS_MAPPING中能够找到ele的索引,那么我们把ele设置成STATUS_MAPPING中的内容
                ele = STATUS_MAPPING[ele] if i == 6 else ele

                cell = QTableWidgetItem(str(ele))#注意我们的数据格式转为str,比如说状态的数据可能本身是int类型
                if i in [0, 4, 5, 6]:
                    # 不可修改
                    cell.setFlags(Qt.ItemIsSelectable | Qt.ItemIsEnabled)#指定索引单元格不能选中和修改
                table_widget.setItem(current_row_count, i, cell)#写入数据到指定单元格

            current_row_count += 1 #行数+1,这样下次遍历行时会在新的行下面新增一行
            # 如果不新增行其实也可以用  current_row_count=table_widget.rowCount()替代


        # # 开启右键复制功能,在表格中点击右键时,自动触发 right_menu 函数
        table_widget.setContextMenuPolicy(Qt.CustomContextMenu)#设置启用右键菜单
        table_widget.customContextMenuRequested.connect(self.table_right_menu)#右键菜单的实现函数

        table_layout.addWidget(table_widget)  # 把表格添加到表格布局中
        return table_layout

    #右键菜单功能
    def table_right_menu(self, pos):

        # 只有选中一行时,才支持右键 selected_item_list可以是一个多行的列表
        # 我们只处理选中的第一行,所以要 selected_item_list[0] 表示选中的多行中的第一行
        selected_item_list = self.table_widget.selectedItems()
        if len(selected_item_list) == 0:
            return

        menu = QMenu()
        item_copy = menu.addAction("复制")
        item_log = menu.addAction("查看日志")
        item_log_clear = menu.addAction("清除日志")

        action = menu.exec_(self.table_widget.mapToGlobal(pos))# 获取选中的对象

        if action == item_copy:
            # 赋值当前型号 B08166SLDF
            clipboard = QApplication.clipboard()#新建剪切板对象
            clipboard.setText(selected_item_list[0].text())#剪切板复制得到ASIN

        if action == item_log:
            # 查看日志,在对话框中显示日志信息
            # 获取选中的型号
            row_index = selected_item_list[0].row()#得到选中第一行的行号
            asin = self.table_widget.item(row_index, 0).text().strip()#得到选中行的商品ASIN


            dialog = LogDialog(asin)

            #打开我们写好的日志窗口,针对每个商品创建的日志的保存和修改也在这个窗口中实现
            dialog.setWindowModality(Qt.ApplicationModal)

            dialog.exec_()

        if action == item_log_clear:
            # 清空日志
            row_index = selected_item_list[0].row()
            asin = self.table_widget.item(row_index, 0).text().strip()
            file_path = os.path.join("log", "{}.log".format(asin))
            if os.path.exists(file_path):#如果已经存在了日志
                os.remove(file_path)#删除日志文件


    def init_footer(self):
        # 4.底部菜单
        footer_layout = QHBoxLayout()

        self.label_status=label_status = QLabel("未检测", self)
        footer_layout.addWidget(label_status)

        footer_layout.addStretch()  # 添加弹簧,更加美观

        btn_reset = QPushButton("重新初始化")
        btn_reset.clicked.connect(self.event_reset_click)#新增一个点击事件
        footer_layout.addWidget(btn_reset)

        btn_recheck = QPushButton("重新检测")
        footer_layout.addWidget(btn_recheck)

        btn_reset_count = QPushButton("次数清零")
        btn_reset_count.clicked.connect(self.event_reset_count_click)
        footer_layout.addWidget(btn_reset_count)

        btn_delete = QPushButton("删除检测项")
        btn_delete.clicked.connect(self.event_delete_click)
        footer_layout.addWidget(btn_delete)

        btn_alert = QPushButton("SMTP报警配置")
        btn_alert.clicked.connect(self.event_alert_click)
        footer_layout.addWidget(btn_alert)

        btn_proxy = QPushButton("代理IP")
        btn_proxy.clicked.connect(self.event_proxy_click)
        footer_layout.addWidget(btn_proxy)

        return footer_layout

    # 添加数据按钮事件
    def event_add_click(self):
        # 1.获取输入框中的内容
        text = self.txt_asin.text()
        text = text.strip()#去掉空格
        if not text:#如果输入的是空格空字符
            QMessageBox.warning(self, "错误", "商品的ASIN输入错误")
            return
        # B07YN82X3B=100
        asin, price = text.split("=")#把数据分为id 和 价格
        price = float(price)#价格转为浮点型
        print(asin, price)

        # 2.加入到表格中(型号、底价)
        new_row_list = [asin, "", "", price, 0, 0, 0, 5]

        # 写入表格,具体操作和之前初始化表格一样
        current_row_count = self.table_widget.rowCount() # 当前表格有多少行
        self.table_widget.insertRow(current_row_count)
        for i, ele in enumerate(new_row_list):
            ele = STATUS_MAPPING[ele] if i == 6 else ele
            cell = QTableWidgetItem(str(ele))
            if i in [0, 4, 5, 6]:
                # 不可修改
                cell.setFlags(Qt.ItemIsSelectable | Qt.ItemIsEnabled)
            self.table_widget.setItem(current_row_count, i, cell)

        # 3.发送请求自动获取标题
        # 注意:不能再主线程中做数据获取的事,创建一个线程去做数据获取,爬取到数据再更新到窗体应用(信号)。
        from utils.threads import NewTaskThread #导入我们写的线程py文件中的类

        thread = NewTaskThread(current_row_count, asin, self)#初始化线程中类的对象,传入了 表格总共的行数{current_row_count} 和 当前在表格的第几行{asin} 以及自身这个对象 {self}
        thread.success.connect(self.init_task_success_callback)#添加数据成功事件成功的回调,马上开始初始化数据
        thread.error.connect(self.init_task_error_callback)#事件失败的回调,弹框提示错误
        thread.start()#线程开始运行

        pass

    # 添加数据成功事件成功的回调,得到数据后马上开始初始化数据
    def init_task_success_callback(self, row_index, asin, title, url):
        print("成功",row_index, asin, title, url)
        # 更新标题
        cell_title = QTableWidgetItem(title)
        self.table_widget.setItem(row_index, 1, cell_title)

        # 更新URL
        cell_url = QTableWidgetItem(url)
        self.table_widget.setItem(row_index, 2, cell_url)

        # 更新状态 成功后未待执行
        cell_status = QTableWidgetItem(STATUS_MAPPING[1])
        cell_status.setFlags(Qt.ItemIsSelectable | Qt.ItemIsEnabled)
        self.table_widget.setItem(row_index, 6, cell_status)

        # 输入框清空
        self.txt_asin.clear()

    # 添加数据失败事件成功的回调,得到数据后马上开始初始化数据
    def init_task_error_callback(self, row_index, asin, title, url):
        print("错误",row_index, asin, title, url)
        # 更新标题
        cell_title = QTableWidgetItem(title)
        self.table_widget.setItem(row_index, 1, cell_title)

        # 更新URL
        cell_url = QTableWidgetItem(url)
        self.table_widget.setItem(row_index, 2, cell_url)

        # 更新状态为 初始化失败
        cell_status = QTableWidgetItem(STATUS_MAPPING[11])
        cell_status.setFlags(Qt.ItemIsSelectable | Qt.ItemIsEnabled)
        self.table_widget.setItem(row_index, 6, cell_status)

    def event_reset_click(self):
        # 1.获取已经选中的行
        row_list = self.table_widget.selectionModel().selectedRows()
        if not row_list:#如果没有选中行,提示错误
            QMessageBox.warning(self, "错误", "请选择要重新初始化的行")
            return
        # 2.获取每一行进行重新初始化
        for row_object in row_list:
            index = row_object.row()
            print("选中的行:", index)
            # 获取信号
            asin = self.table_widget.item(index, 0).text().strip()#得到了商品id

            # 状态重新初始化
            cell_status = QTableWidgetItem(STATUS_MAPPING[0])#把状态设置成初始化中
            print("选中的行:", index,"---",STATUS_MAPPING[0]) # 选中的行: 1 --- 初始化中
            cell_status.setFlags(Qt.ItemIsSelectable | Qt.ItemIsEnabled)
            self.table_widget.setItem(index, 6, cell_status)

            # 创建线程去进行初始化动作
            from utils.threads import NewTaskThread
            thread = NewTaskThread(index, asin, self)#传入 选中的行/商品id/窗体对象
            thread.success.connect(self.init_task_success_callback)
            thread.error.connect(self.init_task_error_callback)
            thread.start()

    # 点击数量清零
    def event_reset_count_click(self):
        # 1.获取已经选中的行
        row_list = self.table_widget.selectionModel().selectedRows()
        if not row_list:
            QMessageBox.warning(self, "错误", "请选择要操作的行")
            return
        # 2.获取每一行进行重新初始化
        for row_object in row_list:
            index = row_object.row()
            # print("选中的行:", index)
            # # 获取信号
            # asin = self.table_widget.item(index, 0).text().strip()

            # 状态重新初始化
            cell_status = QTableWidgetItem(str(0))#得到一个字符串格式的 0
            cell_status.setFlags(Qt.ItemIsSelectable | Qt.ItemIsEnabled)
            self.table_widget.setItem(index, 4, cell_status)#设置成功次数清零

            cell_status = QTableWidgetItem(str(0))#得到一个字符串格式的 0
            cell_status.setFlags(Qt.ItemIsSelectable | Qt.ItemIsEnabled)
            self.table_widget.setItem(index, 5, cell_status)#设置503次数清零

    # 点击删除
    def event_delete_click(self):
        # 1.获取已经选中的行
        row_list = self.table_widget.selectionModel().selectedRows()
        if not row_list:
            QMessageBox.warning(self, "错误", "请选择要操作的行")
            return

        # 2.翻转,先删除掉行号大的数据,然后在删除行号小的数据,避免删除行号小的数据变动行号大的数据。
        row_list.reverse()
        print (row_list)

        # 3.删除
        for row_object in row_list:
            index = row_object.row()
            print("当前删除的行是:",index)
            self.table_widget.removeRow(index)

    # 点击邮件配置
    def event_alert_click(self):
        # 创建弹窗并在弹窗中进行设置

        from utils.dialog import AlertDialog

        dialog = AlertDialog()
        dialog.setWindowModality(Qt.ApplicationModal)
        dialog.exec_()

    # 点击代理
    def event_proxy_click(self):
        from utils.dialog import ProxyDialog

        dialog = ProxyDialog()
        dialog.setWindowModality(Qt.ApplicationModal)
        dialog.exec_()

    # 点击开始
    def event_start_click(self):
        if self.switch != STOP:
            QMessageBox.warning(self, "错误", "正在执行获取终止中,请勿重复操作")
            return
        self.switch = RUNNING

        # 1.为每一行创建一个线程去执行 ( 所有的线程记录)  [x,x,x,x,]
        from utils.scheduler import SCHEDULER

        # 函数方法作为参数传入SCHEDULER
        SCHEDULER.start(
            BASE_DIR,
            self,
            self.task_start_callback,
            self.task_stop_callback,
            self.task_counter_callback,
            self.task_error_counter_callback
        )

        # 2.左下角标签显示执行中
        self.update_status_message("执行中")

    # 将状态改为执行中
    def task_start_callback(self, row_index):
        # 对表格中的数据进行状态更新
        cell_status = QTableWidgetItem(STATUS_MAPPING[2])#得到对应行的状态文本
        cell_status.setFlags(Qt.ItemIsSelectable | Qt.ItemIsEnabled)#不可选中 不可修改
        self.table_widget.setItem(row_index, 6, cell_status)#修改状态

    # 将状态改为待执行
    def task_stop_callback(self, row_index):
        cell_status = QTableWidgetItem(STATUS_MAPPING[1])
        cell_status.setFlags(Qt.ItemIsSelectable | Qt.ItemIsEnabled)
        self.table_widget.setItem(row_index, 6, cell_status)
    # 成功次数增加
    def task_counter_callback(self, row_index):
        # 原有个数+1
        old_count = self.table_widget.item(row_index, 4).text().strip()
        new_count = int(old_count) + 1

        # 表格赋值
        cell_status = QTableWidgetItem(str(new_count))
        cell_status.setFlags(Qt.ItemIsSelectable | Qt.ItemIsEnabled)
        self.table_widget.setItem(row_index, 4, cell_status)

    # 503 次数增加
    def task_error_counter_callback(self, row_index):
        # 原有个数+1
        old_count = self.table_widget.item(row_index, 5).text().strip()
        new_count = int(old_count) + 1

        # 表格赋值
        cell_status = QTableWidgetItem(str(new_count))
        cell_status.setFlags(Qt.ItemIsSelectable | Qt.ItemIsEnabled)
        self.table_widget.setItem(row_index, 5, cell_status)


    # 点击停止
    def event_stop_click(self):
        if self.switch != RUNNING:#如果点击停止时不是执行状态
            QMessageBox.warning(self, "错误", "不在执行中,不需要停止")
            return

        self.switch = STOPPING

        # 1.执行中的线程逐一终止
        from utils.scheduler import SCHEDULER

        SCHEDULER.stop()
        # 2.更新状态

    # 接收message信息,更新到左下角信息标签
    def update_status_message(self, message):
        if message == "已终止":
            self.switch = STOP
        self.label_status.setText(message)
        self.label_status.repaint()

if __name__ == '__main__':
    app = QApplication(sys.argv)  # 实例化一个Application应用,所有的窗口均在其下运行

    window = MainWindow()  # 实例化窗口对象
    window.show()  # 窗口展示

    sys.exit(app.exec_())
    # app.exec_()运行主循环,并在退出时返回状态代码。
    # sys.exit(n)退出您的应用程序并返回n到父进程(通常是您的shell)

threads.py

import time
from PyQt5.QtCore import QThread, pyqtSignal


class NewTaskThread(QThread):
    # 信号,触发信号,更新窗体中的数据
    success = pyqtSignal(int, str, str, str)  # 成功后向ui对象发送数据的声明
    error = pyqtSignal(int, str, str, str)  # 失败后向ui对象发送数据的声明

    def __init__(self, row_index, asin, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.row_index = row_index  # 行数
        self.asin = asin  # 商品id

    def run(self):
        """ 具体线程应该做的事"""
        try:
            title = "good_title_{}".format(self.asin)  # good_title_B07YN82X3B
            url = "https://blog.csdn.net/u011027547/{}".format(self.asin)

            # 获取到title和url,将这个信息填写到 表格上 & 写入文件中。
            self.success.emit(self.row_index, self.asin, title, url)
        except Exception as e:
            print(e)
            title = "监控项 {} 添加失败。".format(self.asin)
            self.error.emit(self.row_index, self.asin, title, str(e))




class TaskThread(QThread):
    start_signal = pyqtSignal(int)#开始信号
    stop_signal = pyqtSignal(int)#结束信号
    counter_signal = pyqtSignal(int)#总数统计信号
    error_counter_signal = pyqtSignal(int)#错误进程信号

    def __init__(self, scheduler, log_file_path, row_index, asin, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.scheduler = scheduler
        self.log_file_path = log_file_path#日志文件
        self.row_index = row_index
        self.asin = asin

    def run(self):
        # 触发start_signal
        self.start_signal.emit(self.row_index)

        import time
        import random
        while True:
            # 停止
            if self.scheduler.terminate:#终止线程的执行。根据操作系统的调度策略,线程可能会立即终止,也可能不会立即终止。可以在终止()之后使用QThread.wait()。
                self.stop_signal.emit(self.row_index)
                # 自己的线程在thread_list中移除掉
                self.scheduler.destroy_thread(self)
                return
            try:
                time.sleep(random.randint(1, 3))#随机休眠
                self.counter_signal.emit(self.row_index)

                # 日志写入,注意用mode=a 表示在原来的文件末尾续写而不是覆盖
                with open(self.log_file_path, mode='a', encoding='utf-8') as f:
                    f.write("日志\n")

                # 监控的动作
                # 1.根据型号访问通过bs4获取数据
                # 2.获取到数据 价格是否小于预期
                # 3.发送报警(邮件)

                time.sleep(5)#检测的5秒延时
            except Exception as e:
                self.error_counter_signal.emit(self.row_index)#如果错误,提交错误的行号


class StopThread(QThread):
    update_signal = pyqtSignal(str)#线程传递数据的信号

    def __init__(self, scheduler, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.scheduler = scheduler

    def run(self):
        # 1.监测线程数量(总线程数)
        while True:#死循环,直到所有的线程都结束了才跳出循环
            running_count = len(self.scheduler.thread_list)#得到存储当前正在运行的线程列表的长度
            self.update_signal.emit("正在终止({})".format(running_count))#正在终止X个还没有结束的线程
            if running_count == 0:#如果执行中的线程数为0,说明所有的线程都结束了,退出循环
                break
            time.sleep(1)

        self.update_signal.emit("已终止")#提交数据,更新左下角状态

scheduler.py

class Scheduler(object):
    def __init__(self):
        self.thread_list = []
        self.window = None
        self.terminate = False  # 标志,点击停止,false表示此时可以启动,true表示正在关闭

    # 开始线程
    def start(self, base_dir, window, fn_start, fn_stop, fn_counter, fn_error_counter):
        self.window = window  # 这里的window就是我们main传入的窗体对象,通过这个 .window.控件 我们可以调用窗体对象中的所有控件
        self.terminate = False
        # 1.获取表格中的所有数据,每一行创建一个线程去执行监控
        for row_index in range(window.table_widget.rowCount()):
            # 0/1/2/3
            asin = window.table_widget.item(row_index, 0).text().strip()  # 得到型号
            status_text = window.table_widget.item(row_index, 6).text().strip()  # 得到状态文本

            # 日志
            import os
            log_folder = os.path.join(base_dir, 'log')
            if not os.path.exists(log_folder):  # 如果日志文件夹不存在就新建
                os.makedirs(log_folder)
            log_file_path = os.path.join(log_folder, "{}.log".format(asin))  # 日志文件

            # 只有是待执行的时,才创建线程去执行
            if status_text != "待执行":
                continue

            # 2.每个线程 执行 & 状态实时的显示在表格中 信号+回调
            from utils.threads import TaskThread
            t = TaskThread(self, log_file_path, row_index, asin, window)
            t.start_signal.connect(fn_start)
            t.counter_signal.connect(fn_counter)
            t.error_counter_signal.connect(fn_error_counter)
            t.stop_signal.connect(fn_stop)
            t.start()

            self.thread_list.append(t)

    # 停止线程
    def stop(self):
        self.terminate = True  # 设置标记为true
        # 创建线程,去监测 thread_list 中的数量 + 实时更新的窗体的label中
        # self.window.update_status_message("xxx")
        from utils.threads import StopThread
        t = StopThread(self, self.window)
        t.update_signal.connect(self.window.update_status_message)  # 窗口更新数据的声明
        t.start()

    # 在线程列表中删除线程
    def destroy_thread(self, thread):
        self.thread_list.remove(thread)  # 关闭线程时要把线程从我们正在执行的线程列表中移除


# 单例模式,实例化了这一个对象SCHEDULER,后面的其他py文件引用时也会引用这个实例(所有的共同一个,方便状态的管理)
SCHEDULER = Scheduler()


总结

大家喜欢的话,给个👍,点个关注!给大家分享更多计算机专业学生的求学之路!

版权声明:

发现你走远了@mzh原创作品,转载必须标注原文链接

Copyright 2023 mzh

Crated:2023-3-1

欢迎关注 『pyqt5 从0基础开始项目实战』 专栏,持续更新中
欢迎关注 『pyqt5 从0基础开始项目实战』 专栏,持续更新中
『未完待续』


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

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

相关文章

C. Anadi and Domino(思维 + 枚举)

Problem - C - Codeforces 阿纳迪有一副多米诺骨牌。每张多米诺骨牌都有两个部分,每个部分都包含一些小点。对于每一个a和b,如1≤a≤b≤6,1≤a≤b≤6,正好有一块多米诺骨牌的一半是a点,另一半是b点。这组多米诺骨牌正好…

手动实现 Tomcat 底层机制+ 自己设Servlet

目录 手动实现 Tomcat 底层机制 自己设Servlet 完成小案例 运行效果 此项目用maven至于怎么配置在下一篇文章 创建cal.html CalServlet.java web.xml WebUtils 问题: Tomcat 整体架构分析 测试分析: 抓包情况 手动实现 Tomcat 底层机制 自己设计 Serv…

十个高质量工具网站推荐,AI自动抠图换背景,任意背景自动融合

AI 背景更换是一种利用生成式人工智能创建新图像背景的软件工具。与传统方法需要移除原有的背景并更换新的不同,AI背景生成器使用先进的算法生成与前景完美融合的全新背景。这项技术彻底改变了图像编辑的方式,为设计提供了更多的创造自由和灵活性。 特点…

数据结构--B树、B+树

数据结构--B树、B树 1. 什么是B树2.建立B树的要求3.什么是B树4.Mysql里面为什么使用B树作为索引结构? 1. 什么是B树 B树是一种数据结构,用于在硬盘或其他非易失性存储介质上快速存储和访问大量数据。它是一种平衡树,其每个节点可以存储多个键…

zabbix SNMP traps 监控案例

目标 根据H3C网络设备 发送 SNMP trap 信息进行网络端口的告警。 具体过程 继上次配置的trap 方式进行监控一个案例。 其中log数据中的内容是: 20230330.163810 ZBXTRAP 192.168.21.148 UDP: [192.168.21.148]:52289->[172.18.18.2]:1162 DISMAN-EVENT-MIB::…

Keil5软件安装方法(兼容stm32与c51方法)

目录 一、下载软件包 二、安装软件 1、安装C51v960a.exe (1)右键以管理员权限运行程序 (2)开始安装软件 (3)勾选协议 (4)选择安装路径 (5)填写名字与邮箱 &#xff0…

我国元宇宙行业分析:政策、技术、资金助推行业探索多元化应用场景

1.元宇宙行业概述、特征及产业链图解 元宇宙是人类运用数字技术构建的,由现实世界映射或超越现实世界,可与现实世界交互的虚拟世界,具备新型社会体系的数字生活空间,主要具有沉浸式体验、开放性、虚拟身份、不断演化、知识互动、…

c/c++:指针,指针定义和使用,指针大小4字节,野指针,空指针*p=NULL

c/c:指针,指针定义和使用,指针大小4字节,野指针,空指针*pNULL 2022找工作是学历、能力和运气的超强结合体,遇到寒冬,大厂不招人,此时学会c的话, 我所知道的周边的会c的同学&#xf…

VMware开机自启虚拟机系统

一、前提 wmware开机自启,安装完毕wmware不用管,默认该软件以及相关服务就是开机自启准备waware虚拟机(一般都linux,我用centos7,你随意) 二、脚本 脚本命令如下,等待30秒(给服务自启…

NXP公司K20+PF8100实现硬件窗口看门狗

Kinetis K20 72 MHz MCU系列为中等性能的Kinetis产品组合提供了可扩展的入门级产品,具有差异化的集成,配备高精度模拟集成和灵活的低功耗功能。其相关资源可在NXP的官网获得。 PF81/PF82为PMIC系列专为高性能处理应用而设计,如娱乐中控、车载…

阅读完synchronized和ReentrantLock的源码后,我竟发现其完全相似

👏作者简介:大家好,我是爱敲代码的小黄,独角兽企业的Java开发工程师,CSDN博客专家,阿里云专家博主📕系列专栏:Java设计模式、数据结构和算法、Kafka从入门到成神、Kafka从成神到升仙…

【数据结构】七大排序之快速排序详解(挖坑法快排,非递归快排,二路快排,三路快排)

目录 1.快速排序核心思路 2.挖坑法快速排序(递归) 2.1步骤 2.2代码(详细注释) 3.非递归快排(用栈实现快速排序) 3.1思路 3.2代码 4.二路快排 4.1思路 4.2代码 5.三路快排 5.1思路 5.2代码 1.快速…

大白话chatGPT及其原理之快速理解篇

大白话chatGPT及其原理之快速理解篇 从GPT名字理解chatGPTchatGPT三步曲 声明:本文为原创,未经同意请勿转载,感谢配合😄 chatGPT今年年初的时候是非常火爆的,现在也有很多相关的应用和插件。当然现在也有很多新的技术出…

老宋 带你五分钟搞懂vue

Vue 1.1 什么是框架 任何编程语言在最初的时候都是没有框架的,后来随着在实际开发过程中不断总结『经验』,积累『最佳实践』,慢慢的人们发现很多『特定场景』下的『特定问题』总是可以『套用固定解决方案』。于是有人把成熟的『固定解决方案…

袋鼠云春季生长大会圆满落幕,带来数实融合下的新产品、新方案、新实践

4月20日,以“数实融合,韧性生长”为主题的袋鼠云春季生长大会圆满落幕。 在春季生长大会中,袋鼠云带来了数实融合趋势下的最新行业沉淀、最佳实践经验和行业前瞻性的产品发布。从大数据基础软件“数栈”、到低代码数字孪生世界“易知微”&…

离散数学-考纲版-01-命题逻辑

文章目录 1. 命题逻辑的等值演算与推理演算参考1.1 命题1.2 常用联结词1.3 命题公式命题公式的分类-重言式-矛盾式-可满足式等价关系式-逻辑等价 logically equivalent 1.4 命题的等值演算与推理基本等价式逻辑蕴涵重言式 logically implication重言蕴涵推到归结法 1.5 命题公式…

log4j2日志简单使用

log4j2日志使用 1、log4j2介绍 Apache Log4j2是对Log4j的升级版, log4j2借鉴了logback的一些优秀的设计,并且修复了一些问题,因此带来了一些重大的提升,主要有: 1、异常处理:在logback中,Appe…

Makefile通用模板

工程目录 假如我们有以下目录结构&#xff1a; . ├── inc │ ├── add.h │ └── sub.h ├── main.c └── src├── add.c└── sub.c文件中的内容如下&#xff1a; //main.c #include <stdio.h> #include "add.h" #include "sub.h&q…

Mysql 学习(六)Mysql的数据目录

数据库中数据的存放 Mysql中 InnoDB 和 MyISAM 这样的存储引擎都是把数据存储到磁盘上的&#xff0c;而我们把这种存放到磁盘上的东西叫做文件系统&#xff0c;当我们想读取对应数据的时候&#xff0c;就会把数据从文件系统上加载&#xff0c;并且处理返回给我们&#xff0c;当…

每日学术速递4.19

CV - 计算机视觉 | ML - 机器学习 | RL - 强化学习 | NLP 自然语言处理 Subjects: cs.CV 1.Visual Instruction Tuning 标题&#xff1a;可视化指令调优 作者&#xff1a;Haotian Liu, Chunyuan Li, Qingyang Wu, Yong Jae Lee 文章链接&#xff1a;https://arxiv.org/ab…