【问题标题】:Tkinter/Matplotlib animation assistanceTkinter/Matplotlib 动画辅助
【发布时间】:2016-11-23 04:54:21
【问题描述】:

我正在开发一个程序,我需要对两个不同的图表进行动画处理。我很难弄清楚如何使用我正在使用的结构来做到这一点。我将在下面粘贴我的代码,以便您尝试。我已经尽可能地精简了它,同时仍然保留了核心功能,所以希望它不会太难理解。在当前状态下,动画线没有做任何事情,所以请让我知道我哪里出错了。

from Tkinter import *               #Used for GUI elements
import time                         #Used for timing elements
import matplotlib                   #Used for graphing
matplotlib.use("TkAgg")
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg, NavigationToolbar2TkAgg
from matplotlib.figure import Figure
import matplotlib.animation as animation
import numpy as np                  #Used for arrays to find min/max of float array
import random                       #Only used for fake data input


class tkgui:
    def __init__(self, parent):
        #--------------The following are variables that need to be accessed by other functions----------------------
        #Raw input values
        self.x = 209500
        self.y = 0
        self.timeElapsed = 0

        #State values
        self.curFrame = 1

        #List Values
        self.timeList = np.array([])
        self.yList = np.array([])
        self.xList = np.array([])

        self.lastX = 0
        #----------------------------------------------------------------------------------------------------------

        #Make Tkinter fullscreen
        w, h = 320,240 #int(str(root.winfo_screenwidth())), int(str(root.winfo_screenheight())) #320, 240 is the RPiTFT

        #The base layer of the GUI
        topLevelContainer = Frame(parent)
        topLevelContainer.pack()

        #The two 'screens' to switch between. They contain everything on the GUI
        self.buttonsFrame = Frame(topLevelContainer)
        self.graphFrame = Frame(topLevelContainer)

        #Stack the frames so that they are switchable
        for frame in self.buttonsFrame, self.graphFrame:
            frame.grid(row=0, column=0, sticky='news', padx=5, pady=(10, 10))

        buttonsFrameButtons = Frame(self.buttonsFrame)
        buttonsFrameButtons.pack(side=LEFT, padx=(0, 50))

        #X button
        self.xButton = Button(buttonsFrameButtons, command=self.xButtonClick)
        self.xButton.configure(text="X", background="#C8C8C8", width=6, padx=35, pady=35)
        self.xButton.pack(side=TOP, pady=10)

        #Y button
        self.yButton = Button(buttonsFrameButtons, command=self.yButtonClick)
        self.yButton.configure(text="Y", background="#C8C8C8", width=6, padx=35, pady=35)
        self.yButton.pack(side=TOP, pady=10)

        #Bar graph
        buttonsFrameBar = Frame(self.buttonsFrame)
        buttonsFrameBar.pack(side=LEFT)

        self.fBar = Figure(figsize=(2, 4), dpi=50)
        aBar = self.fBar.add_subplot(111)
        self.xBar = aBar.bar([0, 1], [0, 0], width=1)

        lAxes = self.fBar.gca()
        lAxes.axes.get_xaxis().set_ticklabels([])

        aBar.set_ylim([-30000, 30000])
        self.fBar.tight_layout()

        self.buttonsFrame.tkraise()         

        #Setup the matplotlib graph
        self.fGraph = Figure(figsize=(5, 3), dpi=50)
        #Create the Y axis
        aGraph = self.fGraph.add_subplot(111)
        aGraph.set_xlabel("Time (s)")
        aGraph.set_ylabel("Y")
        self.yLine, = aGraph.plot([],[], "r-")

        #Create the X axis
        a2Graph = aGraph.twinx()
        self.xLine, = a2Graph.plot([], [])
        a2Graph.set_ylabel("X")

        #Final matplotlib/Tkinter setup
        self.canvasGraph = FigureCanvasTkAgg(self.fGraph, master=self.graphFrame)
        self.canvasGraph.show()
        self.canvasGraph.get_tk_widget().pack(side=LEFT, fill=BOTH, expand=1)

        self.canvasBar = FigureCanvasTkAgg(self.fBar, master=buttonsFrameBar)
        self.canvasBar.show()
        self.canvasBar.get_tk_widget().pack(side=BOTTOM, fill=BOTH, expand=1)

        #Resize the plot to fit all of the labels in
        self.fGraph.subplots_adjust(bottom=0.13, left=0.15, right=0.87)       

    def refreshGraph(self, frameno):     #Redraw the graph with the updated arrays and resize it accordingly
        #Update data
        self.yLine.set_data(self.timeList, self.yList)
        self.xLine.set_data(self.timeList, self.xList)

        #Update y axis
        ax = self.canvasGraph.figure.axes[0]
        ax.set_xlim(self.timeList.min(), self.timeList.max())
        ax.set_ylim(self.yList.min(), self.yList.max())

        #Update x axis
        ax2 = self.canvasGraph.figure.axes[1]
        ax2.set_xlim(self.timeList.min(), self.timeList.max())
        ax2.set_ylim(self.xList.min(), self.xList.max())

        #Redraw
        self.canvasGraph.draw()


    def refreshBar(self, frameno):
        curX = self.x
        dif = curX - self.lastX
        i = [dif]
        for rect, h in zip(self.xBar, i):
            rect.set_height(h)
            if dif > 0:
                rect.set_color('b')
            else:
                rect.set_color('r')

        self.canvasBar.draw()
        self.lastX=curX

    def switchFrame(self):      #Switch the current screen. Either x/y buttons or graph
        if self.curFrame:
            self.graphFrame.tkraise()
            self.curFrame = 0
        else:
            self.buttonsFrame.tkraise()
            self.curFrame = 1

    def xButtonClick(self):
        self.switchFrame()

    def yButtonClick(self):
        self.close()

    def close(e):               #Exit the program
        sys.exit()

#Initialisation of global variables
lastTime = 0        #Used for the 'last time' iterated
yState = 0       

def updateNumbers():        #Used to generate fake input variables. Will be replaced by ADC values
    global lastTime
    global yState

    curTime = time.time()                                           #Update the time each time the function is called
    if curTime - lastTime > 0.5:                                    #Only update numbers every 0.5 seconds
        gui.x = random.randrange(200000, 230000)                   #Generates x
        if yState:
            gui.y = gui.y - 20                                #Decrease y
            if gui.y < 1:
                yState = 0                                       #Until it gets to a minimum of 0
        else:
            gui.y = gui.y + 20                                #Increase y
            if gui.y > 1300:
                yState = 1                                       #Until it reaches a maximum of 1300
        gui.yList = np.append(gui.yList, gui.y)            #Add the new y values to the array
        gui.xList = np.append(gui.xList, gui.x/10000.0)          #Add the new x values to the array
        lastTime = time.time()                                      #Record the last time iterated for timing purposes
        gui.timeElapsed += 0.5                                      
        gui.timeList = np.append(gui.timeList, gui.timeElapsed)     #Add the latest time to the array
##        gui.refreshGraph()                                          #Call the function that will redraw the graph with the new figures

if __name__ == "__main__":
    root = Tk()         #Root Tkinter setup
    gui = tkgui(root)   #Setup the gui class

    updateNumbers()
    aniGraph = animation.FuncAnimation(gui.fGraph,gui.refreshGraph,interval=500,frames=100,repeat=True)
    aniBar = animation.FuncAnimation(gui.fBar,gui.refreshBar,interval=500,frames=100,repeat=True)

    while(1):                   #Main loop
        updateNumbers()         #Update fake values

        root.update()           #Update the gui loop

    root.mainloop()             #Tkinter main loop

为了清楚起见,我只是在问如何让动画为这段代码工作。

【问题讨论】:

  • 如果您使用while(1):,则永远不会使用root.mainloop()。在updateNumber 结束时,您可以使用root.after(500, updateNumber),它会在500 毫秒(0.5 秒)后再次运行updateNumber,因此您不需要while(1) 循环。
  • 一开始我看到了条形动画,点击 X 按钮后我看到了线条动画 - 它可以工作。不知道是什么问题。
  • 这个版本的代码中没有显示,但由于其他原因我需要运行常量循环。我的印象是 while(1) 中的 root.update() 解决了这个问题?你有没有改变任何东西,因为当我运行相同的代码时,我看不到任何动画。
  • mainloop 获取键/鼠标事件,将其发送到windgets,更新小部件中的数据,调用分配给按钮的函数,并重绘小部件。如果您运行无休止的 while-loop,那么您就不会运行 mainloop 并且它无法完成它的工作。 update() 强制 tkinter 做一个循环。但是tkinter has root.after()` 将函数添加到特殊列表,mainloop 稍后执行它 - 但只执行一次。但是,如果您将 after 放在此函数的末尾,那么它会像在 while(1) 循环中一样一次又一次地自行运行,并且不会停止 mainloop
  • 好吧,after 可以在 tkinter 中使用,类似于 FuncAnimation 在 matplotlib 中使用。所以也许你只能使用after

标签: python matplotlib tkinter


【解决方案1】:

你的代码对我有用——我看到了所有的动画——但如果你在没有While(1):(或更多pythonic While True:)的情况下运行它,那么你可以使用root.after(milliseconds, function_name)。您可以使用它代替FuncAnimation

它可以让你控制功能 - 启动/停止它。

if self.run_bar:
   root.after(100, self.refreshBar)

您使用启动它(或重新启动它)

self.run_bar = True
self.refreshBar()

你可以阻止它

self.run_bar = False

在代码中查看所有# &lt;--

from Tkinter import *               #Used for GUI elements
import time                         #Used for timing elements
import matplotlib                   #Used for graphing
matplotlib.use("TkAgg")
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg, NavigationToolbar2TkAgg
from matplotlib.figure import Figure
import matplotlib.animation as animation
import numpy as np                  #Used for arrays to find min/max of float array
import random                       #Only used for fake data input


class TkGUI: # <-- CamelCase names for classes
             # <-- empty line for readabelity
    def __init__(self, parent):
        #--- The following are variables that need to be accessed by other functions----------------------
        #Raw input values
        self.x = 209500
        self.y = 0
        self.timeElapsed = 0

        #State values
        self.curFrame = 1

        #List Values
        self.timeList = np.array([])
        self.yList = np.array([])
        self.xList = np.array([])

        self.lastX = 0
        #-----------------------------------------------------------

        #Make Tkinter fullscreen
        w, h = 320,240 #int(str(root.winfo_screenwidth())), int(str(root.winfo_screenheight())) #320, 240 is the RPiTFT

        #The base layer of the GUI
        topLevelContainer = Frame(parent)
        topLevelContainer.pack()

        #The two 'screens' to switch between. They contain everything on the GUI
        self.buttonsFrame = Frame(topLevelContainer)
        self.graphFrame = Frame(topLevelContainer)

        #Stack the frames so that they are switchable
        for frame in self.buttonsFrame, self.graphFrame:
            frame.grid(row=0, column=0, sticky='news', padx=5, pady=(10, 10))

        buttonsFrameButtons = Frame(self.buttonsFrame)
        buttonsFrameButtons.pack(side=LEFT, padx=(0, 50))

        #X button
        self.xButton = Button(buttonsFrameButtons, command=self.xButtonClick)
        self.xButton.configure(text="X", background="#C8C8C8", width=6, padx=35, pady=35)
        self.xButton.pack(side=TOP, pady=10)

        #Y button
        self.yButton = Button(buttonsFrameButtons, command=self.yButtonClick)
        self.yButton.configure(text="Y", background="#C8C8C8", width=6, padx=35, pady=35)
        self.yButton.pack(side=TOP, pady=10)

        #Bar graph
        buttonsFrameBar = Frame(self.buttonsFrame)
        buttonsFrameBar.pack(side=LEFT)

        self.fBar = Figure(figsize=(2, 4), dpi=50)
        aBar = self.fBar.add_subplot(111)
        self.xBar = aBar.bar([0, 1], [0, 0], width=1)

        lAxes = self.fBar.gca()
        lAxes.axes.get_xaxis().set_ticklabels([])

        aBar.set_ylim([-30000, 30000])
        self.fBar.tight_layout()

        self.buttonsFrame.tkraise()         

        #Setup the matplotlib graph
        self.fGraph = Figure(figsize=(5, 3), dpi=50)
        #Create the Y axis
        aGraph = self.fGraph.add_subplot(111)
        aGraph.set_xlabel("Time (s)")
        aGraph.set_ylabel("Y")
        self.yLine, = aGraph.plot([],[], "r-")

        #Create the X axis
        a2Graph = aGraph.twinx()
        self.xLine, = a2Graph.plot([], [])
        a2Graph.set_ylabel("X")

        #Final matplotlib/Tkinter setup
        self.canvasGraph = FigureCanvasTkAgg(self.fGraph, master=self.graphFrame)
        self.canvasGraph.show()
        self.canvasGraph.get_tk_widget().pack(side=LEFT, fill=BOTH, expand=1)

        self.canvasBar = FigureCanvasTkAgg(self.fBar, master=buttonsFrameBar)
        self.canvasBar.show()
        self.canvasBar.get_tk_widget().pack(side=BOTTOM, fill=BOTH, expand=1)

        #Resize the plot to fit all of the labels in
        self.fGraph.subplots_adjust(bottom=0.13, left=0.15, right=0.87)       

    def refreshGraph(self): # <-- without argument
        '''Redraw the graph with the updated arrays and resize it accordingly''' # <-- docstring used by documentation generator and IDE as help 

        #Update data
        self.yLine.set_data(self.timeList, self.yList)
        self.xLine.set_data(self.timeList, self.xList)

        #Update y axis
        ax = self.canvasGraph.figure.axes[0]
        ax.set_xlim(self.timeList.min(), self.timeList.max())
        ax.set_ylim(self.yList.min(), self.yList.max())

        #Update x axis
        ax2 = self.canvasGraph.figure.axes[1]
        ax2.set_xlim(self.timeList.min(), self.timeList.max())
        ax2.set_ylim(self.xList.min(), self.xList.max())

        #Redraw
        self.canvasGraph.draw()

        # run again after 100ms (0.1s)
        root.after(100, self.refreshGraph)  # <-- run again  like in loop

    def refreshBar(self): # <-- without argument
        curX = self.x
        dif = curX - self.lastX
        i = [dif]
        for rect, h in zip(self.xBar, i):
            rect.set_height(h)
            if dif > 0:
                rect.set_color('b')
            else:
                rect.set_color('r')

        self.canvasBar.draw()
        self.lastX=curX
        # run again after 100ms (0.1s)
        root.after(100, self.refreshBar)  # <-- run again like in loop

    def switchFrame(self):      #Switch the current screen. Either x/y buttons or graph
        if self.curFrame:
            self.graphFrame.tkraise()
            self.curFrame = 0
        else:
            self.buttonsFrame.tkraise()
            self.curFrame = 1

    def xButtonClick(self):
        self.switchFrame()

    def yButtonClick(self):
        self.close()

    def close(e):  # Exit the program
        sys.exit()

#Initialisation of global variables
lastTime = 0        #Used for the 'last time' iterated
yState = 0       

def updateNumbers():        #Used to generate fake input variables. Will be replaced by ADC values
    global lastTime
    global yState

    curTime = time.time()                                           #Update the time each time the function is called
    if curTime - lastTime > 0.5:                                    #Only update numbers every 0.5 seconds
        gui.x = random.randrange(200000, 230000)                   #Generates x
        if yState:
            gui.y = gui.y - 20                                #Decrease y
            if gui.y < 1:
                yState = 0                                       #Until it gets to a minimum of 0
        else:
            gui.y = gui.y + 20                                #Increase y
            if gui.y > 1300:
                yState = 1                                       #Until it reaches a maximum of 1300
        gui.yList = np.append(gui.yList, gui.y)            #Add the new y values to the array
        gui.xList = np.append(gui.xList, gui.x/10000.0)          #Add the new x values to the array
        lastTime = time.time()                                      #Record the last time iterated for timing purposes
        gui.timeElapsed += 0.5                                      
        gui.timeList = np.append(gui.timeList, gui.timeElapsed)     #Add the latest time to the array

    # run again after 100ms (0.1s)
    root.after(100, updateNumbers)  # <-- run again like in loop       

if __name__ == "__main__":
    root = Tk()
    gui = TkGUI(root)

    # <-- vvv - without While and without FuncAnimation - vvv

    updateNumbers()     # run first time 

    gui.refreshBar()    # run first time 
    gui.refreshGraph()  # run first time 

    # <-- ^^^ - without While and without FuncAnimation - ^^^

    root.mainloop()     # Tkinter main loop

编辑:当然你可以在没有after的情况下保留FuncAnimation,并且只在updateNumbers中使用after

from Tkinter import *               #Used for GUI elements
import time                         #Used for timing elements
import matplotlib                   #Used for graphing
matplotlib.use("TkAgg")
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg, NavigationToolbar2TkAgg
from matplotlib.figure import Figure
import matplotlib.animation as animation
import numpy as np                  #Used for arrays to find min/max of float array
import random                       #Only used for fake data input


class TkGUI: # <-- CamelCase names for classes
             # <-- empty line for readabelity
    def __init__(self, parent):
        #--- The following are variables that need to be accessed by other functions----------------------
        #Raw input values
        self.x = 209500
        self.y = 0
        self.timeElapsed = 0

        #State values
        self.curFrame = 1

        #List Values
        self.timeList = np.array([])
        self.yList = np.array([])
        self.xList = np.array([])

        self.lastX = 0
        #-----------------------------------------------------------

        #Make Tkinter fullscreen
        w, h = 320,240 #int(str(root.winfo_screenwidth())), int(str(root.winfo_screenheight())) #320, 240 is the RPiTFT

        #The base layer of the GUI
        topLevelContainer = Frame(parent)
        topLevelContainer.pack()

        #The two 'screens' to switch between. They contain everything on the GUI
        self.buttonsFrame = Frame(topLevelContainer)
        self.graphFrame = Frame(topLevelContainer)

        #Stack the frames so that they are switchable
        for frame in self.buttonsFrame, self.graphFrame:
            frame.grid(row=0, column=0, sticky='news', padx=5, pady=(10, 10))

        buttonsFrameButtons = Frame(self.buttonsFrame)
        buttonsFrameButtons.pack(side=LEFT, padx=(0, 50))

        #X button
        self.xButton = Button(buttonsFrameButtons, command=self.xButtonClick)
        self.xButton.configure(text="X", background="#C8C8C8", width=6, padx=35, pady=35)
        self.xButton.pack(side=TOP, pady=10)

        #Y button
        self.yButton = Button(buttonsFrameButtons, command=self.yButtonClick)
        self.yButton.configure(text="Y", background="#C8C8C8", width=6, padx=35, pady=35)
        self.yButton.pack(side=TOP, pady=10)

        #Bar graph
        buttonsFrameBar = Frame(self.buttonsFrame)
        buttonsFrameBar.pack(side=LEFT)

        self.fBar = Figure(figsize=(2, 4), dpi=50)
        aBar = self.fBar.add_subplot(111)
        self.xBar = aBar.bar([0, 1], [0, 0], width=1)

        lAxes = self.fBar.gca()
        lAxes.axes.get_xaxis().set_ticklabels([])

        aBar.set_ylim([-30000, 30000])
        self.fBar.tight_layout()

        self.buttonsFrame.tkraise()         

        #Setup the matplotlib graph
        self.fGraph = Figure(figsize=(5, 3), dpi=50)
        #Create the Y axis
        aGraph = self.fGraph.add_subplot(111)
        aGraph.set_xlabel("Time (s)")
        aGraph.set_ylabel("Y")
        self.yLine, = aGraph.plot([],[], "r-")

        #Create the X axis
        a2Graph = aGraph.twinx()
        self.xLine, = a2Graph.plot([], [])
        a2Graph.set_ylabel("X")

        #Final matplotlib/Tkinter setup
        self.canvasGraph = FigureCanvasTkAgg(self.fGraph, master=self.graphFrame)
        self.canvasGraph.show()
        self.canvasGraph.get_tk_widget().pack(side=LEFT, fill=BOTH, expand=1)

        self.canvasBar = FigureCanvasTkAgg(self.fBar, master=buttonsFrameBar)
        self.canvasBar.show()
        self.canvasBar.get_tk_widget().pack(side=BOTTOM, fill=BOTH, expand=1)

        #Resize the plot to fit all of the labels in
        self.fGraph.subplots_adjust(bottom=0.13, left=0.15, right=0.87)       

    def refreshGraph(self, i):
        '''Redraw the graph with the updated arrays and resize it accordingly''' # <-- docstring used by documentation generator and IDE as help 

        #Update data
        self.yLine.set_data(self.timeList, self.yList)
        self.xLine.set_data(self.timeList, self.xList)

        #Update y axis
        ax = self.canvasGraph.figure.axes[0]
        ax.set_xlim(self.timeList.min(), self.timeList.max())
        ax.set_ylim(self.yList.min(), self.yList.max())

        #Update x axis
        ax2 = self.canvasGraph.figure.axes[1]
        ax2.set_xlim(self.timeList.min(), self.timeList.max())
        ax2.set_ylim(self.xList.min(), self.xList.max())

        #Redraw
        self.canvasGraph.draw()

    def refreshBar(self, i):
        curX = self.x
        dif = curX - self.lastX
        i = [dif]
        for rect, h in zip(self.xBar, i):
            rect.set_height(h)
            if dif > 0:
                rect.set_color('b')
            else:
                rect.set_color('r')

        self.canvasBar.draw()
        self.lastX=curX

    def switchFrame(self):
        '''Switch the current screen. Either x/y buttons or graph'''

        if self.curFrame:
            self.graphFrame.tkraise()
            self.curFrame = 0
        else:
            self.buttonsFrame.tkraise()
            self.curFrame = 1

    def xButtonClick(self):
        self.switchFrame()

    def yButtonClick(self):
        self.close()

    def close(e):  # Exit the program
        sys.exit()

#Initialisation of global variables
lastTime = 0        #Used for the 'last time' iterated
yState = 0       

def updateNumbers():        #Used to generate fake input variables. Will be replaced by ADC values
    global lastTime
    global yState

    curTime = time.time()                                           #Update the time each time the function is called
    if curTime - lastTime > 0.5:                                    #Only update numbers every 0.5 seconds
        gui.x = random.randrange(200000, 230000)                   #Generates x
        if yState:
            gui.y = gui.y - 20                                #Decrease y
            if gui.y < 1:
                yState = 0                                       #Until it gets to a minimum of 0
        else:
            gui.y = gui.y + 20                                #Increase y
            if gui.y > 1300:
                yState = 1                                       #Until it reaches a maximum of 1300
        gui.yList = np.append(gui.yList, gui.y)            #Add the new y values to the array
        gui.xList = np.append(gui.xList, gui.x/10000.0)          #Add the new x values to the array
        lastTime = time.time()                                      #Record the last time iterated for timing purposes
        gui.timeElapsed += 0.5                                      
        gui.timeList = np.append(gui.timeList, gui.timeElapsed)     #Add the latest time to the array

    # run again after 100ms (0.1s)
    root.after(100, updateNumbers)  # <-- run again like in loop       

if __name__ == "__main__":
    root = Tk()
    gui = TkGUI(root)

    aniGraph = animation.FuncAnimation(gui.fGraph,gui.refreshGraph,interval=500,frames=100,repeat=True)
    aniBar = animation.FuncAnimation(gui.fBar,gui.refreshBar,interval=500,frames=100,repeat=True)

    # <-- vvv - without While - vvv

    updateNumbers()     # run first time 

    # <-- ^^^ - without While - ^^^

    root.mainloop()     # Tkinter main loop

【讨论】:

  • 谢谢,这会奏效。但是我想知道使用 matplotlib 动画的东西是否会比仅仅重绘它更优化。我这样说是因为在我的主代码中,当我开始获取大型数据集和长图时,或者如果我更快地更新它,事情就会开始运行有点慢。
  • 我从不检查这两种方法的速度,但我没有更改refreshBarrefreshGraph 中的任何内容,因此这两种方法应该以相同的方式以相同的速度工作。也许如果你检查 FuncAnimation 源代码,那么也许你也会看到 after() :) matplotlib/FuncAnimation 要使用 tkinter 必须使用 tkinter 函数。
  • 顺便说一句:你可以在没有after 的情况下保留FuncAnimation 并仅在updateNumbers 中使用after - 它也可以。
  • 所以回到我原来的问题,使用上面答案中的编辑,我没有得到任何动画,即使它是完全相同的代码。
  • 在终端/控制台/cmd.exe/powershell中运行代码,可能会出现一些错误信息。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2020-04-19
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2013-08-28
  • 2014-02-07
相关资源
最近更新 更多