吾爱破解 - 52pojie.cn

 找回密码
 注册[Register]

QQ登录

只需一步,快速开始

查看: 11722|回复: 59
上一主题 下一主题
收起左侧

[原创工具] 股票盯盘摸鱼工具,一键隐藏,程序员自用稳定版,提供完整源码,低调使用

  [复制链接]
跳转到指定楼层
楼主
raccooncp 发表于 2025-12-4 16:28 回帖奖励
本帖最后由 raccooncp 于 2026-4-15 14:18 编辑

平时盯盘总被多个软件切换烦扰?手动刷新数据滞后?这款纯 Java 编写的桌面股票监控工具,专为散户打造高效盯盘体验,完全免费无广告,支持自定义配置,用过的都说香!

核心功能亮点:
实时数据监控:自动 30 秒刷新一次,同步显示股票最新价、涨跌幅、成交额、涨跌百分位等关键数据,数据来源稳定可靠
自定义自选股:支持右键快速添加 / 删除股票,自动保存配置文件,下次启动直接加载
可视化提醒:涨跌幅超阈值自动变色高亮,大额成交额加粗标注,关键数据一眼识别
灵活配置:可自定义涨跌幅阈值、字体大小、高亮颜色,适配不同盯盘习惯
便捷操作:支持按涨跌幅升 / 降序排序,Alt+F6 全局快捷键隐藏 / 显示窗口,系统托盘后台运行不占桌面
异常处理:网络波动自动重试,数据解析容错机制,断网后恢复自动重连


详细使用教程(零基础也能上手):
一、基础运行
下载解压后,直接双击程序文件即可启动(无需安装,Windows 全版本兼容)
首次启动会自动加载 11 只热门龙头股(茅台、宁德时代、比亚迪等),同时生成配置文件
程序窗口可自由调整大小,点击右上角最小化会自动隐藏到系统托盘,不占用桌面空间
二、自选股管理(核心操作)
添加股票
在表格任意位置右键,选择「新增股票」
在弹出的对话框中输入股票代码:
沪市股票需加前缀「sh」(如贵州茅台:sh600519)
深市股票需加前缀「sz」(如宁德时代:sz300750)
若忘记前缀,直接输入数字(如 600519),程序会提示自动补全
点击「确定添加」,程序会自动验证代码有效性,验证通过后立即加载数据并保存到配置文件
删除股票
鼠标左键选中要删除的股票行(点击表格行即可)
右键点击选中行,选择「删除选中股票」
确认删除后,该股票会从列表中移除,配置文件同步更新
三、个性化设置
调整涨跌幅阈值:点击「设置阈值」按钮,输入想要的百分比(默认 5%),超过该数值的涨跌幅会高亮显示;还可自定义高亮颜色、正常显示颜色
调整字号:点击「字号 +」「字号 -」按钮(范围 8-16 号),适配不同屏幕大小和观看距离
排序功能:点击「按涨跌幅降序 / 升序排序」,可快速将股票按涨跌幅排序,涨跌情况一目了然
四、快捷操作
隐藏 / 显示窗口:按下「Alt+F6」快捷键,可快速隐藏或唤醒窗口,上班盯盘超隐蔽
手动刷新数据:右键表格选择「手动刷新数据」,可立即更新所有股票数据(无需等待自动刷新)
系统托盘操作:双击托盘图标可唤醒窗口,右键托盘可快速打开阈值设置或退出程序
五、数据解读
代码 / 名称:显示股票代码和名称,方便识别
最新价:实时更新的股票当前价格
涨跌幅 (%):当日涨跌幅百分比,超过阈值自动变色,正值加粗
成交额 (万元):当日成交总金额,超过 2 亿元自动加粗标注
涨幅 / 跌幅百分位:相对于昨收价的涨跌幅度百分比
昨收:前一交易日收盘价


适用人群:
散户投资者:无需打开券商 APP,桌面端实时盯盘,关键波动不遗漏
多股持仓者:集中管理自选股,避免切换软件分散注意力
上班族盯盘:后台运行 + 快捷键唤醒,低调不张扬
完全开源无捆绑,纯本地运行不收集任何数据,安全性拉满!解压即可使用,程序员可根据需求二次开发(已预留扩展接口)。
不管是短线交易需要实时盯盘,还是长线持仓关注关键点位,这款工具都能帮你节省时间、提高效率~ 有需要的朋友直接拿走,使用中遇到问题可留言交流,后续会持续更新功能!


使用说明:运行 JAR 包:命令行 java -jar StockMonitor.jar  需要有jdk环境 具体可以看三楼



有不少同学可能不知道怎么弄java环境,我打包了一个绿色版本的,把jre还有配置文件都放到一起了,直接 双击 run.bat 即可启动
通过网盘分享的文件:green.zip
链接: https://pan.baidu.com/s/1owUTju-2JDf_wXY3GYG4lw?pwd=2bvv

image.png (5.79 KB, 下载次数: 8)

image.png

image.png (86.66 KB, 下载次数: 4)

image.png

stock.zip

24.84 KB, 下载次数: 591, 下载积分: 吾爱币 -2 CB

免费评分

参与人数 9吾爱币 +13 热心值 +9 收起 理由
dhgluck + 1 我很赞同!
lijie1562 + 1 + 1 感谢发布原创作品,吾爱破解论坛因你更精彩!
wwyyqq777 + 1 + 1 谢谢@Thanks!
ywsdm + 1 谢谢@Thanks!
ryf0000007 + 1 + 1 鼓励转贴优秀软件安全工具和文档!
JF300666 + 1 + 1 谢谢@Thanks!
lgg51 + 1 + 1 鼓励转贴优秀软件安全工具和文档!
hyeiz + 1 + 1 谢谢@Thanks!
风之暇想 + 7 + 1 感谢发布原创作品,吾爱破解论坛因你更精彩!

查看全部评分

发帖前要善用【论坛搜索】功能,那里可能会有你要找的答案或者已经有人发布过相同内容了,请勿重复发帖。

推荐
JF300666 发表于 2025-12-6 18:59
大哥,咋启动啊?
推荐
 楼主| raccooncp 发表于 2025-12-4 16:32 |楼主
import javax.swing.*;
import javax.swing.table.DefaultTableCellRenderer;
import javax.swing.table.DefaultTableModel;
import javax.swing.table.TableCellRenderer;

import java.awt.*;
import java.awt.event.*;
import java.awt.image.BufferedImage;
import java.io.*;
import java.net.HttpURLConnection;
import java.net.URL;
import java.net.URLEncoder;
import java.text.DecimalFormat;
import java.util.*;
import java.util.Timer;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.zip.GZIPInputStream;

public class StockMonitorApp {
    private final JFrame frame;
    private final JTable stockTable;
    private final DefaultTableModel tableModel;
    private final java.util.List<String> stockCodes = new java.util.ArrayList<>();
    private final JLabel statusLabel;
    private SystemTray systemTray;
    private TrayIcon trayIcon;
    private java.util.List<Stock> currentStocks = new java.util.ArrayList<>();
    private boolean sortAscending = false;
   
    // 配置参数(涨跌幅阈值)
    private double changeThreshold = 5.0;
    private Color highChangeColor = Color.RED;
    private Color normalChangeColor = Color.BLACK;
   
    // 字号配置
    private int currentFontSize = 12;
    private final int MIN_FONT_SIZE = 8;
    private final int MAX_FONT_SIZE = 16;
    private final int FONT_STEP = 2;
   
    // 格式化数字显示
    private static final DecimalFormat PRICE_FORMAT = new DecimalFormat("#.00");
    private static final DecimalFormat CHANGE_FORMAT = new DecimalFormat("0.00");
    private static final DecimalFormat PERCENT_FORMAT = new DecimalFormat("0.00");
    private static final DecimalFormat AMOUNT_FORMAT = new DecimalFormat("#,##0.00");

    // 网络配置参数
    private static final int BASE_CONNECT_TIMEOUT = 10000;
    private static final int BASE_READ_TIMEOUT = 15000;
    private static final int MAX_RETRIES = 3;
    private static final int INITIAL_RETRY_DELAY = 500;

    // 浏览器请求头
    private static final String USER_AGENT = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36";
    private static final String REFERER = "https://finance.sina.cn/";
    private static final String ACCEPT = "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7";
    private static final String ACCEPT_ENCODING = "gzip, deflate, br";
    private static final String ACCEPT_LANGUAGE = "zh-CN,zh;q=0.9,en;q=0.8";

    // 配置文件路径
    private static final String CONFIG_FILE = "stock_codes.txt";
    private static final String SETTINGS_FILE = "settings.properties";

    public StockMonitorApp() {
        // 加载配置
        loadSettings();
        
        // 初始化窗口
        frame = new JFrame("自选股监控(稳定版)");
        frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        frame.setSize(900, 550);
        frame.setLocationRelativeTo(null);

        // 表格列名:换手率改为成交额
        String[] columnNames = {"代码/名称", "最新价", "涨跌幅(%)", "成交额(万元)", "涨幅百分位(%)", "跌幅百分位(%)", "昨收"};
        tableModel = new DefaultTableModel(columnNames, 0) {
            @Override
            public boolean isCellEditable(int row, int column) {
                return false;
            }
        };
        stockTable = new JTable(tableModel);
        customizeTableRenderer();
        
        // 设置右键菜单
        setupTableRightClickMenu();

        // 操作按钮
        JButton sortButton = new JButton("按涨跌幅降序排序");
        sortButton.addActionListener(e -> sortStocksByChange());
        
        JButton settingsButton = new JButton("设置阈值");
        settingsButton.addActionListener(e -> showSettingsDialog());
        
        JButton fontPlusButton = new JButton("字号+");
        fontPlusButton.addActionListener(e -> adjustFontSize(true));
        
        JButton fontMinusButton = new JButton("字号-");
        fontMinusButton.addActionListener(e -> adjustFontSize(false));

        // 面板布局
        JPanel topPanel = new JPanel();
        topPanel.setLayout(new FlowLayout(FlowLayout.LEFT, 10, 5));
        topPanel.add(new JLabel("操作:"));
        topPanel.add(sortButton);
        topPanel.add(settingsButton);
        topPanel.add(new JLabel(" | 字号:"));
        topPanel.add(fontMinusButton);
        topPanel.add(fontPlusButton);

        JScrollPane scrollPane = new JScrollPane(stockTable);
        statusLabel = new JLabel(String.format("准备就绪. 涨跌幅阈值:%s%%,当前字号:%d号 (Alt+F6 隐藏/显示)",
                changeThreshold, currentFontSize));
        statusLabel.setHorizontalAlignment(SwingConstants.LEFT);
        statusLabel.setBorder(BorderFactory.createEmptyBorder(5, 10, 5, 10));

        JPanel mainPanel = new JPanel();
        mainPanel.setLayout(new BorderLayout());
        mainPanel.add(topPanel, BorderLayout.NORTH);
        mainPanel.add(scrollPane, BorderLayout.CENTER);
        mainPanel.add(statusLabel, BorderLayout.SOUTH);
        frame.add(mainPanel);

        // 初始化功能
        initializeStockCodes();
        SwingUtilities.invokeLater(() -> statusLabel.setText(String.format("首次加载数据中...涨跌幅阈值:%s%%,当前字号:%d号",
                changeThreshold, currentFontSize)));
        fetchAndUpdateStockData();
        startAutoRefresh();
        setupSystemTray();
        setupGlobalHotkey();

        frame.setVisible(true);
    }
   
    /**
     * 设置表格右键菜单
     */
    private void setupTableRightClickMenu() {
        JPopupMenu rightClickMenu = new JPopupMenu();
        
        // 删除股票菜单项
        JMenuItem deleteItem = new JMenuItem("删除选中股票");
        deleteItem.addActionListener(e -> deleteSelectedStock());
        
        // 新增股票菜单项
        JMenuItem addItem = new JMenuItem("新增股票");
        addItem.addActionListener(e -> addNewStock());
        
        // 刷新数据菜单项
        JMenuItem refreshItem = new JMenuItem("手动刷新数据");
        refreshItem.addActionListener(e -> {
            statusLabel.setText("手动刷新中...");
            fetchAndUpdateStockData();
        });
        
        rightClickMenu.add(deleteItem);
        rightClickMenu.add(addItem);
        rightClickMenu.addSeparator();
        rightClickMenu.add(refreshItem);
        
        // 为表格添加右键菜单事件
        stockTable.addMouseListener(new MouseAdapter() {
            @Override
            public void mousePressed(MouseEvent e) {
                if (SwingUtilities.isRightMouseButton(e)) {
                    int row = stockTable.rowAtPoint(e.getPoint());
                    if (row >= 0) {
                        stockTable.setRowSelectionInterval(row, row);
                        rightClickMenu.show(stockTable, e.getX(), e.getY());
                    }
                }
            }
            
            @Override
            public void mouseReleased(MouseEvent e) {
                if (SwingUtilities.isRightMouseButton(e)) {
                    int row = stockTable.rowAtPoint(e.getPoint());
                    if (row >= 0) {
                        stockTable.setRowSelectionInterval(row, row);
                    }
                }
            }
        });
    }
   
    /**
     * 删除选中的股票
     */
    private void deleteSelectedStock() {
        int selectedRow = stockTable.getSelectedRow();
        if (selectedRow == -1) {
            JOptionPane.showMessageDialog(frame, "请先选中要删除的股票!", "提示", JOptionPane.INFORMATION_MESSAGE);
            return;
        }
        
        // 获取选中股票的代码
        String stockInfo = (String) tableModel.getValueAt(selectedRow, 0);
        String stockCode = stockInfo.split(" / ")[0];
        
        int confirm = JOptionPane.showConfirmDialog(frame,
                "确定要删除股票:" + stockInfo + " 吗?",
                "确认删除", JOptionPane.YES_NO_OPTION);
        
        if (confirm == JOptionPane.YES_OPTION) {
            // 从列表中移除
            stockCodes.remove(stockCode);
            
            // 更新配置文件
            saveStockCodesToFile();
            
            // 刷新表格
            fetchAndUpdateStockData();
            
            statusLabel.setText("已删除股票:" + stockInfo + ",配置文件已更新");
        }
    }
   
    /**
     * 新增股票
     */
    private void addNewStock() {
        JDialog dialog = new JDialog(frame, "新增股票", true);
        dialog.setSize(350, 180);
        dialog.setLocationRelativeTo(frame);
        dialog.setLayout(new GridLayout(4, 2, 10, 10));
        dialog.setResizable(false);
        
        JLabel codeLabel = new JLabel("股票代码(如:sh600519):");
        JTextField codeField = new JTextField();
        
        JLabel nameLabel = new JLabel("股票名称(可选):");
        JTextField nameField = new JTextField();
        
        JButton okButton = new JButton("确定添加");
        JButton cancelButton = new JButton("取消");
        
        okButton.addActionListener(e -> {
            String code = codeField.getText().trim().toLowerCase();
            if (code.isEmpty()) {
                JOptionPane.showMessageDialog(dialog, "股票代码不能为空!", "错误", JOptionPane.ERROR_MESSAGE);
                return;
            }
            
            // 验证代码格式
            if (!code.startsWith("sh") && !code.startsWith("sz")) {
                int confirm = JOptionPane.showConfirmDialog(dialog,
                        "股票代码格式应为shxxx或szxxx,是否自动补全为sh" + code + "?",
                        "格式提示", JOptionPane.YES_NO_CANCEL_OPTION);
               
                if (confirm == JOptionPane.YES_OPTION) {
                    code = "sh" + code;
                } else if (confirm == JOptionPane.NO_OPTION) {
                    code = "sz" + code;
                } else {
                    return;
                }
            }
            
            if (stockCodes.contains(code)) {
                JOptionPane.showMessageDialog(dialog, "该股票已存在!", "提示", JOptionPane.INFORMATION_MESSAGE);
                return;
            }
            
            // 测试股票代码是否有效
            Stock testStock = fetchSingleStockWithExponentialBackoff(code);
            if (testStock == null) {
                int retry = JOptionPane.showConfirmDialog(dialog,
                        "无法获取该股票数据,可能代码错误或网络问题。是否继续添加?",
                        "警告", JOptionPane.YES_NO_OPTION);
               
                if (retry != JOptionPane.YES_OPTION) {
                    return;
                }
            }
            
            // 添加到列表
            stockCodes.add(code);
            
            // 更新配置文件
            saveStockCodesToFile();
            
            // 刷新表格
            fetchAndUpdateStockData();
            
            dialog.dispose();
            statusLabel.setText("已添加股票:" + code + ",配置文件已更新");
        });
        
        cancelButton.addActionListener(e -> dialog.dispose());
        
        dialog.add(codeLabel);
        dialog.add(codeField);
        dialog.add(nameLabel);
        dialog.add(nameField);
        dialog.add(new JLabel()); // 占位
        dialog.add(new JLabel()); // 占位
        dialog.add(okButton);
        dialog.add(cancelButton);
        
        dialog.setVisible(true);
    }
   
    /**
     * 保存股票代码到配置文件
     */
    private void saveStockCodesToFile() {
        try (BufferedWriter writer = new BufferedWriter(new FileWriter(CONFIG_FILE))) {
            writer.write("# 自选股配置文件\n");
            writer.write("# 每行一个股票代码(sh=沪市,sz=深市)\n");
            
            for (String code : stockCodes) {
                // 尝试获取股票名称用于注释
                String name = "";
                for (Stock stock : currentStocks) {
                    if (stock.getCode().equals(code)) {
                        name = stock.getName();
                        break;
                    }
                }
               
                if (!name.isEmpty()) {
                    writer.write(code + "    # " + name + "\n");
                } else {
                    writer.write(code + "\n");
                }
            }
            
        } catch (IOException e) {
            System.err.println("保存股票代码失败:" + e.getMessage());
            JOptionPane.showMessageDialog(frame, "保存配置文件失败:" + e.getMessage(), "错误", JOptionPane.ERROR_MESSAGE);
        }
    }

    /**
     * 调整字号
     */
    private void adjustFontSize(boolean isIncrease) {
        int newSize = isIncrease ? currentFontSize + FONT_STEP : currentFontSize - FONT_STEP;
        newSize = Math.max(MIN_FONT_SIZE, Math.min(newSize, MAX_FONT_SIZE));
        
        if (newSize == currentFontSize) {
            String tip = isIncrease ? "已达到最大字号(16号)" : "已达到最小字号(8号)";
            JOptionPane.showMessageDialog(frame, tip, "提示", JOptionPane.INFORMATION_MESSAGE);
            return;
        }
        
        currentFontSize = newSize;
        updateTableFont();
        
        statusLabel.setText(String.format("字号已调整为:%d号. 涨跌幅阈值:%s%% (Alt+F6 隐藏/显示)",
                currentFontSize, changeThreshold));
    }

    /**
     * 更新表格字体
     */
    private void updateTableFont() {
        Font newFont = new Font("宋体", Font.PLAIN, currentFontSize);
        
        TableCellRenderer headerRenderer = stockTable.getTableHeader().getDefaultRenderer();
        if (headerRenderer instanceof JLabel) {
            ((JLabel) headerRenderer).setFont(newFont);
        }
        stockTable.getTableHeader().setFont(newFont);
        
        stockTable.setFont(newFont);
        
        int newRowHeight = currentFontSize + 10;
        stockTable.setRowHeight(newRowHeight);
        
        stockTable.repaint();
        stockTable.getTableHeader().repaint();
    }

    /**
     * 加载配置文件
     */
    private void loadSettings() {
        File settingsFile = new File(SETTINGS_FILE);
        Properties props = new Properties();
        
        try {
            if (settingsFile.exists()) {
                props.load(new FileInputStream(settingsFile));
                changeThreshold = Double.parseDouble(props.getProperty("change.threshold", "5.0"));
                String highColor = props.getProperty("high.change.color", "#FF0000");
                String normalColor = props.getProperty("normal.change.color", "#000000");
                highChangeColor = Color.decode(highColor);
                normalChangeColor = Color.decode(normalColor);
               
                String savedFontSize = props.getProperty("font.size", "12");
                currentFontSize = Integer.parseInt(savedFontSize);
                currentFontSize = Math.max(MIN_FONT_SIZE, Math.min(currentFontSize, MAX_FONT_SIZE));
            } else {
                props.setProperty("change.threshold", "5.0");
                props.setProperty("high.change.color", "#FF0000");
                props.setProperty("normal.change.color", "#000000");
                props.setProperty("font.size", "12");
                props.store(new FileOutputStream(settingsFile), "Stock Monitor Settings");
            }
        } catch (Exception e) {
            System.err.println("加载配置失败,使用默认值:" + e.getMessage());
            changeThreshold = 5.0;
            currentFontSize = 12;
        }
    }

    /**
     * 保存配置
     */
    private void saveSettings() {
        Properties props = new Properties();
        props.setProperty("change.threshold", String.valueOf(changeThreshold));
        props.setProperty("high.change.color", String.format("#%02X%02X%02X",
                highChangeColor.getRed(), highChangeColor.getGreen(), highChangeColor.getBlue()));
        props.setProperty("normal.change.color", String.format("#%02X%02X%02X",
                normalChangeColor.getRed(), normalChangeColor.getGreen(), normalChangeColor.getBlue()));
        props.setProperty("font.size", String.valueOf(currentFontSize));
        
        try {
            props.store(new FileOutputStream(SETTINGS_FILE), "Stock Monitor Settings");
        } catch (IOException e) {
            System.err.println("保存配置失败:" + e.getMessage());
        }
    }

    /**
     * 阈值设置对话框
     */
    private void showSettingsDialog() {
        JDialog dialog = new JDialog(frame, "阈值设置", true);
        dialog.setSize(300, 220);
        dialog.setLocationRelativeTo(frame);
        dialog.setLayout(new GridLayout(5, 2, 10, 5));
        dialog.setResizable(false);

        JTextField thresholdField = new JTextField(String.valueOf(changeThreshold));
        JButton highColorButton = new JButton("高涨幅颜色");
        JButton normalColorButton = new JButton("正常颜色");
        
        JLabel fontSizeLabel = new JLabel("当前字号:");
        JLabel fontSizeValue = new JLabel(currentFontSize + "号(可通过主界面按钮调整)");
        
        highColorButton.setBackground(highChangeColor);
        normalColorButton.setBackground(normalChangeColor);

        highColorButton.addActionListener(e -> {
            Color selected = JColorChooser.showDialog(dialog, "选择高涨幅颜色", highChangeColor);
            if (selected != null) {
                highChangeColor = selected;
                highColorButton.setBackground(selected);
            }
        });

        normalColorButton.addActionListener(e -> {
            Color selected = JColorChooser.showDialog(dialog, "选择正常颜色", normalChangeColor);
            if (selected != null) {
                normalChangeColor = selected;
                normalColorButton.setBackground(selected);
            }
        });

        JButton okButton = new JButton("确认");
        okButton.addActionListener(e -> {
            try {
                changeThreshold = Double.parseDouble(thresholdField.getText());
                saveSettings();
                statusLabel.setText(String.format("配置已更新. 涨跌幅阈值:%s%%,当前字号:%d号 (Alt+F6 隐藏/显示)",
                        changeThreshold, currentFontSize));
                stockTable.repaint();
                dialog.dispose();
            } catch (NumberFormatException ex) {
                JOptionPane.showMessageDialog(dialog, "请输入有效的数字!", "错误", JOptionPane.ERROR_MESSAGE);
            }
        });

        dialog.add(new JLabel("涨跌幅阈值(%):"));
        dialog.add(thresholdField);
        dialog.add(new JLabel("高涨幅颜色:"));
        dialog.add(highColorButton);
        dialog.add(new JLabel("正常颜色:"));
        dialog.add(normalColorButton);
        dialog.add(fontSizeLabel);
        dialog.add(fontSizeValue);
        dialog.add(new JLabel(""));
        dialog.add(okButton);

        dialog.setVisible(true);
    }

    /**
     * 初始化股票代码
     */
    private void initializeStockCodes() {
        if (loadStockCodesFromFile()) {
            statusLabel.setText(String.format("成功加载 %d 只股票代码. 涨跌幅阈值:%s%%,当前字号:%d号",
                    stockCodes.size(), changeThreshold, currentFontSize));
        } else {
            loadDefaultStockCodes();
            statusLabel.setText(String.format("配置文件不存在,使用默认股票代码. 涨跌幅阈值:%s%%,当前字号:%d号",
                    changeThreshold, currentFontSize));
        }
    }

    /**
     * 从配置文件读取股票代码
     */
    private boolean loadStockCodesFromFile() {
        File configFile = new File(CONFIG_FILE);

        if (!configFile.exists()) {
            createDefaultConfigFile();
            return false;
        }

        try (BufferedReader reader = new BufferedReader(new FileReader(configFile))) {
            String line;
            while ((line = reader.readLine()) != null) {
                line = line.trim();
                if (!line.isEmpty() && !line.startsWith("#")) {
                    // 提取代码部分(忽略注释)
                    String code = line.split("#")[0].trim();
                    stockCodes.add(code);
                }
            }
            return !stockCodes.isEmpty();
        } catch (IOException e) {
            System.err.println("读取配置文件失败:" + e.getMessage());
            return false;
        }
    }

    /**
     * 创建默认配置文件
     */
    private void createDefaultConfigFile() {
        try (BufferedWriter writer = new BufferedWriter(new FileWriter(CONFIG_FILE))) {
            writer.write("# 自选股配置文件\n");
            writer.write("# 每行一个股票代码(sh=沪市,sz=深市)\n");
            writer.write("sh600519    # 贵州茅台\n");
            writer.write("sz000858    # 五粮液\n");
            writer.write("sz300750    # 宁德时代\n");
            writer.write("sz000001    # 平安银行\n");
            writer.write("sh601318    # 中国平安\n");
            writer.write("sh600036    # 招商银行\n");
            writer.write("sz002594    # 比亚迪\n");
            writer.write("sh601899    # 紫金矿业\n");
            writer.write("sh600030    # 中信证券\n");
            writer.write("sz000725    # 京东方A\n");
            writer.write("sz301552    # 新莱福\n");
        } catch (IOException e) {
            System.err.println("创建默认配置文件失败:" + e.getMessage());
        }
    }

    /**
     * 加载默认股票代码
     */
    private void loadDefaultStockCodes() {
        stockCodes.add("sh600519");
        stockCodes.add("sz000858");
        stockCodes.add("sz300750");
        stockCodes.add("sz000001");
        stockCodes.add("sh601318");
        stockCodes.add("sh600036");
        stockCodes.add("sz002594");
        stockCodes.add("sh601899");
        stockCodes.add("sh600030");
        stockCodes.add("sz000725");
        stockCodes.add("sz301552");
    }

    /**
     * 按涨跌幅排序
     */
    private void sortStocksByChange() {
        if (currentStocks.isEmpty()) {
            JOptionPane.showMessageDialog(frame, "暂无股票数据可排序!", "提示", JOptionPane.INFORMATION_MESSAGE);
            return;
        }

        sortAscending = !sortAscending;

        if (sortAscending) {
            Collections.sort(currentStocks, Comparator.comparingDouble(Stock::getChange));
            statusLabel.setText(String.format("已按涨跌幅升序排序. 涨跌幅阈值:%s%%,当前字号:%d号",
                    changeThreshold, currentFontSize));
        } else {
            Collections.sort(currentStocks, (s1, s2) -> Double.compare(s2.getChange(), s1.getChange()));
            statusLabel.setText(String.format("已按涨跌幅降序排序. 涨跌幅阈值:%s%%,当前字号:%d号",
                    changeThreshold, currentFontSize));
        }

        updateTableWithStocks(currentStocks);

        JButton sortButton = (JButton) ((JPanel) ((JPanel) frame.getContentPane()).getComponent(0)).getComponent(1);
        sortButton.setText(sortAscending ? "按涨跌幅升序排序" : "按涨跌幅降序排序");
    }

    /**
     * 自定义表格渲染器
     */
    private void customizeTableRenderer() {
        updateTableFont();

        // 共享的颜色渲染器(用于涨跌幅和涨跌百分位)
        DefaultTableCellRenderer sharedRenderer = new DefaultTableCellRenderer() {
            @Override
            public Component getTableCellRendererComponent(JTable table, Object value,
                    boolean isSelected, boolean hasFocus, int row, int column) {
                Component c = super.getTableCellRendererComponent(table, value, isSelected, hasFocus, row, column);
               
                if (row < currentStocks.size()) {
                    double percentValue = 0.0;
                    
                    try {
                        // 根据列类型获取对应的值
                        if (column == 2) { // 涨跌幅列
                            Stock stock = currentStocks.get(row);
                            percentValue = stock.getChange();
                        } else if (column == 4 || column == 5) { // 涨跌百分位列
                            String valueStr = value.toString().replace("%", "").trim();
                            percentValue = Double.parseDouble(valueStr);
                        }
                        
                        // 统一的颜色显示逻辑
                        double absPercent = Math.abs(percentValue);
                        
                        if (absPercent > changeThreshold) {
                            c.setForeground(highChangeColor);
                        } else {
                            c.setForeground(normalChangeColor);
                        }
                        
                        // 正值加粗显示
                        if (percentValue > 0) {
                            c.setFont(c.getFont().deriveFont(Font.BOLD));
                        } else {
                            c.setFont(c.getFont().deriveFont(Font.PLAIN));
                        }
                        
                    } catch (Exception e) {
                        c.setForeground(normalChangeColor);
                        c.setFont(c.getFont().deriveFont(Font.PLAIN));
                    }
                }
               
                // 选中状态处理
                if (isSelected) {
                    c.setBackground(table.getSelectionBackground());
                    c.setForeground(table.getSelectionForeground());
                } else {
                    c.setBackground(table.getBackground());
                }
               
                return c;
            }
        };
        sharedRenderer.setHorizontalAlignment(SwingConstants.RIGHT);

        // 成交额渲染器
        DefaultTableCellRenderer amountRenderer = new DefaultTableCellRenderer() {
            @Override
            public Component getTableCellRendererComponent(JTable table, Object value,
                    boolean isSelected, boolean hasFocus, int row, int column) {
                Component c = super.getTableCellRendererComponent(table, value, isSelected, hasFocus, row, column);
               
                if (isSelected) {
                    c.setBackground(table.getSelectionBackground());
                } else {
                    c.setBackground(table.getBackground());
                }
               
                // 大额成交额特殊显示
                try {
                    String valueStr = value.toString().replace(",", "").replace("万元", "").trim();
                    double amount = Double.parseDouble(valueStr);
                    if (amount > 20000) { // 超过2亿元
                        c.setFont(c.getFont().deriveFont(Font.BOLD));
                        c.setForeground(new Color(0, 10, 0));
                    } else {
                        c.setFont(c.getFont().deriveFont(Font.PLAIN));
                        c.setForeground(normalChangeColor);
                    }
                } catch (Exception e) {
                    // 保持默认样式
                }
               
                return c;
            }
        };
        amountRenderer.setHorizontalAlignment(SwingConstants.RIGHT);

        DefaultTableCellRenderer rightRenderer = new DefaultTableCellRenderer();
        rightRenderer.setHorizontalAlignment(SwingConstants.RIGHT);

        DefaultTableCellRenderer leftRenderer = new DefaultTableCellRenderer();
        leftRenderer.setHorizontalAlignment(SwingConstants.LEFT);

        // 设置渲染器
        stockTable.getColumnModel().getColumn(0).setCellRenderer(leftRenderer);
        stockTable.getColumnModel().getColumn(1).setCellRenderer(rightRenderer);
        stockTable.getColumnModel().getColumn(2).setCellRenderer(sharedRenderer); // 涨跌幅
        stockTable.getColumnModel().getColumn(3).setCellRenderer(amountRenderer); // 成交额
        stockTable.getColumnModel().getColumn(4).setCellRenderer(sharedRenderer);  // 涨幅百分位
        stockTable.getColumnModel().getColumn(5).setCellRenderer(sharedRenderer);  // 跌幅百分位
        stockTable.getColumnModel().getColumn(6).setCellRenderer(rightRenderer);

        // 调整列宽适配新内容
        stockTable.getColumnModel().getColumn(0).setPreferredWidth(150);
        stockTable.getColumnModel().getColumn(1).setPreferredWidth(80);
        stockTable.getColumnModel().getColumn(2).setPreferredWidth(90);
        stockTable.getColumnModel().getColumn(3).setPreferredWidth(120); // 成交额列加宽
        stockTable.getColumnModel().getColumn(4).setPreferredWidth(110);
        stockTable.getColumnModel().getColumn(5).setPreferredWidth(110);
        stockTable.getColumnModel().getColumn(6).setPreferredWidth(80);
    }

    /**
     * 启动自动刷新
     */
    private void startAutoRefresh() {
        Timer timer = new Timer("StockRefreshTimer", true);
        timer.scheduleAtFixedRate(new TimerTask() {
            @Override
            public void run() {
                if (frame.isVisible()) {
                    SwingUtilities.invokeLater(() -> statusLabel.setText(String.format("自动刷新中...涨跌幅阈值:%s%%,当前字号:%d号",
                            changeThreshold, currentFontSize)));
                    fetchAndUpdateStockData();
                }
            }
        }, 30000, 30000);
    }

    /**
     * 获取并更新股票数据
     */
    private void fetchAndUpdateStockData() {
        new Thread(() -> {
            java.util.List<Stock> stocks = new java.util.ArrayList<>();
            AtomicInteger failedCount = new AtomicInteger(0);

            for (String code : stockCodes) {
                Stock stock = fetchSingleStockWithExponentialBackoff(code);
                if (stock != null) {
                    stocks.add(stock);
                } else {
                    failedCount.incrementAndGet();
                }

                try {
                    Thread.sleep(300);
                } catch (InterruptedException e) {
                    Thread.currentThread().interrupt();
                }
            }

            currentStocks = new java.util.ArrayList<>(stocks);

            SwingUtilities.invokeLater(() -> {
                updateTableWithStocks(stocks);

                if (failedCount.get() > 0) {
                    statusLabel.setText(String.format("刷新完成!%d 只股票加载失败. 涨跌幅阈值:%.1f%%,当前字号:%d号",
                            failedCount.get(), changeThreshold, currentFontSize));
                } else {
                    statusLabel.setText(String.format("刷新成功!下次刷新:30秒后. 涨跌幅阈值:%.1f%%,当前字号:%d号",
                            changeThreshold, currentFontSize));
                }
            });
        }, "StockDataFetcher").start();
    }

    /**
     * 更新表格数据(展示成交额)
     */
    private void updateTableWithStocks(java.util.List<Stock> stocks) {
        tableModel.setRowCount(0);
        
        for (Stock stock : stocks) {
            // 计算涨跌百分位(相对于昨收价)
            double highPercent = ((stock.getHigh() - stock.getClose()) / stock.getClose()) * 100;
            double lowPercent = ((stock.getLow() - stock.getClose()) / stock.getClose()) * 100;
            
            Object[] rowData = {
                stock.getCode() + " / " + stock.getName(),
                PRICE_FORMAT.format(stock.getPrice()),
                CHANGE_FORMAT.format(stock.getChange()) + "%",
                AMOUNT_FORMAT.format(stock.getAmount()/10000) , // 成交额(万元)
                PERCENT_FORMAT.format(highPercent) + "%",        // 涨幅百分位
                PERCENT_FORMAT.format(lowPercent) + "%",         // 跌幅百分位
                PRICE_FORMAT.format(stock.getClose())
            };
            tableModel.addRow(rowData);
        }
    }

    /**
     * 初始化系统托盘
     */
    private void setupSystemTray() {
        if (!SystemTray.isSupported()) {
            JOptionPane.showMessageDialog(frame, "您的系统不支持系统托盘功能");
            return;
        }

        systemTray = SystemTray.getSystemTray();
        Image image = createSimpleIcon();
        trayIcon = new TrayIcon(image, "自选股监控");
        trayIcon.setImageAutoSize(true);

        PopupMenu popupMenu = new PopupMenu();
        MenuItem showHideItem = new MenuItem("显示/隐藏窗口");
        MenuItem settingsItem = new MenuItem("阈值设置");
        MenuItem exitItem = new MenuItem("退出");

        showHideItem.addActionListener(e -> toggleWindowVisibility());
        settingsItem.addActionListener(e -> SwingUtilities.invokeLater(this::showSettingsDialog));
        exitItem.addActionListener(e -> {
            systemTray.remove(trayIcon);
            System.exit(0);
        });

        popupMenu.add(showHideItem);
        popupMenu.add(settingsItem);
        popupMenu.addSeparator();
        popupMenu.add(exitItem);
        trayIcon.setPopupMenu(popupMenu);

        trayIcon.addMouseListener(new MouseAdapter() {
            @Override
            public void mouseClicked(MouseEvent e) {
                if (e.getClickCount() == 2) {
                    toggleWindowVisibility();
                }
            }
        });

        try {
            systemTray.add(trayIcon);
        } catch (AWTException e) {
            JOptionPane.showMessageDialog(frame, "无法添加系统托盘图标: " + e.getMessage());
        }
    }

    /**
     * 创建托盘图标
     */
    private Image createSimpleIcon() {
        int size = 16;
        BufferedImage image = new BufferedImage(size, size, BufferedImage.TYPE_INT_ARGB);
        Graphics2D g2d = image.createGraphics();
        g2d.setColor(new Color(0, 102, 204, 255));
        g2d.fillRect(0, 0, size, size);
        g2d.setColor(Color.WHITE);
        g2d.setFont(new Font("Arial", Font.BOLD, 12));
        g2d.drawString("股", 2, 13);
        g2d.dispose();
        return image;
    }

    /**
     * 设置全局快捷键
     */
    private void setupGlobalHotkey() {
        Toolkit.getDefaultToolkit().addAWTEventListener(new AWTEventListener() {
            private boolean altPressed = false;

            @Override
            public void eventDispatched(AWTEvent event) {
                if (event instanceof KeyEvent) {
                    KeyEvent keyEvent = (KeyEvent) event;
                    if (keyEvent.getID() == KeyEvent.KEY_PRESSED) {
                        if (keyEvent.getKeyCode() == KeyEvent.VK_ALT) {
                            altPressed = true;
                        } else if (altPressed && keyEvent.getKeyCode() == KeyEvent.VK_F6) {
                            toggleWindowVisibility();
                            keyEvent.consume();
                        }
                    } else if (keyEvent.getID() == KeyEvent.KEY_RELEASED) {
                        if (keyEvent.getKeyCode() == KeyEvent.VK_ALT) {
                            altPressed = false;
                        }
                    }
                }
            }
        }, AWTEvent.KEY_EVENT_MASK);
    }

    /**
     * 切换窗口显示/隐藏
     */
    private void toggleWindowVisibility() {
        SwingUtilities.invokeLater(() -> {
            boolean isVisible = frame.isVisible();
            frame.setVisible(!isVisible);
            
            if (!isVisible) {
                frame.toFront();
                frame.requestFocus();
                statusLabel.setText(String.format("窗口已显示. Alt+F6隐藏. 涨跌幅阈值:%.1f%%,当前字号:%d号",
                        changeThreshold, currentFontSize));
                trayIcon.displayMessage("自选股监控", "窗口已显示", TrayIcon.MessageType.INFO);
            } else {
                statusLabel.setText("窗口已隐藏. Alt+F6显示.");
                trayIcon.displayMessage("自选股监控", "窗口已隐藏", TrayIcon.MessageType.INFO);
            }
        });
    }

    /**
     * 单股票获取(解析成交额)
     */
    private Stock fetchSingleStockWithExponentialBackoff(String code) {
        for (int retry = 0; retry < MAX_RETRIES; retry++) {
            HttpURLConnection conn = null;
            try {
                long retryDelay = INITIAL_RETRY_DELAY * (1 << retry);
                if (retry > 0) {
                    Thread.sleep(retryDelay);
                }
                String encodedCode = URLEncoder.encode(code, "UTF-8");
                URL url = new URL("https://hq.sinajs.cn/list=" + encodedCode);
                conn = (HttpURLConnection) url.openConnection();
                conn.setRequestMethod("GET");
                conn.setRequestProperty("User-Agent", USER_AGENT);
                conn.setRequestProperty("Referer", REFERER);
                conn.setRequestProperty("Accept", ACCEPT);
                conn.setRequestProperty("Accept-Encoding", ACCEPT_ENCODING);
                conn.setRequestProperty("Accept-Language", ACCEPT_LANGUAGE);
                conn.setConnectTimeout(BASE_CONNECT_TIMEOUT + (retry * 5000));
                conn.setReadTimeout(BASE_READ_TIMEOUT + (retry * 5000));
                if (conn.getResponseCode() != 200) {
                    continue;
                }
                String responseData = readResponse(conn);
                if (responseData == null || !responseData.contains("hq_str_")) {
                    continue;
                }
                Stock stock = parseStockData(responseData, code);
                if (stock != null) {
                    return stock;
                }
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
                return null;
            } catch (Exception e) {
                System.err.println("获取股票数据失败(" + code + "):" + e.getMessage());
                continue;
            } finally {
                if (conn != null) {
                    conn.disconnect();
                }
            }
        }
        return null;
    }

    /**
     * 读取响应数据
     */
    private String readResponse(HttpURLConnection conn) throws IOException {
        String contentEncoding = conn.getContentEncoding();
        InputStream in = conn.getInputStream();
        if (contentEncoding != null && contentEncoding.contains("gzip")) {
            in = new GZIPInputStream(in);
        }
        String[] encodings = {"GBK", "GB18030", "UTF-8"};
        for (String encoding : encodings) {
            try {
                BufferedReader reader = new BufferedReader(new InputStreamReader(in, encoding));
                StringBuilder sb = new StringBuilder();
                String line;
                while ((line = reader.readLine()) != null) {
                    sb.append(line);
                }
                reader.close();
                in.close();
                return sb.toString().trim();
            } catch (IOException e) {
                continue;
            }
        }
        return null;
    }

    /**
     * 解析股票数据(提取成交额)
     */
    private Stock parseStockData(String data, String code) {
        try {
            String[] parts = data.split("=");
            if (parts.length < 2) {
                System.err.println("解析失败(" + code + "):数据格式错误");
                return null;
            }
            String quotePart = parts[1].replaceAll("\"|;", "").trim();
            String[] stockFields = quotePart.split(",");
            
            if (stockFields.length < 10) {
                System.err.println("解析失败(" + code + "):核心字段不足(实际" + stockFields.length + "个)");
                return null;
            }
            
            String name = stockFields[0].trim().isEmpty() ? "未知名称" : stockFields[0].trim();
            double close = parseDoubleSafely(stockFields[2], 0.0);
            double price = parseDoubleSafely(stockFields[3], 0.0);
            double high = parseDoubleSafely(stockFields[4], 0.0);
            double low = parseDoubleSafely(stockFields[5], 0.0);
            double volume = parseDoubleSafely(stockFields[8], 0.0);
            double amount = parseDoubleSafely(stockFields[9], 0.0); // 成交额(万元)
            
            double change = (close == 0) ? 0.0 : ((price - close) / close) * 100;
            
            if (price <= 0) {
                System.err.println("解析失败(" + code + "):最新价无效(" + stockFields[3] + ")");
                return null;
            }
            
            return new Stock(code, name, price, change, volume, amount, 0.0, high, low, close);
        } catch (Exception e) {
            System.err.println("解析股票数据失败(" + code + "):" + e.getMessage());
            return null;
        }
    }
   
    /**
     * 安全解析double
     */
    private double parseDoubleSafely(String str, double defaultValue) {
        if (str == null || str.trim().isEmpty()) {
            return defaultValue;
        }
        try {
            return Double.parseDouble(str.trim());
        } catch (NumberFormatException e) {
            return defaultValue;
        }
    }

    /**
     * 股票数据模型(包含成交额)
     */
    public static class Stock {
        private final String code;
        private final String name;
        private final double price;
        private final double change;
        private final double volume;
        private final double amount; // 成交额(万元)
        private final double open;
        private final double high;
        private final double low;
        private final double close;

        public Stock(String code, String name, double price, double change, double volume,
                    double amount, double open, double high, double low, double close) {
            this.code = code;
            this.name = name;
            this.price = price;
            this.change = change;
            this.volume = volume;
            this.amount = amount;
            this.open = open;
            this.high = high;
            this.low = low;
            this.close = close;
        }

        public String getCode() { return code; }
        public String getName() { return name; }
        public double getPrice() { return price; }
        public double getChange() { return change; }
        public double getVolume() { return volume; }
        public double getAmount() { return amount; } // 成交额getter
        public double getHigh() { return high; }
        public double getLow() { return low; }
        public double getClose() { return close; }
    }

    /**
     * 主方法
     */
    public static void main(String[] args) {
        SwingUtilities.invokeLater(StockMonitorApp::new);
    }
}
4#
 楼主| raccooncp 发表于 2025-12-4 16:32 |楼主
本帖最后由 raccooncp 于 2025-12-4 16:48 编辑

代码编译教程(适合程序员 / 二次开发)
如果需要自行编译源码(验证安全性或定制功能),按以下步骤操作:
一、准备环境
安装 JDK:需 JDK 8 及以上版本(推荐 JDK 11,兼容性最好)
配置环境变量:安装后设置 JAVA_HOME 为 JDK 安装路径,Path 中添加 %JAVA_HOME%\bin(Windows 系统)
验证环境:打开 CMD 输入 java -version 和 javac -version,显示版本号即配置成功
二、编译步骤
保存源码:将完整代码复制到文本编辑器,保存为 StockMonitorApp.java(文件名必须与类名一致,大小写敏感)
打开编译目录:在源码所在文件夹按住 Shift + 右键,选择「在此处打开命令窗口」或「打开 PowerShell 窗口」
执行编译命令:输入以下命令(一行完成),编译生成 class 文件:
javac StockMonitorApp.java

(若提示 "编码 GBK 的不可映射字符",添加编码参数:javac -encoding UTF-8 StockMonitorApp.java)
生成可执行 JAR 包(可选,方便运行):
先创建清单文件 MANIFEST.MF,内容如下(注意最后一行空行必须保留):
Main-Class: StockMonitorApp
Class-Path: .

执行打包命令:
jar cvfm StockMonitor.jar StockMonitorApp.class MANIFEST.MF

运行程序:
运行 JAR 包:命令行 java -jar StockMonitor.jar
三、常见问题解决
编译报错「找不到符号」:检查源码是否完整复制,文件名与类名是否一致,JDK 版本是否过低
运行时闪退:确认系统已安装 JRE(JDK 包含 JRE),或重新配置环境变量
网络请求失败:检查防火墙是否拦截程序网络访问,确保能正常访问新浪财经网站
中文乱码:编译时添加 -encoding UTF-8 参数,运行时确保系统编码为 UTF-8
编译后的程序与原版功能完全一致,所有配置文件(股票代码、设置参数)会自动生成在程序运行目录,可放心使用!
5#
zt185 发表于 2025-12-6 16:04
这个我需要,上班正好用得上
6#
lgg51 发表于 2025-12-6 17:42
谢谢楼主,看看,学习学习。
7#
lgg51 发表于 2025-12-6 17:50
下载解压后,直接双击程序文件即可启动,没有找到启动文件,不会用
8#
wpdzdx 发表于 2025-12-7 13:29
不会用,是不是少了文件呀
9#
freedover 发表于 2025-12-7 16:51
哪位大咔打包成一个绿色文件吧,不会使用啊
10#
wenganan 发表于 2025-12-8 03:23
下载完就两个文件,没有运行程序啊,怎么用?
您需要登录后才可以回帖 登录 | 注册[Register]

本版积分规则

返回列表

RSS订阅|小黑屋|处罚记录|联系我们|吾爱破解 - 52pojie.cn ( 京ICP备16042023号 | 京公网安备 11010502030087号 )

GMT+8, 2026-10-16 11:40

Powered by Discuz!

Copyright © 2001-2020, Tencent Cloud.

快速回复 返回顶部 返回列表