Smooth Transition in Tkinter - python

Is tkinter able to make a smooth text transition(slowly appear into the Window)? In Windows 10, Python 3 ? I have tried searching through the web but no similar questions, I have tried seeing if the widget has an option to do that, but no luck!

Is tkinter able to make a smooth text transition
If you are talking about a tkinter.Label, then you may be able to fake it by interpolating between two colors (the start color being the background color of the label, the end color being the desired foreground color of the label). Here's an example I came up with, where a label fades in from the background color (to fake transparency) into the desired foreground color (red in this case):
import tkinter as tk
def interpolate(color_a, color_b, t):
# 'color_a' and 'color_b' are RGB tuples
# 't' is a value between 0.0 and 1.0
# this is a naive interpolation
return tuple(int(a + (b - a) * t) for a, b in zip(color_a, color_b))
class Application(tk.Tk):
def __init__(self, *args, **kwargs):
tk.Tk.__init__(self, *args, **kwargs)
self.title("Font Color Test")
self.geometry("256x64")
self.resizable(width=False, height=False)
self.label = tk.Label(self, text="Hello World", pady=32)
self.label.pack()
# On my system (Windows 7, classic theme) this is "SystemButtonFace"
label_background_system_color = self.label.cget("background")
label_background_16_bit_color = self.label.winfo_rgb(label_background_system_color)
# Again, on my system, this is RGB(212, 208, 200)
label_background_8_bit_color = tuple(value >> 8 for value in label_background_16_bit_color)
# This isn't really required. Making a custom label foreground color just to show it doesn't have to be black.
label_foreground_8_bit_color = tuple((255, 0, 0))
# I want the the label to "fade in" from the background color to completely red
self.start_color = label_background_8_bit_color
self.end_color = label_foreground_8_bit_color
# Let's say I want a smooth fade in transition at a rate of 60 fps and a duration of 1 second
self.duration_ms = 1000
self.frames_per_second = 60
self.ms_sleep_duration = 1000 // self.frames_per_second
self.current_step = 0
self.update_label()
def update_label(self):
t = (1.0 / self.frames_per_second) * self.current_step
self.current_step += 1
new_color = interpolate(self.start_color, self.end_color, t)
self.label.configure(foreground="#%02x%02x%02x" % new_color)
if self.current_step <= self.frames_per_second:
self.after(self.ms_sleep_duration, self.update_label)
def main():
application = Application()
application.mainloop()
return 0
if __name__ == "__main__":
import sys
sys.exit(main())

You might be able to fake it using images. Use a timeout function to replace them one after the other. Not sure if that would be fast enough to appear smooth.
But for things like this I think other toolkits would be better suited. For example pysdl2.

Related

Tkinter widgets created in an Update function run by tk.after function do not create untill the aforementioned Update ends

I intend to make a Py code which creates a tkinter dot that turns on a key press and deletes on a key press of couple keys.
The dot already is functional but i need it switch on and off on certain keypresses/mouse clicks which means i need an outside tkinter.mainloop() Update function.
The Update function with a while in it to constantly check if conditions to turn it off/on are present. But the Tkinter widget Somehow gets applied to the screen Only when the function nds. Like widget could be created but it will only take effect when function ends. And i need to turn it off/on dynamically.
I have tried to use a tkinter.after() with additional one at the end of called function only to find out an error of Recursion depth. What i expected to happen was that the function would be called over and over again, instead it runs that function like a while loop. I also have tried Asyncio.run() but it would result not making it visible till the function ends at least once. And I need to change it dynamically.
from tkinter import *
from tkinter import Canvas
from winsound import Beep
from time import sleep
import asyncio
import keyboard
import mouse
root = Tk()
width = root.winfo_screenwidth()
height = root.winfo_screenheight()
class tk_Dot():
def __init__(self,x=-1,y=-1,radius=4,color="red"):
self.x = x
if x == -1:
self.x = width/2-radius//2
print(self.x)
self.y = y
if y == -1:
self.y = height/2+radius//2
print(self.y)
self.radius=radius
self.color = color
self.lines = []
self.count = 1
def line(self,i):
return canvas.create_line(self.x, self.y-i, self.x+self.radius, self.y-i, fill=self.color)
def create(self):
self.lines = []
for i in range(0,self.radius):
self.lines.append(self.line(i))
def delete(self):
for i in range(0,self.radius):
canvas.delete(self.lines[i])
canvas.dtag(self.lines[i])
opacity_of_tk_window = 1 # From 0 to 1 0 meaning completely transparent 1 meaning everything created in canvas will give the color it was given
root.attributes('-alpha',opacity_of_tk_window)
# Invisible Tkinter window label
root.overrideredirect(True)
# Makes Transparent background
transparent_color = '#f0f0f0'
root.wm_attributes('-transparent', transparent_color)
canvas = Canvas()
# Rectangle filled with color that is specified above as the transparent color so practically making transparent background
canvas.create_rectangle(0, 0, width, height, fill=transparent_color)
canvas.pack(fill=BOTH, expand=1)
radius = 2
radius = 1+radius\*2
# Create a dot class
game_dot = tk_Dot(width/2-radius//2+1,height/2+1+radius//2,radius,"Red")
# Create a Dot at the middle of the calorant crosshair
# game_dot.create()
# Delete the dot
# game_dot.delete()
def Update():
game_dot.create()
print("Dot should be visible by now")
print("Is it?")
sleep(5) #sec
print("Oh yeah after the function ends.") # the problem
def Delete():
game_dot.delete()
root.geometry('%dx%d+%d+%d' % (width, height, -2,-2))
# Tkinter window always on top
root.attributes('-topmost',True)
root.after(1000,Update())
root.mainloop()

Qt Custom Animated Button (Ellipse effect)

I am trying to make a custom animated button on PyQt. I found a website which has custom buttons: Buttons website
I already created a topic for making a 3rd button: Stackoverflow for 3rd button
#musicamante helped for the 3rd button, thank you very much again. Now I'm trying to make the 19th button.
My code for 19th button:
import sys, os, time
from math import *
from PySide6 import QtCore, QtWidgets, QtGui
from PySide6.QtWidgets import *
from PySide6.QtCore import *
from PySide6.QtGui import *
class Button19(QPushButton):
Radius = 10
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.backgroundColors = (QtGui.QColor(QtCore.Qt.lightGray),QtGui.QColor(QtCore.Qt.white))
self.foregroundColors = (QtGui.QColor(QtCore.Qt.black), QtGui.QColor(QtCore.Qt.lightGray))
font = self.font()
font.setBold(True)
self.setFont(font)
self.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
self.hoverAnimation = QtCore.QVariantAnimation(self)
self.hoverAnimation.setStartValue(0.)
self.hoverAnimation.setEndValue(1.)
self.hoverAnimation.setEasingCurve(QtCore.QEasingCurve.OutCubic)
self.hoverAnimation.setDuration(400)
self.hoverAnimation.valueChanged.connect(self.update)
self.setText("Button")
_m_isHover = False
def enterEvent(self, event):
super().enterEvent(event)
self._m_isHover = True
self.hoverAnimation.setDirection(self.hoverAnimation.Forward)
self.hoverAnimation.start()
def leaveEvent(self, event):
super().leaveEvent(event)
self._m_isHover = False
self.hoverAnimation.setDirection(self.hoverAnimation.Backward)
self.hoverAnimation.start()
def isHover(self):
return self._m_isHover
def paintEvent(self, event):
aniValue = self.hoverAnimation.currentValue()
painter = QPainter(self)
painter.setRenderHint(QPainter.Antialiasing)
path, path2 = QPainterPath(), QPainterPath()
painter.setBrush(QBrush(self.backgroundColors[0]))
painter.setPen(Qt.NoPen)
rect = QRectF(0, 0, self.width(), self.height())
padding = 10
rect = rect.adjusted(-padding * aniValue, -padding * aniValue, padding * aniValue, padding * aniValue)
path.addRoundedRect(rect.adjusted(padding / 2, padding, -padding / 2, -padding), self.Radius, self.Radius)
painter.drawPath(path)
painter.setBrush(QBrush(self.foregroundColors[0]))
painter.setClipPath(path)
radiusEffectSize = 75
path2.addEllipse(self.rect().center(), radiusEffectSize * aniValue, radiusEffectSize * aniValue)
painter.drawPath(path2)
if self.isHover() or self.hoverAnimation.currentValue() > 0.1: # when leaveEvent triggered, still background color black. So must wait to change textcolor (ofcourse there is probably a better way)
painter.setPen(self.foregroundColors[1])
else:
painter.setPen(self.foregroundColors[0])
painter.drawText(self.rect(), Qt.AlignCenter, self.text())
if __name__ == "__main__":
app = QApplication(sys.argv)
wind = QMainWindow()
wind.setStyleSheet("QMainWindow{background-color:rgb(247,247,250)}")
wind.resize(150, 80)
wid = QWidget()
lay = QHBoxLayout(wid)
lay.setAlignment(Qt.AlignCenter)
mycustombutton = Button19()
lay.addWidget(mycustombutton)
wind.setCentralWidget(wid)
wind.show()
sys.exit(app.exec())
Still feels different, not the same. I need help, thanks!
The main issue in your code is that the padding computation is wrong.
You are increasing the size of the padding from the current rectangle and then decrease it by half the padding size, which doesn't make a lot of sense.
You should instead consider the default padding minus the extent based on the animation value, then adjust (reduce) the rectangle based to it:
padding = 10 * (1 - aniValue)
path.addRoundedRect(
rect.adjusted(padding, padding, -padding, -padding),
self.Radius, self.Radius
)
That will not be sufficient, though: the radius has to consider the actual size of the widget, but that can be misleading: if you take the smaller dimension (between width and height) the ellipse could be smaller than the rectangle, while in the opposite case it would grow up too early, making the animation quite odd. The actual radius should actually be computed using the hypotenuse of the right triangle of the widget width and height (a "perfect" implementation should also consider the radius of the rounded rectangle, but that would be quite too much):
# using hypot() from the math module
radius = hypot(self.width(), self.height()) / 2
path2.addEllipse(self.rect().center(), radius, radius)
Not enough, though: if you closely look at the original animation, you'll see that the "leave" event will not be the same: there is no circle, the "black" rounded rectangle just fades out. We need to take care of that too:
radius = min(self.width(), self.height())
if (self.hoverAnimation.state()
and self.hoverAnimation.direction() == self.hoverAnimation.Forward):
radius *= aniValue
# always full opacity on "fade in"
opacity = 1.
else:
# "fade out"
opacity = aniValue
path2.addEllipse(self.rect().center(), radius, radius)
painter.save()
painter.setOpacity(opacity)
painter.drawPath(path2)
painter.restore()
Nearly there. But the text drawing still has issues. First of all, the "base" should always be painted, and the "hover" should be painted over with the opacity value specified above (unless you want an alpha value). Then, we should always remember that buttons could also use "mnemonics" (keyboard shortcuts that are always highlighted with an underlined character, specified with a preceding & in Qt).
For optimization reasons, it's better to "replicate" similar functions instead of using local variables. It might not be wonderful for reading purposes, but painting functions should be always try to be as fast as possible.
So, here's the final result:
def paintEvent(self, event):
aniValue = self.hoverAnimation.currentValue()
painter = QPainter(self)
painter.setRenderHint(QPainter.Antialiasing)
painter.setBrush(QBrush(self.backgroundColors[0]))
painter.setPen(Qt.NoPen)
rect = self.rect()
path = QPainterPath()
padding = 10 * (1 - aniValue)
path.addRoundedRect(
QRectF(rect).adjusted(padding, padding, -padding, -padding),
self.Radius, self.Radius
)
painter.setClipPath(path)
painter.drawPath(path)
painter.setBrush(QBrush(self.foregroundColors[0]))
if aniValue < 1:
# only draw the default text when the animation isn't finished yet
painter.setPen(self.foregroundColors[0])
painter.drawText(rect, Qt.AlignCenter|Qt.TextShowMnemonic, self.text())
if not aniValue:
# no hover, just ignore the rest
return
hoverPath = QPainterPath()
radius = hypot(self.width(), self.height()) / 2
if (aniValue and self.hoverAnimation.state()
and self.hoverAnimation.direction() == self.hoverAnimation.Forward):
hoverPath.addEllipse(rect.center(),
radius * aniValue, radius * aniValue)
painter.drawPath(hoverPath)
else:
hoverPath.addEllipse(rect.center(), radius, radius)
painter.save()
painter.setOpacity(aniValue)
painter.drawPath(hoverPath)
painter.restore()
painter.setPen(self.foregroundColors[1])
painter.drawText(rect, Qt.AlignCenter|Qt.TextShowMnemonic, self.text())
Some further notes:
isHover() is quite pointless unless you need it for something else but painting: except from extreme performance requirements (for which value caching would make sense), underMouse() is usually sufficient; for this case, it is also a bit irrelevant, as we can be quite sure that the hover state only happens when the animation value is 1 or the animation is active (animation.state()) and its direction is Forward;
the "smoothness" of the animation completely depends on its easingCurve(), so please do experiment with all available curves to find what best suits your needs;
when working with plain shapes and no borders ("pens"), Qt normally works fine, as it happens with the code above, but be aware that painting with pixel-based devices (as QWidgets) could create artifacts while using anti-aliasing; in that case you have to consider the "pen width" and translate the drawing by half its size to obtain a "perfect" shape on the screen;

Blending text into background while using native font rendering

I'm trying to implement a plain text display widget, which fades-out into the background on both its sides.
Unfortunately the only way I've been able to achieve this fade-out effect while using the Windows' font engine is by overlaying a gradient going from a solid background color into transparency. This method works fine for when the background behind the widget is consistent, but this is not always the case (e.g. when placed into a QTabWidget it uses the Button role instead of the Window role, or anything non uniform) and causes the gradient's color to be mismatched
Here's an example of when I'm using the Window color role for the background but the actual background is using the Button color role
I have tried painting both into QImage and then painting it as a whole into the widget, and a QGraphicsOpacityEffect set on the widget, but both of these do not use the native Windows drawing and thus have degraded looks, which is highlighted on these images compared to the current method.
The first image highlights how it should look, with it being rendered using ClearType. On the second image, painting into a QImage is used which loses the subpixel anti-aliasing. The third image is using the QGraphicsOpacityEffect which causes the text to look even more blurry, and darker.
The current overlaying is done by painting simple gradient images over the text like so:
def paint_event(self, paint_event: QtGui.QPaintEvent) -> None:
"""Paint the text at its current scroll position with the fade-out gradients on both sides."""
text_y = (self.height() - self._text_size.height()) // 2
painter = QtGui.QPainter(self)
painter.set_clip_rect(
QtCore.QRect(
QtCore.QPoint(0, text_y),
self._text_size,
)
)
painter.draw_static_text(
QtCore.QPointF(-self._scroll_pos, text_y),
self._static_text,
)
# Show more transparent half of gradient immediately to prevent text from appearing cut-off.
if self._scroll_pos == 0:
fade_in_width = 0
else:
fade_in_width = min(
self._scroll_pos + self.fade_width // 2, self.fade_width
)
painter.draw_image(
-self.fade_width + fade_in_width,
text_y,
self._fade_in_image,
)
fade_out_width = self._text_size.width() - self.width() - self._scroll_pos
if fade_out_width > 0:
fade_out_width = min(self.fade_width, fade_out_width + self.fade_width // 2)
painter.draw_image(
self.width() - fade_out_width,
text_y,
self._fade_out_image,
)
And the whole widget code can be found at https://github.com/Numerlor/Auto_Neutron/blob/3c1bdb8211411e86846710cceec9dc2b23b91cc6/auto_neutron/windows/gui/plain_text_scroller.py#L16
As far as I know, and at least on Linux, I sincerely doubt that that would be possible, as "blending" the background would require knowing the (possibly cumulative) background of the parent(s), and subpixel rendering is not available whenever the background and/or foreground have alpha value below 1.0 (or 255) for raster drawing.
Also, text rendering with subpixel correction requires a surface that is aware of the screen, which makes drawing on image pointless.
If you're fine with the default text antialiasing, though, there's a much simpler approach to achieve the fading, and there's no need to override the painting, as you can achieve this with a basic QLabel and using a QLinearGradient set for the WindowText palette role.
The trick is to use the minimumSizeHint() to get the optimal width for the text and compute the correct stops of the gradient, since those values are always in the range between 0 and 1.
class FaderLabel(QtWidgets.QLabel):
fadeWidth = 20
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
palette = self.palette()
self.baseColor = palette.color(palette.WindowText)
self.fadeColor = QtGui.QColor(self.baseColor)
self.fadeColor.setAlpha(0)
self.grad = QtGui.QLinearGradient(0, 0, 1, 0)
self.grad.setCoordinateMode(self.grad.ObjectBoundingMode)
self.setMinimumWidth(self.fadeWidth * 2)
def updateColor(self):
fadeRatio = self.fadeWidth / self.minimumSizeHint().width()
self.grad.setStops([
(0, self.fadeColor),
(fadeRatio, self.baseColor),
(1 - fadeRatio, self.baseColor),
(1, self.fadeColor)
])
palette = self.palette()
palette.setBrush(palette.WindowText, QtGui.QBrush(self.grad))
self.setPalette(palette)
def setText(self, text):
super().setText(text)
self.updateColor()
def resizeEvent(self, event):
super().resizeEvent(event)
self.updateColor()
app = QtWidgets.QApplication([])
p = app.palette()
p.setColor(p.Window, QtCore.Qt.black)
p.setColor(p.WindowText, QtCore.Qt.white)
app.setPalette(p)
test = FaderLabel('Hello, I am fading label')
test.show()
app.exec()
The subpixel rendering (like ClearType) will be not be available as written above, since using a gradient makes it almost impossible for the engine to properly draw the "mid" pixels.
Another problem with the above code is that it won't work when using stylesheets. In that case, the solution is to create a helper function that will set the existing stylesheet (including the inherited one), get the actual text color, then create a custom stylesheet with the gradient and finally apply that.
class FaderLabel2(QtWidgets.QLabel):
fadeWidth = 20
_styleSheet = ''
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.updateTimer = QtCore.QTimer(
singleShot=True, interval=1, timeout=self.updateColor)
def updateColor(self):
# restore the default stylesheet (if any)
super().setStyleSheet(self._styleSheet)
# ensure that the palette is properly updated
self.ensurePolished()
baseColor = self.palette().color(QtGui.QPalette.WindowText)
fadeColor = QtGui.QColor(baseColor)
fadeColor.setAlpha(0)
fadeRange = self.fadeWidth / self.minimumSizeHint().width()
styleSheet = '''
color: qlineargradient(x1:0, y1:0, x2:1, y2:0,
stop:0 {fade},
stop:{start} {full},
stop:{end} {full},
stop:1 {fade});
'''.format(
fade=fadeColor.name(QtGui.QColor.HexArgb),
full=baseColor.name(QtGui.QColor.HexArgb),
start=fadeRange, end=1-fadeRange)
super().setStyleSheet(styleSheet)
def changeEvent(self, event):
if event.type() == event.StyleChange:
self.updateTimer.start()
def setText(self, text):
super().setText(text)
self.updateColor()
def setStyleSheet(self, styleSheet):
self._styleSheet = styleSheet
self.updateTimer.start()
def resizeEvent(self, event):
super().resizeEvent(event)
self.updateTimer.start()

Best Python UI Framework for simple display (no direct interaction)

TL;DR
How to create a simple "UI" to display a few pictures ("live feed") and update a few sliders to show the state of the application. Basically, how to tell the UI: "show this new image instead of the current one" and "highlight this unclickable button to show it is activated".
I have absolutely zero experience with user interface and graphical display, so I don't know what to google for this question. I have some python code that uses the raspberry pi camera to take pictures, process them a bit (filters) and that's it. I also have a few hardware components (buttons, joystick) to control that camera and the processing applied to the pictures, like filter selection and filter intensity. The hardware events are already handled through a main loop that polls the buttons/joystick controller. The loop also captures the pictures and processes them. Basically, the application is done.
Now, what would be the best way to simply display this information ? What is the best Python framework to achieve this ?
Would PyQT or Kivy allow me to do this ? I would want to show the "live feed", some button information (which button has been pressed to show what processing is currently done on the pictures) in a simple cute layout. There does not need to be anything clickable on the interface, as the interaction occurs on the hardware.
Thanks !
I really like the Tkinter library for these simpler tasks. It is quite similar to java Swing should you be familiar with that. Here is a sample code that does pretty much all you need, even though it is quite messy. It should help you out though.
Note that Tkinter is very raw and not recommended for more elaborate needs.
import tkinter as tk
from PIL import ImageTk, Image
import time
#manipulating canvas items attributes
def change_color(canvas):
if canvas.itemcget(label, 'fill') == 'black':
canvas.itemconfig(label, fill='green')
else:
canvas.itemconfig(label, fill='black')
#changing image displayed
def change_image(canvas, current):
if current == 1:
canvas.itemconfig(img, image=photo2)
return 2
else:
canvas.itemconfig(img, image=photo1)
return 1
#slider gauge
def set_gauge(canvas, percentage):
coords = canvas.coords(gaugefg)
new_y = -2*percentage + 400
coords[1] = new_y
canvas.coords(gaugefg,coords)
color = 'black'
gauge = 0
delta = 10
root = tk.Tk()
#frame size and background
root.geometry('600x600')
root.configure(bg='white')
#make frame pop upfront
root.attributes("-topmost", True)
#uploading images from the same folder
photo1 = ImageTk.PhotoImage(Image.open('download.jpeg'))
photo2 = ImageTk.PhotoImage(Image.open('download1.jpeg'))
current_photo = 1
#canvas on with we'll be drawing
canvas = tk.Canvas(master=root, width=600, height=600)
#text label (xcenter,ycenter)
label = canvas.create_text(500, 500, text='Cool', fill=color, font=('purisa', 20))
#img component (xcenter, ycenter)
img = canvas.create_image(300, 300, image=photo1)
#gauge/slider = two superposed rectangles
#NOTE: (xtopleft, ytopleft, xbottomright, ybottomright)
gaugebg = canvas.create_rectangle(510, 200, 540, 400, fill='black')
gaugefg = canvas.create_rectangle(510, 300, 540, 400, fill='blue')
canvas.pack()
#Main Loop
while True:
change_color(canvas)
current_photo = change_image(canvas, current_photo)
gauge += delta
if gauge == 100 or gauge == 0:
delta = -delta
set_gauge(canvas, gauge)
#update frame and canvas
root.update()
time.sleep(1)
I hope that helps you accomplish what you need. Any questions just text.

What is the best real time plotting widget for wxPython?

I would like to show a real time graph with one or two curves an up to 50 samples per second using Python and wxPython.
The widget should support both Win32 and Linux platforms.
Any hints are welcome.
Edited to add:
I don't need to update the display at 50 fps, but up need to show up to 50 samples of data on both curves, with a reasonable update rate for the display (5..10 fps should be okay).
Edited to add:
I have used mathplotlib in a project with good success.
I have then settled for wx.lib.plot for other projects, which I found to be simpler, but somewhat easier to use and consuming less CPU cycles. As wx.lib comes as part of the standard wxPython distribution is is particularly easy to use.
If you want high performance with a minimal code footprint, look no farther than Python's built-in plotting library tkinter. No need to write special C / C++ code or use a large plotting package to get performance much better than 50 fps.
The following code scrolls a 1000x200 strip chart at 400 fps on a 2.2 GHz Core 2 duo, 1000 fps on a 3.4 GHz Core i3. The central routine "scrollstrip" plots a set of data points and corresponding colors at the right edge along with an optional vertical grid bar, then scrolls the stripchart to the left by 1. To plot horizontal grid bars just include them in the data and color arrays as constants along with your variable data points.
from tkinter import *
import math, random, threading, time
class StripChart:
def __init__(self, root):
self.gf = self.makeGraph(root)
self.cf = self.makeControls(root)
self.gf.pack()
self.cf.pack()
self.Reset()
def makeGraph(self, frame):
self.sw = 1000
self.h = 200
self.top = 2
gf = Canvas(frame, width=self.sw, height=self.h+10,
bg="#002", bd=0, highlightthickness=0)
gf.p = PhotoImage(width=2*self.sw, height=self.h)
self.item = gf.create_image(0, self.top, image=gf.p, anchor=NW)
return(gf)
def makeControls(self, frame):
cf = Frame(frame, borderwidth=1, relief="raised")
Button(cf, text="Run", command=self.Run).grid(column=2, row=2)
Button(cf, text="Stop", command=self.Stop).grid(column=4, row=2)
Button(cf, text="Reset", command=self.Reset).grid(column=6, row=2)
self.fps = Label(cf, text="0 fps")
self.fps.grid(column=2, row=4, columnspan=5)
return(cf)
def Run(self):
self.go = 1
for t in threading.enumerate():
if t.name == "_gen_":
print("already running")
return
threading.Thread(target=self.do_start, name="_gen_").start()
def Stop(self):
self.go = 0
for t in threading.enumerate():
if t.name == "_gen_":
t.join()
def Reset(self):
self.Stop()
self.clearstrip(self.gf.p, '#345')
def do_start(self):
t = 0
y2 = 0
tx = time.time()
while self.go:
y1 = 0.2*math.sin(0.02*math.pi*t)
y2 = 0.9*y2 + 0.1*(random.random()-0.5)
self.scrollstrip(self.gf.p,
(0.25+y1, 0.25, 0.7+y2, 0.6, 0.7, 0.8),
( '#ff4', '#f40', '#4af', '#080', '#0f0', '#080'),
"" if t % 65 else "#088")
t += 1
if not t % 100:
tx2 = time.time()
self.fps.config(text='%d fps' % int(100/(tx2 - tx)))
tx = tx2
# time.sleep(0.001)
def clearstrip(self, p, color): # Fill strip with background color
self.bg = color # save background color for scroll
self.data = None # clear previous data
self.x = 0
p.tk.call(p, 'put', color, '-to', 0, 0, p['width'], p['height'])
def scrollstrip(self, p, data, colors, bar=""): # Scroll the strip, add new data
self.x = (self.x + 1) % self.sw # x = double buffer position
bg = bar if bar else self.bg
p.tk.call(p, 'put', bg, '-to', self.x, 0,
self.x+1, self.h)
p.tk.call(p, 'put', bg, '-to', self.x+self.sw, 0,
self.x+self.sw+1, self.h)
self.gf.coords(self.item, -1-self.x, self.top) # scroll to just-written column
if not self.data:
self.data = data
for d in range(len(data)):
y0 = int((self.h-1) * (1.0-self.data[d])) # plot all the data points
y1 = int((self.h-1) * (1.0-data[d]))
ya, yb = sorted((y0, y1))
for y in range(ya, yb+1): # connect the dots
p.put(colors[d], (self.x,y))
p.put(colors[d], (self.x+self.sw,y))
self.data = data # save for next call
def main():
root = Tk()
root.title("StripChart")
app = StripChart(root)
root.mainloop()
main()
It's not difficult to create a C++ widget that would read from your data source, and truly update at 50 FPS. The beautiful thing about this approach is that very little (if any) Python code would be executing at 50FPS, it would all be in the C++, depending on how you hand your updated data to the widget.
You could even push an event handler into the custom real-time data viewer from the Python side, to handle all the mouse events and user interaction, and leave just the rendering in C++.
It would be a small C++ class that extends wxWidget's wxWindow class
class RealtimeDataViewer: public wxWindow {
...
and override OnPaint
void OnPaint(wxPaintEvent &WXUNUSED(event)) {
....
Then it would get a device context, and start drawing lines and shapes...
You would then have to take the .h file, and copy it to .i, and tweak it just a bit to make it a definition that SWIG could use to extend wxPython.
The build process could be handled by Python's own distutils using the following parameter to setup:
ext_modules=[Extension('myextension', sources,
include_dirs=includeDirs
library_dirs=usual_libs,
)],
It would be a few days work to get it looking great and working well... But it's probably the one option that would really accelerate your project into the future.
And all of this works well on Mac, Windows, and Linux.
wxPython is really a hidden Gem that would really take over the world with more professionally supported IDE / designer tools.
That said, try matplotlib first, it has lots of beautiful optimized rendering, and can do updates in real time too.
If you want really something fast with 50 frames per second, I think you need something like PyGame and kind of talk directly to the display, not a plotting module.
Check the related threads:
What is the fastest way to draw an image from discrete pixel values in Python?
https://stackoverflow.com/search?q=python+pygame
I use PyQtGraph for this kind of thing. It is much faster than Matplotlib for realtime plotting and has lots of nice convenience features like a context menu in the plotting canvas with auto-scaling and scrolling without any extra work.
Maybe Chaco? I don't know if it can do 50 frames per second, but I saw in a demonstration how it did very smooth realtime plotting. It should definitely be faster than matplotlib.

Categories