#include "Canvas.h" #include "SxLog.h" #include "Window.h" static bool SxIsNoisyMsg(UINT m) { return m == WM_MOUSEMOVE; } static const char* SxCanvasMsgName(UINT m) { switch (m) { case WM_MOUSEMOVE: return "WM_MOUSEMOVE"; case WM_LBUTTONDOWN: return "WM_LBUTTONDOWN"; case WM_LBUTTONUP: return "WM_LBUTTONUP"; case WM_KEYDOWN: return "WM_KEYDOWN"; case WM_KEYUP: return "WM_KEYUP"; default: return "WM_UNKNOWN"; } } Canvas::Canvas() :Control(0, 0, 100, 100) { this->id = "Canvas"; } Canvas::Canvas(int x, int y, int width, int height) :Control(x, y, width, height) { this->id = "Canvas"; } void Canvas::relayoutManagedChildren() { // Canvas 负责子控件从“父局部设计矩形”到“当前世界矩形”的转换。 // 当 Canvas 自己的位置或尺寸变化后,所有受它管理的子控件都要重新走一次统一解算。 for (auto& child : controls) { const StellarX::ResolvedLayoutRect rect = child->resolveLayoutRect(localWidth, localHeight, x, y, width, height); child->applyResolvedLayoutRect(rect); } } void Canvas::setX(int x) { // 公开 setter 在 Canvas 上不能再视为“单纯改自己的 x”: // 一旦容器移动,子控件的世界坐标也必须整体重算。 applyRuntimeRectDirect(x, y, width, height); relayoutManagedChildren(); onWindowResize(); } void Canvas::setY(int y) { applyRuntimeRectDirect(this->x, y, width, height); relayoutManagedChildren(); onWindowResize(); } void Canvas::setWidth(int width) { applyRuntimeRectDirect(x, y, width, height); relayoutManagedChildren(); onWindowResize(); } void Canvas::setHeight(int height) { applyRuntimeRectDirect(x, y, width, height); relayoutManagedChildren(); onWindowResize(); } void Canvas::applyResolvedLayoutRect(const StellarX::ResolvedLayoutRect& rect) { // 统一解算器已经给出当前运行态世界矩形; // Canvas 在应用自身矩形后,还必须继续刷新全部子控件。 applyRuntimeRectDirect(rect.worldX, rect.worldY, rect.width, rect.height); relayoutManagedChildren(); } void Canvas::clearAllControls() { controls.clear(); } void Canvas::draw() { if (!dirty || !show) { for (auto& control : controls) if (auto c = dynamic_cast(control.get())) c->draw(); return; } saveStyle(); setlinecolor(canvasBorderClor);//设置线色 if (StellarX::FillMode::Null != canvasFillMode) setfillcolor(canvasBkClor);//设置填充色 setfillstyle((int)canvasFillMode);//设置填充模式 setlinestyle((int)canvasLineStyle, canvaslinewidth); // 在绘制画布之前,先恢复并更新背景快照: // 1. 如果已有快照,则先回贴旧快照以清除之前的内容。 // 2. 当坐标或尺寸变化,或缓存图像无效时,丢弃旧快照并重新抓取新的背景。 int margin = canvaslinewidth > 1 ? canvaslinewidth : 1; if (hasSnap) { // 恢复旧快照,清除上一次绘制 restBackground(); // 如果位置或尺寸变了,或没有有效缓存,则重新抓取 if (!saveBkImage || saveBkX != this->x - margin || saveBkY != this->y - margin || saveWidth != this->width + margin * 2 || saveHeight != this->height + margin * 2) { invalidateBackgroundSnapshot(); saveBackground(this->x - margin, this->y - margin, this->width + margin * 2, this->height + margin * 2); } } else { // 首次绘制或没有快照时直接抓取背景 saveBackground(this->x - margin, this->y - margin, this->width + margin * 2, this->height + margin * 2); } // 再次恢复最新快照,确保绘制区域干净 restBackground(); //根据画布形状绘制 switch (shape) { case StellarX::ControlShape::RECTANGLE: fillrectangle(x, y, x + width, y + height);//有边框填充矩形 break; case StellarX::ControlShape::B_RECTANGLE: solidrectangle(x, y, x + width, y + height);//无边框填充矩形 break; case StellarX::ControlShape::ROUND_RECTANGLE: fillroundrect(x, y, x + width, y + height, rouRectangleSize.ROUND_RECTANGLEwidth, rouRectangleSize.ROUND_RECTANGLEheight);//有边框填充圆角矩形 break; case StellarX::ControlShape::B_ROUND_RECTANGLE: solidroundrect(x, y, x + width, y + height, rouRectangleSize.ROUND_RECTANGLEwidth, rouRectangleSize.ROUND_RECTANGLEheight);//无边框填充圆角矩形 break; } // 绘制所有子控件 for (auto& control : controls) { control->setDirty(true); control->draw(); } restoreStyle(); dirty = false; //标记画布不需要重绘 } bool Canvas::handleEvent(const ExMessage& msg) { if (!show) return false; resetEventVisualChanged(); bool consumed = false; bool anyDirty = false; bool anyVisualChanged = false; Control* firstConsumer = nullptr; 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; } } if (firstConsumer && !SxIsNoisyMsg(msg.message)) { SX_LOGD("Event") << SX_T("Canvas 消耗消息: ","Canvas consumed: ") << SxCanvasMsgName(msg.message) << SX_T(" 子控件 id=", " childId=") << firstConsumer->getId(); } if (anyDirty) { // 只要任一子控件因本次事件进入 dirty,就把这笔重绘继续向上汇报。 // 在托管模式下,这不会立即绘制,而是登记为 Canvas 对应的重绘 root。 if (!SxIsNoisyMsg(msg.message)) SX_LOGD("Dirty") << SX_T("Canvas检测有控件为脏状态 -> 请求重绘, ","Canvas anyDirty -> requestRepaint, ")<<"id = " << id; requestRepaint(parent); } markEventVisualChanged(anyVisualChanged); return consumed; } void Canvas::addControl(std::unique_ptr control) { control->setParent(this); // 新子控件加入容器时,立刻按“当前容器运行态矩形”解算一次, // 避免后续第一次 draw / resize 前 world 坐标仍停留在设计态。 const StellarX::ResolvedLayoutRect rect = control->resolveLayoutRect(localWidth, localHeight, this->x, this->y, this->width, this->height); control->applyResolvedLayoutRect(rect); SX_LOGI("Canvas") << SX_T("添加子控件:父=Canvas 子id=", "addControl: parent=Canvas childId=") << control->getId() << SX_T(" 相对坐标=(", " local=(") << control->getLocalX() << "," << control->getLocalY() << SX_T(") 绝对坐标=(", ") abs=(") << control->getX() << "," << control->getY() << ")"; controls.push_back(std::move(control)); dirty = true; } void Canvas::setShape(StellarX::ControlShape shape) { switch (shape) { case StellarX::ControlShape::RECTANGLE: case StellarX::ControlShape::B_RECTANGLE: case StellarX::ControlShape::ROUND_RECTANGLE: case StellarX::ControlShape::B_ROUND_RECTANGLE: this->shape = shape; dirty = true; break; case StellarX::ControlShape::CIRCLE: case StellarX::ControlShape::B_CIRCLE: case StellarX::ControlShape::ELLIPSE: case StellarX::ControlShape::B_ELLIPSE: this->shape = StellarX::ControlShape::RECTANGLE; dirty = true; break; } } void Canvas::setCanvasfillMode(StellarX::FillMode mode) { this->canvasFillMode = mode; dirty = true; } void Canvas::setBorderColor(COLORREF color) { this->canvasBorderClor = color; dirty = true; } void Canvas::setCanvasBkColor(COLORREF color) { this->canvasBkClor = color; dirty = true; } void Canvas::setCanvasLineStyle(StellarX::LineStyle style) { this->canvasLineStyle = style; dirty = true; } void Canvas::setLinewidth(int width) { this->canvaslinewidth = width; dirty = true; } void Canvas::setIsVisible(bool visible) { this->show = visible; dirty = true; for (auto& control : controls) { control->setIsVisible(visible); } if (!visible) discardBackground(); } void Canvas::setDirty(bool dirty) { this->dirty = dirty; for (auto& control : controls) control->setDirty(dirty); } void Canvas::onWindowResize() { // resize 语义已收口: // Canvas 不再在这里重新解算布局,只负责丢快照、标脏,并向子控件传播“环境已变化”。 Control::onWindowResize(); for (auto& child : controls) child->onWindowResize(); } void Canvas::requestRepaint(Control* parent) { if (shouldDeferManagedRepaint()) { // 托管路径:由 Window 统一决定这次是否只重画本 Canvas,还是升级为补画 Dialog / 整体场景。 if (auto* host = getHostWindow()) host->requestManagedRepaint(this); return; } if (this == parent) { if (!show) return; // 关键护栏: // - Canvas 自己是脏的 / 没有快照 / 缓存图为空 // => 禁止局部重绘,直接升级为一次完整 draw(先把 dirty 置真,避免 draw() 早退) if (dirty || !hasSnap || !saveBkImage) { SX_LOG_TRACE("Dirty") << SX_T("Canvas 局部重绘降级为全量重绘: id=", "Canvas partial->full draw: id=") << id << " dirty=" << (dirty ? 1 : 0) << " hasSnap=" << (hasSnap ? 1 : 0); this->dirty = true; this->draw(); return; } SX_LOG_TRACE("Dirty") << SX_T("Canvas 请求局部重绘:id=", "Canvas::requestRepaint(partial): id=") << id; for (auto& control : controls) if (control->isDirty() && control->IsVisible()) control->draw(); return; } SX_LOG_TRACE("Dirty") << SX_T("Canvas 请求根级重绘:id=", "Canvas::requestRepaint(root): id=") << id; onRequestRepaintAsRoot(); } bool Canvas::canCommitManagedPartialRepaint() const { // Canvas 只有在“自己本体不脏 + 仍持有有效背景快照”时, // 才能安全地做局部提交(即只更新内部脏子控件)。 return show && !dirty && hasValidBackgroundSnapshot(); } void Canvas::commitManagedRepaint() { if (!show) return; if (canCommitManagedPartialRepaint()) { // 快照完好:沿用 Canvas 自己已有的局部重绘逻辑。 requestRepaint(this); return; } // 自身已经脏了,或快照失效:必须升级为整 Canvas 重画。 this->dirty = true; onRequestRepaintAsRoot(); }