Приложение Python GUI (tkinter.ttk) медленно - PullRequest
2 голосов
/ 25 марта 2012

У меня есть (рабочее) приложение, выполненное с помощью ttk. Он использует самодельный модуль для отображения связанных с компортом элементов управления и холста, который рисует на нем несколько графиков. Когда я создаю экземпляр моего объекта, он запускает поток, в котором обрабатывает последовательный ввод и добавляет его в список (один список на график). Когда у меня 3-6 графиков, приложение становится заметно медленным. В нем также есть несколько ошибок, но я их устраню, когда закончу с общей концепцией.

Вещи, которые могут вам помочь:

  • comport - это экземпляр самописного объекта, который является производным от LabelFrame и Serial.Serial
  • координаты для графиков хранятся в словаре списков: self.graphs = {} self.graphs ['name1'] = [] количество сохраненных координат до ширины холста, поэтому около 1000-2000 на график. Есть шесть графики - умножьте на 6
  • С каждой приходящей новой координатой я выскакиваю (0) из списка и append () новая координата
  • Я забыл, я также сохраняю время поступления каждого нового набора координат в отдельном списке
  • Я использую преиодическую функцию вызова для обработки списков: self.after (100, func = self.periodicCall) Таким образом каждые 100 мс я удаляю (ВСЕ) с холста и я рисую каждый график с этими линиями. Так что, если у меня есть 1000 координат в 6 графиках я рисую 6000 маленьких линий
  • Плюс некоторая служебная информация, например, о нескольких линейках

Так что, думаю, идея ясна. Я хочу выяснить, что будет лучшим подходом. Я только начинающий в python, а также в программировании, поэтому я прошу вашего оправдания за код, который я собираюсь опубликовать, и за боль в ваших глазах, которую это вызовет. У меня нет стиля программирования, и я хочу это исправить. По крайней мере, немного. Так что любые другие комментарии по поводу всего, что вы увидите в коде, приветствуются.

#-------------------------------------------------------------------------------
# Name:        dataVisualizer
# Purpose:
#
# Author:      dccharacter
#
# Created:     23.03.2012
# Copyright:   (c) dccharacter 2012
# Licence:     <your licence>
#-------------------------------------------------------------------------------
#!/usr/bin/env python

from tkinter import *
from tkinter.ttk import *
from robowidgets.serialPortGui import *
import threading
import re
import atexit
import random
from datetime import datetime
import time

class dataVisualizer(LabelFrame):
    def __init__(self, master, comport , cnf={}, **kw):
        self.master = master
        self.comport = comport
        LabelFrame.__init__(self, *cnf, **kw)

        self.messageVar = StringVar()
        Label(self, text="Message format regexp:").pack()
        self.messagePattern = Entry(self, width = 20, text = 234234, textvariable = self.messageVar);
        self.messageVar.set(r'(-*\d+),(-*\d+),(-*\d+),(-*\d+),(-*\d+),(-*\d+)')
        self.messagePattern.pack()
        Button(self, text = "Pause", command = self.pause).pack()
        self.pauseFlag = TRUE

        self.canvWidth, self.canvHeight = 1000, 700
        self.density = 1 ##width of pixel - the bigger, the wider graph
        self.numOfDots = self.canvWidth//self.density
        self.graphs = {}
        self.graphs['name1']=[]
        self.graphs['name2']=[]
        self.graphs['name3']=[]
        self.graphs['name4']=[]
        self.graphs['name5']=[]
        self.graphs['name6']=[]
        self.timings = []
        self.zeroTiming = datetime.now()
        self.colors = ['red', 'blue', 'green', 'orange', 'violet', 'black', 'cyan']

        self.canv = Canvas(self, width = self.canvWidth, height = self.canvHeight)
        self.canv.pack()

        self.thread = threading.Thread(target = self.workerThread)
        self.thread.start()

        self.serialData = []

        self.periodicCall()

    def pause(self):
        self.pauseFlag = ~self.pauseFlag

    def redraw(self):
        self.canv.delete(ALL)

        colorIndex = 0
        for graphName in self.graphs:
            runningAverage = sum(self.graphs[graphName][-10:])//10
            text = str(runningAverage)
            self.canv.create_text(self.canvWidth-60, 20*(colorIndex+1), text = text,
                fill = self.colors[colorIndex], anchor = W)
            prev_xxx, prev_yyy = 0, 0
            for yyy in self.graphs[graphName]:
                self.canv.create_line(prev_xxx, prev_yyy, prev_xxx+self.density, self.canvHeight//2 - yyy,
                    width = 1.4, fill = self.colors[colorIndex])
                prev_xxx, prev_yyy = prev_xxx+self.density, self.canvHeight//2 - yyy
            colorIndex = colorIndex + 1
        self.drawMesh()

    def drawMesh(self):
        self.canv.create_rectangle(3, 3, self.canvWidth,
            self.canvHeight, outline = 'black', width = 2)
        self.canv.create_line(0, self.canvHeight/2, self.canvWidth,
            self.canvHeight/2, fill="black", width = 1)

        mouseX = self.canv.winfo_pointerx() - self.canv.winfo_rootx()
        mouseY = self.canv.winfo_pointery() - self.canv.winfo_rooty()

        if mouseY < 60: aaa = -1
        else: aaa = 1
        if mouseX > self.canvWidth - 200 : bbb = -12
        else: bbb = 1
        try:
            self.canv.create_rectangle(mouseX + 10*bbb - 5, mouseY - 20*aaa +10,
                mouseX + 10*bbb + 115, mouseY - 20*aaa - 30, outline = "black",
                fill = "red")
            self.canv.create_text(mouseX + 10*bbb, mouseY - 40*aaa,
                text = "t="+str(self.timings[mouseX//self.density]),
                anchor = W)
            self.canv.create_text(mouseX + 10*bbb, mouseY - 20*aaa,
                text = "value="+str(self.canvHeight//2 - mouseY),
                anchor = W)
        except IndexError:
            pass
        self.canv.create_line(mouseX, 0, mouseX,
            self.canvHeight, fill="blue", dash = [4, 1, 2, 1], width = 1)
        self.canv.create_line(0, mouseY, self.canvWidth,
            mouseY, fill="blue", dash = [4, 1, 2, 1], width = 1)


    def periodicCall(self):
        self.redraw()
        self.after(100, func=self.periodicCall)

    def workerThread(self):

        while (1):
            try:
                if self.comport.isOpen() and (self.pauseFlag == TRUE):
                    comLine = self.comport.readline()
                    if len(self.timings) == self.numOfDots:
                        self.timings.pop(0)
                    td = datetime.now() - self.zeroTiming

                    ##  b'271;-3:-50\r\n'
                    parsedLine = re.search(self.messagePattern.get(), str(comLine))
                    index = 1
                    if parsedLine:
                        self.timings.append(td)
                        for graphName in self.graphs:
                            if len(self.graphs[graphName]) == self.numOfDots:
                                self.graphs[graphName].pop(0)
                            try:
                                self.graphs[graphName].append(int(parsedLine.group(index)))
                            except IndexError:
                                self.graphs[graphName].append(0)
                            index = index + 1
                else:
                    self.comport.flush();
                    time.sleep(1)
            except TclError:
                self.thread._stop()

def main():
    root = Tk()
    mainWindow = Frame(root)
    mainWindow.pack()
    port = comPortWidget(mainWindow)
    port.pack()
    dv = dataVisualizer(mainWindow, port)
    dv.pack()
    root.mainloop()

if __name__ == '__main__':
    main()

И серийная часть - также может отставать (раньше она отставала, когда я использовал для повторного перечисления портов каждую секунду или около того ...)

#-------------------------------------------------------------------------------
# Name:        robowidgets
# Purpose:
#
# Author:      dccharacter
#
# Created:     10.03.2012
# Copyright:   (c) dccharacter 2012
# Licence:     <your licence>
#-------------------------------------------------------------------------------
#!/usr/bin/env python

import serial
from serial.tools.list_ports_windows import comports
from tkinter import *
from tkinter.ttk import *

class comPortWidget(LabelFrame, serial.Serial):

    commonComPortSpeeds = ["1200", "2400", "4800", "9600", "14400", "19200", "38400", "57600", "115200"]

    def __init__(self, master=None, cnf={}, **kw):
        """Construct a comPortWidget widget with the parent MASTER.

        STANDARD OPTIONS

            borderwidth, cursor, font, foreground,
            highlightbackground, highlightcolor,
            highlightthickness, padx, pady, relief,
            takefocus, text, background, class, colormap, container,
            height, labelanchor, labelwidget,
            visual, width

        WIDGET-SPECIFIC OPTIONS


        """
        self.master = master
        LabelFrame.__init__(self, master, text="Serial settings", *cnf, **kw)
        serial.Serial.__init__(self)
        self.parent = master
        self.draw()

    def draw(self):
        self.strVarComPort = StringVar()
        self.comboComport = Combobox(self,
            textvariable=self.strVarComPort)

        self.comboComport.grid(row=0, column=1)
        self.labelComportName = Label(self, text="Com port:")
        self.labelComportName.grid(row=0, column=0)

        self.strVarComSpeed = StringVar()
        self.comboComSpeed = Combobox(self,
            textvariable=self.strVarComSpeed, values=self.commonComPortSpeeds)
        self.comboComSpeed.current(len(self.commonComPortSpeeds)-1)
        self.comboComSpeed.grid(row=1, column=1)
        self.labelComSpeed = Label(self, text="Com speed:")
        self.labelComSpeed.grid(row=1, column=0)

        self.buttonComOpen = Button(self, text="Open port", command=self.openPort)
        self.buttonComOpen.grid(row=0, column=2)
        self.buttonComClose = Button(self, text="Close port", command=self.closePort)
        self.buttonComClose.grid(row=1, column=2)
        self.buttonRefreshPorts = Button(self, text="Re", width=3, command=self.refreshComPortsCombo)
        ##self.buttonRefreshPorts.grid(row=0, column=2)

        self.refreshComPortsCombo()

    def refreshComPortsCombo(self):
        listComs = self.enumerateComPorts()
        if not listComs:
            listComs.append("No com ports found")
            self.disableControls(~self.isOpen())
            self.buttonComClose.configure(state=DISABLED)
        else:
            self.disableControls(self.isOpen())
        self.buttonRefreshPorts.configure(state=NORMAL)
        self.comboComport.config(values=listComs)
        self.comboComport.current(len(listComs)-1)
        ##self.after(500, func=self.refreshComPortsCombo)

    def enumerateComPorts(self):
        """
        Returns the list ofcom port names in the system or an empty list if
        no ports found
        """
        listComs = []
        for port, desc, hwid in sorted(comports()):
            listComs.append(port)
        return listComs

    def openPort(self):
        if self.isOpen():
            return
        self.port = self.comboComport.get()
        self.baudrate = int(self.comboComSpeed.get())
        self.timeout = 1
        try:
            self.open()
            self.disableControls(self.isOpen())
        except IOError:
            pass

    def closePort(self):
        if self.isOpen():
            self.flush()
            self.close()
            self.disableControls(self.isOpen())

    def disableControls(self, isConnected):
        if isConnected:
            self.labelComportName.configure(state=DISABLED)
            self.labelComSpeed.configure(state=DISABLED)
            self.comboComport.configure(state=DISABLED)
            self.comboComSpeed.configure(state=DISABLED)
            self.buttonComClose.configure(state=NORMAL)
            self.buttonComOpen.configure(state=DISABLED)
            self.buttonRefreshPorts.configure(state=DISABLED)
        else:
            self.labelComportName.configure(state=NORMAL)
            self.labelComSpeed.configure(state=NORMAL)
            self.comboComport.configure(state=NORMAL)
            self.comboComSpeed.configure(state=NORMAL)
            self.buttonComClose.configure(state=DISABLED)
            self.buttonComOpen.configure(state=NORMAL)
            self.buttonRefreshPorts.configure(state=NORMAL)

def main():
    pass

if __name__ == '__main__':
    main()

ОБНОВЛЕНИЕ: Я сделал так, как советовал Брайан. Теперь у меня есть две функции перерисовки экрана. Разница между ними заключается в том, что сначала все линии перемещаются влево, добавляются новые справа и удаляются те, которые падают с холста. Второй перемещает линии влево и повторно развертывает элементы, которые падают с холста вправо (без создания новых). С каждым из них есть огромное улучшение по сравнению с моим первоначальным вариантом, но я не вижу большой разницы между этими двумя невооруженным глазом - возможно, если бы у меня было больше элементов, я бы. Последнее, однако, лучше работает специально для моего приложения, так как мне не нужно отслеживать тех, кто падает со скалы.

Вот функции:

def drawGraph(self): ###needed for self.updateGraph2() only as it is creates the lines
    for graphNum in range(0, self.numOfGraphs):
        self.graphLines.append([])
        self.graphData.append([0,]*self.numOfDots)
        for iii in range(0,self.numOfDots):
            self.graphLines[graphNum].append(
                self.canv.create_line(0,0,0,0,fill=self.colors[graphNum],
                width=1.2, tags=('graphLines', 'graph'+str(graphNum)))
                )


def updateGraph2(self):
    while not self.queue.empty():
        iTuple = self.queue.get()
        self.canv.move('graphLines', -self.density,0)
        for graphNum in range(0, self.numOfGraphs):
            try: self.graphData[graphNum].append(iTuple[graphNum])
            except IndexError:
                self.graphData[graphNum].append(0)
            self.graphData[graphNum].pop(0)
            self.graphLines[graphNum].append(self.graphLines[graphNum].pop(0))
            self.canv.coords(self.graphLines[graphNum][-1],
                self.canv.winfo_width()-self.density,
                int(int(self.graphData[graphNum][-2])+int(self.canv.winfo_height()//2)),
                self.canv.winfo_width(),
                int(int(self.graphData[graphNum][-1])+int(self.canv.winfo_height()//2))
                )

def updateGraph(self):
    while not self.queue.empty():
        self.timingIndex = self.timingIndex + 1
        self.canv.move('graphLines', -self.density, 0)
        iTuple = self.queue.get()
        for iii in range(0, len(iTuple)):
            yyy = int(iTuple[iii])+self.canv.winfo_height()//2
            if yyy < 0: yyy = 0
            if yyy > self.canv.winfo_height(): yyy = self.canv.winfo_height()
            prev_yyy = int(self.prevTuple[iii])+self.canv.winfo_height()//2
            if prev_yyy < 0: prev_yyy = 0
            if prev_yyy > self.canv.winfo_height(): prev_yyy = self.canv.winfo_height()
            self.canv.create_line(
                self.canv.winfo_width()-self.density, prev_yyy,
                self.canv.winfo_width(), yyy,
                width = 1.4, fill = self.colors[iii], tags=('graphLines','graph'+str(iii)))
        self.prevTuple = iTuple

        self.canv.addtag_overlapping('todelete',-1,-1,-3,self.canv.winfo_height()+1)
        self.canv.dtag('preserve','todelete')
        self.canv.delete('todelete')

1 Ответ

2 голосов
/ 25 марта 2012

Мое понимание холста состоит в том, что чем больше идентификаторов элементов было выделено, тем медленнее оно становится. Он может обрабатывать десятки тысяч без особых проблем (и, может быть, даже сотни тысяч), но если вы создаете и удаляете 6000 элементов каждые 100 мс, это, вероятно, ваша проблема. Даже если вы удаляете элементы, это все равно влияет на производительность, особенно когда вы создаете 60 000 в секунду.

Вместо удаления всех элементов каждые 100 мс, просто переместите элементы за пределы экрана и запомните их, а затем повторно используйте их, используя метод coords, чтобы изменить их координаты для нового графика.

...