PyQt5與圖表

      在〈PyQt5與圖表〉中尚無留言

PyQt5 與Matplotlib

首先, 使用qtdesigner產生一個QFrame, 然後在QFrame裏產生一個QVBoxLayout. 此Layout使用addWidget將Matplotlib圖表加入. 

#在QFrame裏產生QVBoxLayout
self.vlayout = QVBoxLayout(self.frame)

#產生FigureCanvas圖表
self.graph = PlotBar(self, width=5, height=4, data=data)

#使用addWidget加入圖表
self.vlayout.addWidget(self.graph)

#使用removeWidget移除圖表
self.vlayout.removeWidget(self.graph)

Matplotlib圖表則為FigureCanvas的子類別, 使用此類別需先import FigureCanvasQTAgg.
代碼中, 使用Figure產生一圖表物件fig, 然後使用FigureCanvas的建構子將fig與FigureCanvas產生關連.
fig.add_subplot定義繪制的區域axes. 緊接著把資料傳入axes即可.
最後由FigureCanvas的draw()方法顯圖

from PyQt5.QtWidgets import QSizePolicy
from matplotlib.backends.backend_qt5agg import FigureCanvasQTAgg as FigureCanvas
from matplotlib.figure import Figure
class PlotBar(FigureCanvas):
def __init__(self, parent=None, width=5, height=4, dpi=100, data=None):
if data is None : self.data=[0]
else : self.data=data
fig = Figure(figsize=(width, height), dpi=dpi)
self.axes = fig.add_subplot(111)
FigureCanvas.__init__(self, fig)
self.setParent(parent)
FigureCanvas.setSizePolicy(self,
QSizePolicy.Expanding,
QSizePolicy.Expanding)
FigureCanvas.updateGeometry(self)
self.plot()
def plot(self):
x=list(range(1, len(self.data)+1))
self.axes.set_xlim([0, len(self.data)+1])
self.axes.bar(x, self.data)
self.axes.set_title('PyQt Matplotlib Bar')
self.draw()

Gps完整代碼如下

from PyQt5.QtCore import QThread, pyqtSignal
import random
import numpy as np

class Gps(QThread):
    callback=pyqtSignal(object)#跟MainWindow溝通的電話
    def __init__(self, parent=None):
        super().__init__(parent)
        self.runFlag=True
    def run(self):
        while self.runFlag:
            count=random.randint(2,20)#抓到幾顆衛星的訊號
            data=np.random.randint(1,20,count)
            self.callback.emit(data)
            self.msleep(1000)

PlotBar完整代碼如下

from PyQt5.QtWidgets import QSizePolicy
#需手動寫入qt5agg套件
from matplotlib.backends.backend_qt5agg import FigureCanvasQTAgg as FigureCanvas
from matplotlib.figure import Figure

#Canvas : 畫布
#PyQT支援將 TkInter 制成的 matplotlib 崁入
class PlotBar(FigureCanvas):
    def __init__(self, parent=None, width=5, height=4, dpi =100):
        #dpi : dot per inch:每英吋的點數,也就是解析度
        self.fig=Figure(figsize=(width, height), dpi=dpi)
        self.ax=self.fig.add_subplot(1,1,1)
        FigureCanvas.__init__(self, self.fig)
        self.setParent(parent)#設定 PlotBar的老爸為 MainWindow
        FigureCanvas.setSizePolicy(self,
                QSizePolicy.Expanding,
                QSizePolicy.Expanding)
        FigureCanvas.updateGeometry(self)
        #self.plot()
    def plot(self):
        self.ax.clear()
        x=list(range(1, len(self.data)+1))
        self.ax.set_xlim(0, len(self.data)+1)
        self.ax.bar(x,self.data)
        self.draw()#進行更新
    def refresh(self, data):
        self.data=data
        self.plot()

MainWindow完整代碼如下

import sys
from PyQt5.QtWidgets import QMainWindow, QApplication, QVBoxLayout
from Gps import Gps
from PlotBar import PlotBar
from ui.ui_mainwindow import Ui_MainWindow
import numpy as np

class MainWindow(QMainWindow, Ui_MainWindow):
    def __init__(self, parent=None):
        super().__init__(parent)
        self.setupUi(self)
        #frame裏有一個 layout, 再將物件加入layout中
        self.vlayout=QVBoxLayout(self.frame)
        self.bar=PlotBar(parent=self)
        self.vlayout.addWidget(self.bar)
        data=np.random.randint(1,20,10)
        self.bar.refresh(data)
        self.flag=False#False表示還沒開始執行,抽像化,用一個不相干的東西來表達另一個東西的狀況
        self.btn.clicked.connect(self.btn_click)
    def btn_click(self, event):
        self.flag=not self.flag
        if self.flag:
            self.btn.setText("停止")
            self.thread=Gps()
            self.thread.callback.connect(self.draw)
            self.thread.start()
        else:
            self.btn.setText("開始")
            self.thread.runFlag=False
    def draw(self, data):
        self.bar.refresh(data)
if __name__=='__main__':
    app=QApplication(sys.argv)
    mainWindow=MainWindow()
    mainWindow.show()
    app.exec()#進入視窗監控的執行緒

PyQt5 與plotly

plotly及plotly-express使用JavaScript將圖表顯示於瀏覽器上, 所以如果想在PyQt5也顯示plotly的圖表, 勢必要在PyQt5崁入瀏覽器. 

幸運的是, PyQt5可以使用QWebEngineView當成內崁的瀏覽器. 但QWebEngineView並不是PyQt5的內定Widget, 所以必需先安裝如下套件
pip install PyQt5 plotly plotly-express PyQtWebEngine

那要如何使用呢? 同樣先使用qtdesigner產生一個QFrame, QFrame中產生QVBoxLayout, 然後使用QWebEngineView產生一個 browser物件, 將之塞入layout 即可. 

至於browser中的內容, 使用 browser.load(QUrl(‘網址’)), 即可載入網頁內容

import sys
from PyQt5.QtCore import QUrl
from PyQt5.QtWebEngineWidgets import QWebEngineView
from PyQt5.QtWidgets import QMainWindow, QVBoxLayout, QApplication, QSizePolicy
from ui.ui_mainwindow import Ui_MainWindow
class MainWindow(QMainWindow, Ui_MainWindow):
def __init__(self, parent=None):
super(MainWindow, self).__init__(parent)
self.setupUi(self)
self.graph = None
self.vlayout = QVBoxLayout(self.frame)
self.browser=QWebEngineView()
self.vlayout.addWidget(self.browser)
self.browser.load(QUrl('http://mahaljsp.asuscomm.com'))
if __name__ == "__main__":
app = QApplication(sys.argv)
mainwindow = MainWindow()
mainwindow.showMaximized()
app.exec()

QWebEngineView 無法真正取代現行的IE or Chrome, 它只是一個實驗性質的web 解析器, 許多新的功能並不支援. 比如它就無法顯示本站首頁的htm5直播影像

好了, 既然有地方顯示網頁的內容了, 那就可以使用plotly產生的 html 來顯示內容了.

底下我們使用以前曾作過的各洲人民平圴壽命及GPD的關係動畫圖. 但這邊跟以往的繪圖有點不一樣. 以前使用 px.scatter等產生fig圖表後, 會交由 plotly.offline(fig, filename, auto_open)方法來顯示. 而現在則不需使用plotly了, 直接利用fig.to_html產生html內容, 再利用browser.setHtml()載入html內容

在下面代碼中, 有一行fig.to_html(include_plotlyjs=’cdn’), 其中cdn表示要將plotly.js檔一併變成輸出
依官方原文說明, 其值可以為True, False, ‘directory’, ‘require’. 經測試, 只有 ‘cdn’有效

import sys
import plotly.express as px
from PyQt5.QtWebEngineWidgets import QWebEngineView
from PyQt5.QtWidgets import QMainWindow, QVBoxLayout, QApplication, QSizePolicy
from ui.ui_mainwindow import Ui_MainWindow
class MainWindow(QMainWindow, Ui_MainWindow):
def __init__(self, parent=None):
super(MainWindow, self).__init__(parent)
self.setupUi(self)
self.graph = None
self.vlayout = QVBoxLayout(self.frame)
self.browser=QWebEngineView()
self.vlayout.addWidget(self.browser)
self.btnStart.clicked.connect(self.btnStart_click)
def btnStart_click(self):
df = px.data.gapminder()
fig = px.scatter(df, x='gdpPercap', y='lifeExp', color='continent',
size='pop', size_max=60, hover_name='country',
animation_frame='year', animation_group='country',
range_x=[100,100000], range_y=[25,90], log_x=True)
self.browser.setHtml(fig.to_html(include_plotlyjs='cdn'))
if __name__ == "__main__":
app = QApplication(sys.argv)
mainwindow = MainWindow()
mainwindow.showMaximized()
app.exec()

台灣黃金儲摺的完整代碼

import sys
from PyQt5.QtCore import QThread, pyqtSignal
from PyQt5.QtWebEngineWidgets import QWebEngineView
from PyQt5.QtWidgets import QMainWindow, QVBoxLayout, QApplication, QSizePolicy
from ui.ui_mainwindow import Ui_MainWindow
import mysql.connector as mysql
from datetime import datetime
import numpy as np
import plotly.graph_objects as go
class GoldThread(QThread):
callback=pyqtSignal(object)
def __init__(self, parent=None):
super().__init__(parent)
self.runFlag = True
def run(self):
conn = mysql.connect(host="ip", user="account", password="pwd", database="db")
d = datetime.now()
cmd = "select * from taiwan_bank_gold order by gd_date"
cursor = conn.cursor()
cursor.execute(cmd)
rows = cursor.fetchall()
x = range(len(rows))
sale = []
buy = []
dates = []
for row in rows:
dates.append(row[1])
buy.append(row[2])
sale.append(row[3])
f = np.poly1d(np.polyfit(x, sale, 15))
reg = f(x)
fig = go.Figure()
fig.add_trace(go.Scatter(x=dates, y=sale, mode='lines', name='黃金賣出', line=dict(color='royalblue', width=2)))
fig.add_trace(go.Scatter(x=dates, y=buy, mode='lines', name='黃金買進', line=dict(color='green', width=2)))
fig.add_trace(go.Scatter(x=dates, y=reg, mode='lines', name='趨勢線', line=dict(color='orange', width=2)))
fig.update_layout(
dragmode="pan",
title_text="台灣黃金存摺歷史價格",
xaxis=go.layout.XAxis(
rangeselector=dict(
buttons=list([
dict(count=1,
label="1 month",
step="month",
stepmode="backward"),
dict(count=6,
label="6 month",
step="month",
stepmode="backward"),
dict(count=1,
label="1 year",
step="year",
stepmode="backward"),
dict(count=1,
label="1 day",
step="day",
stepmode="todate"),
dict(step="all")
])
),
rangeslider=dict(
visible=True
),
range=[datetime(d.year, 1, 1), datetime(d.year, d.month, d.day)],
type="date"
)
)
self.callback.emit(fig)

class MainWindow(QMainWindow, Ui_MainWindow):
def __init__(self, parent=None):
super(MainWindow, self).__init__(parent)
self.setupUi(self)
self.graph = None
self.vlayout = QVBoxLayout(self.frame)
self.browser=QWebEngineView()
self.vlayout.addWidget(self.browser)
self.btnStart.clicked.connect(self.btnStart_click)
def refresh(self, fig):
self.browser.setHtml(fig.to_html(include_plotlyjs='cdn'))
def btnStart_click(self):
self.thread=GoldThread()
self.thread.callback.connect(self.refresh)
self.thread.start()
if __name__ == "__main__":
app = QApplication(sys.argv)
mainwindow = MainWindow()
mainwindow.showMaximized()
app.exec()

發佈留言

發佈留言必須填寫的電子郵件地址不會公開。 必填欄位標示為 *