Python GUI (tkinter.ttk) application slow - performance

I've got (working) application done with ttk. It uses self-created module for showing a comport-related controls and a canvas which draws a few graphs on it. When I crate an instance of my object, it starts a thread in which processes serial input and appends this to a list (one list per graph). When I have 3-6 graphs, the application gets noticeably slow. It also has got a few bugs, but I will adress them when I'm done with the general concept.
Things that may help you help me:
comport is an instance of a self-written object that derives from
LabelFrame and Serial.Serial
coordinates for graphs are stored in a dictionary of lists:
self.graphs = {} self.graphs['name1']=[] number of coordinates stored
is up to the width of canvas, so about 1000-2000 per graph. Have six
graphs - please multiply by 6
With every new coordinate arriving I pop(0) from the list and
append() the new coordinate
I forgot, I also store timing of each new set of coordinates arriving
in a separate list
I use a preiodic call function to process the lists: self.after(100,
func=self.periodicCall)Thus every 100ms I delete(ALL) from the canvas
and I draw every graph with theset of lines. So if I have 1000 coords
in 6 graps, I draw 6000 small lines
Plus some service info of course such as a few rulers
So I guess the idea is clear. I want to figure out what would be the better approach. I'm just a started in python and in programming as well, so I'm asking for your excuse for the code that I'm going to post and for the pain in your eyes it's gonna cause. I don't have any programming style and I want to fix it. At least a bit. So any other comments on anything you'll se in the code are welcome.
#-------------------------------------------------------------------------------
# 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()
And the serial part - may lag as well (used to lag when I used to reenumerate ports evey second or so...)
#-------------------------------------------------------------------------------
# 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()
UPDATE: I did as Brian advised. Now I have two screen redraw functions. Difference between them is that first moves all the lines to the left adding new to the right and deleting those that fall off the canvas. The second one moves lines to the left and re-deploys elements that fall off the canvas to the right (without creating new ones). There's a huge improvement with any of these in respect to my initial variant, but I don't see big difference between the two wit the naked eye - mayme if I had more elements I would. The latter though works better specifically for my application as I don't have to track those who fall off the cliff.
Here the functions:
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')

My understanding of the canvas is that the more element ids that have been allocated, the slower it gets. It can handle tens of thousands without much problem (and maybe even 100's of thousands), but if you're creating and deleting 6000 items every 100ms, that is likely your problem. Even though you are deleting the items, it still affects performance especially when you are creating 60,000 per second.
Instead of deleting all items every 100ms, simply move the items off screen and remember them, then reuse them by using the coords method to change their coordinates for the new graph.

Related

What is a good way to draw a waveform with pyqt6?

Currently making an application which allows me to make a lightshow with some custom build LED-Controllers and for that i need to draw the waveform of the song on a widget.
Although I managed to do this it is still VERY slow (especially with .wav files longer than a few seconds). The thing is I don't know how to optimise this or if my approach is correct since i cant find anything on the web.
So my question is: what is the right way to go about this? How do audio editors display the waveform and are able to zoom in and out without lag?
So my current attempt at this is by using QGraphicsView and a QGraphicsScene, the latter one supposedly being made to represent a lot of custom graphics items.
The main function to look at here is drawWav() in class WavDisplay
Showcreator.py:
from PyQt6 import uic
from PyQt6.QtCore import (
QSize,
Qt
)
from PyQt6.QtGui import (
QAction,
QPen,
QPixmap,
QPainter,
QColor,
QImage
)
from PyQt6.QtWidgets import (
QMainWindow,
QWidget,
QStatusBar,
QFileDialog,
QGraphicsScene,
QGraphicsView,
QGridLayout
)
import sys
import wave
import pyaudio
import numpy as np
import threading
import soundfile as sf
import threading
class MainWindow(QMainWindow):
# audio chunk rate
CHUNK = 1024
def __init__(self):
super().__init__()
# set window title
self.setWindowTitle("LED Music Show")
# create file button
button_action = QAction("Open .wav file", self)
button_action.setStatusTip("Open a Wave file to the Editor.")
button_action.triggered.connect(self.openWav)
# set status bar
self.setStatusBar(QStatusBar(self))
# create menubar
menu = self.menuBar()
# add file button to status bar
file_menu = menu.addMenu("&File")
file_menu.addAction(button_action)
# create layout
layout = QGridLayout()
layout.setContentsMargins(0,0,0,0)
# create Wave display object
self.waveformspace = WavDisplay()
# add widget to layout
layout.addWidget(self.waveformspace, 0, 1)
self.centralWidget = QWidget()
self.centralWidget.setLayout(layout)
self.setCentralWidget(self.centralWidget)
def openWav(self):
# file selection window
self.filename, check = QFileDialog.getOpenFileName(self,"QFileDialog.getOpenFileName()", "","Wave files (*.wav)")
self.file = None
# try to open .wav with two methods
try:
try:
self.file = wave.open(self.filename, "rb")
except:
print("Failed to open with wave")
try:
self.file, samplerate = sf.read(self.filename, dtype='float32')
except:
print("Failed to open with soundfile")
# read file and convert it to array
self.signal = self.file.readframes(-1)
self.signal = np.fromstring(self.signal, dtype = np.int16)
# set file for drawing
self.waveformspace.setWavefile(self.signal)
self.waveformspace.drawWav()
# return file cursor to start
self.file.rewind()
# start thread for the player
# self.player = threading.Thread(target = self.playWav)
# try:
# self.player.daemon = True
# except:
# print("Failed to set player to Daemon")
# self.player.start()
except:
print("Err opening File")
def playWav(self):
lastFile = None
lastpos = None
p = pyaudio.PyAudio()
data = None
sampwidth = None
fps = None
chn = None
farmes = None
currentpos = 0
framespersec = None
while True:
if self.file != lastFile:
# get file info
sampwidth = self.file.getsampwidth()
fps = self.file.getframerate()
chn = self.file.getnchannels()
frames = self.file.getnframes()
lastFile = self.file
# open audio stream
stream = p.open(format = p.get_format_from_width(sampwidth), channels = chn, rate = fps, output = True)
# read first frame
data = self.file.readframes(self.CHUNK)
framespersec = sampwidth * chn * fps
print("file changed")
if self.pos != lastpos:
# read file for offset
self.file.readframes(int(self.pos * framespersec))
lastpos = self.pos
frames = self.file.getnframes()
print("pos changed")
while data and self.running:
# writing to the stream
stream.write(data)
data = self.file.readframes(self.CHUNK)
currentpos = currentpos + self.CHUNK
# cleanup stuff.
self.file.close()
stream.close()
p.terminate()
return
class WavDisplay(QGraphicsView):
file = None
maxAmplitude = 0
fileset = False
def __init__(self):
super().__init__()
def setWavefile(self, externFile):
self.file = externFile
self.fileset = True
# find the max deviation from 0 db to set draw borders
if max(self.file) > abs(min(self.file)):
self.maxAmplitude = max(self.file) * 2
else:
self.maxAmplitude = abs(min(self.file)) * 2
def drawWav(self):
# only draw when there is a set file
if self.fileset:
width = self.frameGeometry().width()
height = self.frameGeometry().height()
vStep = height / self.maxAmplitude
scene = QGraphicsScene(self)
# to draw on the middle of the widget
h = height / 2
# method 1 of drawing: looks at sections of the file and determines the max and min amplitude that would be visible on a single "column" of pixels and draws a vertical line between them
if width < len(self.file):
hStep = len(self.file) / width
drawArray = np.empty((width, 3))
for i in range(width - 1):
buffer = self.file[int(np.ceil(i * hStep)) : int(np.ceil((i + 1) * hStep))]
drawArray[i][0] = (min(buffer) * vStep) + h
drawArray[i][1] = (max(buffer) * vStep) + h
for i in range(width - 1):
self.line = scene.addLine(i, drawArray[i][0], i, drawArray[i][1])
# method 2 of drawing: this only happens when the amount of samples to draw is less than the windows width (e.g. when zoomed in and you can see the individual samples)
else:
hStep = width / len(self.file)
for i in range(len(self.file) - 1):
self.line = scene.addLine(i * hStep, int(self.file[i] * vStep + h), (i + 1) * hStep, int(self.file[i + 1] * vStep + h))
self.setScene(scene)
self.setContentsMargins(0,0,0,0)
self.show()
def resizeEvent(self, event) -> None:
# has to redraw the wave file if the window gets resized
self.drawWav()
# class not used yet
class effectList(QGraphicsView):
bpm = 130
trackBeats = 0
def __init__(self):
super().__init__()
def setBeatsAndBpm(self, trackLenght, Bpm):
self.bpm = Bpm
self.trackBeats = (trackLenght / 60) * self.bpm
main.py:
from PyQt6 import QtCore, QtGui, QtWidgets
from Showcreator import MainWindow
app = QtWidgets.QApplication([])
window = MainWindow()
window.show()
app.exec()
In essence: Where do i need to start to make this wave file view like one in for example Audacity? (Aka a fast rendering view which doesnt take ages)
Btw i have looked at seemingly duplicates of this question and as you can see in the code i have an algorythem that is only drawing as many lines as the window is wide and not all the 100000+ lines for each sample so the main problem i have should be the rendering method i guess.
Edit: I have all the data preloaded as im loading a wave file and convert it into a numpy array. And i need to display the file as a whole but be able to zoom in dynamically-

GUI plot refreshing incorrectly on wxpython panel

Ok, so I've been tasked with creating a VERY simple GUI at work (I'm an intern). The task is to eventually connect to a machine and process real data, but right now I'm working on randomly generated sine data with noise. I've chose to work in Python 3.0, and use wxpython to create my GUI components.
As I want everything to appear on the same window, I'm using panels (hence wx.lib.plot.PlotCanvas rather than something like matplotlib.pyplot)
The problem that I have is that over time, the plot seems to 'expand' off of the panel. This is temporarily solved when I manually resize the window, but resumes again immediately after (you need to run the code to see what I mean).
Expansion over time in panel
Another problem (that has bugged me since I have started writing the code) is that sometimes when I resize the window (manually) or minimize it and then maximize it again, the timer randomly starts and stops.
I have tried all sorts of things (changing padding in sizers, extra arguments, changing time between refreshes GetBestSize()) but I believe that I simply don't understand wxpython well enough to identify where the problem is
I would really appreciate any help you can shed on either of these problems (I don't know, they might even be linked to each other).
FYI: I am not an experienced coder, and my code is not finished (I have more functions to code, but I feel like I should resolve this first). I have constructed this code by looking at different techniques from various tutorials and websites like stackoverflow, so I know it's not formatted well and could definitely be made more efficient. Also, I have removed some parts just to be safe about confidentiality - nothing important, just strings in messages.
PS: If you do have an easier way to do the whole plot/update thing that doesn't have this problem (preferably still in wx) I would be thrilled to hear that as well
And here's my code:
EDIT: Solved the expanding problem by using self.p2.SetSize((W+0,L+0)) instead of (self.p2.GetBestSize())
EDIT: Made transitions much smoother by just regenerating data and redrawing it on existing canvas in the evt_timer function (instead of recreating the whole canvas, which gave a blink-y appearance if you know what I mean)
import wx
import numpy as np
import matplotlib.pyplot as plt
from scipy.optimize import leastsq
import wx.lib.plot as plot
import time
import os
wildcard = "Text File (*.txt)|*.txt|"\
"Picture (*.png)|*.png|"\
"All files (*.*)|*.*"#This wildcard shows the options for file endings in the "SAVE" tab - see OnSave(self,event)
wildcard2 = "Picture (*.png)|*.png|"\
"Text File (*.txt)|*.txt|"\
"All files (*.*)|*.*"
class PlotCanvas(plot.PlotCanvas):
def __init__(self,parent,id,size,accepted):
"""
This randomly generates sine data (with noise) and plots it to a panel.
Incorporated as a separate class instead of instatiating it as a plot.PlotCanvas object
to overcome an issue of the size of the plot in the panel.
"""
plot.PlotCanvas.__init__(self,parent,id,style=wx.BORDER_SUNKEN,size = size)
N = 100 # number of data points
self.t = np.linspace(0, 4*np.pi, N)
f = 1.15247 # Optional!! Advised not to use
self.data = 3.0*np.sin(f*self.t+0.001) + 0.5 + np.random.randn(N) # create artificial data with noise
guess_mean = np.mean(self.data)
guess_phase = 0
guess_freq = 1
guess_amp = 1
optimize_func = lambda x: x[0]*np.sin(x[1]*self.t+x[2]) + x[3] - self.data
est_amp, est_freq, est_phase, est_mean = leastsq(optimize_func, [guess_amp, guess_freq, guess_phase, guess_mean])[0]
fine_t = np.arange(0,max(self.t),0.1)
data_fit=est_amp*np.sin(est_freq*fine_t+est_phase)+est_mean
multiplier = 1
dataset1 = [(x,[d for d in self.data][[td for td in self.t].index(x)])for x in [td for td in self.t]]
fitdata1 = [(x,[df for df in data_fit][[tf for tf in fine_t].index(x)]) for x in [tf for tf in fine_t]]
dataset =[(x,y*multiplier) for (x,y) in dataset1]
fitdata = [(x,y*multiplier) for (x,y) in fitdata1]
self.data = dataset
self.data2 = fitdata
line = plot.PolyLine(self.data,legend = 'random',colour = 'light blue', width =2)
line2 = plot.PolyLine(self.data2,legend = 'sineline',colour ='black',width =2)
a = []
if "D" in accepted:
a.append(line)
if "S" in accepted:
a.append(line2)
if "G" in accepted:
pass
if "L" in accepted:
pass
gc = plot.PlotGraphics(a,'Line Graph','X','Y')
xmin = self.t[0]-0.01*(self.t[-1]-self.t[0])
xmax = self.t[-1]+0.01*(self.t[-1]-self.t[0])
self.Draw(gc,xAxis=(xmin,xmax),yAxis=(min([x[1] for x in dataset])-0.01*(max([x[1] for x in dataset])-min([x[1] for x in dataset])),
max([x[1] for x in dataset])+0.01*(max([x[1] for x in dataset])-min([x[1] for x in dataset]))))
#self.showLegend = True
#self.enableZoom = True
def Dialog(self, parent, message, c):# Will be used to notify the user of errors/processes
if c == "W":
caption = "Warning!"
dlg = wx.MessageDialog(parent, message, caption, wx.OK | wx.ICON_WARNING)
elif c == "I":
caption = "Information"
dlg = wx.MessageDialog(parent, message, caption, wx.OK | wx.ICON_INFORMATION)
dlg.ShowModal()
dlg.Destroy()#Destroys dialog on close
class Frame(wx.Frame):
"""
This is the main class. In it, we declare the separate panels, canvas, menubar, buttons and sizers.
"""
def __init__(self,parent,id,title):
wx.Frame.__init__(self, parent, id, title, wx.DefaultPosition)
self.CurrentDirectory = os.getcwd()
self.timer=wx.Timer(self)#Instantiating the timer
self.count=0
self.Bind(wx.EVT_TIMER,self.evt_timer)#Binding it to itself so that it is always triggered
self.Bind(wx.EVT_PAINT,self.paint)
menubar = wx.MenuBar()
fileMenu = wx.Menu() #Creating the Menubar at the top
#Creating 3 menus: fileMenu,fit,and help
save = wx.Menu()
z = wx.MenuItem(save,wx.ID_ANY,'Save Raw Data\tCtrl+D')
self.Bind(wx.EVT_MENU,self.OnSave,z)
save.Append(z)
z= wx.MenuItem(save,wx.ID_ANY,'Save Image\tCtrl+I')
self.Bind(wx.EVT_MENU,self.OnSaveImage,z)
save.Append(z)
fileMenu.AppendSubMenu(save,'&Save')
fileMenu.AppendSeparator()
z = wx.MenuItem(fileMenu, wx.ID_EXIT, '&Quit\tCtrl+W')
self.Bind(wx.EVT_MENU, self.OnQuit, z)
fileMenu.Append(z)
fit = wx.Menu()#Making a check menu
self.gaussian = fit.Append(wx.ID_ANY,'Gaussian',kind = wx.ITEM_CHECK)
#self.Bind(wx.EVT_MENU,self.ToggleGaussian,self.gaussian)
fit.Check(self.gaussian.GetId(),False)
self.sine = fit.Append(wx.ID_ANY,'Sine',kind = wx.ITEM_CHECK)
#self.Bind(wx.EVT_MENU,self.ToggleSine,self.sine)
fit.Check(self.sine.GetId(),False)
self.linear = fit.Append(wx.ID_ANY,'Linear',kind=wx.ITEM_CHECK)
#self.Bind(wx.EVT_MENU,self.ToggleLinear,self.linear)
fit.Check(self.linear.GetId(),False)
help = wx.Menu()
z = wx.MenuItem(help,wx.ID_ANY,'&About\tCtrl+H')
self.Bind(wx.EVT_MENU,self.OnHelp,z)
help.Append(z)
menubar.Append(fileMenu, '&File')
menubar.Append(fit, '&Fit')
menubar.Append(help, '&Help')#adding menus to menubar
self.SetMenuBar(menubar)#formatting the frame with menubar
self.sp = wx.SplitterWindow(self)#Splitting the window into 2 panels
self.p1 = wx.Panel(self.sp,style = wx.SUNKEN_BORDER)#For buttons and user events
self.p2 = wx.Panel(self.sp,style = wx.SUNKEN_BORDER)#For display of the plot
self.sp.SplitVertically(self.p1,self.p2,300)
sizer = wx.GridBagSizer(3, 3)#Versatile sizer for layout of first panel self.p1
bitmappath = self.CurrentDirectory + "\\BITMAPS"
bmp = wx.Bitmap(bitmappath+"\\SAVE.BMP",wx.BITMAP_TYPE_BMP)
self.saveBtn = wx.BitmapButton(self.p1,wx.ID_ANY,bitmap = bmp,size =(bmp.GetWidth()+10,bmp.GetHeight()+10))
self.Bind(wx.EVT_BUTTON,self.OnSave,self.saveBtn)
sizer.Add(self.saveBtn, (0, 0), wx.DefaultSpan, wx.ALL,5)
bmp = wx.Bitmap(bitmappath +"\\START.BMP",wx.BITMAP_TYPE_BMP)
self.startBtn = wx.BitmapButton(self.p1,-1,bitmap = bmp,size =(bmp.GetWidth()+10,bmp.GetHeight()+10))# A button that starts and stops the plotting
self.startBtn.startval = "START"
self.Bind(wx.EVT_BUTTON,self.paint,self.startBtn)
sizer.Add(self.startBtn, (0, 1), wx.DefaultSpan,wx.ALL,5)
sizer1 = wx.BoxSizer(wx.VERTICAL)
W,L = self.p2.GetSize()
self.p2.canvas = PlotCanvas(self.p2,wx.ID_ANY,(W,L),["D"])
sizer1.Add(self.p2.canvas,1,wx.ALL,0,0)
self.p2.SetSizerAndFit(sizer1)
self.p1.SetSizerAndFit(sizer)
self.p2.SetSizerAndFit(sizer1)
self.p2.SetSize(W,L)
self.Maximize(True)
self.Centre()
self.Show()
############### event methods ###########
def paint(self,event):
"""
Updates the canvas based on the value of the startbtn(not the image). Bound to self.timer.
"""
bitmappath = self.CurrentDirectory + "\\BITMAPS"
if self.startBtn.startval == "START":
self.timer.Start(1)# increase the value for more time
bmp = wx.Bitmap(bitmappath + "\\STOP.BMP",wx.BITMAP_TYPE_BMP)
self.startBtn.SetBitmap(bmp)
self.startBtn.startval = "STOP"
elif self.startBtn.startval == "STOP":
self.timer.Stop()
bmp = wx.Bitmap(bitmappath+ "\\START.BMP",wx.BITMAP_TYPE_BMP)
self.startBtn.SetBitmap(bmp)
self.startBtn.startval = "START"
def evt_timer(self,event):
self.count +=1
if self.count== 10:# By increasing count (or the number in self.timer.Start()) you can increase the interval between updates
#self.p2.canvas.Clear()
sizer1 = wx.BoxSizer(wx.VERTICAL)
W,L = self.p2.GetSize()
a = ["D"]
if self.sine.IsChecked():
a.append("S")
elif self.linear.IsChecked():
a.append("L")
elif self.gaussian.IsChecked():
a.append("G")
self.p2.canvas = PlotCanvas(self.p2,wx.ID_ANY,(W,L),a)
sizer1.Add(self.p2.canvas,1,wx.ALL,0,0)
self.p2.SetSizerAndFit(sizer1)
self.p2.SetSize(self.p2.GetBestSize())
self.count=0 # reset the count
def Dialog(self, parent, message, c):# Will be used to notify the user of errors/processes
if c == "W":
caption = "Warning!"
dlg = wx.MessageDialog(parent, message, caption, wx.OK | wx.ICON_WARNING)
elif c == "I":
caption = "Information"
dlg = wx.MessageDialog(parent, message, caption, wx.OK | wx.ICON_INFORMATION)
dlg.ShowModal()
dlg.Destroy()#Destroys dialog on close
def OnSave(self,event):#Triggered by menubar and button
try:
rawdata = self.p2.canvas.data
raw_X =[x[0] for x in rawdata]
raw_Y =[x[1] for x in rawdata]
dlg = wx.FileDialog(#Code for this from http://www.blog.pythonlibrary.org
self, message="Save file as ...",
defaultDir=self.CurrentDirectory,
defaultFile=str(time.ctime()), wildcard=wildcard, style=wx.FD_SAVE
)
if dlg.ShowModal() == wx.ID_OK:
path = dlg.GetPath()
dlg.Destroy()
f = open(path+".txt","w+")
for i in range(len(raw_X)):
f.write(str(raw_X[i])+"\t"+str(raw_Y[i])+"\n")
f.close()
self.Dialog(None,"File successfully saved","I")
except UnboundLocalError:#Catch error when user closes save window without selecting any directory or filename
pass
def OnSaveImage(self,event):
try:
rawdata = self.p2.canvas.data
raw_X = [x[0] for x in rawdata]
raw_Y = [x[1] for x in rawdata]
dlg = wx.FileDialog(
self, message="Save file as ...",
defaultDir=self.CurrentDirectory,
defaultFile=str(time.ctime()), wildcard=wildcard2, style=wx.FD_SAVE
)
if dlg.ShowModal() == wx.ID_OK:
path = dlg.GetPath()
dlg.Destroy()
fig1 = plt.figure()
plt.plot(raw_X,raw_Y)
plt.title("Raw Data")
fig1.savefig(path+".png")
self.Dialog(None,"File successfully saved","I")
except UnboundLocalError:
pass
def OnMultiply(self,e):
try:
factor = self.x.GetValue()
factor = float(factor)
self.IntegrationTime = factor
except ValueError as e:
self.Dialog(None,str(e),"W")
def OnQuit(self, e):
self.Close()
def OnHelp(self,e):
self.Dialog(None,"N/A","I")
def ToggleSine(self,e):
pass
def ToggleLinear(self,e):
self.Dialog(None,"Not added yet","W")
def ToggleGaussian(self,e):
self.Dialog(None,"Not added yet","W")
if __name__ =="__main__":
app=wx.App()
Frame(None,-1,"N/A")
app.MainLoop()

pyqt4 pixel information on rotated image

I have adapted an image viewer (see code below) to allow me to get pixel information from a loaded image. You load an image using the 'Load image' button, then you can zoom in and out using the scroll wheel, and pan using mouse left click and drag. When you press the button 'Enter pixel info mode', the dragging is disabled (you can still zoom) and clicking on the image will give the pixel coordinate (integer pixel indices) and grayscale value of the pixel.
The problem is that if you rotate the image, by pressing the 'Rotate image' button, using the pixel info button no longer gives the correct pixel info. I imagine that the mapToScene method is not the right thing to use on a rotated image but can find no other way to do it. I have tried various things, such as using toImage() on the rotated pixmap and then replacing the original image with this, but nothing seems to work. What would be the best way to resolve this?
The code:
from PyQt4 import QtCore, QtGui
class PhotoViewer(QtGui.QGraphicsView):
photoClicked = QtCore.pyqtSignal(QtCore.QPoint)
def __init__(self, parent):
super(PhotoViewer, self).__init__(parent)
self._zoom = 0
self._empty = True
self._scene = QtGui.QGraphicsScene(self)
self._photo = QtGui.QGraphicsPixmapItem()
self._scene.addItem(self._photo)
self.setScene(self._scene)
self.setTransformationAnchor(QtGui.QGraphicsView.AnchorUnderMouse)
self.setResizeAnchor(QtGui.QGraphicsView.AnchorUnderMouse)
self.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff)
self.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff)
self.setBackgroundBrush(QtGui.QBrush(QtGui.QColor(30, 30, 30)))
self.setFrameShape(QtGui.QFrame.NoFrame)
def fitInView(self):
rect = QtCore.QRectF(self._photo.pixmap().rect())
if not rect.isNull():
unity = self.transform().mapRect(QtCore.QRectF(0, 0, 1, 1))
self.scale(1 / unity.width(), 1 / unity.height())
viewrect = self.viewport().rect()
scenerect = self.transform().mapRect(rect)
factor = min(viewrect.width() / scenerect.width(),
viewrect.height() / scenerect.height())
self.scale(factor, factor)
self.centerOn(rect.center())
self._zoom = 0
def hasPhoto(self):
return not self._empty
def toggleDragMode(self):
if self.dragMode() == QtGui.QGraphicsView.ScrollHandDrag:
self.setDragMode(QtGui.QGraphicsView.NoDrag)
elif self.hasPhoto():
self.setDragMode(QtGui.QGraphicsView.ScrollHandDrag)
def setPhoto(self, pixmap=None):
self._zoom = 0
if pixmap and not pixmap.isNull():
self._empty = False
self.setDragMode(QtGui.QGraphicsView.ScrollHandDrag)
self._photo.setPixmap(pixmap)
self.fitInView()
else:
self._empty = True
self.setDragMode(QtGui.QGraphicsView.NoDrag)
self._photo.setPixmap(QtGui.QPixmap())
def wheelEvent(self, event):
if not self._photo.pixmap().isNull():
if event.delta() > 0:
factor = 1.25
self._zoom += 1
else:
factor = 0.8
self._zoom -= 1
if self._zoom > 0:
self.scale(factor, factor)
elif self._zoom == 0:
self.fitInView()
else:
self._zoom = 0
def mousePressEvent(self, event):
if (self.hasPhoto() and
self.dragMode() == QtGui.QGraphicsView.NoDrag and
self._photo.isUnderMouse()):
self.photoClicked.emit(QtCore.QPoint(event.pos()))
super(PhotoViewer, self).mousePressEvent(event)
class Window(QtGui.QWidget):
def __init__(self):
super(Window, self).__init__()
self.viewer = PhotoViewer(self)
# 'Load image' button
self.btnLoad = QtGui.QToolButton(self)
self.btnLoad.setText('Load image')
self.btnLoad.clicked.connect(self.loadImage)
# Button to change from drag/pan to getting pixel info
self.btnPixInfo = QtGui.QToolButton(self)
self.btnPixInfo.setText('Enter pixel info mode')
self.btnPixInfo.clicked.connect(self.pixInfo)
self.editPixInfo = QtGui.QLineEdit(self)
self.editPixInfo.setReadOnly(True)
# Button to rotate image by 10 degrees
self.btnRotate = QtGui.QToolButton(self)
self.btnRotate.setText('Rotate image')
self.btnRotate.clicked.connect(self.rotateImage)
self.viewer.photoClicked.connect(self.photoClicked)
# Arrange layout
VBlayout = QtGui.QVBoxLayout(self)
VBlayout.addWidget(self.viewer)
HBlayout = QtGui.QHBoxLayout()
HBlayout.setAlignment(QtCore.Qt.AlignLeft)
HBlayout.addWidget(self.btnLoad)
HBlayout.addWidget(self.btnRotate)
HBlayout.addWidget(self.btnPixInfo)
HBlayout.addWidget(self.editPixInfo)
VBlayout.addLayout(HBlayout)
def loadImage(self):
self.viewer.setPhoto(QtGui.QPixmap('pic.jpg'))
def pixInfo(self):
self.viewer.toggleDragMode()
def rotateImage(self):
self.viewer._photo.setRotation(10)
def photoClicked(self, pos):
pos = self.viewer.mapToScene(pos)
# p.s. I realise the following lines are probably a very convoluted way of getting
# a grayscale value from RGB, but I couldn't make it work any other way I tried
rot_image = self.viewer._photo.pixmap().toImage().pixel(pos.x(), pos.y())
colour = QtGui.QColor.fromRgb(rot_image)
gsval = QtGui.qGray(colour.red(), colour.green(), colour.blue())
self.editPixInfo.setText('X:%d, Y:%d Grayscale: %d' % (pos.x(), pos.y(), gsval))
if __name__ == '__main__':
import sys
app = QtGui.QApplication(sys.argv)
window = Window()
window.setGeometry(500, 300, 800, 600)
window.show()
sys.exit(app.exec_())
You need to map the scene coordinates to item coordinates:
pos = self.viewer._photo.mapFromScene(self.viewer.mapToScene(pos))

wxpython, how to set background color and foreground color in textctrl?

I want to set color for words, background and foreground colors both are needed. I learned tkinter first, but it seems wxpython have no similar methods.
the following code is easy to test, copy "hello world, Hello World, heLLo WORLD" to area_example, tell me how to highlight "hello", ignorecase
#!/usr/bin/env python
# coding=utf8
import wx
rows = 5
cols = 2
vgap = 20
hgap = 10
class Search(wx.Frame):
#not_resizable = wx.DEFAULT_FRAME_STYLE ^ (wx.RESIZE_BORDER | wx.MAXIMIZE_BOX) # ok
not_resizable = wx.DEFAULT_FRAME_STYLE & ~(wx.RESIZE_BORDER | wx.MAXIMIZE_BOX)
def __init__(self, parent, title, size):
super(Search, self).__init__(parent, title=title, size=size, style=self.not_resizable)
self.init_elements()
self.lay_out()
self.Centre()
self.Show()
def init_elements(self):
self.panel = wx.Panel(self)
self.entry_name = wx.TextCtrl(self.panel)
self.entry_name.SetFocus()
self.btn_add = wx.Button(self.panel, label="Add")
self.btn_add.Disable()
self.btn_recite = wx.Button(self.panel, label="Recite")
self.btn_recite.Disable()
self.btn_flash = wx.Button(self.panel, label="Flash")
self.btn_flash.Disable()
self.label_phonetic = wx.StaticText(self.panel, label='')
self.area_meaning = wx.TextCtrl(self.panel, style=wx.TE_MULTILINE)
self.area_example = wx.TextCtrl(self.panel, style=wx.TE_MULTILINE)
self.btn_save = wx.Button(self.panel, label="Save")
self.btn_sort = wx.Button(self.panel, label="Sort")
def lay_out(self):
hbox = wx.BoxSizer(wx.HORIZONTAL)
grid = wx.FlexGridSizer(rows, cols, vgap, hgap)
grid.AddMany([
(self.entry_name), (self.btn_add),
(self.label_phonetic), (self.btn_recite),
(self.area_meaning, 1, wx.EXPAND), (self.btn_flash),
(self.area_example, 1, wx.EXPAND), (self.btn_sort),
(self.btn_save),
])
grid.AddGrowableCol(0, 1)
grid.AddGrowableRow(2, 1)
grid.AddGrowableRow(3, 1)
hbox.Add(grid, proportion=1, flag=wx.ALL | wx.EXPAND, border=15)
self.panel.SetSizer(hbox)
#self.panel.SetSizerAndFit(hbox)
def OnKeyUp(self, e):
code = e.GetKeyCode()
if code == wx.WXK_RETURN:
self.enter_handler(e)
def enter_handler(self, e):
word = self.entry_name.GetValue()
if word:
self.highlight(word)
def highlight(self, name):
# todo
# add background color and foreground color, ignore case
print 'highlight'
def search_test():
app = wx.App()
title = 'Search Test'
size = (800, 500)
s = Search(None, title, size)
s.entry_name.Bind(wx.EVT_KEY_UP, s.OnKeyUp)
app.MainLoop()
if __name__ == '__main__':
search_test()
the doc version when I asked the question was wxPython 3.0.3, last updated 13 March 2015 from revision 1725+2c3b7a8.
but the wxPython version brew install on osx was 3.0.2, some classes and methods were not available.
On the docs it explains how to do this: http://wxpython.org/Phoenix/docs/html/TextCtrl.html#phoenix-title-textctrl-styles
Here is an example snippet (Should go below your definition of self.area_example)
self.area_example.SetDefaultStyle(wx.TextAttr(wx.RED))
self.area_example.AppendText("Red text\n")
self.area_example.SetDefaultStyle(wx.TextAttr(wx.NullColour,
wx.LIGHT_GREY))
self.area_example.AppendText("Red on grey text\n")
self.area_example.SetDefaultStyle(wx.TextAttr(wx.BLUE))
self.area_example.AppendText("Blue on grey text\n")
As for checking if the word is "hello", I can only think right now as to bind it and check it.
self.area_example.Bind(wx.EVT_CHAR, self.OnKeyDown)
The "OnKeyDown" function is just an example. It runs but you'll most likely want a better way of doing it.
def OnKeyDown(self, e):
last_word = self.area_example.GetValue().split()[-1]
if last_word.lower() == "hello":
print("Change color")
e.Skip()
From there you should be able to accomplish what you need.

Python 2.7 Tkinter Change Label text on button event

Im very new to Python(2.7) im learning GUI design(Tkinter) and keep running into different syntax/no call method/global name not defined errors when trying to implement a simple label text change from a Entry object on button click. Can someone show me the correct syntax for the action
from Tkinter import *
class Part3:
def __init__(self, parent):
GUIFrame =Frame(parent,width= 300, height=200)
GUIFrame.pack(expand = False, anchor = CENTER)
entry = Entry(text="enter your choice")
entry.place(x=65, y = 10)
self.test = StringVar()
self.test.set('''Hi, I'm a Label :)''')
self.Label1 = Label(parent, textvariable = self.test)
self.Label1.place(x = 85, y = 100)
self.Button2 = Button(parent, text='edit',command=self.LabelChange)
self.Button2.place(x= 80, y = 60)
self.Button3 = Button(parent, text='exit', command= parent.quit)
self.Button3.place(x= 160, y = 60)
def LabelChange(self):
test = self.entry.get()
self.Label1(test)
root = Tk()
MainFrame =Part3(root)
root.title('Input Test')
root.mainloop()
The Idea being on the 'edit' (button2) click, the text of Label1 is changed to the text of entry.
Try:
self.entry = Entry(text="enter your choice")
...
test = self.entry.get()
self.test.set(test)
I know most tutorials give examples using textvariables, but in most cases you don't need them. You can get and set the values in the widget without using textvariable. Textvariables are mostly useful for doing traces on variables. Variable traces are a somewhat advanced technique that you will rarely need.
from Tkinter import *
class Part3:
def __init__(self, parent):
GUIFrame =Frame(parent,width= 300, height=200)
GUIFrame.pack(expand = False, anchor = CENTER)
self.entry = Entry(text="enter your choice") # this needs to be in self
self.entry.place(x=65, y = 10)
self.test = StringVar()
self.test.set('''Hi, I'm a Label :)''')
self.Label1 = Label(parent, textvariable = self.test)
self.Label1.place(x = 85, y = 100)
self.Button2 = Button(parent, text='edit',command=self.LabelChange)
self.Button2.place(x= 80, y = 60)
self.Button3 = Button(parent, text='exit', command= parent.quit)
self.Button3.place(x= 160, y = 60)
def LabelChange(self):
self.test.set(self.entry.get())
root = Tk()
MainFrame =Part3(root)
root.title('Input Test')
root.mainloop()
root.destroy()
Use can use a .after command. For example:
Lbl = Label(text='Hi')
def change():
Lbl.after(3000, lambda: Lbl.config(text="hola")
# Or you can use the one below to remove delay.
Lbl.config(text='hola')
return change
Btn = Button(command=change ())
Lbl.pack()
Btn.pack()

Resources