I have a sensor that needs to be calibrated. The error depends on the orientation of the sensor and can be estimated and shown to the user. I would like to do this visually using tkinter for python 3.x.
The ideal result would be something like this with the black bar live updating depending on the live error:
How could I do this best in tkinter? I looked at the Scale and Progressbar widgets but they did not have the needed functionality.
I was thinking about showing the colorbar as an image and overlaying the black indicator bar and constantly updating the position of this black bar. Would this be possible?
I shall split up the answer in two parts. The first part solves the issue of live updating the data, by using two threads as suggested by #Martineau. The communication between the threads is done by a simple lock and a global variable.
The second part creates the calibration bar widget using the gradient calculation algorithm defined by #Martineau.
PART 1:
This example code shows a small window with one number. The number is generated in one thread and the GUI is shown by another thread.
import threading
import time
import copy
import tkinter as tk
import random
class ThreadCreateData(threading.Thread):
def __init__(self, name):
threading.Thread.__init__(self)
self.name = name
def run(self):
#Declaring data global allows to access it between threads
global data
# create data for the first time
data_original = self.create_data()
while True: # Go in the permanent loop
print('Data creator tries to get lock')
lock.acquire()
print('Data creator has it!')
data = copy.deepcopy(data_original)
print('Data creator is releasing it')
lock.release()
print('Data creator is creating data...')
data_original = self.create_data()
def create_data(self):
'''A function that returns a string representation of a number changing between one and ten.'''
a = random.randrange(1, 10)
time.sleep(1) #Simulating calculation time
return str(a)
class ThreadShowData(threading.Thread):
def __init__(self, name):
threading.Thread.__init__(self)
self.name = name
def run(self):
# Declaring data global allows to access it between threads
global data
root = tk.Tk()
root.geometry("200x150")
# creation of an instance
app = Window(root, lock)
# mainloop
root.mainloop()
# Here, we are creating our class, Window, and inheriting from the Frame
# class. Frame is a class from the tkinter module. (see Lib/tkinter/__init__)
class Window(tk.Frame):
# Define settings upon initialization. Here you can specify
def __init__(self, master=None,lock=None):
# parameters that you want to send through the Frame class.
tk.Frame.__init__(self, master)
# reference to the master widget, which is the tk window
self.master = master
#Execute function update_gui after 1ms
self.master.after(1, self.update_gui(lock))
def update_gui(self, lock):
global data
print('updating')
print('GUI trying to get lock')
lock.acquire()
print('GUI got the lock')
new_data = copy.deepcopy(data)
print('GUI releasing lock')
lock.release()
data_label = tk.Label(self.master, text=new_data)
data_label.grid(row=1, column=0)
print('GUI wating to update')
self.master.after(2000, lambda: self.update_gui(lock)) #run update_gui every 2 seconds
if __name__ == '__main__':
# creating the lock
lock = threading.Lock()
#Initializing data
data = None
#creating threads
a = ThreadCreateData("Data_creating_thread")
b = ThreadShowData("Data_showing_thread")
#starting threads
b.start()
a.start()
PART 2: Below the code for a simple calibration bar widget is shown. The bar only contains 5 ticks you can adapt the code to add more if wanted. Pay attention to the needed input formats. To test the widget a random value is generated and shown on the widget every 0.5s.
import tkinter as tk
from PIL import ImageTk, Image
import sys
EPSILON = sys.float_info.epsilon # Smallest possible difference.
###Functions to create the color bar (credits to Martineau)
def convert_to_rgb(minval, maxval, val, colors):
for index, color in enumerate(colors):
if color == 'YELLOW':
colors[index] = (255, 255, 0)
elif color == 'RED':
colors[index] = (255, 0, 0)
elif color == 'GREEN':
colors[index] = (0, 255, 0)
# "colors" is a series of RGB colors delineating a series of
# adjacent linear color gradients between each pair.
# Determine where the given value falls proportionality within
# the range from minval->maxval and scale that fractional value
# by the total number in the "colors" pallette.
i_f = float(val - minval) / float(maxval - minval) * (len(colors) - 1)
# Determine the lower index of the pair of color indices this
# value corresponds and its fractional distance between the lower
# and the upper colors.
i, f = int(i_f // 1), i_f % 1 # Split into whole & fractional parts.
# Does it fall exactly on one of the color points?
if f < EPSILON:
return colors[i]
else: # Otherwise return a color within the range between them.
(r1, g1, b1), (r2, g2, b2) = colors[i], colors[i + 1]
return int(r1 + f * (r2 - r1)), int(g1 + f * (g2 - g1)), int(b1 + f * (b2 - b1))
def create_gradient_img(size, colors):
''''Creates a gradient image based on size (1x2 tuple) and colors (1x3 tuple with strings as entries,
possible entries are GREEN RED and YELLOW)'''
img = Image.new('RGB', (size[0],size[1]), "black") # Create a new image
pixels = img.load() # Create the pixel map
for i in range(img.size[0]): # For every pixel:
for j in range(img.size[1]):
pixels[i,j] = convert_to_rgb(minval=0,maxval=size[0],val=i,colors=colors) # Set the colour accordingly
return img
### The widget
class CalibrationBar(tk.Frame):
""""The calibration bar widget. Takes as arguments the parent, the start value of the calibration bar, the
limits in the form of a 1x5 list these will form the ticks on the bar and the boolean two sided. In case it
is two sided the gradient will be double."""
def __init__(self, parent, limits, name, value=0, two_sided=False):
tk.Frame.__init__(self, parent)
#Assign attributes
self.value = value
self.limits = limits
self.two_sided = two_sided
self.name=name
#Test that the limits are 5 digits
assert len(limits)== 5 , 'There are 5 ticks so you should give me 5 values!'
#Create a canvas in which we are going to put the drawings
self.canvas_width = 400
self.canvas_height = 100
self.canvas = tk.Canvas(self,
width=self.canvas_width,
height=self.canvas_height)
#Create the color bar
self.bar_offset = int(0.05 * self.canvas_width)
self.bar_width = int(self.canvas_width*0.9)
self.bar_height = int(self.canvas_height*0.8)
if two_sided:
self.color_bar = ImageTk.PhotoImage(create_gradient_img([self.bar_width,self.bar_height],['RED','GREEN','RED']))
else:
self.color_bar = ImageTk.PhotoImage(create_gradient_img([self.bar_width,self.bar_height], ['GREEN', 'YELLOW', 'RED']))
#Put the colorbar on the canvas
self.canvas.create_image(self.bar_offset, 0, image=self.color_bar, anchor = tk.NW)
#Indicator line
self.indicator_line = self.create_indicator_line()
#Tick lines & values
for i in range(0,5):
print(str(limits[i]))
if i==4:
print('was dees')
self.canvas.create_line(self.bar_offset + int(self.bar_width - 2), int(self.canvas_height * 0.7),
self.bar_offset + int(self.bar_width - 2), int(self.canvas_height * 0.9), fill="#000000", width=3)
self.canvas.create_text(self.bar_offset + int(self.bar_width - 2), int(self.canvas_height * 0.9), text=str(limits[i]), anchor=tk.N)
else:
self.canvas.create_line(self.bar_offset + int(i * self.bar_width / 4), int(self.canvas_height * 0.7), self.bar_offset + int(i * self.bar_width / 4), int(self.canvas_height * 0.9), fill="#000000", width=3)
self.canvas.create_text(self.bar_offset + int(i * self.bar_width / 4), int(self.canvas_height * 0.9), text=str(limits[i]), anchor=tk.N)
#Text
self.label = tk.Label(text=self.name+': '+str(self.value),font=14)
#Positioning
self.canvas.grid(row=0,column=0,sticky=tk.N)
self.label.grid(row=1,column=0,sticky=tk.N)
def create_indicator_line(self):
""""Creates the indicator line"""
diff = self.value-self.limits[0]
ratio = diff/(self.limits[-1]-self.limits[0])
if diff<0:
ratio=0
elif ratio>1:
ratio=1
xpos = int(self.bar_offset+ratio*self.bar_width)
return self.canvas.create_line(xpos, 0, xpos, 0.9 * self.canvas_height, fill="#000000", width=3)
def update_value(self,value):
self.value = value
self.label.config(text = self.name+': '+str(self.value))
self.canvas.delete(self.indicator_line)
self.indicator_line = self.create_indicator_line()
###Creation of window to place the widget
class App(tk.Tk):
def __init__(self):
tk.Tk.__init__(self)
self.geometry('400x400')
self.calibration_bar = CalibrationBar(self, value= -5, limits=[-10, -5, 0, 5, 10], name='Inclination angle', two_sided=True)
self.calibration_bar.grid(column=0, row=4)
self.after(500,self.update_data)
def update_data(self):
""""Randomly assing values to the widget and update the widget."""
import random
a = random.randrange(-15, 15)
self.calibration_bar.update_value(a)
self.after(500, self.update_data)
###Calling our window
if __name__ == "__main__":
app=App()
app.mainloop()
This is how it looks like:
To get a live updating calibration bar you should just combine part one and two in your application.
Related
I want to have some transition to show the images in the canvas, I'm using Tkinter and I'm looping through the images to show them on the canvas but I need to have some transition while switching among the images.
I'm using canvasName.create_image method for showing the images. Need a way to show them smoothly.
Here is my code:
def Multi_view_rotate():
window.geometry(str(scr_w)+"x"+str(scr_h)+"+0+0")
z_out = 20
global timeSleep
timeSleepVal = int(timeSleep.get())
global footerPath
footerPath = footerPath.get()
#geting director from entry boxes
global portDirEntry
portDirEntry = portDirEntry.get()
global colorEntry
bgcolor = colorEntry.get()
allPaths = getPaths(portDirEntry)
#directory = r"C:\Users\DotNet\Desktop\Ragazinana Data reduced\diashow\4 Random\Landschaft"
#Get paths
pathsPrt = allPaths[0]
pathsLand = allPaths[1]
#read the image
#call the function to get the picture object with new size
global numOfImagesPort
global numOfImagesLand
#footer path
#footerPath = "C:/Users/DotNet/Desktop/Ragazinana Data reduced/diashow/ragaziana_s.jpg"
#Footer will take 8% of the screen width
per_w_footer = cal_per_num(8, scr_w)
# Footer Image operations
canvasFoot = Canvas(window,width=per_w_footer, height=scr_h, bg=bgcolor, highlightthickness=1, highlightbackground=bgcolor)
canvasFoot.grid(row=0, column=0)
#footerImg = get_img_fit_size(footerPath, scr_h, per_w_footer, True)
footerImg1 = Image.open(footerPath)
footerImg2 = footerImg1.transpose(Image.ROTATE_270)
footerImg3 = footerImg2.resize((int(per_w_footer),int(scr_h)), Image.ANTIALIAS)
footerImg = ImageTk.PhotoImage(footerImg3)
footer = canvasFoot.create_image(per_w_footer/2,scr_h/2,anchor=CENTER, image=footerImg)
while(numOfImagesPort<=len(pathsPrt)-1 or numOfImagesLand<=len(pathsLand)-1 ):
pathPort = pathsPrt[numOfImagesPort]
#increase the index to get the next file in the next loop
numOfImagesPort=numOfImagesPort+1
#if the next photo is out of bound then assign it to the first index
if(numOfImagesPort >= len(pathsPrt)):# if total is 5 pic, 1st loop 0 > 6 /reset the loop
numOfImagesPort=0
# each image will take as following in percentage
per_w_imgs_portriate = cal_per_num(42, scr_w)
per_w_imgs_landscape= cal_per_num(50, scr_w)
#Create the canvases
canvasPort = Canvas(window,width=per_w_imgs_portriate, height=scr_h, bg=bgcolor, highlightthickness=10, highlightbackground=bgcolor)
#gird plays the canvas without it the canvas will not work
canvasPort.grid(row=0, column=1)
#in order to make the picture fit in the rotated state in the half of the screen
# we make the get_img_fit_size adjust it to us to that size by providing
# screen hight as a width and half of the screen with as a height
imgPort = get_img_fit_size(pathPort, scr_h, per_w_imgs_landscape, True)
portImgCanvas = canvasPort.create_image(int(scr_w/4.3),int(scr_h/2),anchor=CENTER, image=imgPort)**
window.update()
time.sleep(timeSleepVal/2)
# Landscape image
pathLand = pathsLand[numOfImagesLand]
numOfImagesLand = numOfImagesLand+1
if(numOfImagesLand >= len(pathsLand)):
numOfImagesLand=0
canvasLand = Canvas(window,width=per_w_imgs_landscape, height=scr_h, bg=bgcolor, highlightthickness=10, highlightbackground=bgcolor)
canvasLand.grid(row=0, column=2)
imgLand = get_img_fit_size(pathLand, scr_h, per_w_imgs_portriate, True)
landImgCanvas = canvasLand.create_image(int(scr_w/4.5),int(scr_h/2),anchor=CENTER, image=imgLand)
window.update()
time.sleep(timeSleepVal/2)
window.mainloop()
I don't think there is something like this built into Tkinter.PhotoImage, but you could manually create a "fade" transition by randomly selecting pixels and setting them to the color values of the next image:
import tkinter, random
root = tkinter.Tk()
c = tkinter.Canvas(root, width=800, height=400)
c.pack()
img_a = tkinter.PhotoImage(file="a.gif")
img_b = tkinter.PhotoImage(file="b.gif")
i = c.create_image(0, 0, image=img_a, anchor="nw")
pixels = [(x, y) for x in range(img_a.width()) for y in range(img_a.height())]
random.shuffle(pixels)
def fade(n=1000):
global pixels, i
for _ in range(min(n, len(pixels))):
x, y = pixels.pop()
col = "#%02x%02x%02x" % img_b.get(x,y)
img_a.put(col, (x, y))
c.delete(i)
i = c.create_image(0, 0, image=img_a, anchor="nw")
if pixels:
c.after(1, fade)
fade()
root.mainloop()
This is slow, though. The after with 1 ms is only to keep the UI from freezing (don't use while with time.sleep in Tkinter!). For a smoother transition, instead of replacing pixel values you might gradually shift all pixels towards the values in the next image, but that will be even slower since you'd change all pixels in each step.
Instead of pure tkinter, we can try it wit PIL and numpy, but it is not noticeably faster, and least not the way I did it:
import numpy as np
from PIL import Image, ImageTk
from itertools import islice
...
arr_a = np.array(Image.open("a.gif").convert("RGB"))
arr_b = np.array(Image.open("b.gif").convert("RGB"))
img = ImageTk.PhotoImage(Image.fromarray(arr_a, mode="RGB"))
i = c.create_image(0, 0, image=img, anchor="nw")
h, w, _ = arr_a.shape
pixels = [(x, y) for x in range(w) for y in range(h)]
random.shuffle(pixels)
def fade(k=0, n=1000):
global i, img
X, Y = zip(*islice(pixels, k, k+n))
arr_a[Y,X] = arr_b[Y,X]
c.delete(i)
img = ImageTk.PhotoImage(Image.fromarray(arr_a, mode="RGB"))
i = c.create_image(0, 0, image=img, anchor="nw")
if k + n < w * h:
c.after(1, fade, k+n, n)
fade()
root.mainloop()
However, this also allows us to replace entire lines at once. The effect is not quite as nice, but it is much faster (also note changed n and if condition).
...
h, w, _ = arr_a.shape
lines = list(range(h))
random.shuffle(lines)
def fade(k=0, n=10):
global i, img
Y = lines[k:k+n]
arr_a[Y] = arr_b[Y]
...
if k + n < h:
c.after(1, fade, k+n, n)
...
This can also easily be transformed to a vertical or horizontal slide transition by simply not shuffling the lines (for columns, use arr_a[:,X] = arr_b[:,X]).
Using PySide2 or PyQt5, I want to make a table widget with header labels that are on a 45 degree angle, like in the image here.
I don't see anything like this in QtCreator (Designer) for the QTable widget. I can rotate a label using something like this:
class MyLabel(QtGui.QWidget):
def paintEvent(self, event):
painter = QtGui.QPainter(self)
painter.setPen(QtCore.Qt.black)
painter.translate(20, 100)
painter.rotate(-45)
painter.drawText(0, 0, "hellos")
painter.end()
But, there are several niggles. Ideally this would be a QLineEdit widget, I would need the widgets to 'play nice' so as not to overlap anything else, and I would like them to fill in above the table from the header. I'm looking for suggestions.
This is a very interesting topic, as Qt doesn't provide such a feature, but it can be implemented.
The following example is far from perfect, I'll list its main pros/cons.
Pros
it works ;-)
changing horizontal header labels automatically updates the header height
supports horizontal scrolling "over" the last item position (if the table view is smaller than its contents, the horizontal scrollbar allows to see the full header text)
it works :-D
Cons
sections are fixed
sections are not movable
QAbstractItemView.ScrollPerPixel is mandatory for the horizontal scroll mode in this implementation. Qt's ScrollPerItem mode is a bit complex, and has some issues if it's not overrided with huge care. This doesn't mean that it's not possible to use that mode, but it requires a lot of efforts, possibly by carefully reading and understanding the source code of both QTableView and QAbstractItemView. Long story short: ScrollPerItem works until you reach the maximum value of the horizontal scrollbar; at that point, the view will try to resize and adapt its viewport and scrollbar value/range, and the last header labels will be "cut out".
if all horizontal columns are visible (meaning that the items wouldn't require horizontal scrolling), the last horizontal headers are not completely shown, since the horizontal scroll bar is not required.
I think that it should be possible to support all header features (custom/stretchable section size, movable sections, item scroll, etc.), but it would require a very deep reimplementation process of both QTableView and QHeaderView methods.
Anyhow, that's the result I've got so far, which supports scrolling, painting, and basic mouse interaction (section highlight on click).
Example screenshot:
Scrolled (near the right edge) screenshot:
Table sized slightly after the right edge of the last horizontal column:
Example code
import sys
from math import sqrt, sin, acos, hypot, degrees, radians
from PyQt5 import QtCore, QtGui, QtWidgets
class AngledHeader(QtWidgets.QHeaderView):
borderPen = QtGui.QColor(0, 190, 255)
labelBrush = QtGui.QColor(255, 212, 0)
def __init__(self, parent=None):
QtWidgets.QHeaderView.__init__(self, QtCore.Qt.Horizontal, parent)
self.setSectionResizeMode(self.Fixed)
self.setDefaultSectionSize(sqrt((self.fontMetrics().height() + 4)** 2 *2))
self.setSectionsClickable(True)
self.setDefaultSectionSize(int(sqrt((self.fontMetrics().height() + 4)** 2 *2)))
self.setMaximumHeight(100)
# compute the ellipsis size according to the angle; remember that:
# 1. if the angle is not 45 degrees, you'll need to compute this value
# using trigonometric functions according to the angle;
# 2. we assume ellipsis is done with three period characters, so we can
# "half" its size as (usually) they're painted on the bottom line and
# they are large enough, allowing us to show as much as text is possible
self.fontEllipsisSize = int(hypot(*[self.fontMetrics().height()] * 2) * .5)
self.setSectionsClickable(True)
def sizeHint(self):
# compute the minimum height using the maximum header label "hypotenuse"'s
hint = QtWidgets.QHeaderView.sizeHint(self)
count = self.count()
if not count:
return hint
fm = self.fontMetrics()
width = minSize = self.defaultSectionSize()
# set the minimum width to ("hypotenuse" * sectionCount) + minimumHeight
# at least, ensuring minimal horizontal scroll bar interaction
hint.setWidth(width * count + self.minimumHeight())
maxDiag = maxWidth = maxHeight = 1
for s in range(count):
if self.isSectionHidden(s):
continue
# compute the diagonal of the text's bounding rect,
# shift its angle by 45° to get the minimum required
# height
rect = fm.boundingRect(
str(self.model().headerData(s, QtCore.Qt.Horizontal)) + ' ')
# avoid math domain errors for empty header labels
diag = max(1, hypot(rect.width(), rect.height()))
if diag > maxDiag:
maxDiag = diag
maxWidth = max(1, rect.width())
maxHeight = max(1, rect.height())
# get the angle of the largest boundingRect using the "Law of cosines":
# https://en.wikipedia.org/wiki/Law_of_cosines
angle = degrees(acos(
(maxDiag ** 2 + maxWidth ** 2 - maxHeight ** 2) /
(2. * maxDiag * maxWidth)
))
# compute the minimum required height using the angle found above
minSize = max(minSize, sin(radians(angle + 45)) * maxDiag)
hint.setHeight(min(self.maximumHeight(), minSize))
return hint
def mousePressEvent(self, event):
width = self.defaultSectionSize()
start = self.sectionViewportPosition(0)
rect = QtCore.QRect(0, 0, width, -self.height())
transform = QtGui.QTransform().translate(0, self.height()).shear(-1, 0)
for s in range(self.count()):
if self.isSectionHidden(s):
continue
if transform.mapToPolygon(
rect.translated(s * width + start, 0)).containsPoint(
event.pos(), QtCore.Qt.WindingFill):
self.sectionPressed.emit(s)
return
def paintEvent(self, event):
qp = QtGui.QPainter(self.viewport())
qp.setRenderHints(qp.Antialiasing)
width = self.defaultSectionSize()
delta = self.height()
# add offset if the view is horizontally scrolled
qp.translate(self.sectionViewportPosition(0) - .5, -.5)
fmDelta = (self.fontMetrics().height() - self.fontMetrics().descent()) * .5
# create a reference rectangle (note that the negative height)
rect = QtCore.QRectF(0, 0, width, -delta)
diagonal = hypot(delta, delta)
for s in range(self.count()):
if self.isSectionHidden(s):
continue
qp.save()
qp.save()
qp.setPen(self.borderPen)
# apply a "shear" transform making the rectangle a parallelogram;
# since the transformation is applied top to bottom
# we translate vertically to the bottom of the view
# and draw the "negative height" rectangle
qp.setTransform(qp.transform().translate(s * width, delta).shear(-1, 0))
qp.drawRect(rect)
qp.setPen(QtCore.Qt.NoPen)
qp.setBrush(self.labelBrush)
qp.drawRect(rect.adjusted(2, -2, -2, 2))
qp.restore()
qp.translate(s * width + width, delta)
qp.rotate(-45)
label = str(self.model().headerData(s, QtCore.Qt.Horizontal))
elidedLabel = self.fontMetrics().elidedText(
label, QtCore.Qt.ElideRight, diagonal - self.fontEllipsisSize)
qp.drawText(0, -fmDelta, elidedLabel)
qp.restore()
class AngledTable(QtWidgets.QTableView):
def __init__(self, *args, **kwargs):
QtWidgets.QTableView.__init__(self, *args, **kwargs)
self.setHorizontalHeader(AngledHeader(self))
self.verticalScrollBarSpacer = QtWidgets.QWidget()
self.addScrollBarWidget(self.verticalScrollBarSpacer, QtCore.Qt.AlignTop)
self.fixLock = False
def setModel(self, model):
if self.model():
self.model().headerDataChanged.disconnect(self.fixViewport)
QtWidgets.QTableView.setModel(self, model)
model.headerDataChanged.connect(self.fixViewport)
def fixViewport(self):
if self.fixLock:
return
self.fixLock = True
# delay the viewport/scrollbar states since the view has to process its
# new header data first
QtCore.QTimer.singleShot(0, self.delayedFixViewport)
def delayedFixViewport(self):
# add a right margin through the horizontal scrollbar range
QtWidgets.QApplication.processEvents()
header = self.horizontalHeader()
if not header.isVisible():
self.verticalScrollBarSpacer.setFixedHeight(0)
self.updateGeometries()
return
self.verticalScrollBarSpacer.setFixedHeight(header.sizeHint().height())
bar = self.horizontalScrollBar()
bar.blockSignals(True)
step = bar.singleStep() * (header.height() / header.defaultSectionSize())
bar.setMaximum(bar.maximum() + step)
bar.blockSignals(False)
self.fixLock = False
def resizeEvent(self, event):
# ensure that the viewport and scrollbars are updated whenever
# the table size change
QtWidgets.QTableView.resizeEvent(self, event)
self.fixViewport()
class TestWidget(QtWidgets.QWidget):
def __init__(self):
QtWidgets.QWidget.__init__(self)
l = QtWidgets.QGridLayout()
self.setLayout(l)
self.table = AngledTable()
l.addWidget(self.table)
model = QtGui.QStandardItemModel(4, 5)
self.table.setModel(model)
self.table.setHorizontalScrollMode(self.table.ScrollPerPixel)
model.setVerticalHeaderLabels(['Location {}'.format(l + 1) for l in range(8)])
columns = ['Column {}'.format(c + 1) for c in range(8)]
columns[3] += ' very, very, very, very, very, very, long'
model.setHorizontalHeaderLabels(columns)
if __name__ == '__main__':
app = QtWidgets.QApplication(sys.argv)
w = TestWidget()
w.show()
sys.exit(app.exec_())
Please note that I edited the painting and click detection code using QTransforms instead QPolygons: while it's a bit more complex to understand its mechanics, it's faster than creating a polygon and computing its points each time a column header has to be drawn.
Also, I've added support for maximum header height (in case any header label get too long), and a "spacer" widget that shifts the vertical scrollbar to the actual "beginning" of the table contents.
musicamante posted such an excellent answer that I've used it as the basis to add a few more (stolen) bits. In this code, when a user double clicks an angled header they are greeted with a popup where they can rename the header. Because of the wonderful code that music provided, it redraws everything automatically.
import sys
from math import sqrt, sin, acos, hypot, degrees, radians
from PySide2 import QtCore, QtGui, QtWidgets
class AngledHeader(QtWidgets.QHeaderView):
borderPen = QtGui.QColor(0, 190, 255)
labelBrush = QtGui.QColor(255, 212, 0)
def __init__(self, parent=None):
QtWidgets.QHeaderView.__init__(self, QtCore.Qt.Horizontal, parent)
self.setSectionResizeMode(self.Fixed)
self.setDefaultSectionSize(sqrt((self.fontMetrics().height() + 4)** 2 *2))
self.setSectionsClickable(True)
def sizeHint(self):
# compute the minimum height using the maximum header
# label "hypotenuse"'s
fm = self.fontMetrics()
width = minSize = self.defaultSectionSize()
count = self.count()
for s in range(count):
if self.isSectionHidden(s):
continue
# compute the diagonal of the text's bounding rect,
# shift its angle by 45° to get the minimum required
# height
rect = fm.boundingRect(str(self.model().headerData(s, QtCore.Qt.Horizontal)) + ' ')
diag = hypot(rect.width(), rect.height())
# get the angle of the boundingRect using the
# "Law of cosines":
# https://en.wikipedia.org/wiki/Law_of_cosines
angle = degrees(acos((diag ** 2 + rect.width() ** 2 - rect.height() ** 2) / (2. * diag * rect.width())))
# compute the minimum required height using the
# angle found above
minSize = max(minSize, sin(radians(angle + 45)) * diag)
hint = QtCore.QSize(width * count + 2000, minSize)
return hint
def mousePressEvent(self, event):
width = self.defaultSectionSize()
first = self.sectionViewportPosition(0)
rect = QtCore.QRect(0, 0, width, -self.height())
transform = QtGui.QTransform().translate(0, self.height()).shear(-1, 0)
for s in range(self.count()):
if self.isSectionHidden(s):
continue
if transform.mapToPolygon(rect.translated(s * width + first,
0)).containsPoint(event.pos(), QtCore.Qt.WindingFill):
self.sectionPressed.emit(s)
self.last = ("Click", s) #log initial click and define the column index
return
def mouseReleaseEvent(self, event):
if self.last[0] == "Double Click":#if this was a double click then we have work to do
index = self.last[1]
oldHeader = str(self.model().headerData(index, QtCore.Qt.Horizontal))
newHeader, ok = QtWidgets.QInputDialog.getText(self,
'Change header label for column %d' % index,
'Header:',
QtWidgets.QLineEdit.Normal,
oldHeader)
if ok:
self.model().horizontalHeaderItem(index).setText(newHeader)
self.update()
def mouseDoubleClickEvent(self, event):
self.last = ("Double Click", self.last[1])
#log that it's a double click and pass on the index
def paintEvent(self, event):
qp = QtGui.QPainter(self.viewport())
qp.setRenderHints(qp.Antialiasing)
width = self.defaultSectionSize()
delta = self.height()
# add offset if the view is horizontally scrolled
qp.translate(self.sectionViewportPosition(0) - .5, -.5)
fmDelta = (self.fontMetrics().height() - self.fontMetrics().descent()) * .5
# create a reference rectangle (note that the negative height)
rect = QtCore.QRectF(0, 0, width, -delta)
for s in range(self.count()):
if self.isSectionHidden(s):
continue
qp.save()
qp.save()
qp.setPen(self.borderPen)
# apply a "shear" transform making the rectangle a parallelogram;
# since the transformation is applied top to bottom
# we translate vertically to the bottom of the view
# and draw the "negative height" rectangle
qp.setTransform(qp.transform().translate(s * width, delta).shear(-1, 0))
qp.drawRect(rect)
qp.setPen(QtCore.Qt.NoPen)
qp.setBrush(self.labelBrush)
qp.drawRect(rect.adjusted(2, -2, -2, 2))
qp.restore()
qp.translate(s * width + width, delta)
qp.rotate(-45)
qp.drawText(0, -fmDelta, str(self.model().headerData(s, QtCore.Qt.Horizontal)))
qp.restore()
class AngledTable(QtWidgets.QTableView):
def __init__(self, *args, **kwargs):
QtWidgets.QTableView.__init__(self, *args, **kwargs)
self.setHorizontalHeader(AngledHeader(self))
self.fixLock = False
def setModel(self, model):
if self.model():
self.model().headerDataChanged.disconnect(self.fixViewport)
QtWidgets.QTableView.setModel(self, model)
model.headerDataChanged.connect(self.fixViewport)
def fixViewport(self):
if self.fixLock:
return
self.fixLock = True
# delay the viewport/scrollbar states since the view has to process its
# new header data first
QtCore.QTimer.singleShot(0, self.delayedFixViewport)
def delayedFixViewport(self):
# add a right margin through the horizontal scrollbar range
QtWidgets.QApplication.processEvents()
header = self.horizontalHeader()
bar = self.horizontalScrollBar()
bar.blockSignals(True)
step = bar.singleStep() * (header.height() / header.defaultSectionSize())
bar.setMaximum(bar.maximum() + step)
bar.blockSignals(False)
self.fixLock = False
def resizeEvent(self, event):
# ensure that the viewport and scrollbars are updated whenever
# the table size change
QtWidgets.QTableView.resizeEvent(self, event)
self.fixViewport()
class TestWidget(QtWidgets.QWidget):
def __init__(self):
QtWidgets.QWidget.__init__(self)
l = QtWidgets.QGridLayout()
self.setLayout(l)
self.table = AngledTable()
l.addWidget(self.table)
model = QtGui.QStandardItemModel(4, 5)
self.table.setModel(model)
self.table.setHorizontalScrollMode(self.table.ScrollPerPixel)
self.table.headerlist = ['Column{}'.format(c + 1) for c in range(8)]
model.setVerticalHeaderLabels(['Location 1', 'Location 2', 'Location 3', 'Location 4'])
model.setHorizontalHeaderLabels(self.table.headerlist)
if __name__ == '__main__':
app = QtWidgets.QApplication(sys.argv)
w = TestWidget()
w.show()
sys.exit(app.exec_())
I wrote some code using Tkinter in Python 3 that plots a graph in a canvas. I also made it such that when I move the mouse over the canvas the graph scrolls to the left.
The problem is that I want the graph to scroll when I press the space bar for example. But I don't want it to scroll 1 step each time I press the space bar but I want it to start scrolling indefinitely when I press it once and stop the scroll when I press it again. I want the space bar to be a play/pause key.
How can I accomplish this? I don't want to use matplotlib anywhere.
MY CODE AS IT IS NOW:
from tkinter import *
import numpy as np
# The function of the graph
def f(x):
return np.sin(x)+np.sin(3*x-1)+np.sin(0.5*(x+np.pi))+0.3*np.sin(10*x)
class GraphPlot():
def __init__(self, master):
self.master = master
# Data for the graph and steps to move to the right
self.data_x = np.linspace(0, 4*np.pi, 1000)
self.data_y = f(self.data_x)
self.step = 0.1
# A switch to delete to clear the canvas each iteration before plotting the next frame
self.gate = False
# Setting the Tkinter window and the canvas in place
self.ws = master.winfo_screenwidth()
self.hs = master.winfo_screenheight()
ww = self.ws*0.75
hw = self.hs*0.50
self.canvas = Canvas(self.master, width = ww, height = hw, bg = 'black')
self.canvas.grid()
self.master.update()
self.w = self.canvas.winfo_width()
self.h = self.canvas.winfo_height()
self.canvas.focus_set()
# Plot first frame
self.drawData(self.data_x, self.data_y)
# Plot next frames each time I press the space bar
self.canvas.bind('<KeyPress-space>', self.updateData)
def drawData(self, data_x, data_y):
'''This is my function to plot a grpah in a canvas
canvas without embedding any matplotlib figure'''
# Setting the axis limits
x_min, x_max = min(data_x), max(data_x)
y_min, y_max = min(data_y), max(data_y)
# Translating data to pixel positions inside the canvas
pixel_x = (data_x-x_min)*self.w/(x_max-x_min)
pixel_y = -(data_y-y_max)*self.h/(y_max-y_min)
points = []
for i in range(len(data_x)):
points.append(pixel_x[i])
points.append(pixel_y[i])
points = tuple(points)
# Deleting previous frame before plotting the next frame (except for the first frame)
if self.gate:
self.canvas.delete('curve')
else:
self.gate = True
# Plotting
self.canvas.create_line(points, fill = 'white', tag = 'curve')
def updateData(self, event):
# Changing data for the next frame
self.data_x += self.step
self.data_y = f(self.data_x)
# Plot new frame
self.drawData(self.data_x, self.data_y)
root = Tk()
GraphPlot(root)
root.mainloop()
I've tried some ideas. For example I used a new function, PlayPause(), with a while loop and a new switch, self.go, but this didn't work as expected.
CODE THAT I EXPECTED TO WORK BUT DIDN'T:
from tkinter import *
import numpy as np
def f(x):
return np.sin(x)+np.sin(3*x-1)+np.sin(0.5*(x+np.pi))+0.3*np.sin(10*x)
class GraphPlot():
def __init__(self, master):
self.master = master
self.data_x = np.linspace(0, 4*np.pi, 1000)
self.data_y = f(self.data_x)
self.step = 0.1
self.go = False # The new switch
self.gate = False
self.ws = master.winfo_screenwidth()
self.hs = master.winfo_screenheight()
ww = self.ws*0.75
hw = self.hs*0.50
self.canvas = Canvas(self.master, width = ww, height = hw, bg = 'black')
self.canvas.grid()
self.master.update()
self.w = self.canvas.winfo_width()
self.h = self.canvas.winfo_height()
self.canvas.focus_set()
self.drawData(self.data_x, self.data_y)
self.canvas.bind('<KeyPress-space>', self.PlayPause)
def drawData(self, data_x, data_y):
x_min, x_max = min(data_x), max(data_x)
y_min, y_max = min(data_y), max(data_y)
pixel_x = (data_x-x_min)*self.w/(x_max-x_min)
pixel_y = -(data_y-y_max)*self.h/(y_max-y_min)
points = []
for i in range(len(data_x)):
points.append(pixel_x[i])
points.append(pixel_y[i])
points = tuple(points)
if self.gate:
self.canvas.delete('curve')
else:
self.gate = True
self.canvas.create_line(points, fill = 'white', tag = 'curve')
def updateData(self):
self.data_x += self.step
self.data_y = f(self.data_x)
self.drawData(self.data_x, self.data_y)
def PlayPause(self, event):
if self.go:
self.go = False
else:
self.go = True
while self.go:
self.updateData()
root = Tk()
GraphPlot(root)
root.mainloop()
You could add a method to toggle_play_pause, and bind the space key to it. Upon space key press, this method toggles a boolean flag pause that when turned off allows the update to be called.
update will keep calling itself every 10/1000 of a second, until the space key is pressed again, and the pause flag set to True.
import tkinter as tk
import numpy as np
def f(x):
return np.sin(x)+np.sin(3*x-1)+np.sin(0.5*(x+np.pi))+0.3*np.sin(10*x)
class GraphPlot():
def __init__(self, master):
self.master = master
# Data for the graph and steps to move to the right
self.data_x = np.linspace(0, 4*np.pi, 1000)
self.data_y = f(self.data_x)
self.step = 0.1
# A switch to delete to clear the canvas each iteration before plotting the next frame
self.gate = False
# Setting the Tkinter window and the canvas in place
self.ws = master.winfo_screenwidth()
self.hs = master.winfo_screenheight()
ww = self.ws * 0.75
hw = self.hs * 0.50
self.canvas = tk.Canvas(self.master, width=ww, height=hw, bg='black')
self.canvas.grid()
self.master.update()
self.w = self.canvas.winfo_width()
self.h = self.canvas.winfo_height()
self.canvas.focus_set()
# Plot first frame
self.drawData(self.data_x, self.data_y)
# Plot next frames each time I press the space bar
self.canvas.bind('<KeyPress-space>', self.toggle_play_pause)
self.pause = True
self._update_call_handle = None
def drawData(self, data_x, data_y):
'''This is my function to plot a grpah in a canvas
canvas without embedding any matplotlib figure'''
# Setting the axis limits
x_min, x_max = min(data_x), max(data_x)
y_min, y_max = min(data_y), max(data_y)
# Translating data to pixel positions inside the canvas
pixel_x = (data_x-x_min)*self.w/(x_max-x_min)
pixel_y = -(data_y-y_max)*self.h/(y_max-y_min)
points = []
for i in range(len(data_x)):
points.append(pixel_x[i])
points.append(pixel_y[i])
points = tuple(points)
# Deleting previous frame before plotting the next frame (except for the first frame)
if self.gate:
self.canvas.delete('curve')
else:
self.gate = True
# Plotting
self.canvas.create_line(points, fill = 'white', tag = 'curve')
def toggle_play_pause(self, dummy_event):
self.pause = not self.pause
if not self.pause:
self.updateData()
def updateData(self):
# Changing data for the next frame
self.data_x += self.step
self.data_y = f(self.data_x)
# Plot new frame
self.drawData(self.data_x, self.data_y)
if not self.pause:
self._update_call_handle = root.after(10, self.updateData)
else:
root.after_cancel(self._update_call_handle)
self._update_call_handle = None
root = tk.Tk()
GraphPlot(root)
root.mainloop()
I use tinter.after() for refreshing the display of an analog clock on a raspberry pi every 200ms. At the beginning it's OK but gradually the time between each refresh reaches about 2-3 seconds. Is there any solution to keep the refresh interval to 200ms?
#!/usr/bin/env python
# coding: UTF-8
# license: GPL
#
## #package _08c_clock
#
# A very simple analog clock.
#
# The program transforms worldcoordinates into screencoordinates
# and vice versa according to an algorithm found in:
# "Programming principles in computer graphics" by Leendert Ammeraal.
#
# Based on the code of Anton Vredegoor (anton.vredegoor#gmail.com)
#
# #author Paulo Roma
# #since 01/05/2014
# #see https://code.activestate.com/recipes/578875-analog-clock
# #see http://orion.lcg.ufrj.br/python/figuras/fluminense.png
import sys, types, os
from time import localtime
from datetime import timedelta,datetime
from math import sin, cos, pi
from threading import Thread
try:
from tkinter import * # python 3
except ImportError:
try:
from mtTkinter import * # for thread safe
except ImportError:
from Tkinter import * # python 2
hasPIL = True
# we need PIL for resizing the background image
# in Fedora do: yum install python-pillow-tk
# or yum install python3-pillow-tk
try:
from PIL import Image, ImageTk
except ImportError:
hasPIL = False
## Class for handling the mapping from window coordinates
# to viewport coordinates.
#
class mapper:
## Constructor.
#
# #param world window rectangle.
# #param viewport screen rectangle.
#
def __init__(self, world, viewport):
self.world = world
self.viewport = viewport
x_min, y_min, x_max, y_max = self.world
X_min, Y_min, X_max, Y_max = self.viewport
f_x = float(X_max-X_min) / float(x_max-x_min)
f_y = float(Y_max-Y_min) / float(y_max-y_min)
self.f = min(f_x,f_y)
x_c = 0.5 * (x_min + x_max)
y_c = 0.5 * (y_min + y_max)
X_c = 0.5 * (X_min + X_max)
Y_c = 0.5 * (Y_min + Y_max)
self.c_1 = X_c - self.f * x_c
self.c_2 = Y_c - self.f * y_c
## Maps a single point from world coordinates to viewport (screen) coordinates.
#
# #param x, y given point.
# #return a new point in screen coordinates.
#
def __windowToViewport(self, x, y):
X = self.f * x + self.c_1
Y = self.f * -y + self.c_2 # Y axis is upside down
return X , Y
## Maps two points from world coordinates to viewport (screen) coordinates.
#
# #param x1, y1 first point.
# #param x2, y2 second point.
# #return two new points in screen coordinates.
#
def windowToViewport(self,x1,y1,x2,y2):
return self.__windowToViewport(x1,y1),self.__windowToViewport(x2,y2)
## Class for creating a new thread.
#
class makeThread (Thread):
"""Creates a thread."""
## Constructor.
# #param func function to run on this thread.
#
def __init__ (self,func):
Thread.__init__(self)
self.__action = func
self.debug = False
## Destructor.
#
def __del__ (self):
if ( self.debug ): print ("Thread end")
## Starts this thread.
#
def run (self):
if ( self.debug ): print ("Thread begin")
self.__action()
## Class for drawing a simple analog clock.
# The backgroung image may be changed by pressing key 'i'.
# The image path is hardcoded. It should be available in directory 'images'.
#
class clock:
## Constructor.
#
# #param deltahours time zone.
# #param sImage whether to use a background image.
# #param w canvas width.
# #param h canvas height.
# #param useThread whether to use a separate thread for running the clock.
#
def __init__(self,root,deltahours = 0,sImage = True,w = 400,h = 400,useThread = False):
self.world = [-1,-1,1,1]
self.imgPath = './images/fluminense.png' # image path
if hasPIL and os.path.exists (self.imgPath):
self.showImage = sImage
else:
self.showImage = False
self.setColors()
self.circlesize = 0.09
self._ALL = 'handles'
self.root = root
width, height = w, h
self.pad = width/16
if self.showImage:
self.fluImg = Image.open(self.imgPath)
self.root.bind("<Escape>", lambda _ : root.destroy())
self.delta = timedelta(hours = deltahours)
self.canvas = Canvas(root, width = width, height = height, background = self.bgcolor)
viewport = (self.pad,self.pad,width-self.pad,height-self.pad)
self.T = mapper(self.world,viewport)
self.root.title('Clock')
self.canvas.bind("<Configure>",self.resize)
self.root.bind("<KeyPress-i>", self.toggleImage)
self.canvas.pack(fill=BOTH, expand=YES)
if useThread:
st=makeThread(self.poll)
st.debug = True
st.start()
else:
self.poll()
## Called when the window changes, by means of a user input.
#
def resize(self,event):
sc = self.canvas
sc.delete(ALL) # erase the whole canvas
width = sc.winfo_width()
height = sc.winfo_height()
imgSize = min(width, height)
self.pad = imgSize/16
viewport = (self.pad,self.pad,width-self.pad,height-self.pad)
self.T = mapper(self.world,viewport)
if self.showImage:
flu = self.fluImg.resize((int(0.8*0.8*imgSize), int(0.8*imgSize)), Image.ANTIALIAS)
self.flu = ImageTk.PhotoImage(flu)
sc.create_image(width/2,height/2,image=self.flu)
else:
self.canvas.create_rectangle([[0,0],[width,height]], fill = self.bgcolor)
self.redraw() # redraw the clock
## Sets the clock colors.
#
def setColors(self):
if self.showImage:
self.bgcolor = 'antique white'
self.timecolor = 'dark orange'
self.circlecolor = 'dark green'
else:
self.bgcolor = '#000000'
self.timecolor = '#ffffff'
self.circlecolor = '#808080'
## Toggles the displaying of a background image.
#
def toggleImage(self,event):
if hasPIL and os.path.exists (self.imgPath):
self.showImage = not self.showImage
self.setColors()
self.resize(event)
## Redraws the whole clock.
#
def redraw(self):
start = pi/2 # 12h is at pi/2
step = pi/6
for i in range(12): # draw the minute ticks as circles
angle = start-i*step
x, y = cos(angle),sin(angle)
self.paintcircle(x,y)
self.painthms() # draw the handles
if not self.showImage:
self.paintcircle(0,0) # draw a circle at the centre of the clock
## Draws the handles.
#
def painthms(self):
self.canvas.delete(self._ALL) # delete the handles
T = datetime.timetuple(datetime.utcnow()-self.delta)
x,x,x,h,m,s,x,x,x = T
self.root.title('%02i:%02i:%02i' %(h,m,s))
angle = pi/2 - pi/6 * (h + m/60.0)
x, y = cos(angle)*0.70,sin(angle)*0.70
scl = self.canvas.create_line
# draw the hour handle
scl(self.T.windowToViewport(0,0,x,y), fill = self.timecolor, tag=self._ALL, width = self.pad/3)
angle = pi/2 - pi/30 * (m + s/60.0)
x, y = cos(angle)*0.90,sin(angle)*0.90
# draw the minute handle
scl(self.T.windowToViewport(0,0,x,y), fill = self.timecolor, tag=self._ALL, width = self.pad/5)
angle = pi/2 - pi/30 * s
x, y = cos(angle)*0.95,sin(angle)*0.95
# draw the second handle
scl(self.T.windowToViewport(0,0,x,y), fill = self.timecolor, tag=self._ALL, arrow = 'last')
## Draws a circle at a given point.
#
# #param x,y given point.
#
def paintcircle(self,x,y):
ss = self.circlesize / 2.0
sco = self.canvas.create_oval
sco(self.T.windowToViewport(-ss+x,-ss+y,ss+x,ss+y), fill = self.circlecolor)
## Animates the clock, by redrawing everything after a certain time interval.
#
def poll(self):
self.redraw()
self.root.after(200,self.poll)
## Main program for testing.
#
# #param argv time zone, image background flag,
# clock width, clock height, create thread flag.
#
def main(argv=None):
if argv is None:
argv = sys.argv
if len(argv) > 2:
try:
deltahours = int(argv[1])
sImage = (argv[2] == 'True')
w = int(argv[3])
h = int(argv[4])
t = (argv[5] == 'True')
except ValueError:
print ("A timezone is expected.")
return 1
else:
deltahours = 3
sImage = True
w = h = 400
t = False
root = Tk()
root.geometry ('+0+0')
# deltahours: how far are you from utc?
# Sometimes the clock may be run from another timezone ...
clock(root,deltahours,sImage,w,h,t)
root.mainloop()
if __name__=='__main__':
sys.exit(main())
No, there is not. Tkinter only guarantees that the event will be triggered sometime after the delay. Usually the difference is only a millisecond or two, but it can be longer.
That being said, if you trigger the new event before doing any work, you'll likely witness less drift than if you do it after doing work.
For example, consider this code:
def callback(self):
<do some work>
self.after(200, self.callback)
This will drift by whatever amount of time it takes to "do some work". If that work takes 100ms, than the next call won't happen until 300ms after the current call.
If you want as little drift as possible you can schedule the new callback immediately, before any other code:
def callback(self):
self.after(200, self.callback)
<do some work>
If you want to do something every 200 ms, choose the second method. If you want to wait for 200ms after the first callback has finished, use the first method.
You can get closer timing with something like this:
def callback(self):
time_start = time.time()
# DO YOUR WORK HERE
time_used = (time.time() - time_start) * 1000
sleep = 200 - time_used
if sleep < 1:
sleep = 1
self.after(sleep, self.callback)
I've been using this successfully to ensure puslseaudio sets volume every 1/10th second while python calculates the next volume and factors in time for subprocess Linux shell command launched in background to complete.
Trying to animate a sequence of PIL images using tkinter. The graph of my frame durations (ms) looks like this:
Anyone have any idea what could be causing this spiky sawtooth pattern?
Here's a script to reproduce:
from PIL import Image, ImageTk
import Tkinter
import time
import sys
def generate_frames(n):
"""
keep n under 101 * 101
"""
out = []
last_pil = None
for i in range(n):
if last_pil:
pil_image = last_pil.copy()
else:
pil_image = Image.new('L', (101, 101), 255)
x = i / 101
y = i % 101
pil_image.load()[x, y] = 0
out.append(ImageTk.PhotoImage(pil_image))
last_pil = pil_image
return out
def draw():
FRAME_COUNT =5000
master = Tkinter.Tk()
w = Tkinter.Canvas(master, width=302, height=302)
w.create_rectangle(49, 49, 252, 252)
w.pack()
frames = generate_frames(FRAME_COUNT)
def draw_frame(f, canvas_image):
print repr(time.time())
frame = frames[f]
if canvas_image is None:
canvas_image = w.create_image((151, 151), image=frame, anchor='center')
else:
w.itemconfigure(canvas_image, image=frame)
w.current_frame = frame # save a reference
next_frame = f + 1
if next_frame < FRAME_COUNT:
master.after(1, draw_frame, next_frame, canvas_image)
else:
sys.exit(0)
master.after(10, draw_frame, 0, None)
master.mainloop()
draw()
To see the plot, pipe output through
import sys
last = None
for line in sys.stdin:
value = float(line.strip()) * 1000
if last is None:
pass
else:
print (value - last)
last = value
then through
from matplotlib import pyplot
import sys
X = []
Y = []
for index, line in enumerate(sys.stdin):
line = line.strip()
X.append(index)
Y.append(float(line))
pyplot.plot(X, Y, '-')
pyplot.show()
Making it multi-threaded doesn't help:
class AnimationThread(threading.Thread):
FRAME_COUNT = 5000
def __init__(self, canvas):
threading.Thread.__init__(self)
self.canvas = canvas
self.frames = generate_frames(self.FRAME_COUNT)
def run(self):
w = self.canvas
frames = self.frames
canvas_image = None
for i in range(self.FRAME_COUNT):
print repr(time.time())
frame = frames[i]
if canvas_image is None:
canvas_image = w.create_image((151, 151), image=frame, anchor='center')
else:
w.itemconfigure(canvas_image, image=frame)
w.current_frame = frame
time.sleep(1 * .001)
def draw_threaded():
FRAME_COUNT = 5000
master = Tkinter.Tk()
w = Tkinter.Canvas(master, width=302, height=302)
w.create_rectangle(49, 49, 252, 252)
w.pack()
animation_thread = AnimationThread(w)
animation_thread.start()
master.mainloop()
animation_thread.join()
draw_threaded()
That closely resembles this kind of interference pattern when competing 60 and 50 Hz samples mingle:
(Original Wolfram|Alpha plot)
This is likely caused by having two things at different (but close) refresh rates. It's the same type of thing that happens when you try to film a TV screen and it looks like a black bar keeps moving down the image, or when car wheels appear to rotate backwards around their axles in car commercials. It is essentially an extension of the Moiré Effect.
I don't know whether or not it is caused by video drivers and/or hardware, but it is almost certainly caused by interfering cyclical patterns. It looks a lot like it should be the GC cycle interfering with your for loop (hence the sudden drop in the sawtooth-like wave as memory is freed up and can be allocated)