Note
Go to the endto download the full example code.
Fourier Demo WX#
importwximportnumpyasnpfrommatplotlib.backends.backend_wxaggimportFigureCanvasWxAggasFigureCanvasfrommatplotlib.figureimportFigureclassKnob:""" Knob - simple class with a "setKnob" method. A Knob instance is attached to a Param instance, e.g., param.attach(knob) Base class is for documentation purposes. """defsetKnob(self,value):passclassParam:""" The idea of the "Param" class is that some parameter in the GUI may have several knobs that both control it and reflect the parameter's state, e.g. a slider, text, and dragging can all change the value of the frequency in the waveform of this example. The class allows a cleaner way to update/"feedback" to the other knobs when one is being changed. Also, this class handles min/max constraints for all the knobs. Idea - knob list - in "set" method, knob object is passed as well - the other knobs in the knob list have a "set" method which gets called for the others. """def__init__(self,initialValue=None,minimum=0.,maximum=1.):self.minimum=minimumself.maximum=maximumifinitialValue!=self.constrain(initialValue):raiseValueError('illegal initial value')self.value=initialValueself.knobs=[]defattach(self,knob):self.knobs+=[knob]defset(self,value,knob=None):self.value=valueself.value=self.constrain(value)forfeedbackKnobinself.knobs:iffeedbackKnob!=knob:feedbackKnob.setKnob(self.value)returnself.valuedefconstrain(self,value):ifvalue<=self.minimum:value=self.minimumifvalue>=self.maximum:value=self.maximumreturnvalueclassSliderGroup(Knob):def__init__(self,parent,label,param):self.sliderLabel=wx.StaticText(parent,label=label)self.sliderText=wx.TextCtrl(parent,-1,style=wx.TE_PROCESS_ENTER)self.slider=wx.Slider(parent,-1)# self.slider.SetMax(param.maximum*1000)self.slider.SetRange(0,int(param.maximum*1000))self.setKnob(param.value)sizer=wx.BoxSizer(wx.HORIZONTAL)sizer.Add(self.sliderLabel,0,wx.EXPAND|wx.ALL,border=2)sizer.Add(self.sliderText,0,wx.EXPAND|wx.ALL,border=2)sizer.Add(self.slider,1,wx.EXPAND)self.sizer=sizerself.slider.Bind(wx.EVT_SLIDER,self.sliderHandler)self.sliderText.Bind(wx.EVT_TEXT_ENTER,self.sliderTextHandler)self.param=paramself.param.attach(self)defsliderHandler(self,event):value=event.GetInt()/1000.self.param.set(value)defsliderTextHandler(self,event):value=float(self.sliderText.GetValue())self.param.set(value)defsetKnob(self,value):self.sliderText.SetValue(f'{value:g}')self.slider.SetValue(int(value*1000))classFourierDemoFrame(wx.Frame):def__init__(self,*args,**kwargs):super().__init__(*args,**kwargs)panel=wx.Panel(self)# create the GUI elementsself.createCanvas(panel)self.createSliders(panel)# place them in a sizer for the Layoutsizer=wx.BoxSizer(wx.VERTICAL)sizer.Add(self.canvas,1,wx.EXPAND)sizer.Add(self.frequencySliderGroup.sizer,0,wx.EXPAND|wx.ALL,border=5)sizer.Add(self.amplitudeSliderGroup.sizer,0,wx.EXPAND|wx.ALL,border=5)panel.SetSizer(sizer)defcreateCanvas(self,parent):self.lines=[]self.figure=Figure()self.canvas=FigureCanvas(parent,-1,self.figure)self.canvas.callbacks.connect('button_press_event',self.mouseDown)self.canvas.callbacks.connect('motion_notify_event',self.mouseMotion)self.canvas.callbacks.connect('button_release_event',self.mouseUp)self.state=''self.mouseInfo=(None,None,None,None)self.f0=Param(2.,minimum=0.,maximum=6.)self.A=Param(1.,minimum=0.01,maximum=2.)self.createPlots()# Not sure I like having two params attached to the same Knob,# but that is what we have here... it works but feels kludgy -# although maybe it's not too bad since the knob changes both params# at the same time (both f0 and A are affected during a drag)self.f0.attach(self)self.A.attach(self)defcreateSliders(self,panel):self.frequencySliderGroup=SliderGroup(panel,label='Frequency f0:',param=self.f0)self.amplitudeSliderGroup=SliderGroup(panel,label=' Amplitude a:',param=self.A)defmouseDown(self,event):ifself.lines[0].contains(event)[0]:self.state='frequency'elifself.lines[1].contains(event)[0]:self.state='time'else:self.state=''self.mouseInfo=(event.xdata,event.ydata,max(self.f0.value,.1),self.A.value)defmouseMotion(self,event):ifself.state=='':returnx,y=event.xdata,event.ydataifxisNone:# outside the Axesreturnx0,y0,f0Init,AInit=self.mouseInfoself.A.set(AInit+(AInit*(y-y0)/y0),self)ifself.state=='frequency':self.f0.set(f0Init+(f0Init*(x-x0)/x0))elifself.state=='time':if(x-x0)/x0!=-1.:self.f0.set(1./(1./f0Init+(1./f0Init*(x-x0)/x0)))defmouseUp(self,event):self.state=''defcreatePlots(self):# This method creates the subplots, waveforms and labels.# Later, when the waveforms or sliders are dragged, only the# waveform data will be updated (not here, but below in setKnob).self.subplot1,self.subplot2=self.figure.subplots(2)x1,y1,x2,y2=self.compute(self.f0.value,self.A.value)color=(1.,0.,0.)self.lines+=self.subplot1.plot(x1,y1,color=color,linewidth=2)self.lines+=self.subplot2.plot(x2,y2,color=color,linewidth=2)# Set some plot attributesself.subplot1.set_title("Click and drag waveforms to change frequency and amplitude",fontsize=12)self.subplot1.set_ylabel("Frequency Domain Waveform X(f)",fontsize=8)self.subplot1.set_xlabel("frequency f",fontsize=8)self.subplot2.set_ylabel("Time Domain Waveform x(t)",fontsize=8)self.subplot2.set_xlabel("time t",fontsize=8)self.subplot1.set_xlim(-6,6)self.subplot1.set_ylim(0,1)self.subplot2.set_xlim(-2,2)self.subplot2.set_ylim(-2,2)self.subplot1.text(0.05,.95,r'$X(f) = \mathcal{F}\{x(t)\}$',verticalalignment='top',transform=self.subplot1.transAxes)self.subplot2.text(0.05,.95,r'$x(t) = a \cdot \cos(2\pi f_0 t) e^{-\pi t^2}$',verticalalignment='top',transform=self.subplot2.transAxes)defcompute(self,f0,A):f=np.arange(-6.,6.,0.02)t=np.arange(-2.,2.,0.01)x=A*np.cos(2*np.pi*f0*t)*np.exp(-np.pi*t**2)X=A/2* \(np.exp(-np.pi*(f-f0)**2)+np.exp(-np.pi*(f+f0)**2))returnf,X,t,xdefsetKnob(self,value):# Note, we ignore value arg here and just go by state of the paramsx1,y1,x2,y2=self.compute(self.f0.value,self.A.value)# update the data of the two waveformsself.lines[0].set(xdata=x1,ydata=y1)self.lines[1].set(xdata=x2,ydata=y2)# make the canvas draw its contents again with the new dataself.canvas.draw()classApp(wx.App):defOnInit(self):self.frame1=FourierDemoFrame(parent=None,title="Fourier Demo",size=(640,480))self.frame1.Show()returnTrueif__name__=="__main__":app=App()app.MainLoop()