# DESIGN —— ek-ui(S1b 常驻观测界面)
Frozen ref: docs/ek-ui-station-plan.md(DV 表 + S1b 段验收)。本档只记**结构决定**与
执行期钉。形式沿用 ek-dm/ek-station 的 BD-U1.. 编号加序。
## 结构
- `state.rs`:`UiShared`(总线面写 / 窗口面读)+ `LogRing`(cap 512,挤旧 +
spilled 单调计数)+ `BusSnap`(svctl.list 行 + 逐插件服务 id 目录)。
锁粒度:每次操作 lock/clone,不跨帧持锁;`snapshot()` 返回 `Arc<BusSnap>` 零拷贝。
- `plugin.rs`:`UiPlugin`("ui"):routes = `eLog`/`ePlugUp`/`ePlugDown`/`eTimer` +
`eTick.`(行情带唯一源)+ `eContract.`/`eContractFinish`(查询合约窗数据流,
ek-ctp 登录后自动查询,UI 只收不催)。
- eLog:payload `LogData`,`is_warning_or_above` 打 WARN 标签(+UCH 转储环形);
- ePlugUp/Down:payload `PlugNotice` → 单行 host 日志 + rev;
- eTick./eContract.:payload 直落 `tick_store`/`contract_store`(键 EX.symbol);
- eTimer:`ask(svctl.list, 2s)` + 逐插件 `ctx.services_of` → `set_snapshot`;
ask 失败进日志面,不堆 panic。
- `render.rs`:`render_body(ctx, &UiShared)` 唯一渲染入口(L2;v5 面板化,
BD-UI-27/29):TopBottomPanel(菜单)→ TopBottomPanel(行情带,resizable)→
SidePanel(下单面板 + 盘口,resizable)→ CentralPanel(2×2 监控格,响应式)→
弹窗面(K线/运维/CTP/查询合约,egui::Window,面板之后)。
- `runtime.rs`:
- `ui_kinds(&Arc<UiShared>)` = station 缺省表 + ui 工厂;工厂闭包绑定**同一个**
shared → ui 插件被 svctl 摘除/再注,窗口观测态不丢(状态在 shared 不在插件体);
- `spawn_host_watcher`:`sleep 120ms` 轮询 stop 位 → `engine.close_all(3s)`;
close 顺序(逆 start 序)仍由宿主引擎决定,观察线程只送信号;
- `run_station_ui`:boot(不 start 由 boot 内部? 否—— boot 只装配)→ `engine.start()`
→ 观察线程 → eframe `run_native`;关窗 `on_close_event → request_stop` →
join 观察线程 → 返回。启动失败(start Err)以 `Err(String)` 上抛(供 S1c
launcher 的失败信号,不在窗口内吞错)。
## 执行期钉
- **BD-UI-9(UI 即插件 / 双线程形貌)**:总线 worker 写、eframe 主线程读,以
`Arc<UiShared>` 相连;不加自制通道、不退回向量共享消息队列。
- **BD-UI-10(L2 单渲染入口)**:`render_body(ctx, &UiShared)` 被真实 App 与
kittest 同调;面板组合在 render_body 内(BD-UI-29),测试走
`Harness::builder().build(|ctx| …)`(ctx 级,不再包 CentralPanel)。
- **BD-UI-7(四件套版本锁)**:eframe = egui = egui_kittest = `0.33.3`;
egui-charts 0.2.0 进入点定为 S4(K 线页),进入时与四件套重验一版。
- **BD-UI-11(关窗协议)**:`App::on_close_event` 只置 stop 位;close_all 由
观察线程完成 —— 窗口永不直接持有 `MainEngine::close_all` 调用链。
- **BD-UI-12(kittest 用法纪律,源自官方 README)**:优先节点/文本断言,少用
图像快照;快照只对静态首帧;更新基线仅经 `UPDATE_SNAPSHOTS='true'`;冲突
先 diff 图后压缩容差。
- **BD-UI-13(纯观测零服务)**:ui 插件 `services()` 返回空;管制面唯一在 svctl
—— 界面无特权路径。动态 add 的 ask 窗口期(`on_init` 后)与 headless 同一契约。
- **BD-UI-14(启动器协议,与 ek-station bin `--from/--ready-file` 同口径)**:
清单 = 行式文本(`#` 注释;空行跳过;行内空白拒);就绪标记 = `ready\n`;
spawn 分离式(stdin/stdout 静默 + Windows `CREATE_NO_WINDOW` + stderr 管道,
**stderr 生存期 ≤ 64KB** —— 子进程不朝 stderr 泼流,违约即协议故障);
等待 = 50ms 轮询标记 + 存活检查,30s 上限(超时 kill);失败分类 `Exited{code,
stderr_tail}` / `NoReadySignal{timeout}`;成功路径 **drop Handle = 分离脱管**。
- **BD-UI-15(清单解析地面)**:pick.rs 与 ek-station main.rs 的 `--from` 解析
S1c 各自落地(等语义);S3 toml-lite 重构时统一抽到共享模块(单点 PR),避免
现在跨 crate 发版拖 III 制订的节奏。
- **BD-UI-16(pick 窗两线程形貌)**:UI 线程只画与翻位;spawn+wait 在工作线程
(30s 不冻 UI);结果回槽 `RunShared`;App 见果即关窗,
收场归 `run_pick_launcher`(join + 读槽)。ui 插件自动补入 pick 后配置,
其余勾选即装配面(与 boot 同表)。
- **BD-UI-17(隔离红标的闭环面)**:`ePlugQDown.<名>` 是定向广播(未知名无法
预订阅 —— 严格 exact-type 路由),UI 侧红标走快照行状态扫描
(`is_quarantined`,1s 节拍内落实);隔离行仍可重注入(svctl.start 对隔离行
被 Rejected(重名)拒;既定语义链 = 先 remove 再 start)。红标卡件 trace:
`ek-core::lifecycle::EVENT_PLUG_DOWN_PREFIX("ePlugQDown.")` 常量名与后缀型
一致,但仅作 reference,不预订阅。
- **BD-UI-18(操作指令的唯一通道)**:窗口面动作只 `push_cmd(UiCmd)`,
NOT 直 ask —— `PluginContext` 只在 ui 插件 worker 可用;eTimer 拍先辞行
指令再拉快照;移除走 `pending_confirm` 单槽二步(Armed→Fired;Switched =
目标迁移),取消钮可擦;重注入为单步(加性动作);拒绝类回执
(`Rejected/NoSuchName`)进日志面板,不在 UI 抛错。
- **BD-UI-19(配置面 → 2026-10-17 用户定盘撤销,零配置口径)**:原 S3 落地过
toml-lite + `setting/global.toml` 融入读法 + 状态条 + 脱敏(cfg.rs/cfg 测试
9 例/s7 kittest 1 例)。**现行口径:ek-ui 零配置、宿主零配置** —— 不读任何
配置文件,界面本身无设置;插件结构只由 `--from` 清单或 demo 缺省决定。
需要配置的插件(唯一 = ek-ctp 虚拟账号),配置面长在插件自身,激活时在插件
内完成(见「配置面撤销记录」);宿主/界面不代读不转发。cfg.rs 整块删除;
脱敏纪律(redact)随配置面一同撤销(无配置输入即无秘密暴露面;插件自带
配置面若需展示账号,脱敏规则届时由该插件自持)。
- **BD-UI-20(S4 行情面口径)**:主题面(数据)与身体面(交互)分离——
`MarketStore`持直播线上屏(eBar./eBarUpdate.),(symbol,interval) 格缓存
4096 丢头保尾,帧/更新/回灌三计数 = 「刷新不丢帧统计可见」的三处钉面;
K线 chart 实例**每帧重建**(v1 不保 pan/缩放;首帧快照入库即 review 约定
—— BD-UI-8);图表层引用在本 crate 集中(`render::render_kline_cell`
一点换器,egui_charts→egui_plot 退守路线只动此处,BD-UI-4)。
- **BD-UI-21(S4 直播面工厂纪律)**:UI 宿主的 dm 工厂在 `ui_kinds` 内换建
(real_time=true),ek-station headless 的 rt-off 工厂**不动** ——
宿主差异收在各自工厂表,不回写 ek-station、不新增 svctl 控制面;
dm 工厂 real_time 门为装配参数(缺省 true;UI 观测面要实时门 —— 非配置文件
输入,零配置口径,BD-UI-19 现行版)。
- **BD-UI-15 兑现记录(S3)**:清单地面统一抽世到 `ek-station 0.1.1::pickfile`
(已发布;`ek-ui::pick` 变薄转发,launcher/bin/测试共一处,双实现消灭)。
- **BD-UI-22(S5 纸面网关契约纪律)**:sim 前三口直接复用 ek-ctp 的
Req/Reply 类型(SubscribeReq/SendOrderReq/CancelOrderReq 及回包)——
同构 = 同一类型面,接线换 `sim.` 即插真网关;第四口 `query_rate` 为 sim
自定(ek-ctp 0.2.0 无此口,CTP 流控语义镜像)。撮合规则钉:限价成交于
委托价、市价成交于 close/last、bar.volume = 本拍成交预算、挂单 FIFO ——
手数排队由此可断言。撮合器 `SimMatcher` 纯逻辑零总线(金样本直断),
插件壳只做路由/锁/上总线(锁内撮合锁外 put,与 DmPlugin 同形)。
sim 不引 ek-oms:成交经 eOrder./eTrade./ePosition. 总线入账由 OmsPlugin
被动完成(插件不直调铁律不破)。
- **BD-UI-23(S5 监控页读数纪律)**:监控页读数 = ui 插件 eTimer 拍 ask
`oms.get_all_*` 全量查询帧(与 headless 同源契约;oms 不在 → 空帧不抛错);
下单/撤单/流控三口走 UiCmd → ask `sim.*`(窗口面零直调)。撤单钮只挂
ACTIVE_STATUSES 行(与 ek-data 同口径)。kittest 点击纪律:**`click_accesskit`
取代坐标 `click`** —— 坐标点击对折叠线下(clip 外)节点无效,accesskit
动作可达不可见控件;`get_all_by_label` 精确匹配、`query_by_label_contains`
子串匹配且多命中即炸,撞名列按语义选面。
- **BD-UI-24(S6 CTP 手工配置面,2026-10-17 集成测试战役)**:「插件需要配置 →
配置面在插件自身」的正向界面落地 —— CTP 段 9 输入位全部**人敲**(界面与宿主
不读文件;密码/AuthCode `password(true)` 掩码不回显),`连接 CTP` →
`UiCmd::CtpConfigure(CtpForm)` → ask `ctp.configure`(ek-ctp 0.3.0
D-CONFIGURE:校验/守卫复位/发起归插件,md 账号回落也在插件侧裁决 —— 表单
`to_configure_req` 不预处理);`订阅行情` → ask `ctp.subscribe`(订阅位
逗号/空白解析,空位红字不入列)。`UiCmd` 携 `CtpForm`(全 String 可 PartialEq;
ek-ctp `ConfigureReq` 无 PartialEq,抬线在 ui 插件出队点一次转换,装版不动)。
资金小节 = `oms.get_all_accounts` 帧(可用 = balance−frozen 由 AccountData
自算)。native 经 feature `ctp-native` 透传(默认关;实连战役开,工厂
`with_native` 在 ek-station 侧)。装配序纪律:要观察插件 on_start 的日志,
ui 须在清单里先于该插件(观察序 = 装配序,c0 钉)。
- **BD-UI-25(票据网关选择子,2026-10-17 战役)**:票据新增「网关」位(空 =
`sim` 纸面;填 `ctp` = ask `ctp.send_order`/`ctp.cancel_order`);`UiCmd`
随单携 `gateway`;行级撤单钮跟随委托来路 `gateway_name`(空兜底 sim)。
- **BD-UI-26(CJK 字体回退,2026-10-18 乱码根因)**:epaint 0.33 缺省字体组
(Hack/Ubuntu-Light/NotoEmoji/emoji-icon)**无 CJK 字形** → 中文豆腐块。
`fonts::apply_cjk_fonts` 启动时从系统字体候选表装载一枚 CJK 字体追加
Proportional/Monospace 两族末位(**只收纯 .ttf/.otf** —— epaint 走 ab_glyph,
不吃 .ttc 集合且解析失败即 panic;VF 按缺省实例解析,排位靠后);两处
run_native 取帧闭包接线(runtime + pick 窗)。验收三钉:
f0 字形 API(22 常用汉字 has_glyph)、s16 汉字密集页快照 baseline、实窗
stderr 证据行 `[ek-ui] CJK 回退字体已装载: <path>`。界面文案保持中文单语
(用户定盘:要的是不乱码,不是双语切换)。
- **BD-UI-27(v4 带框架 + 订阅两入口,2026-10-18 布局重做定稿)**:
① **带框架(Python 主界面等价)**:菜单栏(运维|网关|查询合约|帮助)→ 行情带
(全宽,tick 唯一源,交易所二级 tab,双击行弹 K线窗)→ 交易带(左下单面板
9 字段 + 委托/全撤 + 右 2×2 监控格「委托|未成交」「成交」「持仓」「资金|日志」);
回灌/票据界面面退役(总线面 UiCmd::Reload 保留);默认窗 1280×860。
② **订阅两入口(用户裁定「订阅不在网关窗定死」)**:下单面板代码框回车 =
CtpSubscribe(bare symbol,Python returnPressed 等价);菜单「查询合约」弹窗
(Python ContractManager 等价:查询框留空 = 全部 + 勾选 + 订阅选中)。CTP 网关窗
无订阅字段。UI 对合约流只收不催(无催查询总线口)。
③ **egui 0.33 帧内 sizing 退让钉(实窗截图审出)**:帧内 Grid 的 sizing 预估遇
ComboBox 高行会退让,网格后挂的面板内容会叠回网格 → 下单表单 = 网格(按钮/
提示语都是网格行,网格后不挂内容),盘口独立帧,交易带整体包 ScrollArea 保小窗
可达(D7 口径)。④ **kittest 钉**:建构面统一 `harness_for()` 1280×860(实窗
同源);点击帧断言用 `harness.step()` 单帧(`run()` 空闲帧会撤走单帧红字);
撞名 label(委托钮/tab/表题)用 `get_all_by_label` 按渲染序取。
- **BD-UI-28(第五轮审计,2026-10-18 复审修正)**:
① **回车订阅真缺陷(实码修复)**:egui 单行 TextEdit 遇 Enter 即
`surrender_focus`,`has_focus()` 在检查时已 false —— 原 `has_focus +
key_pressed` 口径下回车订阅**实窗从不触发**;改规范口径 `lost_focus() +
key_pressed(Enter)`(s20 钉实测)。回车输入带 `EX.` 前缀则剥成裸码并回写
表单(与查询合约窗同口径,ctp.subscribe 收裸码)。
② **悬空面清除**:`selected_symbol` 四处写零读(注释声称「联动下单面板」
未兑现)→ 字段/访问器删除;行情带行**单击 = 带入下单面板**(方案「行单击
= 选中合约」落地,s8 钉);委托行/持仓行单击无动作(Python 同口径)。
③ **委托行双击 = 撤单**(Python OrderMonitor `itemDoubleClicked→
cancel_order` 等价,活动行限定,s11 钉;原实现双击弹 K线为偏离,已撤 ——
K线入口唯一在行情带双击)。④ **表列补全(Python 蓝图逐值抄录)**:委托表
13→17 列(+名称/交易所/类型/来源=reference)、成交表 10→12(+名称/交易所)、
持仓表 11→16(+名称/交易所/在途/市值=market_value/成本=price);数据面
OrderData/TradeData/PositionData 字段全在场,零新增依赖。⑤ **tick_of
交易所感知**:交易所非空 = EX.symbol 精确匹配(跨所同码不串),空 = 仅按
代码。⑥ **kittest 双击口径**:`step()` 每事件一帧且缺省 step_dt=0.25s →
两击释放间隔 0.5s > egui `max_double_click_delay` 0.3s → 双击钉用
`with_step_dt(0.1)` 建构(s11);同帧四连击不可用(每事件一帧)。Node 借用
harness 树,取 rect/id 后再 step(借用检查);`Role::TextInput` 非 TextBox。
⑦ **Frame 布局泄漏钉(s21 满表快照显形)**:`Frame::show` 继承父级
`ui.horizontal` 的横向布局 → 2×2 格内 tab 行/表题/宽网格横流挤同一行
(空表期隐形,满表现形)→ `group_frame` 内容强制 `ui.vertical`(行情带同款
口径:短行成行,宽网格末位溢出走横向滚动);下单面板帧不动(其内容 = 网格 +
罕见 err label,横流反而护住 BD-UI-27 叠行钉)。委托表表题撤除(tab
「委托|未成交」即 Python 表身份,三重撞名降为两面,s12 口径同步)。
满表视觉钉 s21(委托 17/成交 12/持仓 16 列入 baseline)。
- **BD-UI-29(面板化重构,布局 v5;实窗演示暴露 + 官方文档核对)**:实窗
1280 演示暴露三症:① 交易带定宽 250+140+480+480+间距 ≈ 1438 > 1280 →
右列裁切且带级 ScrollArea 只开纵向 → 裁掉内容不可达(违 BD-UI-27 可达
口径);② 2×2 格定高 250 实际只渲染 ~85px → 底部死区 ~300px;③ 菜单/两带
全挤单 CentralPanel 手工嵌套,无 Python splitter 语义。
- **机制实锤(dbg 探针)**:`[row] avail=[9984, 18]` —— egui horizontal 行
成员的可用高度初始只有一行高(interact_size),**高度分配不沿横向向下
传播**,行按内容收缩 → 格内 `ScrollArea.max_height` 被钳到最小滚动尺寸,
定高失效(②的根因;①为定宽算术,1438>1280 可复现)。
- **官方口径核对**(egui 0.33.3 源码模块文档 = docs.rs 同源;crates.io 包
不含 examples/,egui.rs 书为 JS 渲染页):panel.rs「面板是放控件的正规
区域;顶层面板不得互相嵌套;CentralPanel 必须最后加;Window 挂面板之后」;
`TopBottomPanel::resizable/default_height`、`SidePanel::resizable/
default_width` = QSplitter 官方等价;resizable 前提 = 内容吃满可用空间
(ScrollArea auto_shrink(false) 满足)。
- **落地**:render_body 改收 `&egui::Context` 自开四面板(菜单 → 行情带
240 可拖 → 下单侧栏 250 可拖[下单面板 + 盘口纵排,Python TradingWidget
内序] → CentralPanel 最后 → 弹窗);2×2 格响应式 `allocate_exact_size`
预留 (可用宽-间距)/2 × (可用高-间距)/2 矩形 + `new_child` 在预留矩形内
开 Frame+ScrollArea(高度才吃满;`add_sized` 收 impl Widget 不适用闭包
内容,已试撤);`group_frame` 保留 `ui.vertical` 泄漏钉(BD-UI-28⑦ 延续)。
- **BD-UI-28⑦「下单面板帧不动」被本钉取代**:面板化后下单侧栏内容天然
纵向,err label 后置改由 ScrollArea 吸收 sizing 退让(BD-UI-27 口径不变,
护垫从「横流侥幸」换成「官方吸收面」)。
- **钉**:s3 加结构钉(右列存在贴边不裁切/底行沉底无死区/侧栏在左/两列
等宽随窗分配,先于字体让位执行);快照 s3/s10/s16/s21 重基线;行为钉
s0–s20 全数不动全绿;kittest 建构面 `build_ui` → `build`(ctx 级)。
## 与 ek-station 的边界
ek-ui 依赖 `ek-station = "0.1.7"` / `ek-ctp = "0.5.0"`(装版;禁 path —— 同纪律)。ek-station
本 bin 保持 headless 不引 ek-ui;UI 总装配在 ek-ui(`ui_kinds` 复用 `default_kinds`
+ ui 工厂),launcher(S1c)也在 ek-ui 侧。
## 审计修正记录(2026-10-17 全面复审)
- **BD-UI-22 补钉(撮合两守卫)**:① **tick/bar 双重计价防护** —— tick 成交
挂账 `tick_filled`,bar 收口预算 = bar.volume − 本段 tick 已成(同一笔市场
量只吃一次;u5 钉);② **bar 单调守卫** —— 同 symbol `bar.datetime` 非严格
递增的帧(重复/重放)直接忽略,不重复撮合(u6 钉);③ ask 臂锁面开块
(match 暂存守卫会跨臂存活 —— 锁内造单锁外上总线,与 on_mail 同形)。
- **ek-station 0.1.2(pickfile BOM 剥离)**:Notepad 系 UTF-8 BOM 使首行注释
识别失效 → 整表 BadLine 拒启(活跑实测撞到);read 剥 BOM,p3 钉。
- 复审确认未动面:ek-station headless 工厂表(rt-off)未波及;已发布六仓仅
rustfmt 1.8.0 纯格式对齐(无语义变);setting/global.toml 只读;
ek-ctp/ek-oms 契约类型只引用未改。
## 配置面撤销记录(2026-10-17 用户定盘)
- 用户口径:**界面零配置 + 宿主零配置** —— ek-ui 不读任何配置文件;station
侧也不预读配置转发。插件结构只由 `--from` 清单或 demo 缺省决定。
- **配置归插件自身(定盘)**:激活列表(pick 窗勾选 / svctl start 重注入)时,
哪个插件需要配置,配置面就在该插件内部完成(插件自带配置面/配置服务);
宿主与界面都不代读、不转发、不集中。暂时不需要任何多配置机制。
- 全生态唯一需要配置的插件 = ek-ctp(虚拟账号)。其配置面归 ek-ctp 插件自身
(S6 接线时按此定盘落地:激活 ctp 时配置在插件内完成,不落共享配置树)。
- 落地:cfg.rs(toml-lite/UiConfig/locate_setting/redact)+ CfgStatus 状态条 +
`--config` 形态 + 发现链整块删除;cfg 测试 9 例 + s7 kittest 1 例随撤;
测试 51 → 41。**setting/ 目录已整树清理(2026-10-17 定盘)**:global.toml +
backtest/common/live/test 分层样例(含凭据档)全部删除——插件配置自持,
工作区不留共享配置文件;Python 侧 c:\dev\enkeep_trade_setting 原档不在
清理范围(只读纪律,原样未动)。
## BD-UI-30(0.1.1 薄下单者,OMS 中枢链,2026-10-19)
- **裁定**:下单/撤单直发 `{gw}.send_order`/`{gw}.cancel_order` 的旁路口
**删除**——UI 是薄下单者,管制面收敛到 oms 两服务:
`UiCmd::SendOrder{req, gateway, mode}` → `oms.submit_order`(五段管线:
入口校验/拆单槽位/风控槽位/发送段/记账段;parent 聚合 SENT|PARTIAL|REJECTED
逐腿显形进日志);`UiCmd::CancelOrder{orderid, gateway}` → `oms.cancel_order`
(守卫 4000 不可绕过;symbol/exchange 不再随令携带,oms 订单缓存解;
全撤 = 界面侧逐个扇出到同一守卫口)。BD-UI-23/25 的「ask sim.*/ctp.*」
口径自此被取代(历史记录保留)。
- **「自动开平」勾选(OrderForm.auto_split,默认开)**:拆单四态的 UI 映射 =
Auto(勾选)/None(取消,原样旁路);风控与守卫不可旁,拆单可逐请求旁
(DESIGN ek-oms §6.2 口径的 UI 面)。s22 钉勾选双向 → mode 逐位。
- **装配口径统一**:ui_kinds 复用 station 0.1.7 `default_kinds`——oms 已装配
ek-risk 三闸(standard_gates 缺省限额),面板与 headless 同一台 oms。
- **fixture 退役**:ui_converter_chain 从「offset 夹具进链」升格真管线链
(拆单器入驻 oms 管线,夹具失去存在意义;dev-dep ek-converter 删除)。
顺带实锤忠实初始化口径:无成交史的首轮 `ePositionDone.` 只记时刻等下一轮
(原稿 1268-1288/1282-1285),第二轮距首轮 ≥3s 才置位——链测补第二轮。
- **实连钉**:ui_ctp_live 第 13 步(有仓则 CLOSE 经管线 = CLOSETODAY 实连
证据;7×24 快照市无成交 = OBSERVE 如实记档)。13 步 110s 全收场。
- **依赖**:ek-oms 0.2.1→0.3.1、ek-station 0.1.5→0.1.7(强制 bump,防同构建
两份 oms)。
## BD-UI-31(0.2.0 界面体验轮,2026-10-19;用户四点裁定)
- **R1 菜单一键直达**:「运维/网关/查询合约」menu_button→下拉→入口钮的
两层结构废除,菜单钮即入口,单击直接弹窗(「帮助」保留菜单——多行内容)。
破坏性:三个入口钮 pub 常量删除(标签 = 菜单名)。
- **R2 成交表方向上色**:方向列空绿/多红,补齐与委托/持仓表的一致口径
(此前成交表方向为裸文本 = 遗漏,用户实看指出)。
- **R3 表格行选中 + 悬停跳色**(`UiShared::row_selected/select_row` 新口 +
`paint_row_bg` 渲染工具):委托/未成交/成交/持仓/查询合约窗行首格单击 =
选中行(蓝调半透明跳底,每表一行,换行即换选);行情带选中行 = 下单面板
当前合约(带入即跳色,不另存态);全表悬停 = 淡灰底。行首格既有双击动作
(撤单/带入/K线)全保留——egui 双击先经 clicked,选中顺带发生,无冲突。
叠底为半透明(行文字先画,叠后仍可读)。
- **R4 标题文字撤除**:「行情带(唯一源…)」「下单面板」「盘口(合约)」三处
题字整行删(带位/侧栏即身份;Python TickMonitor/TradingWidget 无题同口径;
盘口括号合约代码一并删——选中合约由下单面板代码格与行情带跳色行指认)。
- **不动面(用户裁定)**:K 线窗标题保留合约;运维窗维持浮动弹窗形态
(O1 内嵌页方案否决);「帮助」菜单保留。
- **钉**:s23 行选中(委托换选 + 成交独立槽);s17 改单击直达;快照重基线。
- **0.2.1 续(动作归行,用户裁定)**:行内任何一格单击/双击都触发该行动作
(`click_cell` 标签矩形叠感应,外观不变);行情带盘口四列带方向
(`import_tick`:买1 价/量 → 卖,卖1 价/量 → 买,价格列顺带填价,其余列
方向/价格保持默认)。r8 钉逐列口径。
## BD-UI-32(0.2.2 字号与独立窗轮,2026-10-19;用户三点裁定)
- **R1 全局字号上调**(`apply_ui_style` 新口):body/button 16、small 14、
heading 22、monospace 15(egui 缺省 13/12/18/12)——用户实看「整体字偏小」,
对照 Python 版;render_body 顶部施加,真实 App 与 kittest 同源(快照重基线)。
- **R2/R3 查询合约窗 + K线窗独立 OS 窗**(`show_viewport_immediate`):
原生最小化/最大化/关闭钮(此前 egui::Window 只有关闭钮,且被物理关在
主窗矩形内不能拖出——用户对照 Python 独立窗形态指出);可拖到屏幕任意
位置、独立任务栏条目。OS 关闭钮 = close_requested → 状态位翻负 → 父窗
下一拍不再 show → eframe 销毁窗。Embedded 回退(后端不支持多窗:
kittest/WEB)= 主窗内弹窗同一本体,测试断言面零改动。
- **不动面**:运维窗/CTP 窗维持主窗内弹窗(用户只点了这两个窗)。
- **选型**:immediate 而非 deferred——回调可捕获 `&UiShared`(UiShared 非
Clone,deferred 需 Arc 化会动 render_body 签名 = 无谓破坏性);StationApp
1s 节流重绘节奏下 immediate 的父子联动重绘代价可忽略。
- **0.2.3 勘误(关不掉根因,用户实看指出):** egui 销毁判据 = 子 viewport
本拍未 used——0.2.2 调用点未按状态位守卫,每拍 show = 每拍 used = OS 窗
永不销毁(X 翻位后又被 show 回来,等效 CancelClose),且窗在主窗一开就
自己出现。修复 = show 整段包进 `contracts_window_open()` 守卫。测试面
教训:Embedded 回退掩盖 Immediate 真窗面(21 绿 ≠ 真窗面正确),s24 注入
recording renderer 补钉;复审纪律补条:新机制的每个调用点必须按状态位辞行。
## BD-UI-33(0.2.4 四窗一型独立化,2026-10-19;用户裁定)
- **运维窗 + CTP 网关窗也转独立 OS 窗**(「四窗皆可脱离主窗」):与查询合约/
K线同形态同守卫;Embedded 回退维持主窗内弹窗(kittest 断言面与快照零改动)。
- **合约表吃满窗口:** 撤 max_height(260) 内嵌时代残留;动作行(计数 +
订阅钮)前置,ScrollArea auto_shrink(false) 吃满剩余窗高——放大窗口 =
表格变高(用户「放大窗口意义在哪里」的直接回应)。
- **`show_independent_window` 助手(四窗一型):** 守卫模式内建(0.2.3 教训
固化成调用方纪律:位关不 show = GC 销毁);Embedded/Immediate 两面的
"被关"统一以返回值出表,消除四份 viewport 样板重复。
## BD-UI-34(0.2.5 表头固定 + 主题 + K线最大化/ESC,2026-10-19;用户裁定)
- **表头固定(`pinned_table` 五表全上):** 官方 DIY 模式(scroll_area.rs:412
注释「painting a sticky header on top of it」)——单 grid 实例(头行+体行)
保列对齐,ScrollArea 之后以本拍捕获的表头格矩形重画表头带(背景盖行 +
Label 重画;矩形已含滚动平移 → 横向跟随、纵向盖顶、零拍滞后;Label 无
交互 sense → 点击穿透)。两轮机制实锤:①vertical 嵌 horizontal 高度塌缩
(s3 底行钉 462<500 抓到);②同 id 双 grid 各从空白重建 State,save 交替
request_repaint = 重绘环(kittest max_steps 爆)——两案皆弃,留 overlay 案。
- **主题四套(金融深色默认/egui深/egui浅/Qt灰白):** 菜单钮一键循环;
Theme 枚举 + AtomicU8 运行期态(零配置口径);apply_ui_style_for 统一施加
(主窗/独立子窗/kittest 三处同源)。语义色跨主题恒定。回答用户「颜色是
默认还是自配」:此前 = egui 默认深色骨架 + 自配语义色(非全默认)。
- **K线窗:** maximized 打开即最大化(s25 钉 builder.maximized=Some(true));
ESC 快捷关闭(body 返回 true → close_kline 同链;s25 钉 Embedded 面注入
Event::Key;Immediate 面键按子窗焦点路由)。
- **行情带外层 ScrollArea 不可撤:** resizable 面板吃满前提(BD-UI-27),
本轮实撤一次即 s3 底行钉实测塌到 462——已恢复并注释钉死。
## BD-UI-34 返修(0.2.6,2026-10-19;用户实看三报,全有机制实锤)
- **竖线(资金右):** 覆盖带 ui.put 视口外表头格 → 布局 min_rect 撑宽 →
Frame 描边矩形撑到 x=1222(= 346.9 + 16 列总宽)。二分实锤:关覆盖带线
消失;空内容时线跟内容宽走(1007)。修 = painter.text/rect_filled 纯绘制
(零布局副作用)+ clip 裁越界。教训:覆盖/装饰一律走 painter,不得用布局
调用(put/label)画视口外内容。
- **表头缝:** 带高须 = 表头格高 + item_spacing.y(3px),否则滚动行从间距带
露出。
- **空表缩格:** 空态不得提前 return——pinned_table 照调(空 body),表头与
吃满滚动区在场。s26 钉三表空态表头同在场。
- **group_frame_plain 使用纪律(0.2.6 钉死):** Frame::show 描边矩形按内容
min_rect 收缩(实锤:label 内容时 gw 宽缩到 112)——只配 auto_shrink(false)
填充物(pinned_table)使用;非表格格(资金/日志)必须走带填充 ScrollArea 的
group_frame,否则 Frame 收缩、右边框落行中段。
## BD-UI-35(0.2.8,2026-10-19;用户实看三报,全机制实锤)
- 行情带状态行「tick N 格 / 最后更新」= 用户裁定撤除(考证:从未删过,
0.2.0 撤的是题字)。s8 反向钉防回归。
- 表头缝硬根因 = clip_rect_margin(默认 3.0)外扩裁剪;覆盖带必须按
inner_rect ± margin 起画。s27 滚动快照 + 像素扫描口径入 CHANGELOG。
- 空态提示一律在表头之下(grid body 首格),不得再置于 pinned_table 之前。
- 列头单源纪律扩散到全部四张格表(const HEADERS):本轮自审又抓到两处
空态分支凭记忆写错——手写第二套表头 = 必错模式,单源是唯一防线。
- 快照回读串行化(SNAP_LOCK):本机 wgpu 并发回读会随机成组失败,测试闸
对此环境敏感;新增快照测试必须走 snap() 不得裸调 harness.snapshot。
## BD-UI-36(0.2.9,2026-10-19;纸面网关迁出界面包)
- 用户裁定:纸面网关 = 网关,应与 ek-ctp 同级独立成包(Python 蓝图 PaperAccount
即与 CTP/Xt/Qmt 同级独立 app;SimNow 登不上时切换的本地模拟流)。
曾议落位:留 ek-ui(否决:界面包藏网关,无头测不到)/并入 ek-ctp(否决:污染
纯 CTP 协议面)→ 裁定 = 独立 crate ek-paper。
- 语义零迁移实锤:旧 sim.rs 与新 matcher+plugin 非注释代码行 449↔449,
Compare-Object 仅 4 处 Arc 写法面差异。
- 工厂表归一:sim 登记项上移 ek-station 0.1.8 default_kinds(无头/有头同一张表);
ek-ui ui_kinds/pick_kinds 撤自建 sim 项防双份(注册名冲突 = add_plugin Duplicate)。
- 教训钉(注册表耦合顺序):改工厂表必须【先发布被依赖方】——station 0.1.8
未发布前 ek-ui 测试 UnknownKind("sim")(本地改动对 registry 依赖不可见,
这正是禁 path 耦合的代价与回报)。
## BD-UI-38(0.2.10,2026-10-19;纸面网关整体退场)
- 用户裁定(2026-10-19):纸面账户功能整体撤除,界面不再有 sim——
**ctp/SimNow 两套前置(12 小时/7×24,选前置用 ek-ctp scripts/simnow-fronts.ps1
-Probe)已满足当前需求**。BD-UI-37(sim 对外面对齐 CTP 的改造方案)作废,
未执行半成品整堆丢弃(ek-paper 工作区回滚到 0.1.1 已发布态)。
- 退场涟漪(三包):ek-station 0.1.12 撤 ek-paper 依赖 + 工厂表 sim 项
(pipeline_headless 改 dm/oms/ctp 装配 + CTP 回报行直发注入,账本/行情/因子
钉数值零变);ek-ui 0.2.10 撤 ek-paper 依赖 + 下拉/兜底/钮/测试全清;
ek-paper 0.1.x 档案原样留存(不删仓库不发版不 yank——yank 反咬已发布老版)。
- 界面口径:接口下拉 ∪ 集改 {ctp};parse_gateway 空兜底 sim→ctp;运维窗
「流控查询」钮撤(UiCmd::QueryRate 变体同步撤——公共 API 减项);
demo 窗 = dm/oms/ui(网关经运维窗激活);demo_tour 纸面段整段删(8 步重编)。
- 测试口径:ui_sim.rs 整删(9 钉);ui_converter_chain 网关位改测试本地
"ctp" 受理夹具(受理形制忠实 ek-ctp:SUBMITTING 单上总线 + ok 回执 +
`{gw}.` 全号;成交按 OnOrderTrade/OnRtnTrade 同构行直发)——本钉主体
一直是 oms 五段管线,与网关撮合无涉,换顶替零减钉;kittest 夹具标签
sim→ctp(五张快照重基线:s3/s10/s16/s21/s27,钮撤除 + 标签改名像素变);
71→62 钉(减的 9 = ui_sim)。
- 纪律钉:历史条目(BD-UI-36、CHANGELOG 0.1.8/0.1.9 sim 入表等)是史实,
不改写;退场只记新条目。
## BD-UI-39(多账号面:账户面板取代网关窗,规划 docs/ek-multi-account-plan.md)
> 本条取代的旧口径(史实保留不改写):BD-UI-24 的 `连接 CTP` → 硬编码
> `ctp.configure` / `ctp.subscribe`;BD-UI-27/33 的「CTP 网关配置窗」单账号窗体;
> BD-UI-38 的接口下拉 ∪ {ctp} 与 `parse_gateway` 空兜底 `"ctp"`。
- **菜单「网关」→「账户」**:旧单账号网关窗退役,收纳为账户面板的添加/编辑表单
(9 输入位 + 密码掩码 + 收录勾选原样沿用,`ctp_grid` 未改字段)。旧窗的死面
(`ctp_window` 状态位、`MENU_CTP`、`BTN_CTP_CONNECT`)一并删除,不留"供旧测试引用"。
- **账户清册在界面侧**(`UiShared.accounts`):行 = 派生注册名 + broker + 账号 +
在场位 + 元账号标记 + 表单留档。派生名唯一入口 `ek_ctp::derived_gateway_name`
(界面不自己拼字符串);密码只在内存,本轮不持久化(规划 §10.2)。
- **添加时序钉(规划 §5.A)**:一个钮两条 cmd —— `Start{factory:"ctp", name:<派生名>}`
先装配,`CtpConfigure{gateway:<派生名>, form}` 后注入凭据(凭据不进 svctl.start 通道)。
- **单源纪律在"添加"这一刻定源(规划 §5.C)**:无元账号且无在场账户 → 新账户自动成为
**行情元账号**(md 前置保留;清掉它等于这个账户自己也永远收不到 tick);已有元账号或
已有在场账户 → 按纯 TD 装配(md 前置清空)。订阅两个入口(下单面板代码回车 /
查询合约窗勾选)一律经 `subscribe_target` 路由到元账号,未指定就明确报错不静默发。
- **元账号连接后锁定(用户裁定「账户连接后不允许切换」)**:任何账户已装配即拒切换;
换源流程 = 撤挂单 → 移除 → 改配置 → 重连 → 重放订阅(订阅集 `note_subscriptions`
登记,元账号新装配后自动重放)。
- **移除守卫**:该账户有活跃委托 = 不给移除钮(只给"先撤单才能移除"提示);有持仓 =
可移除但警告"柜台侧不受影响,重加后由轮转查询重建视图";两步确认沿用 `confirm_step`。
- **网关名回落只剩一个阶梯**:`default_gateway()` = 上次手动选择 → 首个已装配 → 首个;
一个账户都没有 = 禁用「委托」钮并给提示(旧"空 → 字面量 ctp"兜底退役,B4)。
撤单三处(行内钮 / 全撤 / 行双击)一律随该单自己的 `gateway_name`。
- **诚实标注钉**:账户表状态列写「已装配 / 未装配」不写「已连接」——界面只能从
`svctl.list` 看见实例在场,登录成败 ek-ctp 无状态口可查(开放项)。
**(0.2.19 已接上:`{gw}.status` 落地,状态列改两段口径「已装配·<登录态>」;
「不写已连接」这条口径不变,见文末 0.2.19 节。)**
- **测试口径**:s13 重写(添加账户 → 派生名 + 两条 cmd + 首个账户自动定源 +
第二账户 md 清空不被抢源)、s17 菜单改「账户」、s18/s20 订阅路由到元账号、
新增 s28(元账号锁定 + 挂单移除守卫 + 守卫解除);五张快照重基线
(s3/s10/s16/s21/s27,菜单标签与 tick 列名「接口」→「行情源」像素变)。
## 0.2.19(接上网关状态口 + 『断开』钮;规划 docs/ek-ui-account-tabs-plan.md P3,2026-10-22)
事实源换成了 ek-ctp 0.9.0 的两个服务口(`{gw}.status` / `{gw}.disconnect`)。界面侧的六条口径:
- **D-UI-STATUS-1 状态列两段,不合并**:装配位(`svctl.list` 在场)与登录态(`{gw}.status`
读数)是两件事,拼在一栏显示(`已装配·在线` / `·掉线` / `·登录失败` / `·查询断供` /
`·已断开` / `·登录态未知`)但**不合成一个布尔**。0.2.18 的「已装配·登录态未知」保留为
读数缺失时的显式档位 —— 缺口从「不可见」变成「本轮还没问到」,两者必须长得不一样。
- **D-UI-STATUS-2 只认 `logged_in`,不认 `handle_up`**:网关的「句柄已建 / registerFront 已发」
在掉线时仍是真(ek-ctp DESIGN D-STATUS ①)。界面照抄它 = 替掉线的账户谎称在线(s48 读数二钉)。
同理 **"从未回报过"不当"0 秒前回报过"用**:`td_last_account_ms = None` 是"没有这件事",
不是"很久没做"。
- **D-UI-STATUS-3 阈值住在界面**:「查询断供」= 连续被拒达 `QUERY_REJECT_STREAK_LIMIT`(3)
**或** 资金回报超 `ACCOUNT_STALE_SECS`(30 秒)。网关只给原料(计数与时刻),"几次/多久算断供"
是界面口径 —— 它才知道自己多久刷一次、多久问一次。覆盖位 `set_status_thresholds` 已开,
配置文件面还没定(规划 Q10 只说"进配置",ek-ui 现在零配置)。
- **D-UI-STATUS-4 轮询只问"已装配且在场"的行**:每 `STATUS_POLL_TICKS = 2` 拍问一次。
实例被摘除后清册行还在,照问不误就是每次等满 2 秒超时、按账户把 eTimer 刷新循环拖住(c5 钉)。
状态口在 ek-ctp 侧只读、不写日志,所以这条轮询不吃 eLog 环(512 行环保护不变)。
**问不到就退回「未知」并写明原因**,上一轮的「在线」不得留档(s50)。
- **D-UI-DISCONNECT-1 断开 = 停会话,移除 = 停实例,两条口不共用**(规划裁定 1):
『断开』发 `UiCmd::Disconnect` → `{gw}.disconnect`;『移除』发 `UiCmd::Remove` → `svctl.remove`。
断开后装配态不变、徽标「已断开」、恢复入口仍是『连接』(插件侧走「已在场 → 重投凭据」。
**同一道活跃委托守卫两者共用**:会话一停就撤不了单也不再回报,与『移除』要挡的是同一个
观测黑洞(规划 §5.B),出口也同一个(『先撤 N 笔』)—— 这是执行自查补上的一条(s54)。
- **D-UI-STATUS-5 实例不在场即作废读数**:`sync_accounts_connected` 把行从「已装配」打回
「未装配」时,登录态与细节一并清零;渲染侧只给已装配的行印状态细节(s55)。
留着就是替一个已经不存在的会话说话。
- **纯行情账户看 MD 半边**:判据是"该行 td 前置留档是否为空"。TD 半边对它是全假的,
照抄只会得到「未知」——那是假信息(s53)。
- **测试口径**:kittest 45 → 53 钉(s48 三档读数与旧读数不留档、s49 断供两判据与阈值覆盖、
s50 问不到退回、s51 『断开』入列与徽标翻转、s52 未装配无『断开』钮、s53 纯行情看 MD、
s54 断开共用活跃委托守卫、s55 摘除即作废读数);s37 口径翻转;总线级新增 c4/c5
(真起一个 ek-ctp 实例:服务目录含 `.status`/`.disconnect`、ask 真拿到 `GatewayStatus`
—— 读数里没有「问不到」就是两份 ek-ctp 没静默断的证据);s39/s40/s41 快照重基线。
## 0.2.20(三条开放口径落进界面 + 接 ek-ctp 0.9.1 分项;2026-10-22)
- **D-UI-F4-SCOPE 勾选的作用范围写死成一句话,两处同源**:勾选只管**这个网关自己**收录并上抛的
合约;公用合约表 = 在场各网关的并集(规划 D3「合约与行情公用」的直接推论);改勾选须
『连接/重试』才下发。为什么必须写在界面上:实连 S14b 里账户 B 改勾选「期货+期权」后公用表
仍 852 条、期权 0 条,而界面没有任何地方区分「没生效」与「网关没查到」—— 让用户猜就是
把口径缺陷转嫁给用户。**同一口径不得只写在一处**(面板底部 + 配置弹窗,s57 数到 2 处)。
- **D-UI-Q17 运维窗对账户清册实例只列不操作**:名字在清册里 → 那一格改写成「起停归『账户』面板
(带撤单守卫)」,不给『移除』也不给『重注入』(后者对命名实例本来就是 unknown factory)。
理由不是"整洁",是**守卫可绕过**:账户面板的『移除』带活跃委托闸 + 二次确认 + 持仓提示,
运维窗那条是同一动作的无守卫副本,两条路并存等于守卫形同虚设。**清册外的 `ctp_*` 实例仍留
这条出口**(手工起的 / 旧流程残留没有别的退路)—— 边界收口不等于把门焊死。实例仍列在表里:
砍的是操作,不是运维可见性(s56)。
- **D-UI-LOG-WINDOW 日志窗自报是窗口不是历史**:溢出计数原来只有数字;现在同一行接上去处
(「登录/掉线/查询被拒看状态列与状态细节,别回扫这里」)。依据是巡回 G1 的实连证据 ——
90 秒没抓到「交易服务器登录成功」行而资金行下一拍就刷新,**登录其实成功,是 512 行环被
状态推送流挤满**。取证的事实源自此是网关状态口(0.2.19 起),不是日志回扫(s59)。
- **D-UI-STATUS-6 分项点名但不另开一行**:ek-ctp 0.9.1 的 `td_instrument_rejects` 接进状态细节
句尾(「查询连续被拒 3 次;合约查询被拒 2 次」),同一件事不重复点名,分项为 0 不加句子。
总数与连续串**不分路**是对的(消费侧要的是「查询这条路连续被拒几次」),但合约一条没到手时
下一笔单会被 ContractGate 以 `contract:unknown` 拒掉 —— 状态列只写「在线」就是假信息(s58)。
- **测试口径**:kittest 53 → 57 钉(s56 运维窗收口且工厂插件照旧、s57 口径两处同源、
s58 分项三段、s59 日志窗口径);s39/s40/s41 快照重基线(面板底部多一行口径句);
`.gitignore` 补 `**/snapshots/**/*.old.png`。
## 0.2.21(第三条断供判据:「从未到手」也要超时;2026-10-22,依赖不变)
- **D-UI-STATUS-7 「从未发生」不能只是"没有这句话",它自己也要有超时位**:0.2.19 立「`None` 不当 0 用」
时,只把它用在"距最后一次回报多久"这一判据的**前置条件**上(`Some` 才谈超时)。实连 S15f 第一轮
读数照出洞:F2 账户 `logged_in=true`、`td_query_rejects=0`、`td_last_account_ms=None` 持续 390 秒
不变 —— 于是界面永远是「已装配·在线 + 资金回报尚未到手」。**徽标是绿的,查询却彻底停摆**,
这就是 F2 在界面上的形状。
- **修法与边界**:登录着的分支第三条判据 = 距 `td_last_login_ok_ms` 超过同一个 `ACCOUNT_STALE_SECS`
且资金时刻仍 `None` → `已装配·查询断供` + 「资金回报从未到手(登录已 Ns)」。刚登录几秒不算
(轮转第一拍本就要 2~6 秒);真到手即回在线。**不新建第二个阈值**:"多久算等太久"在两条判据里
必须是同一个数,否则同一件事有两种松紧。
- **为什么修在 ek-ui 而不是 ek-ctp**:判据原料(登录时刻 + 读数时刻)网关早就给了,缺的是消费侧
怎么用 `None`。网关侧加"资金超时"位就是把界面口径往上游搬(D-UI-STATUS-3 的反面)。
- **测试口径**:kittest 57 → 58 钉(`s60` 三段:5 秒 = 在线带话 / 40 秒从未到手 = 断供带时长 / 到手即回在线)。
## BD-UI-40(0.2.26,2026-10-22;第五套主题「Qt深色」= Python 版黑色主题复刻)
- **依据是读出来的,不是印象**:Python 版 `enkeep_trade/enkeep_trade/trader/ui/qt.py:102`
`qapp.setStyleSheet(qdarkstyle.load_stylesheet(qt_api="pyside6"))`;本机已装
**QDarkStyle 3.2.3**(`.venv/Lib/site-packages/qdarkstyle`)。色板取自 `dark/palette.py` +
`colorsystem.py` 原值,规格取自编译产物 `dark/darkstyle.qss`(2216 行实测:
`border-radius: 4px` 出现 48 次且是全主题唯一圆角值;描边一律 1px;padding 以 2px 为主)。
- **主题从四套变五套**,菜单钮一键循环加一站:`Qt灰白 → Qt深色 → 金融深色`。
**缺省仍是金融深色** —— 加主题不换缺省,现有 8 张快照一张没动(git 里只有 s42 是新增)。
- **`Theme as u8` 是 `set_theme`/`theme()` 之间的存储形式**,新变体必须留在枚举末尾、
且 `theme()` 的 match 必须加 `4 =>` 臂。**漏臂是这条链上最阴的一种错**:编译器不报、
`set_theme` 照存、读回来落进 `_ =>` 解回金融深色 —— 钮上文案换了而画面不换。挡它的是
`r9c`(五个判别值逐个钉死 + 每个定点值往返一遍)。
- **映射口径(逐条对过 qss)**:
- `window_fill = panel_fill = extreme_bg_color = #19232D`(Gray.B10)。qdarkstyle 所有容器
同底、靠 1px `#455364` 描边区分 —— 三个字段同值不是偷懒,是照抄。
- 按钮三态 = `#455364 → hover #54687A → pressed/checked #60798B`,且 qss 里
`QPushButton { border: none }` → `bg_stroke` 与 `bg_fill` 取同色(画出来就是无边)。
- `expansion` 一律 0.0:qss 的 hover 只换色不涨块,egui 缺省 hover/active 涨 1px。
- `fg_stroke` 宽度一律 1.0:Qt 不在 hover/pressed 加重文字,egui 缺省 1.5/2.0。
- `selection.bg_fill = #346792`(Blue.B40):qss 里 `QWidget::item:selected` 与
`QLineEdit:selected` 同值。**`selection.stroke.color` 被 egui 当选中项文字色用**
(style.rs:374)→ 取 TEXT1 `#DFE1E2`;照抄 qss 的 focus 边框 `#1A72BB` 会让选中 tab 文字变蓝。
- `weak_text_color = Some(#ACB1B6)`(Gray.B90)、`hyperlink_color = #259AE9`(Blue.B70)、
`faint_bg_color = #293544`(Gray.B20)、圆角全态 `CornerRadius::same(4)`。
- `interact_cursor = Some(PointingHand)`:Qt 按钮是手型光标,egui 缺省是浏览器口径。
- **已知偏离(做不到 / 决定不做,逐条给理由)**:
1. **选中 tab 那条 3px `#259AE9` 下划线**(qdarkstyle 的签名样式)—— egui `Visuals` 没有这个
概念,`selectable_label` 只能整块填充。做它要改 `render.rs` 里 8 处手绘,不在本轮范围。
本轮取「整块强调色」代替「灰蓝底 + 亮蓝下划线」。
2. **字号不动**:Python 版是全局 `QFont.pointSize(11)`(96dpi ≈14.7px),我们 body 16px。
字号在 `apply_ui_style_for` 里是跨主题的全局口径、不是主题属性,改它牵动其余四套与 8 张快照。
3. **禁用文本色 `#788D9C` 做不到**:egui 只调 `disabled_alpha`,不能指定具体灰蓝 → 保持 0.5
(那档亮度约 0.55,视觉近似)。
4. **菜单底色 `#37414F` 做不到**:egui 0.33 的 menu/popup 背景跟 `window_fill`,无独立字段。
5. **不移植图标资源**(qss 的 `rc/*.png` 箭头、树形 branch、窗口关闭钮、14×14 复选框图标)
—— egui 自绘,只映射颜色不映射形状。
6. QGroupBox 的 1px 描边 + 粗体标题、表格 gridline:egui 无对应物,靠
`noninteractive.bg_stroke = #455364` 近似。
- **有意不设的字段(防「多余」)**:`text_edit_bg_color`(缺省跟随 `extreme_bg_color`,同值设两遍
是重复)、`code_bg_color`(本 UI 的 `ui.monospace` 只换字体不带背景)、`warn/error_fg_color`
(语义色在渲染侧直绘、跨主题恒定 —— D-UI-34 那条口径不变)、slider/progress 相关(本 UI
0 处 slider、0 处进度条)。
- **测试口径**:ui_state 13 → 16 钉(`r9` 循环改五站 + `r9b` 色值钉 + `r9c` 编码钉 +
`r9d` 反证钉),kittest 58 → 60 钉(`s42_qdark_theme_full_panel` 快照 + `s61` **真实手势**钉:
不碰 `set_theme`,只点菜单钮,连点四下从缺省走到 Qt深色,每档都判「`shared.theme()` 换到位 +
钮文案跟着换 + 画面底色真的换」,再点一下判环闭合回缺省;`s42` 夹具与 `s21` 同源 ——
同一副内容换主题,快照差异只剩主题本身)。全绿 = 104 钉。
- **快照不只用眼睛看**:`target/theme-pixels.ps1` 把 s42 的 110 万像素数成直方图 ——
面板底 `#19232D` 占 **93.17%**、`#455364` 2.09%、`#293544` 2.17%、正文 `#DFE1E2` 2125 px、
选中块 `#346792` 748 px;反证两条同样硬:egui 原生深色底 `#1B1B1B` **0 px**、
金融深色底 `#18202C` **0 px**(出现即说明主题没切过去)。`#54687A`/`#60798B`/`#259AE9`
在静态快照里 0 px 是**预期**(静态图里没有 hover/pressed 控件、界面里没有超链接),
那三支只在 `r9b` 的 Visuals 层钉住。
- **实连巡回取证(真窗口,不是 kittest)**:`examples/opening_multi_tour.rs` 加了一个
`EK_TOUR_THEME` 开关(`qdark | finance | egui-dark | egui-light | qt-classic`,缺省不动;
`examples/` 不在 `cargo package` 清单里 → 不进发布包、不动 crate API)。2026-10-21 17:3x
用 7×24 前置(`EK_CTP_TD/MD` 指到 `EK_CTP_724_TD/MD`)跑一趟 `EK_TOUR_THEME=qdark`:
控制台首行「【开盘演示】主题 = Qt深色」;元账号 `ctp_9999_278153` 登录 = true、纯 TD
`ctp_9999_220380` 登录 = false(该账号在 7×24 前置未注册 —— 环境侧,与本轮改动无关);
交易状态推送流 129 行。窗口截图 `target/gui-qdark.png`(1416×939)同样过像素直方图:
`#19232D` 占 **83.96%**(余下主要是 OS 标题栏与边框)、`#293544` 3.65%、`#455364` 1.82%、
正文 `#DFE1E2` 2111 px、选中 tab `#346792` 707 px,`#1B1B1B` 与 `#18202C` 仍是 0。
截图里菜单钮读作「主题:Qt深色」,委托/成交两表是柜台真数据(6 笔委托 / 2 笔成交),
语义色没漂(LONG 红 / SHORT 绿)。
## BD-UI-41(0.2.27,2026-10-22;对照 Python 版逐项核出来的界面面:表头带 / 网格线 / 日志表格化 / 页签头与下划线)
起因是用户实看 ek-ui 真窗口后点出的五处,并要求「去 Python 版本再看,逐个对比,肯定还有别的细节」。
核对方法:两边都读代码,不凭印象 —— Python 侧读 `enkeep_trade/enkeep_trade/trader/ui/mainwindow.py`
(布局)+ `widget.py`(表格与监控控件)+ 已装 qdarkstyle 3.2.3 的 `dark/darkstyle.qss`(2216 行,样式)。
### 逐条核对结果(12 条,两边都有行号)
| 1 | 表头带底色 | qss:1946-1970 `QHeaderView::section { background #455364; color #DFE1E2; font-size 13px }` | 覆盖带填 `panel_fill` = 表体同色 → 等于没画 | **补**(本轮核心) |
| 2 | 表格外框 | qss:514-519 / 1875-1884 `border: 1px solid #455364` | 已有(`Frame::group` 描边 = `bg_stroke` `#455364`) | 不动 ✓ |
| 3 | 网格线 | 交易表 `widget.py:367 setShowGrid(False)`(注释「隐藏网格线」);**只有**合约表 `2423` / `2621` 没关 | 全无网格 | **只给合约表补** |
| 4 | 斑马纹 | `widget.py:359 setAlternatingRowColors(True)`,但 qss:515/1879 `alternate-background-color` = `#19232D` = 表体底 → 看不见 | 我们 `striped(true)` 硬开,奇行 `#293544`(实连截图里占 10.72%) | **收掉**,改跟 `visuals.striped` |
| 5 | 日志 | `LogMonitor`(`widget.py:1167-1180`)= 带「时间/信息/接口」表头的 QTableWidget,跟 Dock 伸展 | monospace 行 + `max_height(180.0)` 硬顶 | **换成 `pinned_table`** |
| 6 | 每格都有 tab 头 | 四个区全是 QTabWidget(`mainwindow.py:106/126/168/184`),单 tab 的「成交」「持仓」也有头 | 成交/持仓只是 `ui.label` | **补单 tab** |
| 7 | 选中 tab 下划线 | qss:1592 `:selected { background #54687A; border-bottom: 3px solid #259AE9 }` | 无(0.2.26 记为「做不到」) | **补** —— 那条已知偏离作废 |
| 8 | 未选中 tab 头底 | qss:1579 `QTabBar::tab:top { background #455364 }` + 1559 `!selected { border-bottom 2px }` | 无 | **补** |
| 9 | 服务目录表头 | 合约/账户类表都有 `setHorizontalHeaderLabels` | `svc_grid` 六列纯数据无头 | **补**(用户点名) |
| 10 | 表头字号方向 | 表头 13px < 正文 ≈14.7px(`QFont pointSize 11`,`qt.py:106-107`) | 表头 = 正文 = 16px(方向反了) | **表头改 Small 14** |
| 11 | 单元格 padding | `widget.py:369 item { padding: 4px 8px }` | `item_spacing (8,4)` | 相近,**不动** |
| 12 | 列集合 | 委托 16 列 / 行情按所分表 / 成交 12 列 | 委托 17(多「操作」)、行情 14 用 tab 分所 + 「今开」、成交号放第一列 | **有意不动**(功能差异,不是漏) |
### 落法与理由
- **`Chrome` 取色层**:qdarkstyle 有「表头带」和「选中 tab 下划线」两个概念,egui `Visuals`
里没有对应字段(它只有 `selection.bg_fill` 一根)。所以这层只能在渲染侧直绘。
取色**不新造颜色**,全部从各主题已声明的字段派生:`line = noninteractive.bg_stroke.color`、
`header_text = noninteractive.fg_stroke.color`、`tab_underline = hyperlink_color`、
`tab_fill`(Qt深色)= `hovered.bg_fill`。Qt深色那一列取出来正是 qss 原值
(`#455364` / `#DFE1E2` / `#54687A` / `#259AE9`),其余四套取自己的色板 —— 五套一起改,
因为只改 Qt深色等于没改用户日常看的那一套。
- **为什么文字必须重画**:epaint 0.33 没有 z-order(全 crate 搜过:无 `ZOrder` / `z_order` /
`Shape::bg_fill`),形状按插入顺序画 → 后画的带底会盖住先画的表头文字。所以
「盖底 → 画线 → 重画文字」是固定套路,`pinned_table` 覆盖带、`header_band`、`tab_label` 三处同一个理。
- **`header_band` 而不是 `pinned_table`**:账户清单 / 运维插件表 / 服务目录在**自动尺寸浮动窗**里。
0.2.17 实锤(s39):`pinned_table` = `auto_shrink(false)` 吃满整块可用高 + 覆盖带,
在这种窗里凑不出高度 → 表行与其后的口径句互相压字。这三张表行数个位数,不需要固定表头滚动面,
只要那根带。
- **网格线的画法**:`pinned_table_grid` 从 grid 实测的**列边界矩形**(表头格)推列线 x
(`h.max.x + item_spacing.x * 0.5`),从内容矩形 + 行数等分推行线 y。前提:每格都是 label
→ 行高一致(合约表正是如此)。所以网格线只给合约表,交易表不给(那边关了网格)。
- **日志时间戳口径**:`LogRing::push` 入环打 `chrono::Utc::now().with_timezone(+08)` 的
`%H:%M:%S`,与 `fmt_dt` 同一口径 —— `beijing_tz` 因此从 render 提到 state,一份实现两处用。
本机不是 +08 时口径错 = 日志与成交表差几小时,交易员对不上账(钉:
`log_entries_stamp_beijing_wall_clock`)。
- **`snapshot()` 门面不动**:`LogRing` 内部从 `VecDeque<String>` 换成 `VecDeque<LogEntry>`,
但 `snapshot()` 仍返回正文串 —— 30 处既有调用点(测试 / 巡回取证 / demo)一行没改。
渲染侧另开 `entries()` / `logs_entries()`。两个口各有真实消费者,不是重复。
### 已知偏离(记死,不藏)
1. **日志只有两列**(时间/信息),Python 有第三列「接口」:我们的 `log_line` 调用点不带网关,
正文自带 `[ops]` / `[订阅]` 这类前缀。硬造一列空列不如不造。
2. **日志不自动跟到最后一行**(`stick_to_bottom` 撤除):Python 的 QTableWidget 也不自动滚到底。
3. **斑马纹**:Python 的意图是开(`setAlternatingRowColors(True)`),但 qss 让它不可见。
我们跟「不可见」这一结果。**这一条我先前给过相反判断**(一度主张保留斑马帮读 17 列宽表),
最终按「与 Python 一致」定;若日后要开,应开在主题层(`visuals.striped`)而不是表里硬开。
4. **字号**:全局 Body 16 / Small 14 vs Python 全局 `pointSize 11` ≈14.7px。字号是跨主题全局口径,
本轮只把「表头比正文小」这个**方向**修对,没有动全局字号。
5. **列集合**(表 12 行):功能差异不是漏。
### 审计(遗漏 / 错误 / 重复 / 多余 / 逻辑)
- **改了什么、删了什么,逐条可查**:删除的只有 `group_frame`(四格全走 `group_frame_plain` 后无使用者)
和 render 侧那份 `beijing_tz`(搬到 state,render 改 import)。**没有删任何功能面**:
日志钮、溢出提示句、清空钮、`snapshot()` 门面、`log_grid` 之外的所有表容器都在。
- **发现并修掉的自己的错**(都是本轮内自查抓到,不是门禁抓的):
① 合约表两处调用点我先把**空态分支**换成了 `pinned_table_grid(…, rows.len(), …)`(空表画网格),
下一眼读回时看到并改回;② `Chrome` 字段用了裸 `Color32` —— `use egui::Color32` 是
`theme_visuals` 函数内的局部 import,结构体在函数外看不见,编译报 E0425;
③ `Painter::line` 在 0.33 收 `Vec<Pos2>` 不收元组 → 改 `vline` / `hline`;
④ `Grid::show` 返回 `InnerResponse`,`gr.rect` 不存在 → `gr.response.rect`;
⑤ 新测试里「时间」「服务」「实例」都不是唯一标签(行情带也有「时间」列、运维窗也有「服务」标题、
服务目录有三对「实例」头)→ 改 `query_all_by_label` 数个数,顺带把断言变强。
- **重复**:一度想给日志表加「时间列 + 正文列 + 接口列」三列,与「调用点不带网关」冲突 → 砍成两列。
`snap` / `snap_budget` 两个快照入口是**必要**的两条:一条 0 容差,一条带墙钟预算,共用同一把锁。
- **多余**:`group_frame` 删掉;`Chrome` 里一度准备放 `tab_head_stroke` 与 `tab_underline_width`
两个字段,前者与 `line` 同值、后者只有 2/3 两个常量 → 都不进结构体,写在使用处。
- **逻辑问题(本轮最容易再犯的一种)**:带墙钟时间列的快照天然不确定。第一反应是「调容差」,
真正的处理是把**结构面**钉在 AccessKit 标签上(确定性),把**像素图**降级成带预算的辅助证据,
并在 `snap_budget` 的注释里写清为什么这三张要预算。
- **没动的**(有意):`snapshot_lock()` 旧调用形式(9 处)只在新钉里换成 `snap_guard` ——
改全部旧站点属于另一轮清理,本轮不夹带。
### 证据
- 门禁:`scripts/gate.ps1` 全绿(fmt / test 111 钉 / clippy `-D warnings` 0 / doc 0 / check-package strict)。
- 快照:9 张重生成 + 新增 `s63_logs_table_headers`、`s65_ops_service_headers`;
肉眼核过 `s42`(Qt深色)与 `s21`(金融深色):六张表都有表头带,委托/未成交、资金/日志、
行情带交易所、成交、持仓五处都有 tab 头,选中 tab 有下划线,画面无斑马。
- 像素直方图(`target/theme-pixels.ps1`,本轮加两条硬检查):`#259AE9 ≥ 1`(选中 tab 下划线,
改前 0)与 `#54687A ≥ 200`(选中 tab 底,改前只在 hover 时出现 = 0)。
- 真窗口:`EK_TOUR_THEME=qdark` 实连 7×24 前置一趟,截图过同一套像素检查(见本轮报告)。