Refactor layout pipeline, add KEY5 regression, and fix tooltip hide

This commit is contained in:
Codex
2026-04-10 23:26:25 +08:00
parent 241a564095
commit b7ad960518
13 changed files with 719 additions and 290 deletions
+5 -4
View File
@@ -683,10 +683,11 @@ void Button::hideTooltip()
if (tipVisible) if (tipVisible)
{ {
tipVisible = false; tipVisible = false;
if (auto* host = getHostWindow(); host && host->isManagedDispatchActive()) // Tooltip 是 Button 的内置浮层,不属于独立控件树节点。
tipLabel.invalidateBackgroundSnapshot(); // 因此在隐藏时应直接回贴它自己的背景快照并作废,
else // 不能仅仅作废快照,否则当本轮只重绘按钮本体区域时,
tipLabel.hide(); // 还原快照+作废,防止残影 // Tooltip 占用的那块屏幕可能无人擦除,最终表现为“鼠标移开后提示框残留”。
tipLabel.hide(); // 还原快照 + 作废快照,立即清掉 Tooltip 自身绘制区域
tipHoverTick = GetTickCount64(); // 重置计时基线 tipHoverTick = GetTickCount64(); // 重置计时基线
} }
} }
+51 -184
View File
@@ -32,26 +32,54 @@ Canvas::Canvas(int x, int y, int width, int height)
this->id = "Canvas"; 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) void Canvas::setX(int x)
{ {
this->x = x; // 公开 setter 在 Canvas 上不能再视为“单纯改自己的 x”:
for (auto& c : controls) // 一旦容器移动,子控件的世界坐标也必须整体重算。
{ applyRuntimeRectDirect(x, y, width, height);
c->onWindowResize(); relayoutManagedChildren();
c->setX(c->getLocalX() + this->x); onWindowResize();
}
dirty = true;
} }
void Canvas::setY(int y) void Canvas::setY(int y)
{ {
this->y = y; applyRuntimeRectDirect(this->x, y, width, height);
for (auto& c : controls) relayoutManagedChildren();
{ onWindowResize();
c->onWindowResize(); }
c->setY(c->getLocalY() + this->y);
} void Canvas::setWidth(int width)
dirty = true; {
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() void Canvas::clearAllControls()
@@ -173,11 +201,12 @@ bool Canvas::handleEvent(const ExMessage& msg)
void Canvas::addControl(std::unique_ptr<Control> control) void Canvas::addControl(std::unique_ptr<Control> control)
{ {
//坐标转化
control->setX(control->getLocalX() + this->x);
control->setY(control->getLocalY() + this->y);
control->setParent(this); 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_LOGI("Canvas")
<< SX_T("添加子控件:父=Canvas 子id=", "addControl: parent=Canvas childId=") << SX_T("添加子控件:父=Canvas 子id=", "addControl: parent=Canvas childId=")
<< control->getId() << control->getId()
@@ -264,173 +293,11 @@ void Canvas::setDirty(bool dirty)
void Canvas::onWindowResize() void Canvas::onWindowResize()
{ {
// 首先处理自身的快照等逻辑 // resize 语义已收口:
// Canvas 不再在这里重新解算布局,只负责丢快照、标脏,并向子控件传播“环境已变化”。
Control::onWindowResize(); Control::onWindowResize();
for (auto& child : controls)
// 记录父容器原始尺寸(用于计算子控件的右/下边距) child->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<Table*>(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<double>(finalW) / static_cast<double>(origParentW);
newX = parentX + static_cast<int>(ch->getLocalX() * scaleW + 0.5);
newWidth = static_cast<int>(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<double>(finalH) / static_cast<double>(origParentH);
newY = parentY + static_cast<int>(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<double>(finalH) / static_cast<double>(origParentH);
newY = parentY + static_cast<int>(ch->getLocalY() * scaleH + 0.5);
newHeight = static_cast<int>(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();
}
} }
void Canvas::requestRepaint(Control* parent) void Canvas::requestRepaint(Control* parent)
+7
View File
@@ -43,6 +43,8 @@ public:
void setX(int x)override; void setX(int x)override;
void setY(int y)override; void setY(int y)override;
void setWidth(int width) override;
void setHeight(int height) override;
//绘制容器及其子控件 //绘制容器及其子控件
void draw() override; void draw() override;
@@ -73,7 +75,12 @@ public:
void commitManagedRepaint() override; void commitManagedRepaint() override;
//获取子控件列表 //获取子控件列表
std::vector<std::unique_ptr<Control>>& getControls() { return controls; } std::vector<std::unique_ptr<Control>>& getControls() { return controls; }
protected:
// 统一解算后,按当前运行态矩形把所有受管理子控件重新映射到新的世界坐标。
void applyResolvedLayoutRect(const StellarX::ResolvedLayoutRect& rect) override;
private: private:
// 容器自己的几何变化后,需要统一刷新所有子控件的运行态矩形。
void relayoutManagedChildren();
//用来检查对话框是否模态,此控件不做实现 //用来检查对话框是否模态,此控件不做实现
bool model() const override { return false; }; bool model() const override { return false; };
}; };
+232
View File
@@ -2,6 +2,138 @@
#include "SxLog.h" #include "SxLog.h"
#include<assert.h> #include<assert.h>
#include "Window.h" #include "Window.h"
#include <algorithm>
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<int>((numerator + denominator / 2) / denominator);
return -static_cast<int>(((-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<long long>(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) 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_1 = anchor_1;
this->anchor_2 = anchor_2; 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 StellarX::Anchor Control::getAnchor_1() const
{ {
@@ -99,6 +236,101 @@ StellarX::LayoutMode Control::getLayoutMode() const
{ {
return this->layoutMode; 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() void Control::saveStyle()
+30 -8
View File
@@ -37,10 +37,12 @@ class Window;
class Control class Control
{ {
friend class Window;
friend class Canvas;
protected: protected:
std::string id; // 控件ID std::string id; // 控件ID
int localx, x, localy, y; // 左上角坐标 int localx, x, localy, y; // local* 为设计态父局部坐标,x/y 为运行态世界坐标
int localWidth, width, localHeight, height; // 控件尺寸 int localWidth, width, localHeight, height; // local* 为设计态尺寸,width/height 为运行态尺寸
Control* parent = nullptr; // 父控件 Control* parent = nullptr; // 父控件
Window* hostWindow = nullptr; // 宿主窗口(顶层由 Window 注入,子控件可沿 parent 回溯) Window* hostWindow = nullptr; // 宿主窗口(顶层由 Window 注入,子控件可沿 parent 回溯)
bool dirty = true; // 是否重绘 bool dirty = true; // 是否重绘
@@ -49,8 +51,15 @@ protected:
/* == 布局模式 == */ /* == 布局模式 == */
StellarX::LayoutMode layoutMode = StellarX::LayoutMode::Fixed; // 布局模式 StellarX::LayoutMode layoutMode = StellarX::LayoutMode::Fixed; // 布局模式
StellarX::Anchor anchor_1 = StellarX::Anchor::Top; // 锚点 StellarX::Anchor anchor_1 = StellarX::Anchor::Top; // 旧版兼容锚点 1
StellarX::Anchor anchor_2 = StellarX::Anchor::Right; // 锚点 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<IMAGE> saveBkImage; std::unique_ptr<IMAGE> saveBkImage;
@@ -115,10 +124,10 @@ public:
int getLocalRight() const { return localx + localWidth; } int getLocalRight() const { return localx + localWidth; }
int getLocalBottom() const { return localy + localHeight; } int getLocalBottom() const { return localy + localHeight; }
virtual void setX(int x) { this->x = x; dirty = true; } virtual void setX(int x);
virtual void setY(int y) { this->y = y; dirty = true; } virtual void setY(int y);
virtual void setWidth(int width) { this->width = width; dirty = true; } virtual void setWidth(int width);
virtual void setHeight(int height) { this->height = height; dirty = true; } virtual void setHeight(int height);
public: public:
virtual void draw() = 0; virtual void draw() = 0;
@@ -153,7 +162,20 @@ public:
StellarX::Anchor getAnchor_1() const; StellarX::Anchor getAnchor_1() const;
StellarX::Anchor getAnchor_2() const; StellarX::Anchor getAnchor_2() const;
StellarX::LayoutMode getLayoutMode() const; StellarX::LayoutMode getLayoutMode() const;
// 获取内部统一布局规格;供 Window / Canvas 等统一解算入口使用。
const StellarX::LayoutSpec& getLayoutSpec() const;
// 获取控件能力边界;用于判断某个轴是否允许 Stretch。
const StellarX::LayoutCapability& getLayoutCapability() const;
// 显式将当前运行态矩形提交为新的设计基线。普通 resize / 重排过程中不得自动调用。
void commitCurrentGeometryAsDesignRect();
protected: 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 saveStyle();
void restoreStyle(); void restoreStyle();
void resetEventVisualChanged() { eventVisualChanged = false; } void resetEventVisualChanged() { eventVisualChanged = false; }
+98 -1
View File
@@ -388,4 +388,101 @@ namespace StellarX
Top, Top,
Bottom Bottom
}; };
}
/*******************************************************************************
* @枚举: 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;
};
}
+1 -1
View File
@@ -1,7 +1,7 @@
/******************************************************************************* /*******************************************************************************
* @文件: StellarX.h * @文件: StellarX.h
* @摘要: 星垣(StellarX) GUI框架 - 主包含头文件 * @摘要: 星垣(StellarX) GUI框架 - 主包含头文件
* @版本: v3.0.1 * @版本: v3.0.2
* @描述: * @描述:
* 一个为Windows平台打造的轻量级、模块化C++ GUI框架。 * 一个为Windows平台打造的轻量级、模块化C++ GUI框架。
* 基于EasyX图形库,提供简洁易用的API和丰富的控件。 * 基于EasyX图形库,提供简洁易用的API和丰富的控件。
+42 -24
View File
@@ -62,6 +62,9 @@ inline void TabControl::initTabBar()
inline void TabControl::initTabPage() inline void TabControl::initTabPage()
{ {
if (controls.empty())return; if (controls.empty())return;
// TabControl 内部页签页仍然保留专用布局:
// 这里负责把“当前选项卡容器矩形”拆分成页签栏和页面区,
// 不把这部分细节下放到通用布局解算器里。
//子控件坐标原点 //子控件坐标原点
int nX = 0; int nX = 0;
int nY = 0; int nY = 0;
@@ -148,6 +151,15 @@ inline void TabControl::initTabPage()
} }
} }
void TabControl::refreshRuntimeLayout()
{
// 这是 TabControl 的内部专用布局入口:
// 外层先通过统一解算得到 TabControl 自身矩形,
// 再由这里继续安置页签按钮和页面区。
initTabBar();
initTabPage();
}
TabControl::TabControl() :Canvas() TabControl::TabControl() :Canvas()
{ {
this->id = "TabControl"; this->id = "TabControl";
@@ -165,28 +177,38 @@ TabControl::~TabControl()
void TabControl::setX(int x) void TabControl::setX(int x)
{ {
this->x = x; applyRuntimeRectDirect(x, y, width, height);
initTabBar(); refreshRuntimeLayout();
initTabPage(); onWindowResize();
dirty = true;
for (auto& c : controls)
{
c.first->onWindowResize();
c.second->onWindowResize();
}
} }
void TabControl::setY(int y) void TabControl::setY(int y)
{ {
this->y = y; applyRuntimeRectDirect(x, y, width, height);
initTabBar(); refreshRuntimeLayout();
initTabPage(); onWindowResize();
dirty = true; }
for (auto& c : controls)
{ void TabControl::setWidth(int width)
c.first->onWindowResize(); {
c.second->onWindowResize(); 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() void TabControl::draw()
@@ -348,18 +370,14 @@ void TabControl::setIsVisible(bool visible)
void TabControl::onWindowResize() void TabControl::onWindowResize()
{ {
// 调用基类的窗口变化处理,丢弃快照并标记脏 // 本轮不再在 onWindowResize 中重做页签布局,
// 这里只负责失效快照、标脏,并把 resize 语义向页签按钮和页面传递。
Control::onWindowResize(); Control::onWindowResize();
// 根据当前 TabControl 的新尺寸重新计算页签栏和页面区域
initTabBar();
initTabPage();
// 转发窗口尺寸变化给所有页签按钮和页面
for (auto& c : controls) for (auto& c : controls)
{ {
c.first->onWindowResize(); c.first->onWindowResize();
c.second->onWindowResize(); c.second->onWindowResize();
} }
// 尺寸变化后需要重绘自身
dirty = true; dirty = true;
} }
+7
View File
@@ -39,6 +39,8 @@ private:
// 初始化页签按钮位置和尺寸 // 初始化页签按钮位置和尺寸
inline void initTabBar(); inline void initTabBar();
inline void initTabPage(); inline void initTabPage();
// 统一刷新 TabControl 当前运行态下的页签栏和页面区布局。
void refreshRuntimeLayout();
public: public:
TabControl(); TabControl();
TabControl(int x, int y, int width, int height); TabControl(int x, int y, int width, int height);
@@ -47,6 +49,8 @@ public:
//重写位置设置以适应页签和页面布局 //重写位置设置以适应页签和页面布局
void setX(int x)override; void setX(int x)override;
void setY(int y)override; void setY(int y)override;
void setWidth(int width) override;
void setHeight(int height) override;
void draw() override; void draw() override;
bool handleEvent(const ExMessage& msg) override; bool handleEvent(const ExMessage& msg) override;
@@ -76,4 +80,7 @@ public:
void requestRepaint(Control* parent)override; // 托管模式下登记为 root;非托管模式下局部更新脏按钮/脏页面 void requestRepaint(Control* parent)override; // 托管模式下登记为 root;非托管模式下局部更新脏按钮/脏页面
bool canCommitManagedPartialRepaint() const override; // 判断当前 TabControl 是否可安全做局部提交 bool canCommitManagedPartialRepaint() const override; // 判断当前 TabControl 是否可安全做局部提交
void commitManagedRepaint() override; // 托管收口阶段执行 TabControl 的真正重绘 void commitManagedRepaint() override; // 托管收口阶段执行 TabControl 的真正重绘
protected:
// 外层统一解算后,TabControl 需要同步刷新其内部页签栏和页面区。
void applyResolvedLayoutRect(const StellarX::ResolvedLayoutRect& rect) override;
}; };
+22 -1
View File
@@ -355,17 +355,38 @@ void Table::setWidth(int width)
void Table::setHeight(int height) void Table::setHeight(int height)
{ {
//高度不变 // 当前阶段 Table 明确不支持纵向 Stretch。
// 高度链路依赖表头、表体、页脚、按钮和页码计算,
// 因此这里保持空实现,避免被通用布局层错误拉高/压缩。
} }
Table::Table(int x, int y) Table::Table(int x, int y)
:Control(x, y, 0, 0) :Control(x, y, 0, 0)
{ {
this->id = "Table"; this->id = "Table";
// Table 当前正式能力边界:
// 仅允许 X 轴 StretchY 轴固定尺寸。
this->layoutCapability.allowStretchY = false;
} }
Table::~Table() = default; 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() void Table::draw()
{ {
//在这里先初始化保证翻页按钮不为空 //在这里先初始化保证翻页按钮不为空
+3
View File
@@ -93,6 +93,9 @@ private:
void drawHeader(); //绘制表头 void drawHeader(); //绘制表头
void drawPageNum(); //绘制页码信息 void drawPageNum(); //绘制页码信息
void drawButton(); //绘制翻页按钮 void drawButton(); //绘制翻页按钮
// 统一解算后的内部应用入口:
// Table 需要复用自己已有的 setWidth/setX 等副作用逻辑,因此单独接管应用过程。
void applyResolvedLayoutRect(const StellarX::ResolvedLayoutRect& rect) override;
private: private:
//用来检查对话框是否模态,此控件不做实现 //用来检查对话框是否模态,此控件不做实现
bool model() const override { return false; }; bool model() const override { return false; };
+6 -66
View File
@@ -981,72 +981,12 @@ void Window::scheduleResizeFromModal(int w, int h)
void Window::adaptiveLayout(std::unique_ptr<Control>& c, const int finalH, const int finalW) void Window::adaptiveLayout(std::unique_ptr<Control>& c, const int finalH, const int finalW)
{ {
int origParentW = this->localwidth; // 顶层窗口布局收口:
int origParentH = this->localheight; // 先用统一解算器求出控件新的运行态矩形,再通知控件“外部环境已变化”。
if (c->getLayoutMode() == StellarX::LayoutMode::AnchorToEdges) // onWindowResize() 负责快照失效/标脏,不再承担布局公式。
{ const StellarX::ResolvedLayoutRect rect =
if ((StellarX::Anchor::Left == c->getAnchor_1() && StellarX::Anchor::Right == c->getAnchor_2()) c->resolveLayoutRect(this->localwidth, this->localheight, 0, 0, finalW, finalH);
|| (StellarX::Anchor::Right == c->getAnchor_1() && StellarX::Anchor::Left == c->getAnchor_2())) c->applyResolvedLayoutRect(rect);
{
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());
}
}
c->onWindowResize(); c->onWindowResize();
} }
+215 -1
View File
@@ -1,6 +1,220 @@
// StellarX 星垣GUI框架 - 测试用例 // StellarX 星垣GUI框架 - 测试用例
#include"StellarX.h" #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<Canvas>(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<Label>(18, 8, "KEY5:布局系统专项回归");
headerTitle->textStyle.nHeight = 26;
headerTitle->setTextdisap(true);
auto headerLine1 = std::make_unique<Label>(18, 46, "观察点 1:蓝色区域横向拉伸;绿色嵌套区域随父容器一起变化。");
headerLine1->setTextdisap(true);
auto headerLine2 = std::make_unique<Label>(18, 70, "观察点 2:橙色区域纵向拉伸;紫色区域中 Table 只做横向拉伸;粉色区域整体随底边移动。");
headerLine2->setTextdisap(true);
headerPtr->addControl(std::move(headerTitle));
headerPtr->addControl(std::move(headerLine1));
headerPtr->addControl(std::move(headerLine2));
auto horizontalZone = std::make_unique<Canvas>(20, 140, 1240, 220);
auto horizontalZonePtr = horizontalZone.get();
horizontalZonePtr->setShape(StellarX::ControlShape::ROUND_RECTANGLE);
horizontalZonePtr->setCanvasBkColor(horizontalColor);
horizontalZonePtr->setLayoutMode(StellarX::LayoutMode::AnchorToEdges);
horizontalZonePtr->setAnchor(StellarX::Anchor::Left, StellarX::Anchor::Right);
auto horizontalTitle = std::make_unique<Label>(18, 10, "区域 A(蓝):顶层 Left+Right 拉伸,内部同时验证 Left only / Right only / NoAnchor / 嵌套 Canvas。");
horizontalTitle->setTextdisap(true);
auto fixedLabel = std::make_unique<Label>(18, 42, "固定参考标签:不设置锚点");
fixedLabel->setTextdisap(true);
auto leftFixedBtn = std::make_unique<Button>(18, 78, 130, 36, "左边固定");
leftFixedBtn->setLayoutMode(StellarX::LayoutMode::AnchorToEdges);
leftFixedBtn->setAnchor(StellarX::Anchor::Left, StellarX::Anchor::NoAnchor);
auto rightFixedBtn = std::make_unique<Button>(1090, 78, 130, 36, "右边固定");
rightFixedBtn->setLayoutMode(StellarX::LayoutMode::AnchorToEdges);
rightFixedBtn->setAnchor(StellarX::Anchor::Right, StellarX::Anchor::NoAnchor);
auto stretchBox = std::make_unique<TextBox>(178, 78, 892, 36, "左右拉伸输入框:窗口变宽时我会伸长");
stretchBox->setLayoutMode(StellarX::LayoutMode::AnchorToEdges);
stretchBox->setAnchor(StellarX::Anchor::Left, StellarX::Anchor::Right);
auto nestedZone = std::make_unique<Canvas>(80, 126, 1080, 78);
auto nestedZonePtr = nestedZone.get();
nestedZonePtr->setShape(StellarX::ControlShape::ROUND_RECTANGLE);
nestedZonePtr->setCanvasBkColor(nestedColor);
nestedZonePtr->setLayoutMode(StellarX::LayoutMode::AnchorToEdges);
nestedZonePtr->setAnchor(StellarX::Anchor::Left, StellarX::Anchor::Right);
auto nestedTitle = std::make_unique<Label>(12, 10, "区域 A-1(绿):嵌套 Canvas。父容器拉伸后,这里的左右锚点和世界坐标都要同步更新。");
nestedTitle->setTextdisap(true);
auto nestedLeftBtn = std::make_unique<Button>(18, 40, 120, 30, "内层左固定");
nestedLeftBtn->setLayoutMode(StellarX::LayoutMode::AnchorToEdges);
nestedLeftBtn->setAnchor(StellarX::Anchor::Left, StellarX::Anchor::NoAnchor);
auto nestedRightBtn = std::make_unique<Button>(942, 40, 120, 30, "内层右固定");
nestedRightBtn->setLayoutMode(StellarX::LayoutMode::AnchorToEdges);
nestedRightBtn->setAnchor(StellarX::Anchor::Right, StellarX::Anchor::NoAnchor);
nestedZonePtr->addControl(std::move(nestedTitle));
nestedZonePtr->addControl(std::move(nestedLeftBtn));
nestedZonePtr->addControl(std::move(nestedRightBtn));
horizontalZonePtr->addControl(std::move(horizontalTitle));
horizontalZonePtr->addControl(std::move(fixedLabel));
horizontalZonePtr->addControl(std::move(leftFixedBtn));
horizontalZonePtr->addControl(std::move(rightFixedBtn));
horizontalZonePtr->addControl(std::move(stretchBox));
horizontalZonePtr->addControl(std::move(nestedZone));
auto verticalZone = std::make_unique<Canvas>(20, 380, 420, 278);
auto verticalZonePtr = verticalZone.get();
verticalZonePtr->setShape(StellarX::ControlShape::ROUND_RECTANGLE);
verticalZonePtr->setCanvasBkColor(verticalColor);
verticalZonePtr->setLayoutMode(StellarX::LayoutMode::AnchorToEdges);
verticalZonePtr->setAnchor(StellarX::Anchor::Top, StellarX::Anchor::Bottom);
auto verticalTitle = std::make_unique<Label>(18, 14, "区域 B(橙):Top+Bottom 拉伸,Bottom only 控件要随底边移动。");
verticalTitle->setTextdisap(true);
verticalTitle->setLayoutMode(StellarX::LayoutMode::AnchorToEdges);
verticalTitle->setAnchor(StellarX::Anchor::Top, StellarX::Anchor::NoAnchor);
auto verticalHint = std::make_unique<Label>(18, 88, "中部参考标签:不设置锚点,用来观察固定位置。");
verticalHint->setTextdisap(true);
auto bottomBox = std::make_unique<TextBox>(22, 204, 220, 34, "底边固定输入框");
bottomBox->setLayoutMode(StellarX::LayoutMode::AnchorToEdges);
bottomBox->setAnchor(StellarX::Anchor::Bottom, StellarX::Anchor::NoAnchor);
auto bottomBtn = std::make_unique<Button>(270, 204, 120, 34, "底边固定");
bottomBtn->setLayoutMode(StellarX::LayoutMode::AnchorToEdges);
bottomBtn->setAnchor(StellarX::Anchor::Bottom, StellarX::Anchor::NoAnchor);
verticalZonePtr->addControl(std::move(verticalTitle));
verticalZonePtr->addControl(std::move(verticalHint));
verticalZonePtr->addControl(std::move(bottomBox));
verticalZonePtr->addControl(std::move(bottomBtn));
auto tabControl = std::make_unique<TabControl>(460, 380, 800, 168);
auto tabControlPtr = tabControl.get();
tabControlPtr->setShape(StellarX::ControlShape::ROUND_RECTANGLE);
tabControlPtr->setCanvasfillMode(StellarX::FillMode::Null);
tabControlPtr->setTabPlacement(StellarX::TabPlacement::Top);
tabControlPtr->setTabBarHeight(28);
tabControlPtr->setLayoutMode(StellarX::LayoutMode::AnchorToEdges);
tabControlPtr->setAnchor(StellarX::Anchor::Left, StellarX::Anchor::Right);
auto tabPage1 = std::make_unique<Canvas>(0, 0, 800, 132);
auto tabPage1Ptr = tabPage1.get();
tabPage1Ptr->setShape(StellarX::ControlShape::ROUND_RECTANGLE);
tabPage1Ptr->setCanvasBkColor(RGB(241, 247, 255));
auto tabPage2 = std::make_unique<Canvas>(0, 0, 800, 132);
auto tabPage2Ptr = tabPage2.get();
tabPage2Ptr->setShape(StellarX::ControlShape::ROUND_RECTANGLE);
tabPage2Ptr->setCanvasBkColor(RGB(248, 244, 236));
auto page1Label = std::make_unique<Label>(16, 12, "区域 C(页 1):TabControl 外层参与统一解算,页内 TextBox 继续由 Canvas 负责拉伸。");
page1Label->setTextdisap(true);
auto page1Box = std::make_unique<TextBox>(20, 52, 650, 34, "页内左右拉伸输入框");
page1Box->setLayoutMode(StellarX::LayoutMode::AnchorToEdges);
page1Box->setAnchor(StellarX::Anchor::Left, StellarX::Anchor::Right);
auto page1Btn = std::make_unique<Button>(692, 52, 90, 34, "右固定");
page1Btn->setLayoutMode(StellarX::LayoutMode::AnchorToEdges);
page1Btn->setAnchor(StellarX::Anchor::Right, StellarX::Anchor::NoAnchor);
tabPage1Ptr->addControl(std::move(page1Label));
tabPage1Ptr->addControl(std::move(page1Box));
tabPage1Ptr->addControl(std::move(page1Btn));
auto page2Label = std::make_unique<Label>(16, 18, "区域 C(页 2):切页后再次拖动窗口,页签栏和页区域都应保持稳定。");
page2Label->setTextdisap(true);
auto page2Btn = std::make_unique<Button>(20, 60, 120, 34, "普通按钮");
tabPage2Ptr->addControl(std::move(page2Label));
tabPage2Ptr->addControl(std::move(page2Btn));
tabControlPtr->add(std::make_pair(std::make_unique<Button>(0, 0, 120, 28, "布局页"), std::move(tabPage1)));
tabControlPtr->add(std::make_pair(std::make_unique<Button>(0, 0, 120, 28, "切换页"), std::move(tabPage2)));
tabControlPtr->setActiveIndex(0);
auto tableZone = std::make_unique<Canvas>(460, 566, 800, 132);
auto tableZonePtr = tableZone.get();
tableZonePtr->setShape(StellarX::ControlShape::ROUND_RECTANGLE);
tableZonePtr->setCanvasBkColor(tableZoneColor);
tableZonePtr->setLayoutMode(StellarX::LayoutMode::AnchorToEdges);
tableZonePtr->setAnchor(StellarX::Anchor::Left, StellarX::Anchor::Right);
auto tableTitle = std::make_unique<Label>(16, 10, "区域 D(紫):Table 当前只回归 X 轴拉伸,不做 Y 轴拉伸。");
tableTitle->setTextdisap(true);
auto table = std::make_unique<Table>(20, 46);
auto tablePtr = table.get();
tablePtr->setHeaders({ "编号", "回归点", "预期" });
tablePtr->setData({
{"01", "左右拉伸", "窗口变宽后列宽一起增大"},
{"02", "纵向固定", "表格高度保持当前设计值"},
{"03", "页脚重排", "分页按钮和页码随宽度重排"}
});
tablePtr->setRowsPerPage(2);
tablePtr->setTableBorderWidth(1);
tablePtr->setTableBorder(RGB(86, 88, 132));
tablePtr->setLayoutMode(StellarX::LayoutMode::AnchorToEdges);
tablePtr->setAnchor(StellarX::Anchor::Left, StellarX::Anchor::Right);
tableZonePtr->addControl(std::move(tableTitle));
tableZonePtr->addControl(std::move(table));
auto movingZone = std::make_unique<Canvas>(934, 706, 326, 70);
auto movingZonePtr = movingZone.get();
movingZonePtr->setShape(StellarX::ControlShape::ROUND_RECTANGLE);
movingZonePtr->setCanvasBkColor(movingColor);
movingZonePtr->setLayoutMode(StellarX::LayoutMode::AnchorToEdges);
movingZonePtr->setAnchor(StellarX::Anchor::Bottom, StellarX::Anchor::NoAnchor);
auto movingLabel = std::make_unique<Label>(14, 10, "区域 E(粉):Bottom only 容器。");
movingLabel->setTextdisap(true);
auto movingHint = std::make_unique<Label>(14, 30, "窗口变高时整体向下移动。");
movingHint->setTextdisap(true);
auto movingBtn = std::make_unique<Button>(208, 38, 100, 24, "右固定");
movingBtn->setLayoutMode(StellarX::LayoutMode::AnchorToEdges);
movingBtn->setAnchor(StellarX::Anchor::Right, StellarX::Anchor::NoAnchor);
movingZonePtr->addControl(std::move(movingLabel));
movingZonePtr->addControl(std::move(movingHint));
movingZonePtr->addControl(std::move(movingBtn));
win.addControl(std::move(header));
win.addControl(std::move(horizontalZone));
win.addControl(std::move(verticalZone));
win.addControl(std::move(tabControl));
win.addControl(std::move(tableZone));
win.addControl(std::move(movingZone));
win.draw();
return win.runEventLoop();
}
#endif
#if 1 == KEY #if 1 == KEY
int main() int main()