網頁

2019年2月28日 星期四

Python3.6+PyQt5+Matplotlib3+mpl-finance0.10 整合應用

學習了如何利用tkinter來寫桌面應用程式後才知道為什麼tkinter沒有像 Microsoft Visual Studio 那種 GUI Designer , 因為tkinter的概念比較簡單 , 只要搞懂 Frame 的觀念大概就可以搞定tkinter的GUI設計了 , 程式撰寫的部分也相對簡單容易 , Menu選單子項目的切換就只是 Frames之間 的切換而已 , 對於只是需要簡單GUI應用的人來說tkinter就綽綽有餘了.


最近又開始學習另外一個支援Python GUI 程式開發的套件 - PyQt , 它有附一個GUI Designer , 不過只要有了 PyQt 基本常用的程式碼後 , 就不太需要再用到它的Designer了 , 有趣的是 , 我用 C# 開發出一個GUI應用程式 , 我還是不懂怎麼寫 C# GUI相關的程式(因為我不需要懂) , 但是用Python開發GUI應用程式剛好相反 .

一開始接觸PyQt感覺好像比tkinter難 , 事實上PyQt的概念的確也比tkinter複雜 , 經過幾天邊看PyQt的說明文件邊實作練習 , 再加上不斷上網Google , 總算拼湊出我要的做的東西 , 首先我選擇QMainWindow來做我的Python GUI應用程式的主窗口 , 上面要有 MenuBar 跟 StatusBar .

一開始遇到的是MenuBar中子選單切換的問題 , 在tkinter中Menu子選單的切換就是在多個Frame中做切換 , 在PyQt中那就是在多個QMainWindow中做切換 , 程式的寫法比tkinter麻煩且複雜 , 而且要瞭解PyQt的layout(布局)概念及運作方式 , 你可以把layout當作一個容器(Container) , layout中可以擺放物件(object)及Widget , 也可以再放layout , 透過各種不同layout的交互堆疊來達到排版的目的 .

以下是主程式的部分 :

import sys
from PyQt5 import QtCore, QtGui, QtWidgets
from PyQt5.QtWidgets import QMainWindow
from PyQt5.QtGui import QIcon
import AFDTS, Ticks2K5

#-----------------------------------------------------------------------------
class Ui_MainWindow(object):
    def setupUi(self, MainWindow):
     
        MainWindow.setObjectName("MainWindow")
        MainWindow.resize(1024, 768)
     
        icon = QtGui.QIcon()
        icon.addPixmap(QtGui.QPixmap("afdts.png"),
                       QtGui.QIcon.Normal, QtGui.QIcon.Off)
        MainWindow.setWindowIcon(icon)
     
        self.centralwidget = QtWidgets.QWidget()
        self.centralwidget.setObjectName("centralwidget")
        self.centralwidget.setGeometry(QtCore.QRect(0, 0, 1024, 768))
        self.gridLayout = QtWidgets.QGridLayout(self.centralwidget)
        self.setCentralWidget(self.centralwidget)
     
        self.menubar = QtWidgets.QMenuBar(MainWindow)     
        self.menubar.setGeometry(QtCore.QRect(0, 0, 1024,25))
        self.menubar.setDefaultUp(False)
        self.menubar.setNativeMenuBar(False)
        self.menubar.setObjectName("menubar")
     
        self.menuTrade = QtWidgets.QMenu(self.menubar)
        self.menuTrade.setObjectName("menuTrade")
        self.menuTool = QtWidgets.QMenu(self.menubar)
        self.menuTool.setObjectName("menuTool")
        MainWindow.setMenuBar(self.menubar)
     
        self.actionAFDTS = QtWidgets.QAction(MainWindow)
        self.actionAFDTS.setObjectName("actionAFDTS")
        self.actionTicks2K5 = QtWidgets.QAction(MainWindow)
        self.actionTicks2K5.setObjectName("actionTicks2K5")
        self.actionClose = QtWidgets.QAction(MainWindow)
        self.actionClose.setObjectName("actionClose")
        self.actionClose.triggered.connect(MainWindow.close)
     
        self.menuTrade.addAction(self.actionAFDTS)
        self.menuTrade.addSeparator()
        self.menuTrade.addAction(self.actionClose)
     
        self.menuTool.addAction(self.actionTicks2K5)
     
        self.menubar.addAction(self.menuTrade.menuAction())
        self.menubar.addAction(self.menuTool.menuAction())
     
        self.statusbar = QtWidgets.QStatusBar(MainWindow)
        self.statusbar.setObjectName("statusbar")
        MainWindow.setStatusBar(self.statusbar)
        self.statusbar.showMessage('我準備好了 !')
     
        self.retranslateUi(MainWindow)
        QtCore.QMetaObject.connectSlotsByName(MainWindow)

    def retranslateUi(self, MainWindow):
        _translate = QtCore.QCoreApplication.translate
        MainWindow.setWindowTitle(_translate(
                "MainWindow", "台指期即時自動當沖交易系統 (第三代)"))
        self.menuTrade.setTitle(_translate("MainWindow", "交易"))
        self.menuTool.setTitle(_translate("MainWindow", "工具"))
        self.actionAFDTS.setText(_translate("MainWindow",
                                            "台指期即時自動當沖交易"))
        self.actionTicks2K5.setText(_translate("MainWindow",
                                               "用原始資料畫K線圖"))
        self.actionClose.setText(_translate("MainWindow", "結束程式"))
 
#-----------------------------------------------------------------------------
class Main(QMainWindow, Ui_MainWindow):
    def __init__(self):
        super(Main,self).__init__()
        self.setupUi(self)
        self.child1 = Afdts()
        self.child2 = Ticks2k5()     
        self.actionAFDTS.triggered.connect(self.Afdts)
        self.actionTicks2K5.triggered.connect(self.Ticks2k5)
    def Afdts(self):     
     
        for i in reversed(range(self.gridLayout.count())):
            self.gridLayout.itemAt(i).widget().setParent(None)
     
        self.gridLayout.addWidget(self.child1)
        self.child1.show()
    def Ticks2k5(self):
     
        for i in reversed(range(self.gridLayout.count())):
            self.gridLayout.itemAt(i).widget().setParent(None)
       
        self.gridLayout.addWidget(self.child2)
        self.child2.show()
     
class Afdts(QMainWindow, AFDTS.AFDTS):
    def __init__(self):
        super(Afdts,self).__init__()
        self.setupUi(self)
     
class Ticks2k5(QMainWindow, Ticks2K5.TicksToK5):
    def __init__(self):
        super(Ticks2k5,self).__init__()
        self.setupUi(self)
             
#-----------------------------------------------------------------------------
if __name__ == "__main__":
    app = QtWidgets.QApplication(sys.argv)
    afdts3 = Main()
    afdts = Afdts()
    afdts3.setWindowIcon(QIcon('afdts.png'))
    afdts3.show()
    sys.exit(app.exec_())


在主程式中最重要的是下面的程式碼 :


        for i in reversed(range(self.gridLayout.count())):
            self.gridLayout.itemAt(i).widget().setParent(None)
 
上面程式碼的作用是 , 當某一個MenuBar中的子項目(QMainWindow)被觸發時 , 先將之前的QMainWindow的Parent設成None , 以達到清除前一個QMainWindow的目的 , 因為我們的作法是在Parent QMainWindow產生一個gridLayout當作容器 , 來存放MenuBar中子項目的QMainWindow , 如果不把前一個QMainWindow先清除掉 , 當你觸發 MenuBar中的其他子項目時 , 你會同時看到兩個QMainWindow的內容呈現在主窗口中(因為PyQt會把gridLayout中的Widget全部呈現出來) , 要清除layout容器中的Widget必須採用後進先出(LastInFirstOut)的作法 , 由後往前清 , 所以才會用到reversed() , 以我的狀況來說 , gridLayout容器中只會有 [沒有Widget / 一個Widget] 這兩種狀況 , 上面那段程式的做法就是如果gridLayout中有Widget就從後面開始清 , 如果沒有就不會清 . 程式執行後的畫面如下 :




其他MenuBar中子項目QMainWindow的class寫法可以參考下面的程式碼 :

# 匯入使用者介面(GUI)套件

from PyQt5 import QtCore, QtWidgets, QtGui
# 匯入資料處理套件
import pandas as pd
import xlrd
# 匯入畫K線圖套件
import mpl_finance as mpf
from mpl_finance import candlestick_ochl
# 匯入畫圖套件
import matplotlib.pyplot as plt
from matplotlib import dates as mdates
from matplotlib.backends.backend_qt5agg import FigureCanvasQTAgg as FigureCanvas
from matplotlib.backends.backend_qt5 import NavigationToolbar2QT as NavigationToolbar
from matplotlib.gridspec import GridSpec
# 讓 matplotlib 可以顯示中文
plt.rcParams['font.sans-serif'] = ['Microsoft JhengHei']
plt.rcParams['axes.unicode_minus'] = False

# ----------------------------------------------------------------------------

# 用原始資料畫K線圖 的 類別及函式
class TicksToK5(object):
     
    def setupUi(self, MainWindow):
     
        self.setObjectName("MainWindow")
        self.resize(1024, 768)
     
        self.centralwidget = QtWidgets.QWidget()
        self.centralwidget.setObjectName("centralwidget")
        self.setCentralWidget(self.centralwidget)
             
        self.main_layout = QtWidgets.QVBoxLayout()
     
        self.vbox1 = QtWidgets.QVBoxLayout()
        self.vbox1.setAlignment(QtCore.Qt.AlignTop)
     
        self.vbox2 = QtWidgets.QVBoxLayout()
     
     
        self.hbox1 = QtWidgets.QHBoxLayout()
        self.hbox1.setAlignment(QtCore.Qt.AlignCenter)
     
        self.hbox2 = QtWidgets.QHBoxLayout()
        self.hbox2.setAlignment(QtCore.Qt.AlignCenter)
     
     
        self.vbox1.addLayout(self.hbox1)
        self.vbox1.addLayout(self.hbox2)
     
        self.main_layout.addLayout(self.vbox1)
        self.main_layout.addLayout(self.vbox2)
     
        self.centralwidget.setLayout(self.main_layout)
     
        self.lb_func_title = QtWidgets.QLabel("用原始資料畫K線圖", self)
        self.lb_func_title.setFont(QtGui.QFont('標楷體', 24))
        self.hbox1.addWidget(self.lb_func_title)     
     
     
        self.btn_pickup_file = QtWidgets.QPushButton("選擇檔案", self)
        self.btn_pickup_file.clicked.connect(self.ChosseFile)
     
        self.hbox2.addWidget(self.btn_pickup_file)
     
        self.lb_selected_file = QtWidgets.QLabel("已選取的檔案 : ", self)
     
        self.hbox2.addWidget(self.lb_selected_file)
     
        self.le_selected_file = QtWidgets.QLineEdit(self)
     
        self.hbox2.addWidget(self.le_selected_file)
     
        self.btn_draw_kline = QtWidgets.QPushButton("畫K線圖", self)
        self.btn_draw_kline.clicked.connect(self.DrawKLine)
     
        self.hbox2.addWidget(self.btn_draw_kline)
     
        self.statusbar = QtWidgets.QStatusBar()
        self.statusbar.setObjectName("statusbar")
        self.setStatusBar(self.statusbar)
        self.statusbar.showMessage('我準備好了 !')
     
        QtCore.QMetaObject.connectSlotsByName(self)

    def ChosseFile(self):
     
        self.source_file, self.file_type = QtWidgets.QFileDialog(
                ).getOpenFileName(self, "選取檔案", "D:\HistoryData\MXF")
        self.le_selected_file.setText(str(self.source_file))
     
    def DrawKLine(self):
     
        # 從 Excel 讀入 ticks row data
        fileName = str(self.source_file)
        product = fileName[-17:-14]
        title = fileName[-13:-5]
        date_title = title[:4] + '/' + title[4:6] + '/' + title[6:]
     
        if product == 'MXF' :
            date_title = '小型台指期 - ' + date_title
        else :
            date_title = '台指期 - ' + date_title
         
        book = xlrd.open_workbook(fileName)
        sheet = book.sheet_by_index(0)

        # 先開好一個空的 DataFrame
        ticks = pd.DataFrame(columns=['date', 'open', 'close', 'high', 'low',
                              'volume', 'time'])

        for row in range(sheet.nrows):
            timesecond = sheet.cell_value(row,1)
            price = sheet.cell_value(row,2) / 100
            volume = sheet.cell_value(row,3)

            newRow = [[timesecond, price, price, price, price,
                       volume, timesecond]]
            ticks = ticks.append(pd.DataFrame(newRow, columns=['date', 'open',
                                                       'close', 'high',
                                                       'low', 'volume',
                                                       'time'
                                                       ]), ignore_index=True)

     
        ticks['date'] = pd.to_datetime(ticks['date'], unit='s')
        ticks['time'] = pd.to_timedelta(ticks['time'], unit='s')
        ticks.index = ticks['date']
     
        k5 = ticks.resample('5T', closed='left', label='left').apply({
                'open':'first', 'close':'last', 'high':'max',
                'low':'min', 'volume':'sum', 'time':'first'})

        k5.reset_index(inplace=True)

        k5['date'] = k5['date'].apply(mdates.date2num)
     
# 用 plt.Figure() , 不是 plt.figure()
        fig = plt.Figure(figsize=(15,8))
     
# 用 GridSpec 設定 fig 為 3列 * 1行
        gs = GridSpec(3, 1, figure=fig)

        # ax 會佔用上兩列的空間
        ax = fig.add_subplot(gs.new_subplotspec((0, 0), rowspan=2))
     
        candlestick_ochl(ax, k5.values, width=0.001, colorup='#ff1717',
                 colordown='#53c156', alpha=10.0)

        ax.set_title(date_title)
        ax.set_ylabel('指數')
        ax.set_xticks(k5['date'])
        ax.set_xticklabels(k5['time'], rotation=30, ha='right',
                           fontsize='small')

        ax.grid(True)

        # ax2 會佔用最下面那列的空間
        ax2 = fig.add_subplot(gs[-1, :])
     
        mpf.volume_overlay(ax2, k5['open'],
                           k5['close'], k5['volume'],
                           width=0.3, colorup='#ff1717',
                           colordown='#53c156', alpha=1.0)

        ax2.set_ylabel('成交量')
        ax2.set_xticks(range(0, len(k5['date'])))
        ax2.set_xticklabels(k5['time'], rotation=30, ha='right',
                            fontsize='small')
        ax2.set_xlabel('時間')
        ax2.grid(True)
     
        fig.subplots_adjust(bottom=0.2)
        fig.subplots_adjust(hspace=0.3)

# 在 PYQT5 中畫K線圖
        self.canvas = FigureCanvas(fig)
        self.toolbar = NavigationToolbar(self.canvas, self)
       
        for i in reversed(range(self.vbox2.count())):
            self.vbox2.itemAt(i).widget().setParent(None)
           
        self.vbox2.addWidget(self.toolbar)   
        self.vbox2.addWidget(self.canvas)
       
        self.canvas.draw()

在上面程式的最後一樣用了清除layout容器中Widget的作法 , 這樣在同一個QMainWindow中才可以顯示不同日期的K線圖 , 否則不同的日期會一直從下面疊加進來 . 



最後還有一個小問題沒有解決 , 就是在子QMainWindow中的statusBar 訊息顯示的位置會稍微往上跑XD , 目前還沒找到原因 , 另外 , 要特別聲明 , 以上的解決方法或作法都是從網路上Google來的 , 感謝這些貢獻者 !!

沒有留言:

張貼留言