Direct 2D实现界面库 (1)
http://www.cnblogs.com/mmc1206x/p/3924580.html
上篇说完了每个 LNode 的绘制过程. 也就是 onDraw 的实现.
这篇谈谈消息响应函数 onInput.
onInput 实现
Windows 窗口程序都有一个 WndProc 函数, 该函数作为消息响应的回调.
一旦有消息到窗口, 系统就会调用该窗口的 WndProc.
我们实现的控件都是自绘的, 都不具备这个 WndProc 回调.
我们也不能指望系统可以知道对哪个控件发送消息.
那就自己实现这个消息响应吧.
这个流程并不复杂.
我们在触发父节点消息的时候, 同时对子节点递归触发.
从而实现冒泡消息响应, 从外到内.
因此我们的 onInput 的实现可能如下:
1 void LNode::onInput(UINT uMsg, WPARAM wParam, LPARAM lParam) 2 { 3 for (auto &child: _childs) 4 { 5 child->noInput(uMsg, wParam, lParam); 6 } 7 }
但事实并非如此简单.
想想如何处理鼠标点击响应?
通常只有被点到的那个控件做出响应.
LNode 所有控件都是矩形, 因此计算很方便.
只要点击位置在这个控件矩形内, 则该控件被点击了.
这个过程大概是这样:
查找被点击的控件.
如果该控件有子控件, 则返回上一步.
如果该控件无子控件, 则返回该控件.
这样我们可以得到被点击的那个控件了.
除此之外, 我们需要考虑焦点问题.
例如我在某 LNode 按下鼠标, 然后移出该鼠标范围, 此时的 WM_MOUSEMOVE 响应又被点击的那个 LNode 触发.
当然, 如果不在该 LNode 范围内, 就什么也不做.
如何让 LNode 获取焦点.
也许你会认为, 直接让 Hit 那个 LNode 获取焦点即可.
情况并非如此, 例如某子节点获取了焦点, 该父节点必然也拥有焦点.
焦点在什么时候会变化?
只有在鼠标按下的时候, 焦点才会发生变化. tab 也可以, 但是我们不考虑这个.
下面给出 onInput 完整定义. ( 按键处理暂时没写.)
1 void LNode::onInput(UINT uMsg, WPARAM wParam, LPARAM lParam) 2 { 3 // 特殊处理鼠标消息. 4 // 特殊处理键盘消息. 5 switch (uMsg) 6 { 7 case WM_LBUTTONUP: 8 case WM_MBUTTONUP: 9 case WM_RBUTTONUP: 10 case WM_MOUSEMOVE: 11 { 12 getFocus()->doInput(uMsg, wParam, lParam); 13 } 14 break; 15 case WM_LBUTTONDOWN: 16 case WM_MBUTTONDOWN: 17 case WM_RBUTTONDOWN: 18 { 19 D2D1_POINT_2F hitPoint = { 20 (float)GET_X_LPARAM(lParam), 21 (float)GET_Y_LPARAM(lParam) 22 }; 23 auto pHitNode = getHitNode(hitPoint, _scale); 24 pHitNode->setFocus(); 25 pHitNode->doInput(uMsg, wParam, lParam); 26 } 27 break; 28 default: 29 // 其他消息全部派发至子节点. 30 for (auto &child: _childs) 31 { 32 child->noInput(uMsg, wParam, lParam); 33 } 34 break; 35 } 36 }
这个实现还算简单.
重点就在如何实现 getHitNode.
LNode *LNode::getHitNode(D2D1_POINT_2F hitPoint, D2D1_SIZE_F scale) { const auto &rect = getHitPointAndRect(scale); hitPoint.x -= rect.left; hitPoint.y -= rect.top; scale.width *= _scale.width; scale.height *= _scale.height; auto pHitNode = this; for (auto &child: _childs) { const auto &hitRect = child->getHitPointAndRect(scale); if ( Utils::containPoint(hitRect, hitPoint) ) { pHitNode = child.get(); // 命中子节点. } } if (pHitNode != this) { pHitNode = pHitNode->getHitNode(hitPoint, scale); } return pHitNode; }
getHitNode 通过点击坐标, 返回被点击的LNode.
前面说了, LNode 都是矩形, 只要知道这个点是否在矩形内就可以得到结果了.
事实上, 远不止你想的这么容易.
不要忘了, 每一个 LNode 都可以作为容器, 每一个子节点的坐标跟父节点都是相对的.
这样做的好处是, 父节点移动可以顺便带着一群子节点.
这还不是难点.
LNode 可以缩放. 这个问题也不是什么难事.
如果父节点缩放了, 子节点会继承父节点的缩放,
因此这里就不能只考虑自身的缩放值, 还要加上父节点缩放值.
这里该如何设计? 是每一次计算都通过 _pParent 递归上去得出最终缩放值?
我用了一个流水线的办法, 就是在最初的根节点传入自己的缩放值, 该值一直被流传下去.
不要忘了, LNode 支持锚点..
缩放点不一样, 那么缩放后的矩形位置也不一样.
例如说,
一个 100 * 100的矩形在 0, 0的坐标上.
对这个矩形x, y缩放0.5, 那么该矩形依旧在坐标0, 0处.
如果这个矩形的锚点在0.5, 0.5, 那情况就不太一样了, 缩放之后的坐标显然也发生了变化,
因为是从中心开始缩放.
我觉得我已经快说不下去了.
看看 getHitPointAndRect 的实现.
inline D2D1_RECT_F LNode::getHitPointAndRect(const D2D1_SIZE_F &scale) { const auto &curScale = D2D1::SizeF( scale.width * _scale.width, scale.height * _scale.height ); // 计算 X 轴, 左右实际尺寸. auto xLeft = _contentSize.width * ( 0 + _anchor.x ) * curScale.width; auto xRight = _contentSize.width * ( 1 - _anchor.x ) * curScale.width; // 计算 Y 轴, 左右实际尺寸. auto yLeft = _contentSize.height * ( 0 + _anchor.y ) * curScale.height; auto yRight = _contentSize.height * ( 1 - _anchor.y ) * curScale.height; return D2D1::RectF( _position.x * scale.width - xLeft, _position.y * scale.height - yLeft, _position.x * scale.width + xRight, _position.y * scale.height + yRight ); }
你只要记住, 该函数返回 LNode 的 Hit 区域就行了.
我觉得可以从矩阵中算出这些值. 但是... 我不会~~~
这里再把流程梳理一遍.
举个栗子:
图中,
父节点100*50, 锚点大概就在那个位置, 你应该看到了.
子节点50*25, 锚点在哪你也看到了.
接下来要缩放了.
缩放之后大概就是这样, 向锚点靠拢.
这样的情况下, 就不能纯粹的以 _contentSize 做碰撞检测.
把 _anchor, _position, _scale, _contentSize 统统都要考虑进来, 并且还需要考虑父节点的部分数据..
getHitNode是一个递归调用..
该函数每次递归都需要把传入的 hitPoint 减去自身的坐标, 再去对子节点进行计算.
缩放值基本同理.
如果有兴趣, 可以看看源码.
只写了3天, 时间比较短, Bug难免会有, 不合理设计难免会有,
如果在阅读源码时, 感觉心脏速率过高, 请马上转移注意力.