From b7ad9605185182761dc916a68ffd5a77759d2c79 Mon Sep 17 00:00:00 2001 From: Codex Date: Fri, 10 Apr 2026 23:26:25 +0800 Subject: [PATCH] Refactor layout pipeline, add KEY5 regression, and fix tooltip hide --- Button.cpp | 9 +- Canvas.cpp | 235 +++++++++++-------------------------------------- Canvas.h | 7 ++ Control.cpp | 232 ++++++++++++++++++++++++++++++++++++++++++++++++ Control.h | 38 ++++++-- CoreTypes.h | 99 ++++++++++++++++++++- StellarX.h | 2 +- TabControl.cpp | 66 +++++++++----- TabControl.h | 7 ++ Table.cpp | 23 ++++- Table.h | 3 + Window.cpp | 72 ++------------- z-testDome.cpp | 216 ++++++++++++++++++++++++++++++++++++++++++++- 13 files changed, 719 insertions(+), 290 deletions(-) diff --git a/Button.cpp b/Button.cpp index a4875db..a45e383 100644 --- a/Button.cpp +++ b/Button.cpp @@ -683,10 +683,11 @@ void Button::hideTooltip() if (tipVisible) { tipVisible = false; - if (auto* host = getHostWindow(); host && host->isManagedDispatchActive()) - tipLabel.invalidateBackgroundSnapshot(); - else - tipLabel.hide(); // 还原快照+作废,防止残影 + // Tooltip 是 Button 的内置浮层,不属于独立控件树节点。 + // 因此在隐藏时应直接回贴它自己的背景快照并作废, + // 不能仅仅作废快照,否则当本轮只重绘按钮本体区域时, + // Tooltip 占用的那块屏幕可能无人擦除,最终表现为“鼠标移开后提示框残留”。 + tipLabel.hide(); // 还原快照 + 作废快照,立即清掉 Tooltip 自身绘制区域 tipHoverTick = GetTickCount64(); // 重置计时基线 } } diff --git a/Canvas.cpp b/Canvas.cpp index 759a6f7..20260b8 100644 --- a/Canvas.cpp +++ b/Canvas.cpp @@ -32,26 +32,54 @@ Canvas::Canvas(int x, int y, int width, int height) this->id = "Canvas"; } +void Canvas::relayoutManagedChildren() +{ + // Canvas 负责子控件从“父局部设计矩形”到“当前世界矩形”的转换。 + // 当 Canvas 自己的位置或尺寸变化后,所有受它管理的子控件都要重新走一次统一解算。 + for (auto& child : controls) + { + const StellarX::ResolvedLayoutRect rect = + child->resolveLayoutRect(localWidth, localHeight, x, y, width, height); + child->applyResolvedLayoutRect(rect); + } +} + void Canvas::setX(int x) { - this->x = x; - for (auto& c : controls) - { - c->onWindowResize(); - c->setX(c->getLocalX() + this->x); - } - dirty = true; + // 公开 setter 在 Canvas 上不能再视为“单纯改自己的 x”: + // 一旦容器移动,子控件的世界坐标也必须整体重算。 + applyRuntimeRectDirect(x, y, width, height); + relayoutManagedChildren(); + onWindowResize(); } void Canvas::setY(int y) { - this->y = y; - for (auto& c : controls) - { - c->onWindowResize(); - c->setY(c->getLocalY() + this->y); - } - dirty = true; + applyRuntimeRectDirect(this->x, y, width, height); + relayoutManagedChildren(); + onWindowResize(); +} + +void Canvas::setWidth(int width) +{ + applyRuntimeRectDirect(x, y, width, height); + relayoutManagedChildren(); + onWindowResize(); +} + +void Canvas::setHeight(int height) +{ + applyRuntimeRectDirect(x, y, width, height); + relayoutManagedChildren(); + onWindowResize(); +} + +void Canvas::applyResolvedLayoutRect(const StellarX::ResolvedLayoutRect& rect) +{ + // 统一解算器已经给出当前运行态世界矩形; + // Canvas 在应用自身矩形后,还必须继续刷新全部子控件。 + applyRuntimeRectDirect(rect.worldX, rect.worldY, rect.width, rect.height); + relayoutManagedChildren(); } void Canvas::clearAllControls() @@ -173,11 +201,12 @@ bool Canvas::handleEvent(const ExMessage& msg) void Canvas::addControl(std::unique_ptr control) { - - //坐标转化 - control->setX(control->getLocalX() + this->x); - control->setY(control->getLocalY() + this->y); control->setParent(this); + // 新子控件加入容器时,立刻按“当前容器运行态矩形”解算一次, + // 避免后续第一次 draw / resize 前 world 坐标仍停留在设计态。 + const StellarX::ResolvedLayoutRect rect = + control->resolveLayoutRect(localWidth, localHeight, this->x, this->y, this->width, this->height); + control->applyResolvedLayoutRect(rect); SX_LOGI("Canvas") << SX_T("添加子控件:父=Canvas 子id=", "addControl: parent=Canvas childId=") << control->getId() @@ -264,173 +293,11 @@ void Canvas::setDirty(bool dirty) void Canvas::onWindowResize() { - // 首先处理自身的快照等逻辑 + // resize 语义已收口: + // Canvas 不再在这里重新解算布局,只负责丢快照、标脏,并向子控件传播“环境已变化”。 Control::onWindowResize(); - - // 记录父容器原始尺寸(用于计算子控件的右/下边距) - int origParentW = this->localWidth; - int origParentH = this->localHeight; - - // 当前容器的新尺寸 - int finalW = this->width; - int finalH = this->height; - - // 当前容器的新坐标(全局坐标) - int parentX = this->x; - int parentY = this->y; - - // 调整每个子控件在 AnchorToEdges 模式下的位置与尺寸 - for (auto& ch : controls) - { - // Only adjust when using anchor-to-edges layout - if (ch->getLayoutMode() == StellarX::LayoutMode::AnchorToEdges) - { - // Determine whether this child is a Table; tables keep their height constant - bool isTable = (dynamic_cast(ch.get()) != nullptr); - - // Unpack anchors - auto a1 = ch->getAnchor_1(); - auto a2 = ch->getAnchor_2(); - - bool anchorLeft = (a1 == StellarX::Anchor::Left || a2 == StellarX::Anchor::Left); - bool anchorRight = (a1 == StellarX::Anchor::Right || a2 == StellarX::Anchor::Right); - bool anchorTop = (a1 == StellarX::Anchor::Top || a2 == StellarX::Anchor::Top); - bool anchorBottom = (a1 == StellarX::Anchor::Bottom || a2 == StellarX::Anchor::Bottom); - - // If it's a table, treat as anchored left and right horizontally and anchored top vertically by default. - if (isTable) - { - anchorLeft = true; - anchorRight = true; - // If no explicit vertical anchor was provided, default to top. - if (!(anchorTop || anchorBottom)) - { - anchorTop = true; - } - } - - // Compute new X and width - int newX = ch->getX(); - int newWidth = ch->getWidth(); - if (anchorLeft && anchorRight) - { - // Scale horizontally relative to parent's size. - if (origParentW > 0) - { - // Maintain proportional position and size based on original local values. - double scaleW = static_cast(finalW) / static_cast(origParentW); - newX = parentX + static_cast(ch->getLocalX() * scaleW + 0.5); - newWidth = static_cast(ch->getLocalWidth() * scaleW + 0.5); - } - else - { - // Fallback: keep original - newX = parentX + ch->getLocalX(); - newWidth = ch->getLocalWidth(); - } - } - else if (anchorLeft && !anchorRight) - { - // Only left anchored: keep original width and left margin. - newWidth = ch->getLocalWidth(); - newX = parentX + ch->getLocalX(); - } - else if (!anchorLeft && anchorRight) - { - // Only right anchored: keep original width and right margin. - newWidth = ch->getLocalWidth(); - int origRightDist = origParentW - (ch->getLocalX() + ch->getLocalWidth()); - newX = parentX + finalW - origRightDist - newWidth; - } - else - { - // No horizontal anchor: position relative to parent's left and width unchanged. - newWidth = ch->getLocalWidth(); - newX = parentX + ch->getLocalX(); - } - ch->setX(newX); - ch->setWidth(newWidth); - - // Compute new Y and height - int newY = ch->getY(); - int newHeight = ch->getHeight(); - if (isTable) - { - // Table: Height remains constant; adjust Y based on anchors. - newHeight = ch->getLocalHeight(); - if (anchorTop && anchorBottom) - { - // If both top and bottom anchored, scale Y but keep height. - if (origParentH > 0) - { - double scaleH = static_cast(finalH) / static_cast(origParentH); - newY = parentY + static_cast(ch->getLocalY() * scaleH + 0.5); - } - else - { - newY = parentY + ch->getLocalY(); - } - } - else if (anchorTop && !anchorBottom) - { - // Top anchored only - newY = parentY + ch->getLocalY(); - } - else if (!anchorTop && anchorBottom) - { - // Bottom anchored only - int origBottomDist = origParentH - (ch->getLocalY() + ch->getLocalHeight()); - newY = parentY + finalH - origBottomDist - newHeight; - } - else - { - // No vertical anchor: default to top - newY = parentY + ch->getLocalY(); - } - } - else - { - if (anchorTop && anchorBottom) - { - // Scale vertically relative to parent's size. - if (origParentH > 0) - { - double scaleH = static_cast(finalH) / static_cast(origParentH); - newY = parentY + static_cast(ch->getLocalY() * scaleH + 0.5); - newHeight = static_cast(ch->getLocalHeight() * scaleH + 0.5); - } - else - { - newY = parentY + ch->getLocalY(); - newHeight = ch->getLocalHeight(); - } - } - else if (anchorTop && !anchorBottom) - { - // Top anchored only: keep height constant - newHeight = ch->getLocalHeight(); - newY = parentY + ch->getLocalY(); - } - else if (!anchorTop && anchorBottom) - { - // Bottom anchored only: keep height and adjust Y relative to bottom - newHeight = ch->getLocalHeight(); - int origBottomDist = origParentH - (ch->getLocalY() + ch->getLocalHeight()); - newY = parentY + finalH - origBottomDist - newHeight; - } - else - { - // No vertical anchor: position relative to parent's top, height constant. - newHeight = ch->getLocalHeight(); - newY = parentY + ch->getLocalY(); - } - } - ch->setY(newY); - ch->setHeight(newHeight); - } - // Always forward the window resize event to the child (recursively). - ch->onWindowResize(); - } + for (auto& child : controls) + child->onWindowResize(); } void Canvas::requestRepaint(Control* parent) diff --git a/Canvas.h b/Canvas.h index 6076783..e867175 100644 --- a/Canvas.h +++ b/Canvas.h @@ -43,6 +43,8 @@ public: void setX(int x)override; void setY(int y)override; + void setWidth(int width) override; + void setHeight(int height) override; //绘制容器及其子控件 void draw() override; @@ -73,7 +75,12 @@ public: void commitManagedRepaint() override; //获取子控件列表 std::vector>& getControls() { return controls; } +protected: + // 统一解算后,按当前运行态矩形把所有受管理子控件重新映射到新的世界坐标。 + void applyResolvedLayoutRect(const StellarX::ResolvedLayoutRect& rect) override; private: + // 容器自己的几何变化后,需要统一刷新所有子控件的运行态矩形。 + void relayoutManagedChildren(); //用来检查对话框是否模态,此控件不做实现 bool model() const override { return false; }; }; diff --git a/Control.cpp b/Control.cpp index d27615b..50e6357 100644 --- a/Control.cpp +++ b/Control.cpp @@ -2,6 +2,138 @@ #include "SxLog.h" #include #include "Window.h" +#include + +namespace +{ + // 单轴解算后的临时结果:只描述“在父局部坐标系中的位置和尺寸”。 + struct LayoutAxisResult + { + int pos = 0; + int size = 0; + }; + + int ClampNonNegative(int value) + { + return value < 0 ? 0 : value; + } + + int RoundDiv(long long numerator, long long denominator) + { + if (denominator == 0) + return 0; + if (numerator >= 0) + return static_cast((numerator + denominator / 2) / denominator); + return -static_cast(((-numerator) + denominator / 2) / denominator); + } + + // 旧接口兼容层: + // 将 setAnchor(a1, a2) 的双锚点输入映射为单轴 LayoutSpec。 + // 这里只负责“从旧输入翻译成新模型”,不直接参与后续求解。 + StellarX::AxisLayoutSpec BuildLegacyAxisSpec(StellarX::Anchor anchor1, StellarX::Anchor anchor2, + StellarX::Anchor startAnchor, StellarX::Anchor endAnchor) + { + StellarX::AxisLayoutSpec spec{}; + spec.anchorStart = (anchor1 == startAnchor || anchor2 == startAnchor); + spec.anchorEnd = (anchor1 == endAnchor || anchor2 == endAnchor); + + if (spec.anchorStart && spec.anchorEnd) + { + spec.sizePolicy = StellarX::AxisSizePolicy::Stretch; + spec.alignPolicy = StellarX::AxisAlignPolicy::Start; + } + else if (spec.anchorEnd) + { + spec.sizePolicy = StellarX::AxisSizePolicy::FixedSize; + spec.alignPolicy = StellarX::AxisAlignPolicy::End; + } + else + { + spec.sizePolicy = StellarX::AxisSizePolicy::FixedSize; + spec.alignPolicy = StellarX::AxisAlignPolicy::Start; + } + + return spec; + } + + // 单轴统一解算器: + // 输入设计态父尺寸、当前父尺寸、子控件设计态位置/尺寸,以及当前轴策略, + // 输出该轴在“父局部坐标系”下的运行态位置与尺寸。 + LayoutAxisResult ResolveAxis(int parentDesignSize, int parentCurrentSize, int childDesignPos, int childDesignSize, + const StellarX::AxisLayoutSpec& spec, bool allowStretch) + { + LayoutAxisResult result{}; + const int startMargin = childDesignPos; + const int endMargin = parentDesignSize - (childDesignPos + childDesignSize); + + const bool wantsStretch = spec.sizePolicy == StellarX::AxisSizePolicy::Stretch; + const bool canStretch = allowStretch && spec.anchorStart && spec.anchorEnd; + + if (wantsStretch && canStretch) + { + // 双边锚定 + Stretch: + // 保持设计态中的起边距和终边距,让尺寸随父容器变化。 + result.pos = startMargin; + result.size = ClampNonNegative(parentCurrentSize - startMargin - endMargin); + return result; + } + + // 其余情况一律按固定尺寸处理;即便调用方请求 Stretch, + // 只要锚点条件或控件能力边界不满足,也会在这里自然降级为 FixedSize。 + result.size = ClampNonNegative(childDesignSize); + + if (spec.anchorStart && !spec.anchorEnd) + { + // 仅锚定起边:保持起边距,尺寸不变。 + result.pos = startMargin; + return result; + } + + if (!spec.anchorStart && spec.anchorEnd) + { + // 仅锚定终边:保持终边距,尺寸不变。 + result.pos = parentCurrentSize - endMargin - result.size; + return result; + } + + if (spec.anchorStart && spec.anchorEnd) + { + // 双边锚定但尺寸固定: + // 位置由 alignPolicy 决定,用于表达“只位移、不拉伸”的场景。 + switch (spec.alignPolicy) + { + case StellarX::AxisAlignPolicy::End: + result.pos = parentCurrentSize - endMargin - result.size; + break; + case StellarX::AxisAlignPolicy::Center: + // 保持相对父容器中心的偏移关系。 + result.pos = childDesignPos + (parentCurrentSize - parentDesignSize) / 2; + break; + case StellarX::AxisAlignPolicy::Proportional: + { + // 保持设计态中的相对位置比例。 + // 注意:这里只调整位置,不改变尺寸。 + const int designTravel = parentDesignSize - result.size; + const int currentTravel = parentCurrentSize - result.size; + if (designTravel <= 0 || currentTravel <= 0) + result.pos = startMargin; + else + result.pos = RoundDiv(static_cast(startMargin) * currentTravel, designTravel); + break; + } + case StellarX::AxisAlignPolicy::Start: + default: + result.pos = startMargin; + break; + } + return result; + } + + // 无锚点:退回设计态位置,尺寸保持设计值。 + result.pos = childDesignPos; + return result; + } +} StellarX::ControlText& StellarX::ControlText::operator=(const ControlText& text) { @@ -86,6 +218,11 @@ void Control::setAnchor(StellarX::Anchor anchor_1, StellarX::Anchor anchor_2) { this->anchor_1 = anchor_1; this->anchor_2 = anchor_2; + // 旧 API 只作为兼容输入层存在: + // 这里把历史上的 anchor_1 / anchor_2 映射为新的水平/垂直轴布局规格, + // 后续统一解算全部以 layoutSpec 为准。 + 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); } StellarX::Anchor Control::getAnchor_1() const { @@ -99,6 +236,101 @@ StellarX::LayoutMode Control::getLayoutMode() const { return this->layoutMode; } +const StellarX::LayoutSpec& Control::getLayoutSpec() const +{ + return layoutSpec; +} + +const StellarX::LayoutCapability& Control::getLayoutCapability() const +{ + return layoutCapability; +} + +void Control::setX(int x) +{ + this->x = x; + dirty = true; +} + +void Control::setY(int y) +{ + this->y = y; + dirty = true; +} + +void Control::setWidth(int width) +{ + this->width = width; + dirty = true; +} + +void Control::setHeight(int height) +{ + this->height = height; + dirty = true; +} + +void Control::commitCurrentGeometryAsDesignRect() +{ + // 该接口是“显式提交新的设计基线”的唯一入口之一。 + // 普通布局解算、父容器重排、窗口 resize 均不得自动回写 local*, + // 否则会导致设计基线漂移,后续解算越来越不稳定。 + localx = parent ? (x - parent->getX()) : x; + localy = parent ? (y - parent->getY()) : y; + localWidth = width; + localHeight = height; +} + +StellarX::ResolvedLayoutRect Control::resolveLayoutRect(int parentDesignW, int parentDesignH, + int parentWorldX, int parentWorldY, int parentCurrentW, int parentCurrentH) const +{ + StellarX::ResolvedLayoutRect rect{}; + + if (layoutMode != StellarX::LayoutMode::AnchorToEdges) + { + // 非锚点布局模式:直接使用设计态矩形,再映射到当前世界坐标。 + rect.localX = localx; + rect.localY = localy; + rect.width = localWidth; + rect.height = localHeight; + rect.worldX = parentWorldX + rect.localX; + rect.worldY = parentWorldY + rect.localY; + return rect; + } + + // 第 1 层:先在父局部坐标系内分别解算水平轴和垂直轴。 + const LayoutAxisResult horizontal = ResolveAxis(parentDesignW, parentCurrentW, localx, localWidth, + layoutSpec.horizontal, layoutCapability.allowStretchX); + const LayoutAxisResult vertical = ResolveAxis(parentDesignH, parentCurrentH, localy, localHeight, + layoutSpec.vertical, layoutCapability.allowStretchY); + + // 第 2 层:把父局部矩形映射到世界坐标。 + rect.localX = horizontal.pos; + rect.localY = vertical.pos; + rect.width = horizontal.size; + rect.height = vertical.size; + rect.worldX = parentWorldX + rect.localX; + rect.worldY = parentWorldY + rect.localY; + return rect; +} + +void Control::applyResolvedLayoutRect(const StellarX::ResolvedLayoutRect& rect) +{ + // 默认实现只应用运行态世界矩形。 + // 若某个控件还需要在应用后继续刷新内部布局,可重写此函数。 + applyRuntimeRectDirect(rect.worldX, rect.worldY, rect.width, rect.height); +} + +void Control::applyRuntimeRectDirect(int worldX, int worldY, int width, int height) +{ + // 最底层运行态赋值入口: + // 只修改当前世界坐标和运行态尺寸,不触碰设计基线 local*。 + this->x = worldX; + this->y = worldY; + this->width = width; + this->height = height; + dirty = true; +} // 保存当前的绘图状态(字体、颜色、线型等) // 在控件绘制前调用,确保不会影响全局绘图状态 void Control::saveStyle() diff --git a/Control.h b/Control.h index 3520d26..6d59cdd 100644 --- a/Control.h +++ b/Control.h @@ -37,10 +37,12 @@ class Window; class Control { + friend class Window; + friend class Canvas; protected: std::string id; // 控件ID - int localx, x, localy, y; // 左上角坐标 - int localWidth, width, localHeight, height; // 控件尺寸 + int localx, x, localy, y; // local* 为设计态父局部坐标,x/y 为运行态世界坐标 + int localWidth, width, localHeight, height; // local* 为设计态尺寸,width/height 为运行态尺寸 Control* parent = nullptr; // 父控件 Window* hostWindow = nullptr; // 宿主窗口(顶层由 Window 注入,子控件可沿 parent 回溯) bool dirty = true; // 是否重绘 @@ -49,8 +51,15 @@ protected: /* == 布局模式 == */ StellarX::LayoutMode layoutMode = StellarX::LayoutMode::Fixed; // 布局模式 - StellarX::Anchor anchor_1 = StellarX::Anchor::Top; // 锚点 - StellarX::Anchor anchor_2 = StellarX::Anchor::Right; // 锚点 + StellarX::Anchor anchor_1 = StellarX::Anchor::Top; // 旧版兼容锚点 1 + StellarX::Anchor anchor_2 = StellarX::Anchor::Right; // 旧版兼容锚点 2 + // 新布局模型:内部统一使用“水平轴 + 垂直轴”的规格描述,不再直接依赖旧锚点求解。 + StellarX::LayoutSpec layoutSpec{ + { false, true, StellarX::AxisSizePolicy::FixedSize, StellarX::AxisAlignPolicy::End }, + { true, false, StellarX::AxisSizePolicy::FixedSize, StellarX::AxisAlignPolicy::Start } + }; + // 控件能力边界:用于限制某些控件在特定轴上是否允许 Stretch。 + StellarX::LayoutCapability layoutCapability{}; /* == 背景快照 == */ std::unique_ptr saveBkImage; @@ -115,10 +124,10 @@ public: int getLocalRight() const { return localx + localWidth; } int getLocalBottom() const { return localy + localHeight; } - virtual void setX(int x) { this->x = x; dirty = true; } - virtual void setY(int y) { this->y = y; dirty = true; } - virtual void setWidth(int width) { this->width = width; dirty = true; } - virtual void setHeight(int height) { this->height = height; dirty = true; } + virtual void setX(int x); + virtual void setY(int y); + virtual void setWidth(int width); + virtual void setHeight(int height); public: virtual void draw() = 0; @@ -153,7 +162,20 @@ public: StellarX::Anchor getAnchor_1() const; StellarX::Anchor getAnchor_2() const; StellarX::LayoutMode getLayoutMode() const; + // 获取内部统一布局规格;供 Window / Canvas 等统一解算入口使用。 + const StellarX::LayoutSpec& getLayoutSpec() const; + // 获取控件能力边界;用于判断某个轴是否允许 Stretch。 + const StellarX::LayoutCapability& getLayoutCapability() const; + // 显式将当前运行态矩形提交为新的设计基线。普通 resize / 重排过程中不得自动调用。 + void commitCurrentGeometryAsDesignRect(); protected: + // 第 1 层:根据父设计尺寸、父当前尺寸和本控件设计矩形,解算出当前运行态局部矩形。 + StellarX::ResolvedLayoutRect resolveLayoutRect(int parentDesignW, int parentDesignH, + int parentWorldX, int parentWorldY, int parentCurrentW, int parentCurrentH) const; + // 内部受控路径:把统一解算结果应用到运行态矩形。不要把公开 setter 当成纯赋值接口来复用。 + virtual void applyResolvedLayoutRect(const StellarX::ResolvedLayoutRect& rect); + // 最底层运行态赋值入口:仅修改运行态矩形,不回写 local*。 + void applyRuntimeRectDirect(int worldX, int worldY, int width, int height); void saveStyle(); void restoreStyle(); void resetEventVisualChanged() { eventVisualChanged = false; } diff --git a/CoreTypes.h b/CoreTypes.h index 3225ecb..3fb4aee 100644 --- a/CoreTypes.h +++ b/CoreTypes.h @@ -388,4 +388,101 @@ namespace StellarX Top, Bottom }; -} \ No newline at end of file + + /******************************************************************************* + * @枚举: AxisSizePolicy + * @摘要: 单轴尺寸策略,决定该轴是否允许因父容器变化而拉伸 + * @详细说明: + * 该枚举只决定“尺寸是否变化”,不决定“位置如何求解”。 + * 若需要在固定尺寸下决定控件如何位移,应结合 AxisAlignPolicy 一起使用。 + * + * @成员说明: + * Stretch - 允许尺寸随父容器变化而变化 + * FixedSize - 尺寸保持设计态大小,仅位置按锚点/位移策略变化 + ******************************************************************************/ + enum class AxisSizePolicy + { + Stretch, + FixedSize + }; + + /******************************************************************************* + * @枚举: AxisAlignPolicy + * @摘要: 单轴固定尺寸时的位置策略 + * @详细说明: + * 该策略只在“该轴不拉伸”时生效,用于决定控件在父容器变化后如何重新定位。 + * 其中 Proportional 只用于位置求解,不参与尺寸缩放。 + * + * @成员说明: + * Start - 保持起边距离 + * End - 保持终边距离 + * Center - 保持相对父容器中心的偏移关系 + * Proportional - 保持设计态中的相对位置比例 + ******************************************************************************/ + enum class AxisAlignPolicy + { + Start, + End, + Center, + Proportional + }; + + /******************************************************************************* + * @结构体: AxisLayoutSpec + * @摘要: 描述单个坐标轴上的锚点与解算策略 + * @详细说明: + * 一个控件完整布局由“水平轴 + 垂直轴”两份 AxisLayoutSpec 组成。 + * anchorStart / anchorEnd 只表达当前轴是否锚定起止边; + * sizePolicy 决定该轴是否允许拉伸; + * alignPolicy 则用于固定尺寸时的位置求解。 + ******************************************************************************/ + struct AxisLayoutSpec + { + bool anchorStart = false; + bool anchorEnd = false; + AxisSizePolicy sizePolicy = AxisSizePolicy::FixedSize; + AxisAlignPolicy alignPolicy = AxisAlignPolicy::Start; + }; + + /******************************************************************************* + * @结构体: LayoutSpec + * @摘要: 控件完整布局规格,由水平轴和垂直轴两部分组成 + ******************************************************************************/ + struct LayoutSpec + { + AxisLayoutSpec horizontal{}; + AxisLayoutSpec vertical{}; + }; + + /******************************************************************************* + * @结构体: LayoutCapability + * @摘要: 控件能力边界,声明控件在哪些轴上允许 Stretch + * @详细说明: + * 规则表负责定义“某种组合如何解算”,LayoutCapability 负责声明 + * “该控件是否允许采用这种组合”。例如 Table 当前阶段仅允许 X 轴拉伸, + * 因此 allowStretchY 会被显式关闭。 + ******************************************************************************/ + struct LayoutCapability + { + bool allowStretchX = true; + bool allowStretchY = true; + }; + + /******************************************************************************* + * @结构体: ResolvedLayoutRect + * @摘要: 统一布局解算后的运行态矩形结果 + * @详细说明: + * localX/localY 表示解算完成后的“父局部坐标”; + * worldX/worldY 表示映射到窗口世界坐标后的最终绘制位置; + * width/height 为运行态尺寸。 + ******************************************************************************/ + struct ResolvedLayoutRect + { + int localX = 0; + int localY = 0; + int width = 0; + int height = 0; + int worldX = 0; + int worldY = 0; + }; +} diff --git a/StellarX.h b/StellarX.h index d0d9b60..bf593cc 100644 --- a/StellarX.h +++ b/StellarX.h @@ -1,7 +1,7 @@ /******************************************************************************* * @文件: StellarX.h * @摘要: 星垣(StellarX) GUI框架 - 主包含头文件 - * @版本: v3.0.1 + * @版本: v3.0.2 * @描述: * 一个为Windows平台打造的轻量级、模块化C++ GUI框架。 * 基于EasyX图形库,提供简洁易用的API和丰富的控件。 diff --git a/TabControl.cpp b/TabControl.cpp index 2af52ac..4ae2a69 100644 --- a/TabControl.cpp +++ b/TabControl.cpp @@ -62,6 +62,9 @@ inline void TabControl::initTabBar() inline void TabControl::initTabPage() { if (controls.empty())return; + // TabControl 内部页签页仍然保留专用布局: + // 这里负责把“当前选项卡容器矩形”拆分成页签栏和页面区, + // 不把这部分细节下放到通用布局解算器里。 //子控件坐标原点 int nX = 0; int nY = 0; @@ -148,6 +151,15 @@ inline void TabControl::initTabPage() } } +void TabControl::refreshRuntimeLayout() +{ + // 这是 TabControl 的内部专用布局入口: + // 外层先通过统一解算得到 TabControl 自身矩形, + // 再由这里继续安置页签按钮和页面区。 + initTabBar(); + initTabPage(); +} + TabControl::TabControl() :Canvas() { this->id = "TabControl"; @@ -165,28 +177,38 @@ TabControl::~TabControl() void TabControl::setX(int x) { - this->x = x; - initTabBar(); - initTabPage(); - dirty = true; - for (auto& c : controls) - { - c.first->onWindowResize(); - c.second->onWindowResize(); - } + applyRuntimeRectDirect(x, y, width, height); + refreshRuntimeLayout(); + onWindowResize(); } void TabControl::setY(int y) { - this->y = y; - initTabBar(); - initTabPage(); - dirty = true; - for (auto& c : controls) - { - c.first->onWindowResize(); - c.second->onWindowResize(); - } + applyRuntimeRectDirect(x, y, width, height); + refreshRuntimeLayout(); + onWindowResize(); +} + +void TabControl::setWidth(int width) +{ + applyRuntimeRectDirect(x, y, width, height); + refreshRuntimeLayout(); + onWindowResize(); +} + +void TabControl::setHeight(int height) +{ + applyRuntimeRectDirect(x, y, width, height); + refreshRuntimeLayout(); + onWindowResize(); +} + +void TabControl::applyResolvedLayoutRect(const StellarX::ResolvedLayoutRect& rect) +{ + // TabControl 作为外层控件接入统一解算; + // 但页签栏和页面区仍由自身专用逻辑继续排布。 + applyRuntimeRectDirect(rect.worldX, rect.worldY, rect.width, rect.height); + refreshRuntimeLayout(); } void TabControl::draw() @@ -348,18 +370,14 @@ void TabControl::setIsVisible(bool visible) void TabControl::onWindowResize() { - // 调用基类的窗口变化处理,丢弃快照并标记脏 + // 本轮不再在 onWindowResize 中重做页签布局, + // 这里只负责失效快照、标脏,并把 resize 语义向页签按钮和页面传递。 Control::onWindowResize(); - // 根据当前 TabControl 的新尺寸重新计算页签栏和页面区域 - initTabBar(); - initTabPage(); - // 转发窗口尺寸变化给所有页签按钮和页面 for (auto& c : controls) { c.first->onWindowResize(); c.second->onWindowResize(); } - // 尺寸变化后需要重绘自身 dirty = true; } diff --git a/TabControl.h b/TabControl.h index 72a03ab..9ef96d1 100644 --- a/TabControl.h +++ b/TabControl.h @@ -39,6 +39,8 @@ private: // 初始化页签按钮位置和尺寸 inline void initTabBar(); inline void initTabPage(); + // 统一刷新 TabControl 当前运行态下的页签栏和页面区布局。 + void refreshRuntimeLayout(); public: TabControl(); TabControl(int x, int y, int width, int height); @@ -47,6 +49,8 @@ public: //重写位置设置以适应页签和页面布局 void setX(int x)override; void setY(int y)override; + void setWidth(int width) override; + void setHeight(int height) override; void draw() override; bool handleEvent(const ExMessage& msg) override; @@ -76,4 +80,7 @@ public: void requestRepaint(Control* parent)override; // 托管模式下登记为 root;非托管模式下局部更新脏按钮/脏页面 bool canCommitManagedPartialRepaint() const override; // 判断当前 TabControl 是否可安全做局部提交 void commitManagedRepaint() override; // 托管收口阶段执行 TabControl 的真正重绘 +protected: + // 外层统一解算后,TabControl 需要同步刷新其内部页签栏和页面区。 + void applyResolvedLayoutRect(const StellarX::ResolvedLayoutRect& rect) override; }; diff --git a/Table.cpp b/Table.cpp index 7d44601..94ed542 100644 --- a/Table.cpp +++ b/Table.cpp @@ -355,17 +355,38 @@ void Table::setWidth(int width) void Table::setHeight(int height) { - //高度不变 + // 当前阶段 Table 明确不支持纵向 Stretch。 + // 高度链路依赖表头、表体、页脚、按钮和页码计算, + // 因此这里保持空实现,避免被通用布局层错误拉高/压缩。 } Table::Table(int x, int y) :Control(x, y, 0, 0) { this->id = "Table"; + // Table 当前正式能力边界: + // 仅允许 X 轴 Stretch,Y 轴固定尺寸。 + this->layoutCapability.allowStretchY = false; } Table::~Table() = default; +void Table::applyResolvedLayoutRect(const StellarX::ResolvedLayoutRect& rect) +{ + // Table 不能像普通控件那样直接写入 width/height: + // 它的 setWidth() 内部会联动列宽与页脚布局,因此这里必须复用原有 setter 副作用。 + // 同时由于 setHeight() 为空实现,Y 轴会自然保持固定尺寸。 + if (this->x != rect.worldX) + setX(rect.worldX); + if (this->y != rect.worldY) + setY(rect.worldY); + if (this->width != rect.width) + setWidth(rect.width); + if (this->height != rect.height) + setHeight(rect.height); + dirty = true; +} + void Table::draw() { //在这里先初始化保证翻页按钮不为空 diff --git a/Table.h b/Table.h index eeb357e..87e0d87 100644 --- a/Table.h +++ b/Table.h @@ -93,6 +93,9 @@ private: void drawHeader(); //绘制表头 void drawPageNum(); //绘制页码信息 void drawButton(); //绘制翻页按钮 + // 统一解算后的内部应用入口: + // Table 需要复用自己已有的 setWidth/setX 等副作用逻辑,因此单独接管应用过程。 + void applyResolvedLayoutRect(const StellarX::ResolvedLayoutRect& rect) override; private: //用来检查对话框是否模态,此控件不做实现 bool model() const override { return false; }; diff --git a/Window.cpp b/Window.cpp index 2fc1805..c9dfdff 100644 --- a/Window.cpp +++ b/Window.cpp @@ -981,72 +981,12 @@ void Window::scheduleResizeFromModal(int w, int h) void Window::adaptiveLayout(std::unique_ptr& c, const int finalH, const int finalW) { - int origParentW = this->localwidth; - int origParentH = this->localheight; - if (c->getLayoutMode() == StellarX::LayoutMode::AnchorToEdges) - { - if ((StellarX::Anchor::Left == c->getAnchor_1() && StellarX::Anchor::Right == c->getAnchor_2()) - || (StellarX::Anchor::Right == c->getAnchor_1() && StellarX::Anchor::Left == c->getAnchor_2())) - { - int origRightDist = origParentW - (c->getLocalX() + c->getLocalWidth()); - int newWidth = finalW - c->getLocalX() - origRightDist; - c->setWidth(newWidth); - // 左侧距离固定,ctrl->x 保持为 localx 相对窗口左侧(父容器为窗口,偏移0) - c->setX(c->getLocalX()); - } - else if ((StellarX::Anchor::Left == c->getAnchor_1() && StellarX::Anchor::NoAnchor == c->getAnchor_2()) - || (StellarX::Anchor::NoAnchor == c->getAnchor_1() && StellarX::Anchor::Left == c->getAnchor_2()) - || (StellarX::Anchor::Left == c->getAnchor_1() && StellarX::Anchor::Left == c->getAnchor_2())) - { - // 仅左锚定:宽度固定不变 - c->setX(c->getLocalX()); - c->setWidth(c->getLocalWidth()); - } - else if ((StellarX::Anchor::Right == c->getAnchor_1() && StellarX::Anchor::NoAnchor == c->getAnchor_2()) - || (StellarX::Anchor::NoAnchor == c->getAnchor_1() && StellarX::Anchor::Right == c->getAnchor_2()) - || (StellarX::Anchor::Right == c->getAnchor_1() && StellarX::Anchor::Right == c->getAnchor_2())) - { - int origRightDist = origParentW - (c->getLocalX() + c->getLocalWidth()); - c->setWidth(c->getLocalWidth()); // 宽度不变 - c->setX(finalW - origRightDist - c->getWidth()); - } - else if (StellarX::Anchor::NoAnchor == c->getAnchor_1() && StellarX::Anchor::NoAnchor == c->getAnchor_2()) - { - c->setX(c->getLocalX()); - c->setWidth(c->getLocalWidth()); - } - - if ((StellarX::Anchor::Top == c->getAnchor_1() && StellarX::Anchor::Bottom == c->getAnchor_2()) - || (StellarX::Anchor::Bottom == c->getAnchor_1() && StellarX::Anchor::Top == c->getAnchor_2())) - { - // 上下锚定:高度随窗口变化 - int origBottomDist = origParentH - (c->getLocalY() + c->getLocalHeight()); - int newHeight = finalH - c->getLocalY() - origBottomDist; - c->setHeight(newHeight); - c->setY(c->getLocalY()); - } - else if ((StellarX::Anchor::Top == c->getAnchor_1() && StellarX::Anchor::NoAnchor == c->getAnchor_2()) - || (StellarX::Anchor::NoAnchor == c->getAnchor_1() && StellarX::Anchor::Top == c->getAnchor_2()) - || (StellarX::Anchor::Top == c->getAnchor_1() && StellarX::Anchor::Top == c->getAnchor_2())) - { - c->setY(c->getLocalY()); - c->setHeight(c->getLocalHeight()); - } - else if ((StellarX::Anchor::Bottom == c->getAnchor_1() && StellarX::Anchor::NoAnchor == c->getAnchor_2()) - || (StellarX::Anchor::NoAnchor == c->getAnchor_1() && StellarX::Anchor::Bottom == c->getAnchor_2()) - || (StellarX::Anchor::Bottom == c->getAnchor_1() && StellarX::Anchor::Bottom == c->getAnchor_2())) - { - int origBottomDist = origParentH - (c->getLocalY() + c->getLocalHeight()); - c->setHeight(c->getLocalHeight()); - c->setY(finalH - origBottomDist - c->getHeight()); - } - else - { - // 垂直无锚点:默认为顶部定位,高度固定 - c->setY(c->getLocalY()); - c->setHeight(c->getLocalHeight()); - } - } + // 顶层窗口布局收口: + // 先用统一解算器求出控件新的运行态矩形,再通知控件“外部环境已变化”。 + // onWindowResize() 负责快照失效/标脏,不再承担布局公式。 + const StellarX::ResolvedLayoutRect rect = + c->resolveLayoutRect(this->localwidth, this->localheight, 0, 0, finalW, finalH); + c->applyResolvedLayoutRect(rect); c->onWindowResize(); } diff --git a/z-testDome.cpp b/z-testDome.cpp index a92c102..5cb0d11 100644 --- a/z-testDome.cpp +++ b/z-testDome.cpp @@ -1,6 +1,220 @@ // StellarX 星垣GUI框架 - 测试用例 #include"StellarX.h" -#define KEY 4 +#ifndef KEY +#define KEY 5 +#endif + +#if 5 == KEY + +#include"StellarX.h" + +int main() +{ + StellarX::SxLogger::setGBK(); + StellarX::SxLogger::Get().enableConsole(true); + StellarX::SxLogger::Get().setMinLevel(StellarX::SxLogLevel::Debug); + StellarX::SxLogger::Get().setLanguage(StellarX::SxLogLanguage::ZhCN); + + const COLORREF headerColor = RGB(232, 238, 245); + const COLORREF horizontalColor = RGB(217, 233, 252); + const COLORREF nestedColor = RGB(219, 244, 223); + const COLORREF verticalColor = RGB(255, 233, 205); + const COLORREF tableZoneColor = RGB(235, 226, 250); + const COLORREF movingColor = RGB(249, 224, 232); + + Window win(1280, 780, 1, RGB(246, 248, 251), "StellarX 布局系统专项回归 KEY5"); + + auto header = std::make_unique(20, 20, 1240, 100); + auto headerPtr = header.get(); + headerPtr->setShape(StellarX::ControlShape::ROUND_RECTANGLE); + headerPtr->setCanvasBkColor(headerColor); + headerPtr->setLayoutMode(StellarX::LayoutMode::AnchorToEdges); + headerPtr->setAnchor(StellarX::Anchor::Left, StellarX::Anchor::Right); + + auto headerTitle = std::make_unique