花几个小时做一个看股票的摸鱼神器


项目介绍

你还在为了不能及时看到股价发愁吗?你还在为了上班偷看股票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线图
  • 增加交易快捷键和接口
  • 实时滚动大盘信息

当然啦,这些功能都比较专业了,难度不小,想继续做下去的不妨留言区留言一起讨论一下呀!

posted @ 2021-07-22 12:58  chegxy  阅读(2409)  评论(0编辑  收藏  举报