如何使用多线程改进我的程序

How to improve my program with Multi-threading

我写了一个更长的代码,读取天平的重量并将它们打印在 excel 文件中。现在我想添加一个 "plot" 函数,绘制每个余额的最后十个值。我遇到的问题是程序只会在测量 运行ds 后绘制,这可能需要更长的时间。现在我发现,多线程可以避免这个问题。但我不知道如何在我现有的代码中实现它。也许有人可以告诉我如何在右边添加 类 。因此,当主循环将它们写入 excel 文件时,我将能够绘制一些其他值。

以下是我的基本代码,很抱歉代码太长了,但我无法在不丢失整个上下文的情况下显示更小的版本。我删掉了天平 2 到 10 的测量命令,而且我只留下了一个绘图命令。因为他们都在工作并且与其他人非常相似。

测量功能"def Measurement():"将花费最长的时间,并且在当前运行正在前台活跃的部分

测量功能"def Measurement():"将花费最长的时间,并且在当前运行正在前台活跃的部分。这将阻止 GUI 中的所有其他命令,并导致其他命令排队。例如:我按下"plot"或"Help"按钮,它们会在一个运行测量循环后打开。

# -*- coding: utf-8 -*-

Created on Thu Aug  8 15:22:25 2019

The following program, is planed and designed to collect the weight information form 10 OHAUS SCOUT SKX2202 balances.

The program generates an GUI for the user to input all needed information and dates for the measurements. 

After collecting, the program writes all needed and important information to an Excel file (`.xlsx`). Also the program should give the possibility to plot some weight values while the measurement is running. 

# import  all needed basic libraries
import time 
import matplotlib.pyplot as plt
import os
import os.path
import string 
import serial
import sys
import xlsxwriter

from openpyxl import Workbook
from datetime import datetime
from os import path

#import all needed libraries for the graphic user interface

from PyQt5.QtWidgets import QApplication
from PyQt5.QtWidgets import QWidget
from PyQt5.QtWidgets import QLabel
from PyQt5.QtWidgets import QPushButton

from PyQt5.QtWidgets import QMessageBox
from PyQt5.QtWidgets import QLineEdit
from PyQt5.QtWidgets import QGridLayout
from PyQt5.QtWidgets import QHBoxLayout
from PyQt5.QtWidgets import QProgressBar
from PyQt5.QtWidgets import QVBoxLayout,QRadioButton, QPlainTextEdit

#creating the main window, show the code to be an application 

app = QApplication([])

app.setStyle('Fusio')
app.setStyleSheet("QPushButton {margin: 10ex;}")
window = QWidget()
window.setWindowTitle('Balance Messurements')

#creating the basic window elements 

#all buttons for the main window (text added to the button will show their use)
buttonOkName = QPushButton('Confirm User Name')
buttonOkAnzahl = QPushButton('Confirm input ')
buttonOkZeit = QPushButton('Confirm time ')
buttonStarter = QPushButton("Start")
buttonhelp = QPushButton("Help")
buttonClose = QPushButton("close")
buttonOpenFile = QPushButton("Open File")
buttonPlot = QPushButton("Plot Graph")
chooseTime = QLabel("Choose if your Input Time are Minutes or Seconds")

minWahl = QRadioButton("Minutes")
sekWahl = QRadioButton("Seconds")

#user input mask for the user name, only accept Letters, no numbers, no space

user_name = QLineEdit()
user_name.setPlaceholderText("User Name")
user_name.setInputMask("AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAaaaa")

#user input mask for number of measurements, only accept numbers , no Letters, no spaces
user_anzahl = QLineEdit()
user_anzahl.setPlaceholderText("Number")
user_anzahl.setInputMask('99999')

#user input mask for timeinterval, only accept numbers , no Letters, no spaces
user_zeit = QLineEdit()
user_zeit.setPlaceholderText("Timeintervall")
user_zeit.setInputMask('9999999999999')

#user input for the project name (written to the Excel file)
user_project=QLineEdit() 

#the labels to name the single inputfields
user_name_label = QLabel("User Name:")
user_anzahl_label= QLabel("Number of measurements:")
user_zeit_label = QLabel("Break interval:")
Savepath =QLabel("Saving Path: ")
nameoffile = QLabel("Filename: ")
proname = QLabel("Project Name: ")

#progressbar, get values at the lower code
progcounter = QProgressBar()

#implememting the single parts to a full layout

firstlay = QHBoxLayout()
firstlay.addWidget(user_name_label)
firstlay.addWidget(user_name) 
firstlay.addWidget (buttonOkName)
firstlay.addWidget(buttonhelp)

secondlay = QHBoxLayout()
secondlay.addWidget(user_anzahl_label)
secondlay.addWidget(user_anzahl)
secondlay.addWidget(buttonOkAnzahl)

layer10 = QHBoxLayout()
warning = QLabel("A breakinterval under 12 seconds can lead to irregular measurements.")
layer10.addWidget(warning)
layer10.addWidget(chooseTime)

layer11 = QHBoxLayout()
layer11.addWidget(minWahl)
layer11.addWidget(sekWahl)
layer11.addWidget(buttonOkZeit)

thirdlay = QHBoxLayout()
thirdlay.addWidget(user_zeit_label)
thirdlay.addWidget(user_zeit)

layer4 = QHBoxLayout()
notes = QLabel("Remarks/notes")
text_notes = QLineEdit()
layer4.addWidget(notes)
layer4.addWidget(text_notes)

layer5 =QHBoxLayout()
progres = QLabel("Progres: ")
layer5.addWidget(progres)
layer5.addWidget(progcounter)
layer5.addWidget(buttonStarter)

layer6= QHBoxLayout()
layer6.addWidget(proname)
layer6.addWidget(user_project)

layer7 = QHBoxLayout()
layer7.addWidget(Savepath)

layer8 = QHBoxLayout()
layer8.addWidget(nameoffile)

layer9 = QHBoxLayout()
layer9.addWidget(buttonPlot)

layer12 = QVBoxLayout()
layer12.addWidget(buttonClose)

#implementierung aller elemente in das zu zeugende Fenster

mainLayout = QGridLayout()
mainLayout.addLayout(firstlay, 1, 1, 1, 1)
mainLayout.addLayout(layer7,  2, 1, 1, 1)
mainLayout.addLayout(secondlay, 3 ,1, 1, 1)
mainLayout.addLayout(layer10, 5, 1, 1,1)
mainLayout.addLayout(layer11, 6, 1, 1, 1)
mainLayout.addLayout(thirdlay, 4,1, 1, 1)
mainLayout.addLayout(layer6, 7, 1, 1, 1)
mainLayout.addLayout(layer8, 9, 1, 1, 1)
mainLayout.addLayout(layer4, 8, 1, 1, 1)
mainLayout.addLayout(layer5, 10, 1, 1, 1)
mainLayout.addLayout(layer9, 11, 1, 1, 1)
mainLayout.addLayout(layer12,12,1,1,1)

#befehl das Fenster zu zeigen 

window.setLayout(mainLayout)
window.show()
# funktion um Eingabe auf "NUr Letter" zu überprüfen  
allowed_alpha = string.ascii_letters + string.whitespace

#following all functions will be defined

def on_minWahl_clicked():

    global zeit 
    zeit_vorab = int(user_zeit.text())
    zeit = zeit_vorab * 60
    global Pause 
    Pause_vorab = int(user_zeit.text())
    Pause = Pause_vorab * 60
    global Zeitangabe 
    Zeitangabe = " minutes"
    global intervall
    intervall = 'Time [min]'

def on_sekWahl_clicked():

    global zeit 
    zeit_vorab = int(user_zeit.text())
    zeit = zeit_vorab
    global Pause
    Pause = int(user_zeit.text())
    global Zeitangabe
    Zeitangabe = " seconds"
    global intervall
    intervall = 'Time [sec]'

#function to confirm the user name
def on_OkName_clicked(self):

     #convert the input to a global string for all other functions 
    name = str(user_name.text())
    user = str(name)

# function to confirm the number of measurements 
def on_OkAnzahl_clicked():
    while True:
        try: 
            #convert the input to a global integer for all other functions 
            global Anzahl 
            Anzahl = int(user_anzahl.text())
            break 
        except Exception as inst:
            print (type(inst) )    # the exception instance
            print (inst.args)      # arguments stored in .args
            print (inst)           # __str__ allows args to be printed directly
            x, y = inst.args
            print ('x =', x)
            print ('y =', y)

#function to confirm the time for the break interval
def on_OkZeit_clicked():
    while True:
        try: 
            break 
        except Exception as inst:
            print (type(inst) )    # the exception instance
            print (inst.args)      # arguments stored in .args
            print (inst)           # __str__ allows args to be printed directly
            x, y = inst.args
            print ('x =', x)
            print ('y =', y)

        intzeit = user_zeit.text()
        if intzeit < 12:
            alert = QMessageBox()
            alert.setText('Intervall needs to be 12 seconds or longer!')
            alert.exec_()

# start the main function/measurement function of this program by click on start button 
def on_start_clicked(): 
    while True: #function to run the measurements 
        try:
            Measurement() #start the measurements 
            break
        except IOError:  # return information if an error appears 
            alert = QMessageBox()
            alert.setText('Failed start the measurements!')
            alert.exec_()
            break        

# function to check the saving path

def check_path():

   dt = datetime.now()
   global save_path          
   global user
   user = user_name.text()
   global filename 
   filename =  filename = 'data_'+  dt.strftime("%Y%m%d_%I%M%S") + '.xlsx'     # generates the file name
   save_path = ('C:/Users/satzh/.spyder-py3/CPU Temp Measurements/' +user+'/')  #generates the full saving path


   a = os.path.exists(save_path)    # function to found out if path exist

   if a == True:     # path exist = use the existing path

       saveText = QLabel("Savepath: "+ save_path)   #show saving path in the main window
       layer7.addWidget(saveText)

       #shows the saving path in a message box
       alert = QMessageBox()   
       alert.setText("Userpath exist, Data will be saved into: %s"%save_path)
       alert.exec_()

   else:    #path does not excist, 

       os.makedirs(save_path) # creats a new folder for the user, as save path
       saveText = QLabel("Savepath: "+ save_path) # shows the new savepath in main window
       layer7.addWidget(saveText)
       #shows the saving path in a message box
       alert = QMessageBox()
       alert.setText("Path does not exist, file will be generated at:\n%s" %save_path)
       alert.exec_()
   name_end = QLabel(filename)
   layer8.addWidget(name_end)

   #check if their is a file withe the same name (not really probably, cause it's always named for the current time )
   while True:
    try:
        path.exists(save_path,filename)

        global wb     # open an new excel workbook/file 
        wb = Workbook()

        global sheet  # creates in the workbook a new  sheet
        sheet = wb.active
        sheet.title =  "Book1"
        wb.save(save_path+filename)   # saves the workbook/file at the given path

        #if the file exist, adding a 1 to the name and produce a new file 
        new_filename = 'filename'+'1'
        global workbook
        workbook = xlsxwriter.Workbook(save_path+ new_filename) 
        global worksheet
        worksheet = workbook.add_worksheet()

        break        #if an error appears, it is creating a provisionally file 
    except TypeError: 
        workbook = xlsxwriter.Workbook(save_path + filename)    
        worksheet = workbook.add_worksheet()

        break

    else:
        os.makedirs(save_path)
        saveText = QLabel("Savepath: "+ save_path)
        layer7.addWidget(saveText)
        workbook = xlsxwriter.Workbook(save_path+filename) 
        worksheet = workbook.add_worksheet()

# measurement main function, collect values form the balances and write this in an Excel file 
def Measurement():

    c = 0
    row = 2
    col = 0
    n = 0
    # generates the title line and basic information for the Excel file

    worksheet.write(0,0, 'Project Name:')
    worksheet.write(0,1, user_project.text())
    worksheet.write(0,2, "Remarks/Notes:")
    worksheet.write(0,3, text_notes.text())
    worksheet.write(0,4,"Number of measurements: %d"%Anzahl)
    worksheet.write(0,5, "Breakinterval: %d"%Pause)
    worksheet.write(1,0, 'Date' )
    worksheet.write(1,1, 'Balance 1')
    worksheet.write(1,2, 'Balance 2')
    worksheet.write(1,3, 'Balance 3')
    worksheet.write(1,4, 'Balance 4')
    worksheet.write(1,5, 'Balance 5')
    worksheet.write(1,6, 'Balance 6')
    worksheet.write(1,7, 'Balance 7')
    worksheet.write(1,8, 'Balance 8')
    worksheet.write(1,9, 'Balance 9')
    worksheet.write(1,10, 'Balance 10')
    worksheet.set_column('A:B', 30)
    worksheet.set_column('B:L',15)

    #generates and show a message box that the measurements are started
    alert = QMessageBox()
    alert.setText("Measurements in progress")
    alert.exec_()

    # generating the value list for each balance for the print command
    global lb1,lb2,lb3,lb4,lb5,lb6,lb7,lb8,lb9,lb10
    lb1 =[]
    lb2=[]
    lb3=[]
    lb4=[]
    lb5=[]
    lb6=[]
    lb7=[]
    lb8=[]
    lb9=[]
    lb10=[]

    # measurments loop, 
    while c <= Anzahl:
        dt = datetime.now()   #create a shortcut for the date and time value
        date_format = workbook.add_format({'num_format': 'd.mm.yyyy  hh:mm:ss'}) #create the format which is written to the Excel file
        worksheet.write_datetime(row,col, dt, date_format) 

        # the following notes  are for all 10 measurements loops the same:

        #Measurement loop for balance 1
        while True:
            try:
                bal1 = serial.Serial('COM7' ,9600,    bytesize=8, stopbits=1, timeout =0.1) #opens the serial/com-port for balance 1 
                #and set the bal1 variable for the port name/class

                break  #ends the loop when balance was opened
            except OSError:  #except Error for failing to open the serial port 
                alert = QMessageBox()     #generates a messagebox to say that an error appears
                alert.setText("Failed to open Balance 1")
                alert.exec_()
                break
        while True:
            try: 
                bal1.write(b'Z\r\n')             #sends the first few characters to the balance, to activate her and make her responding to the Computer 
                s=bal1.read(17)                  #reads out the  weight information
                bal1.write(b'\x50')              #sends the "print" command to the balance
                t=list(str(s))                   # creating a list of strings of the measurement values
                liste = t 
                satz = "".join(liste [3:13] )     # reads the numbers out of the list and generate a new combined string 

                zwischen_string = satz           # generates the values for the plotting lists 
                lb1.append(zwischen_string)      # connect the value to the print list
                gewicht = zwischen_string.replace('.', ',') #change the dot to a comma for the exported Value to excel
                worksheet.write(row, 1 , gewicht)           # writes the value to the Excel file
                bal1.close()                                # close the serial port
                break 
            except OSError:
                alert = QMessageBox()
                alert.setText("Failed to open collect and write the information form balance 1")
                alert.exec_()
                break

        row += 1 #say the program, to use a new row for the next measuremnt in the Excel file
        c +=1    # counter for measurements +1, next measurements for the while loop
        time.sleep(Pause) # set the break, which is given by the user 

        #loop to set the maximum number of list elements to 10
        #for the direct printing of the values

        def  check_plotlists():
            if len(lb1)>=10:
                del lb1[0]
            if len(lb2)>=10:
               del lb2[0]
            if len(lb3)>=10:
                del lb3[0]
            if len(lb4)>=10:
                del lb4[0]
            if len(lb5)>=10:
                del lb5[0]
            if len(lb6)>=10:
                del lb6[0]
            if len(lb7)>=10:
                del lb7[0]
            if len(lb8)>=10:
                del lb8[0]
            if len(lb9)>=10:
                del lb9[0]
            if len(lb10)>=10:
                del lb10[0]

        app.processEvents()   #allows to send back the percent value vor the processbar
        prozent =(n/Anzahl)*100 #creating the percent value
        progcounter.setValue(prozent) # gives back the percent value to the processbar
        n +=1                         #counter to generate the percent value
    workbook.close()   # close the excel file
    layer9.addWidget(buttonOpenFile) # add the "open file" button 

# öffnet am ende die angelegt datei

def on_OpenFile_clicked():

   os.chdir
   os.system('start excel.exe "%s%s"' % (save_path,filename ))

#helpFuntion, reads text from Help Instructions.txt file

#generating the help window, by clicking on the Help button 
def open_help():
    f = open('C:\Users\satzh\.spyder-py3\Help Instructions.txt',"r") #source for the help text 
    x = f.read() #reads the text from the txt. file 

    global windowhelp
    windowhelp = QWidget()
    windowhelp.setWindowTitle('Help') 
    buttonClose = QPushButton("Close")
    buttonClose.clicked.connect(on_close_clicked)
    helplayer1 = QVBoxLayout()
    help_text= QPlainTextEdit()
    help_text.appendPlainText(x)
    help_text.zoomIn(4)
    helplayer1.addWidget(help_text)
    helplayer1.addWidget(buttonClose)
    HelpLayout = QGridLayout()
    HelpLayout.addLayout(helplayer1,1,1,1,1)
    windowhelp.setLayout(HelpLayout)
    windowhelp.setGeometry(300,300 ,800,480 )
    windowhelp.show()

# close function for help window
def on_close_clicked():
    windowhelp.close()

#generating plot menu
def on_plot_clicked():
    #window generating

    global printwindow
    printwindow = QWidget()
    printwindow.setWindowTitle('Plott') 

    #button generating 
    Balance1=QPushButton("Print Balance 1")
    Balance2=QPushButton("Print Balance 2")
    Balance3=QPushButton("Print Balance 3")
    Balance4=QPushButton("Print Balance 4")
    Balance5=QPushButton("Print Balance 5")
    Balance6=QPushButton("Print Balance 6")
    Balance7=QPushButton("Print Balance 7")
    Balance8=QPushButton("Print Balance 8")
    Balance9=QPushButton("Print Balance 9")
    Balance10=QPushButton("Print Balance 10")
    Balance11=QPushButton("Print All together")
    Pclose = QPushButton("Close")

    #generate all buttons for the print menu
    printLayout1 = QHBoxLayout()
    printLayout1.addWidget(Balance1)
    printLayout1.addWidget(Balance2)
    printLayout1.addWidget(Balance3)
    printLayout2 = QHBoxLayout()
    printLayout2.addWidget(Balance4)
    printLayout2.addWidget(Balance5)
    printLayout2.addWidget(Balance6)
    printLayout3 = QHBoxLayout()
    printLayout3.addWidget(Balance7)
    printLayout3.addWidget(Balance8)
    printLayout3.addWidget(Balance9)
    printLayout4 = QHBoxLayout()
    printLayout4.addWidget(Balance10)    
    printLayout4.addWidget(Balance11)

    printLayout5 = QHBoxLayout()
    printLayout5.addWidget(Pclose)

    #generate the printlayout
    printFinal = QVBoxLayout()
    printFinal.addLayout(printLayout1)
    printFinal.addLayout(printLayout2)
    printFinal.addLayout(printLayout3)
    printFinal.addLayout(printLayout4)
    printFinal.addLayout(printLayout5)

    #connect the print buttons to print orders
    printwindow.setLayout(printFinal)
    printwindow.show()
    Balance1.clicked.connect(print_1)
    Balance2.clicked.connect(print_2)
    Balance3.clicked.connect(print_3)
    Balance1.clicked.connect(print_4)
    Balance5.clicked.connect(print_5)
    Balance6.clicked.connect(print_6)
    Balance7.clicked.connect(print_7)
    Balance8.clicked.connect(print_8)
    Balance9.clicked.connect(print_9)
    Balance10.clicked.connect(print_10)
    Balance11.clicked.connect(print_11)
    Pclose.clicked.connect(on_pclose_clicked)

#closing the print menue    
def on_pclose_clicked():
    printwindow.close()

#printing for balancce 1
    #following notes are for all print funtions the same 
def print_1():
    fig = plt.figure()  #generates the basic figure for the plot, like the new window
    plt.plot(lb1)       #give the dates which should be printed
    plt.title('Hydrogen Evolution')  #generates the head-title 
    plt.ylabel('Weight [g]')         # name fo the y cooridnates 
    plt.xlabel(intervall)            # name for the x intervall (is defined by the choose between seconds and minutes functions above)
    ax = plt.subplot(111)          
    ax.legend()
    plt.ylim(-1, 425)                # defined the intervall on the y coordinates 
    xmax= Anzahl*Pause               #defined the intervall on the x coordinates
    plt.xlim(0, xmax )
    plt.show()
    timestamp = time.strftime("%d%m%Y-%H%M%S")                              #generates part of the saving name
    command = "fig.savefig('"+timestamp+"_h2evo_multiplot.png', dpi=200)"   # saves the figure
    exec(command)
    fig.savefig('multiplot.png' , dpi=200)





#closing the Mainwindow and main Process
def close_all():

    window.close()
    sys.exit(app.exec_())
    exit()

#connect the Buttons with their functions 

buttonOkName.clicked.connect(on_OkName_clicked) #confirm name with checking user name 
buttonOkName.clicked.connect( check_path) #confrim name with function to check saving path 
buttonOkAnzahl.clicked.connect(on_OkAnzahl_clicked) #confirm number of messurements
buttonOkZeit.clicked.connect(on_OkZeit_clicked)    #confirm time input
buttonStarter.clicked.connect(on_start_clicked)
buttonOpenFile.clicked.connect(on_OpenFile_clicked)
minWahl.toggled.connect(on_minWahl_clicked)
sekWahl.toggled.connect(on_sekWahl_clicked)
buttonhelp.clicked.connect(open_help)
buttonPlot.clicked.connect(on_plot_clicked)
buttonClose.clicked.connect(close_all)

app.exec_()




在这里的另一个线程中,我发现了以下内容:QThread 可以 运行 事件循环,QRunnable 没有,所以不要将它用于设计为具有事件循环的任务。来自:C++/Qt - QThread vs QRunnable

假设您想要运行一个名为my_func的函数使用一个线程。

import threading
my_thread = threading.Thread(target=my_func, args=())
my_thread.start()

如果你想让线程在整个程序退出后退出,在开始之前输入这个

my_thread.setDaemon(True)

如果你想传递函数需要的变量(参数)。假设你有一个函数 add(num1, num2) returns num1+num2,你在列表的 args 中传递 num1 和 num2,如下所示:

add_thread = threading.Thread(target=add, args=([4, 6]))

Quamash这样的库应该用来防止Qt UI线程中断进程相关的线程执行。 自定义 QEventLoop 实现可能是做类似事情的替代且耗时的方式。 如果 python 线程和 Qt UI 线程不受管理,代码开始生成意外的 errors/behaviors

import sys
import asyncio
import time

from PyQt5.QtWidgets import QApplication, QProgressBar
from quamash import QEventLoop, QThreadExecutor

app = QApplication(sys.argv)
loop = QEventLoop(app)
asyncio.set_event_loop(loop)  # NEW must set the event loop

progress = QProgressBar()
progress.setRange(0, 99)
progress.show()

async def master():
    await first_50()
    with QThreadExecutor(1) as exec:
        await loop.run_in_executor(exec, last_50)
    # TODO announce completion?

async def first_50():
    for i in range(50):
        progress.setValue(i)
        await asyncio.sleep(.1)

def last_50():
    for i in range(50,100):
        loop.call_soon_threadsafe(progress.setValue, i)
        time.sleep(.1)

with loop: ## context manager calls .close() when loop completes, and releases all resources
    loop.run_until_complete(master())