Expose layout API and refresh regression docs

This commit is contained in:
Codex
2026-05-09 19:15:23 +08:00
parent 738cf035bb
commit 2388f22c99
21 changed files with 2491 additions and 367 deletions
+68
View File
@@ -162,6 +162,14 @@ namespace
<< SX_T(" 原因=当前控件禁止该轴 Stretch", " reason=stretch disabled by capability"); << SX_T(" 原因=当前控件禁止该轴 Stretch", " reason=stretch disabled by capability");
} }
} }
void ActivateExplicitLayoutSpecMode(StellarX::LayoutMode& mode)
{
// 新公开布局 API 明确属于“按边约束解算”的语义层。
// 一旦调用,说明外部希望控件进入统一锚定布局模型,
// 因此直接切换到 AnchorToEdges,避免出现“策略已设但 layoutMode 仍是 Fixed”的半失效状态。
mode = StellarX::LayoutMode::AnchorToEdges;
}
} }
StellarX::ControlText& StellarX::ControlText::operator=(const ControlText& text) StellarX::ControlText& StellarX::ControlText::operator=(const ControlText& text)
@@ -253,6 +261,56 @@ void Control::setAnchor(StellarX::Anchor anchor_1, StellarX::Anchor anchor_2)
this->layoutSpec.horizontal = BuildLegacyAxisSpec(anchor_1, anchor_2, StellarX::Anchor::Left, StellarX::Anchor::Right); this->layoutSpec.horizontal = BuildLegacyAxisSpec(anchor_1, anchor_2, StellarX::Anchor::Left, StellarX::Anchor::Right);
this->layoutSpec.vertical = BuildLegacyAxisSpec(anchor_1, anchor_2, StellarX::Anchor::Top, StellarX::Anchor::Bottom); this->layoutSpec.vertical = BuildLegacyAxisSpec(anchor_1, anchor_2, StellarX::Anchor::Top, StellarX::Anchor::Bottom);
} }
void Control::setHorizontalLayoutSpec(const StellarX::AxisLayoutSpec& spec)
{
ActivateExplicitLayoutSpecMode(this->layoutMode);
this->layoutSpec.horizontal = spec;
}
void Control::setVerticalLayoutSpec(const StellarX::AxisLayoutSpec& spec)
{
ActivateExplicitLayoutSpecMode(this->layoutMode);
this->layoutSpec.vertical = spec;
}
void Control::setHorizontalAnchors(bool left, bool right)
{
ActivateExplicitLayoutSpecMode(this->layoutMode);
this->layoutSpec.horizontal.anchorStart = left;
this->layoutSpec.horizontal.anchorEnd = right;
}
void Control::setVerticalAnchors(bool top, bool bottom)
{
ActivateExplicitLayoutSpecMode(this->layoutMode);
this->layoutSpec.vertical.anchorStart = top;
this->layoutSpec.vertical.anchorEnd = bottom;
}
void Control::setHorizontalSizePolicy(StellarX::AxisSizePolicy policy)
{
ActivateExplicitLayoutSpecMode(this->layoutMode);
this->layoutSpec.horizontal.sizePolicy = policy;
}
void Control::setVerticalSizePolicy(StellarX::AxisSizePolicy policy)
{
ActivateExplicitLayoutSpecMode(this->layoutMode);
this->layoutSpec.vertical.sizePolicy = policy;
}
void Control::setHorizontalAlignPolicy(StellarX::AxisAlignPolicy policy)
{
ActivateExplicitLayoutSpecMode(this->layoutMode);
this->layoutSpec.horizontal.alignPolicy = policy;
}
void Control::setVerticalAlignPolicy(StellarX::AxisAlignPolicy policy)
{
ActivateExplicitLayoutSpecMode(this->layoutMode);
this->layoutSpec.vertical.alignPolicy = policy;
}
StellarX::Anchor Control::getAnchor_1() const StellarX::Anchor Control::getAnchor_1() const
{ {
return this->anchor_1; return this->anchor_1;
@@ -265,6 +323,16 @@ StellarX::LayoutMode Control::getLayoutMode() const
{ {
return this->layoutMode; return this->layoutMode;
} }
StellarX::AxisLayoutSpec Control::getHorizontalLayoutSpec() const
{
return layoutSpec.horizontal;
}
StellarX::AxisLayoutSpec Control::getVerticalLayoutSpec() const
{
return layoutSpec.vertical;
}
const StellarX::LayoutSpec& Control::getLayoutSpec() const const StellarX::LayoutSpec& Control::getLayoutSpec() const
{ {
return layoutSpec; return layoutSpec;
+24
View File
@@ -177,12 +177,36 @@ public:
void setLayoutMode(StellarX::LayoutMode layoutMode_); void setLayoutMode(StellarX::LayoutMode layoutMode_);
// 设置旧版双锚点输入,并映射到内部统一 LayoutSpec // 设置旧版双锚点输入,并映射到内部统一 LayoutSpec
void setAnchor(StellarX::Anchor anchor_1, StellarX::Anchor anchor_2); void setAnchor(StellarX::Anchor anchor_1, StellarX::Anchor anchor_2);
// 直接设置水平轴布局规格。
// 调用该接口后会自动切换到 AnchorToEdges 布局模式;
// 这是新布局模型的公开入口,后设置者会覆盖旧 setAnchor() 对水平轴的映射结果。
void setHorizontalLayoutSpec(const StellarX::AxisLayoutSpec& spec);
// 直接设置垂直轴布局规格。
// 调用该接口后会自动切换到 AnchorToEdges 布局模式;
// 这是新布局模型的公开入口,后设置者会覆盖旧 setAnchor() 对垂直轴的映射结果。
void setVerticalLayoutSpec(const StellarX::AxisLayoutSpec& spec);
// 设置水平轴锚定边集合(left / right)。
void setHorizontalAnchors(bool left, bool right);
// 设置垂直轴锚定边集合(top / bottom)。
void setVerticalAnchors(bool top, bool bottom);
// 设置水平轴尺寸策略(Stretch / FixedSize)。
void setHorizontalSizePolicy(StellarX::AxisSizePolicy policy);
// 设置垂直轴尺寸策略(Stretch / FixedSize)。
void setVerticalSizePolicy(StellarX::AxisSizePolicy policy);
// 设置水平轴固定尺寸位移策略(Start / End / Center / Proportional)。
void setHorizontalAlignPolicy(StellarX::AxisAlignPolicy policy);
// 设置垂直轴固定尺寸位移策略(Start / End / Center / Proportional)。
void setVerticalAlignPolicy(StellarX::AxisAlignPolicy policy);
// 获取旧版锚点 1(兼容读取入口) // 获取旧版锚点 1(兼容读取入口)
StellarX::Anchor getAnchor_1() const; StellarX::Anchor getAnchor_1() const;
// 获取旧版锚点 2(兼容读取入口) // 获取旧版锚点 2(兼容读取入口)
StellarX::Anchor getAnchor_2() const; StellarX::Anchor getAnchor_2() const;
// 获取旧版布局模式 // 获取旧版布局模式
StellarX::LayoutMode getLayoutMode() const; StellarX::LayoutMode getLayoutMode() const;
// 获取水平轴布局规格;返回的是当前生效的新模型状态,不要求可逆回旧 anchor 语义。
StellarX::AxisLayoutSpec getHorizontalLayoutSpec() const;
// 获取垂直轴布局规格;返回的是当前生效的新模型状态,不要求可逆回旧 anchor 语义。
StellarX::AxisLayoutSpec getVerticalLayoutSpec() const;
// 获取内部统一布局规格;供 Window / Canvas 等统一解算入口使用。 // 获取内部统一布局规格;供 Window / Canvas 等统一解算入口使用。
const StellarX::LayoutSpec& getLayoutSpec() const; const StellarX::LayoutSpec& getLayoutSpec() const;
// 获取控件能力边界;用于判断某个轴是否允许 Stretch。 // 获取控件能力边界;用于判断某个轴是否允许 Stretch。
+441 -367
View File
@@ -1,8 +1,50 @@
// StellarX 星垣GUI框架 - 测试用例 // StellarX 星垣GUI框架 - 测试用例
//
// 当前阶段测试入口说明(2026-04):
// 1. KEY1:旧页签链路 + Table 超界残留回归
// - 目标:验证 TabControl 重复激活同一页签、切页、关页后的快照/清理链是否稳定。
// - 预期:
// * 外部重复激活当前页签,不再扰动已激活页的快照链。
// * 页签 1 中 Table 超出页区域的部分,在切页或关闭页签时不会残留。
// * 触发正常全量重绘后,不应出现额外残影或层级错乱。
//
// 2. KEY2:公开布局 API 首个迁移样例
// - 目标:验证新的 AxisSizePolicy / AxisAlignPolicy 公开接口在真实场景里可用。
// - 预期:
// * 32 位选择区中的“位号 + 位按钮”作为单元整体位移,窗口拉伸后保持对齐。
// * 位按钮点击、位取反、左移、右移、一键置 0/1、签名切换,会统一刷新十六进制、
// 十进制、上次值、本次值和 initData。
// * 顶层五个区块在正常拉伸/最大化后继续铺满窗口,不再依赖旧双锚点硬凑。
// * 当前更偏重横向铺满验证;极窄窗口下功能区是否进一步自适应,不作为本阶段硬指标。
//
// 3. KEY3:旧业务大场景保留用例
// - 目标:保留历史业务页示例,观察老场景在新主线下是否出现明显倒退。
// - 预期:
// * 登录页、TabControl、旧业务页基本行为保持可运行。
// * 当前不作为本阶段主回归集,不要求覆盖新的布局 API 或 overlay 专项。
//
// 4. KEY4Dialog / MessageBox 专项回归
// - 目标:验证模态/非模态对话框、遮挡交互、关闭后 hover 恢复、拖拽 resize 等链路。
// - 预期:
// * 非模态 Dialog 遮挡底层按钮时,不应出现 hover/tooltip 穿透或残留。
// * 模态 Dialog 打开后拖拽窗口,标题、关闭按钮和底层恢复链保持稳定。
// * 对话框关闭后,底层按钮 hover 能及时恢复。
// - 备注:当前它更适合作为 Dialog 专项补充,不纳入每轮主集。
//
// 5. KEY5:第二阶段专项主回归
// - 目标:覆盖三层 Canvas、TabControl、Tooltip、overlay、Table、页码与分页按钮等主线风险点。
// - 预期:
// * 三层 Canvas 嵌套下,深层按钮的 hover / press / release / tooltip 都能正确刷新。
// * Window / Canvas / TabControl 的 overlay 补画与 coverage 链闭合,不再出现遮挡错层。
// * TabControl 页签按钮与页面层级正确;页签 tooltip 不会再被页面盖掉。
// * Table 分页按钮、页码标签、与上层浮层相交时的重绘链保持正确。
//
// 当前阶段建议主回归集:KEY1 + KEY2 + KEY5
// Dialog / MessageBox 补充专项:KEY4
#include"StellarX.h" #include"StellarX.h"
#include <vector> #include <vector>
#ifndef KEY #ifndef KEY
#define KEY 4 #define KEY 5
#endif #endif
#if 5 == KEY #if 5 == KEY
@@ -433,7 +475,7 @@ int main()
table->setHeaders({ "name","age","seorc","home" }); table->setHeaders({ "name","age","seorc","home" });
table->setData({ table->setData({
{"zhangsan","20","99.99","wadsacafadsawd"}, {"zhangsan","20","99.99","wadsacafadsawd"},
{"lisi","20","99.99","wadsacafadssssssssssssssssssssssssssssssssssssssssssssssssssawd"}, {"lisi","20","99.99","wadsacafadsawd"},
{"wangwu","20","99.99","wadsacafadsawd"}, {"wangwu","20","99.99","wadsacafadsawd"},
{"zhaoliu","20","99.99","wadsacafadsawd"}, {"zhaoliu","20","99.99","wadsacafadsawd"},
{"1","20","99.99","wadsacafadsawd" }, {"1","20","99.99","wadsacafadsawd" },
@@ -725,108 +767,147 @@ char initData[33] = "00000000000000000000000000000000";//初始数据
bool gSigned = false; //是否为有符号数 bool gSigned = false; //是否为有符号数
int main() int main()
{ {
StellarX::SxLogger::Get().enableConsole(true); //StellarX::SxLogger::Get().enableConsole(true);
StellarX::SxLogger::Get().enableFile("log.txt",true,1024);
StellarX::SxLogger::Get().setMinLevel(StellarX::SxLogLevel::Debug); // Info/Debug/Trace 自己切 StellarX::SxLogger::Get().setMinLevel(StellarX::SxLogLevel::Debug); // Info/Debug/Trace 自己切
StellarX::SxLogger::Get().setLanguage(StellarX::SxLogLanguage::ZhCN); // ZhCN / EnUS StellarX::SxLogger::Get().setLanguage(StellarX::SxLogLanguage::ZhCN); // ZhCN / EnUS
Window mainWindow(700, 510, 1, RGB(255, 255, 255), "寄存器查看工具 V1.0——我在人间做废物 (同类工具定制:3150131407(Q / V))"); Window mainWindow(700, 510, 1, RGB(255, 255, 255), "寄存器查看工具 V1.0——我在人间做废物 (同类工具定制:3150131407(Q / V))");
//选择区控件 auto setStretchX = [](Control* control) {
auto selectionAreaLabel = std::make_unique<Label>(18, 0, "32位选择区"); control->setHorizontalAnchors(true, true);
selectionAreaLabel->setTextdisap(true); control->setHorizontalSizePolicy(StellarX::AxisSizePolicy::Stretch);
std::vector<std::unique_ptr<Label>>selectionAreaButtonLabel; control->setHorizontalAlignPolicy(StellarX::AxisAlignPolicy::Start);
std::vector<std::unique_ptr<Button>>selectionAreaButton; };
std::vector<Button*>selectionAreaButton_ptr; auto setStretchY = [](Control* control) {
auto selectionArea = std::make_unique <Canvas>(10, 10, 680, 150); control->setVerticalAnchors(true, true);
control->setVerticalSizePolicy(StellarX::AxisSizePolicy::Stretch);
control->setVerticalAlignPolicy(StellarX::AxisAlignPolicy::Start);
};
auto setLeftFixed = [](Control* control) {
control->setHorizontalAnchors(true, false);
control->setHorizontalSizePolicy(StellarX::AxisSizePolicy::FixedSize);
control->setHorizontalAlignPolicy(StellarX::AxisAlignPolicy::Start);
};
auto setRightFixed = [](Control* control) {
control->setHorizontalAnchors(false, true);
control->setHorizontalSizePolicy(StellarX::AxisSizePolicy::FixedSize);
control->setHorizontalAlignPolicy(StellarX::AxisAlignPolicy::End);
};
auto setCenterFixed = [](Control* control) {
control->setHorizontalAnchors(true, true);
control->setHorizontalSizePolicy(StellarX::AxisSizePolicy::FixedSize);
control->setHorizontalAlignPolicy(StellarX::AxisAlignPolicy::Center);
};
auto setBottomFixed = [](Control* control) {
control->setVerticalAnchors(false, true);
control->setVerticalSizePolicy(StellarX::AxisSizePolicy::FixedSize);
control->setVerticalAlignPolicy(StellarX::AxisAlignPolicy::End);
};
auto setTopFixed = [](Control* control) {
control->setVerticalAnchors(true, false);
control->setVerticalSizePolicy(StellarX::AxisSizePolicy::FixedSize);
control->setVerticalAlignPolicy(StellarX::AxisAlignPolicy::Start);
};
auto setProportionalX = [](Control* control) {
control->setHorizontalAnchors(true, true);
control->setHorizontalSizePolicy(StellarX::AxisSizePolicy::FixedSize);
control->setHorizontalAlignPolicy(StellarX::AxisAlignPolicy::Proportional);
};
auto styleTitleLabel = [](Label* label) {
label->setTextdisap(true);
};
auto styleActionButton = [&](Button* button) {
button->textStyle.color = RGB(226, 116, 152);
button->setButtonShape(StellarX::ControlShape::B_RECTANGLE);
};
auto styleInputBox = [&](TextBox* box, int maxChars) {
box->setMaxCharLen(maxChars);
box->textStyle.color = RGB(226, 116, 152);
box->setTextBoxBk(RGB(244, 234, 142));
box->setTextBoxshape(StellarX::ControlShape::B_RECTANGLE);
};
auto styleReadonlyBox = [&](TextBox* box, int maxChars) {
box->setMaxCharLen(maxChars);
box->textStyle.color = RGB(255, 69, 0);
box->setTextBoxBk(RGB(141, 141, 141));
box->setTextBoxshape(StellarX::ControlShape::B_RECTANGLE);
box->setMode(StellarX::TextBoxmode::READONLY_MODE);
};
//选择区控件
auto selectionArea = std::make_unique <Canvas>(10, 10, 680, 150);
selectionArea->setCanvasBkColor(blackColor); selectionArea->setCanvasBkColor(blackColor);
selectionArea->setShape(StellarX::ControlShape::B_ROUND_RECTANGLE); selectionArea->setShape(StellarX::ControlShape::B_ROUND_RECTANGLE);
setStretchX(selectionArea.get());
setTopFixed(selectionArea.get());
for (int y = 0; y < 2; y++) auto selectionAreaLabel = std::make_unique<Label>(18, 0, "32位选择区");
styleTitleLabel(selectionAreaLabel.get());
setLeftFixed(selectionAreaLabel.get());
auto topRow = std::make_unique<Canvas>(18, 24, 644, 54);
topRow->setCanvasBkColor(blackColor);
topRow->setShape(StellarX::ControlShape::B_RECTANGLE);
setStretchX(topRow.get());
setTopFixed(topRow.get());
auto bottomRow = std::make_unique<Canvas>(18, 88, 644, 54);
bottomRow->setCanvasBkColor(blackColor);
bottomRow->setShape(StellarX::ControlShape::B_RECTANGLE);
setStretchX(bottomRow.get());
setBottomFixed(bottomRow.get());
Canvas* topRowPtr = topRow.get();
Canvas* bottomRowPtr = bottomRow.get();
std::array<Button*, 32> selectionAreaButton_ptr{};
auto vecIndexFromBit = [](int bit) { return 31 - bit; };
auto makeBitCell = [&](Canvas* row, int visualIndex, int bit)
{ {
const int cellX = visualIndex * 38 + (visualIndex / 4) * 10;
auto cell = std::make_unique<Canvas>(cellX, 0, 32, 50);
cell->setCanvasBkColor(blackColor);
cell->setShape(StellarX::ControlShape::B_RECTANGLE);
setProportionalX(cell.get());
std::ostringstream os; std::ostringstream os;
for (int x = 0; x < 16; x++) os << std::setw(2) << std::setfill('0') << bit;
{ auto bitLabel = std::make_unique<Label>(bit >= 10 ? 8 : 12, 0, os.str(), RGB(208, 208, 208));
if (0 == y) bitLabel->setTextdisap(true);
{
selectionAreaButtonLabel.push_back(std::make_unique<Label>(x * 35 + 25 + 28 * (x / 4), 26, "", RGB(208, 208, 208)));
os << std::setw(2) << std::setfill('0') << 31 - x;
selectionAreaButtonLabel.back()->setText(os.str());
selectionAreaButtonLabel.back()->setTextdisap(true);
selectionAreaButton.push_back( auto bitButton = std::make_unique<Button>(3, 20, 26, 28, "0",
std::make_unique<Button>(x * 35 + 27 + 28 * (x / 4), 58, 25, 30, "0", blackColor, RGB(171, 196, 220), StellarX::ButtonMode::TOGGLE);
blackColor, RGB(171, 196, 220), StellarX::ButtonMode::TOGGLE)); styleActionButton(bitButton.get());
selectionAreaButton.back()->textStyle.color = RGB(226, 116, 152); selectionAreaButton_ptr[vecIndexFromBit(bit)] = bitButton.get();
selectionAreaButton.back()->setButtonShape(StellarX::ControlShape::B_RECTANGLE);
selectionAreaButton_ptr.push_back(selectionAreaButton.back().get()); cell->addControl(std::move(bitLabel));
int k = 32 - x - 1; cell->addControl(std::move(bitButton));
//选择区按钮被点击后在二进制0和1之间切换,并更新initData row->addControl(std::move(cell));
selectionAreaButton_ptr.back()->setOnToggleOnListener([k, btn = selectionAreaButton_ptr.back()]() };
{
btn->setButtonText("1"); for (int i = 0; i < 16; ++i)
initData[k] = '1'; makeBitCell(topRowPtr, i, 31 - i);
}); for (int i = 0; i < 16; ++i)
selectionAreaButton_ptr.back()->setOnToggleOffListener([k, btn = selectionAreaButton_ptr.back()]() makeBitCell(bottomRowPtr, i, 15 - i);
{
btn->setButtonText("0");
initData[k] = '0';
});
}
else
{
selectionAreaButtonLabel.push_back(std::make_unique<Label>(x * 35 + 25 + 28 * (x / 4), 90, "", RGB(208, 208, 208)));
os << std::setw(2) << std::setfill('0') << 15 - x;
selectionAreaButtonLabel.back()->setText(os.str());
selectionAreaButtonLabel.back()->setTextdisap(true);
selectionAreaButton.push_back(
std::make_unique<Button>(x * 35 + 27 + 28 * (x / 4), 120, 25, 30, "0",
blackColor, RGB(171, 196, 220), StellarX::ButtonMode::TOGGLE));
selectionAreaButton.back()->textStyle.color = RGB(226, 116, 152);
selectionAreaButton.back()->setButtonShape(StellarX::ControlShape::B_RECTANGLE);
selectionAreaButton_ptr.push_back(selectionAreaButton.back().get());
int k = 15 - x;
selectionAreaButton.back()->setOnToggleOnListener([k, btn = selectionAreaButton_ptr.back()]()
{
btn->setButtonText("1");
initData[k] = '1';
});
selectionAreaButton.back()->setOnToggleOffListener([k, btn = selectionAreaButton_ptr.back()]()
{
btn->setButtonText("0");
initData[k] = '0';
});
}
os.str("");
os.clear();
}
}
selectionAreaLabel->setAnchor(StellarX::Anchor::Right, StellarX::Anchor::Left);
selectionAreaLabel->setLayoutMode(StellarX::LayoutMode::AnchorToEdges);
selectionArea->addControl(std::move(selectionAreaLabel)); selectionArea->addControl(std::move(selectionAreaLabel));
for (auto& s : selectionAreaButton) selectionArea->addControl(std::move(topRow));
{ selectionArea->addControl(std::move(bottomRow));
s->setAnchor(StellarX::Anchor::Right, StellarX::Anchor::Left);
s->setLayoutMode(StellarX::LayoutMode::AnchorToEdges);
selectionArea->addControl(std::move(s));
}
for (auto& s : selectionAreaButtonLabel)
{
s->setAnchor(StellarX::Anchor::Right, StellarX::Anchor::Left);
s->setLayoutMode(StellarX::LayoutMode::AnchorToEdges);
selectionArea->addControl(std::move(s));
}
//功能区控件 //功能区控件
//功能区总容器
auto function = std::make_unique<Canvas>(10, 170, 680, 70); auto function = std::make_unique<Canvas>(10, 170, 680, 70);
function->setCanvasfillMode(StellarX::FillMode::Null); function->setCanvasfillMode(StellarX::FillMode::Null);
function->setShape(StellarX::ControlShape::B_ROUND_RECTANGLE); function->setShape(StellarX::ControlShape::B_ROUND_RECTANGLE);
function->setCanvasBkColor(blackColor); function->setCanvasBkColor(blackColor);
setStretchX(function.get());
setTopFixed(function.get());
auto bitInvert_que = std::make_unique<Canvas>(0, 0, 220, 70); auto bitInvert_que = std::make_unique<Canvas>(0, 0, 220, 70);
auto leftShift_que = std::make_unique<Canvas>(230, 0, 220, 70); auto leftShift_que = std::make_unique<Canvas>(230, 0, 220, 70);
auto rightShift_que = std::make_unique<Canvas>(460, 0, 220, 70); auto rightShift_que = std::make_unique<Canvas>(460, 0, 220, 70);
setLeftFixed(bitInvert_que.get());
setCenterFixed(leftShift_que.get());
setRightFixed(rightShift_que.get());
auto bitInvert = bitInvert_que.get(); auto bitInvert = bitInvert_que.get();
auto leftShift = leftShift_que.get(); auto leftShift = leftShift_que.get();
@@ -839,347 +920,340 @@ int main()
rightShift->setCanvasBkColor(blackColor); rightShift->setCanvasBkColor(blackColor);
rightShift->setShape(StellarX::ControlShape::B_ROUND_RECTANGLE); rightShift->setShape(StellarX::ControlShape::B_ROUND_RECTANGLE);
function->addControl(std::move(bitInvert_que));
function->addControl(std::move(leftShift_que));
function->addControl(std::move(rightShift_que));
auto bitInvertLabel = std::make_unique<Label>(13, -10, "位取反"); auto bitInvertLabel = std::make_unique<Label>(13, -10, "位取反");
bitInvertLabel->setTextdisap(true);
auto leftShiftLabel = std::make_unique<Label>(13, -10, "左移位"); auto leftShiftLabel = std::make_unique<Label>(13, -10, "左移位");
leftShiftLabel->setTextdisap(true);
auto rightShiftLabel = std::make_unique<Label>(13, -10, "右移位"); auto rightShiftLabel = std::make_unique<Label>(13, -10, "右移位");
rightShiftLabel->setTextdisap(true); styleTitleLabel(bitInvertLabel.get());
styleTitleLabel(leftShiftLabel.get());
styleTitleLabel(rightShiftLabel.get());
// 数值显示区
auto NumericalDisplayArea = std::make_unique<Canvas>(10, 255, 680, 70);
NumericalDisplayArea->setCanvasBkColor(blackColor);
NumericalDisplayArea->setShape(StellarX::ControlShape::B_ROUND_RECTANGLE);
setStretchX(NumericalDisplayArea.get());
setTopFixed(NumericalDisplayArea.get());
auto numericalTitle = std::make_unique<Label>(18, -10, "数值显示区");
styleTitleLabel(numericalTitle.get());
setLeftFixed(numericalTitle.get());
auto hexGroup = std::make_unique<Canvas>(18, 20, 300, 38);
hexGroup->setCanvasfillMode(StellarX::FillMode::Null);
hexGroup->setShape(StellarX::ControlShape::B_RECTANGLE);
setLeftFixed(hexGroup.get());
auto decGroup = std::make_unique<Canvas>(362, 20, 300, 38);
decGroup->setCanvasfillMode(StellarX::FillMode::Null);
decGroup->setShape(StellarX::ControlShape::B_RECTANGLE);
setRightFixed(decGroup.get());
auto hexLabel = std::make_unique<Label>(0, 8, "十六进制");
styleTitleLabel(hexLabel.get());
auto decLabel = std::make_unique<Label>(0, 8, "十进制");
styleTitleLabel(decLabel.get());
auto hexBox = std::make_unique<TextBox>(86, 4, 204, 30, "00000000");
auto decBox = std::make_unique<TextBox>(70, 4, 220, 30, "0");
styleReadonlyBox(hexBox.get(), 11);
styleReadonlyBox(decBox.get(), 11);
auto hex = hexBox.get();
auto dec = decBox.get();
hexGroup->addControl(std::move(hexLabel));
hexGroup->addControl(std::move(hexBox));
decGroup->addControl(std::move(decLabel));
decGroup->addControl(std::move(decBox));
NumericalDisplayArea->addControl(std::move(numericalTitle));
NumericalDisplayArea->addControl(std::move(hexGroup));
NumericalDisplayArea->addControl(std::move(decGroup));
// 二进制显示区
auto BinaryDisplayArea = std::make_unique<Canvas>(10, 335, 680, 110);
BinaryDisplayArea->setCanvasBkColor(blackColor);
BinaryDisplayArea->setShape(StellarX::ControlShape::B_ROUND_RECTANGLE);
setStretchX(BinaryDisplayArea.get());
BinaryDisplayArea->setVerticalAnchors(true, true);
BinaryDisplayArea->setVerticalSizePolicy(StellarX::AxisSizePolicy::Stretch);
BinaryDisplayArea->setVerticalAlignPolicy(StellarX::AxisAlignPolicy::Start);
auto binaryTitle = std::make_unique<Label>(18, -10, "二进制显示区");
styleTitleLabel(binaryTitle.get());
setLeftFixed(binaryTitle.get());
auto lastRow = std::make_unique<Canvas>(18, 20, 644, 30);
lastRow->setCanvasfillMode(StellarX::FillMode::Null);
lastRow->setShape(StellarX::ControlShape::B_RECTANGLE);
setStretchX(lastRow.get());
setTopFixed(lastRow.get());
auto thisRow = std::make_unique<Canvas>(18, 67, 644, 30);
thisRow->setCanvasfillMode(StellarX::FillMode::Null);
thisRow->setShape(StellarX::ControlShape::B_RECTANGLE);
setStretchX(thisRow.get());
setBottomFixed(thisRow.get());
auto lastLabel = std::make_unique<Label>(0, 6, "上次值");
auto thisLabel = std::make_unique<Label>(0, 6, "本次值");
styleTitleLabel(lastLabel.get());
styleTitleLabel(thisLabel.get());
auto lastBox = std::make_unique<TextBox>(80, 0, 564, 30, "0000_0000_0000_0000_0000_0000_0000_0000");
auto thisBox = std::make_unique<TextBox>(80, 0, 564, 30, "0000_0000_0000_0000_0000_0000_0000_0000");
styleReadonlyBox(lastBox.get(), 40);
styleReadonlyBox(thisBox.get(), 40);
lastBox->setHorizontalAnchors(true, true);
lastBox->setHorizontalSizePolicy(StellarX::AxisSizePolicy::Stretch);
lastBox->setHorizontalAlignPolicy(StellarX::AxisAlignPolicy::Start);
thisBox->setHorizontalAnchors(true, true);
thisBox->setHorizontalSizePolicy(StellarX::AxisSizePolicy::Stretch);
thisBox->setHorizontalAlignPolicy(StellarX::AxisAlignPolicy::Start);
auto Last = lastBox.get();
auto This = thisBox.get();
lastRow->addControl(std::move(lastLabel));
lastRow->addControl(std::move(lastBox));
thisRow->addControl(std::move(thisLabel));
thisRow->addControl(std::move(thisBox));
BinaryDisplayArea->addControl(std::move(binaryTitle));
BinaryDisplayArea->addControl(std::move(lastRow));
BinaryDisplayArea->addControl(std::move(thisRow));
// 配置区
auto configuration = std::make_unique<Canvas>(10, 455, 680, 40);
configuration->setCanvasBkColor(blackColor);
configuration->setShape(StellarX::ControlShape::B_ROUND_RECTANGLE);
setStretchX(configuration.get());
setBottomFixed(configuration.get());
auto configurationLabel = std::make_unique<Label>(20, -10, "配置区");
styleTitleLabel(configurationLabel.get());
setLeftFixed(configurationLabel.get());
auto clearButton = std::make_unique<Button>(420, 10, 90, 20, "一键置0",
blackColor, RGB(171, 196, 220));
auto fillButton = std::make_unique<Button>(530, 10, 90, 20, "一键置1",
blackColor, RGB(171, 196, 220));
styleActionButton(clearButton.get());
styleActionButton(fillButton.get());
setRightFixed(clearButton.get());
setRightFixed(fillButton.get());
auto signedToggle = std::make_unique<Button>(
300, 10, 90, 20, "无符号",
blackColor, RGB(171, 196, 220), StellarX::ButtonMode::TOGGLE);
styleActionButton(signedToggle.get());
setCenterFixed(signedToggle.get());
auto* signedTogglePtr = signedToggle.get();
// ====== 公用小工具====== // ====== 公用小工具======
auto clamp = [](int v, int lo, int hi) { return v < lo ? lo : (v > hi ? hi : v); }; auto clamp = [](int v, int lo, int hi) { return v < lo ? lo : (v > hi ? hi : v); };
auto toInt = [](const std::string& s, int def = 0) { auto toInt = [](const std::string& s, int def = 0) {
try { return std::stoi(s); } try { return std::stoi(s); }
catch (...) { return def; } catch (...) { return def; }
}; };
// bit号(31..0) -> selectionAreaButton下标(0..31)
auto vecIndexFromBit = [](int bit) { return 31 - bit; };
// 读取当前32位点击态
auto snapshotBits = [&]() { auto snapshotBits = [&]() {
std::array<bool, 32> a{}; std::array<bool, 32> bits{};
for (int b = 0; b < 32; ++b) for (int bit = 0; bit < 32; ++bit)
a[b] = selectionAreaButton_ptr[vecIndexFromBit(b)]->isClicked(); bits[bit] = selectionAreaButton_ptr[vecIndexFromBit(bit)]->isClicked();
return a; return bits;
}; };
// 应用目标态:仅当不同才 setButtonClick auto valueFromBits = [](const std::array<bool, 32>& bits) -> uint32_t {
auto applyBits = [&](const std::array<bool, 32>& a) { uint32_t value = 0;
for (int b = 0; b < 32; ++b) { for (int bit = 0; bit < 32; ++bit)
auto btn = selectionAreaButton_ptr[vecIndexFromBit(b)]; if (bits[bit]) value |= (1u << bit);
if (btn->isClicked() != a[b]) btn->setButtonClick(a[b]); return value;
};
auto binaryGroupedFromBits = [](const std::array<bool, 32>& bits) -> std::string {
std::string text;
text.reserve(39);
for (int bit = 31; bit >= 0; --bit) {
text.push_back(bits[bit] ? '1' : '0');
if (bit % 4 == 0 && bit != 0) text.push_back('_');
} }
}; return text;
};
//取反区控件 auto syncInitData = [&](const std::array<bool, 32>& bits) {
std::array<std::unique_ptr<Label>, 4> bitInvertFunctionLabel; for (int bit = 31; bit >= 0; --bit)
bitInvertFunctionLabel[0] = std::make_unique<Label>(30, 10, "低位"); initData[31 - bit] = bits[bit] ? '1' : '0';
bitInvertFunctionLabel[1] = std::make_unique<Label>(90, 10, "高位"); initData[32] = '\0';
bitInvertFunctionLabel[2] = std::make_unique<Label>(15, 38, ""); };
bitInvertFunctionLabel[3] = std::make_unique<Label>(75, 38, "");
std::array<std::unique_ptr<TextBox>, 2> bitInvertFunctionTextBox; auto refreshDisplaysWithBits = [&](const std::array<bool, 32>& bits, bool pushHistory) {
bitInvertFunctionTextBox[0] = std::make_unique<TextBox>(35, 35, 35, 30, "0"); const uint32_t value = valueFromBits(bits);
bitInvertFunctionTextBox[1] = std::make_unique<TextBox>(95, 35, 35, 30, "0"); const int32_t signedValue = static_cast<int32_t>(value);
auto invL = bitInvertFunctionTextBox[0].get(); char hexbuf[16];
auto invH = bitInvertFunctionTextBox[1].get(); std::snprintf(hexbuf, sizeof(hexbuf), "%08X", value);
auto bitInvertFunctionButton = std::make_unique<Button>(135, 35, 80, 30, "位取反",
if (pushHistory)
Last->setText(This->getText());
hex->setText(hexbuf);
dec->setText(gSigned ? std::to_string(signedValue) : std::to_string(value));
This->setText(binaryGroupedFromBits(bits));
syncInitData(bits);
};
bool batchApplyingBits = false;
auto applyBits = [&](const std::array<bool, 32>& bits, bool pushHistory) {
batchApplyingBits = true;
for (int bit = 0; bit < 32; ++bit) {
auto* button = selectionAreaButton_ptr[vecIndexFromBit(bit)];
if (button->isClicked() != bits[bit])
button->setButtonClick(bits[bit]);
button->setButtonText(bits[bit] ? "1" : "0");
}
batchApplyingBits = false;
refreshDisplaysWithBits(bits, pushHistory);
};
// 取反区控件
auto invStartLabel = std::make_unique<Label>(18, 12, "");
auto invEndLabel = std::make_unique<Label>(82, 12, "");
styleTitleLabel(invStartLabel.get());
styleTitleLabel(invEndLabel.get());
auto invStartBox = std::make_unique<TextBox>(34, 28, 34, 28, "0");
auto invEndBox = std::make_unique<TextBox>(98, 28, 34, 28, "31");
styleInputBox(invStartBox.get(), 3);
styleInputBox(invEndBox.get(), 3);
auto invL = invStartBox.get();
auto invH = invEndBox.get();
auto bitInvertFunctionButton = std::make_unique<Button>(140, 28, 64, 28, "位取反",
blackColor, RGB(171, 196, 220)); blackColor, RGB(171, 196, 220));
bitInvertFunctionButton->textStyle.color = RGB(226, 116, 152); styleActionButton(bitInvertFunctionButton.get());
bitInvertFunctionButton->setButtonShape(StellarX::ControlShape::B_RECTANGLE);
auto bitInvertFunctionButton_ptr = bitInvertFunctionButton.get(); auto bitInvertFunctionButton_ptr = bitInvertFunctionButton.get();
bitInvert->addControl(std::move(bitInvertFunctionButton));
bitInvert->addControl(std::move(bitInvertLabel)); bitInvert->addControl(std::move(bitInvertLabel));
for (auto& b : bitInvertFunctionTextBox) bitInvert->addControl(std::move(invStartLabel));
{ bitInvert->addControl(std::move(invEndLabel));
b->setMaxCharLen(3); bitInvert->addControl(std::move(invStartBox));
b->textStyle.color = RGB(226, 116, 152); bitInvert->addControl(std::move(invEndBox));
b->setTextBoxBk(RGB(244, 234, 142)); bitInvert->addControl(std::move(bitInvertFunctionButton));
b->setTextBoxshape(StellarX::ControlShape::B_RECTANGLE);
bitInvert->addControl(std::move(b));
}
for (auto& b : bitInvertFunctionLabel)
{
b->setTextdisap(true);
bitInvert->addControl(std::move(b));
}
//左移控件
auto leftShiftFunctionLabel = std::make_unique<Label>(198, 30, "");
leftShiftFunctionLabel->setTextdisap(true);
auto leftShiftFunctionTextBox = std::make_unique<TextBox>(90, 30, 100, 30, "0"); // 左移控件
leftShiftFunctionTextBox->setMaxCharLen(3); auto leftShiftFunctionLabel = std::make_unique<Label>(178, 30, "");
leftShiftFunctionTextBox->textStyle.color = RGB(226, 116, 152); styleTitleLabel(leftShiftFunctionLabel.get());
leftShiftFunctionTextBox->setTextBoxBk(RGB(244, 234, 142));
leftShiftFunctionTextBox->setTextBoxshape(StellarX::ControlShape::B_RECTANGLE); auto leftShiftFunctionTextBox = std::make_unique<TextBox>(82, 28, 86, 28, "0");
styleInputBox(leftShiftFunctionTextBox.get(), 3);
auto shlBox = leftShiftFunctionTextBox.get(); auto shlBox = leftShiftFunctionTextBox.get();
auto leftShiftFunctionButton = std::make_unique<Button>(15, 30, 60, 30, "左移",
auto leftShiftFunctionButton = std::make_unique<Button>(16, 28, 56, 28, "左移",
blackColor, RGB(171, 196, 220)); blackColor, RGB(171, 196, 220));
leftShiftFunctionButton->textStyle.color = RGB(226, 116, 152); styleActionButton(leftShiftFunctionButton.get());
leftShiftFunctionButton->setButtonShape(StellarX::ControlShape::B_RECTANGLE);
auto leftShiftFunctionButton_ptr = leftShiftFunctionButton.get(); auto leftShiftFunctionButton_ptr = leftShiftFunctionButton.get();
leftShift->addControl(std::move(leftShiftLabel));
leftShift->addControl(std::move(leftShiftFunctionButton)); leftShift->addControl(std::move(leftShiftFunctionButton));
leftShift->addControl(std::move(leftShiftFunctionTextBox)); leftShift->addControl(std::move(leftShiftFunctionTextBox));
leftShift->addControl(std::move(leftShiftLabel));
leftShift->addControl(std::move(leftShiftFunctionLabel)); leftShift->addControl(std::move(leftShiftFunctionLabel));
//右移控件 // 右移控件
auto rightShiftFunctionLabel = std::make_unique<Label>(198, 30, ""); auto rightShiftFunctionLabel = std::make_unique<Label>(178, 30, "");
rightShiftFunctionLabel->setTextdisap(true); styleTitleLabel(rightShiftFunctionLabel.get());
auto rightShiftFunctionTextBox = std::make_unique<TextBox>(90, 30, 100, 30, "0");
rightShiftFunctionTextBox->setMaxCharLen(3); auto rightShiftFunctionTextBox = std::make_unique<TextBox>(82, 28, 86, 28, "0");
rightShiftFunctionTextBox->textStyle.color = RGB(226, 116, 152); styleInputBox(rightShiftFunctionTextBox.get(), 3);
rightShiftFunctionTextBox->setTextBoxBk(RGB(244, 234, 142));
rightShiftFunctionTextBox->setTextBoxshape(StellarX::ControlShape::B_RECTANGLE);
auto shrBox = rightShiftFunctionTextBox.get(); auto shrBox = rightShiftFunctionTextBox.get();
auto rightShiftFunctionButton = std::make_unique<Button>(15, 30, 60, 30, "右移", auto rightShiftFunctionButton = std::make_unique<Button>(16, 28, 56, 28, "右移",
blackColor, RGB(171, 196, 220)); blackColor, RGB(171, 196, 220));
rightShiftFunctionButton->textStyle.color = RGB(226, 116, 152); styleActionButton(rightShiftFunctionButton.get());
rightShiftFunctionButton->setButtonShape(StellarX::ControlShape::B_RECTANGLE);
auto rightShiftFunctionButton_ptr = rightShiftFunctionButton.get(); auto rightShiftFunctionButton_ptr = rightShiftFunctionButton.get();
rightShift->addControl(std::move(rightShiftFunctionButton));
rightShift->addControl(std::move(rightShiftFunctionTextBox));
rightShift->addControl(std::move(rightShiftLabel)); rightShift->addControl(std::move(rightShiftLabel));
rightShift->addControl(std::move(rightShiftFunctionButton));
rightShift->addControl(std::move(rightShiftFunctionTextBox));
rightShift->addControl(std::move(rightShiftFunctionLabel)); rightShift->addControl(std::move(rightShiftFunctionLabel));
//显示区控件 // 位按钮与显示区联动
//数值显示 for (int bit = 0; bit < 32; ++bit) {
auto NumericalDisplayArea = std::make_unique<Canvas>(10, 255, 680, 70); auto* button = selectionAreaButton_ptr[vecIndexFromBit(bit)];
NumericalDisplayArea->setCanvasBkColor(blackColor); button->setOnToggleOnListener([&, bit]() {
NumericalDisplayArea->setShape(StellarX::ControlShape::B_ROUND_RECTANGLE); selectionAreaButton_ptr[vecIndexFromBit(bit)]->setButtonText("1");
if (!batchApplyingBits)
std::array<std::unique_ptr<Label>, 3> NumericalDisplayAreaLabel; refreshDisplaysWithBits(snapshotBits(), true);
NumericalDisplayAreaLabel[0] = std::make_unique<Label>(18, -10, "数值显示区"); });
NumericalDisplayAreaLabel[1] = std::make_unique<Label>(20, 25, "十六进制"); button->setOnToggleOffListener([&, bit]() {
NumericalDisplayAreaLabel[2] = std::make_unique<Label>(330, 25, "十进制"); selectionAreaButton_ptr[vecIndexFromBit(bit)]->setButtonText("0");
if (!batchApplyingBits)
std::array<std::unique_ptr<TextBox>, 2> NumericalDisplayAreaTextBox; refreshDisplaysWithBits(snapshotBits(), true);
NumericalDisplayAreaTextBox[0] = std::make_unique<TextBox>(110, 25, 200, 30, "0"); });
NumericalDisplayAreaTextBox[1] = std::make_unique<TextBox>(400, 25, 200, 30, "0");
auto hex = NumericalDisplayAreaTextBox[0].get();
auto dec = NumericalDisplayAreaTextBox[1].get();
for (auto& b : NumericalDisplayAreaLabel)
{
b->setTextdisap(true);
NumericalDisplayArea->addControl(std::move(b));
}
for (auto& b : NumericalDisplayAreaTextBox)
{
b->setMaxCharLen(11);
b->textStyle.color = RGB(255, 69, 0);
b->setTextBoxBk(RGB(141, 141, 141));
b->setTextBoxshape(StellarX::ControlShape::B_RECTANGLE);
b->setMode(StellarX::TextBoxmode::READONLY_MODE);
NumericalDisplayArea->addControl(std::move(b));
}
//二进制显示
auto BinaryDisplayArea = std::make_unique<Canvas>(10, 335, 680, 110);
BinaryDisplayArea->setCanvasBkColor(blackColor);
BinaryDisplayArea->setShape(StellarX::ControlShape::B_ROUND_RECTANGLE);
std::array<std::unique_ptr<Label>, 3> BinaryDisplayAreaLabel;
BinaryDisplayAreaLabel[0] = std::make_unique<Label>(18, -10, "二进制显示区");
BinaryDisplayAreaLabel[1] = std::make_unique<Label>(35, 20, "上次值");
BinaryDisplayAreaLabel[2] = std::make_unique<Label>(35, 67, "本次值");
std::array<std::unique_ptr<TextBox>, 2> BinaryDisplayAreaTextBox;
BinaryDisplayAreaTextBox[0] = std::make_unique<TextBox>(110, 20, 520, 30, "0000_0000_0000_0000_0000_0000_0000_0000");
BinaryDisplayAreaTextBox[1] = std::make_unique<TextBox>(110, 67, 520, 30, "0000_0000_0000_0000_0000_0000_0000_0000");
auto Last = BinaryDisplayAreaTextBox[0].get();
auto This = BinaryDisplayAreaTextBox[1].get();
for (auto& b : BinaryDisplayAreaLabel)
{
b->setTextdisap(true);
BinaryDisplayArea->addControl(std::move(b));
}
for (auto& b : BinaryDisplayAreaTextBox)
{
b->setMaxCharLen(40);
b->textStyle.color = RGB(255, 69, 0);
b->setTextBoxBk(RGB(141, 141, 141));
b->setTextBoxshape(StellarX::ControlShape::B_RECTANGLE);
b->setMode(StellarX::TextBoxmode::READONLY_MODE);
BinaryDisplayArea->addControl(std::move(b));
} }
// 由位图 bits 生成数值 bitInvertFunctionButton_ptr->setOnClickListener([&]() {
auto valueFromBits = [](const std::array<bool, 32>& bits) -> uint32_t { int low = clamp(toInt(invL->getText(), 0), 0, 31);
uint32_t v = 0; int high = clamp(toInt(invH->getText(), 31), 0, 31);
for (int b = 0; b < 32; ++b) if (bits[b]) v |= (1u << b); if (low > high) std::swap(low, high);
return v;
};
// 由位图 bits 生成 "0000_0000_..._0000"MSB→LSB31..0 auto bits = snapshotBits();
auto binaryGroupedFromBits = [](const std::array<bool, 32>& bits) -> std::string { for (int bit = low; bit <= high; ++bit)
std::string s; s.reserve(39); bits[bit] = !bits[bit];
for (int b = 31; b >= 0; --b) { applyBits(bits, true);
s.push_back(bits[b] ? '1' : '0'); });
if (b % 4 == 0 && b != 0) s.push_back('_');
}
return s;
};
// 用“目标位图 bits”刷新显示区 leftShiftFunctionButton_ptr->setOnClickListener([&]() {
auto refreshDisplaysWithBits = [&](const std::string& prevThis, int count = clamp(toInt(shlBox->getText(), 0), 0, 31);
const std::array<bool, 32>& bits, auto bits = snapshotBits();
TextBox* hex, TextBox* dec, TextBox* Last, TextBox* This) std::array<bool, 32> next{};
{ for (int bit = 31; bit >= 0; --bit)
const uint32_t val = valueFromBits(bits); next[bit] = (bit >= count) ? bits[bit - count] : false;
const int32_t s = static_cast<int32_t>(val); applyBits(next, true);
char hexbuf[16]; });
std::snprintf(hexbuf, sizeof(hexbuf), "%08X", val);
hex->setText(hexbuf); // HEX(大写8位) rightShiftFunctionButton_ptr->setOnClickListener([&]() {
dec->setText(gSigned ? std::to_string(s) : std::to_string(val)); // DEC int count = clamp(toInt(shrBox->getText(), 0), 0, 31);
Last->setText(prevThis); // 上次值 ← 刷新前的本次值 auto bits = snapshotBits();
This->setText(binaryGroupedFromBits(bits)); // 本次值 ← 由“目标位图”生成 std::array<bool, 32> next{};
}; for (int bit = 0; bit < 32; ++bit)
next[bit] = (bit + count <= 31) ? bits[bit + count] : false;
applyBits(next, true);
});
bitInvertFunctionButton_ptr->setOnClickListener([=, &snapshotBits, &applyBits, &refreshDisplaysWithBits]() { clearButton->setOnClickListener([&]() {
const std::string prevThis = This->getText(); std::array<bool, 32> bits{};
applyBits(bits, true);
});
int L = clamp(toInt(invL->getText(), 0), 0, 31); fillButton->setOnClickListener([&]() {
int H = clamp(toInt(invH->getText(), 0), 0, 31); std::array<bool, 32> bits{};
if (L > H) std::swap(L, H); bits.fill(true);
applyBits(bits, true);
auto cur = snapshotBits(); });
for (int b = L; b <= H; ++b) cur[b] = !cur[b];
applyBits(cur); // 只改按钮点击态(触发位按钮自回调)
refreshDisplaysWithBits(prevThis, cur, hex, dec, Last, This);
});
leftShiftFunctionButton_ptr->setOnClickListener([=, &snapshotBits, &applyBits, &refreshDisplaysWithBits]() {
const std::string prevThis = This->getText();
int n = clamp(toInt(shlBox->getText(), 0), 0, 31);
auto cur = snapshotBits();
std::array<bool, 32> nxt{}; // 默认全 0
// 逻辑左移:高位丢弃、低位补 0
for (int b = 31; b >= 0; --b) nxt[b] = (b >= n) ? cur[b - n] : false;
applyBits(nxt);
refreshDisplaysWithBits(prevThis, nxt, hex, dec, Last, This);
});
rightShiftFunctionButton_ptr->setOnClickListener([=, &snapshotBits, &applyBits, &refreshDisplaysWithBits]() {
const std::string prevThis = This->getText();
int n = clamp(toInt(shrBox->getText(), 0), 0, 31);
auto cur = snapshotBits();
std::array<bool, 32> nxt{};
// 逻辑右移:低位丢弃、高位补 0
for (int b = 0; b < 32; ++b) nxt[b] = (b + n <= 31) ? cur[b + n] : false;
applyBits(nxt);
refreshDisplaysWithBits(prevThis, nxt, hex, dec, Last, This);
});
//配置区控件clearrectangle(10, 440, 690, 490);
auto configuration = std::make_unique<Canvas>(10, 455, 680, 40);
configuration->setCanvasBkColor(blackColor);
configuration->setShape(StellarX::ControlShape::B_ROUND_RECTANGLE);
auto configurationLabel = std::make_unique<Label>(20, -10, "配置区");
configurationLabel->setTextdisap(true);
std::array<std::unique_ptr<Button>, 2> configurationButton;
configurationButton[0] = std::make_unique<Button>(420, 10, 90, 20, "一键置0",
blackColor, RGB(171, 196, 220));
configurationButton[0]->textStyle.color = RGB(226, 116, 152);
configurationButton[0]->setButtonShape(StellarX::ControlShape::B_RECTANGLE);
configurationButton[1] = std::make_unique<Button>(530, 10, 90, 20, "一键置1",
blackColor, RGB(171, 196, 220));
configurationButton[1]->textStyle.color = RGB(226, 116, 152);
configurationButton[1]->setButtonShape(StellarX::ControlShape::B_RECTANGLE);
configurationButton[0]->setOnClickListener(
[&]() {
for (auto& s : selectionAreaButton_ptr)
if (s->isClicked()) s->setButtonClick(false);
// 刷新显示:prevThis 用当前 This 文本
const std::string prevThis = This->getText();
auto cur = snapshotBits();
{
char hexbuf[16];
uint32_t u = 0; for (int b = 0; b < 32; ++b) if (cur[b]) u |= (1u << b);
int32_t s = static_cast<int32_t>(u);
std::snprintf(hexbuf, sizeof(hexbuf), "%08X", u);
hex->setText(hexbuf);
dec->setText(gSigned ? std::to_string(s) : std::to_string(u));
Last->setText(prevThis);
This->setText(binaryGroupedFromBits(cur));
}
});
configurationButton[1]->setOnClickListener(
[&]() {
for (auto& s : selectionAreaButton_ptr)
if (!s->isClicked()) s->setButtonClick(true);
const std::string prevThis = This->getText();
auto cur = snapshotBits();
char hexbuf[16];
uint32_t u = 0; for (int b = 0; b < 32; ++b) if (cur[b]) u |= (1u << b);
int32_t s = static_cast<int32_t>(u);
std::snprintf(hexbuf, sizeof(hexbuf), "%08X", u);
hex->setText(hexbuf);
dec->setText(gSigned ? std::to_string(s) : std::to_string(u));
Last->setText(prevThis);
This->setText(binaryGroupedFromBits(cur));
});
auto signedToggle = std::make_unique<Button>(
330, 10, 80, 20, "无符号",
blackColor, RGB(171, 196, 220), StellarX::ButtonMode::TOGGLE);
signedToggle->textStyle.color = RGB(226, 116, 152);
signedToggle->setButtonShape(StellarX::ControlShape::B_RECTANGLE);
auto* signedTogglePtr = signedToggle.get();
signedTogglePtr->setOnToggleOnListener([&]() { signedTogglePtr->setOnToggleOnListener([&]() {
gSigned = true; gSigned = true;
signedTogglePtr->setButtonText("有符号"); signedTogglePtr->setButtonText("有符号");
StellarX::MessageBox::showModal(mainWindow, "有符号模式下,\n最高位为符号位,\n其余位为数值位。", "有符号模式"); StellarX::MessageBox::showModal(mainWindow, "有符号模式下,\n最高位为符号位,\n其余位为数值位。", "有符号模式");
// 立即刷新十进制显示:用当前位图算出新值,仅改 dec refreshDisplaysWithBits(snapshotBits(), false);
auto cur = snapshotBits(); });
const uint32_t u = [&] { uint32_t v = 0; for (int b = 0; b < 32; ++b) if (cur[b]) v |= (1u << b); return v; }();
const int32_t s = static_cast<int32_t>(u);
dec->setText(std::to_string(s));
});
signedTogglePtr->setOnToggleOffListener([&]() { signedTogglePtr->setOnToggleOffListener([&]() {
gSigned = false; gSigned = false;
signedTogglePtr->setButtonText("无符号"); signedTogglePtr->setButtonText("无符号");
StellarX::MessageBox::showAsync(mainWindow, "无符号模式下,\n所有位均为数值位。", "无符号模式"); StellarX::MessageBox::showAsync(mainWindow, "无符号模式下,\n所有位均为数值位。", "无符号模式");
auto cur = snapshotBits(); refreshDisplaysWithBits(snapshotBits(), false);
const uint32_t u = [&] { uint32_t v = 0; for (int b = 0; b < 32; ++b) if (cur[b]) v |= (1u << b); return v; }(); });
dec->setText(std::to_string(u));
});
signedTogglePtr->enableTooltip(true); signedTogglePtr->enableTooltip(true);
signedTogglePtr->setTooltipTextsForToggle("切换无符号模式", "切换有符号模式"); signedTogglePtr->setTooltipTextsForToggle("切换无符号模式", "切换有符号模式");
configuration->addControl(std::move(configurationButton[0])); function->addControl(std::move(bitInvert_que));
configuration->addControl(std::move(configurationButton[1])); function->addControl(std::move(leftShift_que));
configuration->addControl(std::move(signedToggle)); function->addControl(std::move(rightShift_que));
configuration->addControl(std::move(configurationLabel));
selectionArea->setAnchor(StellarX::Anchor::Right, StellarX::Anchor::Left); configuration->addControl(std::move(configurationLabel));
selectionArea->setLayoutMode(StellarX::LayoutMode::AnchorToEdges); configuration->addControl(std::move(signedToggle));
configuration->addControl(std::move(clearButton));
configuration->addControl(std::move(fillButton));
refreshDisplaysWithBits(snapshotBits(), false);
mainWindow.addControl(std::move(selectionArea)); mainWindow.addControl(std::move(selectionArea));
mainWindow.addControl(std::move(function)); mainWindow.addControl(std::move(function));
@@ -0,0 +1,68 @@
# BUG-20260410-0004
> 适用场景:记录问题本身,不展开完整修复方案。修复内容写入对应的 Fix 文档。
## 基本信息
- ID: `BUG-20260410-0004`
- 标题: 按钮 Tooltip 显示后,鼠标移出按钮区域提示框不消失
- 状态:已修复
- 严重性:S3
- 优先级:P1
- 模块: Button / Tooltip / 托管重绘
- 版本 / 分支: 当前工作区 / 下一版本开发中
- 环境: Windows + EasyX,本地 GUI Demo
- 发现人: 用户
- 关联 Fix ID: `Fix-BUG-20260410-0004`
## 问题描述
- 现象:
- 按钮启用 Tooltip 后,提示框成功显示。
- 鼠标离开按钮区域后,Tooltip 仍残留在屏幕上,不会及时消失。
- 影响范围:
- 所有启用了 Tooltip 的 `Button`
- 典型场景包括 `KEY == 4` 的综合回归用例
- 期望结果:
- 鼠标离开按钮后,Tooltip 应立即回贴并消失,不留下残影
- 实际结果:
- Tooltip 逻辑状态已切换为隐藏,但屏幕上的提示框区域未被及时擦除
## 复现信息
- 前置条件:
- 使用启用 Tooltip 的按钮
- 复现步骤:
1. 运行带 Tooltip 按钮的 Demo
2. 将鼠标悬停在按钮上,等待 Tooltip 显示
3. 将鼠标移出按钮区域,观察 Tooltip 是否立即消失
- 复现概率:高概率
- 最小复现 Demo:
- `z-testDome.cpp``KEY == 4`
- 证据:现象观察 + 代码链路分析
## 初步分析
- 疑似位置:
- [`Button.cpp`](D:/programming/imGUI-easyX/imGui-easyX/Button.cpp)
- `Button::hideTooltip()`
- 触发条件:
- Tooltip 已经显示
- 鼠标离开按钮,进入 Tooltip 隐藏分支
- 相关线索:
- Tooltip 不是独立控件树成员,而是 `Button::draw()` 末尾补画的内置浮层
- 隐藏时若只作废快照而不回贴 Tooltip 自己的背景,按钮本体重绘并不会覆盖 Tooltip 占用区域
- 最近相关改动:
- 事件改状态 / Window 托管重绘收口
- 布局系统第一阶段重构
## 跟踪信息
- 首次发现时间: 2026-04-10
- 最后更新时间: 2026-04-10
- 修复版本: 待定
- 验证版本: 当前工作区
- 备注:
- 本文档仅记录问题本身,具体修复方案见关联 Fix 文档
@@ -0,0 +1,73 @@
# BUG-20260415-0005
> 适用场景:记录问题本身,不展开完整修复方案。修复内容写入对应的 Fix 文档。
## 基本信息
- ID: BUG-20260415-0005
- 标题: 局部重绘未补画上层兄弟导致遮挡错误
- 状态:已修复
- 严重性:S2
- 优先级:P1
- 模块: Window / Canvas / TabControl 局部重绘与合成
- 版本 / 分支: 当前工作区
- 环境: Windows + EasyX
- 发现人: 用户回归测试
- 关联 Fix ID[可选]
- `Fix-BUG-20260415-0005`
## 问题描述
- 现象:
- 下层控件发生局部重绘后,位于其上方且与 coverage 相交的兄弟控件没有被补画回来。
- 表现为上层 `Canvas / Dialog / 页 / 页签按钮` 被“切掉”或被下层重新盖住。
- 影响范围:
- 顶层普通控件之间
- `Canvas` 直接子控件之间
- `TabControl` 页签按钮与页面之间
- 期望结果:
- 任意局部重绘提交后,父容器应按实际绘制顺序把 coverage 上方相交的兄弟重新合成回来。
- 实际结果:
- 仅 dirty root 或 dirty child 被重画,上层兄弟未恢复。
## 复现信息
- 前置条件:[可选]
- 使用 `KEY5` 测试场景
- 复现步骤:
1. 进入 `KEY5`
2. 触发下层控件局部重绘,例如 `Table` 翻页或相交子控件 hover / click
3. 观察上层相交兄弟区域
- 复现概率:高概率
- 最小复现 Demo[可选]
- `KEY5``Table` 与顶层粉色浮层重叠场景
- 同父 `Canvas` 兄弟相交场景
- 证据:截图 / 日志 / 调用栈 / 录屏 / 断点观察
- 断点观察显示 dirty root 已提交,但上层 sibling 未重新执行 draw
## 初步分析
- 疑似位置:
- [`Window.cpp`](D:/programming/imGUI-easyX/imGui-easyX/Window.cpp)
- [`Canvas.cpp`](D:/programming/imGUI-easyX/imGui-easyX/Canvas.cpp)
- [`TabControl.cpp`](D:/programming/imGUI-easyX/imGui-easyX/TabControl.cpp)
- 触发条件:
- 局部重绘提交写入了下层像素,但没有做 overlay 兄弟补画
- 相关线索:
- 该问题不依赖“是否回贴背景快照”,直接重绘同样会破坏上层合成结果
- `Dialog` 之前之所以需要补画,本质上是同一类问题
- 最近相关改动:[可选]
- 第二阶段开始引入更严格的局部重绘与 overlay 收口后,问题被系统性暴露
## 跟踪信息
- 首次发现时间: 2026-04-15
- 最后更新时间: 2026-04-15
- 修复版本:[可选]
- 当前工作区
- 验证版本:[可选]
- 当前工作区
- 备注:[可选]
- 后续仍需继续关注 `Dialog` 旧 synthetic move 机制与新模型的统一。
@@ -0,0 +1,53 @@
# BUG-20260415-0006
> 适用场景:记录问题本身,不展开完整修复方案。修复内容写入对应的 Fix 文档。
## 基本信息
- ID: BUG-20260415-0006
- 标题: 托管局部重绘未正确提交脏子树导致嵌套 Canvas 按钮状态不刷新
- 状态:已修复
- 严重性:S2
- 优先级:P1
- 模块: 重绘 / 托管局部提交 / Canvas 嵌套
- 版本 / 分支: 当前工作区
- 环境: Windows / EasyX / KEY5 回归场景
- 发现人: 用户
- 关联 Fix IDFix-BUG-20260415-0006
## 问题描述
- 现象: KEY5 中第二层、第三层 Canvas 内按钮可正常触发 hover / click 回调,但视觉状态不刷新。
- 影响范围: 嵌套 Canvas 内的深层按钮、页内嵌套容器按钮、所有依赖托管局部重绘的脏后代场景。
- 期望结果: 只要深层按钮状态变化,最外层托管 root 应能正确提交对应脏子树并把视觉结果画出来。
- 实际结果: 日志显示事件和回调已执行,但屏幕上不出现 hover / press / release 反馈,直到触发更大范围重绘才会补出来。
## 复现信息
- 前置条件: 使用 KEY5 场景,启用三层 Canvas 嵌套回归区。
- 复现步骤:
1. 打开 KEY5。
2. 将鼠标移到第二层或第三层 Canvas 内的按钮上,观察 hover。
3. 点击按钮并观察按下、松开状态。
- 复现概率:必现
- 最小复现 DemoKEY5 A 区三层 Canvas 嵌套
- 证据:日志显示按钮回调已执行,但按钮视觉状态不刷新
## 初步分析
- 疑似位置: Canvas 托管局部重绘提交链、Window 托管重绘登记 coverage
- 触发条件: root 的直接子控件本身不 dirty,但其下存在 dirty descendant
- 相关线索:
- 托管局部提交只认直接 dirty child
- 深层按钮状态变化未提升到 root 下直接脏分支
- 最近相关改动:第二阶段布局与托管重绘收口
## 跟踪信息
- 首次发现时间: 2026-04-15
- 最后更新时间: 2026-04-15
- 修复版本:当前工作区
- 验证版本:KEY5 编译级验证通过,待用户手测
- 备注:该问题已按机制修复,不再使用临时整容器重绘补丁
@@ -0,0 +1,53 @@
# BUG-20260415-0007
> 适用场景:记录问题本身,不展开完整修复方案。修复内容写入对应的 Fix 文档。
## 基本信息
- ID: BUG-20260415-0007
- 标题: 实际绘制 coverage 低估导致 Tooltip 与 overlay 补画漏算
- 状态:已修复
- 严重性:S2
- 优先级:P1
- 模块: Tooltip / overlay 重组 / 托管 coverage
- 版本 / 分支: 当前工作区
- 环境: Windows / EasyX / KEY5 回归场景
- 发现人: 用户
- 关联 Fix IDFix-BUG-20260415-0007
## 问题描述
- 现象: C 米区容器内按钮 Tooltip 会画到 D 橙区上层;类似问题也可能出现在带附加绘制区域的其他控件上。
- 影响范围: Tooltip、页签按钮 Tooltip、内部按钮与浮层、overlay 补画依赖 coverage 的所有链路。
- 期望结果: overlay 补画应按“实际写到屏幕上的区域”判断,而不是只按控件本体 bounds。
- 实际结果: 按钮本体未与上层控件相交时,Tooltip 超出部分不会触发 overlay 补画,导致 Tooltip 压到不应被压住的上层控件之上。
## 复现信息
- 前置条件: 使用 KEY5,保证 C 米区按钮 Tooltip 会伸进 D 橙区。
- 复现步骤:
1. 打开 KEY5。
2. 将鼠标移到 C 米区容器内按钮上,触发 Tooltip。
3. 观察 Tooltip 与 D 橙区的遮挡关系。
- 复现概率:必现
- 最小复现 DemoKEY5 C / D 交界区
- 证据:Tooltip 可见区域超出按钮 bounds,但 overlay 补画未跟上
## 初步分析
- 疑似位置: Button Tooltip coverage、Window/Canvas/TabControl 的 coverage 计算链
- 触发条件: 控件本体 bounds 未与 overlay 相交,但附加绘制区域(例如 Tooltip)已伸入 overlay 区
- 相关线索:
- 之前的 coverage 默认等于 `getBoundsRect()`
- `Button` 的 Tooltip 由 `tipLabel` 额外绘制,不属于按钮本体矩形
- 最近相关改动:顶层 overlay 传递式补画机制已补齐,但 coverage 仍按本体矩形计算
## 跟踪信息
- 首次发现时间: 2026-04-15
- 最后更新时间: 2026-04-15
- 修复版本:当前工作区
- 验证版本:KEY5 编译级验证通过,待用户手测
- 备注:Tooltip 智能选位明确后置,本次只修根因链路
@@ -0,0 +1,68 @@
# BUG-20260415-0008
> 适用场景:记录问题本身,不展开完整修复方案。修复内容写入对应的 Fix 文档。
## 基本信息
- ID: BUG-20260415-0008
- 标题: TabControl 页签层级与重复激活链路导致 Tooltip 和残影异常
- 状态:已修复
- 严重性:S2
- 优先级:P1
- 模块: TabControl / 页签绘制 / 外部激活页签
- 版本 / 分支: 当前工作区
- 环境: Windows / EasyX / KEY1 / KEY5
- 发现人: 用户
- 关联 Fix IDFix-BUG-20260415-0008
## 问题描述
- 现象:
- 有任意页签打开时,页签按钮 Tooltip 无法正常显示,只有全部页签关闭时正常。
- KEY1 中页签 1 的表格超出 TabControl 区域,外部按钮重复激活同一页签后,再切页或关闭页签会留下超出部分残影。
- 影响范围: TabControl 页签按钮 Tooltip、外部 `setActiveIndex()` 重复调用、页内超出页面边界绘制的控件。
- 期望结果:
- 页签按钮应始终位于页面之上,Tooltip 正常显示。
- 外部重复激活已激活页签不应破坏当前可见页面的快照链。
- 实际结果:
- 页签按钮 Tooltip 会被页面盖掉。
- 外部重复激活同一页签后,页内超出页面区域的 Table 在切页/关页时会残留。
## 复现信息
- 前置条件:
- KEY5:存在可见页面和页签按钮 Tooltip
- KEY1:页签 1 内的 Table 长于 TabControl 页面区域,存在外部按钮 `test`
- 复现步骤:
1. 在 KEY5 中打开任意页签,将鼠标移到页签按钮上触发 Tooltip。
2. 观察 Tooltip 是否被页面盖掉。
3. 在 KEY1 中点击一次 `test` 激活页签 1。
4. 再点击一次 `test`,然后切页或关闭页签 1。
- 复现概率:必现
- 最小复现 DemoKEY5 TabControl 区、KEY1 页签 1 + 外部 `test` 按钮
- 证据:Tooltip 被页面覆盖;重复激活后切页/关页出现残影
## 初步分析
- 疑似位置:
- `TabControl::draw()`
- `TabControl::requestRepaint()`
- `TabControl::setActiveIndex()`
- `Button::setButtonClick()`
- 触发条件:
- 页签按钮先画、页面后画
- `TOGGLE` 同状态重复 set 仍触发 onToggleOn
- 相关线索:
- 页面绘制层级高于页签按钮时,页签 Tooltip 会被后画的页面盖掉
- 重复激活已激活页签会重复执行页面 `onWindowResize()` / `setIsVisible(true)`
- 最近相关改动:TabControl overlay / Tooltip 回归专项修复
## 跟踪信息
- 首次发现时间: 2026-04-15
- 最后更新时间: 2026-04-15
- 修复版本:当前工作区
- 验证版本:KEY1 / KEY5 编译级验证通过,待用户手测
- 备注:本次只修页签层级和重复激活链路,不扩到 TabControl 其它内部布局语义
@@ -0,0 +1,88 @@
# Fix-BUG-20260410-0004
> 适用场景:记录某个 BUG 的修复方案、影响评估、验证结果与落地信息。
## 关联信息
- Fix ID: `Fix-BUG-20260410-0004`
- 关联 BUG ID: `BUG-20260410-0004`
- 修复目标: 让按钮 Tooltip 在鼠标移出后立即回贴并消失,不再残留在屏幕上
- 状态:已验证
- 负责人: Codex 协作修改
- 分支 / 版本: 当前工作区 / 下一版本开发中
## 根因分析
- 根因:
- Tooltip 由 `Button` 在绘制末尾以内置浮层方式直接补画,不属于独立控件树节点。
- 隐藏路径里根据托管分发状态分叉处理,在托管分发阶段仅调用 `invalidateBackgroundSnapshot()`,只作废 Tooltip 快照,不执行回贴。
- 当本轮后续重绘只覆盖按钮本体区域时,Tooltip 占用的屏幕区域不会被擦除,因此表现为 Tooltip 留在屏幕上。
- 触发条件:
- Tooltip 已显示
- 鼠标移出按钮区域,进入 `hideTooltip()` 分支
- 为什么之前没发现:
- 之前更关注 Tooltip 的显示、Hover 切换和对话框遮挡链路
- “逻辑状态已隐藏,但屏幕区域无人回贴”的问题只有在当前托管重绘路径下才更稳定暴露
- 关键证据:
- [`Button.cpp`](D:/programming/imGUI-easyX/imGui-easyX/Button.cpp) 中 `hideTooltip()` 在托管分发阶段只作废快照,不回贴 Tooltip 自身区域
## 修复方案
- 修复思路:
- Tooltip 既然是 `Button` 的内置浮层,就按“内置浮层”语义处理隐藏逻辑
- 隐藏时直接调用 Tooltip 现有接口回贴背景快照并作废,不再区分托管分发阶段
- 关键改动:
- 简化 `Button::hideTooltip()` 分支
- 只要 `tipVisible == true`,统一调用 `tipLabel.hide()`
- 保留 `tipHoverTick` 重置逻辑
- 涉及文件 / 类 / 函数:
- [`Button.cpp`](D:/programming/imGUI-easyX/imGui-easyX/Button.cpp)
- `Button::hideTooltip()`
- 影响的 API / 行为:
- 无公开 API 变化
- 内部行为调整为“Tooltip 隐藏时总是立即回贴其自身区域”
- 关键约束 / 不变量:
- Tooltip 继续作为 `Button` 内置浮层存在
- 不引入新的 Tooltip 控件树托管协议
- 不扩大 `Window` 托管重绘 coverage
- 回滚点 / 开关:
- 若后续需要回退,只需恢复 `Button::hideTooltip()` 的旧分支逻辑
## 影响评估
- 影响范围:
- 启用 Tooltip 的 `Button`
- `KEY == 4` 等包含 Tooltip 的回归场景
- 兼容性影响:无
- 行为变化:有(Tooltip 隐藏时由“部分路径只作废快照”改为“统一立即回贴并作废”)
- 性能影响:无
- 回归风险:
- Tooltip 隐藏时会发生一次即时局部回贴
- 由于 Tooltip 本身就是按钮内置浮层,这个行为与其显示方式一致,风险相对可控
## 验证结果
- 验证步骤:
1. 修改 [`Button.cpp`](D:/programming/imGUI-easyX/imGui-easyX/Button.cpp) 中 `Button::hideTooltip()` 的 Tooltip 隐藏分支
2. 编译 `Button.cpp`
3. 编译 `z-testDome.cpp` 并指定 `KEY=4`,确认 Tooltip 相关 demo 入口可正常编译
- 验证结果:
- 代码已完成修改
- 源码级编译验证通过
- 基于当前代码链路推演,Tooltip 离开按钮后会立即回贴自身区域,不再依赖按钮本体重绘覆盖 Tooltip 区域
- 回归检查:
- 未做 GUI 手动交互回归
- 需用户进一步确认实际 Tooltip 行为
- 验证证据:
- `Button.cpp` 编译通过
- `z-testDome.cpp /DKEY=4` 编译通过
## 落地信息
- Commit: 当前工作区未提交
- PR[可选]
- 发布版本:[可选]
- 备注:
- 本次修复未改动 Tooltip 对外接口,只调整内部隐藏路径
@@ -0,0 +1,83 @@
# Fix-BUG-20260415-0005
> 适用场景:记录某个 BUG 的修复方案、影响评估、验证结果与落地信息。
## 关联信息
- Fix ID: Fix-BUG-20260415-0005
- 关联 BUG ID: BUG-20260415-0005
- 修复目标: 收口局部重绘提交后的 overlay 兄弟补画机制
- 状态:已完成
- 负责人: Codex 协作修改
- 分支 / 版本: 当前工作区
## 根因分析
- 根因:
- 局部重绘提交只重画了 dirty root 或 dirty child,没有按父容器真实绘制顺序把 coverage 上方相交的兄弟补画回来。
- 触发条件:
- 下层区域写入像素后,上层 sibling 与本次 coverage 相交。
- 为什么之前没发现:[可选]
- 旧场景多集中在对话框覆盖链,普通 sibling overlay 问题没有被系统化回归。
- 关键证据:[可选]
- `KEY5``Table` 翻页覆盖顶层粉色浮层可稳定复现。
- 同父 `Canvas` 相交子控件在局部重绘后会出现相同症状。
## 修复方案
- 修复思路:
- 不做控件之间的长期联动标记。
- 改为由父容器在局部重绘提交后,按真实绘制顺序动态补画 coverage 上方相交的直接绘制单元。
- 关键改动:
- `Window`:补画普通顶层控件 overlay,而不再只补 `Dialog`
- `Canvas`:补画 coverage 上方相交的直接子控件
- `TabControl`:按自己的真实绘制顺序补画页签按钮与页面
- overlay 补画前统一 `invalidateBackgroundSnapshot()`,避免旧快照反贴旧背景
- 涉及文件 / 类 / 函数:
- [`Window.cpp`](D:/programming/imGUI-easyX/imGui-easyX/Window.cpp)
- [`Canvas.cpp`](D:/programming/imGUI-easyX/imGui-easyX/Canvas.cpp)
- [`TabControl.cpp`](D:/programming/imGUI-easyX/imGui-easyX/TabControl.cpp)
- 影响的 API / 行为:[可选]
- 无对外 API 变化
- 局部重绘行为更严格遵守视觉合成顺序
- 关键约束 / 不变量:[可选]
- 父容器只处理自己的直接绘制单元
- overlay 补画顺序必须与实际 `draw()` 顺序一致
- 不升级为整窗 / 整容器重绘,除非父容器自身快照或 dirty 条件不满足
- 回滚点 / 开关:[可选]
- 无单独开关,回滚需回退相关局部重绘提交逻辑
## 影响评估
- 影响范围:
- `Window / Canvas / TabControl` 的局部重绘提交路径
- 兼容性影响:无
- 行为变化:有(局部重绘后会补画上层 overlay)
- 性能影响:有(增加必要的 overlay 补画),但仍显著小于整窗 / 整容器重绘
- 回归风险:
- 若 coverage 计算不准,仍可能遗漏或多画
- `TabControl` 局部重绘顺序必须和真实 `draw()` 顺序保持一致
## 验证结果
- 验证步骤:
1. 编译 [`Window.cpp`](D:/programming/imGUI-easyX/imGui-easyX/Window.cpp)、[`Canvas.cpp`](D:/programming/imGUI-easyX/imGui-easyX/Canvas.cpp)、[`TabControl.cpp`](D:/programming/imGUI-easyX/imGui-easyX/TabControl.cpp)
2. 编译 [`z-testDome.cpp`](D:/programming/imGUI-easyX/imGui-easyX/z-testDome.cpp) `KEY=5`
3. 回归 `Table` 翻页、相交 `Canvas``TabControl` 页签/页面遮挡场景
- 验证结果:
- 编译级验证通过
- GUI 手动回归需继续在本机确认
- 回归检查:[可选]
- `Table` 分页按钮与页码链已同步收口
- 验证证据:[可选]
- `KEY5` 已新增更明确的 overlay 专项场景
## 落地信息
- Commit: 未提交(当前工作区)
- PR[可选]
- 发布版本:[可选]
- 备注:[可选]
- `Dialog` 旧 synthetic move 逻辑本轮保留,后续可再评估是否纳入通用清理模型。
@@ -0,0 +1,69 @@
# Fix-BUG-20260415-0006
> 适用场景:记录某个 BUG 的修复方案、影响评估、验证结果与落地信息。
## 关联信息
- Fix ID: Fix-BUG-20260415-0006
- 关联 BUG ID: BUG-20260415-0006
- 修复目标: 让托管局部重绘能够正确提交嵌套容器中的脏后代
- 状态:已完成
- 负责人: Codex
- 分支 / 版本: 当前工作区
## 根因分析
- 根因: 托管局部重绘提交只识别 root 的直接 dirty child,不识别“自己不脏,但下面有 dirty descendant”的直接子分支。
- 触发条件: 深层按钮状态变化后,叶子控件已 dirty,但中间层 Canvas 自己未 dirty。
- 为什么之前没发现: 第一阶段主要覆盖顶层和浅层容器,三层嵌套专项场景在 KEY5 重构后才稳定暴露。
- 关键证据:
- 按钮 hover / click 回调正常执行
- 日志正常,但视觉状态直到更大重绘才补出来
## 修复方案
- 修复思路: 把托管重绘提交从“只认直接 dirty child”改成“识别 dirty descendant,并提升到 root 下直接脏分支提交”。
- 关键改动:
- 新增 `hasManagedDirtySubtree()`
- 新增 `getManagedRepaintDirectBranch(root)`
- `Canvas / TabControl` 局部提交时,直接子单元若拥有 dirty descendant,也继续 `commitManagedRepaint()`
- `Window::requestManagedRepaint()` 的 coverage 从最深叶子提升到 root 下直接脏分支
- 涉及文件 / 类 / 函数:
- `Control.h / Control.cpp`
- `Canvas.cpp`
- `TabControl.cpp`
- `Window.cpp`
- 影响的 API / 行为:无公开 API 变化
- 关键约束 / 不变量:
- dirty descendant 不能再被中间层漏掉
- 托管局部提交顺序仍需保持与真实绘制顺序一致
- 回滚点 / 开关:无
## 影响评估
- 影响范围: 嵌套 Canvas、页内嵌套容器、深层按钮 hover / click / tooltip 状态刷新
- 兼容性影响:无
- 行为变化:有(深层按钮视觉状态会及时刷新)
- 性能影响:有(coverage 更保守,局部补画可能略增)
- 回归风险:
- 局部提交 coverage 扩大后,上层 overlay 补画次数可能增加
- 需重点回归嵌套容器与顶层 overlay 场景
## 验证结果
- 验证步骤:
1. 编译 `Control.cpp / Canvas.cpp / TabControl.cpp / Window.cpp / z-testDome.cpp`
2. 使用 KEY5 三层嵌套区作为主回归入口
3. 检查深层按钮 hover / press / release 是否能即时刷新
- 验证结果: 编译通过;逻辑推演闭合;GUI 需用户本机手测
- 回归检查:KEY5 三层嵌套、页内嵌套、overlay 补画主线
- 验证证据:编译级验证通过
## 落地信息
- Commit: 未单独提交
- PR[可选]
- 发布版本:[可选]
- 备注:该修复为机制修正,不是 KEY5 专项补丁
@@ -0,0 +1,71 @@
# Fix-BUG-20260415-0007
> 适用场景:记录某个 BUG 的修复方案、影响评估、验证结果与落地信息。
## 关联信息
- Fix ID: Fix-BUG-20260415-0007
- 关联 BUG ID: BUG-20260415-0007
- 修复目标: 让托管 coverage 与 overlay 补画基于实际绘制范围,而非控件本体 bounds
- 状态:已完成
- 负责人: Codex
- 分支 / 版本: 当前工作区
## 根因分析
- 根因: `Window / Canvas / TabControl` 的 coverage 计算链默认使用 `getBoundsRect()`,没有把 Tooltip 这类附加绘制区域纳入。
- 触发条件: 控件本体未与 overlay 相交,但附加绘制区域超出本体并写入更上层区域。
- 为什么之前没发现: 第一轮主要解决普通控件和顶层 overlay;Tooltip 属于本体外附加绘制,直到 C / D 交界回归才稳定暴露。
- 关键证据:
- 顶层按钮 Tooltip 正常,但容器内按钮 Tooltip 压到 D 橙区
- 根因与按钮本体 bounds 无关,与 Tooltip 超出范围有关
## 修复方案
- 修复思路: 引入“实际绘制 coverage”接口,让托管重绘与局部提交统一走真实覆盖范围。
- 关键改动:
-`Control` 增加 `getManagedRepaintCoverageRect()`
- `Button` 在 Tooltip 可见时返回“按钮矩形 + Tooltip 矩形”的并集
- `Canvas / TabControl / Table` 将可见内部绘制单元 coverage 递归并入
- `Window::requestManagedRepaint()` 和顶层 overlay 重组改为走 coverage 接口
- 涉及文件 / 类 / 函数:
- `Control.h / Control.cpp`
- `Button.h / Button.cpp`
- `Canvas.h / Canvas.cpp`
- `TabControl.h / TabControl.cpp`
- `Table.h / Table.cpp`
- `Window.cpp`
- 影响的 API / 行为:新增内部 virtual 接口,无公开 API 变化
- 关键约束 / 不变量:
- Tooltip 智能选位不在本次修复范围内
- overlay 补画继续按父容器真实绘制顺序工作
- 回滚点 / 开关:无
## 影响评估
- 影响范围: Tooltip、overlay 补画、复杂容器内附加绘制区域
- 兼容性影响:无
- 行为变化:有(Tooltip 与 overlay 层级恢复正确)
- 性能影响:有(coverage 更保守,可能触发更多 overlay 补画)
- 回归风险:
- coverage 扩大后顶层/容器层局部提交会多画一些 overlay
- 需重点回归 Tooltip 与多跳 overlay 场景
## 验证结果
- 验证步骤:
1. 编译 `Control.cpp / Button.cpp / Canvas.cpp / TabControl.cpp / Table.cpp / Window.cpp / z-testDome.cpp`
2. 在 KEY5 中验证 C 米区容器内按钮 Tooltip 与 D 橙区的遮挡关系
3. 回归顶层按钮 Tooltip、页签按钮 Tooltip、浮层按钮 Tooltip
- 验证结果: 编译通过;逻辑推演闭合;GUI 需用户本机手测
- 回归检查:KEY5 C/D 交界区、TabControl 页签、三层嵌套按钮 Tooltip
- 验证证据:编译级验证通过
## 落地信息
- Commit: 未单独提交
- PR[可选]
- 发布版本:[可选]
- 备注:Tooltip 智能选位明确延期,本次只修“coverage 正确性”
@@ -0,0 +1,75 @@
# Fix-BUG-20260415-0008
> 适用场景:记录某个 BUG 的修复方案、影响评估、验证结果与落地信息。
## 关联信息
- Fix ID: Fix-BUG-20260415-0008
- 关联 BUG ID: BUG-20260415-0008
- 修复目标: 修正 TabControl 页签层级和重复激活已激活页签的回调链
- 状态:已完成
- 负责人: Codex
- 分支 / 版本: 当前工作区
## 根因分析
- 根因:
- `TabControl::draw()` 与局部重绘顺序为“先页签按钮,后页面”,导致页签 Tooltip 被页面盖掉。
- `Button::setButtonClick()``TOGGLE` 没有做“同状态短路”,外部重复激活同一页签会重复触发 `onToggleOnCallback`
- `TabControl::setActiveIndex()` 也没有对“目标已是当前激活页”做保护,导致当前可见页的 `onWindowResize()` / `setIsVisible(true)` 链被重复执行。
- 触发条件:
- 任意页打开时触发页签 Tooltip
- 外部对已激活页签再次 `setActiveIndex()`
- 为什么之前没发现: KEY1 外部按钮重复激活场景和 KEY5 页签 Tooltip 场景是后续专项回归才补出来的。
- 关键证据:
- 所有页关闭时页签 Tooltip 正常,说明问题在页面层级覆盖
- 只点一次外部激活正常,重复激活后残影出现,说明问题在重复回调链
## 修复方案
- 修复思路:
- 把 TabControl 绘制/局部重绘顺序改为“先页面、后页签按钮”
-`TOGGLE` 状态设置和 `setActiveIndex()` 增加同状态短路
- 关键改动:
- `TabControl::draw()` 改成先画页面、后画页签按钮
- `TabControl::requestRepaint(this)` 的局部提交顺序同步调整
- `Button::setButtonClick()``TOGGLE` 同状态时直接返回
- `TabControl::setActiveIndex()` 若目标已是当前激活页则直接返回
- 涉及文件 / 类 / 函数:
- `TabControl.cpp`
- `Button.cpp`
- 影响的 API / 行为:
- 重复设置相同 `TOGGLE` 状态不再重复触发回调
- 关键约束 / 不变量:
- 页签按钮视觉层级应始终高于页面
- 外部重复激活同一页签不应重新走页面显示链
- 回滚点 / 开关:无
## 影响评估
- 影响范围: TabControl 页签 Tooltip、外部页签激活、页内超出页面边界的复合控件
- 兼容性影响:有(收紧了 TOGGLE 同状态重复 set 的回调语义)
- 行为变化:有(页签 Tooltip 层级恢复正确;重复激活同一页签不再重复触发回调)
- 性能影响:无
- 回归风险:
- 如有外部代码依赖“同状态重复 set 也要重复触发回调”,该行为将被收掉
- 需重点回归 KEY1 和 KEY5 的 TabControl 场景
## 验证结果
- 验证步骤:
1. 编译 `Button.cpp / TabControl.cpp`
2. 编译 `z-testDome.cpp``KEY=1`
3. 回归 `KEY5` 页签 Tooltip 与 `KEY1` 外部按钮重复激活页签场景
- 验证结果: 编译通过;逻辑推演闭合;GUI 需用户本机手测
- 回归检查:KEY1 页签 1 表格超出区、KEY5 页签 Tooltip
- 验证证据:编译级验证通过
## 落地信息
- Commit: 未单独提交
- PR[可选]
- 发布版本:[可选]
- 备注:此修复不包含 TabControl 其它内部布局专题
@@ -0,0 +1,78 @@
# 功能变更 ID: Feature-20260415-0008
> 适用场景:记录小到中等规模的功能、接口、行为、默认值或配置变化。
> 不适用场景:新增核心模块、重大模块重构、架构级设计,请使用“新增功能模块”模板。
## 基本信息
- ID: Feature-20260415-0008
- 标题: KEY5 第二阶段专项回归场景增强
- 状态:已完成
- 类型:修改
- 级别:L2 中等
- 模块: 测试用例 / 回归场景
- 版本 / 分支: 当前工作区
- 环境: Windows + EasyX
- 负责人: Codex 协作修改
## 变更背景
- 背景:
- 旧版 `KEY5` 已能覆盖第一阶段布局主线,但场景层次不够集中,说明文字偏长,部分区域排版紧。
- 第二阶段引入了内容驱动、overlay 补画、容器职责边界等新验证点,旧 `KEY5` 覆盖不完整。
- 目标:
-`KEY5` 重构成第二阶段专项回归入口。
- 增加三层 `Canvas` 嵌套、通用 overlay、跨容器 hover 清理、`Label` 内容驱动规则验证。
- 不做什么:[可选]
- 不追求视觉美化。
- 不把测试用例变成完整 demo 页。
## 变更内容
- 变更摘要:
- 重构 `KEY5` 的区域布局与说明文案。
- 引入三层嵌套 `Canvas` 场景。
- 增加 `Window / Canvas / TabControl / Table` 四类关键回归点。
- 新增项:[可选]
- 三层 `Canvas` 嵌套与多层锚点链回归。
- `Label` 文本变化 / 样式变化按钮。
- 跨容器相邻按钮 hover/tooltip 回归点。
- 顶层浮层覆盖 `Table` 的 overlay 回归点。
- 修改项:[可选]
- 缩短说明 `Label` 文案,减少普通排版重叠。
- 重新组织分区顺序,提升单区可读性。
- 删除 / 废弃项:[可选]
- 删除过长、重复的说明文案。
- 受影响的文件 / 类 / 函数:
- [`z-testDome.cpp`](D:/programming/imGUI-easyX/imGui-easyX/z-testDome.cpp)
- 对外 API / 属性变化:[可选]
-
## 行为对照
- 变更前:
- `KEY5` 主要覆盖第一阶段布局规则,层次和交互链回归点相对分散。
- 说明文字较长,部分区域排版偏挤。
- 变更后:
- `KEY5` 成为第二阶段主回归入口。
- 三层嵌套、overlay、跨容器 hover、`Label` 内容驱动、`Table` 分页与页码都能直接观察。
- 兼容性说明:兼容
- 迁移说明:[可选]
- 仍然通过 `#define KEY 5` 进入,无额外迁移。
## 验证与落地
- 验证方式:
- 编译 `z-testDome.cpp /DKEY=5`
- 手动观察窗口 resize、hover、tooltip、overlay、分页和 `Label` 变化
- 验证结果:
- 编译级验证通过
- GUI 手动验证待本机执行
- 关联 BUG / Fix[可选]
- `BUG-20260415-0005`
- `Fix-BUG-20260415-0005`
- Commit: 未提交(当前工作区)
- PR[可选]
- 发布版本:[可选]
- 备注:[可选]
- `Label` 样式变化验证明确依赖 `setDirty(true)` 使用约定。
@@ -0,0 +1,87 @@
# 功能变更 ID: Feature-20260416-0009
> 适用场景:记录小到中等规模的功能、接口、行为、默认值或配置变化。
> 不适用场景:新增核心模块、重大模块重构、架构级设计,请使用“新增功能模块”模板。
## 基本信息
- ID: Feature-20260416-0009
- 标题: KEY2 使用新布局 API 重构与联动刷新
- 状态:开发中
- 类型:修改
- 级别:L2 中等
- 模块: 测试用例 / Layout API 迁移样例
- 版本 / 分支: 当前工作区
- 环境: Windows + EasyX
- 负责人: Codex 协作修改
## 变更背景
- 背景:
- `KEY2` 原有布局主要依赖旧 `setAnchor(...)` 语义,无法清晰表达“固定尺寸 + 比例位移”。
- 32 位位号标签与位按钮分别按各自尺寸参与比例位移,窗口拉伸后会发生错位。
- 位按钮点击后,下面的十六进制、十进制与二进制显示区刷新链散落在多个回调中,维护成本高且容易漏更新。
- 目标:
-`KEY2` 改成新布局 API 的首个完整迁移样例。
- 修复位标签与位按钮 resize 后错位问题。
- 让位按钮、位操作按钮、配置区按钮共用一条统一的显示刷新链。
- 不做什么:[可选]
- 不重构 `KEY1 / KEY5`
- 不引入 `KEY2` 以外的新功能
## 变更内容
- 变更摘要:
- 重构 `KEY2` 位选择区为“每一位一个小 Canvas 单元”。
- 使用新布局 API 重排 `KEY2` 的选择区、功能区、数值显示区、二进制显示区与配置区。
- 抽出统一的 bit 快照、应用、显示刷新链,收口位按钮与功能按钮的联动。
- 新增项:[可选]
- `KEY2` 内部的布局辅助策略函数
- `snapshotBits / applyBits / refreshDisplaysWithBits` 这类统一刷新路径
- 修改项:[可选]
- 32 位位标签与位按钮的组织方式
- 顶层区域的拉伸策略
- 一键置 0 / 一键置 1 / 左移 / 右移 / 位取反 / signed toggle 的显示刷新方式
- 删除 / 废弃项:[可选]
- 废弃旧的“位标签和位按钮分别做比例位移”的选择区组织方式
- 废弃散在多个回调中的局部显示刷新
- 受影响的文件 / 类 / 函数:
- [`z-testDome.cpp`](D:/programming/imGUI-easyX/imGui-easyX/z-testDome.cpp)
- 对外 API / 属性变化:[可选]
- 无框架级 API 变化,属于测试用例迁移
## 行为对照
- 变更前:
- 32 位位标签与位按钮在窗口拉伸后会发生错位。
- 位按钮点击后,显示区更新链不统一,后续维护难度高。
- `KEY2` 的多个区域仍依赖旧锚点表达,窗口拉伸时不够稳定。
- 变更后:
- 每一位作为独立单元整体位移,位标签与位按钮不再分离漂移。
- 位按钮、一键置 0/1、位取反、左移、右移、signed toggle 均通过统一刷新链更新显示区。
- 顶层五个区域在窗口拉伸后继续铺满可用空间,内部小控件保持固定尺寸。
- 兼容性说明:兼容
- 迁移说明:[可选]
- 这是新布局 API 的示例迁移,不影响其它 `KEY` 入口。
## 验证与落地
- 验证方式:
- 编译 `z-testDome.cpp /DKEY=2`
- 编译 `z-testDome.cpp /DKEY=5`
- 手动验证 `KEY2`
- 窗口横向拉伸
- 位按钮点击
- 位取反 / 左移 / 右移
- 一键置 0 / 一键置 1
- 有符号 / 无符号切换
- 验证结果:
- 编译级验证通过。
- GUI 手动验证待本机继续确认。
- 关联 BUG / Fix[可选]
- 无单独 BUG 记录,本次属于布局 API 落地后的示例迁移
- Commit:
- PR[可选]
- 发布版本:[可选]
- 备注:[可选]
- 当前功能区采用左/中/右固定宽度面板布局;窗口变宽时可稳定铺满,若后续需要进一步适配极窄窗口,再考虑把功能区做成等比分栏。
@@ -0,0 +1,156 @@
# 功能变更 ID: Feature-20260418-0010
> 适用场景:记录小到中等规模的功能、接口、行为、默认值或配置变化。
> 不适用场景:新增核心模块、重大模块重构、架构级设计,请使用“新增功能模块”模板。
## 基本信息
- ID: Feature-20260418-0010
- 标题: 当前阶段测试用例矩阵与定位
- 状态:已完成
- 类型:修改
- 级别:L1 轻量
- 模块: 测试用例 / 回归策略
- 版本 / 分支: 当前工作区
- 环境: Windows + EasyX
- 负责人: Codex 协作修改
## 变更背景
- 背景:
- 第二阶段主线、布局策略公开 API、`KEY2` 迁移以及近期重绘 / tooltip / overlay / `TabControl` / `Table` 修复已基本收口。
- 当前项目中同时存在 `KEY1 ~ KEY5` 多组测试入口,但各自负责覆盖什么、哪些是主集、哪些是专项补充,若不显式记录,后续容易继续依赖聊天上下文记忆。
- 目标:
- 把当前阶段各测试入口的职责、覆盖范围和预期行为正式写成测试矩阵。
- 明确本阶段建议主回归集与补充专项。
- 不做什么:[可选]
- 不新增测试逻辑。
- 不修改运行时代码。
- 不把测试矩阵扩成自动化测试体系。
## 变更内容
- 变更摘要:
- 新增当前阶段测试矩阵记录。
- 同步 `KEY1 ~ KEY5` 的阶段定位与行为预期。
- 新增项:[可选]
- 当前阶段建议主回归集:`KEY1 + KEY2 + KEY5`
- Dialog / MessageBox 补充专项:`KEY4`
- 修改项:[可选]
- 无代码修改,仅补充测试约定与阶段定位。
- 删除 / 废弃项:[可选]
-
- 受影响的文件 / 类 / 函数:
- [`z-testDome.cpp`](D:/programming/imGUI-easyX/imGui-easyX/z-testDome.cpp)
- 对外 API / 属性变化:[可选]
-
## 行为对照
- 变更前:
- `KEY1 ~ KEY5` 的职责划分主要依赖源码注释和聊天上下文。
- 很难快速判断“当前阶段主回归集”与“专项补充集”分别是什么。
- 变更后:
- 每个 `KEY` 入口的阶段定位、覆盖重点和行为预期都有正式记录。
- 可以按矩阵组织回归,而不再只靠记忆。
- 兼容性说明:兼容
- 迁移说明:[可选]
- 无需迁移,属于当前阶段测试约定落地。
## 验证与落地
- 验证方式:
- 对照当前 [`z-testDome.cpp`](D:/programming/imGUI-easyX/imGui-easyX/z-testDome.cpp) 中 `KEY1 ~ KEY5` 的实际结构与近期修复范围进行人工核对。
- 验证结果:
- 当前矩阵与现有测试入口和阶段主线一致。
- 关联 BUG / Fix[可选]
- [Module-20260415-0004-布局系统第二阶段验收与封口.md](D:/programming/imGUI-easyX/imGui-easyX/开发记录/模块/Module-20260415-0004-布局系统第二阶段验收与封口.md)
- [Module-20260416-0005-布局策略公开API落地.md](D:/programming/imGUI-easyX/imGui-easyX/开发记录/模块/Module-20260416-0005-布局策略公开API落地.md)
- Commit:
- PR[可选]
- 发布版本:[可选]
- 备注:[可选]
- resize 过程中若开启高频 console 日志,可能出现一帧视觉延迟;当前判断属于调试态 I/O 现象,不按 bug 处理。
## 当前阶段测试矩阵
### KEY1:旧页签链路 + Table 超界残留回归
- 覆盖目标:
- `TabControl` 重复激活同一页签
- 切页 / 关页后的快照与清理链
- 页内容超出页范围时的残影与恢复
- 当前预期:
- 外部重复激活当前页签,不再扰动当前页快照链。
- 页签 1 中 `Table` 超出页区域的部分,在切页或关闭页签时不会残留。
- 正常全量重绘后,不应再出现额外残影或层级错乱。
- 阶段定位:
- 当前主回归集之一。
### KEY2:公开布局 API 首个迁移样例
- 覆盖目标:
- 新公开布局 API 的真实使用
- 位选择区容器化重构
- 显示区统一刷新链
- 顶层区块拉伸铺满
- 当前预期:
- 32 位选择区中的“位号 + 位按钮”以单元整体位移,窗口拉伸后保持对齐。
- 位按钮点击、位取反、左移、右移、一键置 0/1、签名切换,会统一刷新十六进制、十进制、上次值、本次值和 `initData`
- 顶层五个区块在正常拉伸 / 最大化后继续铺满窗口。
- 当前更偏重横向铺满验证;极窄窗口下功能区进一步自适应,不作为本阶段硬指标。
- 阶段定位:
- 当前主回归集之一。
- 同时是新布局 API 的首个迁移示例。
### KEY3:旧业务大场景保留用例
- 覆盖目标:
- 保留老业务页示例
- 观察新主线是否把旧业务场景直接打坏
- 当前预期:
- 登录页、`TabControl`、旧业务页基本行为保持可运行。
- 不要求覆盖新布局 API、overlay、tooltip 或第二阶段专项行为。
- 阶段定位:
- 保留用例,不纳入当前主回归集。
### KEY4Dialog / MessageBox 专项回归
- 覆盖目标:
- 模态 / 非模态 `Dialog`
- 遮挡交互
- 关闭后 hover 恢复
- 拖拽 resize
- 当前预期:
- 非模态 `Dialog` 遮挡底层按钮时,不应出现 hover / tooltip 穿透或残留。
- 模态 `Dialog` 打开后拖拽窗口,标题、关闭按钮和底层恢复链保持稳定。
- 对话框关闭后,底层按钮 hover 能及时恢复。
- 阶段定位:
- 当前补充专项,不纳入每轮主回归集。
### KEY5:第二阶段专项主回归
- 覆盖目标:
- 三层 `Canvas` 嵌套
- 跨容器 hover / tooltip
- overlay 补画与 coverage 链
- `TabControl`
- `Table`
- 页码与分页按钮
- 当前预期:
- 三层 `Canvas` 嵌套下,深层按钮 hover / press / release / tooltip 都能正确刷新。
- `Window / Canvas / TabControl` 的 overlay 补画与 coverage 链闭合,不再出现遮挡错层。
- `TabControl` 页签按钮与页面层级正确;页签 tooltip 不会再被页面盖掉。
- `Table` 分页按钮、页码标签、与上层浮层相交时的重绘链保持正确。
- 阶段定位:
- 当前主回归集之一。
- 第二阶段及重绘主线的核心专项入口。
## 当前建议
- 当前阶段建议主回归集:
- `KEY1 + KEY2 + KEY5`
- 当前补充专项:
- `KEY4`
- 暂不纳入每轮主集:
- `KEY3`
@@ -0,0 +1,295 @@
# 功能变更 ID: Feature-20260509-0011
> 适用场景:记录小到中等规模的功能、接口、行为、默认值或配置变化。
> 不适用场景:新增核心模块、重大模块重构、架构级设计,请使用“新增功能模块”模板。
## 基本信息
- ID: Feature-20260509-0011
- 标题: 布局 API 使用说明与旧接口覆盖规则
- 状态:已验证
- 类型:新增
- 级别:L2 中等
- 模块: Layout API / Control
- 版本 / 分支: 当前工作区
- 环境: Windows + EasyX
- 负责人: Codex 协作修改
## 变更背景
- 背景:
- 当前布局系统内部已经以 `LayoutSpec` 为统一解算依据。
- 旧接口 `setLayoutMode(...)` / `setAnchor(...)` 仍然保留,但只能表达有限的双锚点语义。
- 新增公开布局 API 后,需要明确用户应如何设置轴向锚点、尺寸策略和固定尺寸位移策略。
- 目标:
- 说明新布局 API 的职责和使用方式。
- 明确新旧接口混用时的覆盖规则。
- 给出常见布局组合和 `KEY2` 迁移场景中的推荐写法。
- 不做什么:[可选]
- 不开放 `LayoutCapability` 的普通外部写入接口。
- 不修改 `Table` 当前版本 `Y Fixed` 的能力边界。
- 不处理 Tooltip 智能选位、`Table` 内部局部重绘、`Dialog` synthetic move 统一等后置项。
## 变更内容
- 变更摘要:
- 增加布局 API 使用说明文档。
- 明确新接口与旧接口的覆盖关系。
- 汇总常见布局模式的代码写法。
- 新增项:[可选]
- 新公开接口说明
- 旧接口映射说明
- 混用覆盖规则
- 常见写法示例
- 调试态日志说明
- 修改项:[可选]
- 无运行时代码修改,本文件只记录使用说明。
- 删除 / 废弃项:[可选]
-
- 受影响的文件 / 类 / 函数:
- [`Control.h`](D:/programming/imGUI-easyX/imGui-easyX/Control.h)
- [`Control.cpp`](D:/programming/imGUI-easyX/imGui-easyX/Control.cpp)
- [`z-testDome.cpp`](D:/programming/imGUI-easyX/imGui-easyX/z-testDome.cpp)
- 对外 API / 属性变化:[可选]
- `setHorizontalLayoutSpec(...)`
- `setVerticalLayoutSpec(...)`
- `setHorizontalAnchors(...)`
- `setVerticalAnchors(...)`
- `setHorizontalSizePolicy(...)`
- `setVerticalSizePolicy(...)`
- `setHorizontalAlignPolicy(...)`
- `setVerticalAlignPolicy(...)`
- `getHorizontalLayoutSpec()`
- `getVerticalLayoutSpec()`
## 行为对照
- 变更前:
- 用户主要通过 `setLayoutMode(...)``setAnchor(...)` 设置布局。
- 固定尺寸 + 居中、固定尺寸 + 比例位移等组合无法通过旧接口清晰表达。
- 旧双锚点在某些测试场景中会被迫承担 Stretch 语义,导致固定按钮和内容驱动标签在 resize 后错位。
- 变更后:
- 用户可按水平轴 / 垂直轴分别设置锚点集合、尺寸策略和固定尺寸位移策略。
- 旧接口仍可用,但只作为兼容输入层。
- 新旧接口混用时,最后调用的接口覆盖对应轴的布局规格。
- 兼容性说明:兼容
- 迁移说明:[可选]
- 新代码建议优先使用新布局 API。
- 旧代码可以继续使用 `setAnchor(...)`,但复杂布局应逐步迁移到新接口。
## 验证与落地
- 验证方式:
- 编译 `Control.cpp`
- 编译 `z-testDome.cpp /DKEY=2`
- 编译 `z-testDome.cpp /DKEY=5`
- 手测 `KEY1 / KEY2 / KEY5`
- 验证结果:
- 编译级验证通过。
- 用户手测当前暂未发现问题。
- 关联 BUG / Fix[可选]
- [Feature-20260416-0009-KEY2-使用新布局API重构与联动刷新.md](D:/programming/imGUI-easyX/imGui-easyX/开发记录/功能变更/Feature-20260416-0009-KEY2-使用新布局API重构与联动刷新.md)
- [Feature-20260418-0010-当前阶段测试用例矩阵与定位.md](D:/programming/imGUI-easyX/imGui-easyX/开发记录/功能变更/Feature-20260418-0010-当前阶段测试用例矩阵与定位.md)
- Commit:
- PR[可选]
- 发布版本:[可选]
- 备注:[可选]
- resize 收口阶段若开启高频 console 日志,可能出现一帧视觉延迟;当前按调试态 I/O 现象记录,不作为 bug 处理。
## API 总览
### 完整轴向规格接口
```cpp
void setHorizontalLayoutSpec(const StellarX::AxisLayoutSpec& spec);
void setVerticalLayoutSpec(const StellarX::AxisLayoutSpec& spec);
```
- 直接覆盖对应轴的完整布局规格。
- 调用后自动切换到 `StellarX::LayoutMode::AnchorToEdges`
- 适合调用方已经准备好完整 `AxisLayoutSpec` 的场景。
### 锚点集合接口
```cpp
void setHorizontalAnchors(bool left, bool right);
void setVerticalAnchors(bool top, bool bottom);
```
- 水平轴对应 `left / right`
- 垂直轴对应 `top / bottom`
- 只修改对应轴的锚点集合,不主动改变尺寸策略或位移策略。
### 尺寸策略接口
```cpp
void setHorizontalSizePolicy(StellarX::AxisSizePolicy policy);
void setVerticalSizePolicy(StellarX::AxisSizePolicy policy);
```
- `Stretch`:允许该轴尺寸随父容器变化。
- `FixedSize`:该轴尺寸保持设计态尺寸。
- 控件能力边界仍然生效。例如 `Table` 当前版本不允许 `Y Stretch`,即使用户设置垂直 `Stretch`,也会被能力边界降级。
### 固定尺寸位移策略接口
```cpp
void setHorizontalAlignPolicy(StellarX::AxisAlignPolicy policy);
void setVerticalAlignPolicy(StellarX::AxisAlignPolicy policy);
```
- 只在 `FixedSize` 语义下决定位置。
- 不负责改变尺寸。
- 常用值:
- `Start`:保持起边关系,例如左固定、上固定。
- `End`:保持终边关系,例如右固定、下固定。
- `Center`:保持居中关系。
- `Proportional`:保持设计态相对位置比例,适合固定尺寸控件在可变宽容器中按比例移动。
### 读取接口
```cpp
StellarX::AxisLayoutSpec getHorizontalLayoutSpec() const;
StellarX::AxisLayoutSpec getVerticalLayoutSpec() const;
```
- 返回当前生效的新模型轴向规格。
- 不保证能完整逆映射回旧 `anchor_1 / anchor_2` 语义。
## 新旧接口覆盖规则
### 旧接口仍然保留
```cpp
void setLayoutMode(StellarX::LayoutMode layoutMode);
void setAnchor(StellarX::Anchor anchor1, StellarX::Anchor anchor2);
```
- 旧接口是兼容入口。
- `setAnchor(...)` 内部会把旧双锚点映射到新的 `layoutSpec.horizontal``layoutSpec.vertical`
- 旧接口无法表达所有新模型状态,例如固定尺寸 + 比例位移、固定尺寸 + 居中等。
### 后调用者生效
规则:
- 先调用 `setAnchor(...)`,再调用新 API,则新 API 覆盖对应轴规格。
- 先调用新 API,再调用 `setAnchor(...)`,则旧接口重新覆盖水平轴和垂直轴规格。
- 新 API 只覆盖自己负责的轴或字段。例如 `setHorizontalSizePolicy(...)` 只修改水平轴尺寸策略,不修改垂直轴。
示例:
```cpp
button->setLayoutMode(StellarX::LayoutMode::AnchorToEdges);
button->setAnchor(StellarX::Anchor::Left, StellarX::Anchor::Right);
// 覆盖水平轴:改为固定尺寸 + 居中,不再沿用旧接口映射出的 Stretch。
button->setHorizontalSizePolicy(StellarX::AxisSizePolicy::FixedSize);
button->setHorizontalAlignPolicy(StellarX::AxisAlignPolicy::Center);
```
### 新 API 自动切换布局模式
所有新布局 setter 都会自动将 `layoutMode` 切换为 `AnchorToEdges`
因此新代码通常不需要再额外写:
```cpp
control->setLayoutMode(StellarX::LayoutMode::AnchorToEdges);
```
## 常见布局写法
### 左固定
```cpp
control->setHorizontalAnchors(true, false);
control->setHorizontalSizePolicy(StellarX::AxisSizePolicy::FixedSize);
control->setHorizontalAlignPolicy(StellarX::AxisAlignPolicy::Start);
```
### 右固定
```cpp
control->setHorizontalAnchors(false, true);
control->setHorizontalSizePolicy(StellarX::AxisSizePolicy::FixedSize);
control->setHorizontalAlignPolicy(StellarX::AxisAlignPolicy::End);
```
### 水平拉伸
```cpp
control->setHorizontalAnchors(true, true);
control->setHorizontalSizePolicy(StellarX::AxisSizePolicy::Stretch);
control->setHorizontalAlignPolicy(StellarX::AxisAlignPolicy::Start);
```
### 固定尺寸居中
```cpp
control->setHorizontalAnchors(true, true);
control->setHorizontalSizePolicy(StellarX::AxisSizePolicy::FixedSize);
control->setHorizontalAlignPolicy(StellarX::AxisAlignPolicy::Center);
```
### 固定尺寸比例位移
```cpp
control->setHorizontalAnchors(true, true);
control->setHorizontalSizePolicy(StellarX::AxisSizePolicy::FixedSize);
control->setHorizontalAlignPolicy(StellarX::AxisAlignPolicy::Proportional);
```
适用场景:
- 一组固定尺寸按钮需要随着父容器变宽按比例展开。
- `KEY2` 中每个 bit 单元就是该策略的典型使用方式。
## KEY2 迁移要点
`KEY2` 原问题不是单纯参数错误,而是布局单元拆分不合理:
- 位号 `Label` 是内容驱动尺寸。
- 位按钮是固定尺寸。
- 二者分别使用比例位移时,因为自身尺寸不同,resize 后会出现错位。
当前迁移方式:
- 每一位创建一个小 `Canvas` 单元。
- 单元内部固定放置位号 `Label` 与位按钮。
- 单元自身使用 `FixedSize + Proportional`
- 这样移动的是整个 bit 单元,而不是标签和按钮分别移动。
示意写法:
```cpp
auto cell = std::make_unique<Canvas>(cellX, 0, 32, 50);
cell->setHorizontalAnchors(true, true);
cell->setHorizontalSizePolicy(StellarX::AxisSizePolicy::FixedSize);
cell->setHorizontalAlignPolicy(StellarX::AxisAlignPolicy::Proportional);
```
## 能力边界说明
布局 API 表达的是“调用方希望如何布局”,但控件能力边界仍然优先。
当前关键边界:
- `Canvas``X / Y` 均允许 Stretch。
- `TabControl`:外层允许 Stretch,内部页签栏 / 页面区仍由自身管理。
- `Table`:当前版本 `X Stretch / Y Fixed`,这是当前实现边界,不是永久产品结论。
- `TextBox`:默认更适合 `X Stretch / Y FixedSize`
- `Label`:默认固定或内容驱动刷新运行态尺寸,不建议做双轴 Stretch。
- `Dialog`:只居中,不参与父级拉伸。
如果用户设置的策略超出控件能力边界,统一解算层会按现有降级规则处理,并保留必要日志。
## 调试态说明
resize / 最大化 / 还原过程中,如果开启大量 console 日志,可能出现一帧视觉延迟或短暂残影。
当前判断原因是日志系统同步写控制台且默认逐行 flush,导致主线程 resize 收口绘制被 I/O 拖慢。
处理建议:
- 常规手测可关闭 console 日志。
- 需要日志时可优先输出到文件。
- 当前不作为布局或重绘 bug 处理,后续可在官网/API 文档中作为调试说明记录。
@@ -0,0 +1,161 @@
# 新增功能模块 / 模块重构
> 适用场景:新增模块、重大模块重构、核心架构能力演进。
> 不适用场景:小接口或轻量功能变更,请使用“功能变更”模板。
## 基本信息
- 模块 ID`Module-20260410-0002`
- 模块名称: 锚点与布局系统第一阶段重构
- 状态:已验证
- 类型:模块重构
- 所属系统 / 子系统: GUI 框架 / Layout
- 版本 / 分支: 当前工作区 / 下一版本开发中
- 环境: Windows + EasyX
- 负责人: Codex 协作修改
## 背景与目标
- 背景:
- 原有锚点系统仍依赖 `anchor_1 / anchor_2`
- `Window::adaptiveLayout()``Canvas::onWindowResize()` 长期并存两套布局语义
- `Canvas` 布局层存在 `Table` 外部特判
- `TabControl` 外层布局与内部布局耦合较深
- 当前痛点:
- 双锚点表达能力不足,难以覆盖更完整的边集合语义
- 顶层窗口与容器子控件的解算规则不统一,维护成本高
- 特殊控件能力边界没有收回自身语义层
- 缺少针对布局系统的专项回归用例
- 目标:
- 建立统一的布局数据模型与统一解算入口
- 正式区分设计态矩形与运行态矩形
- 保留旧 API 的兼容输入能力
-`Table` 的当前能力边界收回控件自身
- 增加布局专项回归用例 `KEY == 5`
- 非目标:
- 不做字体随控件缩放
- 不做 `Table` 纵向拉伸
- 不重构重绘系统
## 模块边界
- 职责:
- 提供统一的布局规格描述
- 统一顶层窗口与容器子控件的几何解算
- 在保持旧 API 可用的前提下,将内部布局实现迁移到新模型
- 通过控件能力边界约束非法或暂不支持的拉伸组合
- 不负责什么:
- 字体缩放与内容排版自适应
- `Dialog` 内部布局语义重构
- `Table` 纵向拉伸能力
- 外部依赖:
- EasyX 绘制环境
- 现有 `Control / Window / Canvas / TabControl / Table` 控件体系
- 对外能力 / API:
- 继续保留 `setLayoutMode(...)`
- 继续保留 `setAnchor(a1, a2)`
- 当前阶段新增能力主要用于内部统一实现,不额外新增用户层 API
- 关键数据 / 状态:
- `localx / localy / localWidth / localHeight`
- `x / y / width / height`
- `LayoutSpec`
- `LayoutCapability`
- `ResolvedLayoutRect`
## 设计说明
- 核心流程:
- 先在父局部坐标系内按水平轴 / 垂直轴独立解算
- 再将局部矩形映射为世界坐标矩形
- 由控件内部受控路径应用运行态矩形
- 关键对象 / 类关系:
- [`Control`](D:/programming/imGUI-easyX/imGui-easyX/Control.h) 作为统一布局规格与基础解算入口
- [`Window`](D:/programming/imGUI-easyX/imGui-easyX/Window.cpp) 负责顶层控件统一收口
- [`Canvas`](D:/programming/imGUI-easyX/imGui-easyX/Canvas.cpp) 负责容器子控件重映射
- [`TabControl`](D:/programming/imGUI-easyX/imGui-easyX/TabControl.cpp) 外层接入统一解算,内部页签栏 / 页面区仍自管
- [`Table`](D:/programming/imGUI-easyX/imGui-easyX/Table.cpp) 通过 `LayoutCapability` 显式禁止 `Y` 轴 Stretch
- 生命周期:
- 设计态矩形 `local*` 在普通 resize / 重排过程中不自动回写
- 运行态矩形由统一解算器产出,再通过内部路径应用
- 若确需同步设计基线,只能显式调用 `commitCurrentGeometryAsDesignRect()`
- 事件 / 渲染 / 数据流:
- 事件阶段只改状态,不直接扩散成多套布局公式
- `Window``Canvas` 共用 `resolveLayoutRect()`
- `onWindowResize()` 收口为“快照失效 + 标脏 + 必要传播”,不再承担布局求解
- 关键不变量:
- `local*` 始终表示设计态父局部矩形
- `x / y / width / height` 始终表示运行态绘制矩形
- 旧 API 只作兼容输入层,不再作为内部解算依据
- `Table` 当前阶段只允许 `X` 轴 Stretch
- 降级 / 回退策略:
- 对不满足能力边界的拉伸请求,自然降级为固定尺寸位移
- 旧接口输入通过映射层退回到新模型的有限子集
## 实现与影响
- 关键实现点:
- 引入 `AxisSizePolicy / AxisAlignPolicy / AxisLayoutSpec / LayoutSpec / LayoutCapability / ResolvedLayoutRect`
-`Control` 中增加统一解算与内部受控应用路径
-`Window::adaptiveLayout()` 改为统一解算入口
-`Canvas` 子控件布局从旧比例缩放逻辑切换为统一解算
-`TabControl` 外层接入统一解算,同时保留内部页签系统专用布局
-`Table``Y` 轴固定能力边界收回控件自身
-`z-testDome.cpp` 增加 `KEY == 5` 布局专项回归
- 涉及文件 / 类 / 函数:
- [`CoreTypes.h`](D:/programming/imGUI-easyX/imGui-easyX/CoreTypes.h)
- [`Control.h`](D:/programming/imGUI-easyX/imGui-easyX/Control.h)
- [`Control.cpp`](D:/programming/imGUI-easyX/imGui-easyX/Control.cpp)
- [`Canvas.h`](D:/programming/imGUI-easyX/imGui-easyX/Canvas.h)
- [`Canvas.cpp`](D:/programming/imGUI-easyX/imGui-easyX/Canvas.cpp)
- [`Window.cpp`](D:/programming/imGUI-easyX/imGui-easyX/Window.cpp)
- [`TabControl.h`](D:/programming/imGUI-easyX/imGui-easyX/TabControl.h)
- [`TabControl.cpp`](D:/programming/imGUI-easyX/imGui-easyX/TabControl.cpp)
- [`Table.h`](D:/programming/imGUI-easyX/imGui-easyX/Table.h)
- [`Table.cpp`](D:/programming/imGUI-easyX/imGui-easyX/Table.cpp)
- [`z-testDome.cpp`](D:/programming/imGUI-easyX/imGui-easyX/z-testDome.cpp)
- 兼容性影响:
- 向后兼容旧锚点 API
- 当前阶段未删除旧 getter / setter
- 性能影响:
- 无明显新增性能负担
- 主要是布局求解路径从多处分散逻辑收口为统一函数
- 风险点:
- 若某些控件运行态尺寸变化后未明确同步设计基线,后续 resize 仍可能出现“回到旧设计态”的现象
- `TabControl` 的外层统一解算与内部专用布局之间存在边界风险
- `Table` 纵向仍为固定尺寸,后续若扩展能力需单独立项
## 测试与验证
- 测试范围:
- 顶层窗口 resize
- `Canvas` 嵌套布局
- `TabControl` 外层布局接入
- `Table` 横向拉伸与纵向固定
- `KEY == 5` 布局专项回归
- 验证步骤:
1. 编译 `Control.cpp / Canvas.cpp / Table.cpp / TabControl.cpp / Window.cpp`
2. 编译 `z-testDome.cpp /DKEY=2`
3. 编译 `z-testDome.cpp /DKEY=5`
- 验证结果:
- 源码级编译验证通过
- `KEY == 5` 已补齐布局专项回归用例
- GUI 交互仍需用户本机手动确认
- 已知限制 / 遗留问题:
- 本轮不包含字体缩放
- 本轮不包含 `Table` 纵向拉伸
- Tooltip 问题已另外拆分为独立 `BUG / Fix`
## 落地信息
- 关联功能变更 ID[可选]
- 关联 BUG / Fix:
- `BUG-20260410-0004`
- `Fix-BUG-20260410-0004`
- Commit: 当前工作区未提交
- PR[可选]
- 发布版本:[可选]
- 相关文档:
- [`BUG-20260410-0004-按钮Tooltip移出后不消失.md`](D:/programming/imGUI-easyX/imGui-easyX/开发记录/BUG/BUG-20260410-0004-按钮Tooltip移出后不消失.md)
- [`Fix-BUG-20260410-0004-按钮Tooltip移出后不消失.md`](D:/programming/imGUI-easyX/imGui-easyX/开发记录/Fix/Fix-BUG-20260410-0004-按钮Tooltip移出后不消失.md)
@@ -0,0 +1,157 @@
# 新增功能模块 / 模块重构
> 适用场景:新增模块、重大模块重构、核心架构能力演进。
> 不适用场景:小接口或轻量功能变更,请使用“功能变更”模板。
## 基本信息
- 模块 IDModule-20260415-0003
- 模块名称: 布局系统第二阶段收口
- 状态:已完成
- 类型:模块重构
- 所属系统 / 子系统: GUI 框架 / Layout
- 版本 / 分支: 当前工作区 / 下一版本开发中
- 环境: Windows + EasyX
- 负责人: Codex 协作修改
## 背景与目标
- 背景:
- 第一阶段已经完成统一布局链路打通,但几何写入口、内容驱动控件规则、复合控件职责边界仍有残留混用。
- `Label` 曾在 `draw()` 阶段临时决定尺寸,`TabControl``Canvas` 在子树几何映射上也存在职责重叠。
- `WM_MOUSEMOVE` 的容器分发和局部重绘合成,在复杂遮挡场景下容易暴露 hover/tooltip 与 overlay 残留问题。
- 当前痛点:
- 几何语义还不够制度化,后续继续演进布局系统时容易再次混回“draw 阶段改几何”。
- 内容驱动控件和复合控件没有完全落成可解释的边界。
- 局部重绘与上层兄弟补画机制不完整时,会直接破坏遮挡正确性。
- 目标:
- 收口几何写入口语义,明确公开 setter、统一布局应用路径、内容驱动路径、显式设计基线提交。
- 收口 `Label / Table` 的内容驱动规则与设计基线边界。
- 收口 `TabControl / Canvas` 的职责边界,不重写页签系统,但消除页内子控件手工回填。
- 建立轻量级鼠标瞬时状态清理路径和 overlay 补画机制,保证 hover / tooltip / 局部重绘链正确。
- 非目标:[可选]
- 不做字体缩放。
- 不做 `Table` 纵向拉伸。
- 不做 `Dialog` 旧 synthetic move 机制统一。
- 不做 `Table` 内部局部重绘体系。
## 模块边界
- 职责:
- 定义并收口布局系统第二阶段的运行态 / 设计态几何语义。
- 为当前主线控件显式写出能力边界和默认策略。
- 收口局部重绘下的 overlay 补画规则。
- 不负责什么:
- 不扩展新的布局表达能力。
- 不处理字体、图标、DPI 自适应。
- 不把所有控件都改造成内容驱动或局部重绘型复合控件。
- 外部依赖:
- EasyX 绘制环境
- 现有 `Control / Window / Canvas / TabControl / Table / Label / Button` 体系
- 对外能力 / API:
- 保留 `setLayoutMode(...)`
- 保留 `setAnchor(a1, a2)`
- 保留 `commitCurrentGeometryAsDesignRect()`
- `Label::textStyle` 继续保持公开,但要求样式修改后显式 `setDirty(true)`
- 关键数据 / 状态:
- `localx / localy / localWidth / localHeight`
- `x / y / width / height`
- `LayoutSpec / LayoutCapability / ResolvedLayoutRect`
- `eventVisualChanged`
## 设计说明
- 核心流程:
- 先在父局部坐标系内完成统一布局解算。
- 再由内部受控路径把运行态矩形应用到控件。
- 内容驱动控件在自己的受控路径里刷新运行态尺寸。
- 局部重绘提交后,由父容器按实际绘制顺序补画 coverage 上方的 overlay 兄弟。
- 关键对象 / 类关系:
- [`Control`](D:/programming/imGUI-easyX/imGui-easyX/Control.h):几何语义、解算入口、设计基线提交入口。
- [`Label`](D:/programming/imGUI-easyX/imGui-easyX/Label.cpp):内容驱动尺寸,`draw()` 只消费运行态矩形。
- [`Canvas`](D:/programming/imGUI-easyX/imGui-easyX/Canvas.cpp):管理直接子控件的世界坐标映射与局部 overlay 补画。
- [`TabControl`](D:/programming/imGUI-easyX/imGui-easyX/TabControl.cpp):外层接统一解算,内部页签栏 / 页面区继续自管。
- [`Table`](D:/programming/imGUI-easyX/imGui-easyX/Table.cpp):当前版本 `X Stretch / Y Fixed`,并保留内部受控结构尺寸基线刷新。
- [`Window`](D:/programming/imGUI-easyX/imGui-easyX/Window.cpp):顶层托管重绘收口与 overlay 兄弟补画。
- 生命周期:
- 普通 resize / 父容器重排不自动回写 `local*`
- 显式设计基线提交只通过 `commitCurrentGeometryAsDesignRect()` 或控件内部受控结构刷新点发生。
- 内容驱动尺寸刷新优先发生在 `draw()` 之前。
- 事件 / 渲染 / 数据流:[按模块类型填写]
- `WM_MOUSEMOVE`:第一个命中的兄弟收到真实消息,后续兄弟只清理瞬时鼠标状态。
- 局部重绘:先画本次 dirty 单元,再补画 coverage 上方相交 overlay。
- 运行态几何:统一解算或内容驱动路径写入;设计基线不自动漂移。
- 关键不变量:
- `local*` 始终表示设计态父局部矩形。
- `x / y / width / height` 始终表示运行态绘制矩形。
- `draw()` 不再承担新的几何决策入口。
- `Table` 当前版本 `Y Fixed` 是实现边界,不是永久产品结论。
- 降级 / 回退策略:[可选]
- Stretch 条件不满足时自然降级为固定尺寸位置策略。
- 控件能力边界禁止 Stretch 时,通过日志输出拦截原因。
## 实现与影响
- 关键实现点:
- 在 [`Control.cpp`](D:/programming/imGUI-easyX/imGui-easyX/Control.cpp) 增加布局降级日志、能力边界拦截日志、显式设计基线提交日志。
- 在 [`Label.cpp`](D:/programming/imGUI-easyX/imGui-easyX/Label.cpp) 收口内容驱动尺寸刷新,并显式关闭双轴 Stretch。
- 在 [`Canvas.cpp`](D:/programming/imGUI-easyX/imGui-easyX/Canvas.cpp)、[`TabControl.cpp`](D:/programming/imGUI-easyX/imGui-easyX/TabControl.cpp)、[`Window.cpp`](D:/programming/imGUI-easyX/imGui-easyX/Window.cpp) 收口 overlay 补画。
- 在 [`Table.cpp`](D:/programming/imGUI-easyX/imGui-easyX/Table.cpp) 收口 `Y Fixed`、分页按钮视觉链与页码重绘。
- 涉及文件 / 类 / 函数:
- [`Control.h`](D:/programming/imGUI-easyX/imGui-easyX/Control.h)
- [`Control.cpp`](D:/programming/imGUI-easyX/imGui-easyX/Control.cpp)
- [`Label.h`](D:/programming/imGUI-easyX/imGui-easyX/Label.h)
- [`Label.cpp`](D:/programming/imGUI-easyX/imGui-easyX/Label.cpp)
- [`Canvas.cpp`](D:/programming/imGUI-easyX/imGui-easyX/Canvas.cpp)
- [`TabControl.cpp`](D:/programming/imGUI-easyX/imGui-easyX/TabControl.cpp)
- [`Table.cpp`](D:/programming/imGUI-easyX/imGui-easyX/Table.cpp)
- [`TextBox.cpp`](D:/programming/imGUI-easyX/imGui-easyX/TextBox.cpp)
- [`Dialog.cpp`](D:/programming/imGUI-easyX/imGui-easyX/Dialog.cpp)
- [`Window.cpp`](D:/programming/imGUI-easyX/imGui-easyX/Window.cpp)
- 兼容性影响:
- 对旧锚点 API 保持兼容。
- `Label::textStyle` 仍为公开字段,但使用约定更严格。
- 性能影响:
- `WM_MOUSEMOVE` 和 overlay 补画路径增加了必要的状态清理与补画,但避免了整窗级重绘。
- `Table` 分页按钮视觉变化当前仍提升为整表重绘,颗粒度偏粗,但正确性优先。
- 风险点:
- `Dialog` 旧 synthetic move 机制仍与新清理模型并存。
- `Table` 内部局部重绘体系尚未建立,分页区仍偏重。
## 测试与验证
- 测试范围:
- 顶层 resize
- 三层 `Canvas` 嵌套
- `TabControl` 外层 resize 与页内稳定性
- overlay 补画
- `Table` 横向拉伸、分页按钮、页码重绘
- `Label` 文本 / 字体样式变化
- 验证步骤:
1. 编译 `Control.cpp / Label.cpp / Canvas.cpp / TabControl.cpp / Table.cpp / TextBox.cpp / Dialog.cpp`
2. 编译 `z-testDome.cpp /DKEY=5`
3. 手动回归 `KEY5` 的 hover、tooltip、overlay、分页与页码场景
- 验证结果:
- 编译级验证通过。
- 手动 GUI 回归依赖本机继续执行。
- 已知限制 / 遗留问题:[可选]
- `Dialog` 旧 synthetic move 机制暂未统一。
- `Table` 尚未引入内部局部重绘模型。
- 全项目所有控件的能力边界总审计未做。
## 落地信息
- 关联功能变更 ID[可选]
- `Feature-20260415-0008`
- 关联 BUG / Fix[可选]
- `BUG-20260415-0005`
- `Fix-BUG-20260415-0005`
- Commit: 未提交(当前工作区)
- PR[可选]
- 发布版本:[可选]
- 相关文档:[可选]
- [Feature-20260415-0008-KEY5-第二阶段专项回归场景增强.md](D:/programming/imGUI-easyX/imGui-easyX/开发记录/功能变更/Feature-20260415-0008-KEY5-第二阶段专项回归场景增强.md)
- [BUG-20260415-0005-局部重绘未补画上层兄弟导致遮挡错误.md](D:/programming/imGUI-easyX/imGui-easyX/开发记录/BUG/BUG-20260415-0005-局部重绘未补画上层兄弟导致遮挡错误.md)
- [Fix-BUG-20260415-0005-局部重绘补画上层兄弟修复.md](D:/programming/imGUI-easyX/imGui-easyX/开发记录/Fix/Fix-BUG-20260415-0005-局部重绘补画上层兄弟修复.md)
@@ -0,0 +1,177 @@
# 新增功能模块 / 模块重构
> 适用场景:新增模块、重大模块重构、核心架构能力演进。
> 不适用场景:小接口或轻量功能变更,请使用“功能变更”模板。
## 基本信息
- 模块 IDModule-20260415-0004
- 模块名称: 布局系统第二阶段验收与封口
- 状态:已验证
- 类型:模块重构
- 所属系统 / 子系统: GUI 框架 / Layout
- 版本 / 分支: 当前工作区 / 下一版本开发中
- 环境: Windows + EasyX
- 负责人: Codex 协作修改
## 背景与目标
- 背景:
- 第一阶段已完成统一锚点模型和统一解算入口。
- 第二阶段继续围绕“几何所有权收口 + 内容驱动规则收口”推进,实现几何写入口分层、内容驱动控件规则收口、局部重绘与 overlay 补画链修复。
-`KEY1 / KEY5` 回归过程中,又暴露出脏子树提交、coverage 低估、TabControl 层级顺序等一组重绘链问题,需要一并收口后才能视为阶段稳定。
- 当前痛点:
- 缺少一份明确的阶段验收记录,难以区分“本阶段完成项”和“明确延期项”。
- 当前主线 bug 虽已基于测试用例修复,但如果不做封口记录,后续继续推进下一主题时容易重复回头整理。
- 目标:
- 明确第二阶段已经完成的主线改造和已验证的 bug 修复。
- 明确当前接受的边界与明确延期的技术债。
- 为后续“公开布局 API + 旧 demo 迁移”提供稳定起点。
- 非目标:[可选]
- 本记录不新增运行时代码逻辑。
- 本记录不覆盖未来的 Tooltip 智能选位、`Table` 纵向拉伸、`Dialog` synthetic move 统一改造。
## 模块边界
- 职责:
- 汇总布局系统第二阶段的最终语义收口结果。
- 记录本阶段已完成的运行态 / 设计态几何规则、局部重绘提交规则与 overlay 补画规则。
- 明确已知延期项与后续主题边界。
- 不负责什么:
- 不扩展新的布局表达能力。
- 不重写 `Dialog` 旧 synthetic move 机制。
- 不实现 `Table` 内部局部重绘体系。
- 外部依赖:
- `Control / Window / Canvas / TabControl / Table / Label / Button`
- EasyX 绘制与消息循环环境
- 对外能力 / API:
- 保持旧 API`setLayoutMode(...)``setAnchor(...)`
- 保持显式设计基线提交入口:`commitCurrentGeometryAsDesignRect()`
- 保持 `Label::textStyle` 公开,但要求样式修改后手动 `setDirty(true)`
- 关键数据 / 状态:
- `localx / localy / localWidth / localHeight`
- `x / y / width / height`
- `LayoutSpec / LayoutCapability / ResolvedLayoutRect`
- `eventVisualChanged / dirty / coverage / overlay`
## 设计说明
- 核心流程:
- 几何变化先在父局部坐标系内统一解算,再通过内部受控路径应用到运行态矩形。
- 内容驱动控件在自身受控路径中刷新运行态尺寸;设计基线不得在普通布局过程中自动漂移。
- 托管局部重绘按“脏子树提交 -> 直接分支 coverage -> 传递式 overlay 补画”收口,确保嵌套容器、Tooltip、上层兄弟遮挡链闭合。
- 关键对象 / 类关系:
- [`Control.h`](D:/programming/imGUI-easyX/imGui-easyX/Control.h):统一布局解算、设计基线提交、托管重绘底座
- [`Label.cpp`](D:/programming/imGUI-easyX/imGui-easyX/Label.cpp):内容驱动尺寸刷新前移,`draw()` 只消费运行态矩形
- [`Canvas.cpp`](D:/programming/imGUI-easyX/imGui-easyX/Canvas.cpp):直接子树映射、脏子树提交、局部 overlay 补画
- [`TabControl.cpp`](D:/programming/imGUI-easyX/imGui-easyX/TabControl.cpp):外层统一布局,内部页签栏/页面区自管,绘制顺序与局部提交顺序统一
- [`Table.cpp`](D:/programming/imGUI-easyX/imGui-easyX/Table.cpp):当前版本 `X Stretch / Y Fixed`、分页按钮视觉与页码重绘修复
- [`Window.cpp`](D:/programming/imGUI-easyX/imGui-easyX/Window.cpp):顶层托管重绘、传递式 overlay 补画、顶层 coverage 收口
- 生命周期:
- 普通 resize / 重排不自动回写 `local*`
- 公开 setter 只写运行态,不隐式提交设计基线
- 显式设计基线提交只通过 `commitCurrentGeometryAsDesignRect()` 或控件内部受控结构刷新触发
- 事件 / 渲染 / 数据流:[按模块类型填写]
- `WM_MOUSEMOVE`:真实命中分支处理事件,后续兄弟仅清理鼠标瞬时状态
- 托管局部重绘:先提交 dirty root / dirty branch,再按 coverage 传递式补画 overlay
- Tooltip / 扩展绘制 coverage:通过 `getManagedRepaintCoverageRect()` 纳入托管 coverage 计算
- 关键不变量:
- `local*` 只表示设计态父局部矩形
- `x / y / width / height` 只表示运行态绘制矩形
- `draw()` 不再承担新的几何决策入口
- `Table` 当前版本 `Y Fixed` 是实现边界,不是永久产品结论
- 降级 / 回退策略:[可选]
- Stretch 不满足条件时降级为固定尺寸位移策略,并输出最小必要日志
- 控件能力边界拦截 Stretch 请求时,通过日志说明被拦截的轴和原因
## 实现与影响
- 关键实现点:
- 收口公开 setter、统一布局应用路径、内容驱动路径、显式设计基线提交四类几何写入口
- 修复 `WM_MOUSEMOVE` 短路后 hover / tooltip 无法及时清理的问题
- 修复局部重绘只认直接 dirty child、不认 dirty descendant 的链路缺口
- 修复 coverage 低估导致 Tooltip / overlay 漏补画的问题
- 修复 Tab 页签按钮与页面绘制顺序不一致导致 Tooltip 被页面覆盖的问题
- 修复重复激活已激活页签时的残影 / 快照链扰动问题
- 涉及文件 / 类 / 函数:
- [`Control.h`](D:/programming/imGUI-easyX/imGui-easyX/Control.h)
- [`Control.cpp`](D:/programming/imGUI-easyX/imGui-easyX/Control.cpp)
- [`Label.h`](D:/programming/imGUI-easyX/imGui-easyX/Label.h)
- [`Label.cpp`](D:/programming/imGUI-easyX/imGui-easyX/Label.cpp)
- [`Button.h`](D:/programming/imGUI-easyX/imGui-easyX/Button.h)
- [`Button.cpp`](D:/programming/imGUI-easyX/imGui-easyX/Button.cpp)
- [`Canvas.h`](D:/programming/imGUI-easyX/imGui-easyX/Canvas.h)
- [`Canvas.cpp`](D:/programming/imGUI-easyX/imGui-easyX/Canvas.cpp)
- [`TabControl.h`](D:/programming/imGUI-easyX/imGui-easyX/TabControl.h)
- [`TabControl.cpp`](D:/programming/imGUI-easyX/imGui-easyX/TabControl.cpp)
- [`Table.h`](D:/programming/imGUI-easyX/imGui-easyX/Table.h)
- [`Table.cpp`](D:/programming/imGUI-easyX/imGui-easyX/Table.cpp)
- [`TextBox.h`](D:/programming/imGUI-easyX/imGui-easyX/TextBox.h)
- [`TextBox.cpp`](D:/programming/imGUI-easyX/imGui-easyX/TextBox.cpp)
- [`Dialog.h`](D:/programming/imGUI-easyX/imGui-easyX/Dialog.h)
- [`Dialog.cpp`](D:/programming/imGUI-easyX/imGui-easyX/Dialog.cpp)
- [`Window.h`](D:/programming/imGUI-easyX/imGui-easyX/Window.h)
- [`Window.cpp`](D:/programming/imGUI-easyX/imGui-easyX/Window.cpp)
- [`MessageBox.h`](D:/programming/imGUI-easyX/imGui-easyX/MessageBox.h)
- [`z-testDome.cpp`](D:/programming/imGUI-easyX/imGui-easyX/z-testDome.cpp)
- 兼容性影响:
- 旧锚点 API 仍可用,内部实现已完全转到新布局模型
- `Label::textStyle` 仍为公开字段,但要求样式修改后显式 `setDirty(true)`
- `TabControl::setActiveIndex()``Button::setButtonClick()` 对重复同状态调用新增短路,不再重复触发链路
- 性能影响:
- 局部重绘 coverage 和 overlay 补画会更保守,补画次数可能略增
- 相比整窗 / 整容器强制重绘,这仍是更合理的正确性与性能折中
- `Table` 分页按钮视觉变化当前仍提升为整张 `Table` 重绘,颗粒度偏粗但正确性优先
- 风险点:
- `Dialog` 旧 synthetic move 机制仍与新清理模型并存
- `Table` 尚未拥有自己的内部局部重绘体系
- 公开 `AxisSizePolicy / AxisAlignPolicy` API 仍未开放,旧 demo 仍受旧入口表达能力限制
## 测试与验证
- 测试范围:
- `KEY1`:页签重复激活、表格超出页范围残影回归
- `KEY5`:三层 `Canvas` 嵌套、跨容器 hover / tooltip、overlay 补画、`TabControl``Table`、页码与分页按钮
- 核心源码编译级验证:布局主线、重绘主线、Tab / Table / Label / TextBox / Dialog 主线
- 验证步骤:
1. 编译 `Control.cpp / Button.cpp / Label.cpp / Canvas.cpp / TabControl.cpp / Table.cpp / TextBox.cpp / Dialog.cpp / Window.cpp`
2. 编译 `z-testDome.cpp /DKEY=1`
3. 编译 `z-testDome.cpp /DKEY=5`
4. 手动回归 `KEY1 / KEY5` 中的 tooltip、overlay、页签、分页、三层嵌套和跨容器按钮场景
- 验证结果:
- 编译级验证通过
- 基于当前 `KEY1 / KEY5` 用例回归,已知 bug 已修复
- GUI 手动回归依赖本机继续确认,当前结论基于现有测试反馈成立
- 已知限制 / 遗留问题:[可选]
- `Dialog` 旧 synthetic `WM_MOUSEMOVE` 机制尚未统一到新模型
- `Table` 内部局部重绘体系尚未实现
- Tooltip 智能选位明确后置
- 公开 `AxisSizePolicy / AxisAlignPolicy` API 尚未开放,`KEY2` 等旧场景仍受旧 anchor 语义限制
## 落地信息
- 关联功能变更 ID[可选]
- `Feature-20260415-0008`
- 关联 BUG / Fix[可选]
- `BUG-20260415-0005`
- `Fix-BUG-20260415-0005`
- `BUG-20260415-0006`
- `Fix-BUG-20260415-0006`
- `BUG-20260415-0007`
- `Fix-BUG-20260415-0007`
- `BUG-20260415-0008`
- `Fix-BUG-20260415-0008`
- Commit: 未提交(当前工作区)
- PR[可选]
- 发布版本:[可选]
- 相关文档:[可选]
- [Module-20260415-0003-布局系统第二阶段收口.md](D:/programming/imGUI-easyX/imGui-easyX/开发记录/模块/Module-20260415-0003-布局系统第二阶段收口.md)
- [Feature-20260415-0008-KEY5-第二阶段专项回归场景增强.md](D:/programming/imGUI-easyX/imGui-easyX/开发记录/功能变更/Feature-20260415-0008-KEY5-第二阶段专项回归场景增强.md)
- [BUG-20260415-0006-托管局部重绘未正确提交脏子树导致嵌套Canvas按钮状态不刷新.md](D:/programming/imGUI-easyX/imGui-easyX/开发记录/BUG/BUG-20260415-0006-托管局部重绘未正确提交脏子树导致嵌套Canvas按钮状态不刷新.md)
- [Fix-BUG-20260415-0006-托管局部重绘脏子树提交链修复.md](D:/programming/imGUI-easyX/imGui-easyX/开发记录/Fix/Fix-BUG-20260415-0006-托管局部重绘脏子树提交链修复.md)
- [BUG-20260415-0007-实际绘制coverage低估导致Tooltip与overlay补画漏算.md](D:/programming/imGUI-easyX/imGui-easyX/开发记录/BUG/BUG-20260415-0007-实际绘制coverage低估导致Tooltip与overlay补画漏算.md)
- [Fix-BUG-20260415-0007-实际绘制coverage与overlay补画链修复.md](D:/programming/imGUI-easyX/imGui-easyX/开发记录/Fix/Fix-BUG-20260415-0007-实际绘制coverage与overlay补画链修复.md)
- [BUG-20260415-0008-TabControl页签层级与重复激活链路导致Tooltip和残影异常.md](D:/programming/imGUI-easyX/imGui-easyX/开发记录/BUG/BUG-20260415-0008-TabControl页签层级与重复激活链路导致Tooltip和残影异常.md)
- [Fix-BUG-20260415-0008-TabControl页签层级与重复激活链路修复.md](D:/programming/imGUI-easyX/imGui-easyX/开发记录/Fix/Fix-BUG-20260415-0008-TabControl页签层级与重复激活链路修复.md)
@@ -0,0 +1,146 @@
# 新增功能模块 / 模块重构
> 适用场景:新增模块、重大模块重构、核心架构能力演进。
> 不适用场景:小接口或轻量功能变更,请使用“功能变更”模板。
## 基本信息
- 模块 IDModule-20260416-0005
- 模块名称: 布局策略公开 API 落地
- 状态:开发中
- 类型:架构演进
- 所属系统 / 子系统: GUI 框架 / Layout API
- 版本 / 分支: 当前工作区 / 下一开发阶段
- 环境: Windows + EasyX
- 负责人: Codex 协作修改
## 背景与目标
- 背景:
- 第一阶段与第二阶段已经把内部布局模型收口到 `LayoutSpec / LayoutCapability / AxisSizePolicy / AxisAlignPolicy`
- 外部调用层仍主要依赖 `setLayoutMode(...)``setAnchor(...)`,无法直接表达“固定尺寸 + 比例位移”“固定尺寸 + 居中”这类语义。
- `KEY2` 顶部位选择区就是典型例子:旧双锚点语义不足,导致按钮与标签 resize 后错位,且难以继续扩展。
- 当前痛点:
- 外部用户无法直接设置 `AxisSizePolicy / AxisAlignPolicy`
- 旧 demo 继续依赖旧锚点入口,验证成本高,迁移路径不清晰。
- 新旧 API 混用规则若不写清,后续容易再次回到“旧 anchor 硬凑新布局”的状态。
- 目标:
- 将当前内部布局策略正式开放为公开 API。
- 明确新旧 API 混用规则与对外可见语义。
-`KEY2` 作为新布局 API 的首个迁移样例。
- 非目标:[可选]
- 不统一 `Dialog` 旧 synthetic `WM_MOUSEMOVE` 机制。
- 不实现 `Table` 内部局部重绘体系。
- 不实现 Tooltip 智能选位。
- 不扩展 `Table` 纵向拉伸和字体缩放。
## 模块边界
- 职责:
- 对外开放按轴设置布局规格的最小 API。
- 固化新旧 API 的混用规则与默认行为。
- 提供首个迁移样例,验证新 API 可表达旧场景中“旧 anchor 无法清晰表达”的布局。
- 不负责什么:
- 不开放 `LayoutCapability` 的通用外部修改接口。
- 不改变控件内部硬能力边界。
- 不把所有旧 demo 一次性全部迁完。
- 外部依赖:
- `Control / Canvas / TabControl / Table / Label / TextBox`
- `z-testDome.cpp`
- 对外能力 / API:
- `setHorizontalLayoutSpec(...)`
- `setVerticalLayoutSpec(...)`
- `setHorizontalAnchors(...)`
- `setVerticalAnchors(...)`
- `setHorizontalSizePolicy(...)`
- `setVerticalSizePolicy(...)`
- `setHorizontalAlignPolicy(...)`
- `setVerticalAlignPolicy(...)`
- `getHorizontalLayoutSpec()`
- `getVerticalLayoutSpec()`
- 关键数据 / 状态:
- `layoutSpec`
- `layoutMode`
- `layoutCapability`
- `localx / localy / localWidth / localHeight`
- `x / y / width / height`
## 设计说明
- 核心流程:
- 外部通过新 API 直接写入水平轴 / 垂直轴布局规格。
- 新 API 被调用后,控件自动切到 `AnchorToEdges` 布局模式。
- 运行时统一解算继续以 `layoutSpec` 为唯一依据。
- 关键对象 / 类关系:
- [`Control.h`](D:/programming/imGUI-easyX/imGui-easyX/Control.h):公开布局策略 API 的对外入口。
- [`Control.cpp`](D:/programming/imGUI-easyX/imGui-easyX/Control.cpp):新 API 的实现、新旧 API 映射共存规则。
- [`z-testDome.cpp`](D:/programming/imGUI-easyX/imGui-easyX/z-testDome.cpp)`KEY2` 迁移示例与验证入口。
- 生命周期:
- 新 API 只影响运行态布局策略,不自动提交设计基线。
- 设计基线仍由显式提交路径或控件内部受控路径维护。
- 事件 / 渲染 / 数据流:[按模块类型填写]
- 新 API 不改变事件分发与局部重绘主链。
- 只是改变布局规格输入层与解算参数来源。
- 关键不变量:
- 新旧 API 混用时,后调用者生效。
- 旧 API 仍保留兼容,但只覆盖新模型的有限子集。
- 公开布局 API 不得自动回写 `local*`
- 降级 / 回退策略:[可选]
- 非法组合继续沿用现有降级规则,并输出最小必要日志。
- 若后续回退,只需回退 `Control` 中新增 API 与 `KEY2` 迁移代码,不影响第二阶段主线。
## 实现与影响
- 关键实现点:
-`Control` 层开放最小够用的布局策略 API。
- 新 API 调用后自动切换到 `AnchorToEdges`
- 保留 `setAnchor(...)` 与旧 getter,不破坏兼容路径。
-`KEY2` 位选择区、功能区、显示区和配置区作为迁移样例,验证新 API 的可用性。
- 涉及文件 / 类 / 函数:
- [`Control.h`](D:/programming/imGUI-easyX/imGui-easyX/Control.h)
- [`Control.cpp`](D:/programming/imGUI-easyX/imGui-easyX/Control.cpp)
- [`z-testDome.cpp`](D:/programming/imGUI-easyX/imGui-easyX/z-testDome.cpp)
- 兼容性影响:
- 兼容旧 `setAnchor(...)` 调用。
- 新 API 为新增能力,不影响现有二进制接口使用方式。
- 混用时以最后一次设置为准。
- 性能影响:
- 布局解算主逻辑不变,性能影响可忽略。
- `KEY2` 迁移后控件层次变多,但仍处于测试用例范围。
- 风险点:
- `KEY2` 迁移后可能暴露旧 demo 中原本被旧锚点语义掩盖的布局问题。
- 若后续没有补使用说明,外部调用仍可能继续滥用旧锚点入口。
## 测试与验证
- 测试范围:
- `Control` 新公开布局 API 编译级验证
- `KEY2` 迁移后的布局与显示联动
- `KEY5` 回归,确认新 API 未破坏第二阶段主线
- 验证步骤:
1. 编译 `Control.cpp`
2. 编译 `z-testDome.cpp /DKEY=2`
3. 编译 `z-testDome.cpp /DKEY=5`
4. 手动回归 `KEY2` 位选择区、功能区、显示区与配置区的 resize 和联动行为
- 验证结果:
- 编译级验证通过。
- GUI 手动回归待本机继续确认。
- 已知限制 / 遗留问题:[可选]
- `Dialog` 旧 synthetic `WM_MOUSEMOVE` 机制暂未统一。
- `Table` 内部局部重绘体系暂未实现。
- resize 过程中若开启高频 console 日志,可能出现一帧视觉延迟;当前判断属于调试态 I/O 现象,不作为 bug 处理,后续可在官网或 API 文档中注明。
## 落地信息
- 关联功能变更 ID[可选]
- `Feature-20260416-0009`
- 关联 BUG / Fix[可选]
- [Module-20260415-0004-布局系统第二阶段验收与封口.md](D:/programming/imGUI-easyX/imGui-easyX/开发记录/模块/Module-20260415-0004-布局系统第二阶段验收与封口.md)
- Commit:
- PR[可选]
- 发布版本:[可选]
- 相关文档:[可选]
- [Module-20260410-0002-锚点与布局系统第一阶段重构.md](D:/programming/imGUI-easyX/imGui-easyX/开发记录/模块/Module-20260410-0002-锚点与布局系统第一阶段重构.md)
- [Module-20260415-0004-布局系统第二阶段验收与封口.md](D:/programming/imGUI-easyX/imGui-easyX/开发记录/模块/Module-20260415-0004-布局系统第二阶段验收与封口.md)