529 lines
14 KiB
C++
529 lines
14 KiB
C++
#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";
|
|
}
|
|
}
|
|
|
|
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()
|
|
:Control(0, 0, 100, 100)
|
|
{
|
|
this->id = "Canvas";
|
|
// Canvas 是通用容器,当前阶段显式允许双轴 Stretch。
|
|
this->layoutCapability.allowStretchX = true;
|
|
this->layoutCapability.allowStretchY = true;
|
|
}
|
|
|
|
Canvas::Canvas(int x, int y, int width, int height)
|
|
:Control(x, y, width, height)
|
|
{
|
|
this->id = "Canvas";
|
|
this->layoutCapability.allowStretchX = true;
|
|
this->layoutCapability.allowStretchY = true;
|
|
}
|
|
|
|
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<Table*>(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;
|
|
|
|
if (msg.message == WM_MOUSEMOVE)
|
|
{
|
|
// WM_MOUSEMOVE 需要特殊处理:
|
|
// - 第一个命中的兄弟分支收到真实消息;
|
|
// - 后续兄弟不再重新命中,只清理旧 hover / tooltip 等临时状态。
|
|
for (auto it = controls.rbegin(); it != controls.rend(); ++it)
|
|
{
|
|
Control* c = it->get();
|
|
if (!consumed)
|
|
{
|
|
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;
|
|
}
|
|
}
|
|
}
|
|
|
|
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;
|
|
}
|
|
|
|
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)
|
|
{
|
|
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;
|
|
|
|
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)
|
|
{
|
|
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;
|
|
}
|
|
|
|
SX_LOG_TRACE("Dirty") << SX_T("Canvas 请求根级重绘:id=", "Canvas::requestRepaint(root): id=") << id;
|
|
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
|
|
{
|
|
// Canvas 只有在“自己本体不脏 + 仍持有有效背景快照”时,
|
|
// 才能安全地做局部提交(即只更新内部脏子控件)。
|
|
return show && !dirty && hasValidBackgroundSnapshot();
|
|
}
|
|
|
|
void Canvas::commitManagedRepaint()
|
|
{
|
|
if (!show)
|
|
return;
|
|
|
|
if (canCommitManagedPartialRepaint())
|
|
{
|
|
// 快照完好:沿用 Canvas 自己已有的局部重绘逻辑。
|
|
requestRepaint(this);
|
|
return;
|
|
}
|
|
|
|
// 自身已经脏了,或快照失效:必须升级为整 Canvas 重画。
|
|
this->dirty = true;
|
|
onRequestRepaintAsRoot();
|
|
}
|
|
|