# StockWidgetDotNet **Repository Path**: xy506/StockWidgetDotNet ## Basic Information - **Project Name**: StockWidgetDotNet - **Description**: StockWidget Python→.NET 8 WPF 改写 | 透明股票行情浮窗 | Clean Code - **Primary Language**: Unknown - **License**: Not specified - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 0 - **Created**: 2026-07-17 - **Last Updated**: 2026-07-17 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # StockWidget Python → .NET 8 WPF 改写说明 ## 项目概述 将基于 Python + PySide6 的 Windows 透明股票行情浮窗([sbr0574/StockWidget](https://github.com/sbr0574/StockWidget))改写为 .NET 8 WPF 程序,全程应用 Clean Code 原则。 - **原始语言**:Python 3 + PySide6(Qt6) - **目标语言**:C# 12 + .NET 8 WPF - **配置兼容**:与原 Python 配置文件 `%APPDATA%\StockWidget\SW_config.json` 完全互通(snake_case 键名、旧 flags 数组/字典、b1s1_price 布尔) --- ## 模块对照表 | Python 文件 | 职责 | .NET 对应 | 说明 | |---|---|---|---| | `App.py` | 应用入口、托盘、配置存档、开机自启 | `App.xaml.cs` | 实现 `IAppControl` 接口;托盘改用自写 `NativeTrayIcon`(Win32 `Shell_NotifyIcon`) | | `WidgetPanel.py` | 浮窗 UI、行情获取、格式化、拖拽、热键、右键菜单 | `FloatWidget.xaml` + `.cs` | 拆分为纯 UI + 刷新调度;数据获取与格式化下放到服务层 | | `Display.py` | 表格模型、K 线绘制 | `QuoteTableView.cs` + `KLinePainter.cs` + `QuoteTableRendering.cs` | 自绘 `FrameworkElement` 替代 Qt 表格模型 | | `SettingPanel.py` | 设置对话框(4 标签页) | `SettingsWindow.xaml` + `.cs` | 非模态窗口,实时应用变更 | | —(散落在各处) | 股票代码规格化 | `StockCodeNormalizer.cs` | 从 SettingPanel 中抽离为独立可测试服务 | | —(散落在各处) | 行情获取与解析 | `SinaQuoteProvider.cs` + `IQuoteProvider.cs` | 从 180 行 `_get_price` 中拆出纯 HTTP+解析 | | —(散落在各处) | 显示格式化 | `QuoteFormatter.cs` | 从 180 行 `_get_price` 中拆出纯格式化投影 | | —(散落在各处) | 配置持久化 | `JsonConfigRepository.cs` + `IConfigRepository.cs` | 原子写 + 旧配置兼容回退 | | —(散落在各处) | 开机自启 | `RegistryStartupManager.cs` + `IStartupManager.cs` | HKCU Run 注册表 | | —(散落在各处) | 全局热键 | `Win32GlobalHotkey.cs` + `IGlobalHotkey.cs` | Win32 RegisterHotKey 替代 Python keyboard 库 | --- ## 模型层 Models/ | 文件 | 对应 Python 概念 | Clean Code 改进 | |---|---|---| | `ColumnId.cs` | 中文字符串列标识 | 枚举替代字符串,获得类型安全与重命名支持 | | `SignKind.cs` | 1/0/-1 魔术数 | 枚举 + 扩展方法消除到处出现的条件判断 | | `B1S1DisplayMode.cs` | "qty"/"price"/"both" 字符串 | 枚举 + 配置字符串互转 | | `StockQuote.cs` | `_get_price` 中 parts[] 字段映射 | 纯数据 POCO,不含任何显示逻辑 | | `QuoteRow.cs` | 显示行数据 | 将数据(StockQuote)与显示投影分离 | | `ColumnCatalog.cs` | `ALL_HEADERS` + `right_cols` 两处维护 | 唯一定义来源,消除重复 | | `WidgetConfig.cs` | 散落在 FloatLabel 上的几十个属性 | 集中配置模型,`IsColumnVisible`/`SetColumnVisible` 消除长 if-elif 链;含持仓成本/数量/交易时段/暂停刷新 | --- ## 服务层 Services/ | 文件 | 职责 | 接口抽象 | |---|---|---| | `IQuoteProvider.cs` / `SinaQuoteProvider.cs` | 新浪行情 HTTP 请求 + GBK 解析 | `IQuoteProvider` 封装第三方边界 | | `QuoteFormatter.cs` | StockQuote → QuoteRow 显示投影 | 纯函数式,精度变量消除 ETF/非 ETF 重复代码 | | `StockCodeNormalizer.cs` | 6 位代码自动补前缀、规格化去重 | 从 SettingPanel 抽离为可复用可测试服务 | | `IConfigRepository.cs` / `JsonConfigRepository.cs` | JSON 配置读写 | 原子写(tmp + Move)+ 旧 Python 配置兼容回退 | | `IStartupManager.cs` / `RegistryStartupManager.cs` | 注册表开机自启 | `IStartupManager` 封装注册表边界 | | `IGlobalHotkey.cs` / `Win32GlobalHotkey.cs` | Win32 RegisterHotKey | `IGlobalHotkey` 封装 Win32 边界 | | `IAppControl.cs` | 应用级控制(热键、开机启动) | 避免视图直接耦合 App | --- ## 显示层 Controls/ | 文件 | 对应 Python | 说明 | |---|---|---| | `QuoteTableRendering.cs` | Display.py 颜色常量 + FloatLabel 样式参数 | `TablePalette` + `QuoteTableState` | | `KLinePainter.cs` | `KLineDelegate` | 当日 K 线绘制(开收高低 + 昨收虚线) | | `QuoteTableView.cs` | `SimpleTableModel` + 表格样式 | 自绘 `FrameworkElement`,单一职责:只渲染不交互 | --- ## 视图层 Views/ ### FloatWidget — 透明置顶行情浮窗 对应原 Python `WidgetPanel.FloatLabel`,只承担 UI 交互与刷新调度: - 透明无框置顶窗口(`AllowsTransparency` + `Topmost`) - 拖拽移动(DPI 感知坐标换算) - 双击隐藏 / 右键菜单 - `DispatcherTimer` 定时刷新 + 置顶保持 - 错误提示("无网络连接" 友好消息) - 实时设置变更方法(`SetFlag`、`SetRefreshInterval`、`SetFgColor` 等) - `HandleCreated` 事件(供热键注册获取 HWND) - `OpenSettingsRequested` 事件(解耦设置窗口创建) ### SettingsWindow — 设置面板 对应原 Python `SettingPanel.SettingsDialog`,4 标签页: 1. **自选列表**:DataGrid + 添加/删除/上移/下移,代码实时规格化 2. **显示数据→刷新时间**:刷新间隔、非交易时段暂停复选框、交易时段 DataGrid、列可见性(动态 CheckBox)、代码格式、名称长度、买一/卖一显示模式 3. **外观**:表头/网格、默认颜色、文字/背景色(WinForms ColorDialog)、不透明度、字体、字号、行距 4. **常规**:全局热键(PreviewKeyDown 捕获)、开机自启 所有变更通过 FloatWidget 的 `Set*` 方法实时应用,配置持久化由 App 订阅 `ConfigChanged` 统一处理。 --- ## App — 应用入口 对应原 Python `App.App`: - 创建 `FloatWidget`、`JsonConfigRepository`、`RegistryStartupManager` - 创建 `NativeTrayIcon` 系统托盘(Win32 `Shell_NotifyIcon` 原生 API,左键切换可见性,右键菜单) - 从嵌入资源加载 `.ico` 为 native HICON,持有 `System.Drawing.Icon` 引用防止 GC 销毁句柄 - 订阅 `HandleCreated` → 创建 `Win32GlobalHotkey` 并注册热键 - 订阅 `ConfigChanged` → 原子写保存配置 - 订阅 `OpenSettingsRequested` → 创建/激活 `SettingsWindow` - 实现 `IAppControl`:`UpdateHotkey`(注销→重注册)、`SetStartOnBoot` - `ShutdownMode=OnExplicitShutdown`,退出时清理热键/托盘/浮窗 ### NativeTrayIcon — Win32 原生系统托盘 放弃 `H.NotifyIcon.Wpf`,自写 `NativeTrayIcon.cs`: - **原因**:H.NotifyIcon 内部 `ToStream()` 对 `InteropBitmap`/无 URI 的 `BitmapImage`/`file://` 路径均抛异常或返回 null,单文件发布后 `pack://` URI 也无法解析 - **实现**:`Shell_NotifyIcon` API + 隐藏消息窗口接收回调,`WndProc` 子类化处理 `WM_LBUTTONUP`/`WM_RBUTTONUP` - **图标加载**:`Assembly.GetManifestResourceStream` → `System.Drawing.Icon(stream)`,持有引用防止 Dispose 销毁 HICON ## 交易时段暂停刷新 - `TradingSession` 模型(Open/Close 时间字符串 + 预解析分钟数) - `TradingHours` 静态工具类:`IsInSession()`、`UntilNextOpen()` - `WidgetConfig.PauseOffHours`(bool)+ `TradingSessions`(List,默认 A 股 09:15-11:30 + 13:00-15:00) - `FloatWidget._scheduleTimer` 每分钟检查交易时段,`CheckSchedule()` 控制 `_refreshTimer` 启停 - `ShowWidget()` 首次强制刷新一次(`RefreshAsync(force: true)`),避免非交易时段首次启动窗口缩成小黑点 - 设置页"刷新时间"标签页:暂停复选框 + 交易时段 DataGrid + 下次开盘倒计时提示 ## 持仓成本与盈亏 - `ColumnId.Cost` / `ColumnId.Pnl` 列 - `WidgetConfig.CostPrices`(Dictionary)+ `Quantities`(Dictionary) - `QuoteFormatter.FormatPosition()` 计算持仓盈亏 - 成本列不设 Sign(回退到前景色,暗背景下可见),盈亏列保留红绿涨跌着色 - 设置页自选列表添加成本/数量列编辑 --- ## 关键 Clean Code 改进总结 | 问题 | 原始 Python | .NET 改写 | |---|---|---| | **重复代码 P0** | `App.py` 中 `_resolve_icon` 完全重复两遍 | 去除图标选择功能,单一 `LoadIcon()` | | **过长函数 P1** | `_get_price` 180 行混杂网络+解析+格式化 | 拆为 `SinaQuoteProvider`(HTTP+解析)+ `QuoteFormatter`(投影) | | **SRP 违反** | `FloatLabel` 承求数据/UI/交互/热键/配置 | `FloatWidget`(UI)+ 7 个服务类 | | **长 if-elif 链** | `header_is_visible` / `set_flag` | `WidgetConfig.IsColumnVisible` switch 表达式 | | **重复列定义** | `ALL_HEADERS` + `right_cols` 两处维护 | `ColumnCatalog.All` 唯一定义 | | **ETF/非 ETF 重复** | 两段近乎相同的行情构建代码 | `Decimals(q)` 精度变量统一 | | **魔术数** | 1/0/-1 到处出现 | `SignKind` 枚举 + `SignKindExtensions.Of()` | | **中文字符串列标识** | 到处用 "代码"、"名称" | `ColumnId` 枚举 | | **第三方边界无封装** | 直接 `urllib` + `keyboard` + `winreg` | `IQuoteProvider` / `IGlobalHotkey` / `IStartupManager` | | **配置散落** | 几十个独立属性在 FloatLabel 上 | `WidgetConfig` 集中模型 | --- ## 全局架构 ``` StockWidgetDotNet/ ├── App.xaml / App.xaml.cs # 应用入口、托盘、IAppControl ├── NativeTrayIcon.cs # Win32 Shell_NotifyIcon 原生托盘 ├── GlobalUsings.cs # WPF vs WinForms 命名空间冲突别名 ├── StockWidget.ico # 应用图标(BMP 格式,AI 生成) ├── Models/ │ ├── ColumnId.cs # 列标识枚举(含 Cost, Pnl) │ ├── ColumnCatalog.cs # 列定义唯一定义源 │ ├── SignKind.cs # 涨跌方向枚举 │ ├── B1S1DisplayMode.cs # 买一/卖一显示模式枚举 │ ├── StockQuote.cs # 新浪行情纯数据 │ ├── QuoteRow.cs # 显示投影 + K线数据 │ ├── TradingSession.cs # 交易时段模型(在 WidgetConfig.cs 顶部) │ └── WidgetConfig.cs # 集中配置模型(含持仓、交易时段) ├── Services/ │ ├── IQuoteProvider.cs # 行情接口 │ ├── SinaQuoteProvider.cs # 新浪 HTTP+GBK 解析 │ ├── QuoteFormatter.cs # 数据→显示投影(含持仓计算) │ ├── StockCodeNormalizer.cs # 代码规格化 │ ├── IConfigRepository.cs # 配置仓储接口 │ ├── JsonConfigRepository.cs # JSON 读写+旧配置兼容 │ ├── IStartupManager.cs # 开机自启接口 │ ├── RegistryStartupManager.cs # 注册表实现 │ ├── IGlobalHotkey.cs # 热键接口 │ ├── Win32GlobalHotkey.cs # Win32 RegisterHotKey │ ├── IAppControl.cs # 应用控制接口 │ └── TradingHours.cs # 交易时段判断工具类 ├── Controls/ │ ├── QuoteTableRendering.cs # 调色板+渲染状态 │ ├── KLinePainter.cs # K线绘制 │ └── QuoteTableView.cs # 自绘表格 └── Views/ ├── FloatWidget.xaml / .cs # 透明置顶浮窗(含吸附、交易时段暂停) └── SettingsWindow.xaml / .cs # 4标签页设置面板(含刷新时间+交易时段) ``` ## 配置文件兼容性 .NET 版与 Python 版共享同一配置文件 `%APPDATA%\StockWidget\SW_config.json`: - **序列化**:`JsonNamingPolicy.SnakeCaseLower`,与 Python snake_case 一致 - **旧 flags 兼容**:新键缺失时回退到 `flags` 数组(bool[])或字典(按中文标题查) - **旧 b1s1_price 兼容**:布尔键映射到 `B1S1DisplayMode.Price` / `Quantity` - **原子写**:临时文件 + `File.Move(overwrite: true)` 避免写坏