花幾個小時做一個看股票的摸魚神器



項目介紹

你還在為了不能及時看到股價發愁嗎?你還在為了上班偷看股票APP而擔心嗎?現在我們隆重推出新的一款Windows實時股票顯示應用,你看它簡潔的界面,豐富的功能,還支持貼邊自動隱藏,現在開始不要998,不要998,統統免費,只需要看本教程,你就可以自己做出這個應用,有興趣還可以擴展一下功能。我敢保證,你只要把這應用往桌面一放,那你的股票一定得連漲10天!!!

哈哈哈,上面略有誇張成分,接下來有興趣的小伙伴們一起來和我完成這款應用吧,所需要的軟件:

  • python3.7
  • PyQt5==5.15.4
  • pyqt5-plugins==5.15.4.2.2
  • pyqt5-tools==5.15.4.3.2

代碼可以參考我的github

先從界面開始吧

我們先建立自己的工程目錄,工程名就命名為RTStock吧,翻譯過來就是實時股票(Real Time Stock)。然后我們在工程目錄中添加文件Window.py,它就作為我們的界面文件,同時也是應用的主入口。

Window.py中添加類Window,該類繼承了QWidget,我們為該類先定義兩個方法,第一個方法是構造方法__init__(類實例化時第一個調用的函數),該方法有5個參數:

  • self,指向自己的引用
  • screen_width,屏幕的寬
  • screen_hight,屏幕的長
  • window_width,窗口的寬
  • window_height,窗口的長

我們用這些參數將窗口設置到屏幕中央,並調用我們的提供的第二個函數initUI,該函數負責將界面豐富化,並且顯示出界面。同時,我們還要提供主函數,在主函數中實例化一個Window類,同時要實例化一個QApplication類,該類提供了exec_函數使得界面可以等待進行交互式操作。下面給出了實現代碼及需要導入的包:

import sys
from PyQt5.QtCore import Qt, QRect
from PyQt5.QtWidgets import QWidget, QApplication

class Window(QWidget):
	"""docstring for Window"""
	def __init__(self, screen_width, screen_height, window_width, window_height):
		super().__init__()
		self.setGeometry(QRect(	screen_width / 2 - window_width / 2,
								screen_height / 2 - window_height / 2,
								window_width,
								window_height
								))
		self.screen_width = screen_width
		self.screen_height = screen_height
		self.window_width = self.frameGeometry().width()
		self.window_height = self.frameGeometry().height()

		self.initUI()

	def initUI(self):
		self.show()

if __name__ == '__main__':

	app = QApplication(sys.argv)
	
	screen_width = app.primaryScreen().geometry().width()
	screen_height = app.primaryScreen().geometry().height()

	window = Window(screen_width, screen_height, 800, 300)

	sys.exit(app.exec_())

運行代碼可以得到下面的界面:

接着我們對窗口做一些美化,例如修改透明度,去邊框,光標樣式等等,在initUI中添加下面的代碼:

def initUI(self):
		self.setWindowTitle('RTStock') # set title
		self.move(100, 100) # modify the position of window
		self.setWindowFlags(Qt.FramelessWindowHint | Qt.WindowStaysOnTopHint) # remove frame and keep it always at the top
		self.setWindowOpacity(0.8) # set the transparency of the window
		self.setCursor(Qt.PointingHandCursor) # set the style of the cursor
        
        self.show()

再次運行一下可以得到更漂亮的窗口(其實就是一張半透明的白紙,哈哈哈~):

然后,我們對窗口填充一些組件用於展示我們想要的信息,在本文中,我將該界面分為三塊:

  • 頭部放一個logo
  • 中間放一張表,去展示信息
  • 尾部是一個時間戳,展示的數據最后一次更新的時間

代碼如下:

# some acquired packages
from PyQt5.QtWidgets import QTableWidgetItem, QAbstractItemView, QHeaderView
from PyQt5.QtWidgets import QLabel, QTableWidget, QHBoxLayout 
from PyQt5.QtGui import QFont, QBrush, QColor

# add code in the function initUI
def initUI(self):
		...
        
		# the header of window (a label)
		self.HeadView = QLabel('RT Stock',self)
		self.HeadView.setAlignment(Qt.AlignCenter)
		self.HeadView.setGeometry(QRect(0, 0, self.window_width, 25))
		self.HeadView.setFont(QFont("Roman times",12,QFont.Bold))

		# the body of window (a table)		
		self.stockTableView = QTableWidget(self)
		self.stockTableView.setGeometry(QRect(0, 40, self.window_width, 220))
		self.stockTableView.verticalHeader().setVisible(False)  # remove the vertical header of the table
		self.stockTableView.setEditTriggers(QAbstractItemView.NoEditTriggers) # disable editing
		self.stockTableView.horizontalHeader().setSectionResizeMode(QHeaderView.Stretch) # let the size of horizontal header of the table stretch automatically 
		self.stockTableView.setShowGrid(False) # remove the grid of the table
		self.stockTableView.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOff) # remove the scroll bar of the table
		self.stockTableView.setColumnCount(5) # the table has 5 column(name, price, change rate, change, operation)
		self.stockTableView.setHorizontalHeaderLabels(['名稱↑↓','現價↑↓','漲跌幅↑↓','漲跌↑↓','操作'])

		# the tail of window (a label)
		self.timeView = QLabel('last update: ',self)
		self.timeView.setGeometry(QRect(0, 270, self.window_width, 20))

		self.show()

運行效果如下圖所示:

可以看到,我們的顯示區(表格)有5列,每一行顯示一只股票的現價情況,而操作一欄用於提供一些功能,例如將該只股票置頂或者刪除。現在的界面已經大致成型,接下來,我們將開始設計窗口的拖拽和隱藏功能。

讓我們的窗口動起來,還能隱藏哦

我們先來設計移動功能,也就是拖拽窗口。有同學可能會說,這個功能還用設計?不是一開始窗口就能被拖拽嗎?哈哈,那你就單純了,我們已經去掉了窗口邊框,而靠窗體是沒有辦法移動的,不信的話你試試。

說到底,拖拽窗口就三個步驟:

  • 在窗體中按下鼠標
  • 移動鼠標到目標位置
  • 釋放鼠標按鍵

因此我們只要將這三個事件對應的事件函數實現就好,即QWidgetmousePressEvent,mouseMoveEvent,mouseReleaseEvent事件函數,它們在鼠標做出相應動作后會被自動調用,下面是相關的實現代碼:

# add these functions in the class Window
def mousePressEvent(self, event):
	if event.button() == Qt.LeftButton: # press the left key of mouse
		self._startPos = event.pos() # record the start position of the cursor before moving

def mouseMoveEvent(self, event):
	self._wmGap = event.pos() - self._startPos # move distance = current position - start position
	final_pos = self.pos() + self._wmGap # the position where the window is currently supposed to be
		
	# The window should not be moved out of the left side of the screen.
	if self.frameGeometry().topLeft().x() + self._wmGap.x() <= 0:
		final_pos.setX(0)
	# The window should not be moved out of the top side of the screen.
	if self.frameGeometry().topLeft().y() + self._wmGap.y() <= 0:
		final_pos.setY(0)
	# The window should not be moved out of the right side of the screen.
	if self.frameGeometry().bottomRight().x() + self._wmGap.x() >= self.screen_width:
		final_pos.setX(self.screen_width - self.window_width)
	# The window should not be moved out of the below side of the screen.
	if self.frameGeometry().bottomRight().y() + self._wmGap.y() >= self.screen_height:
		final_pos.setY(self.screen_height - self.window_height)
	self.move(final_pos) # move window

def mouseReleaseEvent(self, event):
    # clear _startPos and _wmGap
    if event.button() == Qt.LeftButton:
        self._startPos = None
        self._wmGap = None
    if event.button() == Qt.RightButton:
        self._startPos = None
        self._wmGap = None

這時候,你再試着拖拽一下窗體,此時它變得可以移動了,而且由於代碼中做了邊界檢查,所以窗體是不會被移出屏幕的。接着我們再試試貼邊隱藏功能,這個功能其實也簡單,就是在窗體貼住屏幕邊界的時候,將其隱藏,如果你把鼠標移到窗體隱藏的地方,它又會彈出。要實現這個功能要考慮兩個問題:

  • 怎么檢查貼邊,在哪檢查?
  • 怎么隱藏,怎么彈出?

對於怎么彈出或者隱藏,我給出的方案是將窗口形狀改變為一個窄條,看起來就和藏起來一樣,例如窗口大小為800 * 300,那么右側貼邊就變成了10 * 300,上面貼邊就變成800 * 10(我們不考慮向下貼邊,下面是任務欄就不貼邊了吧,哈哈)。而這個變化過程可以使用QPropertyAnimation,我們先實現這個隱藏和彈出函數:

from PyQt5.QtCore import QPropertyAnimation

# add the function in the class Window
# "direction" indicate the direction the window hides or pops
def startAnimation(self, x, y, mode='show', direction=None):
    animation = QPropertyAnimation(self, b"geometry", self)
    animation.setDuration(200) # the duration of the moving animation
    if mode == 'hide':
        if direction == 'right':
            animation.setEndValue(QRect(x, y, 10, self.window_height))
        elif direction == 'left':
            animation.setEndValue(QRect(0, y, 10, self.window_height))
		else:
			animation.setEndValue(QRect(x, 0, self.window_width, 10))
	else:
		animation.setEndValue(QRect(x, y, self.window_width, self.window_height))
	# start show the animation that the shape of the window becomes the shape of EndValue
	animation.start()

然后,對於怎么檢查貼邊,我建議可以在鼠標移入和移除窗體后進行檢查,一般情況是這樣:

  • 窗口隱藏起來后,鼠標移入殘留的窄條中,窗體彈出
  • 窗口被移動到屏幕邊界,鼠標移除窗體后,窗口隱藏

我們用一個變量保存現在窗口的狀態(隱藏或者在外面),然后配和邊界檢查決定窗口怎么隱藏:

# add a variable indicate the status of window
def __init__(self, screen_width, screen_height, window_width, window_height):
    ...
    self.hidden = False # The window has not been hidden.
    self.initUI()
    
# add these functions in the class Window
# invoke automatically after entering windows
def enterEvent(self, event):
	self.hide_or_show('show', event)

# invoke automatically after leaving windows
def leaveEvent(self, event):
	self.hide_or_show('hide', event)

def hide_or_show(self, mode, event):
	pos = self.frameGeometry().topLeft()
    if mode == 'show' and self.hidden: # try to pop up the window
        # The window is hidden on the right side of the screen
        if pos.x() + self.window_width >= self.screen_width:
            self.startAnimation(self.screen_width - self.window_width, pos.y())
            event.accept()
            self.hidden = False
        # The window is hidden on the left side of the screen
        elif pos.x() <= 0:
            self.startAnimation(0, pos.y())
            event.accept()
            self.hidden = False
        # The window is hidden on the top side of the screen
        elif pos.y() <= 0:
            self.startAnimation(pos.x(), 0)
            event.accept()
            self.hidden = False
	elif mode == 'hide' and (not self.hidden): # try to hide the window
		# The window is on the right side of the screen
		if pos.x() + self.window_width >= self.screen_width:
            self.startAnimation(self.screen_width - 10, pos.y(), mode, 'right')
            event.accept()
            self.hidden = True
        # The window is on the left side of the screen	
        elif pos.x() <= 0:
            self.startAnimation(10 - self.window_width, pos.y(), mode, 'left')
            event.accept()
            self.hidden = True
		# The window is on the top side of the screen
        elif pos.y() <= 0:
            self.startAnimation(pos.x(), 10 - self.window_height, mode, 'up')
            event.accept()
            self.hidden = True

現在,你試着把窗口拖拽到屏幕最右邊,最上邊或者最左邊看看效果,你會發現窗口會自動”隱藏起來“。同樣在窗口隱藏后,你把鼠標放到殘留的窗體上,窗口會自動“彈出”。

到現在位置,我們本小節的任務就已經完成,接下來該接觸點股票(數據)相關的東西啦。

從哪爬點股票數據呢

為了能夠獲得股票的數據,我們就需要提供股票的代碼或者名稱,然后通過一些股票分析網站(例如新浪財經,同花順等等)提供的API去獲取該股票當前的行情。因此我們需要設計一個類,該類需要完成如下幾點功能:

  • 通過股票代碼獲取該只股票的當前價格以及開盤價
  • 通過股票名稱獲取該名稱對應股票的代碼

因此,我們先添加一個Fetcher.py文件到工程目錄中,該文件提供一個類Fetcher。為了能夠不被網站識別為非法爬蟲,我們還需要在類的構造函數__init__中設置用戶代理,順便一提我們使用的爬取網頁的庫是request,代碼如下:

class Fetcher:
	def __init__(self):     
        self.headers = {
            'user-agent' : 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/90.0.4430.212 Safari/537.36',
        }

然后,這里我們需要先普及一點股票的小知識,首先是股票代碼,它是由[market][code]格式組成。其次本文選擇的API是新浪財經的API接口,其形式如下:

http://hq.sinajs.cn/list=[market][code]

以貴州茅台sh600519為例通過訪問鏈接http://hq.sinajs.cn/list=sh600519,新浪服務器會返回如下格式的內容,含義請閱讀相關API文檔

var hq_str_sh600519="貴州茅台,1989.000,1980.000,1969.000,2006.000,1962.010,1969.000,1969.500,2978013,5902656221.000,4205,1969.000,100,1968.670,100,1968.660,100,1968.600,400,1968.220,100,1969.500,100,1969.580,300,1969.660,200,1969.680,300,1969.690,2021-07-21,15:00:00,00,";

因此根據這個API,我們通過闖入marketcode,那么我們就能得到其當前的價格,代碼如下:

import requests

# add the function in the class
def get_quotation(self, market : str, code : str) -> dict:
    try:
        rsp = requests.get(url, headers=self.headers)
        rsp = rsp.text.split('=')[1].strip('";\n').split(',')
        rsp = { 'name' : rsp[0],
               'open' : rsp[1],
               'close' : rsp[2],
               'price' : rsp[3],
               'high' : rsp[4],
               'low' : rsp[5],
               'bidding_buy' : rsp[6],
               'bidding_sell' : rsp[7],
               'count' : rsp[8],
               'amount' : rsp[9],
               'quote_buy' : rsp[10:20],
               'quote_sell' : rsp[20:30],
               'date' : rsp[30],
               'time' : rsp[31]
              }
        return rsp
    except Exception:
        print("check your network")
        return {}

我們先測試一下,在Fetcher.py文件最后添加下面的代碼:

# for testing Fetcher
if __name__ == '__main__':
	fetcher = Fetcher()
	print( fetcher.get_quotation('sh', '600519') )

運行這個文件,你如果得到了貴州茅台的股票信息,那么說明你代碼ok:

# result
{'name': '貴州茅台', 'open': '1989.000', 'close': '1980.000', 'price': '1969.000', 'high': '2006.000', 'low': '1962.010', 'bidding_buy': '1969.000', 'bidding_sell': '1969.500', 'count': '2978013', 'amount': '5902656221.000', 'quote_buy': ['4205', '1969.000', '100', '1968.670', '100', '1968.660', '100', '1968.600', '400', '1968.220'], 'quote_sell': ['100', '1969.500', '100', '1969.580', '300', '1969.660', '200', '1969.680', '300', '1969.690'], 'date': '2021-07-21', 'time': '15:00:00'}

對於獲取名字,我們使用另一個鏈接https://suggest3.sinajs.cn/suggest/key=[name],以貴州茅台為例,訪問https://suggest3.sinajs.cn/suggest/key=貴州茅台你可以得到下面的信息:

var suggestvalue="貴州茅台,11,600519,sh600519,貴州茅台,,貴州茅台,99,1";

可以看到,我們的股票代碼就在網頁信息當中,代碼如下:

import re

# add the function in the class
# input: name
# output: (market, code)
def get_market_and_code(self, name : str):
    url = 'https://suggest3.sinajs.cn/suggest/key=%s' % name
    try:
        rsp = requests.get(url, headers=self.headers)
        rsp = re.search('[a-z]{2}[0-9]{6}', rsp.text)

        if rsp:		
            return (rsp.group()[:2], rsp.group()[2:])
        else:
            return None
    except Exception:
        print("check your network")
        return None

添加一支你的自選股吧

怎樣添加一只自選股呢?我想的辦法是右擊窗口,然后彈出一個菜單欄,里面放些功能選項,例如添加一只自選股,或者退出程序。之后點擊”添加一只自選股“按鈕,彈出一個輸入框,這時你把要添加股票的名字輸進去就大功告成啦。接下來的部分我們介紹它的實現機制。

對於右擊彈出菜單欄我們可以使用PyQTQWidget自帶的相關機制。另外,我們需要為其添加兩個活動(功能項),即添加股票和退出,因此需要提供這兩個函數用於去綁定到相應的活動中。

退出函數可以什么都不用寫,暫時留個空殼子就好,而添加股票函數就比較麻煩一些,需要依次完成如下內容:

  • 彈出輸入框
  • 獲取輸入框中的股票名稱
  • 將股票名稱轉化為股票代碼
  • 把代碼保存下來

可以發現我們需要保存股票代碼,這些股票代碼都是我們的自選股。為了能夠將股票代碼的操作分離出來,我們設計了一個新的類OptionalStock,該類可以完成股票代碼的添加,刪除等操作。

新建一個文件OptionalStock.py,添加下面的代碼:

class OptionalStock(object):
	def __init__(self):
		self.stocks = []

	def add_stock(self, market : str, code : str) -> None:
		self.stocks.append({'market' : market,
							'code' : code,
							})

接下來,就可以設計我們的添加股票的函數了:

from PyQt5.QtWidgets import QMenu, QAction, QMessageBox, QInputDialog
from Fetcher import Fetcher
from OptionalStock import OptionalStock

class Window(QWidget):
	"""docstring for Window"""
	def __init__(self, screen_width, screen_height, window_width, window_height):
		...
		self.stocks = OptionalStock()
		self.initUI()
        
	def initUI(self):
		...
		# menu
		self.setContextMenuPolicy(Qt.ActionsContextMenu) # activate menu
		addStockAction = QAction("Add a optional stock", self)
		addStockAction.triggered.connect(self.add_optional_stock_by_name)
		self.addAction(addStockAction)
		quitAction = QAction("Quit", self)
		quitAction.triggered.connect(self.close)
		self.addAction(quitAction)
        
		self.show()
        
    def add_optional_stock_by_name(self):
		name, ok = QInputDialog.getText(self, 'Stock Name', '輸入股票名稱')
		if ok:
			res = Fetcher().get_market_and_code(name)
			if res:
				self.stocks.add_stock(res[0], res[1])
			else:
				QMessageBox.information(self,'error','搜索不到改股票!',QMessageBox.Ok)

	def closeEvent(self, event):
		pass

到此為止,我覺得你應該設想一下我們雖然本次保存股票代碼到self.stocks中了,但下次啟動應用這些股票將不存在了,因此我們有必要將這些信息保存到磁盤中的一個文件中,每次啟動應用的時候將原先的股票代碼讀出來,這些只需要修改一下OptionalStock類就好:

import os

class OptionalStock(object):
	def __init__(self, record : str):
		self.stocks = []

		# if record doesn't exist, generate it.
		if not os.path.exists(record):
			f = open(record, 'w')
			f.close()

		with open(record, mode = 'r') as f:
			self.recordFile = record # the file that stores all code of optional stocks
			line = f.readline()
			while line: # read a optional stock, format: market,code
				stock = list(map(lambda x : x.strip(), line.split(',')))
				if len(stock) == 2:
					self.stocks.append({'market' : stock[0],
										'code' : stock[1],
										'key' : None})
				line = f.readline()

	def add_stock(self, market : str, code : str) -> None:
		with open(self.recordFile, mode = 'a') as f:
			f.write('\n%s, %s' % (market, code)) # store this stock in the file

		self.stocks.append({'market' : market,
							'code' : code,
							'key' : None})

上面代碼中,可以看到self.stocks中是一個字典列表,每個字典有三個內容,包括市場代碼,股票代碼以及關鍵字(這個現在不說,之后會講到,它是用於排序這些股票的規則)。相應地,我們這時候就要修改下這句代碼:

self.stocks = OptionalStock('./record') # This file ’./record‘ stores all codes of stocks

現在你可以嘗試一下設計的功能了,右擊窗體,輸入我們的自選股,回車就能在文件夾下看到一個文件record,里面記錄了我們剛剛添加的自選股。

獲取些股票信息來填充我們的窗體吧

對於填充窗體,我的想法是分兩個步驟進行,第一步填充一些固定不變的數據和格式,例如初始值,一些操作按鈕;第二步我們去獲取一些動態數據填充股票的股價,漲跌幅等等,代碼如下:

def stock_rate(price : float, close : float) -> str:
	""" return a percentage of rate """
	rate = (price - close) / close

	return "%.2f%%" % (rate * 100)

# add a variable self.stockTable
class Window(QWidget):
	"""docstring for Window"""
	def __init__(self, screen_width, screen_height, window_width, window_height):
		...
		self.stockTable = self.stocks.stocks # point to stocks in self.stocks

		self.initUI()
   
	def initUI(self):
        ...
		self.update_table_view()
		self.update_table_view_data()

		self.show()
        
	# flush static data
	def update_table_view(self):
		self.stockTableView.clearContents() # clear table
		self.stockTableView.setRowCount(len(self.stockTable))
		# set format and '--' for each unit in table
		for i in range(len(self.stockTable)):
			for j in range(self.stockTableView.columnCount() - 1):
				item = QTableWidgetItem()
				item.setText('--')
				item.setTextAlignment(Qt.AlignVCenter|Qt.AlignHCenter) # alignment
				self.stockTableView.setItem(i, j, item)

	# flush dynamic data
	def update_table_view_data(self):
		lastTime = ''
		for i in range(len(self.stockTable)): # for each every stock
			stock = self.stockTable[i]
			data = {}
			try:
				data = Fetcher().get_quotation(stock['market'], stock['code'])
			except:
				QMessageBox.information(self,'error','不能獲取到股票信息!',QMessageBox.Ok)
				self.close()

			# fill dynamic data for items of a stock
			
			self.stockTableView.item(i, 0).setText(data['name'])
			self.stockTableView.item(i, 1).setText(data['price'])
			# stcok_rate return a string of a percentage
			self.stockTableView.item(i, 2).setText(stock_rate( float(data['price']) , float(data['close']) ))
			# set font color according to change of stocks
			if float(data['price']) > float(data['close']):
				self.stockTableView.item(i, 2).setForeground(QBrush(QColor(255,0,0)))
			else:
				self.stockTableView.item(i, 2).setForeground(QBrush(QColor(0,255,0)))

			self.stockTableView.item(i, 3).setText("%.2f" % (float(data['price']) - float(data['close'])))
			# set font color according to change of stocks
			if float(data['price']) > float(data['close']):
				self.stockTableView.item(i, 3).setForeground(QBrush(QColor(255,0,0)))
			else:
				self.stockTableView.item(i, 3).setForeground(QBrush(QColor(0,255,0)))

			# set timestamp string
			lastTime = data['date'] + ' ' + data['time']

		self.timeView.setText('last update: ' + lastTime)

現在你再嘗試運行一下你的代碼,假設你的record文件中保存着貴州茅台的股票代碼,那你看到的窗口應該是這個樣子的(可能數據不大一樣):

另外一點,不知道你注意到沒,你現在使用添加股票的功能時,會發現界面並沒有多出你新添加的股票,只有重啟后才會出現。因此,我們需要在add_optional_stock_by_name添加代碼成功后,再追加一個刷新函數,該函數可以更新表項:

class Window(QWidget):
    def add_optional_stock_by_name(self):
		...
		self.stocks.add_stock(res[0], res[1])
		self.__update_optional_stock()
        
    def __update_optional_stock(self):
		self.stockTable = self.stocks.stocks
		self.update_table_view()
		self.update_table_view_data()

這會兒,你再嘗試一下,你就會發現我們的功能恢復正常了。

讓我們的股票價格滾動起來吧

現在我們應用上顯示的股票信息其實是靜態的,為了能夠不停的刷新最新的數據,我們必須設置一個定時器,隔一會去調用update_table_view_data函數,因此,這時就需要用到QTimer了。

我們只需要設置一個QTImer,為其定好超時相應函數,並在窗口關閉的時候停用該定時器就好。另外還需要注意的是,為了防止在添加股票的時候出問題,我們最好在更新表項靜態數據時停掉定時器,具體代碼如下:

from PyQt5.QtCore import QTimer

class Window(QWidget):
	"""docstring for Window"""
	def __init__(self, screen_width, screen_height, window_width, window_height):
		...
		self.initUI()

		self.timer = QTimer(self)
		self.timer.timeout.connect(self.update_table_view_data)
		self.tick = 3000 # time tick is 3 seconds
		self.timer.start(self.tick) # start timer
    
    # modify __update_optional_stock
    def __update_optional_stock(self):
		self.timer.stop()
		self.stockTable = self.stocks.stocks
		self.update_table_view()
		self.update_table_view_data()
		self.timer.start(self.tick)
    
    # modify closeEvent   
    def closeEvent(self, event):
		self.timer.stop()

現在你再去運行一下程序,看看是不是你的股價已經動起來了,是不是很激動!!!哈哈,別急呀,接下來還有更好玩的,我們要為我們的應用加一些更實用的操作。

刪除、置頂、排序,你還能想到什么使用操作呢

像刪除,置頂,排序這些操作,雖然表面上看是更新表格中行的位置,但實質上,根據我們的實現邏輯,你只需要更新self.stocks中每個股票在列表中的位置就好,因此重點還是要回到OptionalStock身上。

對於刪除和置頂,假定我們會得到被操作股票原先的索引號,那么我們可以通過操作列表來達到我們的目的:

def top_stock(self, row):
    self.stocks = [self.stocks[row]] + self.stocks[:row] + self.stocks[(row + 1):]
    # update the file record
    with open(self.recordFile, mode = 'w') as f:
        for stock in self.stocks:
            f.write('%s, %s\n' % (stock['market'], stock['code']))

def del_stock(self, row):
	del self.stocks[row]
    # update the file record
	with open(self.recordFile, mode = 'w') as f:
		for stock in self.stocks:
			f.write('%s, %s\n' % (stock['market'], stock['code']))

對於排序,還記得我們列表中每一項的key關鍵字嗎,我們假設調用者會為這些列表項設置該值,然后我們通過該值進行排序即可,至於逆序還是正序由調用者指定:

def sort_stock(self, reverse = False):
    def sort_method(elem):
        return elem['key']
    self.stocks.sort(key = sort_method, reverse=reverse)
    with open(self.recordFile, mode = 'w') as f:
        for stock in self.stocks:
            f.write('%s, %s\n' % (stock['market'], stock['code']))

好了,回到我們的Window中來,為了執行這些操作,我們必須要為用戶提供調用接口,例如排序,我們可以設置表頭的按鈕觸發事件來調用排序方法:

def initUI(self):

	# menu
	...
	self.addAction(quitAction)
	self.sort_flag = [False] * 4 # reverse or positive sequence  
   	self.stockTableView.horizontalHeader().sectionClicked.connect(
        self.sort_optional_stock)
    
# index: sort by the index'th column 
def sort_optional_stock(self, index : int):
	if index == 0:
		for i in range(len(self.stocks.stocks)):
			self.stocks.stocks[i]['key'] = self.stockTableView.item(i, 0).text()
	elif index == 1 or index == 3:
		for i in range(len(self.stocks.stocks)):
			self.stocks.stocks[i]['key'] = float(self.stockTableView.item(i, index).text())
	elif index == 2:
		for i in range(len(self.stocks.stocks)):
			self.stocks.stocks[i]['key'] = float(self.stockTableView.item(i, index).text().strip('%'))
	else:
		raise ValueError('sort_optional_stock index error')
	self.stocks.sort_stock(self.sort_flag[i])
	self.sort_flag[i] = ~self.sort_flag[i] # inverse sort method
	self.__update_optional_stock()

現在去試試點擊你的表頭,看看能不能去排序這些股票!!

接下來為了提供刪除和置頂的操作接口,我們在操作一欄為每只股票添加其相應的刪除和置頂按鈕:

from PyQt5.QtWidgets import QPushButton
from functools import partial

class Window(QWidget):
	def update_table_view(self):
		...
        self.stockTableView.setItem(i, j, item)
        topBtn = QPushButton()
        topBtn.setText('🔝')
        topBtn.setFixedSize(25,25)
        topBtn.clicked.connect(partial(self.top_optional_stock, i))
        delBtn = QPushButton()
        delBtn.setFixedSize(25,25)
        delBtn.setText('×')
        delBtn.clicked.connect(partial(self.del_optional_stock, i))
        lay = QHBoxLayout()
        lay.addWidget(topBtn)
        lay.addWidget(delBtn)
        subWidget = QWidget()
        subWidget.setLayout(lay)
        self.stockTableView.setCellWidget(i, self.stockTableView.columnCount() - 1, subWidget)
        
    def top_optional_stock(self, row):
		self.stocks.top_stock(row)
		self.__update_optional_stock()

	def del_optional_stock(self, row):
		self.stocks.del_stock(row)
		self.__update_optional_stock()

可以看到我們將添加按鈕放置到了update_table_view函數中,這樣的好處是在每次更新靜態數據的時候也會為每只股票一並創建其操作按鈕。同時partial函數為相應的操作函數傳入了股票的索引號,即行號,這樣一來,我們的全部功能都已經實現完成。

現在再次運行一下,是不是和我們開頭的界面一模一樣呢?


各位看官都看到這了,行行好,點個贊再走唄!!!

一些展望

有興趣的同學可以試着繼續豐富一下這個應用的功能,下面是我給出的一些想法:

  • 鼠標點擊某只股票顯示它的具體信息及K線圖
  • 增加交易快捷鍵和接口
  • 實時滾動大盤信息

當然啦,這些功能都比較專業了,難度不小,想繼續做下去的不妨留言區留言一起討論一下呀!


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM