Finalize layout stage 2 fixes and refresh regression scenes

This commit is contained in:
Codex
2026-04-16 11:40:39 +08:00
parent b7ad960518
commit 738cf035bb
20 changed files with 1470 additions and 308 deletions
+83 -1
View File
@@ -432,6 +432,8 @@ bool Button::handleEvent(const ExMessage& msg)
// 事件吞噬规则: // 事件吞噬规则:
// - 鼠标移动:只有“当前命中按钮”时才吞掉,避免前一个按钮在清 hover 时截断消息, // - 鼠标移动:只有“当前命中按钮”时才吞掉,避免前一个按钮在清 hover 时截断消息,
// 导致后一个真正命中的按钮收不到 WM_MOUSEMOVE。 // 导致后一个真正命中的按钮收不到 WM_MOUSEMOVE。
// 这里的前提是:父容器会保证“没有拿到真实消息的后续兄弟”还能走一遍
// clearTransientMouseState(),从而清掉旧 hover / tooltip。
// - 鼠标按下/抬起:命中按钮区域时吞掉,避免点击穿透到底层控件。 // - 鼠标按下/抬起:命中按钮区域时吞掉,避免点击穿透到底层控件。
if (msg.message == WM_MOUSEMOVE) if (msg.message == WM_MOUSEMOVE)
{ {
@@ -451,6 +453,79 @@ bool Button::handleEvent(const ExMessage& msg)
return consume; return consume;
} }
bool Button::clearTransientMouseState()
{
if (!show)
return false;
bool hoverChanged = false;
bool normalClickChanged = false;
bool bodyVisualChanged = false;
bool tooltipChanged = false;
if (hover)
{
hover = false;
hoverChanged = true;
bodyVisualChanged = true;
}
// NORMAL 模式下的按下态只属于一次鼠标交互过程;
// 如果本次 WM_MOUSEMOVE 已经被同层更上层的兄弟控件消费,
// 当前按钮就不应继续保留这笔临时按下视觉。
if (mode == StellarX::ButtonMode::NORMAL && click)
{
click = false;
normalClickChanged = true;
bodyVisualChanged = true;
}
if (tipVisible)
{
hideTooltip();
tooltipChanged = true;
}
const bool stateChanged = bodyVisualChanged || tooltipChanged;
if (!stateChanged)
return false;
SX_LOG_TRACE("Event")
<< SX_T("清理按钮鼠标临时状态:id=", "clear button transient mouse state: id=")
<< id
<< SX_T(" hover=", " hover=") << (hoverChanged ? 1 : 0)
<< SX_T(" tooltip=", " tooltip=") << (tooltipChanged ? 1 : 0)
<< SX_T(" normalClick=", " normalClick=") << (normalClickChanged ? 1 : 0);
markEventVisualChanged(true);
// 只有按钮本体视觉真正变化时,才请求按钮自身区域重绘;
// 若只是 Tooltip 消失,hideTooltip() 已经通过回贴快照清掉了悬浮层区域,
// 不再额外重绘按钮本体,避免高频鼠标移动时的无意义重画。
if (bodyVisualChanged)
{
dirty = true;
requestRepaint(parent);
}
return true;
}
RECT Button::getManagedRepaintCoverageRect() const
{
RECT coverage = getBoundsRect();
if (!tipEnabled || !tipVisible)
return coverage;
const RECT tipRect = tipLabel.getManagedRepaintCoverageRect();
coverage.left = (std::min)(coverage.left, tipRect.left);
coverage.top = (std::min)(coverage.top, tipRect.top);
coverage.right = (std::max)(coverage.right, tipRect.right);
coverage.bottom = (std::max)(coverage.bottom, tipRect.bottom);
return coverage;
}
void Button::setOnClickListener(std::function<void()> callback) void Button::setOnClickListener(std::function<void()> callback)
{ {
this->onClickCallback = std::move(callback); this->onClickCallback = std::move(callback);
@@ -558,7 +633,14 @@ void Button::setButtonShape(StellarX::ControlShape shape)
//允许通过外部函数修改按钮的点击状态,并执行相应的回调函数 //允许通过外部函数修改按钮的点击状态,并执行相应的回调函数
void Button::setButtonClick(BOOL click) void Button::setButtonClick(BOOL click)
{ {
this->click = click; const bool targetClick = (click != FALSE);
// TOGGLE 状态若没有发生变化,就不应重复触发 onToggleOn/OffCallback。
// 否则像 TabControl 这种“外部再次激活已激活页签”的场景,
// 会把页面显示/隐藏、快照失效、重绘链整条重复执行一遍,最终破坏可见页的背景恢复语义。
if (mode == StellarX::ButtonMode::TOGGLE && this->click == targetClick)
return;
this->click = targetClick;
if (mode == StellarX::ButtonMode::NORMAL && click) if (mode == StellarX::ButtonMode::NORMAL && click)
{ {
+43 -36
View File
@@ -101,87 +101,94 @@ public:
void draw() override; void draw() override;
//按钮事件处理 //按钮事件处理
bool handleEvent(const ExMessage& msg) override; bool handleEvent(const ExMessage& msg) override;
// 清理按钮的鼠标瞬时状态;用于父容器在 WM_MOUSEMOVE 短路后补做 hover/tooltip 收口。
bool clearTransientMouseState() override;
// Tooltip 可见时,按钮实际写像素范围不再等于按钮本体,需要把 Tooltip 矩形并入 coverage。
RECT getManagedRepaintCoverageRect() const override;
//设置回调函数 // 设置 NORMAL 模式下的点击回调
void setOnClickListener(std::function<void()> callback); void setOnClickListener(std::function<void()> callback);
//设置TOGGLE模式下被点击的回调函数 // 设置 TOGGLE 模式下切换到打开状态时的回调
void setOnToggleOnListener(std::function<void()> callback); void setOnToggleOnListener(std::function<void()> callback);
//设置TOGGLE模式下取消点击的回调函数 // 设置 TOGGLE 模式下切换到关闭状态时的回调
void setOnToggleOffListener(std::function<void()> callback); void setOnToggleOffListener(std::function<void()> callback);
//设置按钮模式 // 设置按钮模式NORMAL / TOGGLE / DISABLED
void setbuttonMode(StellarX::ButtonMode mode); void setbuttonMode(StellarX::ButtonMode mode);
//设置圆角矩形椭圆宽度 // 设置圆角矩形的圆角宽度
void setROUND_RECTANGLEwidth(int width); void setROUND_RECTANGLEwidth(int width);
//设置圆角矩形椭圆高度 // 设置圆角矩形的圆角高度
void setROUND_RECTANGLEheight(int height); void setROUND_RECTANGLEheight(int height);
//设置按钮填充模式 // 设置按钮填充模式
void setFillMode(StellarX::FillMode mode); void setFillMode(StellarX::FillMode mode);
//设置按钮填充图案 // 设置按钮图案填充样式
void setFillIma(StellarX::FillStyle ima); void setFillIma(StellarX::FillStyle ima);
//设置按钮填充图像 // 设置按钮图像填充资源
void setFillIma(std::string imaName); void setFillIma(std::string imaName);
//设置按钮边框颜色 // 设置按钮边框颜色
void setButtonBorder(COLORREF Border); void setButtonBorder(COLORREF Border);
//设置按钮未被点击颜色 // 设置按钮默认态颜色
void setButtonFalseColor(COLORREF color); void setButtonFalseColor(COLORREF color);
//设置按钮文本 // 设置按钮文本char* 重载)
void setButtonText(const char* text); void setButtonText(const char* text);
// 设置按钮文本(std::string 重载)
void setButtonText(std::string text); void setButtonText(std::string text);
//设置按钮形状 // 设置按钮几何形状
void setButtonShape(StellarX::ControlShape shape); void setButtonShape(StellarX::ControlShape shape);
//设置按钮点击状态 // 直接设置按钮点击状态;TOGGLE 模式下会按状态变化触发相应回调
void setButtonClick(BOOL click); void setButtonClick(BOOL click);
//判断按钮是否被点击 // 查询按钮当前是否处于点击/选中状态
bool isClicked() const; bool isClicked() const;
//获取按钮文 // 获取按钮文
std::string getButtonText() const; std::string getButtonText() const;
// 获取按钮文本的 C 字符串视图
const char* getButtonText_c() const; const char* getButtonText_c() const;
//获取按钮模式 // 获取按钮模式
StellarX::ButtonMode getButtonMode() const; StellarX::ButtonMode getButtonMode() const;
//获取按钮形状 // 获取按钮形状
StellarX::ControlShape getButtonShape() const; StellarX::ControlShape getButtonShape() const;
//获取按钮填充模式 // 获取按钮填充模式
StellarX::FillMode getFillMode() const; StellarX::FillMode getFillMode() const;
//获取按钮填充图案 // 获取按钮图案填充样式
StellarX::FillStyle getFillIma() const; StellarX::FillStyle getFillIma() const;
//获取按钮填充图像 // 获取按钮图像填充资源
IMAGE* getFillImaImage() const; IMAGE* getFillImaImage() const;
//获取按钮边框颜色 // 获取按钮边框颜色
COLORREF getButtonBorder() const; COLORREF getButtonBorder() const;
//获取按钮文字颜色 // 获取按钮文字颜色
COLORREF getButtonTextColor() const; COLORREF getButtonTextColor() const;
//获取按钮文字样式 // 获取按钮文字样式
StellarX::ControlText getButtonTextStyle() const; StellarX::ControlText getButtonTextStyle() const;
public: public:
// === Tooltip API=== // === Tooltip API===
//设置是否启用提示框 // 开关 Tooltip;关闭时只修改内部启用/显示状态,不负责智能选位
void enableTooltip(bool on) { tipEnabled = on; if (!on) tipVisible = false; } void enableTooltip(bool on) { tipEnabled = on; if (!on) tipVisible = false; }
//设置提示框延时 // 设置 Tooltip 延时(毫秒)
void setTooltipDelay(int ms) { tipDelayMs = (ms < 0 ? 0 : ms); } void setTooltipDelay(int ms) { tipDelayMs = (ms < 0 ? 0 : ms); }
//设置提示框是否跟随鼠标 // 设置 Tooltip 是否跟随鼠标
void setTooltipFollowCursor(bool on) { tipFollowCursor = on; } void setTooltipFollowCursor(bool on) { tipFollowCursor = on; }
//设置提示框位置偏移 // 设置 Tooltip 相对鼠标/按钮的偏移
void setTooltipOffset(int dx, int dy) { tipOffsetX = dx; tipOffsetY = dy; } void setTooltipOffset(int dx, int dy) { tipOffsetX = dx; tipOffsetY = dy; }
//设置提示框样式 // 设置 Tooltip 的文字、背景和透明样式
void setTooltipStyle(COLORREF text, COLORREF bk, bool transparent); void setTooltipStyle(COLORREF text, COLORREF bk, bool transparent);
//设置提示框文本 // 设置 NORMAL 模式下 Tooltip 文本
void setTooltipText(const std::string& s) { tipTextClick = s; tipUserOverride = true; } void setTooltipText(const std::string& s) { tipTextClick = s; tipUserOverride = true; }
// 设置 TOGGLE 模式开/关两种状态下的 Tooltip 文本
void setTooltipTextsForToggle(const std::string& onText, const std::string& offText); void setTooltipTextsForToggle(const std::string& onText, const std::string& offText);
private: private:
//初始化按钮 // 初始化按钮内部状态、颜色和 Tooltip 默认样式
void initButton(const std::string text, StellarX::ButtonMode mode, StellarX::ControlShape shape, COLORREF ct, COLORREF cf, COLORREF ch); void initButton(const std::string text, StellarX::ButtonMode mode, StellarX::ControlShape shape, COLORREF ct, COLORREF cf, COLORREF ch);
//判断鼠标是否在圆形按钮内 // 判断鼠标是否在圆形按钮内
bool isMouseInCircle(int mouseX, int mouseY, int x, int y, int radius); bool isMouseInCircle(int mouseX, int mouseY, int x, int y, int radius);
//判断鼠标是否在椭圆按钮内 // 判断鼠标是否在椭圆按钮内
bool isMouseInEllipse(int mouseX, int mouseY, int x, int y, int width, int height); bool isMouseInEllipse(int mouseX, int mouseY, int x, int y, int width, int height);
//获取对话框类型 //获取对话框类型
bool model() const override { return false; } bool model() const override { return false; }
//文本截断 // 按当前按钮宽度和语言特征裁剪文本
void cutButtonText(); void cutButtonText();
// 统一隐藏&恢复背景 // 统一隐藏 Tooltip 并恢复背景快照
void hideTooltip(); void hideTooltip();
// 根据当前 click 状态选择文案 // 根据当前 click 状态选择 Tooltip 文案
void refreshTooltipTextForState(); void refreshTooltipTextForState();
}; };
+171 -13
View File
@@ -20,16 +20,45 @@ static const char* SxCanvasMsgName(UINT m)
} }
} }
namespace
{
bool SxCanvasRectValid(const RECT& rc)
{
return rc.right > rc.left && rc.bottom > rc.top;
}
bool SxCanvasRectsIntersect(const RECT& a, const RECT& b)
{
return a.left < b.right && a.right > b.left &&
a.top < b.bottom && a.bottom > b.top;
}
RECT SxCanvasUnionRect(const RECT& a, const RECT& b)
{
RECT out{};
out.left = (std::min)(a.left, b.left);
out.top = (std::min)(a.top, b.top);
out.right = (std::max)(a.right, b.right);
out.bottom = (std::max)(a.bottom, b.bottom);
return out;
}
}
Canvas::Canvas() Canvas::Canvas()
:Control(0, 0, 100, 100) :Control(0, 0, 100, 100)
{ {
this->id = "Canvas"; this->id = "Canvas";
// Canvas 是通用容器,当前阶段显式允许双轴 Stretch。
this->layoutCapability.allowStretchX = true;
this->layoutCapability.allowStretchY = true;
} }
Canvas::Canvas(int x, int y, int width, int height) Canvas::Canvas(int x, int y, int width, int height)
:Control(x, y, width, height) :Control(x, y, width, height)
{ {
this->id = "Canvas"; this->id = "Canvas";
this->layoutCapability.allowStretchX = true;
this->layoutCapability.allowStretchY = true;
} }
void Canvas::relayoutManagedChildren() void Canvas::relayoutManagedChildren()
@@ -162,19 +191,46 @@ bool Canvas::handleEvent(const ExMessage& msg)
bool anyVisualChanged = false; bool anyVisualChanged = false;
Control* firstConsumer = nullptr; Control* firstConsumer = nullptr;
for (auto it = controls.rbegin(); it != controls.rend(); ++it) if (msg.message == WM_MOUSEMOVE)
{ {
Control* c = it->get(); // WM_MOUSEMOVE 需要特殊处理:
bool cConsumed = c->handleEvent(msg); // - 第一个命中的兄弟分支收到真实消息;
// - 后续兄弟不再重新命中,只清理旧 hover / tooltip 等临时状态。
if (c->isDirty()) anyDirty = true; for (auto it = controls.rbegin(); it != controls.rend(); ++it)
if (c->didEventAffectVisual()) anyVisualChanged = true;
if (cConsumed)
{ {
firstConsumer = c; Control* c = it->get();
consumed = true; if (!consumed)
break; {
bool cConsumed = c->handleEvent(msg);
if (c->didEventAffectVisual()) anyVisualChanged = true;
if (cConsumed)
{
firstConsumer = c;
consumed = true;
}
}
else if (c->clearTransientMouseState())
{
anyVisualChanged = true;
}
}
}
else
{
for (auto it = controls.rbegin(); it != controls.rend(); ++it)
{
Control* c = it->get();
bool cConsumed = c->handleEvent(msg);
if (c->isDirty()) anyDirty = true;
if (c->didEventAffectVisual()) anyVisualChanged = true;
if (cConsumed)
{
firstConsumer = c;
consumed = true;
break;
}
} }
} }
@@ -198,6 +254,20 @@ bool Canvas::handleEvent(const ExMessage& msg)
return consumed; return consumed;
} }
bool Canvas::clearTransientMouseState()
{
bool changed = false;
for (auto it = controls.rbegin(); it != controls.rend(); ++it)
{
Control* child = it->get();
if (!child->IsVisible())
continue;
if (child->clearTransientMouseState())
changed = true;
}
return changed;
}
void Canvas::addControl(std::unique_ptr<Control> control) void Canvas::addControl(std::unique_ptr<Control> control)
{ {
@@ -333,9 +403,68 @@ void Canvas::requestRepaint(Control* parent)
SX_LOG_TRACE("Dirty") << SX_T("Canvas 请求局部重绘:id=", "Canvas::requestRepaint(partial): id=") << id; SX_LOG_TRACE("Dirty") << SX_T("Canvas 请求局部重绘:id=", "Canvas::requestRepaint(partial): id=") << id;
RECT coverage{};
bool hasCoverage = false;
auto commitManagedChild = [&](Control* child, bool forceOverlayRedraw)
{
if (!child || !child->IsVisible())
return;
const bool directDirty = child->isDirty();
const bool subtreeDirty = child->hasManagedDirtySubtree();
if (forceOverlayRedraw)
{
// overlay 补画必须先作废旧快照:
// 下层兄弟刚刚已经写过像素,若继续沿用旧快照,会把旧背景再贴回来。
child->invalidateBackgroundSnapshot();
child->setDirty(true);
child->draw();
}
else if (directDirty)
{
child->draw();
}
else if (subtreeDirty)
{
// 这次真正脏的是“child 下面的子树”,而不是 child 自身。
// 例如嵌套 Canvas 中,第二层/第三层按钮脏了,但第一层 Canvas 自己并不 dirty。
// 这里必须把这条直接子分支提交下去,否则深层按钮状态永远没有机会被真正画出来。
child->commitManagedRepaint();
}
else
{
return;
}
const RECT childRect = child->getManagedRepaintCoverageRect();
if (!hasCoverage)
{
coverage = childRect;
hasCoverage = true;
}
else
{
coverage = SxCanvasUnionRect(coverage, childRect);
}
};
for (auto& control : controls) for (auto& control : controls)
if (control->isDirty() && control->IsVisible()) {
control->draw(); Control* child = control.get();
if (!child->IsVisible())
continue;
if (child->hasManagedDirtySubtree())
{
commitManagedChild(child, false);
}
else if (hasCoverage && SxCanvasRectsIntersect(child->getManagedRepaintCoverageRect(), coverage))
{
// 位于本次累计 coverage 上方、且发生相交的兄弟控件,需要补画回最上层。
commitManagedChild(child, true);
}
}
return; return;
} }
@@ -344,6 +473,35 @@ void Canvas::requestRepaint(Control* parent)
onRequestRepaintAsRoot(); onRequestRepaintAsRoot();
} }
bool Canvas::hasManagedDirtySubtree() const
{
if (dirty)
return true;
for (const auto& child : controls)
{
if (!child->IsVisible())
continue;
if (child->hasManagedDirtySubtree())
return true;
}
return false;
}
RECT Canvas::getManagedRepaintCoverageRect() const
{
// Canvas::draw() 会先写自身背景,再强制绘制全部可见子控件。
// 因此它的实际 coverage 不能只看本体矩形,还要把可见子控件 coverage 递归并入。
RECT coverage = getBoundsRect();
for (const auto& child : controls)
{
if (!child->IsVisible())
continue;
coverage = SxCanvasUnionRect(coverage, child->getManagedRepaintCoverageRect());
}
return coverage;
}
bool Canvas::canCommitManagedPartialRepaint() const bool Canvas::canCommitManagedPartialRepaint() const
{ {
// Canvas 只有在“自己本体不脏 + 仍持有有效背景快照”时, // Canvas 只有在“自己本体不脏 + 仍持有有效背景快照”时,
+12 -2
View File
@@ -37,19 +37,27 @@ protected:
// 清除所有子控件 // 清除所有子控件
void clearAllControls(); void clearAllControls();
public: public:
// 默认构造:创建一个基础矩形容器
Canvas(); Canvas();
// 指定初始位置和尺寸构造容器
Canvas(int x, int y, int width, int height); Canvas(int x, int y, int width, int height);
~Canvas() {} ~Canvas() {}
// 修改容器运行态 X,并同步重算全部受管理子控件的世界坐标
void setX(int x)override; void setX(int x)override;
// 修改容器运行态 Y,并同步重算全部受管理子控件的世界坐标
void setY(int y)override; void setY(int y)override;
// 修改容器运行态宽度,并同步重算全部受管理子控件的世界坐标
void setWidth(int width) override; void setWidth(int width) override;
// 修改容器运行态高度,并同步重算全部受管理子控件的世界坐标
void setHeight(int height) override; void setHeight(int height) override;
//绘制容器及其子控件 //绘制容器及其子控件
void draw() override; void draw() override;
bool handleEvent(const ExMessage& msg) override; bool handleEvent(const ExMessage& msg) override;
//添加控件 // 递归清理子树中的 hover / tooltip / 临时按下态,不把 Canvas 自己升级为整块重绘源。
bool clearTransientMouseState() override;
// 添加一个由 Canvas 托管的子控件
void addControl(std::unique_ptr<Control> control); void addControl(std::unique_ptr<Control> control);
//设置容器样式 //设置容器样式
void setShape(StellarX::ControlShape shape); void setShape(StellarX::ControlShape shape);
@@ -69,11 +77,13 @@ public:
void onWindowResize() override; void onWindowResize() override;
// 托管模式下登记为 root;非托管模式下走局部或根级重绘 // 托管模式下登记为 root;非托管模式下走局部或根级重绘
void requestRepaint(Control* parent)override; void requestRepaint(Control* parent)override;
bool hasManagedDirtySubtree() const override;
RECT getManagedRepaintCoverageRect() const override;
// 判断当前 Canvas 是否可安全做局部提交 // 判断当前 Canvas 是否可安全做局部提交
bool canCommitManagedPartialRepaint() const override; bool canCommitManagedPartialRepaint() const override;
// 托管收口阶段执行 Canvas 的真正重绘 // 托管收口阶段执行 Canvas 的真正重绘
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: protected:
// 统一解算后,按当前运行态矩形把所有受管理子控件重新映射到新的世界坐标。 // 统一解算后,按当前运行态矩形把所有受管理子控件重新映射到新的世界坐标。
+72
View File
@@ -133,6 +133,35 @@ namespace
result.pos = childDesignPos; result.pos = childDesignPos;
return result; return result;
} }
const char* SxAxisName(bool horizontal)
{
return horizontal ? "X" : "Y";
}
void LogAxisDowngradeIfNeeded(const std::string& id, bool horizontal,
const StellarX::AxisLayoutSpec& spec, bool allowStretch)
{
if (spec.sizePolicy != StellarX::AxisSizePolicy::Stretch)
return;
if (!spec.anchorStart || !spec.anchorEnd)
{
SX_LOGD("Layout")
<< SX_T("布局降级:id=", "layout downgrade: id=") << id
<< SX_T(" axis=", " axis=") << SxAxisName(horizontal)
<< SX_T(" 原因=Stretch 但未同时锚定起止边", " reason=stretch without dual anchors");
return;
}
if (!allowStretch)
{
SX_LOGD("Layout")
<< SX_T("能力边界拦截:id=", "layout capability intercept: id=") << id
<< SX_T(" axis=", " axis=") << SxAxisName(horizontal)
<< SX_T(" 原因=当前控件禁止该轴 Stretch", " reason=stretch disabled by capability");
}
}
} }
StellarX::ControlText& StellarX::ControlText::operator=(const ControlText& text) StellarX::ControlText& StellarX::ControlText::operator=(const ControlText& text)
@@ -270,15 +299,31 @@ void Control::setHeight(int height)
dirty = true; dirty = true;
} }
bool Control::clearTransientMouseState()
{
// 基类默认没有 hover / tooltip / 临时按下态等鼠标瞬时状态。
// 具体控件若需要在“未收到本次 WM_MOUSEMOVE”时做清理,重写此接口即可。
return false;
}
void Control::commitCurrentGeometryAsDesignRect() void Control::commitCurrentGeometryAsDesignRect()
{ {
// 该接口是“显式提交新的设计基线”的唯一入口之一。 // 该接口是“显式提交新的设计基线”的唯一入口之一。
// 普通布局解算、父容器重排、窗口 resize 均不得自动回写 local* // 普通布局解算、父容器重排、窗口 resize 均不得自动回写 local*
// 否则会导致设计基线漂移,后续解算越来越不稳定。 // 否则会导致设计基线漂移,后续解算越来越不稳定。
SX_LOGD("Layout")
<< SX_T("提交设计基线:id=", "commit design rect: id=") << id
<< SX_T(" world=(", " world=(") << x << "," << y << "," << width << "," << height << ")"
<< SX_T(" parentLocalBefore=(", " parentLocalBefore=(")
<< localx << "," << localy << "," << localWidth << "," << localHeight << ")";
localx = parent ? (x - parent->getX()) : x; localx = parent ? (x - parent->getX()) : x;
localy = parent ? (y - parent->getY()) : y; localy = parent ? (y - parent->getY()) : y;
localWidth = width; localWidth = width;
localHeight = height; localHeight = height;
SX_LOGD("Layout")
<< SX_T("提交设计基线完成:id=", "commit design rect done: id=") << id
<< SX_T(" parentLocalAfter=(", " parentLocalAfter=(")
<< localx << "," << localy << "," << localWidth << "," << localHeight << ")";
} }
StellarX::ResolvedLayoutRect Control::resolveLayoutRect(int parentDesignW, int parentDesignH, StellarX::ResolvedLayoutRect Control::resolveLayoutRect(int parentDesignW, int parentDesignH,
@@ -299,6 +344,8 @@ StellarX::ResolvedLayoutRect Control::resolveLayoutRect(int parentDesignW, int p
} }
// 第 1 层:先在父局部坐标系内分别解算水平轴和垂直轴。 // 第 1 层:先在父局部坐标系内分别解算水平轴和垂直轴。
LogAxisDowngradeIfNeeded(id, true, layoutSpec.horizontal, layoutCapability.allowStretchX);
LogAxisDowngradeIfNeeded(id, false, layoutSpec.vertical, layoutCapability.allowStretchY);
const LayoutAxisResult horizontal = ResolveAxis(parentDesignW, parentCurrentW, localx, localWidth, const LayoutAxisResult horizontal = ResolveAxis(parentDesignW, parentCurrentW, localx, localWidth,
layoutSpec.horizontal, layoutCapability.allowStretchX); layoutSpec.horizontal, layoutCapability.allowStretchX);
const LayoutAxisResult vertical = ResolveAxis(parentDesignH, parentCurrentH, localy, localHeight, const LayoutAxisResult vertical = ResolveAxis(parentDesignH, parentCurrentH, localy, localHeight,
@@ -436,6 +483,17 @@ Control* Control::getManagedRepaintRoot()
return root; return root;
} }
Control* Control::getManagedRepaintDirectBranch(Control* root)
{
if (!root)
return nullptr;
Control* branch = this;
while (branch->parent && branch->parent != root)
branch = branch->parent;
return branch;
}
RECT Control::getBoundsRect() const RECT Control::getBoundsRect() const
{ {
RECT rc{}; RECT rc{};
@@ -446,6 +504,13 @@ RECT Control::getBoundsRect() const
return rc; return rc;
} }
RECT Control::getManagedRepaintCoverageRect() const
{
// 基类默认认为“实际绘制 coverage == 控件本体矩形”。
// 复合控件或带浮层的控件会 override,把附加绘制区域一并并入。
return getBoundsRect();
}
bool Control::canCommitManagedPartialRepaint() const bool Control::canCommitManagedPartialRepaint() const
{ {
// 基类默认不承诺自己能安全做局部提交; // 基类默认不承诺自己能安全做局部提交;
@@ -453,6 +518,13 @@ bool Control::canCommitManagedPartialRepaint() const
return false; return false;
} }
bool Control::hasManagedDirtySubtree() const
{
// 基类只感知自身是否为脏;
// 容器类会 override 为“自身或其受管理子树是否有脏内容”。
return dirty;
}
void Control::commitManagedRepaint() void Control::commitManagedRepaint()
{ {
if (!show) if (!show)
+26 -1
View File
@@ -124,6 +124,12 @@ 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; }
// 公开几何 setter
// - 面向用户使用
// - 默认只修改运行态矩形
// - 不允许自动回写设计基线 local*
// 若某个复合控件需要在 setter 后维护自己拥有的运行态子树一致性,
// 由该控件在 override 中自行处理,但仍不得越权承担统一布局器职责。
virtual void setX(int x); virtual void setX(int x);
virtual void setY(int y); virtual void setY(int y);
virtual void setWidth(int width); virtual void setWidth(int width);
@@ -132,6 +138,10 @@ public:
virtual void draw() = 0; virtual void draw() = 0;
virtual bool handleEvent(const ExMessage& msg) = 0;//返回true代表事件已消费 virtual bool handleEvent(const ExMessage& msg) = 0;//返回true代表事件已消费
// 轻量清理接口:仅用于“没有机会收到本次 WM_MOUSEMOVE”的兄弟分支,
// 清掉 hover / tooltip / 临时按下态等鼠标瞬时状态。
// 该接口不是事件分发入口,不参与新的命中判断,也不允许回写设计基线。
virtual bool clearTransientMouseState();
//设置是否显示 //设置是否显示
virtual void setIsVisible(bool show); virtual void setIsVisible(bool show);
//设置父容器指针 //设置父容器指针
@@ -140,8 +150,14 @@ public:
virtual void setHostWindow(Window* host) { this->hostWindow = host; } virtual void setHostWindow(Window* host) { this->hostWindow = host; }
Window* getHostWindow() const; // 获取宿主 Window;子控件可沿 parent 向上回溯 Window* getHostWindow() const; // 获取宿主 Window;子控件可沿 parent 向上回溯
RECT getBoundsRect() const; // 获取当前控件外接矩形,用于覆盖/相交判断 RECT getBoundsRect() const; // 获取当前控件外接矩形,用于覆盖/相交判断
// 获取本控件“本轮实际可能写到屏幕上的覆盖范围”。
// 默认等于控件本体矩形;像 Button 这类会额外绘制 Tooltip 的控件,可 override 后扩大范围。
// 托管重绘 coverage、overlay 相交判断统一走这个接口,而不再默认使用控件本体 bounds。
virtual RECT getManagedRepaintCoverageRect() const;
Control* getManagedRepaintRoot(); // 找到本控件对应的托管重绘 root Control* getManagedRepaintRoot(); // 找到本控件对应的托管重绘 root
Control* getManagedRepaintDirectBranch(Control* root); // 找到“root 下面承接本次脏变化的直接子分支”
bool hasValidBackgroundSnapshot() const { return hasSnap && saveBkImage != nullptr; } // 当前是否持有可用于局部恢复的快照 bool hasValidBackgroundSnapshot() const { return hasSnap && saveBkImage != nullptr; } // 当前是否持有可用于局部恢复的快照
virtual bool hasManagedDirtySubtree() const; // 当前控件或其受管理子树中是否存在待提交的脏内容
virtual bool canCommitManagedPartialRepaint() const; // 当前 root 是否可安全做“局部提交”而非整 root 重画 virtual bool canCommitManagedPartialRepaint() const; // 当前 root 是否可安全做“局部提交”而非整 root 重画
virtual void commitManagedRepaint(); // 托管收口阶段真正执行绘制的入口 virtual void commitManagedRepaint(); // 托管收口阶段真正执行绘制的入口
//设置是否重绘 //设置是否重绘
@@ -157,16 +173,25 @@ public:
//用来检查对话框是否模态,其他控件不用实现 //用来检查对话框是否模态,其他控件不用实现
virtual bool model()const = 0; virtual bool model()const = 0;
//布局 //布局
// 设置旧版布局模式(兼容入口)
void setLayoutMode(StellarX::LayoutMode layoutMode_); void setLayoutMode(StellarX::LayoutMode layoutMode_);
// 设置旧版双锚点输入,并映射到内部统一 LayoutSpec
void setAnchor(StellarX::Anchor anchor_1, StellarX::Anchor anchor_2); void setAnchor(StellarX::Anchor anchor_1, StellarX::Anchor anchor_2);
// 获取旧版锚点 1(兼容读取入口)
StellarX::Anchor getAnchor_1() const; StellarX::Anchor getAnchor_1() const;
// 获取旧版锚点 2(兼容读取入口)
StellarX::Anchor getAnchor_2() const; StellarX::Anchor getAnchor_2() const;
// 获取旧版布局模式
StellarX::LayoutMode getLayoutMode() const; StellarX::LayoutMode getLayoutMode() const;
// 获取内部统一布局规格;供 Window / Canvas 等统一解算入口使用。 // 获取内部统一布局规格;供 Window / Canvas 等统一解算入口使用。
const StellarX::LayoutSpec& getLayoutSpec() const; const StellarX::LayoutSpec& getLayoutSpec() const;
// 获取控件能力边界;用于判断某个轴是否允许 Stretch。 // 获取控件能力边界;用于判断某个轴是否允许 Stretch。
const StellarX::LayoutCapability& getLayoutCapability() const; const StellarX::LayoutCapability& getLayoutCapability() const;
// 显式将当前运行态矩形提交为新的设计基线。普通 resize / 重排过程中不得自动调用。 // 显式将当前运行态矩形提交为新的设计基线。
// 这是“运行态 -> 设计态”的受控入口之一:
// - 普通 resize / 重排过程中不得自动调用
// - 公开 setter 也不得隐式调用
// - 仅用于少数需要把当前结果固化为新设计基线的场景
void commitCurrentGeometryAsDesignRect(); void commitCurrentGeometryAsDesignRect();
protected: protected:
// 第 1 层:根据父设计尺寸、父当前尺寸和本控件设计矩形,解算出当前运行态局部矩形。 // 第 1 层:根据父设计尺寸、父当前尺寸和本控件设计矩形,解算出当前运行态局部矩形。
+4
View File
@@ -5,6 +5,10 @@ Dialog::Dialog(Window& h, std::string text, std::string message, StellarX::Messa
: Canvas(), message(message), type(type), modal(modal), hWnd(h), titleText(text) : Canvas(), message(message), type(type), modal(modal), hWnd(h), titleText(text)
{ {
this->id = "Dialog"; this->id = "Dialog";
// Dialog 当前阶段只做“居中 + 自管内部 chrome”,
// 不参与外部 Stretch;宿主窗口变化时只重新居中。
this->layoutCapability.allowStretchX = false;
this->layoutCapability.allowStretchY = false;
setHostWindow(&hWnd); setHostWindow(&hWnd);
show = false; show = false;
} }
+9 -7
View File
@@ -63,18 +63,19 @@ class Dialog : public Canvas
bool pendingCleanup = false; //延迟清理 bool pendingCleanup = false; //延迟清理
public: public:
StellarX::ControlText textStyle; // 字体样式 StellarX::ControlText textStyle; // 字体样式
// 清理方法声明 // 在事件安全点执行延迟清理;用于非模态关闭后统一回收
void performDelayedCleanup(); void performDelayedCleanup();
//工厂模式下调用非模态对话框时用来返回结果 // 非模态工厂调用时用于回传结果
std::function<void(StellarX::MessageBoxResult)> resultCallback; std::function<void(StellarX::MessageBoxResult)> resultCallback;
//设置非模态获取结果回调函数 // 设置非模态结果回调
void SetResultCallback(std::function<void(StellarX::MessageBoxResult)> cb); void SetResultCallback(std::function<void(StellarX::MessageBoxResult)> cb);
//获取对话框消息,用以去重 // 获取标题文本,用于非模态去重
std::string GetCaption() const; std::string GetCaption() const;
//获取对话框消息,用以去重 // 获取正文文本,用于非模态去重
std::string GetText() const; std::string GetText() const;
public: public:
// 构造对话框;modal=false 时按非模态浮层工作
Dialog(Window& hWnd, std::string text, std::string message = "对话框", StellarX::MessageBoxType type = StellarX::MessageBoxType::OK, bool modal = true); Dialog(Window& hWnd, std::string text, std::string message = "对话框", StellarX::MessageBoxType type = StellarX::MessageBoxType::OK, bool modal = true);
~Dialog(); ~Dialog();
//绘制对话框 //绘制对话框
@@ -94,7 +95,7 @@ public:
// 获取对话框结果 // 获取对话框结果
StellarX::MessageBoxResult GetResult() const; StellarX::MessageBoxResult GetResult() const;
//获取对话框类型 // 返回当前对话框是否为模态
bool model() const override; bool model() const override;
// 显示对话框 // 显示对话框
@@ -119,13 +120,14 @@ private:
void initDialogSize(); void initDialogSize();
// 依据当前 Dialog 的 x/y/width/height 重新创建标题和按钮 // 依据当前 Dialog 的 x/y/width/height 重新创建标题和按钮
void rebuildChrome(); void rebuildChrome();
// 禁止走普通 Canvas 添加路径;Dialog 内部控件由自身 chrome 重建逻辑统一维护
void addControl(std::unique_ptr<Control> control); void addControl(std::unique_ptr<Control> control);
bool canCommitManagedPartialRepaint() const override; // 判断当前 Dialog 是否可安全做局部提交 bool canCommitManagedPartialRepaint() const override; // 判断当前 Dialog 是否可安全做局部提交
void commitManagedRepaint() override; // 托管收口阶段执行 Dialog 的真正重绘 void commitManagedRepaint() override; // 托管收口阶段执行 Dialog 的真正重绘
// 清除所有控件 // 清除所有控件
void clearControls(); void clearControls();
//创建对话框按钮 // 创建标准对话框功能按钮
std::unique_ptr<Button> createDialogButton(int x, int y, const std::string& text); std::unique_ptr<Button> createDialogButton(int x, int y, const std::string& text);
void requestRepaint(Control* parent) override; // 托管模式下登记为 Dialog root;非托管模式下立即更新内部按钮 void requestRepaint(Control* parent) override; // 托管模式下登记为 Dialog root;非托管模式下立即更新内部按钮
}; };
+128 -12
View File
@@ -1,9 +1,81 @@
#include "Label.h" #include "Label.h"
namespace
{
// Label 的内容尺寸刷新不依赖 EasyX 当前绘图上下文,
// 直接使用 GDI 依据“文本 + 字体样式”测量宽高。
// 这样无论是文本变化、字体样式变化,还是控件在窗口正式绘制前进入 dirty,
// 都可以先把运行态尺寸收口好,而不必等到 draw() 再决定几何。
void MeasureLabelText(const std::string& text, const StellarX::ControlText& style, int& width, int& height)
{
width = 0;
height = 0;
HDC hdc = CreateCompatibleDC(nullptr);
if (!hdc)
return;
LOGFONTA fontDesc{};
fontDesc.lfHeight = style.nHeight;
fontDesc.lfWidth = style.nWidth;
fontDesc.lfEscapement = style.nEscapement;
fontDesc.lfOrientation = style.nOrientation;
fontDesc.lfWeight = style.nWeight;
fontDesc.lfItalic = style.bItalic ? TRUE : FALSE;
fontDesc.lfUnderline = style.bUnderline ? TRUE : FALSE;
fontDesc.lfStrikeOut = style.bStrikeOut ? TRUE : FALSE;
fontDesc.lfCharSet = DEFAULT_CHARSET;
fontDesc.lfOutPrecision = OUT_DEFAULT_PRECIS;
fontDesc.lfClipPrecision = CLIP_DEFAULT_PRECIS;
fontDesc.lfQuality = DEFAULT_QUALITY;
fontDesc.lfPitchAndFamily = DEFAULT_PITCH | FF_DONTCARE;
lstrcpynA(fontDesc.lfFaceName, style.lpszFace ? style.lpszFace : "微软雅黑", LF_FACESIZE);
HFONT font = CreateFontIndirectA(&fontDesc);
HFONT oldFont = nullptr;
if (font)
oldFont = (HFONT)SelectObject(hdc, font);
if (text.empty())
{
TEXTMETRICA tm{};
if (GetTextMetricsA(hdc, &tm))
height = tm.tmHeight;
}
else
{
SIZE size{};
if (GetTextExtentPoint32A(hdc, text.c_str(), (int)text.size(), &size))
{
width = size.cx;
height = size.cy;
}
if (height == 0)
{
TEXTMETRICA tm{};
if (GetTextMetricsA(hdc, &tm))
height = tm.tmHeight;
}
}
if (font)
{
if (oldFont)
SelectObject(hdc, oldFont);
DeleteObject(font);
}
DeleteDC(hdc);
}
}
Label::Label() Label::Label()
:Control(0, 0, 0, 0) :Control(0, 0, 0, 0)
{ {
this->id = "Label"; this->id = "Label";
// Label 当前阶段明确走“内容驱动尺寸”语义:
// 外部布局器只安置位置,不通过 Stretch 改写它的宽高。
this->layoutCapability.allowStretchX = false;
this->layoutCapability.allowStretchY = false;
this->text = "默认标签"; this->text = "默认标签";
textStyle.color = RGB(0, 0, 0); textStyle.color = RGB(0, 0, 0);
textBkColor = RGB(255, 255, 255);; //默认白色背景 textBkColor = RGB(255, 255, 255);; //默认白色背景
@@ -13,11 +85,44 @@ Label::Label(int x, int y, std::string text, COLORREF textcolor, COLORREF bkColo
:Control(x, y, 0, 0) :Control(x, y, 0, 0)
{ {
this->id = "Label"; this->id = "Label";
this->layoutCapability.allowStretchX = false;
this->layoutCapability.allowStretchY = false;
this->text = text; this->text = text;
textStyle.color = textcolor; textStyle.color = textcolor;
textBkColor = bkColor; //默认白色背景 textBkColor = bkColor; //默认白色背景
} }
void Label::refreshContentDrivenRuntimeGeometry()
{
// Label 的尺寸来源于“当前文本 + 当前字体样式”。
// 这里只更新运行态 width/height,不自动回写 localWidth/localHeight
// 避免内容变化把设计基线偷偷带偏。
if (contentMeasureValid &&
lastMeasuredText == text &&
!(lastMeasuredStyle != textStyle))
{
return;
}
int newWidth = 0;
int newHeight = 0;
MeasureLabelText(text, textStyle, newWidth, newHeight);
const bool sizeChanged = (newWidth != this->width) || (newHeight != this->height);
if (sizeChanged && hasSnap)
{
// 运行态尺寸变化时,先恢复旧背景,避免文本缩短后旧像素残留在屏幕上。
discardBackground();
}
this->width = newWidth;
this->height = newHeight;
lastMeasuredText = text;
lastMeasuredStyle = textStyle;
contentMeasureValid = true;
dirty = true;
}
void Label::draw() void Label::draw()
{ {
if (dirty && show) if (dirty && show)
@@ -34,16 +139,6 @@ void Label::draw()
settextstyle(textStyle.nHeight, textStyle.nWidth, textStyle.lpszFace, settextstyle(textStyle.nHeight, textStyle.nWidth, textStyle.lpszFace,
textStyle.nEscapement, textStyle.nOrientation, textStyle.nWeight, textStyle.nEscapement, textStyle.nOrientation, textStyle.nWeight,
textStyle.bItalic, textStyle.bUnderline, textStyle.bStrikeOut); //设置字体样式 textStyle.bItalic, textStyle.bUnderline, textStyle.bStrikeOut); //设置字体样式
const int newWidth = textwidth(text.c_str());
const int newHeight = textheight(text.c_str());
if (newWidth != this->width || newHeight != this->height)
{
if (hasSnap)
discardBackground();
this->width = newWidth;
this->height = newHeight;
}
if ((saveBkX != this->x) || (saveBkY != this->y) || (!hasSnap) || (saveWidth != this->width) || (saveHeight != this->height) || !saveBkImage) if ((saveBkX != this->x) || (saveBkY != this->y) || (!hasSnap) || (saveWidth != this->width) || (saveHeight != this->height) || !saveBkImage)
saveBackground(this->x, this->y, this->width, this->height); saveBackground(this->x, this->y, this->width, this->height);
// 恢复背景(清除旧内容) // 恢复背景(清除旧内容)
@@ -53,12 +148,24 @@ void Label::draw()
dirty = false; dirty = false;
} }
} }
//用于“隐藏提示框”时调用(还原并释放快照)
void Label::hide() void Label::hide()
{ {
discardBackground(); // 还原并释放快照 discardBackground(); // 还原并释放快照
dirty = false; dirty = false;
} }
void Label::setDirty(bool dirty)
{
if (dirty)
{
// 只要 Label 进入脏状态,就在绘制前把内容驱动尺寸同步到运行态。
// 这样文本变化和字体样式变化都会在 draw() 之前完成几何刷新。
refreshContentDrivenRuntimeGeometry();
}
this->dirty = dirty;
}
void Label::setTextdisap(bool key) void Label::setTextdisap(bool key)
{ {
textBkDisap = key; textBkDisap = key;
@@ -74,5 +181,14 @@ void Label::setTextBkColor(COLORREF color)
void Label::setText(std::string text) void Label::setText(std::string text)
{ {
this->text = text; this->text = text;
this->dirty = true; refreshContentDrivenRuntimeGeometry();
}
void Label::applyResolvedLayoutRect(const StellarX::ResolvedLayoutRect& rect)
{
// 通用布局器给出的 rect.width/rect.height 只代表“外部几何解算结果”。
// Label 当前阶段的语义是内容驱动尺寸,因此这里只接位置,不接外部宽高,
// 宽高继续由 refreshContentDrivenRuntimeGeometry() 按文本和字体样式刷新。
refreshContentDrivenRuntimeGeometry();
applyRuntimeRectDirect(rect.worldX, rect.worldY, this->width, this->height);
} }
+27 -3
View File
@@ -24,23 +24,47 @@ class Label : public Control
std::string text; //标签文本 std::string text; //标签文本
COLORREF textBkColor; //标签背景颜色 COLORREF textBkColor; //标签背景颜色
bool textBkDisap = false; //标签背景是否透明 bool textBkDisap = false; //标签背景是否透明
std::string lastMeasuredText; // 最近一次参与尺寸测量的文本
StellarX::ControlText lastMeasuredStyle; // 最近一次参与尺寸测量的字体样式
bool contentMeasureValid = false; // 当前内容尺寸缓存是否有效
//标签事件处理(标签无事件)不实现具体代码 //标签事件处理(标签无事件)不实现具体代码
bool handleEvent(const ExMessage& msg) override { return false; } bool handleEvent(const ExMessage& msg) override { return false; }
//用来检查对话框是否模态,此控件不做实现 //用来检查对话框是否模态,此控件不做实现
bool model() const override { return false; }; bool model() const override { return false; };
// 根据当前文本和字体样式刷新运行态尺寸。
// 这是 Label 的内容驱动几何入口:
// - 覆盖文本变化
// - 覆盖字体样式变化
// - 只更新运行态宽高,不自动提交设计基线 local*
void refreshContentDrivenRuntimeGeometry();
public: public:
// 标签字体样式保持公开,便于用户直接改字段。
// 但直接修改 textStyle 后,必须显式调用 setDirty(true)
// 才会触发内容驱动尺寸刷新并让新的运行态宽高生效。
StellarX::ControlText textStyle; //标签文本样式 StellarX::ControlText textStyle; //标签文本样式
public: public:
// 默认构造:创建一个内容驱动尺寸的标签
Label(); Label();
// 指定位置、文本和基础样式构造标签
Label(int x, int y, std::string text = "标签", COLORREF textcolor = BLACK, COLORREF bkColor = RGB(255, 255, 255)); Label(int x, int y, std::string text = "标签", COLORREF textcolor = BLACK, COLORREF bkColor = RGB(255, 255, 255));
// 绘制标签文本
void draw() override; void draw() override;
//用于“隐藏提示框”时调用(还原并释放快照)
void hide(); void hide();
//设置标签背景是否透明 // Label 的内容尺寸由文本和字体样式共同决定。
// 因此只要进入 dirty 状态,就在绘制前先刷新一次运行态尺寸,
// 避免 draw() 阶段继续承担新的几何决策。
void setDirty(bool dirty) override;
// 设置标签背景是否透明
void setTextdisap(bool key); void setTextdisap(bool key);
//设置标签背景颜色 // 设置标签背景颜色
void setTextBkColor(COLORREF color); void setTextBkColor(COLORREF color);
//设置标签文本 // 设置标签文本,并触发内容驱动尺寸刷新
void setText(std::string text); void setText(std::string text);
protected:
// 统一布局器只负责安置 Label 的运行态位置;
// 宽高仍由 Label 自己的内容驱动路径决定。
void applyResolvedLayoutRect(const StellarX::ResolvedLayoutRect& rect) override;
}; };
+2
View File
@@ -28,12 +28,14 @@ namespace StellarX
{ {
public: public:
// 模态:阻塞直到关闭,返回结果 // 模态:阻塞直到关闭,返回结果
// 直接创建并显示模态消息框,直到用户关闭后返回结果
static MessageBoxResult showModal(Window& wnd, static MessageBoxResult showModal(Window& wnd,
const std::string& text, const std::string& text,
const std::string& caption = "提示", const std::string& caption = "提示",
MessageBoxType type = MessageBoxType::OK); MessageBoxType type = MessageBoxType::OK);
// 非模态:立即返回,通过回调异步获取结果 // 非模态:立即返回,通过回调异步获取结果
// 创建并显示非模态消息框,关闭时通过回调返回结果
static void showAsync(Window& wnd, static void showAsync(Window& wnd,
const std::string& text, const std::string& text,
const std::string& caption = "提示", const std::string& caption = "提示",
+219 -64
View File
@@ -1,6 +1,25 @@
#include "TabControl.h" #include "TabControl.h"
#include "SxLog.h" #include "SxLog.h"
#include "Window.h" #include "Window.h"
namespace
{
bool SxTabRectsIntersect(const RECT& a, const RECT& b)
{
return a.left < b.right && a.right > b.left &&
a.top < b.bottom && a.bottom > b.top;
}
RECT SxTabUnionRect(const RECT& a, const RECT& b)
{
RECT out{};
out.left = (std::min)(a.left, b.left);
out.top = (std::min)(a.top, b.top);
out.right = (std::max)(a.right, b.right);
out.bottom = (std::max)(a.bottom, b.bottom);
return out;
}
}
inline void TabControl::initTabBar() inline void TabControl::initTabBar()
{ {
if (controls.empty())return; if (controls.empty())return;
@@ -65,9 +84,9 @@ inline void TabControl::initTabPage()
// TabControl 内部页签页仍然保留专用布局: // TabControl 内部页签页仍然保留专用布局:
// 这里负责把“当前选项卡容器矩形”拆分成页签栏和页面区, // 这里负责把“当前选项卡容器矩形”拆分成页签栏和页面区,
// 不把这部分细节下放到通用布局解算器里。 // 不把这部分细节下放到通用布局解算器里。
//子控件坐标原点 // 但页面内容子树的世界坐标映射职责,继续归页面 Canvas 自己管理。
int nX = 0; // 因此这里不再手工遍历页内子控件做 setX/setY,
int nY = 0; // 只安置每个页面 Canvas 的矩形,让 Canvas 通过自身 relayoutManagedChildren() 收口。
switch (this->tabPlacement) switch (this->tabPlacement)
{ {
case StellarX::TabPlacement::Top: case StellarX::TabPlacement::Top:
@@ -78,16 +97,6 @@ inline void TabControl::initTabPage()
c.second->setWidth(this->width); c.second->setWidth(this->width);
c.second->setHeight(this->height - tabBarHeight); c.second->setHeight(this->height - tabBarHeight);
} }
nX = this->x;
nY = this->y + tabBarHeight;
for (auto& c : controls)
{
for (auto& v : c.second->getControls())
{
v->setX(v->getLocalX() + nX);
v->setY(v->getLocalY() + nY);
}
}
break; break;
case StellarX::TabPlacement::Bottom: case StellarX::TabPlacement::Bottom:
for (auto& c : controls) for (auto& c : controls)
@@ -97,16 +106,6 @@ inline void TabControl::initTabPage()
c.second->setWidth(this->width); c.second->setWidth(this->width);
c.second->setHeight(this->height - tabBarHeight); c.second->setHeight(this->height - tabBarHeight);
} }
nX = this->x;
nY = this->y;
for (auto& c : controls)
{
for (auto& v : c.second->getControls())
{
v->setX(v->getLocalX() + nX);
v->setY(v->getLocalY() + nY);
}
}
break; break;
case StellarX::TabPlacement::Left: case StellarX::TabPlacement::Left:
for (auto& c : controls) for (auto& c : controls)
@@ -116,16 +115,6 @@ inline void TabControl::initTabPage()
c.second->setWidth(this->width - tabBarHeight); c.second->setWidth(this->width - tabBarHeight);
c.second->setHeight(this->height); c.second->setHeight(this->height);
} }
nX = this->x + tabBarHeight;
nY = this->y;
for (auto& c : controls)
{
for (auto& v : c.second->getControls())
{
v->setX(v->getLocalX() + nX);
v->setY(v->getLocalY() + nY);
}
}
break; break;
case StellarX::TabPlacement::Right: case StellarX::TabPlacement::Right:
for (auto& c : controls) for (auto& c : controls)
@@ -135,16 +124,6 @@ inline void TabControl::initTabPage()
c.second->setWidth(this->width - tabBarHeight); c.second->setWidth(this->width - tabBarHeight);
c.second->setHeight(this->height); c.second->setHeight(this->height);
} }
nX = this->x;
nY = this->y;
for (auto& c : controls)
{
for (auto& v : c.second->getControls())
{
v->setX(v->getLocalX() + nX);
v->setY(v->getLocalY() + nY);
}
}
break; break;
default: default:
break; break;
@@ -163,12 +142,18 @@ void TabControl::refreshRuntimeLayout()
TabControl::TabControl() :Canvas() TabControl::TabControl() :Canvas()
{ {
this->id = "TabControl"; this->id = "TabControl";
// TabControl 作为外层容器,当前阶段显式允许双轴 Stretch;
// 内部页签栏和页面区仍由自己的专用布局逻辑管理。
this->layoutCapability.allowStretchX = true;
this->layoutCapability.allowStretchY = true;
} }
TabControl::TabControl(int x, int y, int width, int height) TabControl::TabControl(int x, int y, int width, int height)
: Canvas(x, y, width, height) : Canvas(x, y, width, height)
{ {
this->id = "TabControl"; this->id = "TabControl";
this->layoutCapability.allowStretchX = true;
this->layoutCapability.allowStretchY = true;
} }
TabControl::~TabControl() TabControl::~TabControl()
@@ -218,13 +203,13 @@ void TabControl::draw()
Canvas::draw(); Canvas::draw();
for (auto& c : controls) for (auto& c : controls)
{ {
c.first->setDirty(true); c.second->setDirty(true);
c.first->draw(); c.second->draw();
} }
for (auto& c : controls) for (auto& c : controls)
{ {
c.second->setDirty(true); c.first->setDirty(true);
c.second->draw(); c.first->draw();
} }
// 首次绘制时处理默认激活页签 // 首次绘制时处理默认激活页签
@@ -242,28 +227,90 @@ void TabControl::draw()
bool TabControl::handleEvent(const ExMessage& msg) bool TabControl::handleEvent(const ExMessage& msg)
{ {
if (!show)return false; if (!show)return false;
resetEventVisualChanged();
bool consume = false; bool consume = false;
for (auto& c : controls) bool anyVisualChanged = false;
if (c.first->handleEvent(msg))
{ // TabControl 的同层页签按钮/页面区事件分发顺序,
consume = true; // 与 Window / Canvas 保持一致:按倒序处理,后加入者视为更靠上层。
break; if (msg.message == WM_MOUSEMOVE)
}
if (!consume)
{ {
for (auto& c : controls) for (auto it = controls.rbegin(); it != controls.rend(); ++it)
if (c.second->IsVisible()) {
if (c.second->handleEvent(msg)) if (!consume)
{
if (it->first->handleEvent(msg))
{ {
consume = true; consume = true;
break;
} }
if (it->first->didEventAffectVisual())
anyVisualChanged = true;
}
else if (it->first->clearTransientMouseState())
{
anyVisualChanged = true;
}
}
for (auto it = controls.rbegin(); it != controls.rend(); ++it)
{
if (!it->second->IsVisible())
continue;
if (!consume)
{
if (it->second->handleEvent(msg))
{
consume = true;
}
if (it->second->didEventAffectVisual())
anyVisualChanged = true;
}
else if (it->second->clearTransientMouseState())
{
anyVisualChanged = true;
}
}
}
else
{
for (auto it = controls.rbegin(); it != controls.rend(); ++it)
if (it->first->handleEvent(msg))
{
consume = true;
break;
}
if (!consume)
{
for (auto it = controls.rbegin(); it != controls.rend(); ++it)
if (it->second->IsVisible())
if (it->second->handleEvent(msg))
{
consume = true;
break;
}
}
} }
if (dirty) if (dirty)
requestRepaint(parent); requestRepaint(parent);
markEventVisualChanged(anyVisualChanged || dirty);
return consume; return consume;
} }
bool TabControl::clearTransientMouseState()
{
bool changed = false;
for (auto it = controls.rbegin(); it != controls.rend(); ++it)
{
if (it->first->IsVisible() && it->first->clearTransientMouseState())
changed = true;
if (it->second->IsVisible() && it->second->clearTransientMouseState())
changed = true;
}
return changed;
}
void TabControl::add(std::pair<std::unique_ptr<Button>, std::unique_ptr<Canvas>>&& control) void TabControl::add(std::pair<std::unique_ptr<Button>, std::unique_ptr<Canvas>>&& control)
{ {
controls.push_back(std::move(control)); controls.push_back(std::move(control));
@@ -399,8 +446,15 @@ void TabControl::setActiveIndex(int idx)
defaultActivation = idx; defaultActivation = idx;
else else
{ {
// 外部重复激活“已经处于激活状态”的页签时,不应再把整条 onToggleOn 链重跑一遍。
// 否则当前可见页面会重复执行 onWindowResize()/setIsVisible(true)
// 对页内像 Table 这种会越出页面边界绘制的控件,容易把快照链再次扰乱,留下残影。
if (idx >= 0 && idx < controls.size()) if (idx >= 0 && idx < controls.size())
{
if (getActiveIndex() == idx)
return;
controls[idx].first->setButtonClick(true); controls[idx].first->setButtonClick(true);
}
} }
} }
@@ -444,17 +498,118 @@ void TabControl::requestRepaint(Control* parent)
if (this == parent) if (this == parent)
{ {
RECT coverage{};
bool hasCoverage = false;
auto commitTabUnit = [&](Control* unit, bool forceOverlayRedraw)
{
if (!unit || !unit->IsVisible())
return;
const bool directDirty = unit->isDirty();
const bool subtreeDirty = unit->hasManagedDirtySubtree();
if (forceOverlayRedraw)
{
// 下层单元已经写过像素,上层页签/页面作为 overlay 补画时,
// 必须先丢掉旧快照,重新抓取当前背景后再画,否则会把旧背景再贴回来。
unit->invalidateBackgroundSnapshot();
unit->setDirty(true);
unit->draw();
}
else if (directDirty)
{
unit->draw();
}
else if (subtreeDirty)
{
unit->commitManagedRepaint();
}
else
{
return;
}
const RECT rc = unit->getManagedRepaintCoverageRect();
if (!hasCoverage)
{
coverage = rc;
hasCoverage = true;
}
else
{
coverage = SxTabUnionRect(coverage, rc);
}
};
// 局部重绘必须和 draw() 维持同一套顺序:
// 先页面,再页签按钮。
// 否则页签按钮上的 Tooltip 会被后画的页面盖掉,
// 表现为“有页面打开时 Tooltip 看不到,所有页关闭时才正常”。
for (auto& control : controls) for (auto& control : controls)
{ {
if (control.first->isDirty() && control.first->IsVisible()) Control* page = control.second.get();
control.first->draw(); if (!page->IsVisible())
if (control.second->isDirty() && control.second->IsVisible()) continue;
control.second->draw();
if (page->hasManagedDirtySubtree())
{
commitTabUnit(page, false);
}
else if (hasCoverage && SxTabRectsIntersect(page->getManagedRepaintCoverageRect(), coverage))
{
commitTabUnit(page, true);
}
} }
for (auto& control : controls)
{
Control* button = control.first.get();
if (!button->IsVisible())
continue;
if (button->hasManagedDirtySubtree())
{
commitTabUnit(button, false);
}
else if (hasCoverage && SxTabRectsIntersect(button->getManagedRepaintCoverageRect(), coverage))
{
commitTabUnit(button, true);
}
}
return;
} }
else onRequestRepaintAsRoot();
onRequestRepaintAsRoot(); }
bool TabControl::hasManagedDirtySubtree() const
{
if (dirty)
return true;
for (const auto& control : controls)
{
if (control.first->IsVisible() && control.first->hasManagedDirtySubtree())
return true;
if (control.second->IsVisible() && control.second->hasManagedDirtySubtree())
return true;
}
return false;
}
RECT TabControl::getManagedRepaintCoverageRect() const
{
// TabControl 的 draw() 会写自身背景、全部页签按钮以及全部页面。
// 因此 coverage 也必须按“页签按钮 + 页面”递归并集,避免内部 Tooltip 等附加绘制区域被漏算。
RECT coverage = getBoundsRect();
for (const auto& control : controls)
{
if (control.first->IsVisible())
coverage = SxTabUnionRect(coverage, control.first->getManagedRepaintCoverageRect());
if (control.second->IsVisible())
coverage = SxTabUnionRect(coverage, control.second->getManagedRepaintCoverageRect());
}
return coverage;
} }
bool TabControl::canCommitManagedPartialRepaint() const bool TabControl::canCommitManagedPartialRepaint() const
+11 -5
View File
@@ -42,7 +42,9 @@ private:
// 统一刷新 TabControl 当前运行态下的页签栏和页面区布局。 // 统一刷新 TabControl 当前运行态下的页签栏和页面区布局。
void refreshRuntimeLayout(); void refreshRuntimeLayout();
public: public:
// 默认构造:创建一个空 TabControl
TabControl(); TabControl();
// 指定初始位置和尺寸构造 TabControl
TabControl(int x, int y, int width, int height); TabControl(int x, int y, int width, int height);
~TabControl(); ~TabControl();
@@ -54,10 +56,12 @@ public:
void draw() override; void draw() override;
bool handleEvent(const ExMessage& msg) override; bool handleEvent(const ExMessage& msg) override;
// 只转发清理页签按钮和当前可见页面中的鼠标临时状态,不升级为整块 TabControl 重绘。
bool clearTransientMouseState() override;
//添加页签+页 // 添加一个“页签按钮 + 页面 Canvas”组合
void add(std::pair<std::unique_ptr<Button>, std::unique_ptr<Canvas>>&& control); void add(std::pair<std::unique_ptr<Button>, std::unique_ptr<Canvas>>&& control);
//添加为某个页添加控件 // 按页签文本把控件添加到对应页面
void add(std::string tabText, std::unique_ptr<Control> control); void add(std::string tabText, std::unique_ptr<Control> control);
//设置页签位置 //设置页签位置
void setTabPlacement(StellarX::TabPlacement placement); void setTabPlacement(StellarX::TabPlacement placement);
@@ -68,16 +72,18 @@ public:
void onWindowResize() override; void onWindowResize() override;
//获取当前激活页签索引 //获取当前激活页签索引
int getActiveIndex() const; int getActiveIndex() const;
//设置当前激活页签索引 // 设置当前激活页签索引;若已是当前激活页则直接返回
void setActiveIndex(int idx); void setActiveIndex(int idx);
//获取页签数量 // 获取页签数量
int count() const; int count() const;
//通过页签文本返回索引 // 通过页签文本查找索引
int indexOf(const std::string& tabText) const; int indexOf(const std::string& tabText) const;
//设置脏区并请求重绘 //设置脏区并请求重绘
void setDirty(bool dirty) override; void setDirty(bool dirty) override;
//请求父控件重绘 //请求父控件重绘
void requestRepaint(Control* parent)override; // 托管模式下登记为 root;非托管模式下局部更新脏按钮/脏页面 void requestRepaint(Control* parent)override; // 托管模式下登记为 root;非托管模式下局部更新脏按钮/脏页面
bool hasManagedDirtySubtree() const override;
RECT getManagedRepaintCoverageRect() const override;
bool canCommitManagedPartialRepaint() const override; // 判断当前 TabControl 是否可安全做局部提交 bool canCommitManagedPartialRepaint() const override; // 判断当前 TabControl 是否可安全做局部提交
void commitManagedRepaint() override; // 托管收口阶段执行 TabControl 的真正重绘 void commitManagedRepaint() override; // 托管收口阶段执行 TabControl 的真正重绘
protected: protected:
+123 -5
View File
@@ -152,9 +152,18 @@ void Table::initTextWaH()
// 最终表宽/高:内容 + 对称边框 // 最终表宽/高:内容 + 对称边框
this->width = contentW + (border << 1); this->width = contentW + (border << 1);
this->height = headerH + rowsH + footerH + (border << 1); this->height = headerH + rowsH + footerH + (border << 1);
// 记录原始宽高用于锚点布局的参考;此处仅在初始化单元尺寸时重置 // 这是 Table 自身受控的“结构尺寸 -> 设计基线”刷新点:
// 表格总宽高本来就由表头、表体、页脚和分页按钮共同决定,
// 因此这里允许同步 localWidth/localHeight,作为后续锚点解算参考。
const bool baselineChanged = (this->localWidth != this->width) || (this->localHeight != this->height);
this->localWidth = this->width; this->localWidth = this->width;
this->localHeight = this->height; this->localHeight = this->height;
if (baselineChanged)
{
SX_LOGD("Layout")
<< SX_T("Table 刷新结构设计基线:id=", "Table refresh structural design rect: id=") << id
<< SX_T(" size=(", " size=(") << this->width << "," << this->height << ")";
}
} }
void Table::initButton() void Table::initButton()
@@ -285,6 +294,10 @@ void Table::drawPageNum()
pageNum->textStyle = this->textStyle; pageNum->textStyle = this->textStyle;
if (StellarX::FillMode::Null == tableFillMode) if (StellarX::FillMode::Null == tableFillMode)
pageNum->setTextdisap(true); pageNum->setTextdisap(true);
// Table 每次整表重绘前都会先回贴自己的背景快照,
// 页码区域会被一起清空。即便页码文本本身没变化,也必须强制把 Label 重新设脏,
// 否则 Label::draw() 会因为 dirty=false 直接跳过,导致页码在表格重绘后消失。
pageNum->setDirty(true);
pageNum->draw(); pageNum->draw();
} }
@@ -366,6 +379,7 @@ Table::Table(int x, int y)
this->id = "Table"; this->id = "Table";
// Table 当前正式能力边界: // Table 当前正式能力边界:
// 仅允许 X 轴 StretchY 轴固定尺寸。 // 仅允许 X 轴 StretchY 轴固定尺寸。
this->layoutCapability.allowStretchX = true;
this->layoutCapability.allowStretchY = false; this->layoutCapability.allowStretchY = false;
} }
@@ -375,7 +389,8 @@ void Table::applyResolvedLayoutRect(const StellarX::ResolvedLayoutRect& rect)
{ {
// Table 不能像普通控件那样直接写入 width/height // Table 不能像普通控件那样直接写入 width/height
// 它的 setWidth() 内部会联动列宽与页脚布局,因此这里必须复用原有 setter 副作用。 // 它的 setWidth() 内部会联动列宽与页脚布局,因此这里必须复用原有 setter 副作用。
// 同时由于 setHeight() 为空实现,Y 轴会自然保持固定尺寸。 // 同时 Table 的 Y Fixed 是当前版本的正式行为边界:
// 即便统一解算器给出了新的 rect.height,这里也不会接收该高度。
if (this->x != rect.worldX) if (this->x != rect.worldX)
setX(rect.worldX); setX(rect.worldX);
if (this->y != rect.worldY) if (this->y != rect.worldY)
@@ -383,7 +398,13 @@ void Table::applyResolvedLayoutRect(const StellarX::ResolvedLayoutRect& rect)
if (this->width != rect.width) if (this->width != rect.width)
setWidth(rect.width); setWidth(rect.width);
if (this->height != rect.height) if (this->height != rect.height)
setHeight(rect.height); {
SX_LOGD("Layout")
<< SX_T("Table 忽略 Y 轴运行态高度写入:id=", "Table ignore runtime Y resize: id=")
<< id
<< SX_T(" currentH=", " currentH=") << this->height
<< SX_T(" requestedH=", " requestedH=") << rect.height;
}
dirty = true; dirty = true;
} }
@@ -482,20 +503,117 @@ void Table::draw()
bool Table::handleEvent(const ExMessage& msg) bool Table::handleEvent(const ExMessage& msg)
{ {
if (!show)return false; if (!show)return false;
resetEventVisualChanged();
bool consume = false; bool consume = false;
bool anyVisualChanged = false;
if (!this->isShowPageButton) if (!this->isShowPageButton)
return consume; return consume;
if (msg.message == WM_MOUSEMOVE)
{
// Table 内部分页按钮也按“后绘制者优先”的倒序语义处理。
// drawButton() 里是 prev 再 next,因此这里先 next 再 prev。
if (nextButton)
{
if (!consume)
{
consume = nextButton->handleEvent(msg);
if (nextButton->didEventAffectVisual())
anyVisualChanged = true;
}
else if (nextButton->clearTransientMouseState())
{
anyVisualChanged = true;
}
}
if (prevButton)
{
if (!consume)
{
consume = prevButton->handleEvent(msg);
if (prevButton->didEventAffectVisual())
anyVisualChanged = true;
}
else if (prevButton->clearTransientMouseState())
{
anyVisualChanged = true;
}
}
}
else else
{ {
if (prevButton)consume = prevButton->handleEvent(msg); // 点击类消息仍保持“后绘制者优先”的倒序命中语义。
if (nextButton && !consume) // 但和 WM_MOUSEMOVE 一样,分页按钮的视觉状态变化必须向上提升为 Table 重绘,
// 否则按钮内部已经变成 hover/click,父级却不知道需要重画整张 Table,
// 就会出现“状态实际变了,但屏幕上直到下一次翻页才看见”的现象。
if (nextButton)
{
consume = nextButton->handleEvent(msg); consume = nextButton->handleEvent(msg);
if (nextButton->didEventAffectVisual())
anyVisualChanged = true;
}
if (prevButton && !consume)
{
consume = prevButton->handleEvent(msg);
if (prevButton->didEventAffectVisual())
anyVisualChanged = true;
}
} }
// Table 当前还没有像 TabControl 那样把分页按钮做成可独立提交的内部绘制单元。
// 因此只要内部按钮的 hover / click / leave 造成视觉变化,就必须把整张 Table 设脏,
// 让父容器通过现有的 Table::draw() 链把页脚区域正确重画出来。
if (anyVisualChanged)
dirty = true;
if (dirty) if (dirty)
requestRepaint(parent); requestRepaint(parent);
markEventVisualChanged(anyVisualChanged || dirty);
return consume; return consume;
} }
bool Table::clearTransientMouseState()
{
bool changed = false;
if (nextButton && nextButton->clearTransientMouseState())
changed = true;
if (prevButton && prevButton->clearTransientMouseState())
changed = true;
return changed;
}
RECT Table::getManagedRepaintCoverageRect() const
{
RECT coverage = getBoundsRect();
if (pageNum && pageNum->IsVisible())
{
const RECT pageRect = pageNum->getManagedRepaintCoverageRect();
coverage.left = (std::min)(coverage.left, pageRect.left);
coverage.top = (std::min)(coverage.top, pageRect.top);
coverage.right = (std::max)(coverage.right, pageRect.right);
coverage.bottom = (std::max)(coverage.bottom, pageRect.bottom);
}
if (prevButton && prevButton->IsVisible())
{
const RECT prevRect = prevButton->getManagedRepaintCoverageRect();
coverage.left = (std::min)(coverage.left, prevRect.left);
coverage.top = (std::min)(coverage.top, prevRect.top);
coverage.right = (std::max)(coverage.right, prevRect.right);
coverage.bottom = (std::max)(coverage.bottom, prevRect.bottom);
}
if (nextButton && nextButton->IsVisible())
{
const RECT nextRect = nextButton->getManagedRepaintCoverageRect();
coverage.left = (std::min)(coverage.left, nextRect.left);
coverage.top = (std::min)(coverage.top, nextRect.top);
coverage.right = (std::max)(coverage.right, nextRect.right);
coverage.bottom = (std::max)(coverage.bottom, nextRect.bottom);
}
return coverage;
}
void Table::setHeaders(std::initializer_list<std::string> headers) void Table::setHeaders(std::initializer_list<std::string> headers)
{ {
this->headers.clear(); this->headers.clear();
+12 -2
View File
@@ -101,11 +101,16 @@ private:
bool model() const override { return false; }; bool model() const override { return false; };
public: public:
StellarX::ControlText textStyle; // 文本样式 StellarX::ControlText textStyle; // 文本样式
// 修改表格运行态 X,并同步内部页脚元素位置
void setX(int x) override; void setX(int x) override;
// 修改表格运行态 Y,并同步内部页脚元素位置
void setY(int y) override; void setY(int y) override;
// 修改表格运行态宽度;当前支持横向拉伸
void setWidth(int width) override; void setWidth(int width) override;
// 修改表格运行态高度;当前版本作为行为边界明确忽略纵向 Stretch
void setHeight(int height) override; void setHeight(int height) override;
public: public:
// 构造一个基础表格,后续通过表头和数据初始化内容
Table(int x, int y); Table(int x, int y);
~Table(); ~Table();
@@ -113,11 +118,16 @@ public:
void draw() override; void draw() override;
//事件处理 //事件处理
bool handleEvent(const ExMessage& msg) override; bool handleEvent(const ExMessage& msg) override;
// 只清理分页按钮的 hover / tooltip / 临时按下态,不触发表格整体重绘。
bool clearTransientMouseState() override;
// Table 重绘时会一并绘制页码 Label 和分页按钮,coverage 需要把这些内部绘制单元并入。
RECT getManagedRepaintCoverageRect() const override;
//设置表头 // 设置表头
void setHeaders(std::initializer_list<std::string> headers); void setHeaders(std::initializer_list<std::string> headers);
//设置表格数据 // 设置表格数据(单行追加)
void setData(std::vector<std::string> data); void setData(std::vector<std::string> data);
// 设置表格数据(多行覆盖)
void setData(std::initializer_list<std::vector<std::string>> data); void setData(std::initializer_list<std::vector<std::string>> data);
//设置每页显示的行数 //设置每页显示的行数
void setRowsPerPage(int rows); void setRowsPerPage(int rows);
+5
View File
@@ -6,6 +6,11 @@ TextBox::TextBox(int x, int y, int width, int height, std::string text, StellarX
:Control(x, y, width, height), text(text), mode(mode), shape(shape) :Control(x, y, width, height), text(text), mode(mode), shape(shape)
{ {
this->id = "TextBox"; this->id = "TextBox";
// TextBox 当前阶段的默认布局策略:
// - 水平方向允许通过锚点语义拉伸
// - 垂直方向当前实现保持固定尺寸,不在本轮引入随高度变化的视觉自适应
this->layoutCapability.allowStretchX = true;
this->layoutCapability.allowStretchY = false;
} }
void TextBox::draw() void TextBox::draw()
+10 -7
View File
@@ -31,23 +31,26 @@ class TextBox : public Control
public: public:
StellarX::ControlText textStyle; //文本样式 StellarX::ControlText textStyle; //文本样式
// 构造文本框;支持输入模式和只读模式
TextBox(int x, int y, int width, int height, std::string text = "", StellarX::TextBoxmode mode = StellarX::TextBoxmode::INPUT_MODE, StellarX::ControlShape shape = StellarX::ControlShape::RECTANGLE); TextBox(int x, int y, int width, int height, std::string text = "", StellarX::TextBoxmode mode = StellarX::TextBoxmode::INPUT_MODE, StellarX::ControlShape shape = StellarX::ControlShape::RECTANGLE);
// 绘制文本框
void draw() override; void draw() override;
// 处理输入框鼠标交互和文本录入
bool handleEvent(const ExMessage& msg) override; bool handleEvent(const ExMessage& msg) override;
//设置模式 // 设置文本框模式
void setMode(StellarX::TextBoxmode mode); void setMode(StellarX::TextBoxmode mode);
//设置可输入最大字符长度 // 设置可输入最大字符长度
void setMaxCharLen(size_t len); void setMaxCharLen(size_t len);
//设置形状 // 设置文本框形状
void setTextBoxshape(StellarX::ControlShape shape); void setTextBoxshape(StellarX::ControlShape shape);
//设置边框颜色 // 设置边框颜色
void setTextBoxBorder(COLORREF color); void setTextBoxBorder(COLORREF color);
//设置背景颜色 // 设置背景颜色
void setTextBoxBk(COLORREF color); void setTextBoxBk(COLORREF color);
//设置文本 // 设置当前文本
void setText(std::string text); void setText(std::string text);
//获取文本 // 获取当前文本
std::string getText() const; std::string getText() const;
private: private:
+178 -21
View File
@@ -33,6 +33,9 @@ static bool SxRectsIntersect(const RECT& a, const RECT& b)
a.top < b.bottom && a.bottom > b.top; a.top < b.bottom && a.bottom > b.top;
} }
static void collectManagedControlOverlays(const std::vector<std::unique_ptr<Control>>& controls,
Control* repaintRoot, const RECT& coverage, std::vector<Control*>& overlays);
bool Window::isManagedDispatchActive() const bool Window::isManagedDispatchActive() const
{ {
return managedDispatchActive; return managedDispatchActive;
@@ -56,9 +59,20 @@ void Window::requestManagedRepaint(Control* source)
if (!root) if (!root)
return; return;
RECT coverage = root->getBoundsRect(); RECT coverage = root->getManagedRepaintCoverageRect();
if (root->canCommitManagedPartialRepaint()) if (root->canCommitManagedPartialRepaint())
coverage = source->getBoundsRect(); {
// 对支持局部提交的 root,coverage 不能再盯着最深处的 source;
// 否则像“三层 Canvas 里的按钮变色”这种情况,只会登记成一个很小的叶子矩形,
// 顶层 root 提交时既容易漏掉那条直接脏分支,也会低估后续 overlay 补画范围。
Control* branch = source->getManagedRepaintDirectBranch(root);
coverage = branch ? branch->getManagedRepaintCoverageRect() : source->getManagedRepaintCoverageRect();
const RECT sourceCoverage = source->getManagedRepaintCoverageRect();
coverage.left = (std::min)(coverage.left, sourceCoverage.left);
coverage.top = (std::min)(coverage.top, sourceCoverage.top);
coverage.right = (std::max)(coverage.right, sourceCoverage.right);
coverage.bottom = (std::max)(coverage.bottom, sourceCoverage.bottom);
}
for (auto& item : managedRepaintItems) for (auto& item : managedRepaintItems)
{ {
@@ -138,29 +152,108 @@ void Window::flushManagedRepaint()
return; return;
BeginBatchDraw(); BeginBatchDraw();
std::vector<Control*> overlayDialogs; auto unionCoverage = [](RECT& lhs, const RECT& rhs)
{
lhs.left = (std::min)(lhs.left, rhs.left);
lhs.top = (std::min)(lhs.top, rhs.top);
lhs.right = (std::max)(lhs.right, rhs.right);
lhs.bottom = (std::max)(lhs.bottom, rhs.bottom);
};
for (auto& item : managedRepaintItems) auto redrawOverlayUnit = [](Control* unit)
collectManagedDialogOverlays(item.root, item.coverage, overlayDialogs); {
if (!unit || !unit->IsVisible())
return;
// overlay 补画不是“沿用旧快照再贴回去”,而是重新站到当前顶层场景上合成一遍。
unit->invalidateBackgroundSnapshot();
unit->setDirty(true);
unit->draw();
};
auto processManagedRoot = [&](Control* root, const RECT& initialCoverage)
{
if (!root || !root->IsVisible())
return;
root->commitManagedRepaint();
RECT workingCoverage = initialCoverage;
size_t controlStartIdx = controls.size();
for (size_t i = 0; i < controls.size(); ++i)
{
if (controls[i].get() == root)
{
controlStartIdx = i + 1;
break;
}
}
// 顶层普通控件的 overlay 补画必须是“传递式”的:
// 如果 A 重画后把上层按钮 B 也重画出来,而 B 自己又伸进更上层的 C,
// 那么 C 也必须继续补画回来,而不能只看最初 root 的 coverage 一跳收集。
if (controlStartIdx < controls.size())
{
for (size_t i = controlStartIdx; i < controls.size(); ++i)
{
Control* current = controls[i].get();
if (!current || !current->IsVisible())
continue;
const RECT currentCoverage = current->getManagedRepaintCoverageRect();
if (!SxRectsIntersect(currentCoverage, workingCoverage))
continue;
redrawOverlayUnit(current);
unionCoverage(workingCoverage, current->getManagedRepaintCoverageRect());
}
}
size_t dialogStartIdx = 0;
for (size_t i = 0; i < dialogs.size(); ++i)
{
if (dialogs[i].get() == root)
{
dialogStartIdx = i + 1;
break;
}
}
// Dialog 永远位于普通顶层控件之上,也要使用扩张后的 coverage 做传递式补画。
for (size_t i = dialogStartIdx; i < dialogs.size(); ++i)
{
Control* dialog = dialogs[i].get();
if (!dialog || !dialog->IsVisible())
continue;
const RECT dialogCoverage = dialog->getManagedRepaintCoverageRect();
if (!SxRectsIntersect(dialogCoverage, workingCoverage))
continue;
redrawOverlayUnit(dialog);
unionCoverage(workingCoverage, dialog->getManagedRepaintCoverageRect());
}
};
for (auto& control : controls) for (auto& control : controls)
{ {
for (auto& item : managedRepaintItems) for (auto& item : managedRepaintItems)
{ {
if (item.root == control.get() && item.root && item.root->IsVisible()) if (item.root == control.get())
{ {
item.root->commitManagedRepaint(); processManagedRoot(item.root, item.coverage);
break; break;
} }
} }
} }
for (auto& dialog : overlayDialogs) for (auto& dialog : dialogs)
{ {
if (!dialog || !dialog->IsVisible()) for (auto& item : managedRepaintItems)
continue; {
dialog->setDirty(true); if (item.root == dialog.get())
dialog->draw(); {
processManagedRoot(item.root, item.coverage);
break;
}
}
} }
EndBatchDraw(); EndBatchDraw();
@@ -214,6 +307,44 @@ void Window::collectManagedDialogOverlays(Control* repaintRoot, const RECT& cove
} }
} }
/**
* collectManagedControlOverlays(repaintRoot, coverage, overlays)
* 作用:找出在本轮 root 提交后,需要重新补画回上层的普通顶层控件。
* 规则:
* - 只处理 Window::controls 这一层的直接兄弟;
* - 从 repaintRoot 在 controls 中的位置之后开始收集;
* - 仅收集可见且与 coverage 相交的控件。
*/
static void collectManagedControlOverlays(const std::vector<std::unique_ptr<Control>>& controls,
Control* repaintRoot, const RECT& coverage, std::vector<Control*>& overlays)
{
size_t startIdx = controls.size();
for (size_t i = 0; i < controls.size(); ++i)
{
if (controls[i].get() == repaintRoot)
{
startIdx = i + 1;
break;
}
}
if (startIdx > controls.size())
return;
for (size_t i = startIdx; i < controls.size(); ++i)
{
Control* control = controls[i].get();
if (!control || !control->IsVisible())
continue;
if (SxRectsIntersect(control->getBoundsRect(), coverage))
{
if (std::find(overlays.begin(), overlays.end(), control) == overlays.end())
overlays.push_back(control);
}
}
}
/** /**
* ApplyResizableStyle * ApplyResizableStyle
* 作用:统一设置可拉伸/裁剪样式,并按开关使用 WS_EX_COMPOSITED(合成双缓冲)。 * 作用:统一设置可拉伸/裁剪样式,并按开关使用 WS_EX_COMPOSITED(合成双缓冲)。
@@ -621,19 +752,45 @@ int Window::runEventLoop()
} }
if (!consume) if (!consume)
{ {
for (auto it = controls.rbegin(); it != controls.rend(); ++it) Control* firstConsumer = nullptr;
if (msg.message == WM_MOUSEMOVE)
{ {
Control* current = it->get(); // 顶层普通控件的 hover/tooltip 清理规则:
consume = current->handleEvent(msg); // - 第一个命中的兄弟分支收到真实 WM_MOUSEMOVE
if (consume) // - 后续兄弟不再重新命中,只清理旧 hover / tooltip 等瞬时鼠标状态。
for (auto it = controls.rbegin(); it != controls.rend(); ++it)
{ {
if (!SxIsNoisyMsg(msg.message)) Control* current = it->get();
SX_LOGD("Event") << SX_T("事件被控件处理:", "Event consumed by control: ") if (!consume)
<< SxMsgName(msg.message) {
<< SX_T(" id=", " id=") << current->getId(); consume = current->handleEvent(msg);
break; if (consume)
firstConsumer = current;
}
else
{
current->clearTransientMouseState();
}
} }
} }
else
{
for (auto it = controls.rbegin(); it != controls.rend(); ++it)
{
Control* current = it->get();
consume = current->handleEvent(msg);
if (consume)
{
firstConsumer = current;
break;
}
}
}
if (firstConsumer && !SxIsNoisyMsg(msg.message))
SX_LOGD("Event") << SX_T("事件被控件处理:", "Event consumed by control: ")
<< SxMsgName(msg.message)
<< SX_T(" id=", " id=") << firstConsumer->getId();
} }
managedDispatchActive = false; managedDispatchActive = false;
} }
+22 -18
View File
@@ -69,37 +69,41 @@ public:
mutable bool dialogOpen = false; // 项目内使用的状态位,对话框打开标志 mutable bool dialogOpen = false; // 项目内使用的状态位,对话框打开标志
// —— 构造/析构 ——(仅初始化成员;实际样式与子类化在 draw() 中完成) // —— 构造/析构 ——(仅初始化成员;实际样式与子类化在 draw() 中完成)
// 创建纯色背景窗口
Window(int width, int height, int mode); Window(int width, int height, int mode);
// 创建指定背景色窗口
Window(int width, int height, int mode, COLORREF bkcloc); Window(int width, int height, int mode, COLORREF bkcloc);
// 创建带标题与背景色的窗口
Window(int width, int height, int mode, COLORREF bkcloc, std::string headline); Window(int width, int height, int mode, COLORREF bkcloc, std::string headline);
// 析构窗口并释放背景、控件与子类化资源
~Window(); ~Window();
// —— 绘制与事件循环 ——(draw* 完成一次全量绘制;runEventLoop 驱动事件与统一收口) // —— 绘制与事件循环 ——(draw* 完成一次全量绘制;runEventLoop 驱动事件与统一收口)
void draw(); // 纯色背景版本 void draw(); // 纯色背景执行一次全量绘制
void draw(std::string pImgFile); // 背景图版本 void draw(std::string pImgFile); // 背景图执行一次全量绘制
int runEventLoop(); // 主事件循环(PeekMessage + 统一收口重绘) int runEventLoop(); // 主事件循环(PeekMessage + 统一收口重绘)
// —— 背景/标题设置 ——(更换背景、背景色与标题;立即触发一次批量绘制) // —— 背景/标题设置 ——(更换背景、背景色与标题;立即触发一次批量绘制)
void setBkImage(std::string pImgFile); void setBkImage(std::string pImgFile); // 设置窗口背景图
void setBkcolor(COLORREF c); void setBkcolor(COLORREF c); // 设置窗口纯色背景
void setHeadline(std::string headline); void setHeadline(std::string headline); // 设置窗口标题
// —— 控件/对话框管理 ——(添加到容器,或做存在性判断) // —— 控件/对话框管理 ——(添加到容器,或做存在性判断)
void addControl(std::unique_ptr<Control> control); void addControl(std::unique_ptr<Control> control); // 添加普通顶层控件
void addDialog(std::unique_ptr<Control> dialogs); void addDialog(std::unique_ptr<Control> dialogs); // 添加非模态对话框
bool hasNonModalDialogWithCaption(const std::string& caption, const std::string& message) const; bool hasNonModalDialogWithCaption(const std::string& caption, const std::string& message) const; // 检查是否已有同 caption/message 的非模态对话框
// —— 访问器 ——(只读接口,供外部查询当前窗口/标题/背景等) // —— 访问器 ——(只读接口,供外部查询当前窗口/标题/背景等)
HWND getHwnd() const; HWND getHwnd() const; // 获取窗口句柄
int getWidth() const; int getWidth() const; // 获取当前有效客户区宽度
int getHeight() const; int getHeight() const; // 获取当前有效客户区高度
int getPendingWidth() const; int getPendingWidth() const; // 获取待应用宽度
int getPendingHeight() const; int getPendingHeight() const; // 获取待应用高度
std::string getHeadline() const; std::string getHeadline() const; // 获取窗口标题
COLORREF getBkcolor() const; COLORREF getBkcolor() const; // 获取纯色背景
IMAGE* getBkImage() const; IMAGE* getBkImage() const; // 获取背景图对象
std::string getBkImageFile() const; std::string getBkImageFile() const; // 获取背景图路径
std::vector<std::unique_ptr<Control>>& getControls(); std::vector<std::unique_ptr<Control>>& getControls(); // 获取普通顶层控件列表
// —— 尺寸调整 / 托管重绘 ——(事件阶段登记,收口阶段提交) // —— 尺寸调整 / 托管重绘 ——(事件阶段登记,收口阶段提交)
+313 -111
View File
@@ -1,13 +1,12 @@
// StellarX 星垣GUI框架 - 测试用例 // StellarX 星垣GUI框架 - 测试用例
#include"StellarX.h" #include"StellarX.h"
#include <vector>
#ifndef KEY #ifndef KEY
#define KEY 5 #define KEY 4
#endif #endif
#if 5 == KEY #if 5 == KEY
#include"StellarX.h"
int main() int main()
{ {
StellarX::SxLogger::setGBK(); StellarX::SxLogger::setGBK();
@@ -16,199 +15,402 @@ int main()
StellarX::SxLogger::Get().setLanguage(StellarX::SxLogLanguage::ZhCN); StellarX::SxLogger::Get().setLanguage(StellarX::SxLogLanguage::ZhCN);
const COLORREF headerColor = RGB(232, 238, 245); const COLORREF headerColor = RGB(232, 238, 245);
const COLORREF horizontalColor = RGB(217, 233, 252); const COLORREF nestedZoneColor = RGB(217, 233, 252);
const COLORREF nestedColor = RGB(219, 244, 223); const COLORREF level1Color = RGB(224, 244, 255);
const COLORREF verticalColor = RGB(255, 233, 205); const COLORREF level2Color = RGB(222, 244, 228);
const COLORREF level3Color = RGB(255, 239, 215);
const COLORREF tabZoneColor = RGB(220, 241, 240);
const COLORREF behaviorZoneColor = RGB(243, 238, 214);
const COLORREF overlayZoneColor = RGB(255, 228, 206);
const COLORREF tableZoneColor = RGB(235, 226, 250); const COLORREF tableZoneColor = RGB(235, 226, 250);
const COLORREF movingColor = RGB(249, 224, 232); const COLORREF floatingColor = RGB(249, 224, 232);
const COLORREF buttonFalseColor = RGB(246, 247, 249);
const COLORREF buttonHoverColor = RGB(255, 225, 92);
const COLORREF buttonTrueColor = RGB(236, 138, 88);
Window win(1280, 780, 1, RGB(246, 248, 251), "StellarX 布局系统专项回归 KEY5"); Window win(1380, 880, 1, RGB(246, 248, 251), "StellarX KEY5 第二阶段专项回归");
auto header = std::make_unique<Canvas>(20, 20, 1240, 100); auto configureTooltip = [](Button* button, const std::string& text)
{
button->enableTooltip(true);
button->setTooltipDelay(120);
button->setTooltipText(text);
};
auto makeTestButton = [&](int x, int y, int width, int height, const std::string& text)
{
auto button = std::make_unique<Button>(x, y, width, height, text, buttonTrueColor, buttonFalseColor, buttonHoverColor);
configureTooltip(button.get(), text);
return button;
};
auto header = std::make_unique<Canvas>(20, 20, 1340, 90);
auto headerPtr = header.get(); auto headerPtr = header.get();
headerPtr->setShape(StellarX::ControlShape::ROUND_RECTANGLE); headerPtr->setShape(StellarX::ControlShape::ROUND_RECTANGLE);
headerPtr->setCanvasBkColor(headerColor); headerPtr->setCanvasBkColor(headerColor);
headerPtr->setLayoutMode(StellarX::LayoutMode::AnchorToEdges); headerPtr->setLayoutMode(StellarX::LayoutMode::AnchorToEdges);
headerPtr->setAnchor(StellarX::Anchor::Left, StellarX::Anchor::Right); headerPtr->setAnchor(StellarX::Anchor::Left, StellarX::Anchor::Right);
auto headerTitle = std::make_unique<Label>(18, 8, "KEY5布局系统专项回归"); auto headerTitle = std::make_unique<Label>(18, 8, "KEY5第二阶段专项回归");
headerTitle->textStyle.nHeight = 26; headerTitle->textStyle.nHeight = 26;
headerTitle->setDirty(true);
headerTitle->setTextdisap(true); headerTitle->setTextdisap(true);
auto headerLine1 = std::make_unique<Label>(18, 46, "观察点 1:蓝色区域横向拉伸;绿色嵌套区域随父容器一起变化。"); auto headerLine1 = std::make_unique<Label>(18, 44, "A 三层嵌套 B TabControl C Label/跨容器 hover D 同父 overlay E Table/顶层浮层");
headerLine1->setTextdisap(true); headerLine1->setTextdisap(true);
auto headerLine2 = std::make_unique<Label>(18, 70, "观察点 2:橙色区域纵向拉伸;紫色区域中 Table 只做横向拉伸;粉色区域整体随底边移动。"); auto headerLine2 = std::make_unique<Label>(18, 64, "重点看:stretch、父局部到世界坐标、tooltip 清理、overlay 补画、分页按钮与页码");
headerLine2->setTextdisap(true); headerLine2->setTextdisap(true);
headerPtr->addControl(std::move(headerTitle)); headerPtr->addControl(std::move(headerTitle));
headerPtr->addControl(std::move(headerLine1)); headerPtr->addControl(std::move(headerLine1));
headerPtr->addControl(std::move(headerLine2)); headerPtr->addControl(std::move(headerLine2));
auto horizontalZone = std::make_unique<Canvas>(20, 140, 1240, 220); auto nestedZone = std::make_unique<Canvas>(20, 130, 650, 280);
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(); auto nestedZonePtr = nestedZone.get();
nestedZonePtr->setShape(StellarX::ControlShape::ROUND_RECTANGLE); nestedZonePtr->setShape(StellarX::ControlShape::ROUND_RECTANGLE);
nestedZonePtr->setCanvasBkColor(nestedColor); nestedZonePtr->setCanvasBkColor(nestedZoneColor);
nestedZonePtr->setLayoutMode(StellarX::LayoutMode::AnchorToEdges); nestedZonePtr->setLayoutMode(StellarX::LayoutMode::AnchorToEdges);
nestedZonePtr->setAnchor(StellarX::Anchor::Left, StellarX::Anchor::Right); nestedZonePtr->setAnchor(StellarX::Anchor::Left, StellarX::Anchor::Right);
auto nestedTitle = std::make_unique<Label>(12, 10, "区域 A-1(绿):嵌套 Canvas。父容器拉伸后,这里的左右锚点和世界坐标都要同步更新。"); auto nestedTitle = std::make_unique<Label>(16, 16, "A 蓝:三层 Canvas 嵌套");
nestedTitle->setTextdisap(true); nestedTitle->setTextdisap(true);
auto nestedLeftBtn = std::make_unique<Button>(18, 40, 120, 30, "内层左固定"); auto nestedHint = std::make_unique<Label>(16, 40, "看父局部到世界坐标链。");
nestedHint->setTextdisap(true);
auto nestedLeftBtn = makeTestButton(20, 74, 110, 34, "左固定");
nestedLeftBtn->setLayoutMode(StellarX::LayoutMode::AnchorToEdges); nestedLeftBtn->setLayoutMode(StellarX::LayoutMode::AnchorToEdges);
nestedLeftBtn->setAnchor(StellarX::Anchor::Left, StellarX::Anchor::NoAnchor); nestedLeftBtn->setAnchor(StellarX::Anchor::Left, StellarX::Anchor::NoAnchor);
auto nestedRightBtn = std::make_unique<Button>(942, 40, 120, 30, "内层右固定");
auto nestedStretchBox = std::make_unique<TextBox>(150, 74, 350, 34, "A 区横向拉伸输入框");
nestedStretchBox->setLayoutMode(StellarX::LayoutMode::AnchorToEdges);
nestedStretchBox->setAnchor(StellarX::Anchor::Left, StellarX::Anchor::Right);
auto nestedRightBtn = makeTestButton(520, 74, 110, 34, "右固定");
nestedRightBtn->setLayoutMode(StellarX::LayoutMode::AnchorToEdges); nestedRightBtn->setLayoutMode(StellarX::LayoutMode::AnchorToEdges);
nestedRightBtn->setAnchor(StellarX::Anchor::Right, StellarX::Anchor::NoAnchor); nestedRightBtn->setAnchor(StellarX::Anchor::Right, StellarX::Anchor::NoAnchor);
auto level1 = std::make_unique<Canvas>(18, 126, 614, 134);
auto level1Ptr = level1.get();
level1Ptr->setShape(StellarX::ControlShape::ROUND_RECTANGLE);
level1Ptr->setCanvasBkColor(level1Color);
level1Ptr->setLayoutMode(StellarX::LayoutMode::AnchorToEdges);
level1Ptr->setAnchor(StellarX::Anchor::Left, StellarX::Anchor::Right);
auto level1Title = std::make_unique<Label>(14, 10, "A-1 青:第一层");
level1Title->setTextdisap(true);
auto level1Btn = makeTestButton(18, 42, 110, 30, "内层左");
level1Btn->setLayoutMode(StellarX::LayoutMode::AnchorToEdges);
level1Btn->setAnchor(StellarX::Anchor::Left, StellarX::Anchor::NoAnchor);
auto level2 = std::make_unique<Canvas>(142, 36, 454, 84);
auto level2Ptr = level2.get();
level2Ptr->setShape(StellarX::ControlShape::ROUND_RECTANGLE);
level2Ptr->setCanvasBkColor(level2Color);
level2Ptr->setLayoutMode(StellarX::LayoutMode::AnchorToEdges);
level2Ptr->setAnchor(StellarX::Anchor::Left, StellarX::Anchor::Right);
auto level2Title = std::make_unique<Label>(12, 8, "A-2 绿:第二层");
level2Title->setTextdisap(true);
auto level2Btn = makeTestButton(332, 8, 108, 28, "右固定");
level2Btn->setLayoutMode(StellarX::LayoutMode::AnchorToEdges);
level2Btn->setAnchor(StellarX::Anchor::Right, StellarX::Anchor::NoAnchor);
auto level3 = std::make_unique<Canvas>(16, 42, 422, 28);
auto level3Ptr = level3.get();
level3Ptr->setShape(StellarX::ControlShape::ROUND_RECTANGLE);
level3Ptr->setCanvasBkColor(level3Color);
level3Ptr->setLayoutMode(StellarX::LayoutMode::AnchorToEdges);
level3Ptr->setAnchor(StellarX::Anchor::Left, StellarX::Anchor::Right);
auto level3Label = std::make_unique<Label>(10, 6, "A-3 橙:第三层");
level3Label->setTextdisap(true);
auto level3Btn = makeTestButton(312, 2, 98, 24, "右按钮");
level3Btn->setLayoutMode(StellarX::LayoutMode::AnchorToEdges);
level3Btn->setAnchor(StellarX::Anchor::Right, StellarX::Anchor::NoAnchor);
configureTooltip(level3Btn.get(), "第三层 Canvas 里的右固定按钮");
level3Ptr->addControl(std::move(level3Label));
level3Ptr->addControl(std::move(level3Btn));
level2Ptr->addControl(std::move(level2Title));
level2Ptr->addControl(std::move(level2Btn));
level2Ptr->addControl(std::move(level3));
level1Ptr->addControl(std::move(level1Title));
level1Ptr->addControl(std::move(level1Btn));
level1Ptr->addControl(std::move(level2));
nestedZonePtr->addControl(std::move(nestedTitle)); nestedZonePtr->addControl(std::move(nestedTitle));
nestedZonePtr->addControl(std::move(nestedHint));
nestedZonePtr->addControl(std::move(nestedLeftBtn)); nestedZonePtr->addControl(std::move(nestedLeftBtn));
nestedZonePtr->addControl(std::move(nestedStretchBox));
nestedZonePtr->addControl(std::move(nestedRightBtn)); nestedZonePtr->addControl(std::move(nestedRightBtn));
nestedZonePtr->addControl(std::move(level1));
horizontalZonePtr->addControl(std::move(horizontalTitle)); auto tabZone = std::make_unique<Canvas>(690, 130, 670, 280);
horizontalZonePtr->addControl(std::move(fixedLabel)); auto tabZonePtr = tabZone.get();
horizontalZonePtr->addControl(std::move(leftFixedBtn)); tabZonePtr->setShape(StellarX::ControlShape::ROUND_RECTANGLE);
horizontalZonePtr->addControl(std::move(rightFixedBtn)); tabZonePtr->setCanvasBkColor(tabZoneColor);
horizontalZonePtr->addControl(std::move(stretchBox)); tabZonePtr->setLayoutMode(StellarX::LayoutMode::AnchorToEdges);
horizontalZonePtr->addControl(std::move(nestedZone)); tabZonePtr->setAnchor(StellarX::Anchor::Right, StellarX::Anchor::NoAnchor);
auto verticalZone = std::make_unique<Canvas>(20, 380, 420, 278); auto tabTitle = std::make_unique<Label>(16, 16, "B 青:TabControl 外层 resize");
auto verticalZonePtr = verticalZone.get(); tabTitle->setTextdisap(true);
verticalZonePtr->setShape(StellarX::ControlShape::ROUND_RECTANGLE); auto tabHint = std::make_unique<Label>(16, 40, "看页签、页面和页内控件。");
verticalZonePtr->setCanvasBkColor(verticalColor); tabHint->setTextdisap(true);
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 控件要随底边移动。"); auto tabControl = std::make_unique<TabControl>(18, 60, 634, 200);
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(); auto tabControlPtr = tabControl.get();
tabControlPtr->setShape(StellarX::ControlShape::ROUND_RECTANGLE); tabControlPtr->setShape(StellarX::ControlShape::ROUND_RECTANGLE);
tabControlPtr->setCanvasfillMode(StellarX::FillMode::Null); tabControlPtr->setCanvasfillMode(StellarX::FillMode::Null);
tabControlPtr->setTabPlacement(StellarX::TabPlacement::Top); tabControlPtr->setTabPlacement(StellarX::TabPlacement::Top);
tabControlPtr->setTabBarHeight(28); tabControlPtr->setTabBarHeight(30);
tabControlPtr->setLayoutMode(StellarX::LayoutMode::AnchorToEdges); tabControlPtr->setLayoutMode(StellarX::LayoutMode::AnchorToEdges);
tabControlPtr->setAnchor(StellarX::Anchor::Left, StellarX::Anchor::Right); tabControlPtr->setAnchor(StellarX::Anchor::Left, StellarX::Anchor::Right);
auto tabPage1 = std::make_unique<Canvas>(0, 0, 800, 132); auto tabPage1 = std::make_unique<Canvas>(0, 0, 634, 170);
auto tabPage1Ptr = tabPage1.get(); auto tabPage1Ptr = tabPage1.get();
tabPage1Ptr->setShape(StellarX::ControlShape::ROUND_RECTANGLE); tabPage1Ptr->setShape(StellarX::ControlShape::ROUND_RECTANGLE);
tabPage1Ptr->setCanvasBkColor(RGB(241, 247, 255)); tabPage1Ptr->setCanvasBkColor(RGB(241, 247, 255));
auto tabPage2 = std::make_unique<Canvas>(0, 0, 800, 132);
auto tabPage2 = std::make_unique<Canvas>(0, 0, 634, 170);
auto tabPage2Ptr = tabPage2.get(); auto tabPage2Ptr = tabPage2.get();
tabPage2Ptr->setShape(StellarX::ControlShape::ROUND_RECTANGLE); tabPage2Ptr->setShape(StellarX::ControlShape::ROUND_RECTANGLE);
tabPage2Ptr->setCanvasBkColor(RGB(248, 244, 236)); tabPage2Ptr->setCanvasBkColor(RGB(248, 244, 236));
auto page1Label = std::make_unique<Label>(16, 12, "区域 C(页 1):TabControl 外层参与统一解算,页内 TextBox 继续由 Canvas 负责拉伸"); auto page1Label = std::make_unique<Label>(14, 12, "页 1页内 TextBox 继续由 Canvas ");
page1Label->setTextdisap(true); page1Label->setTextdisap(true);
auto page1Box = std::make_unique<TextBox>(20, 52, 650, 34, "左右拉伸输入框"); auto page1Box = std::make_unique<TextBox>(18, 44, 468, 34, " 1 左右拉伸输入框");
page1Box->setLayoutMode(StellarX::LayoutMode::AnchorToEdges); page1Box->setLayoutMode(StellarX::LayoutMode::AnchorToEdges);
page1Box->setAnchor(StellarX::Anchor::Left, StellarX::Anchor::Right); page1Box->setAnchor(StellarX::Anchor::Left, StellarX::Anchor::Right);
auto page1Btn = std::make_unique<Button>(692, 52, 90, 34, "右固定"); auto page1Btn = makeTestButton(504, 44, 96, 34, "右固定");
page1Btn->setLayoutMode(StellarX::LayoutMode::AnchorToEdges); page1Btn->setLayoutMode(StellarX::LayoutMode::AnchorToEdges);
page1Btn->setAnchor(StellarX::Anchor::Right, StellarX::Anchor::NoAnchor); page1Btn->setAnchor(StellarX::Anchor::Right, StellarX::Anchor::NoAnchor);
configureTooltip(page1Btn.get(), "页 1 右固定按钮");
auto page1InnerCanvas = std::make_unique<Canvas>(18, 92, 582, 56);
auto page1InnerCanvasPtr = page1InnerCanvas.get();
page1InnerCanvasPtr->setShape(StellarX::ControlShape::ROUND_RECTANGLE);
page1InnerCanvasPtr->setCanvasBkColor(RGB(223, 236, 248));
page1InnerCanvasPtr->setLayoutMode(StellarX::LayoutMode::AnchorToEdges);
page1InnerCanvasPtr->setAnchor(StellarX::Anchor::Left, StellarX::Anchor::Right);
auto page1InnerLabel = std::make_unique<Label>(10, 8, "页 1 内层 Canvas");
page1InnerLabel->setTextdisap(true);
auto page1InnerBtn = makeTestButton(462, 14, 108, 28, "右按钮");
page1InnerBtn->setLayoutMode(StellarX::LayoutMode::AnchorToEdges);
page1InnerBtn->setAnchor(StellarX::Anchor::Right, StellarX::Anchor::NoAnchor);
configureTooltip(page1InnerBtn.get(), "页 1 内层 Canvas 按钮");
page1InnerCanvasPtr->addControl(std::move(page1InnerLabel));
page1InnerCanvasPtr->addControl(std::move(page1InnerBtn));
tabPage1Ptr->addControl(std::move(page1Label)); tabPage1Ptr->addControl(std::move(page1Label));
tabPage1Ptr->addControl(std::move(page1Box)); tabPage1Ptr->addControl(std::move(page1Box));
tabPage1Ptr->addControl(std::move(page1Btn)); tabPage1Ptr->addControl(std::move(page1Btn));
tabPage1Ptr->addControl(std::move(page1InnerCanvas));
auto page2Label = std::make_unique<Label>(16, 18, "区域 C页 2:切页后再次拖动窗口,页签栏和页区域都应保持稳定"); auto page2Label = std::make_unique<Label>(14, 12, "页 2:切页后继续测 hover 和 resize");
page2Label->setTextdisap(true); page2Label->setTextdisap(true);
auto page2Btn = std::make_unique<Button>(20, 60, 120, 34, "普通按钮"); auto page2Btn = makeTestButton(18, 44, 126, 34, "Tooltip 按钮");
configureTooltip(page2Btn.get(), "页 2 普通按钮 Tooltip");
auto page2InnerCanvas = std::make_unique<Canvas>(166, 36, 434, 84);
auto page2InnerCanvasPtr = page2InnerCanvas.get();
page2InnerCanvasPtr->setShape(StellarX::ControlShape::ROUND_RECTANGLE);
page2InnerCanvasPtr->setCanvasBkColor(RGB(235, 229, 214));
auto page2InnerLabel = std::make_unique<Label>(10, 8, "页 2 内层 Canvas");
page2InnerLabel->setTextdisap(true);
auto page2InnerBtn = makeTestButton(286, 42, 126, 28, "右侧 Tooltip");
page2InnerBtn->setLayoutMode(StellarX::LayoutMode::AnchorToEdges);
page2InnerBtn->setAnchor(StellarX::Anchor::Right, StellarX::Anchor::NoAnchor);
configureTooltip(page2InnerBtn.get(), "页 2 内层按钮 Tooltip");
page2InnerCanvasPtr->addControl(std::move(page2InnerLabel));
page2InnerCanvasPtr->addControl(std::move(page2InnerBtn));
tabPage2Ptr->addControl(std::move(page2Label)); tabPage2Ptr->addControl(std::move(page2Label));
tabPage2Ptr->addControl(std::move(page2Btn)); tabPage2Ptr->addControl(std::move(page2Btn));
tabPage2Ptr->addControl(std::move(page2InnerCanvas));
tabControlPtr->add(std::make_pair(std::make_unique<Button>(0, 0, 120, 28, "布局页"), std::move(tabPage1))); auto tabBtn1 = std::make_unique<Button>(0, 0, 108, 30, "布局页");
tabControlPtr->add(std::make_pair(std::make_unique<Button>(0, 0, 120, 28, "切换"), std::move(tabPage2))); auto tabBtn2 = std::make_unique<Button>(0, 0, 108, 30, "交互");
configureTooltip(tabBtn1.get(), "布局页页签按钮");
configureTooltip(tabBtn2.get(), "交互页页签按钮");
tabControlPtr->add(std::make_pair(std::move(tabBtn1), std::move(tabPage1)));
tabControlPtr->add(std::make_pair(std::move(tabBtn2), std::move(tabPage2)));
tabControlPtr->setActiveIndex(0); tabControlPtr->setActiveIndex(0);
auto tableZone = std::make_unique<Canvas>(460, 566, 800, 132); tabZonePtr->addControl(std::move(tabTitle));
tabZonePtr->addControl(std::move(tabHint));
tabZonePtr->addControl(std::move(tabControl));
auto behaviorZone = std::make_unique<Canvas>(20, 430, 650, 190);
auto behaviorZonePtr = behaviorZone.get();
behaviorZonePtr->setShape(StellarX::ControlShape::ROUND_RECTANGLE);
behaviorZonePtr->setCanvasBkColor(behaviorZoneColor);
auto behaviorTitle = std::make_unique<Label>(16, 16, "C 米:Label 内容驱动 + 跨容器 hover");
behaviorTitle->setTextdisap(true);
auto dynamicLabel = std::make_unique<Label>(20, 52, "动态标签:默认短文本");
auto dynamicLabelPtr = dynamicLabel.get();
dynamicLabelPtr->textStyle.nHeight = 22;
dynamicLabelPtr->setDirty(true);
dynamicLabelPtr->setTextdisap(true);
auto behaviorHint = std::make_unique<Label>(20, 82, "改样式后会手动 setDirty(true)。");
behaviorHint->setTextdisap(true);
std::vector<std::string> labelTexts =
{
"动态标签:默认短文本",
"动态标签:切到更长一点的文本",
"动态标签:短"
};
int labelIndex = 0;
bool styleToggle = false;
auto changeTextBtn = makeTestButton(20, 120, 120, 34, "改文本");
auto changeStyleBtn = makeTestButton(156, 120, 150, 34, "改样式");
changeTextBtn->setOnClickListener([&]()
{
labelIndex = (labelIndex + 1) % static_cast<int>(labelTexts.size());
dynamicLabelPtr->setText(labelTexts[labelIndex]);
});
changeStyleBtn->setOnClickListener([&]()
{
styleToggle = !styleToggle;
dynamicLabelPtr->textStyle.nHeight = styleToggle ? 30 : 22;
dynamicLabelPtr->textStyle.color = styleToggle ? RGB(184, 87, 44) : RGB(0, 0, 0);
dynamicLabelPtr->setDirty(true);
});
auto innerHoverBtn = makeTestButton(506, 120, 118, 34, "容器内按钮");
innerHoverBtn->setLayoutMode(StellarX::LayoutMode::AnchorToEdges);
innerHoverBtn->setAnchor(StellarX::Anchor::Right, StellarX::Anchor::NoAnchor);
configureTooltip(innerHoverBtn.get(), "Canvas 内按钮 Tooltip");
behaviorZonePtr->addControl(std::move(behaviorTitle));
behaviorZonePtr->addControl(std::move(dynamicLabel));
behaviorZonePtr->addControl(std::move(behaviorHint));
behaviorZonePtr->addControl(std::move(changeTextBtn));
behaviorZonePtr->addControl(std::move(changeStyleBtn));
behaviorZonePtr->addControl(std::move(innerHoverBtn));
auto overlayZone = std::make_unique<Canvas>(690, 430, 670, 190);
auto overlayZonePtr = overlayZone.get();
overlayZonePtr->setShape(StellarX::ControlShape::ROUND_RECTANGLE);
overlayZonePtr->setCanvasBkColor(overlayZoneColor);
overlayZonePtr->setLayoutMode(StellarX::LayoutMode::AnchorToEdges);
overlayZonePtr->setAnchor(StellarX::Anchor::Left, StellarX::Anchor::Right);
auto overlayTitle = std::make_unique<Label>(16, 16, "D 橙:Top+Bottom + 同父 overlay");
overlayTitle->setTextdisap(true);
auto lowerLayer = std::make_unique<Canvas>(20, 56, 356, 104);
auto lowerLayerPtr = lowerLayer.get();
lowerLayerPtr->setShape(StellarX::ControlShape::ROUND_RECTANGLE);
lowerLayerPtr->setCanvasBkColor(RGB(255, 245, 231));
lowerLayerPtr->setLayoutMode(StellarX::LayoutMode::AnchorToEdges);
lowerLayerPtr->setAnchor(StellarX::Anchor::Top, StellarX::Anchor::Bottom);
auto lowerTitle = std::make_unique<Label>(12, 8, "D-1 下层区");
lowerTitle->setTextdisap(true);
auto lowerBox = std::make_unique<TextBox>(12, 34, 222, 30, "下层区输入框");
lowerBox->setLayoutMode(StellarX::LayoutMode::AnchorToEdges);
lowerBox->setAnchor(StellarX::Anchor::Left, StellarX::Anchor::Right);
auto lowerBtn = makeTestButton(248, 34, 92, 30, "下层按钮");
lowerBtn->setLayoutMode(StellarX::LayoutMode::AnchorToEdges);
lowerBtn->setAnchor(StellarX::Anchor::Right, StellarX::Anchor::NoAnchor);
configureTooltip(lowerBtn.get(), "下层 Canvas 按钮");
lowerLayerPtr->addControl(std::move(lowerTitle));
lowerLayerPtr->addControl(std::move(lowerBox));
lowerLayerPtr->addControl(std::move(lowerBtn));
auto upperLayer = std::make_unique<Canvas>(262, 94, 382, 78);
auto upperLayerPtr = upperLayer.get();
upperLayerPtr->setShape(StellarX::ControlShape::ROUND_RECTANGLE);
upperLayerPtr->setCanvasBkColor(RGB(255, 224, 206));
upperLayerPtr->setLayoutMode(StellarX::LayoutMode::AnchorToEdges);
upperLayerPtr->setAnchor(StellarX::Anchor::Right, StellarX::Anchor::NoAnchor);
auto upperTitle = std::make_unique<Label>(12, 8, "D-2 上层区");
upperTitle->setTextdisap(true);
auto upperBtn = makeTestButton(244, 38, 122, 28, "上层 Tooltip");
upperBtn->setLayoutMode(StellarX::LayoutMode::AnchorToEdges);
upperBtn->setAnchor(StellarX::Anchor::Right, StellarX::Anchor::NoAnchor);
configureTooltip(upperBtn.get(), "上层 Canvas 按钮");
upperLayerPtr->addControl(std::move(upperTitle));
upperLayerPtr->addControl(std::move(upperBtn));
auto overlayBottomBtn = makeTestButton(526, 148, 118, 30, "底边固定");
overlayBottomBtn->setLayoutMode(StellarX::LayoutMode::AnchorToEdges);
overlayBottomBtn->setAnchor(StellarX::Anchor::Bottom, StellarX::Anchor::NoAnchor);
overlayZonePtr->addControl(std::move(overlayTitle));
overlayZonePtr->addControl(std::move(lowerLayer));
overlayZonePtr->addControl(std::move(upperLayer));
overlayZonePtr->addControl(std::move(overlayBottomBtn));
auto tableZone = std::make_unique<Canvas>(20, 640, 980, 200);
auto tableZonePtr = tableZone.get(); auto tableZonePtr = tableZone.get();
tableZonePtr->setShape(StellarX::ControlShape::ROUND_RECTANGLE); tableZonePtr->setShape(StellarX::ControlShape::ROUND_RECTANGLE);
tableZonePtr->setCanvasBkColor(tableZoneColor); tableZonePtr->setCanvasBkColor(tableZoneColor);
tableZonePtr->setLayoutMode(StellarX::LayoutMode::AnchorToEdges); tableZonePtr->setLayoutMode(StellarX::LayoutMode::AnchorToEdges);
tableZonePtr->setAnchor(StellarX::Anchor::Left, StellarX::Anchor::Right); tableZonePtr->setAnchor(StellarX::Anchor::Left, StellarX::Anchor::Right);
auto tableTitle = std::make_unique<Label>(16, 10, "区域 D(紫):Table 当前只回归 X 轴拉伸,不做 Y 轴拉伸。"); auto tableTitle = std::make_unique<Label>(16, 12, "E 紫:Table 仅 X Stretch");
tableTitle->setTextdisap(true); tableTitle->setTextdisap(true);
auto table = std::make_unique<Table>(20, 46); auto tableHint = std::make_unique<Label>(16, 34, "看翻页按钮、页码和顶层浮层补画。");
tableHint->setTextdisap(true);
auto table = std::make_unique<Table>(20, 62);
auto tablePtr = table.get(); auto tablePtr = table.get();
tablePtr->setHeaders({ "编号", "回归点", "预期" }); tablePtr->setHeaders({ "编号", "回归点", "预期" });
tablePtr->setData({ tablePtr->setData({
{"01", "左右拉伸", "窗口变宽后列宽一起增大"}, {"01", "左右拉伸", "窗口变宽后列宽一起增大"},
{"02", "纵向固定", "表格高度保持当前设计值"}, {"02", "分页按钮", "hover / click / leave 都能及时刷新"},
{"03", "脚重排", "分页按钮和页码随宽度重排"} {"03", "码标签", "整表重绘后不会消失"},
{"04", "顶层浮层", "下层重绘后上层还能补回来"}
}); });
tablePtr->setRowsPerPage(2); tablePtr->setRowsPerPage(2);
tablePtr->setTableBorderWidth(1); tablePtr->setTableBorderWidth(1);
tablePtr->setTableBorder(RGB(86, 88, 132)); tablePtr->setTableBorder(RGB(86, 88, 132));
tablePtr->setLayoutMode(StellarX::LayoutMode::AnchorToEdges); tablePtr->setLayoutMode(StellarX::LayoutMode::AnchorToEdges);
tablePtr->setAnchor(StellarX::Anchor::Left, StellarX::Anchor::Right); tablePtr->setAnchor(StellarX::Anchor::Left, StellarX::Anchor::Right);
tableZonePtr->addControl(std::move(tableTitle)); tableZonePtr->addControl(std::move(tableTitle));
tableZonePtr->addControl(std::move(tableHint));
tableZonePtr->addControl(std::move(table)); tableZonePtr->addControl(std::move(table));
auto movingZone = std::make_unique<Canvas>(934, 706, 326, 70); auto topHoverBtn = makeTestButton(646, 550, 118, 34, "顶层按钮");
auto movingZonePtr = movingZone.get(); configureTooltip(topHoverBtn.get(), "顶层按钮 Tooltip");
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 容器。"); auto floatingZone = std::make_unique<Canvas>(900, 702, 460, 138);
movingLabel->setTextdisap(true); auto floatingZonePtr = floatingZone.get();
auto movingHint = std::make_unique<Label>(14, 30, "窗口变高时整体向下移动。"); floatingZonePtr->setShape(StellarX::ControlShape::ROUND_RECTANGLE);
movingHint->setTextdisap(true); floatingZonePtr->setCanvasBkColor(floatingColor);
auto movingBtn = std::make_unique<Button>(208, 38, 100, 24, "右固定"); floatingZonePtr->setLayoutMode(StellarX::LayoutMode::AnchorToEdges);
movingBtn->setLayoutMode(StellarX::LayoutMode::AnchorToEdges); floatingZonePtr->setAnchor(StellarX::Anchor::Bottom, StellarX::Anchor::NoAnchor);
movingBtn->setAnchor(StellarX::Anchor::Right, StellarX::Anchor::NoAnchor);
movingZonePtr->addControl(std::move(movingLabel)); auto floatingTitle = std::make_unique<Label>(16, 12, "F 粉:Bottom only 浮层");
movingZonePtr->addControl(std::move(movingHint)); floatingTitle->setTextdisap(true);
movingZonePtr->addControl(std::move(movingBtn)); auto floatingHint = std::make_unique<Label>(16, 36, "看 Table 下层重绘后的补画。");
floatingHint->setTextdisap(true);
auto floatingBtn = makeTestButton(322, 88, 118, 30, "右固定");
floatingBtn->setLayoutMode(StellarX::LayoutMode::AnchorToEdges);
floatingBtn->setAnchor(StellarX::Anchor::Right, StellarX::Anchor::NoAnchor);
configureTooltip(floatingBtn.get(), "粉色浮层里的按钮");
floatingZonePtr->addControl(std::move(floatingTitle));
floatingZonePtr->addControl(std::move(floatingHint));
floatingZonePtr->addControl(std::move(floatingBtn));
win.addControl(std::move(header)); win.addControl(std::move(header));
win.addControl(std::move(horizontalZone)); win.addControl(std::move(nestedZone));
win.addControl(std::move(verticalZone)); win.addControl(std::move(tabZone));
win.addControl(std::move(tabControl)); win.addControl(std::move(behaviorZone));
win.addControl(std::move(topHoverBtn));
win.addControl(std::move(overlayZone));
win.addControl(std::move(tableZone)); win.addControl(std::move(tableZone));
win.addControl(std::move(movingZone)); win.addControl(std::move(floatingZone));
win.draw(); win.draw();
return win.runEventLoop(); return win.runEventLoop();
@@ -231,7 +433,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","wadsacafadsawd"}, {"lisi","20","99.99","wadsacafadssssssssssssssssssssssssssssssssssssssssssssssssssawd"},
{"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" },