大学时尝试过很多次写一个UI库,
初次使用 GDI 绘图, 当时水平很低, GDI功能太弱, 以失败而告终.
之后使用 GDI+ 绘图, 当时水平依旧很低, GDI功能很强, 但效率实在太慢, 以失败而告终.
现在使用 Direct 2D绘图, 水平还是很低, 但凑合着没多少问题.
Demo展示
Demo写的有点粗糙, 咱们把重点放在 L Window 库上.
设计思路
思路跟cocos2dx有点相似.
每一个控件都继承自虚基类 LNode.
LNode 只需要提供2个纯虚函数:
doDraw, doInput.
前者用于重写绘制.
后者用于重写消息响应.
定义如下.
class LNode { virtual void doDraw(ID2D1RenderTarget *pRT, float opacity) = 0; virtual void doInput(UINT uMsg, WPARAM wParam, LPARAM lParam) = 0; };
doDraw需要传入2个参数.
pRT 是Direct 2D的渲染器, 它托管了整个窗口, 渲染的工作都归它.
opacity 是顾名思义, 是指透明度, 因为 L Window 所有控件都支持透明度.
每一个 LNode 都保存了一个 _opacity, 那为什么这里还需要传递一个opacity?
因为所有控件都可能作为其他控件的子节点.
如果父节点的opacity是0.5f, 子节点的opacity是1.0f.
此时子节点被绘制出来的透明度应该传承父节点的0.5f.
LNode.h 清单
#pragma once #include "FileList.h" namespace mmc { class LNode { public: typedef std::shared_ptr<LNode> ChildNodeT; LNode(); virtual ~LNode(); void onDraw(ID2D1RenderTarget *pRT, float opacity); void onInput(UINT uMsg, WPARAM wParam, LPARAM lParam); ; ; void setParent(LNode *pNode); void setFocus(); void setPosition(float x, float y) ; void setAnchor(float x, float y); void setContentSize(float width, float height); void setScale(float width, float height); void setZorder(int z); void setOpacity(float op); void appendChild(LNode *pNode); LNode *getParent(); LNode *getFocus(); D2D1_POINT_2F getPosition(); D2D1_POINT_2F getAnchor(); D2D1_SIZE_F getContentSize(); D2D1_SIZE_F getScale(); int getZorder(); float getOpacity(); std::vector< ChildNodeT > &getChilds(); private: LNode *getHitNode(D2D1_POINT_2F hitPoint, D2D1_SIZE_F scale); D2D1_POINT_2F getScalePoint(); D2D1_POINT_2F getTranslationPoint(); D2D1_RECT_F getHitPointAndRect(const D2D1_SIZE_F &scale); void callDraw(ID2D1RenderTarget *pRT, float opacity, const std::function<void (ID2D1RenderTarget *, float)> &callback); const D2D1_MATRIX_3X2_F &getMatrix(); // data. int _zOrder; float _opacity; LNode * _pFocus; LNode * _pParent; D2D1_POINT_2F _position; D2D1_POINT_2F _anchor; D2D1_SIZE_F _scale; D2D1_SIZE_F _contentSize; D2D1_MATRIX_3X2_F _matrix; std::vector< ChildNodeT > _childs; }; };
LNode 定义也比较简单.
除去一切set, get.
剩下的就是 onDraw, onInput, 一些私有成员函数.
LNode::onDraw 实现
咱们先来想想 LNode 该如何实现整个绘制流程.
既然每一个 LNode 都可能作为父节点, 那么每一个 LNode 绘制都需要把子节点绘制出来.
因此可能会出现如下代码.
// 绘制自身. doDraw(); for_each (_childs) { child->doDraw(); }
结果已经很接近了, 但是并非这么简单.
因为 LNode 还支持Z序, 因此还要考虑子节点绘制顺序问题.
Z序也可能是负数, 所以这也是在考虑的范围内.
同时还要考虑相对坐标的问题.
因为每一个子节点的坐标都是相对父节点的,
假设父节点的X坐标是100, 子节点的X坐标是100, 显示出来的子节点X坐标是200.
也许你认为在绘制的时候递归父节点得出最终绘制坐标.
但是, D2D为我们提供更科学的方法 ------ 矩阵.
不管2D还是3D, 所有的坐标计算都是通过矩阵计算.
一个矩阵可以记录平移, 缩放, 旋转 信息.
我们把渲染目标看作一块画布.
通常我们画画时, 都是纸不动, 笔动.
这里需要换一下, 纸动, 笔不动.
如果我们的父节点X坐标是100, 那么平移100, 纸被平移了.
再绘制子节点的时候, 再把纸平移100, 位置就已经是200了.
缩放原理也是一样, 这一些信息都由一个 _matrix 矩阵保存.
那我们绘制大概的顺序就是:
子节点 Z 从小到大排序.
遍历子节点逐个调用 onDraw, 遇到Z为0中断.
调用自己的 doDraw.
遍历剩下子节点逐个调用 onDraw.
下面是完整的定义.
void LNode::onDraw(ID2D1RenderTarget *pRT, float opacity) { // 每次根据Z序排序. std::sort(std::begin(_childs), std::end(_childs), [&](const ChildNodeT &pNode1, const ChildNodeT &pNode2) { return pNode1->getZorder() < pNode2->getZorder(); }); // 递归调用绘制各个子节点. auto curIter = std::begin(_childs); auto endIter = std::end(_childs); // 传递透明度. opacity -= ( - _opacity); opacity = max(opacity, ); opacity = min(opacity, ); // 绘制Z序小于0的子节点. ; ++curIter) { callDraw( pRT, opacity, BIND_2(&LNode::onDraw, *curIter) ); } // 绘制自身. callDraw( pRT, opacity, BIND_2(&LNode::doDraw, this) ); for (; curIter != endIter; ++curIter) { callDraw( pRT, opacity, BIND_2(&LNode::onDraw, *curIter) ); } }
里面有一个 callDraw,
该函数负责调用 onDraw, doDraw,
并且控制矩阵变化.
inline void LNode::callDraw(ID2D1RenderTarget *pRT, float opacity, const std::function<void (ID2D1RenderTarget *, float)> &callback) { D2D1_MATRIX_3X2_F saveMatrix; pRT->GetTransform(&saveMatrix); pRT->SetTransform(getMatrix() * saveMatrix); callback(pRT, opacity); pRT->SetTransform(saveMatrix); }
每一次绘制结束后, 都需要还原绘制前的矩阵.
这里出现了一个 getMatrix() 函数..
该函数就纯粹的返回当前对象的矩阵, 结果与当前矩阵叉乘, 得到需要的矩阵.
叉乘是什么?
这玩意儿很让人头大,
这让没有上过高中的我情何以堪,
不过还好, 这玩意儿大学才有, 而我恰巧上过大学.
但是, 我初中之后数学就没上过50分~~~
// 该方法被需要绘制该Node的操作调用. const D2D1_MATRIX_3X2_F &LNode::getMatrix() { // 缩放. D2D1_MATRIX_3X2_F scale = D2D1::Matrix3x2F::Scale(_scale, getScalePoint()); // 平移. const auto &transPoint = getTranslationPoint(); D2D1_MATRIX_3X2_F translation = D2D1::Matrix3x2F::Translation(transPoint.x, transPoint.y); _matrix = scale * translation; return _matrix;}
每一个 LNode 都支持平移, 缩放, 因此每一次返回的矩阵都需要包含这2个信息.
缩放和平移的顺序必须固定. 如果我没有记错的话, 乘法因子的顺序是可以交换的.
好吧, 这里不是乘法, 是叉乘.
这里需要注意的有3点,
第1点, 缩放需要设置一个缩放点, 这个缩放点我们通过 getScalePoint 获得.
第2点, 平移需要考虑缩放值, 锚点, 坐标. LNode 是有锚点的, 难度又提升了一小段.
我保证这真的只是一小段. 通过 getTranslationPoint 获得最终绘制的坐标.
第3点, 叉乘因子真的不能交换顺序.
inline D2D1_POINT_2F LNode::getScalePoint() { return D2D1::Point2F( _anchor.x * _contentSize.width, _anchor.y * _contentSize.height ); }
_contentSize 保存了 LNode的实际尺寸._anchor 则是锚点, 取值 0-1.因此我们想得到这个缩放点, 只需要把 尺寸 * 锚点.
inline D2D1_POINT_2F LNode::getTranslationPoint() { return D2D1::Point2F( _position.x - _anchor.x * _contentSize.width, _position.y - _anchor.y * _contentSize.height ); }
因为D2D绘制坐标系原点在左上角.
因此把 LNode 坐标减去锚点偏移的坐标, 就可以得到实际绘制的坐标.
上班时间写博客影响不好.
下次换个上班时间再写..