converted a previous python project over to Class type with new Chart /scope adventure
Short Video on GUI (tkinter)
Python Code Text (tkinter)
# -*- coding: utf-8 -*-
"""
Created on Tue Apr 18 06:52:07 2023
@author: aleja
Using Class with the Analog Meter and Chart
"""
#from tkinter import *
import tkinter as tk
import math
import time
import threading
class MyApp:
def __init__(self,tk):
#GUI setup below
self.myWindow=tk.Tk()
self.myWindow.title("myWindow-Analog Meter and Chart GUI")
w_w=1000
w_h=500
self.myWindow.minsize(w_w,w_h)
self.myWindow.maxsize(w_w,w_h)
print(self.myWindow.winfo_x(),self.myWindow.winfo_y())
#if you want to move the operator table to see rest of belt comment out
#next time use resize=false,false
# if you ever notice that some frame is sticking to bottom look at these setting
self.myWindow.grid_rowconfigure(1,weight=1) #dont want top row frame to grow(row=0)
self.myWindow.grid_columnconfigure(1,weight=1)
self.myWindow.configure(background='gray')
def mainloop(self):
tk.mainloop()
def MakeFrames(self):
#Left side button and input
self.frame1=tk.Frame(self.myWindow,width=120,height=300,bg='lightblue')
self.frame1.grid(row=0, column=0, sticky='NSWE', padx=10, pady=10, columnspan=1,rowspan=2)
self.frame1.rowconfigure(0,weight=1)
self.frame1.columnconfigure(0,weight=0) #the lastcol goes along with rigth side expanding
#frame2 Right
# adjust r,c,sticky, row and column config as need
self.frame2=tk.Frame(self.myWindow,width=350,height=350,bg='blue')
self.frame2.grid(row=1, column=1, sticky='NESW', padx=(0,10), pady=(0,10), columnspan=1) #padding ALSO takes two arguments tuple
self.frame2.rowconfigure(0,weight=0)
self.frame2.columnconfigure(1,weight=0) #the lastcol goes along with rigth side expanding
#Top frame
self.frame3=tk.Frame(self.myWindow,width=100,height=100,bg='lightblue')
self.frame3.grid(row=0, column=1, sticky='NESW', padx=(0,10), pady=10, columnspan=1) #padding ALSO takes two arguments tuple
self.frame3.rowconfigure(1,weight=0)
self.frame3.columnconfigure(1,weight=1) #the lastcol goes along with rigth side expanding
def TopFrameWidget(self,strAppTitle="YourAppTitle",strAppCompany="YourCompanyName"):
self.AppTitle=strAppTitle
self.AppCompany=strAppCompany
#frame3 info
AppTitleLabel=tk.Label(self.frame3, width=100, text=self.AppTitle)
CompanyName=tk.Label(self.frame3, text=self.AppCompany)
AppTitleLabel.grid(row=0, column=0, padx=0, pady=10, columnspan=2) #span 2 so it does not affect component button
CompanyName.grid(row=0, column=3, padx=10, sticky='E') #Keep
def MakeAnalogMeter(self,width=300,height=300):
# ===============Meter_Info =================
self.Meter_Width=width
self.Meter_Height=height
#Meter Povit Point Center Dot
#Meter Povit Point Center Dot
self.Dot_Padding=10
self.Center_Dot_x=self.Meter_Width/2+self.Dot_Padding
self.Center_Dot_y=self.Meter_Height/2+self.Dot_Padding
self.Center_Dot_radius=10
self.canv2=tk.Canvas(self.frame2,width=self.Meter_Width+30, height=self.Meter_Height, bg='lightblue', bd=0, highlightthickness=0)
self.canv2.grid(row=0, column=0, columnspan=1,padx=20,pady=10)
self.canv2.create_oval(self.Center_Dot_x-self.Center_Dot_radius,self.Center_Dot_y-self.Center_Dot_radius,\
self.Center_Dot_x+self.Center_Dot_radius,self.Center_Dot_y+self.Center_Dot_radius,fill='blue')
self.Reading_Value2=self.canv2.create_text(self.Meter_Width/2-15,225,fill="black",text="",font=('Arial',20,''),anchor="center")
#=== Two Arc for Major and Minor tick marks
arcx=self.Center_Dot_x
arcy=self.Center_Dot_y
arc_circle_radius=100
coord_arc=(arcx-arc_circle_radius,arcx-arc_circle_radius,\
arcx+arc_circle_radius,arcx+arc_circle_radius)
arc_startDeg=0
arc_endDeg=180
self.canv2.create_arc(coord_arc,start=arc_startDeg,extent=arc_endDeg)
arc_circle_radius=120
coord_arc=(arcx-arc_circle_radius,arcx-arc_circle_radius,\
arcx+arc_circle_radius,arcx+arc_circle_radius)
self.canv2.create_arc(coord_arc,start=arc_startDeg,extent=arc_endDeg)
#=== create first starting Meter Pointer red arrow line
MeterValue=0 #unscale 0-180
Deg=MeterValue-90
RadDeg=math.radians(Deg)
line_length=115
line_x=line_length * math.sin(RadDeg)
line_y=line_length * math.cos(RadDeg)
self.MeterPointer=self.canv2.create_line(self.Center_Dot_x,self.Center_Dot_y,self.Center_Dot_x+line_x,self.Center_Dot_y-line_y,fill='red',width=3,arrow='last')# remember y going up is less
#======= Major and Minor ====== Tick markers
#major line
for iDeg in range(-90,91,10): #because of axis 0-180 = -90 to 90
RadDeg=math.radians(iDeg)
subline_length=100
subline_x1=subline_length * math.sin(RadDeg)
subline_y1=subline_length * math.cos(RadDeg)
subline_length=120
subline_x2=subline_length * math.sin(RadDeg)
subline_y2=subline_length * math.cos(RadDeg)
self.canv2.create_line(self.Center_Dot_x+subline_x1,self.Center_Dot_y-subline_y1,\
self.Center_Dot_x+subline_x2,self.Center_Dot_y-subline_y2,fill='black',width=3)# remember y going up is less
#minor line
for iDeg in range(-90,91,2):
RadDeg=math.radians(iDeg)
subline_length=100
subline_x1=subline_length * math.sin(RadDeg)
subline_y1=subline_length * math.cos(RadDeg)
subline_length=120
subline_x2=subline_length * math.sin(RadDeg)
subline_y2=subline_length * math.cos(RadDeg)
self.canv2.create_line(self.Center_Dot_x+subline_x1,self.Center_Dot_y-subline_y1,\
self.Center_Dot_x+subline_x2,self.Center_Dot_y-subline_y2,fill='gray',width=1)# remember y going up is less
def Update_Meter_MajorText(self,EngMin=0,EngMax=10):
self.EngMin=EngMin
self.EngMax=EngMax
self.MaxDeg=180 #keep dont change for demo, it important for 180 deg display meter
self.MinDeg=0 #Keep dont change for demo
self.Meter_Text_Canv_List=[]
"""
Updates the Meters Major Text Number
"""
#Number text on Major ticks
MeterNumMajor=0
for iDeg in range(-90,91,10):
MeterNumMajor=self.Deg_to_EngNum(iDeg+90)
RadDeg=math.radians(iDeg)
Text_Radius_Loc=140
Text_x1=Text_Radius_Loc * math.sin(RadDeg)
Text_y1=Text_Radius_Loc * math.cos(RadDeg)
M_T_C=self.canv2.create_text(self.Center_Dot_x+Text_x1,self.Center_Dot_y-Text_y1,fill="black",text=round(MeterNumMajor,1),font=('Arial',9,''))
self.Meter_Text_Canv_List.append(M_T_C)
MeterNumMajor=MeterNumMajor+10
def EngNum_to_Deg(self,EngNum):
M_slope=((self.MaxDeg-self.MinDeg)/(self.EngMax-self.EngMin))
Deg=M_slope*(EngNum-self.EngMin)
return Deg
def Deg_to_EngNum(self,Deg0_180):
M_slope=((self.EngMax-self.EngMin)/(self.MaxDeg-self.MinDeg))
EngNum=M_slope*(Deg0_180)+self.EngMin
return EngNum
def InputVerticalSlider(self):
#binding keys and mouse
#=== Veritical Slider used to change Meter Pointer (reading) ===============================
self.MeasNum=tk.DoubleVar()
self.VSlider=tk.Scale(self.frame1,from_=self.EngMax, to=self.EngMin,length=150,variable=self.MeasNum,digits=3,resolution=0.1)
self.VSlider.grid(row=0,column=0) #dont forget the odd underscore on from
#Becasue the slider would generate tons of event using release and I DONT KNOW how have ,not found ScaleChanged event
self.VSlider.bind('<ButtonRelease-1>',lambda event: self.Update_Meter(event,"Mouse Released"))
#important <Up> is not equal to <KeyRelease-Up> Up only was causing a missing event that cause a one resolution offset
self.VSlider.bind('<KeyRelease-Up>',lambda event: self.Update_Meter(event,"Up key Pressed")) #just using UP will give on missing event which cause odd 0.1 offest
self.VSlider.bind('<KeyRelease-Down>',lambda event: self.Update_Meter(event,"Down key Pressed"))
#Not working this way VSlider.bind('<ButtonRelease-1>,<Up>,<Down>',Update_Meter)
self.VSlider.focus_set() #focus is important
self.frame1.focus_force() #focus is important for demo so the frame will listen to the up and down keys both the keypad, and keyboard area
#interesting ctrl and up key moved it in big steps, not the shift key the ctrl key
def Buttons_Left_side(self):
Button_side_Label=tk.Label(self.frame1,text="Sim_Input")
Button_side_Label.grid(row=0,column=0,padx=10,pady=10,stick="N")
Zero_VSlider_Btn=tk.Button(self.frame1, text='Min',width=15, command=lambda:self.Zero_Slider())
Zero_VSlider_Btn.grid(row=1, column=0,padx=19,pady=2)
Zero_to_10_Btn=tk.Button(self.frame1, text='Eng(0-10)',width=15, command=lambda:self.Meter_and_Slider_Update(0,10))
Zero_to_10_Btn.grid(row=2, column=0,padx=10,pady=2)
Zero_to_5_Btn=tk.Button(self.frame1, text='Eng(0-5)',width=15, command=lambda:self.Meter_and_Slider_Update(0,5))
Zero_to_5_Btn.grid(row=3, column=0,padx=10,pady=2)
Four_to_20_Btn=tk.Button(self.frame1, text='Eng(4-20)',width=15, command=lambda:self.Meter_and_Slider_Update(4,20))
Four_to_20_Btn.grid(row=4, column=0,padx=10,pady=2)
N10_to_P10_Btn=tk.Button(self.frame1, text='Eng(N10-P10)',width=15, command=lambda:self.Meter_and_Slider_Update(-10,10))
N10_to_P10_Btn.grid(row=5, column=0,padx=10,pady=2)
self.Clr_n_Sine_Btn=tk.Button(self.frame1, text='Clr and Sine',width=15, command=lambda:self.Thread_Call_Sine())
self.Clr_n_Sine_Btn.grid(row=6, column=0,padx=10,pady=2)
self.Clr_Btn=tk.Button(self.frame1, text='Clear Plot',width=15, command=lambda:self.Clear_Plot_Lines())
self.Clr_Btn.grid(row=7, column=0,padx=10,pady=2)
def RightSideCanvas(self):
"""
Description: Right side canvas for text and chart use \n
Arguments: no arg\n
Other: \n
Other:
"""
#=== Canvas 3
# canv3 side info on Right
self.canv3=tk.Canvas(self.frame2,width=self.Meter_Width, height=self.Meter_Height, bg='black', bd=0, highlightthickness=0)
self.canv3.grid(row=0, column=1, columnspan=1,padx=20,pady=10)
def Chart_Init(self,C_Width,C_Height):
"""
Description: Initial Chart canvas with outer canvas \n
Arguments: no arg\n
Other: \n
Other:
"""
self.Chart_Width=C_Width
self.Chart_Height=C_Width
#Graph Initialize
self.List_Of_Points=[(0,self.Chart_Height),(0,self.Chart_Height)]
self.Point_Sample_Index=0
UnTuple=[a for x in self.List_Of_Points for a in x] #stackoverflow help credit "Python-tkiner How to create-line with multiple points P.C. answer
self.Plot_Line=self.canv3.create_line(*UnTuple,fill='yellow',width=3)
#print(UnTuple)
#scope outer border canvas
self.canv4=tk.Canvas(self.frame2,width=self.Chart_Width+100, height=self.Chart_Height+100, bg='lightgray', bd=1, highlightthickness=1)
self.canv4.grid(row=0, column=1, columnspan=1,padx=20,pady=10)
tk.Misc.lift(self.canv3)# stackoverflow help
def Chart_Grid_Lines(self):
#===== Chart Grid Line
for x_div in range(11):
self.canv3.create_line(x_div*self.Chart_Width/10,0,x_div*self.Chart_Width/10,self.Chart_Height,fill='green',width=2)
print(x_div)
for y_div in range(11):
self.canv3.create_line(0,y_div*self.Chart_Height/10,self.Chart_Width,y_div*self.Chart_Height/10,fill='green',width=2)
tk.Misc.lift(self.canv3)# stackoverflow
#move here to resolve text behind grid lines
#some info on right side convas - Reading will get a value when updating vertical slider(called scale in python)
self.Info_Label=self.canv3.create_text(10,0,fill="white",text="Use the mouse in demo \n to move the slider or \n the Up, Down keys",anchor='nw',font=('Arial',14,''))
self.Reading_Value=self.canv3.create_text(50,100,fill="white",text="",font=('Arial',20,''),anchor="center")
self.Event_Label_Info=self.canv3.create_text(10,150,fill="white",text="",font=('Arial',14,''))
def Chart_Border_Text(self):
self.Y_Chart_Text_List=[]
for y_scope_text in range(11):
self.Y_Chart_Text=self.canv4.create_text(40,y_scope_text*self.Chart_Height/10+50,text=str(self.EngMax-y_scope_text),font=('Arial',9,''))
self.Y_Chart_Text_List.append(self.Y_Chart_Text)
for x_scope_text in range(11):
self.canv4.create_text(x_scope_text*self.Chart_Width/10+50,self.Chart_Height+60,text=str(x_scope_text*30),font=('Arial',9,''))
self.canv4.create_text(20,self.Chart_Height/2+50,text="Amplitude",angle=90,font=('Arial',12,''))
self.canv4.create_text(self.Chart_Width/2+50,self.Chart_Height+80,text="Sample",font=('Arial',12,''))
self.ChartTitle=self.canv4.create_text(self.Chart_Width/2+50,20,text="Your Chart Title",font=('Arial',12,'bold'))
def Chart_Border_Text_Update(self):
i=0
Y_Div=(self.EngMax-self.EngMin)/10
for Y_Chart_txt in self.Y_Chart_Text_List:
self.canv4.itemconfigure(Y_Chart_txt,text=str(round(self.EngMax-Y_Div*i,1)))
i+=1
def Update_Chart_Title(self,strText=""):
self.canv4.itemconfigure(self.ChartTitle,text=strText)
#button functions
#==== Main Update of Meter Pointer
def Update_Meter(self,event=None,EventMsg="NoMsg"): # I am a relative novice for this event obj arg , its in my learning curve,it did not hiddern this demo
"""
Description: Update the Meters red pointer \n
Arguments: arg event is tkinter obj when bind events, EventMsg is user string\n
Other: currently Dont know how to use event object directly yet, so using EventMsg in this demo
\nOther: using default None so i can use this function without event also.
"""
self.Point_Sample_Index
if event==None:
#just a note to remind when a function call was made that as event=None
str_note="reminder,Please note event arg was empty so dont used it"
#print(str_note)
#Update the pointer on the meter
#MeterValue=(MeasNum.get())
self.MeterValue=self.MeasNum.get()
#VSlider.set(MeterValue)
Deg=(self.EngNum_to_Deg(self.MeterValue))-90
RadDeg=math.radians(Deg)
line_length=115
line_x=line_length * math.sin(RadDeg)
line_y=line_length * math.cos(RadDeg)
self.canv2.coords(self.MeterPointer,self.Center_Dot_x,self.Center_Dot_y,self.Center_Dot_x+line_x,self.Center_Dot_y-line_y)
#canv2.create_line(Center_Dot_x,Center_Dot_y,Center_Dot_x+line_x,Center_Dot_y-line_y,fill='red',width=3,arrow='last')# remember y going up is less
self.canv2.itemconfigure(self.Reading_Value2,text=str(round(self.MeterValue,3)),anchor='w')
self.canv3.itemconfigure(self.Reading_Value,text=str(round(self.MeterValue,3)),anchor='w')
self.canv3.itemconfigure(self.Event_Label_Info,text=str(EventMsg),anchor='w')
#put Point in list
if(self.EngMin==0):self.Scale_MeterValue=(self.Meter_Height/self.EngMax-self.EngMin) * self.MeterValue*(1)+self.Meter_Height/(self.EngMax-self.EngMin)*0 #leave at bottom -0
if(self.EngMin<0):self.Scale_MeterValue=(self.Meter_Height/(self.EngMax-self.EngMin)) * self.MeterValue*(1)+self.Meter_Height/(self.EngMax-self.EngMin)*10 #ref up 10
if(self.EngMin>0):self.Scale_MeterValue=(self.Meter_Height/(self.EngMax-self.EngMin)) * self.MeterValue*(1)+self.Meter_Height/(self.EngMax-self.EngMin)*-4 #ref down 4
#so chart also update when meter called
self.Chart_Border_Text_Update()
self.Update_Chart_Plot()
def Update_Chart_Plot(self):
"""
Description: Updates the chart with scrolling left effect \n
Arguments: no arg\n
Other: \n
Other:
"""
#==========chart style scroll left=================
max_chart_points=self.Chart_Width
if(self.Point_Sample_Index>max_chart_points):
self.List_Of_Points.pop(0)
New_LOP=[]
i=0
for Point in self.List_Of_Points:
#New_LOP
#tuple are unchangeable (has no index)
t_to_lst=list(Point)
t_to_lst[0]=i #to start shifting graph left(the x values=sample index)
Point=tuple(t_to_lst)
#print(Point)
New_LOP.append(Point) #because point shift x for each y it all new
i=i+1
self.List_Of_Points= New_LOP
self.Point_Sample_Index=i
self.List_Of_Points.append((self.Point_Sample_Index,self.Chart_Height-int(self.Scale_MeterValue)))
self.Point_Sample_Index +=1
UnTuple=[a for x in self.List_Of_Points for a in x] #stackoverflow help credit "Python-tkiner How to create-line with multiple points P.C. answer
#self.canv3.delete(self.Plot_Line,'all') #seem to delete ALL "line type" even the grid lines,interesting
self.canv3.delete(self.Plot_Line) #needed but not work complete working in thread completely
self.Plot_Line=self.canv3.create_line(*UnTuple,fill='yellow',width=3)
#print(UnTuple)
self.myWindow.update()
def Zero_Slider(self):
self.VSlider.focus_force
if (self.EngMin<0):
self.MeasNum.set(0)
else:
self.MeasNum.set(self.EngMin)
self.Update_Meter(None,"Min/Zero Button or Function Call") #warning not using event arg or in function
return
def Meter_and_Slider_Update(self,Emin=0,Emax=10):
"""
Description: Update Meter and Slider \n
Arguments: Eng Min and Eng Max\n
Other:Used when moving slider or changing range \n
Other:
"""
self.EngMax=Emax
self.EngMin=Emin
print(self.EngMin,self.EngMax)
#first delete current text
for M_T_C_item in self.Meter_Text_Canv_List:
self.canv2.delete(M_T_C_item)
self.Update_Meter_MajorText(self.EngMin,self.EngMax)
self.VSlider.configure(from_=self.EngMax,to=self.EngMin)
self.VSlider.focus_force
if(self.EngMin<0):
self.VSlider.set(0)
self.MeasNum.set(0)
else:
self.VSlider.set(self.EngMin)
self.MeasNum.set(self.EngMin)
self.Update_Meter(None,"Button released:"+str(self.EngMin)+"--"+str(self.EngMax)) #warning not using event arg or in function
#self.canv3.delete(self.Plot_Line)
return
def Thread_Call_Sine(self):
threading.Thread(target=self.Clr_n_Sine,daemon=True).start() #remember no ()
def Clr_n_Sine(self):
#if real hardware connected must check sin amplitude for your application
#demo sinewave
self.Clr_n_Sine_Btn['state']='disabled'
#print(MeterValue,MeasNum.get(),VSlider.get())
self.VSlider.set(0)
#also clear List of point
self.Point_Sample_Index
self.List_Of_Points=[(0,self.Chart_Height),(0,self.Chart_Height)] #need two points same at zero
UnTuple=[a for x in self.List_Of_Points for a in x]
self.canv3.delete(self.Plot_Line)
if (self.EngMin<0):
self.Num_Of_90s=7
self.StartingPhase=-90
else:
self.Num_Of_90s=8
self.StartingPhase=0
for angle in range(self.StartingPhase,90*self.Num_Of_90s,1):
#for amp 8
#time.sleep(0.005) #commented out to see full speed cpU% wil increase since not delay
max_amp=(self.EngMax-self.EngMin)/2
offset=(self.EngMax+self.EngMin)/2
y=max_amp*math.sin(math.radians(angle-90))+offset
x=5*math.cos(math.radians(angle))
self.VSlider.focus_force
self.MeasNum.set(y)
self.Update_Meter(None,"sin function")
#self.Update_Chart_Plot()
print(angle)
self.Clr_n_Sine_Btn['state']='normal'
self.Chart_Border_Text_Update()
self.Zero_Slider() #zero out or min slider
def Clear_Plot_Lines(self):
self.VSlider.focus_force
self.canv3.delete(self.Plot_Line)
self.List_Of_Points.clear() #Dont for the () ,yes i forget
self.List_Of_Points=[(0,self.Chart_Height),((0,self.Chart_Height))]#but need min two point for create line
self.Point_Sample_Index=0
self.canv3.itemconfigure(self.Info_Label,text="")
self.canv3.itemconfigure(self.Event_Label_Info,text="")
def main():
#start steps to of app
root=MyApp(tk)
root.MakeFrames()
root.TopFrameWidget("Analog Meter Display and Chart","TestEngineerResource.com")
root.MakeAnalogMeter(300,300)
root.Update_Meter_MajorText(0,10)
root.InputVerticalSlider()
root.Buttons_Left_side()
root.RightSideCanvas()
root.Chart_Init(300,300)
root.Chart_Grid_Lines()
root.Chart_Border_Text()
root.Update_Chart_Title("Chart Demo")
#last line in main
root.mainloop()
if __name__ =="__main__":
main()
Python – Analog Meter and Chart Download
Notes
Presentation: Analog Meter Display and Chart GUI using Vertical slider for input to meter, show different scale changes
Programming Language used: Python 3.7 in Spyder
Presentation app: Microsoft’s PowerPoint
Python and Tkinter are products of respective company
Presentation shown to spark ideas of use.
This presentation is not connected to or endorsed by any company.
Use at your own risk.
Tags: Python, Python3.7, Tkinter , Canvas ,GUI, List, Dictionary, StringVar, Calling, Event binding , Default None for Event, Key and Mouse binding, KeyRelease-Up, KeyRelease-Down, Chart, sinewave