Python – Analog Meter with New Chart-Class this time

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

About LV_TS_Test_Engineer_3000_VI

Automated Test Equipment Software
This entry was posted in Test Sector and tagged , , , , , , , , . Bookmark the permalink.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s