【问题标题】:How to put multiple separate graphs into one Tkinter window?如何将多个单独的图形放入一个 Tkinter 窗口?
【发布时间】:2021-07-27 03:42:40
【问题描述】:

我一直在运行这个脚本:

from threading import Thread
import serial
import time
import collections
import matplotlib.pyplot as plt
import matplotlib.animation as animation
import struct
import copy
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg, NavigationToolbar2Tk
import tkinter as Tk
from tkinter.ttk import Frame

class serialPlot:
    def __init__(self, serialPort='/dev/ttyACM0', serialBaud=38400, plotLength=100, dataNumBytes=2, numPlots=4):
        self.port = serialPort
        self.baud = serialBaud
        self.plotMaxLength = plotLength
        self.dataNumBytes = dataNumBytes
        self.numPlots = numPlots
        self.rawData = bytearray(numPlots * dataNumBytes)
        self.dataType = None
        if dataNumBytes == 2:
            self.dataType = 'h'     # 2 byte integer
        elif dataNumBytes == 4:
            self.dataType = 'f'     # 4 byte float
        self.data = []
        self.privateData = None
        for i in range(numPlots):   # give an array for each type of data and store them in a list
            self.data.append(collections.deque([0] * plotLength, maxlen=plotLength))
        self.isRun = True
        self.isReceiving = False
        self.thread = None
        self.plotTimer = 0
        self.previousTimer = 0
        # self.csvData = []

        print('Trying to connect to: ' + str(serialPort) + ' at ' + str(serialBaud) + ' BAUD.')
        try:
            self.serialConnection = serial.Serial(serialPort, serialBaud, timeout=4)
            print('Connected to ' + str(serialPort) + ' at ' + str(serialBaud) + ' BAUD.')
        except:
            print("Failed to connect with " + str(serialPort) + ' at ' + str(serialBaud) + ' BAUD.')

    def readSerialStart(self):
        if self.thread == None:
            self.thread = Thread(target=self.backgroundThread)
            self.thread.start()
            # Block till we start receiving values
            while self.isReceiving != True:
                time.sleep(0.1)

    def getSerialData(self, frame, lines, lineValueText, lineLabel, timeText, pltNumber):
        if pltNumber == 0:  # in order to make all the clocks show the same reading
            currentTimer = time.perf_counter()
            self.plotTimer = int((currentTimer - self.previousTimer) * 1000)     # the first reading will be erroneous
            self.previousTimer = currentTimer
        self.privateData = copy.deepcopy(self.rawData)    # so that the 3 values in our plots will be synchronized to the same sample time
        timeText.set_text('' + str(self.plotTimer) + '')
        data = self.privateData[(pltNumber*self.dataNumBytes):(self.dataNumBytes + pltNumber*self.dataNumBytes)]
        value,  = struct.unpack(self.dataType, data)
        self.data[pltNumber].append(value)    # we get the latest data point and append it to our array
        lines.set_data(range(self.plotMaxLength), self.data[pltNumber])
        lineValueText.set_text('[' + lineLabel + '] = ' + str(value))

    def backgroundThread(self):    # retrieve data
        time.sleep(1.0)  # give some buffer time for retrieving data
        self.serialConnection.reset_input_buffer()
        while (self.isRun):
            self.serialConnection.readinto(self.rawData)
            self.isReceiving = True
            #print(self.rawData)

    def sendSerialData(self, data):
        self.serialConnection.write(data.encode('utf-8'))

    def close(self):
        self.isRun = False
        self.thread.join()
        self.serialConnection.close()
        print('Disconnected...')
        # df = pd.DataFrame(self.csvData)
        # df.to_csv('/home/rikisenia/Desktop/data.csv')


class Window(Frame):
    def __init__(self, figure, master, SerialReference):
        Frame.__init__(self, master)
        self.entry = []
        self.setPoint = None
        self.master = master        # a reference to the master window
        self.serialReference = SerialReference      # keep a reference to our serial connection so that we can use it for bi-directional communicate from this class
        self.initWindow(figure)     # initialize the window with our settings

    def initWindow(self, figure):
        self.master.title("Haptic Feedback Grasping Controller")
        canvas = FigureCanvasTkAgg(figure, master=self.master)
        toolbar = NavigationToolbar2Tk(canvas, self.master)
        canvas.get_tk_widget().pack(side=Tk.TOP, fill=Tk.BOTH, expand=1)

        # create out widgets in the master frame
        lbl1 = Tk.Label(self.master, text="Distance")
        lbl1.pack(padx=5, pady=5)
        self.entry = Tk.Entry(self.master)
        self.entry.insert(0, '0')     # (index, string)
        self.entry.pack(padx=5)
        SendButton = Tk.Button(self.master, text='Send', command=self.sendFactorToMCU)
        SendButton.pack(padx=5)

    def sendFactorToMCU(self):
        self.serialReference.sendSerialData(self.entry.get() + '%')     # '%' is our ending marker

def main():
    # portName = 'COM5'
    portName = '/dev/ttyACM0'
    baudRate = 38400
    maxPlotLength = 100     # number of points in x-axis of real time plot
    dataNumBytes = 4        # number of bytes of 1 data point
    numPlots = 1            # number of plots in 1 graph
    s = serialPlot(portName, baudRate, maxPlotLength, dataNumBytes, numPlots)   # initializes all required variables
    s.readSerialStart()                                               # starts background thread

    # plotting starts below
    pltInterval = 50    # Period at which the plot animation updates [ms]
    xmin = 0
    xmax = maxPlotLength
    ymin = -(1)
    ymax = 200
    fig = plt.figure()
    ax = plt.axes(xlim=(xmin, xmax), ylim=(float(ymin - (ymax - ymin) / 10), float(ymax + (ymax - ymin) / 10)))
    ax.set_title('Strain Gauge/ToF')
    ax.set_xlabel("Time")
    ax.set_ylabel("Force/Distance")

    # put our plot onto Tkinter's GUI
    root = Tk.Tk()
    app = Window(fig, root, s)

    lineLabel = ['W', 'X', 'Y', 'Z']
    style = ['y-', 'r-', 'c-', 'b-']  # linestyles for the different plots
    timeText = ax.text(0.70, 0.95, '', transform=ax.transAxes)
    lines = []
    lineValueText = []
    for i in range(numPlots):
        lines.append(ax.plot([], [], style[i], label=lineLabel[i])[0])
        lineValueText.append(ax.text(0.70, 0.90-i*0.05, '', transform=ax.transAxes))
    anim = animation.FuncAnimation(fig, s.getSerialData, fargs=(lines, lineValueText, lineLabel, timeText), interval=pltInterval)    # fargs has to be a tuple

    plt.legend(loc="upper left")
    root.mainloop()   # use this instead of plt.show() since we are encapsulating everything in Tkinter

    s.close()


if __name__ == '__main__':
    main()

即使我有 4 个传感器具有来自 Arduino 的数据,也会显示一个没有数据通过的窗口。该窗口当前包含 1 个图形,其中包含 4 个绘图。我想要 4 张图,每张图都在一个窗口中。我一直在使用https://thepoorengineer.com/en/python-gui/ 作为在 python 中制作图表的参考。数据传输的代码也在链接中。我尝试结合他的 2 个不同的代码并对其进行调试以制作 4 个图形,每个图形与一个绘图一起使用一个 Tkinter GUI 窗口,但它不起作用。我还收到 TypeError: getSerialData() missing 1 required positional argument: 'pltNumber' 的错误。如果 pltNumber 在代码的括号中,不知道为什么会出现此错误。我是python的初学者。我应该更改什么才能使代码正常工作?

脚本可以生成 4 个单独的图表,每个图表都有一个不在 Tkinter GUI 中的图表(与 4 个传感器一起使用,但我需要在 Tkinter 窗口中使用它们):

from threading import Thread
import serial
import time
import collections
import matplotlib.pyplot as plt
import matplotlib.animation as animation
import struct
import copy


class serialPlot:
    def __init__(self, serialPort='/dev/ttyACM0', serialBaud=38400, plotLength=100, dataNumBytes=2, numPlots=1):
        self.port = serialPort
        self.baud = serialBaud
        self.plotMaxLength = plotLength
        self.dataNumBytes = dataNumBytes
        self.numPlots = numPlots
        self.rawData = bytearray(numPlots * dataNumBytes)
        self.dataType = None
        if dataNumBytes == 2:
            self.dataType = 'h'     # 2 byte integer
        elif dataNumBytes == 4:
            self.dataType = 'f'     # 4 byte float
        self.data = []
        self.privateData = None     # for storing a copy of the data so all plots are synchronized
        for i in range(numPlots):   # give an array for each type of data and store them in a list
            self.data.append(collections.deque([0] * plotLength, maxlen=plotLength))
        self.isRun = True
        self.isReceiving = False
        self.thread = None
        self.plotTimer = 0
        self.previousTimer = 0

        print('Trying to connect to: ' + str(serialPort) + ' at ' + str(serialBaud) + ' BAUD.')
        try:
            self.serialConnection = serial.Serial(serialPort, serialBaud, timeout=4)
            print('Connected to ' + str(serialPort) + ' at ' + str(serialBaud) + ' BAUD.')
        except:
            print("Failed to connect with " + str(serialPort) + ' at ' + str(serialBaud) + ' BAUD.')

    def readSerialStart(self):
        if self.thread == None:
            self.thread = Thread(target=self.backgroundThread)
            self.thread.start()
            # Block till we start receiving values
            while self.isReceiving != True:
                time.sleep(0.1)

    def getSerialData(self, frame, lines, lineValueText, lineLabel, timeText, pltNumber):
        if pltNumber == 0:  # in order to make all the clocks show the same reading
            currentTimer = time.perf_counter()
            self.plotTimer = int((currentTimer - self.previousTimer) * 1000)     # the first reading will be erroneous
            self.previousTimer = currentTimer
        self.privateData = copy.deepcopy(self.rawData)    # so that the 3 values in our plots will be synchronized to the same sample time
        timeText.set_text('' + str(self.plotTimer) + '')
        data = self.privateData[(pltNumber*self.dataNumBytes):(self.dataNumBytes + pltNumber*self.dataNumBytes)]
        value,  = struct.unpack(self.dataType, data)
        self.data[pltNumber].append(value)    # we get the latest data point and append it to our array
        lines.set_data(range(self.plotMaxLength), self.data[pltNumber])
        lineValueText.set_text('[' + lineLabel + '] = ' + str(value))

    def backgroundThread(self):    # retrieve data
        time.sleep(1.0)  # give some buffer time for retrieving data
        self.serialConnection.reset_input_buffer()
        while (self.isRun):
            self.serialConnection.readinto(self.rawData)
            self.isReceiving = True

    def close(self):
        self.isRun = False
        self.thread.join()
        self.serialConnection.close()
        print('Disconnected...')


def makeFigure(xLimit, yLimit, title):
    xmin, xmax = xLimit
    ymin, ymax = yLimit
    fig = plt.figure()
    ax = plt.axes(xlim=(xmin, xmax), ylim=(int(ymin - (ymax - ymin) / 10), int(ymax + (ymax - ymin) / 10)))
    ax.set_title(title)
    ax.set_xlabel("Time")
    ax.set_ylabel("Force/Distance")
    return fig, ax


def main():
    # portName = 'COM5'
    portName = '/dev/ttyACM0'
    baudRate = 38400
    maxPlotLength = 100     # number of points in x-axis of real time plot
    dataNumBytes = 4        # number of bytes of 1 data point
    numPlots = 4            # number of plots in 1 graph
    s = serialPlot(portName, baudRate, maxPlotLength, dataNumBytes, numPlots)   # initializes all required variables
    s.readSerialStart()                                               # starts background thread

    # plotting starts below
    pltInterval = 50    # Period at which the plot animation updates [ms]
    lineLabelText = ['W', 'X', 'Y', 'Z']
    title = ['Strain Gauge 1 Force', 'Strain Gauge 2 Force', 'ToF 1 Distance', 'ToF 2 Distance']
    xLimit = [(0, maxPlotLength), (0, maxPlotLength), (0, maxPlotLength), (0, maxPlotLength)]
    yLimit = [(-1, 1), (-1, 1), (-1, 1), (-1, 1)]
    style = ['y-', 'r-', 'g-', 'b-']    # linestyles for the different plots
    anim = []
    for i in range(numPlots):
        fig, ax = makeFigure(xLimit[i], yLimit[i], title[i])
        lines = ax.plot([], [], style[i], label=lineLabelText[i])[0]
        timeText = ax.text(0.50, 0.95, '', transform=ax.transAxes)
        lineValueText = ax.text(0.50, 0.90, '', transform=ax.transAxes)
        anim.append(animation.FuncAnimation(fig, s.getSerialData, fargs=(lines, lineValueText, lineLabelText[i], timeText, i), interval=pltInterval))  # fargs has to be a tuple
        plt.legend(loc="upper left")
    plt.show()

    s.close()


if __name__ == '__main__':
    main()

【问题讨论】:

    标签: python matplotlib tkinter


    【解决方案1】:

    编辑:被误解的问题,但仍将其留在这里,因为它对您处理图表很有用

    这是我用来生成 5 个列表框并将它们附加到字典中的一些代码,以便稍后在代码中引用它们。

            self.listboxes = []
            for i in range(5):
                self.lb = tk.Listbox(self.modeSelect)
                self.lb.configure(background='#2f2a2d', exportselection='false', font='{Arial} 12 {}', foreground='#feffff', height='23')
                self.lb.configure(relief='flat', width='9', justify='center', selectbackground='#feffff', selectforeground='#000000')
                self.lb.pack(side='left')
                self.listboxes.append(self.lb)
                self.lb.bind("<Double-1>", self.getIndexLB)
    

    然后您可以使用分配函数或调用属性

    self.listboxes[0].get() #for example
    

    这样您可以为每个图表分配点,它还允许您通过执行以下操作同时控制所有图表:

    for child in self.modeSelect.winfo_children():
          if isinstance(child, tk.Listbox):
              child.delete(index)
    

    【讨论】:

    • 动态生成4张图是什么意思?我确实有一个代码可以制作 4 个单独的图表,每个图表都有一个情节。尽管这些图表不在带有文本框和按钮的 Tkinter GUI 中,但我也想要。我将添加该代码供您查看。
    • 编写的代码没有适当的缩进。
    • 图表是否嵌入在 tkinter 对象中? @AshbyS。
    • @MatrixProgrammer 我是 Stack 新手,请见谅
    • 没问题,对不起,我不是有意冒犯你,只是作为一个建议。
    【解决方案2】:

    我认为像this 这样的东西可能很有用。

    this 帖子中也解决了类似问题。

    这里我们可以使用matplotlib的后端类,即FigureCanvasTkAgg。 它的工作方式类似于 tkinter 画布,但还可以在其中绘制图形。

    这意味着我们可以初始化多个 matplotlib 图形,在它们上绘制图形,然后将这些图形绘制到画布上。 这允许我们在同一个 tkinter 窗口上绘制多个图。

    要导入这个类-:

    from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg
    

    然后可以使用 matplotlib 的图形对象在画布上绘制图形,如下所示 -:

    from matplotlib.figure import Figure
    fig = Figure(...) # Initializing the figure object.
    canvas = FigureCanvasTkAgg(fig, master=root) # Initializing the FigureCanvasTkAgg Class Object.
    tk_canvas = canvas.get_tk_widget() # Getting the Figure canvas as a tkinter widget.
    tk_canvas.pack() # Packing it into it's master window.
    canvas.draw() # Drawing the canvas onto the screen.
    

    同样可以初始化多个画布并将其打包到 tk 窗口中,从而给出多个绘制的图形。 可以使用 matplotlib 方法来绘制图形对象。

    两个这样的数字的完整代码将变为-:

    from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg
    from matplotlib.figure import Figure
    
    # FOR FIRST GRAPH
    
    fig = Figure(...) # Initializing the figure object.
    canvas = FigureCanvasTkAgg(fig, master=root) # Initializing the FigureCanvasTkAgg Class Object.
    tk_canvas = canvas.get_tk_widget() # Getting the Figure canvas as a tkinter widget.
    tk_canvas.pack() # Packing it into it's master window.
    canvas.draw() # Drawing the canvas onto the screen.
    
    # FOR SECOND GRAPH
    
    fig_2 = Figure(...) # Initializing the second figure object.
    canvas_2 = FigureCanvasTkAgg(fig_2, master=root) # Initializing the second FigureCanvasTkAgg Class Object.
    tk_canvas_2 = canvas_2.get_tk_widget() # Getting the second Figure canvas as a tkinter widget.
    tk_canvas_2.pack() # Packing it into it's master window.
    canvas_2.draw() # Drawing the second canvas onto the screen.
    
    # CAN BE REPEATED FOR MULTIPLE SUCH GRAPHS....
    

    【讨论】:

    • 另外我建议查看@BryanOakley 回答的this 帖子
    最近更新 更多