371 lines
9.8 KiB
C++
371 lines
9.8 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";
|
|
}
|
|
}
|
|
|
|
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<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;
|
|
|
|
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)
|
|
{
|
|
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();
|
|
}
|
|
|