學習了如何利用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來的 , 感謝這些貢獻者 !!
沒有留言:
張貼留言