# -*- coding: utf-8 -*-
import sys
import pandas as pd
import tkinter as tk
from tkinter import ttk, filedialog, messagebox
from tkinter.scrolledtext import ScrolledText
import os
import json
import akshare as ak
import datetime
from threading import Thread
import threading
import numpy as np
from PIL import Image, ImageTk # 需要先安装 pillow 包:pip install pillow
from pathlib import Path
import signal # 新增导入
def resource_path(relative_path):
"""处理资源路径的跨平台问题"""
if hasattr(sys, '_MEIPASS'):
base_path = sys._MEIPASS
else:
base_path = os.path.abspath(".")
return os.path.join(base_path, relative_path)
class ExcelBrowser:
def __init__(self, master):
self.master = master
try:
icon_path = resource_path("icons/hicolor/256x256/apps/eagle_eyes.png")
if sys.platform == 'win32':
# Windows 使用 ICO
ico_path = resource_path("icons/windows/eagle_eyes.ico")
master.iconbitmap(default=ico_path)
print('windows')
else:
# Linux 使用 PNG
img = Image.open(icon_path)
self.tk_icon = ImageTk.PhotoImage(img)
master.tk.call('wm', 'iconphoto', master._w, self.tk_icon)
print('linux')
except Exception as e:
print(f"Icon load error: {str(e)}")
# 使用 Tk 内置默认图标
master.iconbitmap(default='')
master.title("鹰眼")
master.geometry("1400x800") # 恢复原来的窗口大小
self.fullscreen_state = False
self.font_style = ('Microsoft YaHei', 10) # 调整字体大小为10
self.text_color = 'black'
self.tree_row_height = 26 # 调整行高为26
self.sort_states = {}
self.current_sort_column = None
self.current_df = None
self.original_df = None # 保存原始数据
# 添加数据历史记录
self.data_history = [] # 用于存储历史数据
self.current_data_index = -1 # 当前数据在历史记录中的索引
# 添加自选股票集合
self.favorite_stocks = set()
self.favorite_file = "favorite_stocks.json"
self.load_favorites()
self.settings_file = "app_settings.json"
self.current_price_range = (10, 50)
self.current_change_range = (10, 11)
self.current_volume_ratio_range = (1.5, 3.0)
self.current_volume_range = (10000, 50000) # 默认成交量范围(手)
self.current_turnover_rate_range = (3.0, 10.0)
self.current_pe_range = (0, 30) # 添加市盈率-动态范围
self.current_amount_range = (5000, 50000) # 成交额范围(万元)
self.current_market_cap_range = (1e9, 5e10)
self.load_settings()
# 添加筛选状态
self.filter_states = {
'price': False,
'change': False,
'volume_ratio': False,
'volume': False, # 新增成交量筛选状态
'turnover_rate': False,
'pe': False,
'amount': False,
'market_cap': False
}
self._setup_style()
self.create_widgets()
self._bind_events()
self._update_time() # 启动时间更新
self.help_dialog = None # 添加帮助窗口引用
master.protocol("WM_DELETE_WINDOW", self.quit_app)
signal.signal(signal.SIGINT, self.handle_signal)
def handle_signal(self, signum, frame):
"""处理SIGINT信号"""
self.master.after(0, self.quit_app)
def quit_app(self):
"""安全退出程序"""
self.master.destroy()
self.master.quit() # 确保完全退出主循环
def load_favorites(self):
"""加载自选股票"""
try:
if os.path.exists(self.favorite_file):
with open(self.favorite_file, 'r', encoding='utf-8') as f:
self.favorite_stocks = set(json.load(f))
except Exception as e:
self.log(f"加载自选股票失败: {str(e)}", "error")
def save_favorites(self):
"""保存自选股票"""
try:
with open(self.favorite_file, 'w', encoding='utf-8') as f:
json.dump(list(self.favorite_stocks), f, ensure_ascii=False)
except Exception as e:
self.log(f"保存自选股票失败: {str(e)}", "error")
def load_settings(self):
try:
if os.path.exists(self.settings_file):
with open(self.settings_file, 'r') as f:
settings = json.load(f)
self.current_price_range = tuple(settings.get('price_range', (10, 50)))
self.current_market_cap_range = tuple(settings.get('market_cap_range', (1e9, 5e10)))
self.current_volume_ratio_range = tuple(settings.get('volume_ratio_range', (1.5, 3.0)))
self.current_volume_range = tuple(settings.get('volume_range', (10000, 50000)))
self.current_turnover_rate_range = tuple(settings.get('turnover_rate_range', (3.0, 10.0)))
self.current_amount_range = tuple(settings.get('amount_range', (5000, 50000))) # 添加成交额范围
self.current_pe_range = tuple(settings.get('pe_range', (0, 30))) # 添加市盈率-动态范围
self.current_change_range = tuple(settings.get('change_range', (10, 11)))
except Exception as e:
self.log(f"加载设置失败: {str(e)}", "error")
def save_settings(self):
settings = {
'price_range': self.current_price_range,
'market_cap_range': self.current_market_cap_range,
'volume_ratio_range': self.current_volume_ratio_range,
'volume_range': self.current_volume_range,
'turnover_rate_range': self.current_turnover_rate_range,
'amount_range': self.current_amount_range, # 添加成交额范围
'pe_range': self.current_pe_range , # 添加市盈率-动态范围
'change_range': self.current_change_range
}
try:
with open(self.settings_file, 'w') as f:
json.dump(settings, f)
except Exception as e:
self.log(f"保存设置失败: {str(e)}", "error")
def _setup_style(self):
self.style = ttk.Style()
self.style.configure('TButton', font=self.font_style, foreground=self.text_color)
self.style.configure('Treeview', font=self.font_style, rowheight=self.tree_row_height, foreground=self.text_color)
self.style.configure('Treeview.Heading', font=('Microsoft YaHei', 12, 'bold'), foreground=self.text_color)
self.style.configure('Toggled.TButton', font=self.font_style, foreground=self.text_color, background='lightblue')
def create_widgets(self):
control_frame = ttk.Frame(self.master)
control_frame.pack(pady=3, fill=tk.X)
# 左侧按钮容器
left_buttons_frame = ttk.Frame(control_frame)
left_buttons_frame.pack(side=tk.LEFT, fill=tk.X)
# 最新价按钮
self.price_btn = ttk.Button(
left_buttons_frame,
text=f"最新价{self.current_price_range[0]}-{self.current_price_range[1]}"
)
self.price_btn.pack(side=tk.LEFT, padx=2)
self.price_btn.bind('<Button-1>', self.on_price_click)
self.price_btn.bind('<Double-Button-1>', self.on_price_double_click)
self.change_btn = ttk.Button(
left_buttons_frame,
text=f"涨跌幅{self.current_change_range[0]}%-{self.current_change_range[1]}%"
)
self.change_btn.pack(side=tk.LEFT, padx=2)
self.change_btn.bind('<Button-1>', self.on_change_click)
self.change_btn.bind('<Double-Button-1>', self.on_change_double_click)
self.volume_btn = ttk.Button(
left_buttons_frame,
text=f"成交量{self.current_volume_range[0]//10000}-{self.current_volume_range[1]//10000}万手"
)
self.volume_btn.pack(side=tk.LEFT, padx=2)
self.volume_btn.bind('<Button-1>', self.on_volume_click)
self.volume_btn.bind('<Double-Button-1>', self.on_volume_double_click)
#成交额按钮
self.amount_btn = ttk.Button(
left_buttons_frame,
text=f"成交额{self.current_amount_range[0]}-{self.current_amount_range[1]}万"
)
self.amount_btn.pack(side=tk.LEFT, padx=2)
self.amount_btn.bind('<Button-1>', self.on_amount_click)
self.amount_btn.bind('<Double-Button-1>', self.on_amount_double_click)
# 换手率按钮
self.turnover_rate_btn = ttk.Button(
left_buttons_frame,
text=f"换手率{self.current_turnover_rate_range[0]}%-{self.current_turnover_rate_range[1]}%"
)
self.turnover_rate_btn.pack(side=tk.LEFT, padx=2)
self.turnover_rate_btn.bind('<Button-1>', self.on_turnover_rate_click)
self.turnover_rate_btn.bind('<Double-Button-1>', self.on_turnover_rate_double_click)
# 量比按钮
self.volume_ratio_btn = ttk.Button(
left_buttons_frame,
text=f"量比{self.current_volume_ratio_range[0]}-{self.current_volume_ratio_range[1]}"
)
self.volume_ratio_btn.pack(side=tk.LEFT, padx=2)
self.volume_ratio_btn.bind('<Button-1>', self.on_volume_ratio_click)
self.volume_ratio_btn.bind('<Double-Button-1>', self.on_volume_ratio_double_click)
# 市盈率-动态按钮
self.pe_btn = ttk.Button(
left_buttons_frame,
text=f"市盈率-动态{self.current_pe_range[0]}-{self.current_pe_range[1]}"
)
self.pe_btn.pack(side=tk.LEFT, padx=2)
self.pe_btn.bind('<Button-1>', self.on_pe_click)
self.pe_btn.bind('<Double-Button-1>', self.on_pe_double_click)
# 总市值按钮
self.market_cap_btn = ttk.Button(
left_buttons_frame,
text=f"总市值{self.current_market_cap_range[0]:,.0f}-{self.current_market_cap_range[1]:,.0f}"
)
self.market_cap_btn.pack(side=tk.LEFT, padx=2)
self.market_cap_btn.bind('<Button-1>', self.on_market_cap_click)
self.market_cap_btn.bind('<Double-Button-1>', self.on_market_cap_double_click)
# 右侧状态和时间显示
right_frame = ttk.Frame(control_frame)
right_frame.pack(side=tk.RIGHT, fill=tk.X)
# 时间标签
self.time_label = ttk.Label(
right_frame,
font=self.font_style,
foreground=self.text_color
)
self.time_label.pack(side=tk.RIGHT, padx=10)
# 状态标签
self.status_label = ttk.Label(right_frame, text="")
self.status_label.pack(side=tk.RIGHT, padx=10)
self.log_area = ScrolledText(self.master, height=3, font=self.font_style, foreground=self.text_color)
self.log_area.pack(pady=2, fill=tk.X)
self.log_area.configure(state=tk.DISABLED)
# 创建树形视图和滚动条的容器
tree_frame = ttk.Frame(self.master)
tree_frame.pack(fill=tk.BOTH, expand=True)
# 创建树形视图
self.tree = ttk.Treeview(tree_frame, show="headings", columns=("Index",), selectmode="browse")
self.tree.heading("Index", text="序号", command=lambda: self.sort_treeview("Index"))
self.tree.column("Index", width=60, anchor='center')
# 创建垂直滚动条
vsb = ttk.Scrollbar(tree_frame, orient="vertical", command=self.tree.yview)
vsb.pack(side=tk.RIGHT, fill=tk.Y)
# 创建水平滚动条
hsb = ttk.Scrollbar(self.master, orient="horizontal", command=self.tree.xview)
hsb.pack(side=tk.BOTTOM, fill=tk.X)
# 配置树形视图的滚动
self.tree.configure(yscrollcommand=vsb.set, xscrollcommand=hsb.set)
self.tree.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
def _bind_events(self):
self.master.bind('<F11>', self.toggle_fullscreen)
self.master.bind('<Escape>', lambda e: self.master.attributes('-fullscreen', False))
self.master.bind('<Control-o>', self.load_and_show_file)
self.master.bind('<Control-r>', self.manual_refresh_data)
self.master.bind('<Control-s>', self.save_current_data)
self.master.bind('<Control-f>', self.open_search_dialog)
self.master.bind('<Control-i>', self.show_favorites)
self.master.bind('<Control-z>', self.undo_last_display) # 添加Ctrl+Z快捷键
self.master.bind('<Control-c>', self.copy_selected_row)
self.master.bind('<Control-d>',self.copy_selected_names)
self.tree.bind('<Up>', lambda e: self.tree.yview_scroll(-1, "units"))
self.tree.bind('<Down>', lambda e: self.tree.yview_scroll(1, "units"))
self.tree.bind('<Button-3>', self.show_context_menu) # 右键菜单
self.tree.bind('<Button-1>', self.on_tree_click) # 左键点击
self.master.bind('<Control-a>', self.select_all_rows) # 新增全选绑定
self.master.bind('<Control-h>', self.show_help) # 添加Ctrl+H绑定
self.tree.bind('<Double-Button-1>', self.open_stock_info)
def generate_stock_url(self, stock_code):
"""生成股票信息URL(以新浪财经为例)"""
code = stock_code[2:] # 去除市场前缀(sh/sz)
return f"https://finance.sina.com.cn/realstock/company/{stock_code}/nc.shtml"
def open_stock_info(self, event):
item = self.tree.identify_row(event.y)
if item:
try:
stock_code = self.tree.item(item)['values'][1] # 假设代码在第2列
url = self.generate_stock_url(stock_code)
import webbrowser
webbrowser.open(url)
except Exception as e:
self.log(f"跳转失败:{str(e)}", "error")
def manual_refresh_data(self, event=None):
Thread(target=self._update_data).start()
def _update_data(self):
"""更新数据的具体实现"""
try:
self.master.after(0, lambda: self.log("开始获取A股数据...", "info"))
# 获取A股实时数据
try:
df = ak.stock_zh_a_spot_em()
if df is None or df.empty:
self.master.after(0, lambda: self.log("akshare返回数据为空,可能是非交易时段或网络问题", "error"))
return
# 添加市盈率列
df['市盈率'] = df['市盈率-动态']
self.master.after(0, lambda: self.log(f"成功获取基础数据,共 {len(df)} 条记录", "info"))
except Exception as e:
self.master.after(0, lambda: self.log(f"获取基础数据失败: {str(e)}", "error"))
return
# 使用正确的列名映射
rename_dict = {
'代码': '代码',
'名称': '名称',
'最新价': '最新价',
'涨跌幅': '涨跌幅',
'成交量': '成交量',
'成交额': '成交额',
'换手率': '换手率',
'量比': '量比',
'市盈率-动态': '市盈率-动态',
'总市值': '总市值'
}
# 检查所需列是否存在
missing_cols = [col for col in rename_dict.keys() if col not in df.columns]
if missing_cols:
self.master.after(0, lambda: self.log(f"缺少以下列: {', '.join(missing_cols)}", "warning"))
# 只选择需要的列
available_cols = [col for col in rename_dict.keys() if col in df.columns]
df_final = df[available_cols].copy()
# 数据类型转换(在重命名列之前进行)
try:
# 处理百分比列
df_final['涨跌幅'] = pd.to_numeric(df_final['涨跌幅'], errors='coerce')
df_final['换手率'] = pd.to_numeric(df_final['换手率'], errors='coerce')
df_final['市盈率'] = pd.to_numeric(df_final['市盈率'], errors='coerce')
# 处理其他数值列
df_final['最新价'] = pd.to_numeric(df_final['最新价'], errors='coerce')
df_final['成交量'] = pd.to_numeric(df_final['成交量'], errors='coerce')
df_final['成交额'] = pd.to_numeric(df_final['成交额'], errors='coerce')
df_final['量比'] = pd.to_numeric(df_final['量比'], errors='coerce')
df_final['总市值'] = pd.to_numeric(df_final['总市值'], errors='coerce')
except Exception as e:
self.master.after(0, lambda e=e: self.log(f"数据类型转换出错: {str(e)}", "error"))
# 重命名列
df_final.columns = [rename_dict[col] for col in available_cols]
# 添加股票代码前缀
df_final['代码'] = df_final['代码'].astype(str).apply(
lambda x: f"sh{x}" if str(x).startswith(('6', '68', '7')) else f"sz{x}" if str(x).startswith(('0', '3')) else f"bj{x}"
)
# 更新数据
self.original_df = df_final.copy()
self.current_df = df_final.copy()
self.master.after(0, self.show_data)
self.master.after(0, lambda: self.log(f"数据刷新成功,共 {len(df_final)} 条记录", "success"))
# 重置筛选状态
self.filter_states = {key: False for key in self.filter_states}
# 重置按钮样式
self.price_btn.configure(style='TButton')
self.market_cap_btn.configure(style='TButton')
self.volume_ratio_btn.configure(style='TButton')
self.turnover_rate_btn.configure(style='TButton')
self.amount_btn.configure(style='TButton')
self.pe_btn.configure(style='TButton')
except Exception as e:
self.master.after(0, lambda: self.log(f"数据获取失败: {str(e)}", "error"))
import traceback
self.master.after(0, lambda: self.log(f"错误详情: {traceback.format_exc()}", "error"))
def get_market_data(self):
try:
df = ak.stock_zh_a_spot_em()
df['带前缀代码'] = df['代码'].apply(self.add_market_prefix)
selected_columns = {
'带前缀代码': '代码',
'名称': '名称',
'最新价': '最新价',
'涨跌幅': '涨跌幅',
'成交量': '成交量',
'成交额': '成交额',
'换手率': '换手率',
'量比': '量比',
'市盈率-动态': '市盈率-动态',
'总市值': '总市值'
}
df = df[list(selected_columns.keys())].rename(columns=selected_columns)
numeric_cols = ['最新价', '涨跌幅', '成交量','成交额','换手率', '量比','市盈率-动态','总市值']
df[numeric_cols] = df[numeric_cols].apply(pd.to_numeric, errors='coerce')
return df.dropna()
except Exception as e:
self.log(f"数据获取失败: {str(e)}", "error")
return None
def load_and_show_file(self, event=None):
file = filedialog.askopenfilename(title="选择Excel文件",filetypes=[("Excel文件", "*.xlsx *.xls")])
if not file: return
try:
df = pd.read_excel(file)
df.columns = df.columns.str.strip()
column_mapping = {
'代码': '代码',
'名称': '名称',
'最新价': '最新价',
'涨跌幅': '涨跌幅',
'成交量': '成交量',
'成交额': '成交额',
'换手率': '换手率',
'量比': '量比',
'市盈率-动态': '市盈率-动态',
'总市值': '总市值'
}
df.rename(columns=column_mapping, inplace=True)
required_columns = {'代码', '名称', '最新价', '涨跌幅','成交量','成交额','换手率', '量比','市盈率-动态', '总市值'}
missing_cols = required_columns - set(df.columns)
if missing_cols:
self.log(f"文件 {os.path.basename(file)} 缺少必要列: {missing_cols},已跳过", "error")
return
numeric_cols = ['最新价', '涨跌幅', '成交量','成交额' ,'换手率', '量比', '市盈率-动态','总市值']
df[numeric_cols] = df[numeric_cols].apply(pd.to_numeric, errors='coerce')
self.original_df = df.copy()
self.current_df = df.copy()
self.show_data()
self.log(f"已加载并显示: {os.path.basename(file)} ({len(df)}行)", "success")
except Exception as e:
self.log(f"加载失败: {os.path.basename(file)} - {str(e)}", "error")
def save_current_data(self, event=None):
if self.current_df is not None and not self.current_df.empty:
filename = f"{datetime.datetime.now().strftime('%Y%m%d_%H%M%S')}_筛选股票数据.xlsx"
try:
self.current_df.to_excel(filename, index=False)
self.log(f"数据已保存至: {filename}", "success")
self.status_label.config(text=f"最后保存: {filename}")
except Exception as e:
self.log(f"保存失败: {str(e)}", "error")
else:
self.log("没有可保存的数据", "warning")
def add_market_prefix(self, code):
code_str = str(code)
if code_str.startswith('6'):
return f'sh{code_str}'
elif code_str.startswith(('0', '3')):
return f'sz{code_str}'
elif code_str.startswith(('4', '8')):
return f'bj{code_str}'
return code_str
def show_data(self):
"""显示数据时保存历史记录"""
self.tree.delete(*self.tree.get_children())
if self.current_df is not None and not self.current_df.empty:
# 在显示数据之前保存到历史记录
self.add_to_history(self.current_df)
# 重新排列列的顺序
columns = ["Index", "代码", "名称", "最新价", "涨跌幅", "成交量",
"成交额", "换手率", "量比", "市盈率-动态", "总市值"]
# 确保所有列都存在
existing_columns = ["Index"] + list(self.current_df.columns)
display_columns = [col for col in columns if col in existing_columns]
self.tree["columns"] = display_columns
for col in display_columns:
if col == "Index":
self.tree.heading(col, text="序号", command=lambda c=col: self.sort_treeview(c))
self.tree.column(col, width=60, anchor='center')
else:
self.tree.heading(col, text=col, command=lambda c=col: self.sort_treeview(c))
self.tree.column(col, width=self.get_column_width(col), anchor='w')
# 重新组织数据以匹配新的列顺序
for idx, row in enumerate(self.current_df.iterrows(), start=1):
row_data = row[1]
values = [idx]
for col in display_columns[1:]: # Skip "Index" as it's already added
values.append(row_data.get(col, ""))
self.tree.insert("", tk.END, values=values)
self.show_statistics()
def get_column_width(self, col):
widths = {
"代码": 100,
"名称": 100,
"最新价": 80,
"涨跌幅": 80,
"成交量": 100,
"成交额": 120,
"换手率": 80,
"量比": 70,
"总市值": 120,
"市盈率-动态": 80,
"Index": 60
}
return widths.get(col, 100)
def reset_display(self, event=None):
if hasattr(self, 'search_dialog') and self.search_dialog.winfo_exists():
self.search_dialog.destroy()
if self.current_df is not None:
self._refresh_display()
self.log("已恢复显示全部数据", "info")
def open_search_dialog(self, event=None):
self.search_dialog = tk.Toplevel(self.master)
self.search_dialog.title("股票搜索")
self.search_dialog.geometry("300x100")
ttk.Label(self.search_dialog, text="输入搜索关键词:").pack(pady=5)
self.search_entry = ttk.Entry(self.search_dialog)
self.search_entry.pack(pady=5)
self.search_entry.focus_set()
self.search_entry.bind('<Return>', lambda : self.perform_search())
button_frame = ttk.Frame(self.search_dialog)
button_frame.pack()
ttk.Button(button_frame, text="搜索", command=self.perform_search).pack(side=tk.LEFT, padx=5)
ttk.Button(button_frame, text="取消", command=self.search_dialog.destroy).pack(side=tk.RIGHT, padx=5)
def perform_search(self, event=None):
keyword = self.search_entry.get().strip().lower()
if not keyword:
messagebox.showwarning("警告", "请输入搜索关键词")
return
if self.current_df is None:
messagebox.showwarning("警告", "请先加载数据")
return
mask = (self.current_df['代码'].astype(str).str.lower().str.contains(keyword) |
self.current_df['名称'].astype(str).str.lower().str.contains(keyword))
if mask.any():
self.tree.delete(*self.tree.get_children())
for idx, (_, row) in enumerate(self.current_df[mask].iterrows(), start=1):
values = (idx,) + tuple(row)
self.tree.insert("", tk.END, values=values)
self.search_dialog.destroy()
else:
messagebox.showinfo("提示", "未找到匹配结果")
def toggle_fullscreen(self, event=None):
if not self.fullscreen_state:
# 如果不是全屏,先最大化再全屏
#self.master.state('zoomed') # Windows下最大化窗口
self.master.update() # 确保最大化完成
self.master.attributes('-fullscreen', True)
else:
# 如果是全屏,直接退出全屏
self.master.attributes('-fullscreen', False)
self.fullscreen_state = not self.fullscreen_state
return "break"
def log(self, message, tag=None):
self.log_area.configure(state=tk.NORMAL)
self.log_area.insert(tk.END, message + "\n", tag)
self.log_area.configure(state=tk.DISABLED)
self.log_area.see(tk.END)
def sort_treeview(self, col):
"""排序功能"""
if self.current_df is None or self.current_df.empty:
return
current_state = self.sort_states.get(col, None)
new_order = not current_state if col == self.current_sort_column else True
try:
if col == "Index":
sorted_df = self.current_df.iloc[::-1] if not new_order else self.current_df
else:
sorted_df = self.current_df.sort_values(
by=col,
ascending=new_order,
key=lambda x: x.astype(str).str.lower() if x.dtype == "object" else x
)
self.current_df = sorted_df.reset_index(drop=True)
self.show_data()
# 更新排序状态
self.sort_states[col] = new_order
self.current_sort_column = col
self._update_sort_indicator(col, new_order)
except Exception as e:
messagebox.showerror("排序错误", f"排序失败:{str(e)}")
def _update_sort_indicator(self, col, ascending):
indicator = " ↑" if ascending else " ↓"
for column in self.tree["columns"]:
current_text = self.tree.heading(column)["text"]
if column == col:
new_text = current_text.split(" ↑")[0].split(" ↓")[0] + indicator
else:
new_text = current_text.split(" ↑")[0].split(" ↓")[0]
self.tree.heading(column, text=new_text)
def _refresh_display(self):
self.show_data()
def apply_all_filters(self):
"""应用所有激活的筛选条件"""
# 从原始数据开始
self.current_df = self.original_df.copy()
# 记录激活的筛选条件
active_filters = []
# 应用最新价筛选
if self.filter_states['price']:
min_val, max_val = self.current_price_range
mask = (self.current_df['最新价'] >= min_val) & (self.current_df['最新价'] <= max_val)
self.current_df = self.current_df[mask]
active_filters.append(f"最新价{min_val}-{max_val}")
if self.filter_states['change']:
min_val, max_val = self.current_change_range
mask = (self.current_df['涨跌幅'] >= min_val) & (self.current_df['涨跌幅'] <= max_val)
self.current_df = self.current_df[mask]
active_filters.append(f"涨跌幅{min_val}%-{max_val}%")
# 应用总市值筛选
if self.filter_states['market_cap']:
min_val, max_val = self.current_market_cap_range
mask = (self.current_df['总市值'] >= min_val) & (self.current_df['总市值'] <= max_val)
self.current_df = self.current_df[mask]
active_filters.append(f"总市值{min_val:,.0f}-{max_val:,.0f}")
# 应用量比筛选
if self.filter_states['volume_ratio']:
min_val, max_val = self.current_volume_ratio_range
mask = (self.current_df['量比'] >= min_val) & (self.current_df['量比'] <= max_val)
self.current_df = self.current_df[mask]
active_filters.append(f"量比{min_val}-{max_val}")
if self.filter_states['volume']:
min_val, max_val = self.current_volume_range
mask = (self.current_df['成交量'] >= min_val) & (self.current_df['成交量'] <= max_val)
self.current_df = self.current_df[mask]
active_filters.append(f"成交量{min_val//10000}-{max_val//10000}万手")
# 应用换手率筛选
if self.filter_states['turnover_rate']:
min_val, max_val = self.current_turnover_rate_range
mask = (self.current_df['换手率'] >= min_val) & (self.current_df['换手率'] <= max_val)
self.current_df = self.current_df[mask]
active_filters.append(f"换手率{min_val}%-{max_val}%")
# 应用成交额筛选
if self.filter_states['amount']:
min_val, max_val = self.current_amount_range
mask = (self.current_df['成交额'] >= min_val * 1e4) & (self.current_df['成交额'] <= max_val * 1e4)
self.current_df = self.current_df[mask]
active_filters.append(f"成交额{min_val}-{max_val}万")
# 应用市盈率-动态筛选
if self.filter_states['pe']:
min_val, max_val = self.current_pe_range
mask = (self.current_df['市盈率-动态'] >= min_val) & (self.current_df['市盈率-动态'] <= max_val)
self.current_df = self.current_df[mask]
active_filters.append(f"市盈率-动态{min_val}-{max_val}")
# 显示数据
self.show_data()
# 显示筛选信息
if active_filters:
self.log(f"符合条件记录数: {len(self.current_df)}", "info")
else:
self.log(f"已清除所有筛选,显示全部 {len(self.current_df)} 条记录", "info")
def filter_by_price(self):
"""最新价筛选"""
if self.original_df is None or self.original_df.empty:
messagebox.showwarning("警告", "请先加载数据")
return
# 切换状态
self.filter_states['price'] = not self.filter_states['price']
self.price_btn.configure(style='Toggled.TButton' if self.filter_states['price'] else 'TButton')
# 应用所有筛选条件
self.apply_all_filters()
def filter_by_change(self):
"""涨跌幅筛选"""
if self.original_df is None or self.original_df.empty:
messagebox.showwarning("警告", "请先加载数据")
return
# 切换状态
self.filter_states['change'] = not self.filter_states['change']
self.change_btn.configure(style='Toggled.TButton' if self.filter_states['change'] else 'TButton')
# 应用所有筛选条件
self.apply_all_filters()
def filter_by_market_cap(self):
"""总市值筛选"""
if self.original_df is None or self.original_df.empty:
messagebox.showwarning("警告", "请先加载数据")
return
# 切换状态
self.filter_states['market_cap'] = not self.filter_states['market_cap']
self.market_cap_btn.configure(style='Toggled.TButton' if self.filter_states['market_cap'] else 'TButton')
# 应用所有筛选条件
self.apply_all_filters()
def filter_by_volume_ratio(self):
"""量比筛选"""
if self.original_df is None or self.original_df.empty:
messagebox.showwarning("警告", "请先加载数据")
return
# 切换状态
self.filter_states['volume_ratio'] = not self.filter_states['volume_ratio']
self.volume_ratio_btn.configure(style='Toggled.TButton' if self.filter_states['volume_ratio'] else 'TButton')
# 应用所有筛选条件
self.apply_all_filters()
def filter_by_turnover_rate(self):
"""换手率筛选"""
if self.original_df is None or self.original_df.empty:
messagebox.showwarning("警告", "请先加载数据")
return
# 切换状态
self.filter_states['turnover_rate'] = not self.filter_states['turnover_rate']
self.turnover_rate_btn.configure(style='Toggled.TButton' if self.filter_states['turnover_rate'] else 'TButton')
# 应用所有筛选条件
self.apply_all_filters()
def filter_by_amount(self):
"""成交额筛选"""
if self.original_df is None or self.original_df.empty:
messagebox.showwarning("警告", "请先加载数据")
return
# 切换状态
self.filter_states['amount'] = not self.filter_states['amount']
self.amount_btn.configure(style='Toggled.TButton' if self.filter_states['amount'] else 'TButton')
# 应用所有筛选条件
self.apply_all_filters()
def filter_by_pe(self):
"""市盈率-动态筛选"""
if self.original_df is None or self.original_df.empty:
messagebox.showwarning("警告", "请先加载数据")
return
# 切换状态
self.filter_states['pe'] = not self.filter_states['pe']
self.pe_btn.configure(style='Toggled.TButton' if self.filter_states['pe'] else 'TButton')
# 应用所有筛选条件
self.apply_all_filters()
def on_price_click(self, event):
"""处理最新价按钮单击事件"""
# 使用after_cancel来取消之前的双击检测
if hasattr(self, '_after_id'):
self.master.after_cancel(self._after_id)
del self._after_id
# 延迟执行单击操作,给双击检测留出时间
self._after_id = self.master.after(300, self.filter_by_price)
def on_change_click(self, event):
"""处理涨跌幅按钮单击事件"""
if hasattr(self, '_after_id'):
self.master.after_cancel(self._after_id)
del self._after_id
self._after_id = self.master.after(300, self.filter_by_change)
def on_market_cap_click(self, event):
"""处理总市值按钮单击事件"""
if hasattr(self, '_after_id'):
self.master.after_cancel(self._after_id)
del self._after_id
self._after_id = self.master.after(300, self.filter_by_market_cap)
def on_volume_ratio_click(self, event):
"""处理量比按钮单击事件"""
if hasattr(self, '_after_id'):
self.master.after_cancel(self._after_id)
del self._after_id
self._after_id = self.master.after(300, self.filter_by_volume_ratio)
def on_volume_click(self, event):
"""处理成交量按钮单击事件"""
if hasattr(self, '_after_id'):
self.master.after_cancel(self._after_id)
del self._after_id
self._after_id = self.master.after(300, self.filter_by_volume)
def filter_by_volume(self):
"""成交量筛选"""
if self.original_df is None or self.original_df.empty:
messagebox.showwarning("警告", "请先加载数据")
return
self.filter_states['volume'] = not self.filter_states['volume']
self.volume_btn.configure(style='Toggled.TButton' if self.filter_states['volume'] else 'TButton')
self.apply_all_filters()
def on_volume_double_click(self, event):
"""处理成交量按钮双击事件"""
if hasattr(self, '_after_id'):
self.master.after_cancel(self._after_id)
del self._after_id
dialog = tk.Toplevel(self.master)
dialog.title("设置成交量区间")
dialog.geometry("300x120")
ttk.Label(dialog, text="输入成交量区间(万手,例如:1-5):").pack(pady=5)
entry = ttk.Entry(dialog)
# 显示转换为万手的数值
entry.insert(0, f"{self.current_volume_range[0]//10000}-{self.current_volume_range[1]//10000}")
entry.pack(pady=5)
entry.focus_set()
def save_range():
input_val = entry.get().strip()
try:
min_val, max_val = map(float, input_val.split('-'))
# 转换为手的单位(1万手=10000手)
min_val = int(min_val * 10000)
max_val = int(max_val * 10000)
if min_val > max_val:
min_val, max_val = max_val, min_val
self.current_volume_range = (min_val, max_val)
self.volume_btn.config(text=f"成交量{min_val//10000}-{max_val//10000}万手")
self.save_settings()
if self.filter_states['volume']:
self.apply_all_filters()
dialog.destroy()
except:
messagebox.showerror("错误", "输入格式错误,示例:1-5")
entry.bind('<Return>', lambda e: save_range())
ttk.Button(dialog, text="保存", command=save_range).pack()
def on_pe_click(self, event):
"""处理市盈率-动态单击事件"""
if hasattr(self, '_after_id'):
self.master.after_cancel(self._after_id)
del self._after_id
self._after_id = self.master.after(300, self.filter_by_pe)
def on_turnover_rate_click(self, event):
"""处理换手率按钮单击事件"""
if hasattr(self, '_after_id'):
self.master.after_cancel(self._after_id)
del self._after_id
self._after_id = self.master.after(300, self.filter_by_turnover_rate)
def on_price_double_click(self, event):
"""处理最新价按钮双击事件"""
if hasattr(self, '_after_id'):
self.master.after_cancel(self._after_id)
del self._after_id
dialog = tk.Toplevel(self.master)
dialog.title("设置最新价区间")
dialog.geometry("320x140")
ttk.Label(dialog, text="输入最新价区间(例如:10-50):").pack(pady=5)
entry = ttk.Entry(dialog)
entry.insert(0, f"{self.current_price_range[0]}-{self.current_price_range[1]}")
entry.pack(pady=5)
entry.focus_set()
def save_range():
input_val = entry.get().strip()
try:
min_val, max_val = map(float, input_val.split('-'))
if min_val > max_val:
min_val, max_val = max_val, min_val
self.current_price_range = (min_val, max_val)
self.price_btn.config(text=f"最新价{min_val}-{max_val}")
self.save_settings()
if self.filter_states['price']:
self.apply_all_filters()
dialog.destroy()
except:
messagebox.showerror("错误", "输入格式错误,示例:10-50")
entry.bind('<Return>', lambda e: save_range())
ttk.Button(dialog, text="保存", command=save_range).pack()
def on_change_double_click(self, event):
"""处理涨跌幅按钮双击事件"""
if hasattr(self, '_after_id'):
self.master.after_cancel(self._after_id)
del self._after_id
dialog = tk.Toplevel(self.master)
dialog.title("设置涨跌幅区间")
dialog.geometry("320x140")
ttk.Label(dialog, text="输入涨跌幅区间(例如:-10-10):").pack(pady=5)
entry = ttk.Entry(dialog)
entry.insert(0, f"{self.current_change_range[0]}-{self.current_change_range[1]}")
entry.pack(pady=5)
entry.focus_set()
def save_range():
input_val = entry.get().strip()
try:
min_val, max_val = map(float, input_val.split('-'))
if min_val > max_val:
min_val, max_val = max_val, min_val
self.current_change_range = (min_val, max_val)
self.change_btn.config(text=f"涨跌幅{min_val:+.2f}%-{max_val:+.2f}%")
self.save_settings()
if self.filter_states['change']:
self.apply_all_filters()
dialog.destroy()
except:
messagebox.showerror("错误", "输入格式错误,示例:-10-10")
entry.bind('<Return>', lambda e: save_range())
ttk.Button(dialog, text="保存", command=save_range).pack()
def on_pe_double_click(self, event):
"""处理市盈率-动态按钮双击事件""" # 注意这里添加了缩进
if hasattr(self, '_after_id'):
self.master.after_cancel(self._after_id)
del self._after_id
dialog = tk.Toplevel(self.master)
dialog.title("设置市盈率-动态区间")
dialog.geometry("300x120")
ttk.Label(dialog, text="输入市盈率-动态区间(例如:0-30):").pack(pady=5)
entry = ttk.Entry(dialog)
entry.insert(0, f"{self.current_pe_range[0]:,.0f}-{self.current_pe_range[1]:,.0f}")
entry.pack(pady=5)
entry.focus_set()
def save_range():
input_val = entry.get().strip().replace(',', '')
try:
min_val, max_val = map(float, input_val.split('-'))
if min_val > max_val:
min_val, max_val = max_val, min_val
self.current_pe_range = (min_val, max_val)
self.pe_btn.config(text=f"市盈率-动态{min_val:,.0f}-{max_val:,.0f}")
self.save_settings()
if self.filter_states['pe']:
self.apply_all_filters()
dialog.destroy()
except:
messagebox.showerror("错误", "输入格式错误,示例:0-30")
entry.bind('<Return>', lambda e: save_range())
ttk.Button(dialog, text="保存", command=save_range).pack()
def on_market_cap_double_click(self, event):
"""处理总市值按钮双击事件""" # 注意这里添加了缩进
if hasattr(self, '_after_id'):
self.master.after_cancel(self._after_id)
del self._after_id
dialog = tk.Toplevel(self.master)
dialog.title("设置总市值区间")
dialog.geometry("300x120")
ttk.Label(dialog, text="输入总市值区间(例如:1000000000-50000000000):").pack(pady=5)
entry = ttk.Entry(dialog)
entry.insert(0, f"{self.current_market_cap_range[0]:,.0f}-{self.current_market_cap_range[1]:,.0f}")
entry.pack(pady=5)
entry.focus_set()
def save_range():
input_val = entry.get().strip().replace(',', '')
try:
min_val, max_val = map(float, input_val.split('-'))
if min_val > max_val:
min_val, max_val = max_val, min_val
self.current_market_cap_range = (min_val, max_val)
self.market_cap_btn.config(text=f"总市值{min_val:,.0f}-{max_val:,.0f}")
self.save_settings()
if self.filter_states['market_cap']:
self.apply_all_filters()
dialog.destroy()
except:
messagebox.showerror("错误", "输入格式错误,示例:1000000000-50000000000")
entry.bind('<Return>', lambda e: save_range())
ttk.Button(dialog, text="保存", command=save_range).pack()
def on_amount_double_click(self, event):
"""处理成交额按钮双击事件"""
if hasattr(self, '_after_id'):
self.master.after_cancel(self._after_id)
del self._after_id
dialog = tk.Toplevel(self.master)
dialog.title("设置成交额区间")
dialog.geometry("300x120")
ttk.Label(dialog, text="输入成交额区间(例如:100000-500000):").pack(pady=5)
entry = ttk.Entry(dialog)
entry.insert(0, f"{self.current_amount_range[0]:,.0f}-{self.current_amount_range[1]:,.0f}")
entry.pack(pady=5)
entry.focus_set()
def save_range():
input_val = entry.get().strip().replace(',', '')
try:
min_val, max_val = map(float, input_val.split('-'))
if min_val > max_val:
min_val, max_val = max_val, min_val
self.current_amount_range = (min_val, max_val)
self.amount_btn.config(text=f"成交额{min_val:,.0f}-{max_val:,.0f}万")
self.save_settings()
if self.filter_states['amount']:
self.apply_all_filters()
dialog.destroy()
except:
messagebox.showerror("错误", "输入格式错误,示例:100000-500000")
entry.bind('<Return>', lambda e: save_range())
ttk.Button(dialog, text="保存", command=save_range).pack()
def on_volume_ratio_double_click(self, event):
"""处理量比按钮双击事件"""
if hasattr(self, '_after_id'):
self.master.after_cancel(self._after_id)
del self._after_id
dialog = tk.Toplevel(self.master)
dialog.title("设置量比区间")
dialog.geometry("300x120")
ttk.Label(dialog, text="输入量比区间(例如:1.5-3.0):").pack(pady=5)
entry = ttk.Entry(dialog)
entry.insert(0, f"{self.current_volume_ratio_range[0]}-{self.current_volume_ratio_range[1]}")
entry.pack(pady=5)
entry.focus_set()
def save_range():
input_val = entry.get().strip()
try:
min_val, max_val = map(float, input_val.split('-'))
if min_val > max_val:
min_val, max_val = max_val, min_val
self.current_volume_ratio_range = (min_val, max_val)
self.volume_ratio_btn.config(text=f"量比{min_val}-{max_val}")
self.save_settings()
if self.filter_states['volume_ratio']:
self.apply_all_filters()
dialog.destroy()
except:
messagebox.showerror("错误", "输入格式错误,示例:1.5-3.0")
entry.bind('<Return>', lambda e: save_range())
ttk.Button(dialog, text="保存", command=save_range).pack()
def on_turnover_rate_double_click(self, event):
"""处理换手率按钮双击事件"""
if hasattr(self, '_after_id'):
self.master.after_cancel(self._after_id)
del self._after_id
dialog = tk.Toplevel(self.master)
dialog.title("设置换手率区间")
dialog.geometry("300x120")
ttk.Label(dialog, text="输入换手率区间(例如:3.0-10.0):").pack(pady=5)
entry = ttk.Entry(dialog)
entry.insert(0, f"{self.current_turnover_rate_range[0]}-{self.current_turnover_rate_range[1]}")
entry.pack(pady=5)
entry.focus_set()
def save_range():
input_val = entry.get().strip()
try:
min_val, max_val = map(float, input_val.split('-'))
if min_val > max_val:
min_val, max_val = max_val, min_val
self.current_turnover_rate_range = (min_val, max_val)
self.turnover_rate_btn.config(text=f"换手率{min_val}%-{max_val}%")
self.save_settings()
if self.filter_states['turnover_rate']:
self.apply_all_filters()
dialog.destroy()
except:
messagebox.showerror("错误", "输入格式错误,示例:3.0-10.0")
entry.bind('<Return>', lambda e: save_range())
ttk.Button(dialog, text="保存", command=save_range).pack()
def _update_time(self):
"""更新时间显示"""
weekday_map = {
0: '一', 1: '二', 2: '三',
3: '四', 4: '五', 5: '六', 6: '日'
}
current_time = datetime.datetime.now()
weekday = weekday_map[current_time.weekday()]
time_str = current_time.strftime(f'%Y-%m-%d %H:%M:%S 星期{weekday}')
self.time_label.config(text=time_str)
self.master.after(1000, self._update_time) # 每秒更新一次
def show_context_menu(self, event):
"""显示右键菜单"""
item = self.tree.identify_row(event.y)
if not item:
return
self.tree.selection_set(item)
selected_code = self.tree.item(item)['values'][1] # 获取代码
menu = tk.Menu(self.master, tearoff=0)
if selected_code in self.favorite_stocks:
menu.add_command(label="删除自选", command=lambda: self.remove_from_favorites(selected_code))
else:
menu.add_command(label="加入自选", command=lambda: self.add_to_favorites(selected_code))
menu.post(event.x_root, event.y_root)
def on_tree_click(self, event):
"""处理左键点击事件"""
item = self.tree.identify_row(event.y)
if not item:
return
self.tree.selection_set(item)
def on_tree_click(self, event):
"""处理点击事件键点击事件"""
item = self.tree.identify_row(event.y)
if item and event.num == 3:
self.show_context_menu(event)
def add_to_favorites(self, stock_code):
"""添加到自选股票"""
self.favorite_stocks.add(stock_code)
self.save_favorites()
self.log(f"已将 {stock_code} 加入自选", "success")
def remove_from_favorites(self, stock_code):
"""从自选股票中删除"""
if stock_code in self.favorite_stocks:
self.favorite_stocks.remove(stock_code)
self.save_favorites()
self.log(f"已将 {stock_code} 从自选中删除", "success")
def show_favorites(self, event=None):
"""显示自选股票"""
if self.original_df is None or self.original_df.empty:
messagebox.showwarning("警告", "请先加载数据")
return
# 从原始数据中筛选自选股票
mask = self.original_df['代码'].isin(self.favorite_stocks)
filtered_df = self.original_df[mask].copy()
# 更新当前显示的数据
self.current_df = filtered_df
# 显示数据(即使为空也会清空当前显示)
self.show_data()
# 显示自选股票信息
if filtered_df.empty:
if not self.favorite_stocks:
self.log("当前没有自选股票", "info")
else:
self.log(f"当前有 {len(self.favorite_stocks)} 个自选股票,但在当前数据中未找到", "info")
self.log(f"自选股票列表: {', '.join(sorted(self.favorite_stocks))}", "info")
else:
self.log(f"显示自选股票,共找到 {len(filtered_df)} 条记录", "info")
self.log("按Ctrl+Z可以回退到上一次显示状态", "info")
# ... 类内其他方法保持不变 ...
def copy_selected_row(self, event=None):
try:
selected_items = self.tree.selection()
if not selected_items:
return
# 获取DataFrame实际列名
df_columns = self.current_df.columns.tolist()
required_columns = ["代码", "名称", "最新价", "涨跌幅",
"成交量", "成交额", "换手率", "量比", "市盈率-动态", "总市值"]
rows_data = []
for item in selected_items:
display_index = int(self.tree.item(item, 'values')[0]) - 1
if 0 <= display_index < len(self.current_df):
row = self.current_df.iloc[display_index].copy()
# 格式化数值字段
numeric_cols = ['最新价', '涨跌幅', '成交量', '成交额',
'换手率', '量比', '市盈率-动态', '总市值']
for col in numeric_cols:
if col in row:
value = float(row[col])
row[col] = f"{value:.2f}".replace(',', '')
row_values = [str(row[col]) for col in required_columns]
rows_data.append("\t".join(row_values))
header = "\t".join(required_columns)
clipboard_data = header + "\n" + "\n".join(rows_data)
self.master.clipboard_clear()
self.master.clipboard_append(clipboard_data)
self.log(f"已复制 {len(rows_data)} 行数据到剪贴板", "success")
except Exception as e:
self.log(f"复制失败: {str(e)}", "error")
def copy_selected_names(self, event=None):
"""复制选中行的名称"""
try:
selected_items = self.tree.selection()
if not selected_items:
self.log("没有选中行","warning")
return
names = []
for item in selected_items:
item_values = self.tree.item(item, 'values')
if len(item_values) >= 3:
name = item_values[2]
names.append(name)
if not names:
self.log("选中行中没有名称","warning")
return
clipboard_text = "\n".join(names)
self.master.clipboard_clear()
self.master.clipboard_append(clipboard_text)
self.log(f"已复制 {len(names)} 个名称到剪贴板: {clipboard_text}", "success")
except Exception as e:
self.log(f"复制名称失败: {str(e)}", "error")
def select_all_rows(self, event=None):
"""全选Treeview中的所有行"""
try:
items = self.tree.get_children()
self.tree.selection_set(items)
self.log(f"已全选 {len(items)} 行", "info")
except Exception as e:
self.log(f"全选失败: {str(e)}", "error")
def show_help(self, event=None):
"""显示帮助信息"""
if self.help_dialog and self.help_dialog.winfo_exists():
self.help_dialog.lift()
return
self.help_dialog = tk.Toplevel(self.master)
self.help_dialog.title("快捷键帮助")
self.help_dialog.geometry("400x600+200+200")
self.help_dialog.grab_set()
self.help_dialog.bind('<Escape>',lambda e: self.help_dialog.destroy())
def close_help():
if self.help_dialog.winfo_exists():
self.help_dialog.destroy()
self.help_dialog = None
self.help_dialog.protocol("WM_DELETE_WINDOW", self.help_dialog.destroy)
help_text = """
快捷键说明:
----------------------------
Ctrl + O : 打开Excel文件
Ctrl + R : 刷新实时数据
Ctrl + S : 保存当前数据
Ctrl + F : 打开搜索对话框
Ctrl + Z : 撤销显示操作
Ctrl + C : 复制选中行数据
Ctrl + D : 复制选中股票名称
Ctrl + A : 全选所有行
Ctrl + H : 显示本帮助信息
ESC : 退出全屏/关闭帮助
F11 : 切换全屏模式
筛选操作:
----------------------------
单击筛选按钮 : 启用/禁用筛选
双击筛选按钮 : 设置筛选范围
右键菜单功能:
----------------------------
点击股票行 : 加入/移除自选
"""
text = tk.Text(self.help_dialog, wrap=tk.WORD, font=('Microsoft YaHei', 12),
padx=10, pady=10, bg='#F0F0F0')
text.insert(tk.END, help_text.strip())
text.configure(state=tk.DISABLED)
text.pack(fill=tk.BOTH, expand=True)
def show_statistics(self):
"""显示统计信息"""
if self.current_df is None or self.current_df.empty:
return
try:
# 计算总总市值(转换为万亿元)
total_market_cap = self.current_df['总市值'].sum() / 1e12
# 计算总成交额(转换为亿元)
total_amount = self.current_df['成交额'].sum() / 1e8
# 计算涨跌家数
up_count = len(self.current_df[self.current_df['涨跌幅'] > 0])
flat_count = len(self.current_df[self.current_df['涨跌幅'] == 0])
down_count = len(self.current_df[self.current_df['涨跌幅'] < 0])
# 格式化输出
stats = [
f"总市值: {total_market_cap:.2f} 万亿元",
f"总成交额: {total_amount:.2f} 亿元",
f"上涨家数:{up_count} 平盘家数:{flat_count} 下跌家数: {down_count}"
]
# 清空日志并显示统计信息
self.log_area.configure(state=tk.NORMAL)
self.log_area.delete(1.0, tk.END)
for stat in stats:
self.log_area.insert(tk.END, stat + "\n", "stat")
self.log_area.configure(state=tk.DISABLED)
except KeyError as e:
self.log(f"缺少必要列 {str(e)},无法计算统计信息", "error")
except Exception as e:
self.log(f"统计计算失败: {str(e)}", "error")
def add_to_history(self, df):
"""添加数据到历史记录"""
# 如果当前不是在历史记录的最后,删除当前位置之后的记录
if self.current_data_index < len(self.data_history) - 1:
self.data_history = self.data_history[:self.current_data_index + 1]
# 添加新的数据到历史记录
self.data_history.append(df.copy() if df is not None else None)
self.current_data_index = len(self.data_history) - 1
def undo_last_display(self, event=None):
"""撤销上一次显示操作"""
if self.current_data_index > 0: # 确保有历史记录可以回退
self.current_data_index -= 1
self.current_df = self.data_history[self.current_data_index].copy() if self.data_history[self.current_data_index] is not None else None
self.show_data()
self.log("已回退到上一次显示状态", "info")
def on_amount_click(self, event):
"""处理成交额按钮单击事件"""
if hasattr(self, '_after_id'):
self.master.after_cancel(self._after_id)
del self._after_id
self._after_id = self.master.after(300, self.filter_by_amount)
def on_amount_double_click(self, event):
"""处理成交额按钮双击事件"""
if hasattr(self, '_after_id'):
self.master.after_cancel(self._after_id)
del self._after_id
dialog = tk.Toplevel(self.master)
dialog.title("设置成交额区间")
dialog.geometry("300x120")
ttk.Label(dialog, text="输入成交额区间(万元,例如:5000-50000):").pack(pady=5)
entry = ttk.Entry(dialog)
entry.insert(0, f"{self.current_amount_range[0]}-{self.current_amount_range[1]}")
entry.pack(pady=5)
entry.focus_set()
def save_range():
input_val = entry.get().strip()
try:
min_val, max_val = map(float, input_val.split('-'))
if min_val > max_val:
min_val, max_val = max_val, min_val
self.current_amount_range = (min_val, max_val)
self.amount_btn.config(text=f"成交额{min_val}-{max_val}万")
self.save_settings()
dialog.destroy()
except:
messagebox.showerror("错误", "输入格式错误,示例:5000-50000")
entry.bind('<Return>', lambda e: save_range())
ttk.Button(dialog, text="保存", command=save_range).pack()
if __name__ == "__main__":
if sys.platform == 'win32':
from ctypes import windll
windll.kernel32.SetConsoleOutputCP(65001)
root = tk.Tk()
try:
from ctypes import windll
windll.shcore.SetProcessDpiAwareness(1)
except: pass
app = ExcelBrowser(root)
app.log_area.tag_config("success", foreground=app.text_color)
app.log_area.tag_config("warning", foreground=app.text_color)
app.log_area.tag_config("error", foreground=app.text_color)
app.log_area.tag_config("info", foreground=app.text_color)
root.mainloop()
build-eagle_eyes._out.sh
source ~/venv/bin/activate
pyinstaller --onefile \
--name "eagle_eyes" \
--windowed \
--icon=eagle_eyes.png \
--add-data="icons/hicolor:icons/hicolor" \
--add-data="favorite_stocks.json:." \
--add-data="app_settings.json:." \
--add-data="icons/:icons" \
--add-data="/home/l/venv/lib/python3.12/site-packages/akshare/file_fold/calendar.json:akshare/file_fold" \
--hidden-import="pandas._libs.tslibs.np_datetime" \
--hidden-import="PIL.Image" \
--hidden-import="PIL.ImageTk" \
--hidden-import="PIL._tkinter_finder" \
eagle_eyes.py
install-eagle_eyes.sh
#!/bin/bash
# 股票分析工具安装脚本
set -e
# ===== 配置区 =====
APP_NAME="eagle_eyes" # 内部标识必须用英文
DISPLAY_NAME="鹰眼" # 显示名称可中文
EXE_SOURCE="./dist/eagle_eyes"
ICON_SOURCE="./eagle_eyes.png" # 需为透明背景PNG
# ==================
# 定义安装路径
INSTALL_DIR="/opt/$APP_NAME"
BIN_LINK="/usr/local/bin/$APP_NAME"
DESKTOP_FILE="/usr/share/applications/$APP_NAME.desktop"
ICON_SIZES=(16 32 48 64 128 256 512) # 多尺寸适配
# 检查依赖和文件
echo "[1/7] 检查依赖..."
command -v convert >/dev/null || sudo apt-get install -y imagemagick
[ -f "$EXE_SOURCE" ] || { echo "错误:可执行文件 $EXE_SOURCE 不存在"; exit 1; }
[ -f "$ICON_SOURCE" ] || { echo "错误:图标文件 $ICON_SOURCE 不存在"; exit 1; }
# 创建目录结构
echo "[2/7] 创建目录..."
sudo mkdir -p "$INSTALL_DIR"
# 安装主程序
echo "[3/7] 安装程序文件..."
sudo cp -v "$EXE_SOURCE" "$INSTALL_DIR/"
sudo chmod 755 "$INSTALL_DIR/$(basename "$EXE_SOURCE")"
sudo ln -sf "$INSTALL_DIR/$(basename "$EXE_SOURCE")" "$BIN_LINK"
# 生成多尺寸图标
echo "[4/7] 生成系统图标..."
for size in "${ICON_SIZES[@]}"; do
icon_dir="/usr/share/icons/hicolor/${size}x${size}/apps"
sudo mkdir -p "$icon_dir"
sudo convert "$ICON_SOURCE" -resize "${size}x${size}" -background none \
"$icon_dir/$APP_NAME.png"
done
# 创建桌面菜单项
echo "[5/7] 创建菜单项..."
sudo tee "$DESKTOP_FILE" > /dev/null <<EOF
[Desktop Entry]
Version=1.0
Type=Application
Name=$DISPLAY_NAME
Comment=基于AkShare的股票数据分析工具
Exec=$BIN_LINK
Icon=$APP_NAME
Terminal=false
Categories=Finance;
Keywords=Stock;
EOF
# 更新系统数据库
echo "[6/7] 更新系统索引..."
sudo update-desktop-database
sudo gtk-update-icon-cache -f /usr/share/icons/hicolor/
# 最终提示
echo -e "\n\e[32m[7/7] 安装完成!请执行以下操作:\e[0m"
echo "1. 注销后重新登录系统"
echo "2. 按 Win 键搜索『$DISPLAY_NAME』启动程序"
echo "3. 若图标仍为灰色,请检查图标文件是否为透明背景PNG"
uninstall-eagle_eyes.sh
#!/bin/bash
# 股票分析工具卸载脚本
# 用法: sudo bash uninstalla股.sh
set -e
# 配置区(必须与安装脚本一致)
APP_NAME="eagle_eyes"
DISPLAY_NAME="鹰眼"
INSTALL_DIR="/opt/$APP_NAME"
BIN_LINK="/usr/local/bin/$APP_NAME"
DESKTOP_FILE="/usr/share/applications/$APP_NAME.desktop"
ICON_SIZES=(16 32 48 64 128 256 512)
# 执行卸载
echo "[1/4] 移除程序文件..."
sudo rm -rfv "$INSTALL_DIR"
sudo rm -fv "$BIN_LINK"
sudo rm -fv "$DESKTOP_FILE"
echo "[2/4] 清理图标文件..."
for size in "${ICON_SIZES[@]}"; do
sudo rm -fv "/usr/share/icons/hicolor/${size}x${size}/apps/$APP_NAME.png"
done
echo "[3/4] 更新系统数据库..."
sudo update-desktop-database
sudo gtk-update-icon-cache -f /usr/share/icons/hicolor/
echo -e "\n\e[32m[4/4] 卸载完成!\e[0m"