本篇目的: 理解 Matplotlib 與 PySide6 的整合機制,使用
FigureCanvasQTAgg將圖表嵌入 GUI,並建立可由 Controller 呼叫的繪圖管理器。
我們在 day 27 實作之 GUI 程式內設定有 2 功能:
計算各片產出:已在 day 27 說明
Wafer 繪製與即時互動:在 day 28~29 要說明之內容
我們在很久以前的 day 6~7 (你會記得你吃過多少片麵包嗎) 介紹如何使用 Matplotlib 繪製 wafer mapping,以及調整相關參數達到去背、繪製格線、調整 color scale 之行為。
不過我們當時就只有輸出一張 png 圖檔,如果每次想查看另一片 wafer 或切換繪製的測項,都必須重新執行程式與輸出圖片,這樣的操作很麻煩且無法快速看多片 wafer。
因此這 2 天我們要實作一個功能” 讓 Matplotlib 繪製的 Mapping 直接出現在 PySide6 視窗裡,並且能隨著使用者操作更新”。實作結果如下圖,而今天會先說明如何將 Matplotlib 的圖像放入 QT UI 中。

我們在 day 6 繪製 mapping 時,使用過下面這段 code 建立 Figure 和 Axes,前者管理整張圖表,後者負責座標系統與實際繪圖。
fig, ax = plt.subplots()
Figure 和 Axes 管理圖表內容與座標軸等表層內容,但我們今天要做的是讓 Matplotlib 在 QT UI 中繪圖,會用到比較底層的東西。Matplotlib 的架構大致如下表:
| 物件 | 主要職責 |
|---|---|
Figure |
管理整張圖表及其中的 Axes |
Axes |
管理座標系統與實際繪圖內容 |
FigureCanvas |
將 Figure 繪製到指定的顯示表面 |
Backend |
提供圖形渲染及 GUI 整合機制 |
簡單來說,Figure 與 Axes 負責管理”要畫什麼”、canvas 負責呈現圖表,而 backend 提供背後所需的渲染及介面整合功能。我們今天要將圖表放入 QT UI 之中,因此會用到 Matplotlib 的 backend 與 FigureCanvas 。
Canvas 並非完全獨立於 Backend,不同的 Backend 可以提供不同的 Canvas 實作。因為我們是要在 QT 環境中繪圖,因此選用對應的 QtAgg Backend以及它提供的 FigureCanvasQTAgg 。
from matplotlib.figure import Figure
from matplotlib.backends.backend_qtagg import FigureCanvasQTAgg
FigureCanvasQTAgg 既是 Matplotlib 的 canvas,又能作為 Qt Widget 加入 UI 中顯示圖表。所以我們不必先把 mapping 儲存成 PNG,再交給 QLabel 顯示,而是可以直接將 Matplotlib 畫布嵌入 PySide6。
以下用流程圖表示 Matplotlib 與 PySide6 的整合關係:

我們可以將 canvas 想像成電視螢幕。因此我們要在 UI 設計時就先決定圖表要放在哪,就像買電視前也要評估家裡電視櫃的大小。
UI 設計時,我們先在 Qt Designer 的 mapping 頁面預留一個普通的 QWidget,將 objectName 設為 plot_container。
這個 widget 是空的,目的是提供放置 canvas 的空間,我們會在 python 程式中將 FigureCanvasQTAgg 放在此 widget 中。

接著建立延續先前手法建立一個 class WaferMapView 負責管理 Matplotlib 圖表。__init__ 內的流程如下:
建立 Figure:準備保存 Matplotlib 圖表內容的物件。
建立 Canvas:將 Figure 關聯至 Qt 可以管理的繪圖畫布。
加入 Layout:在 plot_container 中建立 QVBoxLayout,並透過 addWidget() 放入 Canvas。
其中,setContentsMargins(0, 0, 0, 0) 是為了移除 Layout 的內邊距,讓 mapping 圖盡量填滿預留區域。
from matplotlib.backends.backend_qtagg import FigureCanvasQTAgg
from matplotlib.figure import Figure
from PySide6.QtCore import QObject
from PySide6.QtWidgets import QWidget, QVBoxLayout
class WaferMapView(QObject):
def __init__(self, container: QWidget) -> None:
super().__init__(container)
# 建立 Matplotlib Figure
self.figure = Figure(facecolor="white")
# 建立 Qt 相容的 Canvas
self.canvas = FigureCanvasQTAgg(self.figure)
# 將 Canvas 放入預留的 QWidget
layout = QVBoxLayout(container)
layout.setContentsMargins(0, 0, 0, 0)
layout.addWidget(self.canvas)
最後,在 Controller 中建立管理器:
# controller 內
self.map_view = WaferMapView(ui_window.plot_container)
這樣一來,Qt Designer 負責介面位置,WaferMapView 負責畫布建立,Controller 不需要親自處理 FigureCanvas 的初始化。
WaferMapView 繼承 QObject 是為了發射訊號,在 day 29 會有更詳細說明
前面已經將 FigureCanvasQTAgg 加入 GUI,但此時的畫布還沒有實際的 wafer mapping。
畫布既然已加進 UI 中,後續的圖面繪製便與 day 6~7 相同。我們透過以下 function 將繪圖與畫面更新串接起來:
# WaferMapView 內
def draw_map(
self, wafer_id, item, map_data, scale, show_grid
):
self.clear() #清除先前繪製的圖表
self.wafer_id, self.item = wafer_id, item
self.map_data, self.scale = map_data, scale
# 在 Figure 中建立新的 Axes
self.axes = self.figure.add_subplot(111)
# 呼叫封裝好的 pcolormesh() 繪圖功能
self.mesh = self._draw_mesh(
self.figure, self.axes, map_data, scale
)
self.axes.set_title(f"{wafer_id}: {item} mapping") # 設定圖表標題
self.set_grid_visible(show_grid) # 顯示或關閉格線
# 通知 Canvas 更新畫面
self.canvas.draw_idle()
這個 function 的 input 包含 wafer ID、測項、mapping 資料、色階與格線設定,主要完成四件事:
清除舊圖:透過 clear() 清除前一次的 mapping
建立 Axes:使用 add_subplot(111) 建立新的繪圖區域
繪製 Mapping:呼叫 _draw_mesh() 產生 pcolormesh() 圖形,並設定標題與格線
更新 Canvas:呼叫 draw_idle(),讓修改後的 Figure 重新呈現在 GUI 中
其中 draw_idle() 可以要求 Matplotlib 在 GUI 事件迴圈適當的時機重繪圖。也因為 mapping 是直接更新於 FigureCanvas 之中,因此它不會回傳任何圖片。
另外 canvas 只需要在初始化時建立一次,canvas 就像電視只負責顯示不負責內容。後續切換 wafer 或測項時重新在 axes 繪製 mapping 即可,不必再次建立 QWidget、layout 或 canvas。就像我們要看其他台時只要轉台就好,而不是重買一台電視。
最後,我們一樣要將繪圖流程串入 controller 之中。Controller 中的繪圖流程設定如下:
確認頁面狀態:只有使用者正在 Mapping 繪圖頁面時才執行,避免不必要的更新。
取得分析對象:讀取目前的 wafer ID 與測項,清除舊圖,並確認資料是否存在。
準備資料與色階:呼叫 prepare_wafer_map() 整理二維資料,再由 calculate_scale() 計算色階。
更新 GUI:將資料、色階及格線設定傳給 WaferMapView.draw_map()。
管理異常與操作狀態:計算失敗時顯示 QMessageBox;繪圖成功後才啟用圖片儲存按鈕。
另外可以看到下方 function 有 bn_save_img.setEnabled 設定按鈕能不能用,這個目的是為了避免使用者在繪圖失敗時誤觸存檔鈕。這樣靈活的連接功能也是 QT 的優勢。
# controller 中
def refresh_mapping(self, *_args) -> None:
"""使用當前選項更新可見的 Mapping 圖表.
Args:
*_args: Qt 訊號傳入的索引等額外引數, 本方法不使用.
"""
# 僅在繪圖頁面才執行重繪流程
ui = self.ui_window
if (ui.stackedWidget.currentWidget() is not ui.page_mapping
or ui.tab_map.currentWidget() is not ui.tab_map_view):
return
wafer_id, item = ui.cb_map_wafer.currentText(), ui.cb_map_item.currentText()
self.map_view.clear()
ui.bn_save_img.setEnabled(False) # 停用圖片儲存功能
if wafer_id not in self.wafer_data:
return
# 整理資料並繪製,失敗時保留空白圖
try:
data = prepare_wafer_map(self.wafer_data[wafer_id], item)
scale = calculate_scale(data.values, self.map_specs[item])
self.map_view.draw_map(wafer_id, item, data, scale,
ui.check_show_grid.isChecked())
except (ValueError, KeyError) as error:
QMessageBox.warning(ui.centralwidget, "無法繪製 Mapping", str(error))
return
ui.bn_save_img.setEnabled(True) # 若繪製流程正常則啟用圖片儲存功能
當我們設定好 function 後,我們就要設定觸發這 function 的時機。我們設定的時機如下:
使用者在繪圖頁面時,才更新圖面
使用者切換 Wafer 或測項時,重新整理資料並更新 Mapping
關於頁面的判定已在上面的 function 中加入。而切換項目的部分我們使用 day 15~16 介紹的 Signal & Slot,將下拉選單的變更訊號連接到 refresh_mapping(),因此只要選項改變就會觸發。
self.map_view = WaferMapView(ui_window.plot_container)
ui.cb_map_wafer.currentIndexChanged.connect(
self.refresh_mapping
)
ui.cb_map_item.currentIndexChanged.connect(
self.refresh_mapping
)
Controller 負責決定何時畫、畫哪一片 wafer;WaferMapView 則負責如何繪製及顯示**。**因此 controller 中的 refresh_mapping() 不會直接呼叫 pcolormesh(),也沒有自行管理 FigureCanvas。Controller 只負責工作流程,資料整理交給 mapping_cal.py,圖表顯示交給 WaferMapView.py。
這正好呼應我們一直強調的 Function 介面與模組職責分離。
這次我們從 Figure、Axes、Canvas 與 Backend 的關係開始,透過 FigureCanvasQTAgg 將 Matplotlib 嵌入 PySide6,並由 WaferMapView 管理圖表的建立與更新。現在 user 已經可以直接在 GUI 中切換 wafer、測項及色階,不必再另外開啟 Matplotlib 視窗。
不過實務上當我們看到失效,除了看失效在 mapping 圖上的分布以外,我們還會想看晶粒的數值、它到底是失效的多慘。
因此下一篇我們會透過 Matplotlib 的滑鼠事件以及 Signal & Slot,實作滑鼠滑動在 mapping 圖上取值的效果,讓我們在遇到異常分布時可以更及時的做探索分析與討論。