1. 程式人生 > 程式設計 >詳解pyqt5的UI中嵌入matplotlib圖形並實時重新整理(挖坑和填坑)

詳解pyqt5的UI中嵌入matplotlib圖形並實時重新整理(挖坑和填坑)

一、pyqt5的UI中嵌入matplotlib的方法

1、匯入模組

匯入模組比較簡單,首先宣告使用pyqt5,通過FigureCanvasQTAgg建立畫布,可以將畫布的影象顯示到UI,相當於pyqt5的一個控制元件,後面的繪圖就建立在這個畫布上,然後把這個畫布當中pyqt5的控制元件新增到pyqt5的UI上,其次要匯入matplotlib.figure的Figure ,這裡要注意的是matplotlib.figure中的Figure,不是matplotlib.pyplot模組中的Figure,要區分清楚。

import matplotlib
matplotlib.use("Qt5Agg") # 宣告使用pyqt5
from matplotlib.backends.backend_qt5agg import FigureCanvasQTAgg # pyqt5的畫布
import matplotlib.pyplot as plt
# matplotlib.figure 模組提供了頂層的Artist(圖中的所有可見元素都是Artist的子類),它包含了所有的plot元素
from matplotlib.figure import Figure 

2、建立pyqt5畫布,並簡單設定樣式

建立一個畫布類,繼承上面匯入的FigureCanvasQTAgg,通過Figure 建立畫布,並且作為引數傳遞給父類FigureCanvasQTAgg(這裡是關鍵一步!沒有這一步後面一切都是白費,不會新增成功!),最後一步新增繪圖區self.axes

class MyMatplotlibFigure(FigureCanvasQTAgg):
  """
  建立一個畫布類,並把畫布放到FigureCanvasQTAgg
  """
  def __init__(self,width=10,heigh=10,dpi=100):
    plt.rcParams['figure.facecolor'] = 'r' # 設定窗體顏色
    plt.rcParams['axes.facecolor'] = 'b' # 設定繪圖區顏色
    self.width = width
    self.heigh = heigh
    self.dpi = dpi
    self.figs = Figure(figsize=(self.width,self.heigh),dpi=self.dpi)
    super(MyMatplotlibFigure,self).__init__(self.figs) # 在父類種啟用self.fig, 否則不能顯示影象
    self.axes = self.figs.add_subplot(111)

3、填上建立pyqt5畫布挖的坑

上面自定義的畫布類MyMatplotlibFigure寫的時候不會提示錯誤,但是當你繪圖的時候會傻眼了,因為沒有報錯但是閃退了!!!然後逐個把可疑的類和方法try… except … print(er),希望python能告訴你原因,抱歉!最終結果是什麼都沒有得到!使用debug單步除錯慢慢分析,累死累活的一步一步看到最後新增畫布到pyqt5時,跳到一個模組backend_qt5.py檔案的第500行:if self.height() < 0 or self.width() < 0:從debug的變數分析中看到“(<class ‘TypeError'>,TypeError("‘int' object is not callable"),<traceback object at 0x000001C3E0397F08>)這是什麼鬼?

其實這是一個很簡單的錯誤,但是不小心犯了排查起來很麻煩!!!錯誤的原因就是有些程式設計師在自定義類內接收外部傳參時經常把傳遞的引數轉換為全域性變數,比如這個例子中初始化__init__方法接收的三個引數 width、 heigh、 dpi,順手寫了個

self.width = width
self.heigh = heigh
self.dpi = dpi

然後接著呼叫!問題就在這裡了,其實不管你後面有沒有呼叫,都會閃退!!!!!為什麼呢?
因為FigureCanvasQTAgg父類中匯入了backend_qt5.py模組,而backend_qt5模組內部也使用了相同的變數名self.width和self.heigh,所以呢,在這裡用上面的寫法就造成了對父類變數的覆蓋。正確的寫法:

class MyMatplotlibFigure(FigureCanvasQTAgg):
  """
  建立一個畫布類,並把畫布放到FigureCanvasQTAgg
  """
  def __init__(self,dpi=100):
    plt.rcParams['figure.facecolor'] = 'r' # 設定窗體顏色
    plt.rcParams['axes.facecolor'] = 'b' # 設定繪圖區顏色
    # 建立一個Figure,該Figure為matplotlib下的Figure,不是matplotlib.pyplot下面的Figure
    # 這裡還要注意,width,heigh可以直接呼叫引數,不能用self.width、self.heigh作為變數獲取,因為self.width、self.heigh 在模組中已經FigureCanvasQTAgg模組中使用,這裡定義會造成覆蓋
    self.figs = Figure(figsize=(width,heigh),dpi=dpi)
    super(MyMatplotlibFigure,self).__init__(self.figs) # 在父類種啟用self.fig, 否則不能顯示影象(就是在畫板上放置畫布)
    self.axes = self.figs.add_subplot(111) # 新增繪圖區

這裡直接使用傳參字元就可以了,這幾個引數後面用不到了,如果你能用到就隨便改個名字,比如self.w = width self.h = heigh

4、把畫布新增到pyqt5的UI中

這裡就比較簡單了,建立一個簡單的視窗,新增label,例項化上面建立的自定義畫布類,用變數self.canvas接收例項,這就相當於pyqt5的控制元件了,在label上建立佈局,佈局中新增畫布self.canvas
如果僅僅是把matplotlib的影象新增到Ui中,plotcos這個繪圖方法放哪裡都行,也可以在上面的自定義類中新增這個方法,只是最後繪圖的兩行簡單修改即可:

class MainDialogImgBW_(QtWidgets.QMainWindow):
  """
  建立UI主視窗,使用畫板類繪圖。
  """
  def __init__(self):
    super(MainDialogImgBW_,self).__init__()
    self.setWindowTitle("顯示matplotlib")
    self.setObjectName("widget")
    self.resize(800,600)
    self.label = QtWidgets.QLabel(self)
    self.label.setGeometry(QtCore.QRect(0,800,600))
    self.canvas = MyMatplotlibFigure(width=5,heigh=4,dpi=100)
    self.plotcos()
    self.hboxlayout = QtWidgets.QHBoxLayout(self.label)
    self.hboxlayout.addWidget(self.canvas)

  def plotcos(self):
    # plt.clf()
    t = np.arange(0.0,5.0,0.01)
    s = np.cos(2 * np.pi * t)
    self.canvas.aexs.plot(t,s)
    self.canvas.figs.suptitle("sin") # 設定標題

二、實時重新整理matplotlib影象的坑

實時重新整理影象如果通過網路查詢,基本千篇一律的結果都是先clean清除之前的影象、重新plot、加上重繪draw(),從其它帖子找了最具代表性的三部曲步驟如下:

self.axes.cla()
self.axes.plot(x,y,'o',xx,yy)
self.draw()

這三部曲是沒錯,但是隻是他們說的有點簡單了,有些細節需要注意,否則一樣不會重新整理或者報錯閃退。需要注意的坑是draw(),因為他們帖子上寫的簡單,實在不知道他們的self有幾個意思,一般情況下這麼寫是錯的。**cla()清空了繪圖區,plot()重新繪製了影象,這兩個都是對繪圖區的操作,但是要draw()要重繪的是畫布層不是繪圖區。而且僅僅draw()是不夠的,還要flush_events()否則可能在重新整理畫布過程中中途偶然閃退。**完整的正確程式碼如下(上面的第4條例子大的寫法是繪圖方法在UI類內,下面的例子用另外一種寫法,在畫布類中建立繪圖方法):

class MyMatplotlibFigure(FigureCanvasQTAgg):
  """
  建立一個畫布類,並把畫布放到FigureCanvasQTAgg
  """
  def __init__(self,該Figure為matplotlib下的Figure,不是matplotlib.pyplot下面的Figure
    self.figs = Figure(figsize=(width,self).__init__(self.figs) # 在父類種啟用self.fig, 
    self.axes = self.figs.add_subplot(111) # 新增繪圖區
  def mat_plot_drow_axes(self,t,s):
    """
    用清除畫布重新整理的方法繪圖
    :return:
    """
    self.axes.cla() # 清除繪圖區

    self.axes.spines['top'].set_visible(False) # 頂邊界不可見
    self.axes.spines['right'].set_visible(False) # 右邊界不可見
    # 設定左、下邊界在(0,0)處相交
    # self.axes.spines['bottom'].set_position(('data',0)) # 設定y軸線原點資料為 0
    self.axes.spines['left'].set_position(('data',0)) # 設定x軸線原點資料為 0
    self.axes.plot(t,s,'o-r',linewidth=0.5)
    self.figs.canvas.draw() # 這裡注意是畫布重繪,self.figs.canvas
    self.figs.canvas.flush_events() # 畫布重新整理self.figs.canvas

class MainDialogImgBW(QtWidgets.QMainWindow):
  """
  建立UI主視窗,使用畫板類繪圖。
  """
  def __init__(self):
    super(MainDialogImgBW_,0.01)
    s = np.cos(2 * np.pi * t)
    self.canvas.mat_plot_drow_axes(t,s)
    self.canvas.figs.suptitle("sin") # 設定標題


if __name__ == "__main__":
  app = QtWidgets.QApplication(sys.argv)
  main = MainDialogImgBW()
  main.show()
  sys.exit(app.exec_())

**需要注意的地方就是畫布重繪的寫法,既不是self.draw()也不是self.axes.draw()或self.figs.draw(),而是self.figs.canvas.draw()和self.figs.canvas.flush_events(),這裡比較坑的是寫這兩句程式碼時沒有智慧提醒!(我用的pycharm,也有可能是被這個坑了,不知道其他IDE是否會提醒)**這裡還要提醒的是隻有self.figs.canvas.draw()沒有self.figs.canvas.flush_events()時也會重繪,但是有可能在執行過程中閃退,所以還是加上比較安全。

三、實時更新matplotlib的另一種方法

上面是使用axes.cla()的方式重新整理圖表,但是你有可能會遇到,你要展示的下一個圖形於前面一次圖表完全不同,包括畫布背景色等都不同,那麼用上面的axes.cla()只清理繪圖區就不夠了,需要用得到清理畫布figure.clf(),這個地方你要看清楚,清理繪圖區方法是cla(),而清理畫布是clf()一字之差。另外一個需要注意的地方就是,清理畫布後之前畫布上的繪圖區axes也清理了,需要重新新增axes,完整程式碼如下:

class MyMatplotlibFigure(FigureCanvasQTAgg):
  """
  建立一個畫布類,並把畫布放到FigureCanvasQTAgg
  """
  def __init__(self,dpi=100):
    # 建立一個Figure,self).__init__(self.figs) # 在父類種啟用self.fig, 
  def mat_plot_drow(self,s):
    """
    用清除畫布重新整理的方法繪圖
    :return:
    """
    self.figs.clf() # 清理畫布,這裡是clf()
    self.axes = self.figs.add_subplot(111) # 清理畫布後必須重新新增繪圖區
 self.axes.patch.set_facecolor("#01386a") # 設定ax區域背景顏色
    self.axes.patch.set_alpha(0.5) # 設定ax區域背景顏色透明度
    self.figs.patch.set_facecolor('#01386a') # 設定繪圖區域顏色
    self.axes.spines['bottom'].set_color('r') # 設定下邊界顏色
    self.axes.spines['top'].set_visible(False) # 頂邊界不可見
    self.axes.spines['right'].set_visible(False) # 右邊界不可見
    # 設定左、下邊界在(0,0)處相交
    # self.axes.spines['bottom'].set_position(('data',0.01)
    s = np.cos(2 * np.pi * t)
    self.canvas.mat_plot_drow(t,s)
    self.canvas.figs.suptitle("sin") # 設定標題


if __name__ == "__main__":
  app = QtWidgets.QApplication(sys.argv)
  main = MainDialogImgBW()
  main.show()
  sys.exit(app.exec_())

四、animation的方式重新整理matplotlib

如果你在UI中的重新整理頻率非常高,比如股票或期貨的tick資料,上面的重新整理方式就有點不夠用了,雖然也能重新整理但是又可能會閃屏的情況很不舒服,高頻重新整理還是用animation方式重新整理。
使用animation 需要增加匯入matplotlib.animation模組的FuncAnimation方法,全部匯入模組如下:

import matplotlib
matplotlib.use("Qt5Agg") # 宣告使用pyqt5
from matplotlib.backends.backend_qt5agg import FigureCanvasQTAgg # pyqt5的畫布
import matplotlib.pyplot as plt
from matplotlib.figure import Figure 
from matplotlib.animation import FuncAnimation

FuncAnimation的基礎使用這裡就不贅述了,論壇內搜尋就可以找到,只提一個可能存在坑的地方,資料更新函式是巢狀在繪圖方法plot_tick內的(可不要以為這是格式錯誤)。這裡直接上程式碼:

class MyMatPlotAnimation(FigureCanvasQTAgg):
  """
  建立一個畫板類,並把畫布放到容器(畫板上)FigureCanvasQTAgg,再建立一個畫圖區
  """
  def __init__(self,dpi=dpi)
    super(MyMatPlotAnimation,self).__init__(self.figs) 
    self.figs.patch.set_facecolor('#01386a') # 設定繪圖區域顏色
    self.axes = self.figs.add_subplot(111)

  def set_mat_func(self,s):
    """
    初始化設定函式
    """
    self.t = t
    self.s = s
    self.axes.cla()
    self.axes.patch.set_facecolor("#01386a") # 設定ax區域背景顏色
    self.axes.patch.set_alpha(0.5) # 設定ax區域背景顏色透明度

    # self.axes.spines['top'].set_color('#01386a')
    self.axes.spines['top'].set_visible(False) # 頂邊界不可見
    self.axes.spines['right'].set_visible(False) # 右邊界不可見

    self.axes.xaxis.set_ticks_position('bottom') # 設定ticks(刻度)的位置為下方
    self.axes.yaxis.set_ticks_position('left') # 設定ticks(刻度) 的位置為左側
    # 設定左、下邊界在(0,0)處相交
    # self.axes.spines['bottom'].set_position(('data',0)) # 設定x軸線再Y軸0位置
    self.axes.spines['left'].set_position(('data',0)) # 設定y軸在x軸0位置
    self.plot_line,= self.axes.plot([],[],'r-',linewidth=1) # 注意‘,'不可省略
    
  def plot_tick(self):
    plot_line = self.plot_line
    plot_axes = self.axes
    t = self.t
    
    def upgrade(i): # 注意這裡是plot_tick方法內的巢狀函式
      x_data = [] # 這裡注意如果是使用全域性變數self定義,可能會導致繪圖首位相聯
      y_data = []
      for i in range(len(t)):
        x_data.append(i)
        y_data.append(self.s[i])
      plot_axes.plot(x_data,y_data,linewidth=1)
      return plot_line,# 這裡也是注意‘,'不可省略,否則會報錯
      
    ani = FuncAnimation(self.figs,upgrade,blit=True,repeat=False)
    self.figs.canvas.draw() # 重繪還是必須要的

class MainDialogImgBW(QtWidgets.QMainWindow):
  def __init__(self):
    super(MainDialogImgBW_,600))
    self.canvas = MyMatPlotAnimation(width=5,dpi=100)
    self.plotcos()
    self.hboxlayout = QtWidgets.QHBoxLayout(self.label)
    self.hboxlayout.addWidget(self.canvas)

  def plotcos(self):
    t = np.arange(0.0,0.01)
    s = np.cos(2 * np.pi * t)
    self.canvas.set_mat_func(t,s)
    self.canvas.plot_tick()


if __name__ == "__main__":
  app = QtWidgets.QApplication(sys.argv)
  main = MainDialogImgBW()
  main.show()
  sys.exit(app.exec_())

到此這篇關於詳解pyqt5的UI中嵌入matplotlib圖形並實時重新整理(挖坑和填坑)的文章就介紹到這了,更多相關pyqt5嵌入matplotlib圖形內容請搜尋我們以前的文章或繼續瀏覽下面的相關文章希望大家以後多多支援我們!