#include "Control.h" #include "SxLog.h" #include #include "Window.h" #include namespace { // 单轴解算后的临时结果:只描述“在父局部坐标系中的位置和尺寸”。 struct LayoutAxisResult { int pos = 0; int size = 0; }; int ClampNonNegative(int value) { return value < 0 ? 0 : value; } int RoundDiv(long long numerator, long long denominator) { if (denominator == 0) return 0; if (numerator >= 0) return static_cast((numerator + denominator / 2) / denominator); return -static_cast(((-numerator) + denominator / 2) / denominator); } // 旧接口兼容层: // 将 setAnchor(a1, a2) 的双锚点输入映射为单轴 LayoutSpec。 // 这里只负责“从旧输入翻译成新模型”,不直接参与后续求解。 StellarX::AxisLayoutSpec BuildLegacyAxisSpec(StellarX::Anchor anchor1, StellarX::Anchor anchor2, StellarX::Anchor startAnchor, StellarX::Anchor endAnchor) { StellarX::AxisLayoutSpec spec{}; spec.anchorStart = (anchor1 == startAnchor || anchor2 == startAnchor); spec.anchorEnd = (anchor1 == endAnchor || anchor2 == endAnchor); if (spec.anchorStart && spec.anchorEnd) { spec.sizePolicy = StellarX::AxisSizePolicy::Stretch; spec.alignPolicy = StellarX::AxisAlignPolicy::Start; } else if (spec.anchorEnd) { spec.sizePolicy = StellarX::AxisSizePolicy::FixedSize; spec.alignPolicy = StellarX::AxisAlignPolicy::End; } else { spec.sizePolicy = StellarX::AxisSizePolicy::FixedSize; spec.alignPolicy = StellarX::AxisAlignPolicy::Start; } return spec; } // 单轴统一解算器: // 输入设计态父尺寸、当前父尺寸、子控件设计态位置/尺寸,以及当前轴策略, // 输出该轴在“父局部坐标系”下的运行态位置与尺寸。 LayoutAxisResult ResolveAxis(int parentDesignSize, int parentCurrentSize, int childDesignPos, int childDesignSize, const StellarX::AxisLayoutSpec& spec, bool allowStretch) { LayoutAxisResult result{}; const int startMargin = childDesignPos; const int endMargin = parentDesignSize - (childDesignPos + childDesignSize); const bool wantsStretch = spec.sizePolicy == StellarX::AxisSizePolicy::Stretch; const bool canStretch = allowStretch && spec.anchorStart && spec.anchorEnd; if (wantsStretch && canStretch) { // 双边锚定 + Stretch: // 保持设计态中的起边距和终边距,让尺寸随父容器变化。 result.pos = startMargin; result.size = ClampNonNegative(parentCurrentSize - startMargin - endMargin); return result; } // 其余情况一律按固定尺寸处理;即便调用方请求 Stretch, // 只要锚点条件或控件能力边界不满足,也会在这里自然降级为 FixedSize。 result.size = ClampNonNegative(childDesignSize); if (spec.anchorStart && !spec.anchorEnd) { // 仅锚定起边:保持起边距,尺寸不变。 result.pos = startMargin; return result; } if (!spec.anchorStart && spec.anchorEnd) { // 仅锚定终边:保持终边距,尺寸不变。 result.pos = parentCurrentSize - endMargin - result.size; return result; } if (spec.anchorStart && spec.anchorEnd) { // 双边锚定但尺寸固定: // 位置由 alignPolicy 决定,用于表达“只位移、不拉伸”的场景。 switch (spec.alignPolicy) { case StellarX::AxisAlignPolicy::End: result.pos = parentCurrentSize - endMargin - result.size; break; case StellarX::AxisAlignPolicy::Center: // 保持相对父容器中心的偏移关系。 result.pos = childDesignPos + (parentCurrentSize - parentDesignSize) / 2; break; case StellarX::AxisAlignPolicy::Proportional: { // 保持设计态中的相对位置比例。 // 注意:这里只调整位置,不改变尺寸。 const int designTravel = parentDesignSize - result.size; const int currentTravel = parentCurrentSize - result.size; if (designTravel <= 0 || currentTravel <= 0) result.pos = startMargin; else result.pos = RoundDiv(static_cast(startMargin) * currentTravel, designTravel); break; } case StellarX::AxisAlignPolicy::Start: default: result.pos = startMargin; break; } return result; } // 无锚点:退回设计态位置,尺寸保持设计值。 result.pos = childDesignPos; return result; } 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) { { nHeight = text.nHeight; nWidth = text.nWidth; lpszFace = text.lpszFace; color = text.color; nEscapement = text.nEscapement; nOrientation = text.nOrientation; nWeight = text.nWeight; bItalic = text.bItalic; bUnderline = text.bUnderline; bStrikeOut = text.bStrikeOut; return *this; } } bool StellarX::ControlText::operator!=(const ControlText& text) { if(nHeight != text.nHeight) return true; else if (nWidth != text.nWidth) return true; else if (lpszFace != text.lpszFace) return true; else if (color != text.color) return true; else if (nEscapement != text.nEscapement) return true; else if (nOrientation != text.nOrientation) return true; else if (nWeight != text.nWeight) return true; else if (bItalic != text.bItalic) return true; else if (bUnderline != text.bUnderline) return true; else if (bStrikeOut != text.bStrikeOut) return true; return false; } void Control::setIsVisible(bool show) { SX_LOGD("Control") << SX_T("重置可见状态: id=", "setIsVisible: id=") << id << " show=" << (show ? 1 : 0); if (this->show == show) return; this->show = show; this->dirty = true; if (!show) { // 隐藏:擦除自己在屏幕上的内容,并释放快照 discardBackground(); return; } // 显示:不在这里 requestRepaint(避免父容器快照未就绪时子控件抢跑 draw,污染快照) // 仅向上标脏,让事件收口阶段由容器统一重绘。 if (parent) parent->setDirty(true); } void Control::onWindowResize() { SX_LOGD("Layout") << SX_T("尺寸变化:id=", "onWindowResize: id=") << id << SX_T(" -> 丢背景快照 + 标脏", " -> discardSnap + dirty"); // 自己:丢快照 + 标脏 invalidateBackgroundSnapshot(); setDirty(true); } void Control::setLayoutMode(StellarX::LayoutMode layoutMode_) { this->layoutMode = layoutMode_; } void Control::setAnchor(StellarX::Anchor anchor_1, StellarX::Anchor anchor_2) { this->anchor_1 = anchor_1; this->anchor_2 = anchor_2; // 旧 API 只作为兼容输入层存在: // 这里把历史上的 anchor_1 / anchor_2 映射为新的水平/垂直轴布局规格, // 后续统一解算全部以 layoutSpec 为准。 this->layoutSpec.horizontal = BuildLegacyAxisSpec(anchor_1, anchor_2, StellarX::Anchor::Left, StellarX::Anchor::Right); this->layoutSpec.vertical = BuildLegacyAxisSpec(anchor_1, anchor_2, StellarX::Anchor::Top, StellarX::Anchor::Bottom); } StellarX::Anchor Control::getAnchor_1() const { return this->anchor_1; } StellarX::Anchor Control::getAnchor_2() const { return this->anchor_2; } StellarX::LayoutMode Control::getLayoutMode() const { return this->layoutMode; } const StellarX::LayoutSpec& Control::getLayoutSpec() const { return layoutSpec; } const StellarX::LayoutCapability& Control::getLayoutCapability() const { return layoutCapability; } void Control::setX(int x) { this->x = x; dirty = true; } void Control::setY(int y) { this->y = y; dirty = true; } void Control::setWidth(int width) { this->width = width; dirty = true; } void Control::setHeight(int height) { this->height = height; dirty = true; } bool Control::clearTransientMouseState() { // 基类默认没有 hover / tooltip / 临时按下态等鼠标瞬时状态。 // 具体控件若需要在“未收到本次 WM_MOUSEMOVE”时做清理,重写此接口即可。 return false; } void Control::commitCurrentGeometryAsDesignRect() { // 该接口是“显式提交新的设计基线”的唯一入口之一。 // 普通布局解算、父容器重排、窗口 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; localy = parent ? (y - parent->getY()) : y; localWidth = width; 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, int parentWorldX, int parentWorldY, int parentCurrentW, int parentCurrentH) const { StellarX::ResolvedLayoutRect rect{}; if (layoutMode != StellarX::LayoutMode::AnchorToEdges) { // 非锚点布局模式:直接使用设计态矩形,再映射到当前世界坐标。 rect.localX = localx; rect.localY = localy; rect.width = localWidth; rect.height = localHeight; rect.worldX = parentWorldX + rect.localX; rect.worldY = parentWorldY + rect.localY; return rect; } // 第 1 层:先在父局部坐标系内分别解算水平轴和垂直轴。 LogAxisDowngradeIfNeeded(id, true, layoutSpec.horizontal, layoutCapability.allowStretchX); LogAxisDowngradeIfNeeded(id, false, layoutSpec.vertical, layoutCapability.allowStretchY); const LayoutAxisResult horizontal = ResolveAxis(parentDesignW, parentCurrentW, localx, localWidth, layoutSpec.horizontal, layoutCapability.allowStretchX); const LayoutAxisResult vertical = ResolveAxis(parentDesignH, parentCurrentH, localy, localHeight, layoutSpec.vertical, layoutCapability.allowStretchY); // 第 2 层:把父局部矩形映射到世界坐标。 rect.localX = horizontal.pos; rect.localY = vertical.pos; rect.width = horizontal.size; rect.height = vertical.size; rect.worldX = parentWorldX + rect.localX; rect.worldY = parentWorldY + rect.localY; return rect; } void Control::applyResolvedLayoutRect(const StellarX::ResolvedLayoutRect& rect) { // 默认实现只应用运行态世界矩形。 // 若某个控件还需要在应用后继续刷新内部布局,可重写此函数。 applyRuntimeRectDirect(rect.worldX, rect.worldY, rect.width, rect.height); } void Control::applyRuntimeRectDirect(int worldX, int worldY, int width, int height) { // 最底层运行态赋值入口: // 只修改当前世界坐标和运行态尺寸,不触碰设计基线 local*。 this->x = worldX; this->y = worldY; this->width = width; this->height = height; dirty = true; } // 保存当前的绘图状态(字体、颜色、线型等) // 在控件绘制前调用,确保不会影响全局绘图状态 void Control::saveStyle() { gettextstyle(¤tFont); // 获取当前字体样式 currentColor = gettextcolor(); // 获取当前字体颜色 currentBorderColor = getlinecolor(); //保存当前边框颜色 getlinestyle(¤tLineStyle); //保存当前线型 currentBkColor = getfillcolor(); //保存当前填充色 } // 恢复之前保存的绘图状态 // 在控件绘制完成后调用,恢复全局绘图状态 void Control::restoreStyle() { settextstyle(¤tFont); // 恢复默认字体样式 settextcolor(currentColor); // 恢复默认字体颜色 setfillcolor(currentBkColor); setlinestyle(¤tLineStyle); setlinecolor(currentBorderColor); setfillstyle(BS_SOLID);//恢复填充 } void Control::requestRepaint(Control* parent) { if (shouldDeferManagedRepaint()) { // 托管路径:当前正在 Window 的事件分发阶段,不能立即绘制; // 这里只登记 source,真正的 root 选择由 Window 在 requestManagedRepaint 中完成。 if (auto* host = getHostWindow()) host->requestManagedRepaint(this); return; } // 说明: // - 常规路径:子控件调用 requestRepaint(this->parent),然后 parent 负责局部重绘(Canvas/TabControl override) // - 兜底路径:如果某个“容器控件”没 override requestRepaint,就会出现 parent==this 的递归风险 // 此时我们改为向更上层冒泡,直到根重绘。 if (parent == this) { SX_LOGW("Dirty") << SX_T("requestRepaint(默认容器兜底):id=", "requestRepaint(default-container-fallback): id=") << id << SX_T(",parent==this,向上层 parent 继续冒泡", " parent==this, bubble to upper parent"); if (this->parent) this->parent->requestRepaint(this->parent); else onRequestRepaintAsRoot(); return; } SX_LOG_TRACE("Dirty") << SX_T("请求重绘:id=","requestRepaint: id=") << id << " parent=" << (parent ? parent->getId() : "null"); if (parent) parent->requestRepaint(parent); // 交给容器处理(容器可局部重绘) else onRequestRepaintAsRoot(); // 根兜底 } void Control::onRequestRepaintAsRoot() { if (shouldDeferManagedRepaint()) { // 即使已经冒泡到 root,只要还在托管分发期,也不能直接绘制; // 仍然回到 Window 做统一提交。 if (auto* host = getHostWindow()) host->requestManagedRepaint(this); return; } SX_LOG_TRACE("Dirty") << SX_T("触发根重绘:id=", "onRequestRepaintAsRoot: id=") << id << SX_T("(从根节点开始重画)", " (root repaint)"); discardBackground(); setDirty(true); draw(); // 只有“无父”时才允许立即画,不会被谁覆盖 } bool Control::shouldDeferManagedRepaint() const { Window* host = getHostWindow(); return host && host->isManagedDispatchActive(); } // 获取宿主 Window: // - 顶层控件由 Window/addDialog 直接注入; // - 子控件没有直接注入时,沿 parent 链向上回溯即可。 Window* Control::getHostWindow() const { if (hostWindow) return hostWindow; return parent ? parent->getHostWindow() : nullptr; } // 托管重绘 root 选择规则: // - 对于直接挂在 Window 下的控件,root 就是它自己; // - 对于嵌套在 Canvas/TabControl/Dialog 内的子控件,沿 parent 向上找到与宿主 Window 同属一棵树的最上层控件。 Control* Control::getManagedRepaintRoot() { Control* root = this; Window* host = getHostWindow(); while (root->parent && root->parent->getHostWindow() == host) root = root->parent; 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 rc{}; rc.left = x; rc.top = y; rc.right = x + width; rc.bottom = y + height; return rc; } RECT Control::getManagedRepaintCoverageRect() const { // 基类默认认为“实际绘制 coverage == 控件本体矩形”。 // 复合控件或带浮层的控件会 override,把附加绘制区域一并并入。 return getBoundsRect(); } bool Control::canCommitManagedPartialRepaint() const { // 基类默认不承诺自己能安全做局部提交; // 只有 Canvas / TabControl / Dialog 这类“拥有完整背景语义”的 root 才会 override 为 true。 return false; } bool Control::hasManagedDirtySubtree() const { // 基类只感知自身是否为脏; // 容器类会 override 为“自身或其受管理子树是否有脏内容”。 return dirty; } void Control::commitManagedRepaint() { if (!show) return; // 基类兜底:如果没有更具体的容器实现,就按根级重绘处理。 if (dirty) onRequestRepaintAsRoot(); } void Control::saveBackground(int x, int y, int w, int h) { if (w <= 0 || h <= 0) return; saveBkX = x; saveBkY = y; saveWidth = w; saveHeight = h; if (saveBkImage) { //尺寸变了才重建,避免反复 new/delete if (saveBkImage->getwidth() != w || saveBkImage->getheight() != h) { SX_LOGD("Snap") <(w, h); SetWorkingImage(nullptr); // ★抓屏幕 getimage(saveBkImage.get(), x, y, w, h); hasSnap = true; } void Control::restBackground() { if (!hasSnap || !saveBkImage) return; // 直接回贴屏幕(与抓取一致) SetWorkingImage(nullptr); putimage(saveBkX, saveBkY, saveBkImage.get()); } void Control::discardBackground() { if (saveBkImage) { restBackground(); SX_LOGD("Snap") << SX_T("丢弃背景快照:id=","discardBackground: id=") << id << " hasSnap=" << (hasSnap ? 1 : 0); saveBkImage.reset(); } hasSnap = false; saveWidth = saveHeight = 0; } void Control::invalidateBackgroundSnapshot() { if (saveBkImage) { SX_LOGD("Snap") << SX_T("作废背景快照:id=", "invalidateBackgroundSnapshot: id=") << id << " hasSnap=" << (hasSnap ? 1 : 0); saveBkImage.reset(); } hasSnap = false; saveBkX = saveBkY = 0; saveWidth = saveHeight = 0; }