#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
汉缆股份(002498.SZ) 任务栏股价监控程序
常驻Windows系统托盘,实时显示股价
"""
import sys
import os
import time
import json
import threading
from datetime import datetime, timedelta
# 检查并安装依赖
try:
from PyQt5.QtWidgets import (
QApplication, QSystemTrayIcon, QMenu, QAction,
QWidget, QVBoxLayout, QLabel, QHBoxLayout,
QPushButton, QTableWidget, QTableWidgetItem, QHeaderView
)
from PyQt5.QtCore import Qt, QTimer, pyqtSignal, QObject
from PyQt5.QtGui import QIcon, QFont, QColor, QPalette
except ImportError:
print("正在安装 PyQt5...")
os.system("pip install PyQt5")
from PyQt5.QtWidgets import (
QApplication, QSystemTrayIcon, QMenu, QAction,
QWidget, QVBoxLayout, QLabel, QHBoxLayout,
QPushButton, QTableWidget, QTableWidgetItem, QHeaderView
)
from PyQt5.QtCore import Qt, QTimer, pyqtSignal, QObject
from PyQt5.QtGui import QIcon, QFont, QColor, QPalette
try:
import requests
except ImportError:
os.system("pip install requests")
import requests
class StockDataFetcher:
"""股价数据获取器"""
# 使用新浪API获取实时股价 (免费、稳定)
SINA_API_URL = "https://hq.sinajs.cn/list={}"
# 备用: 腾讯API
TENCENT_API_URL = "https://qt.gtimg.cn/q={}"
def __init__(self, stock_code="sz002498"):
self.stock_code = stock_code # 汉缆股份: sz002498
self.stock_name = "汉缆股份"
def fetch_from_sina(self):
"""从新浪获取实时数据"""
try:
url = self.SINA_API_URL.format(self.stock_code)
headers = {
'Referer': 'https://finance.sina.com.cn',
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36'
}
response = requests.get(url, headers=headers, timeout=10)
response.encoding = 'gb2312'
data = response.text
if not data or 'var hq_str_' not in data:
return None
# 解析数据: var hq_str_sz002498="汉缆股份,7.430,7.500,..."
content = data.split('"')[1]
parts = content.split(',')
if len(parts) < 30:
return None
result = {
'name': parts[0],
'current': float(parts[3]), # 当前价
'open': float(parts[1]), # 开盘价
'pre_close': float(parts[2]), # 昨收
'high': float(parts[4]), # 最高价
'low': float(parts[5]), # 最低价
'volume': int(parts[8]), # 成交量
'amount': float(parts[9]), # 成交额
'time': parts[30] + ' ' + parts[31], # 日期时间
'bid1': float(parts[11]), # 买一价
'ask1': float(parts[21]), # 卖一价
'bid1_vol': int(parts[10]), # 买一量
'ask1_vol': int(parts[20]), # 卖一量
}
# 计算涨跌幅
result['change'] = result['current'] - result['pre_close']
result['change_pct'] = (result['change'] / result['pre_close']) * 100
return result
except Exception as e:
print(f"新浪API获取失败: {e}")
return None
def fetch_from_tencent(self):
"""从腾讯获取实时数据 (备用)"""
try:
url = self.TENCENT_API_URL.format(self.stock_code)
headers = {'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64)'}
response = requests.get(url, headers=headers, timeout=10)
data = response.text
if not data or 'v_' not in data:
return None
# 解析: v_sz002498="1~汉缆股份~7.43~..."
content = data.split('"')[1]
parts = content.split('~')
result = {
'name': parts[1],
'current': float(parts[3]),
'pre_close': float(parts[4]),
'open': float(parts[5]),
'high': float(parts[33]),
'low': float(parts[34]),
'volume': int(parts[36]),
'change': float(parts[32]),
'change_pct': float(parts[38]),
'time': parts[30],
}
return result
except Exception as e:
print(f"腾讯API获取失败: {e}")
return None
def get_price(self):
"""获取股价,优先新浪,失败用腾讯"""
data = self.fetch_from_sina()
if data is None:
data = self.fetch_from_tencent()
return data
class SignalEmitter(QObject):
"""用于线程间通信的信号"""
data_updated = pyqtSignal(dict)
class StockTrayApp(QSystemTrayIcon):
"""系统托盘股价应用"""
def __init__(self, parent=None):
super().__init__(parent)
self.fetcher = StockDataFetcher("sz002498")
self.signal = SignalEmitter()
self.signal.data_updated.connect(self.update_display)
# 初始化UI
self.init_ui()
self.init_menu()
self.init_timer()
# 首次获取数据
self.refresh_data()
def init_ui(self):
"""初始化托盘图标显示"""
# 设置图标 (使用文字图标)
self.setIcon(self.create_text_icon("汉缆"))
self.setToolTip("汉缆股份 股价监控\n双击查看详情")
# 双击打开详情窗口
self.activated.connect(self.on_activated)
def create_text_icon(self, text, color="white", bg_color="#1a1a2e"):
"""创建文字图标"""
from PyQt5.QtGui import QPixmap, QPainter, QFont, QBrush
pixmap = QPixmap(64, 64)
pixmap.fill(QColor(bg_color))
painter = QPainter(pixmap)
painter.setPen(QColor(color))
font = QFont("Microsoft YaHei", 10, QFont.Bold)
painter.setFont(font)
painter.drawText(pixmap.rect(), Qt.AlignCenter, text)
painter.end()
return QIcon(pixmap)
def create_price_icon(self, price, change_pct):
"""根据价格涨跌创建动态图标"""
from PyQt5.QtGui import QPixmap, QPainter, QFont
# 根据涨跌设置颜色
if change_pct > 0:
bg_color = "#c0392b" # 红色(涨)
text_color = "white"
elif change_pct < 0:
bg_color = "#27ae60" # 绿色(跌)
text_color = "white"
else:
bg_color = "#7f8c8d" # 灰色(平)
text_color = "white"
pixmap = QPixmap(64, 64)
pixmap.fill(QColor(bg_color))
painter = QPainter(pixmap)
painter.setPen(QColor(text_color))
# 价格字体
font = QFont("Microsoft YaHei", 11, QFont.Bold)
painter.setFont(font)
painter.drawText(pixmap.rect(), Qt.AlignCenter, f"{price:.2f}")
painter.end()
return QIcon(pixmap)
def init_menu(self):
"""初始化右键菜单"""
self.menu = QMenu()
# 显示当前股价
self.price_action = QAction("汉缆股份: 获取中...", self)
self.price_action.setEnabled(False)
self.menu.addAction(self.price_action)
self.menu.addSeparator()
# 刷新
refresh_action = QAction("🔄 立即刷新", self)
refresh_action.triggered.connect(self.refresh_data)
self.menu.addAction(refresh_action)
# 详情窗口
detail_action = QAction("📊 查看详情", self)
detail_action.triggered.connect(self.show_detail_window)
self.menu.addAction(detail_action)
self.menu.addSeparator()
# 设置刷新频率
freq_menu = self.menu.addMenu("⏱️ 刷新频率")
for seconds, label in [(5, "5秒"), (10, "10秒"), (30, "30秒"), (60, "1分钟")]:
action = QAction(label, self)
action.triggered.connect(lambda checked, s=seconds: self.set_refresh_interval(s))
freq_menu.addAction(action)
self.menu.addSeparator()
# 开机启动
self.auto_start_action = QAction("🔌 开机启动", self)
self.auto_start_action.setCheckable(True)
self.auto_start_action.triggered.connect(self.toggle_auto_start)
self.menu.addAction(self.auto_start_action)
self.menu.addSeparator()
# 退出
exit_action = QAction("❌ 退出", self)
exit_action.triggered.connect(self.exit_app)
self.menu.addAction(exit_action)
self.setContextMenu(self.menu)
def init_timer(self):
"""初始化定时器"""
self.timer = QTimer()
self.timer.timeout.connect(self.refresh_data)
self.timer.start(10000) # 默认10秒刷新
# 交易时间检测定时器 (9:30-11:30, 13:00-15:00)
self.market_timer = QTimer()
self.market_timer.timeout.connect(self.check_market_time)
self.market_timer.start(60000) # 每分钟检查
def check_market_time(self):
"""检查是否在交易时间"""
now = datetime.now()
weekday = now.weekday()
# 周末不交易
if weekday >= 5:
return
time_str = now.strftime("%H:%M")
morning_start, morning_end = "09:30", "11:30"
afternoon_start, afternoon_end = "13:00", "15:00"
is_trading = (morning_start <= time_str <= morning_end) or \
(afternoon_start <= time_str <= afternoon_end)
if is_trading:
# 交易时间加快刷新
if self.timer.interval() > 10000:
self.timer.setInterval(10000)
else:
# 非交易时间减慢刷新
self.timer.setInterval(60000)
def set_refresh_interval(self, seconds):
"""设置刷新间隔"""
self.timer.setInterval(seconds * 1000)
print(f"刷新间隔已设置为 {seconds} 秒")
def refresh_data(self):
"""刷新股价数据"""
def fetch():
data = self.fetcher.get_price()
if data:
self.signal.data_updated.emit(data)
thread = threading.Thread(target=fetch)
thread.daemon = True
thread.start()
def update_display(self, data):
"""更新显示"""
price = data['current']
change = data.get('change', 0)
change_pct = data.get('change_pct', 0)
# 更新托盘图标
self.setIcon(self.create_price_icon(price, change_pct))
# 更新工具提示
sign = "+" if change >= 0 else ""
tooltip = f"汉缆股份 (002498)\n"
tooltip += f"当前价: {price:.2f}\n"
tooltip += f"涨跌: {sign}{change:.2f} ({sign}{change_pct:.2f}%)\n"
tooltip += f"最高: {data.get('high', 0):.2f} 最低: {data.get('low', 0):.2f}\n"
tooltip += f"更新时间: {data.get('time', '')}"
self.setToolTip(tooltip)
# 更新菜单显示
sign = "+" if change >= 0 else ""
self.price_action.setText(f"汉缆股份: {price:.2f} {sign}{change:.2f} ({sign}{change_pct:.2f}%)")
# 保存数据供详情窗口使用
self.current_data = data
def on_activated(self, reason):
"""托盘图标被激活"""
if reason == QSystemTrayIcon.DoubleClick:
self.show_detail_window()
def show_detail_window(self):
"""显示详情窗口"""
if not hasattr(self, 'detail_window') or not self.detail_window:
self.detail_window = DetailWindow(self.current_data if hasattr(self, 'current_data') else None)
self.detail_window.show()
self.detail_window.raise_()
self.detail_window.activateWindow()
def toggle_auto_start(self, checked):
"""设置开机启动"""
import winreg
try:
key_path = r"Software\Microsoft\Windows\CurrentVersion\Run"
app_name = "HanlanStockMonitor"
app_path = os.path.abspath(sys.argv[0])
key = winreg.OpenKey(winreg.HKEY_CURRENT_USER, key_path, 0, winreg.KEY_WRITE)
if checked:
winreg.SetValueEx(key, app_name, 0, winreg.REG_SZ, f'"{app_path}"')
print("已设置开机启动")
else:
try:
winreg.DeleteValue(key, app_name)
print("已取消开机启动")
except:
pass
winreg.CloseKey(key)
except Exception as e:
print(f"设置开机启动失败: {e}")
def exit_app(self):
"""退出应用"""
self.timer.stop()
self.market_timer.stop()
QApplication.quit()
class DetailWindow(QWidget):
"""详情窗口"""
def __init__(self, data=None):
super().__init__()
self.setWindowTitle("汉缆股份 (002498.SZ) 实时行情")
self.setGeometry(100, 100, 450, 350)
# 设置窗口置顶
self.setWindowFlags(Qt.WindowStaysOnTopHint)
self.init_ui(data)
def init_ui(self, data):
layout = QVBoxLayout()
# 标题
title = QLabel("汉缆股份 (002498.SZ)")
title_font = QFont("Microsoft YaHei", 16, QFont.Bold)
title.setFont(title_font)
title.setAlignment(Qt.AlignCenter)
layout.addWidget(title)
# 数据表格
self.table = QTableWidget(10, 2)
self.table.setHorizontalHeaderLabels(["项目", "数值"])
self.table.horizontalHeader().setSectionResizeMode(QHeaderView.Stretch)
self.table.verticalHeader().setVisible(False)
if data:
self.update_table(data)
layout.addWidget(self.table)
# 刷新按钮
btn_layout = QHBoxLayout()
refresh_btn = QPushButton("🔄 刷新")
refresh_btn.clicked.connect(self.refresh)
btn_layout.addWidget(refresh_btn)
close_btn = QPushButton("关闭")
close_btn.clicked.connect(self.hide)
btn_layout.addWidget(close_btn)
layout.addLayout(btn_layout)
self.setLayout(layout)
def update_table(self, data):
"""更新表格数据"""
items = [
("股票名称", data.get('name', '汉缆股份')),
("当前价格", f"{data.get('current', 0):.2f}"),
("开盘价", f"{data.get('open', 0):.2f}"),
("昨收价", f"{data.get('pre_close', 0):.2f}"),
("最高价", f"{data.get('high', 0):.2f}"),
("最低价", f"{data.get('low', 0):.2f}"),
("涨跌额", f"{data.get('change', 0):+.2f}"),
("涨跌幅", f"{data.get('change_pct', 0):+.2f}%"),
("成交量", f"{data.get('volume', 0):,}"),
("成交额", f"{data.get('amount', 0):,.0f}"),
("买一", f"{data.get('bid1', 0):.2f} x {data.get('bid1_vol', 0)}"),
("卖一", f"{data.get('ask1', 0):.2f} x {data.get('ask1_vol', 0)}"),
("更新时间", data.get('time', '')),
]
self.table.setRowCount(len(items))
for i, (key, value) in enumerate(items):
self.table.setItem(i, 0, QTableWidgetItem(key))
item = QTableWidgetItem(str(value))
# 涨跌颜色
if "涨跌" in key and isinstance(value, str):
if value.startswith("+"):
item.setForeground(QColor("#e74c3c"))
elif value.startswith("-"):
item.setForeground(QColor("#27ae60"))
self.table.setItem(i, 1, item)
def refresh(self):
"""手动刷新"""
fetcher = StockDataFetcher("sz002498")
data = fetcher.get_price()
if data:
self.update_table(data)
def main():
"""主函数"""
app = QApplication(sys.argv)
app.setQuitOnLastWindowClosed(False) # 关闭窗口不退出
# 检查系统托盘是否可用
if not QSystemTrayIcon.isSystemTrayAvailable():
print("系统托盘不可用")
sys.exit(1)
# 创建应用
tray_app = StockTrayApp()
tray_app.show()
print("=" * 50)
print("汉缆股份(002498) 任务栏股价监控已启动")
print("- 托盘图标显示实时价格")
print("- 双击图标查看详情")
print("- 右键菜单可设置刷新频率、开机启动")
print("- 默认10秒刷新,交易时间自动调整")
print("=" * 50)
sys.exit(app.exec_())
if __name__ == "__main__":
main()

评论已关闭