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.
This commit is contained in:
@@ -0,0 +1,399 @@
|
||||
# -*- 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 级 setFont(QSS ::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()
|
||||
Reference in New Issue
Block a user