#!/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()

py2.png

评论已关闭