Files
Haocode/tests/tune_model_popup.py
T
sorrow404null a7412824e0 chore: import original project baseline
Import the pre-repair source tree as the history baseline.
Runtime data (data/), virtualenvs, bytecode caches and logs are
gitignored so local secrets and user state stay out of the repo.
2026-09-17 16:40:01 +08:00

400 lines
17 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# -*- coding: utf-8 -*-
"""模型选择弹窗 · 可视化调参工具
运行: C:\\Users\\14890\\miniconda3\\envs\\haocode\\python.exe tests\\tune_model_popup.py
- 左侧:调参窗(改参数 → 右侧弹窗实时重渲染)
- 右侧:真实的 ModelSelectPopup 本体(独立窗口,点调参窗不会消失)
- 满意后点「确定」→ 参数 JSON 写入 data/model_popup_tune.json → 自动退出
"""
import os
import sys
import json
import traceback
sys.path.insert(0, os.path.join(os.path.dirname(__file__), ".."))
os.environ["QTWEBENGINE_CHROMIUM_FLAGS"] = "--disable-gpu"
os.environ.setdefault("HAOCODE_RENDER", "software")
from PyQt6.QtWidgets import ( # noqa: E402
QApplication, QWidget, QFormLayout, QHBoxLayout, QSpinBox,
QPushButton, QCheckBox, QLabel, QMessageBox, QComboBox,
)
from PyQt6.QtCore import Qt, QSize # noqa: E402
from PyQt6.QtGui import QPixmap, QIcon, QPainter, QFont, QFontDatabase # noqa: E402
from ui.views.main_window import ModelSelectPopup, MainWindow, _popup_svg_path # noqa: E402 (先于 QApplication)
DEFAULTS = {
# —— 字体 ——
"font_family": "Microsoft YaHei", # 字体族(下拉选择;调参确认值)
# —— 模型行 ——
"item_font": 13, # 模型名字号(经 list_widget.setFont 生效)
"model_item_h": 27, # 模型行高
"model_icon": 16, # 模型图标尺寸
"model_icon_pad": 34, # 模型行左边距(图标透明左垫,仅作用模型行)
# —— 供应商行 ——
"header_height": 38, # 供应商行高(= widget 高;默认 38 = 当前代码实际行高)
"header_font": 13, # 供应商名字号
"count_font": 10, # 数量字号
"provider_svg": "provider.svg", # 供应商图标(3 个 SVG 可切换)
"header_icon": 16, # 供应商图标尺寸
"header_icon_pad": 0, # 供应商图标水平位置(左侧留白,仅推图标与后续内容)
"header_icon_pad_v": 1, # 供应商图标垂直位置(正=下移,负=上移)
"arrow_size": 13, # 展开/收起箭头尺寸
"header_hpad_l": 3, # 供应商行左边距(头部按钮左 margin)
# —— 整体 ——
"popup_width": 340, # 弹窗宽度
"container_vmargin": 4, # 容器上下边距
}
RANGES = {
"item_font": (10, 20),
"model_item_h": (18, 40), "model_icon": (8, 28),
"model_icon_pad": (0, 48), "header_height": (20, 60), "header_font": (9, 18),
"count_font": (8, 16), "header_icon": (10, 28), "header_icon_pad": (0, 30),
"header_icon_pad_v": (-10, 10),
"arrow_size": (8, 20),
"header_hpad_l": (0, 20),
"popup_width": (260, 420), "container_vmargin": (0, 12),
}
CN = {
"font_family": "字体(下拉选择)",
"item_font": "模型名字号 (px)",
"model_item_h": "模型行高 (px)",
"model_icon": "模型图标尺寸 (px)", "model_icon_pad": "模型行左边距 (px)",
"header_height": "供应商行高 (px)", "header_font": "供应商名字号 (px)",
"count_font": "数量字号 (px)", "header_icon": "供应商图标尺寸 (px)",
"provider_svg": "供应商图标(SVG 切换)",
"header_icon_pad": "供应商图标水平位置 (px)",
"header_icon_pad_v": "供应商图标垂直位置 (px,正=下移)",
"arrow_size": "箭头尺寸 (px)", "header_hpad_l": "供应商行左边距 (px)",
"popup_width": "弹窗宽度 (px)",
"container_vmargin": "容器上下边距 (px)",
}
SECTIONS = [
("字体", ["font_family"]),
("模型行", ["item_font", "model_item_h", "model_icon", "model_icon_pad"]),
("供应商行", ["header_height", "header_font", "count_font",
"provider_svg", "header_icon", "header_icon_pad",
"header_icon_pad_v", "arrow_size", "header_hpad_l"]),
("整体", ["popup_width", "container_vmargin"]),
]
# 供应商图标候选(文件名, 显示名)
PROVIDER_SVGS = [
("provider.svg", "图标 1 · 服务器"),
("provider2.svg", "图标 2 · 二级服务器"),
("provider3.svg", "图标 3 · 服务器(细线)"),
]
FONT_STACK = '"HarmonyOS Sans SC", "Microsoft YaHei", "Noto Sans SC", sans-serif'
QSS_TMPL = """
* {
font-family: FONT_STACK;
}
#popup_container {
background-color: #ffffff;
border: 1px solid #dcdcdc;
border-radius: 12px;
}
#group_toggle_btn { background: transparent; border: none; }
#group_toggle_btn:hover { background-color: #f2f5f9; border-radius: 6px; }
#model_list {
border: none;
background: transparent;
outline: none;
}
#model_list::item {
font-family: FONT_STACK;
color: #333333;
font-weight: normal;
/* 实测本 Qt/PyQt6 构建下 ::item 仅 background-color/color 生效;
font-size/font 缩写/font-family/margin/padding/border-radius 全部无效——
字号走 setFont,行高走 sizeHint,边距走图标透明垫/按钮 margin */
}
#model_list::item:hover {
background-color: #f0f4f9;
color: #111111;
}
#model_list::item:selected {
background-color: #e8f0fe;
color: #1a73e8;
font-weight: bold;
}
QScrollBar:vertical {
border: none;
background: transparent;
width: 5px;
margin: 12px 2px;
}
QScrollBar::handle:vertical {
background: #d0d0d0;
min-height: 20px;
border-radius: 3px;
}
QScrollBar::handle:vertical:hover {
background: #a0a0a0;
}
QScrollBar::add-line:vertical, QScrollBar::sub-line:vertical {
height: 0px;
}
"""
class Tuner:
def __init__(self):
self.app = QApplication(sys.argv)
self.params = dict(DEFAULTS)
# ---- 弹窗本体(真实类 + 真实 config + 真实祖先上下文) ----
# ⚠️ 保真关键:实际 App 中弹窗是 MainWindow 的子部件,会继承其全局样式表
# QListWidget::item 的 padding/margin/选中蓝条等);若 parent=None 渲染,
# 就会出“调试与实际不符”的矛盾 → 借 MainWindow.setup_stylesheet 把真实
# 样式表套到宿主上,弹窗挂在宿主下渲染(与生产同构)
self._host = QWidget()
MainWindow.setup_stylesheet(self._host)
self._host.resize(1, 1)
self._host.move(-200, -200)
self._host.show()
cfg_path = os.path.join(os.path.dirname(__file__), "..", "data", "config.json")
with open(cfg_path, "r", encoding="utf-8") as f:
cfg = json.load(f)
self.popup = ModelSelectPopup(self._host, cfg)
self.popup.setWindowFlags(Qt.WindowType.Window | Qt.WindowType.FramelessWindowHint)
self.list = self.popup.list_widget
self._tag_headers()
screen = self.app.primaryScreen().availableGeometry()
self.px = screen.center().x() - 200
self.py = screen.center().y() - 300
self.popup.move(self.px, self.py)
self.popup.show()
# ---- 调参窗 ----
self.win = QWidget()
self.win.setWindowTitle("模型选择弹窗调参 —— 调好后点「确定」")
form = QFormLayout(self.win)
form.setSpacing(7)
self.spins = {}
self.combos = {}
self.broken = set() # 用户标记的“不生效”参数
for title, keys in SECTIONS:
form.addRow(QLabel(f"—— {title} ——"))
for key in keys:
mark = QPushButton("不生效")
mark.setCheckable(True)
mark.setFixedWidth(58)
mark.setCursor(Qt.CursorShape.PointingHandCursor)
mark.setStyleSheet("""
QPushButton { font-size: 11px; color: #888888; border: 1px solid #cccccc;
border-radius: 4px; background: #fafafa; }
QPushButton:checked { color: #ffffff; background: #d93025; border-color: #d93025; }
""")
mark.toggled.connect(lambda on, k=key: self.on_mark(k, on))
row = QHBoxLayout()
row.setSpacing(4)
if key == "font_family":
combo = QComboBox()
fams = ["HarmonyOS Sans SC", "Microsoft YaHei", "Noto Sans SC",
"Segoe UI", "Arial", "Consolas", "SimHei", "PingFang SC"]
seen = set()
for fam in fams + sorted(QFontDatabase.families()):
if fam and fam not in seen:
seen.add(fam)
combo.addItem(fam)
combo.setCurrentText(self.params[key])
combo.currentTextChanged.connect(lambda v, k=key: self.on_change(k, v))
self.combos[key] = combo
row.addWidget(combo, 1)
elif key == "provider_svg":
combo = QComboBox()
for fname, cname in PROVIDER_SVGS:
combo.addItem(cname, fname)
combo.setCurrentIndex(max(combo.findData(self.params[key]), 0))
combo.currentIndexChanged.connect(
lambda i, k=key, c=combo:
self.on_change(k, c.itemData(i) or "provider.svg"))
self.combos[key] = combo
row.addWidget(combo, 1)
else:
lo, hi = RANGES[key]
sp = QSpinBox()
sp.setRange(lo, hi)
sp.setValue(self.params[key])
sp.valueChanged.connect(lambda v, k=key: self.on_change(k, v))
self.spins[key] = sp
row.addWidget(sp, 1)
row.addWidget(mark)
form.addRow(CN[key], row)
self.chk_selected = QCheckBox("显示选中行(预览)")
self.chk_selected.toggled.connect(self.on_toggle_selected)
form.addRow(self.chk_selected)
btns = QHBoxLayout()
b_reset = QPushButton("重置默认")
b_reset.clicked.connect(self.reset)
b_ok = QPushButton("✅ 确定(写入日志并退出)")
b_ok.setStyleSheet("font-weight: bold;")
b_ok.clicked.connect(self.finish)
btns.addWidget(b_reset)
btns.addWidget(b_ok)
form.addRow(btns)
self.win.resize(380, min(self.win.sizeHint().height(), screen.height() - 80))
self.win.move(max(20, self.px - 400), self.py)
self.win.show()
self.apply_all()
# ---------- 启动时给头部标签打角色标签(此时尺寸还是默认值,判定可靠) ----------
def _tag_headers(self):
for g in self.popup._groups:
hwd = self.list.itemWidget(g["header_item"])
for lbl in hwd.findChildren(QLabel):
txt = lbl.text()
if txt == "":
# 默认: 供应商图标 16×16, 箭头 12×12 → 启动时按尺寸区分(此后不再变判定基准)
lbl.setProperty("_role", "icon" if lbl.size().width() >= 14 else "chevron")
elif txt.isdigit():
lbl.setProperty("_role", "count")
else:
lbl.setProperty("_role", "name")
# ---------- 实时渲染 ----------
def on_mark(self, k, on):
if on:
self.broken.add(k)
else:
self.broken.discard(k)
def on_change(self, k, v):
self.params[k] = v
self.apply_all()
def _rebuild_icons(self):
p = self.params
pop = self.popup
pop._provider_pixmap = QPixmap(_popup_svg_path(p.get("provider_svg", "provider.svg"))).scaled(
p["header_icon"], p["header_icon"],
Qt.AspectRatioMode.KeepAspectRatio, Qt.TransformationMode.SmoothTransformation)
pop._arrow_expanded = QIcon(_popup_svg_path("chevron_down.svg")).pixmap(p["arrow_size"], p["arrow_size"])
pop._arrow_collapsed = QIcon(_popup_svg_path("chevron_right.svg")).pixmap(p["arrow_size"], p["arrow_size"])
m = QPixmap(_popup_svg_path("model.svg")).scaled(
p["model_icon"], p["model_icon"],
Qt.AspectRatioMode.KeepAspectRatio, Qt.TransformationMode.SmoothTransformation)
pad_w = p["model_icon"] + p["model_icon_pad"]
_pad = QPixmap(pad_w, p["model_icon"])
_pad.fill(Qt.GlobalColor.transparent)
_pp = QPainter(_pad)
_pp.drawPixmap(p["model_icon_pad"], 0, m)
_pp.end()
pop._model_icon = QIcon(_pad)
# 关键:item 持有 QIcon 的拷贝,类属性替换后必须逐个 setIcon 才会更新(实测)
for g in pop._groups:
for it in g["model_items"]:
it.setIcon(pop._model_icon)
self.list.setIconSize(QSize(pad_w, p["model_icon"]))
def apply_all(self):
p = self.params
pop = self.popup
family = p["font_family"]
stack = f'"{family}", "Microsoft YaHei", "Noto Sans SC", sans-serif'
pop.setFixedWidth(p["popup_width"])
pop.container_layout.setContentsMargins(0, p["container_vmargin"], 0, p["container_vmargin"])
self._rebuild_icons()
for g in pop._groups:
hwd = self.list.itemWidget(g["header_item"])
hwd.setFixedHeight(p["header_height"])
g["header_item"].setSizeHint(QSize(0, p["header_height"])) # 行高 = widget 高
btns = hwd.findChildren(QPushButton)
if btns:
bl = btns[0].layout()
if bl is not None:
bl.setContentsMargins(p["header_hpad_l"], 0, 8, 0)
for lbl in hwd.findChildren(QLabel):
role = lbl.property("_role")
if role == "icon":
pad = p["header_icon_pad"]
pv = p["header_icon_pad_v"]
# 2 倍 margin:抵消 label 行内居中吸收的一半增高 → 1:1 位移
top, bot = 2 * max(pv, 0), 2 * max(-pv, 0)
lbl.setFixedSize(p["header_icon"] + pad, p["header_icon"] + top + bot)
lbl.setContentsMargins(pad, top, 0, bot)
lbl.setPixmap(pop._provider_pixmap)
elif role == "chevron":
lbl.setFixedSize(p["arrow_size"], p["arrow_size"])
lbl.setPixmap(pop._arrow_expanded if g["expanded"] else pop._arrow_collapsed)
elif role == "name":
lbl.setStyleSheet(
f"font-family: {stack}; color: #555555; font-weight: normal;"
f"font-size: {p['header_font']}px; letter-spacing: 1px; background: transparent;")
elif role == "count":
lbl.setStyleSheet(
f"font-family: {stack}; color: #888888;"
f"font-size: {p['count_font']}px; font-weight: normal; background: transparent;")
for it in g["model_items"]:
it.setSizeHint(QSize(0, p["model_item_h"]))
pop.setStyleSheet(QSS_TMPL
.replace("FONT_STACK", stack))
# 模型行字体:widget 级 setFontQSS ::item 的 font-size/缩写都不可靠,实测)
_f = self.list.font()
_f.setPixelSize(p["item_font"])
_f.setFamily(family)
self.list.setFont(_f)
self.app.processEvents()
pop.adjust_popup_height()
pop.adjustSize()
def on_toggle_selected(self, on):
if on:
for g in self.popup._groups:
if g["model_items"] and not g["model_items"][0].isHidden():
self.list.setCurrentItem(g["model_items"][0])
break
else:
self.list.clearSelection()
def reset(self):
self.params = dict(DEFAULTS)
for k, sp in self.spins.items():
sp.blockSignals(True)
sp.setValue(self.params[k])
sp.blockSignals(False)
for k, cb in self.combos.items():
cb.blockSignals(True)
cb.setCurrentText(self.params[k])
cb.blockSignals(False)
self.apply_all()
# ---------- 确定 → 写日志 ----------
def finish(self):
log = os.path.join(os.path.dirname(__file__), "..", "data", "model_popup_tune.json")
os.makedirs(os.path.dirname(log), exist_ok=True)
payload = dict(self.params)
payload["not_working"] = sorted(self.broken) # 用户标记的不生效参数
with open(log, "w", encoding="utf-8") as f:
json.dump(payload, f, ensure_ascii=False, indent=2)
print("TUNE_LOG=" + os.path.abspath(log), flush=True)
print(json.dumps(payload, ensure_ascii=False), flush=True)
self.app.quit()
def main():
try:
Tuner()
QApplication.instance().exec()
except Exception:
err = traceback.format_exc()
try:
with open(os.path.join(os.path.dirname(__file__), "..", "data",
"tune_error.log"), "w", encoding="utf-8") as f:
f.write(err)
except OSError:
pass
QMessageBox.critical(None, "调参工具启动失败", err)
sys.exit(1)
if __name__ == "__main__":
main()