基于现代C++的声明式显示模块API

Posted 软件工程师之路

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了基于现代C++的声明式显示模块API相关的知识,希望对你有一定的参考价值。

对于显示模块这种,开发者通常要处理以下内容:

  1. 图形(点、线、文字、区域等等)
  2. 画笔
  3. 画刷
  4. 可选的变换(旋转、平移、缩放)

除了图形,其它内容均是可选的.为了支持开发者设置这些内容,通常会提供大量的API,例如Qt:

void SimpleExampleWidget::paintEvent(QPaintEvent *)
{
QPainter painter(this);
painter.setPen(Qt::blue);
painter.setFont(QFont("Arial", 30));
painter.drawText(rect(), Qt::AlignCenter, "Qt");
}

软件设计讲“高内聚、低耦合”,上述API设计就会和QPainter产生耦合,必然会造成不好的影响,譬如目前QtQt Quick Scene Graph,其API就和QPainter不一致,在官方示例中,它的使用是这样的:

QSGNode *BezierCurve::updatePaintNode(QSGNode *oldNode, UpdatePaintNodeData *)
{
QSGGeometryNode *node = nullptr;
QSGGeometry *geometry = nullptr;

if (!oldNode) {
node = new QSGGeometryNode;

geometry = new QSGGeometry(QSGGeometry::defaultAttributes_Point2D(), m_segmentCount);
geometry->setLineWidth(2);
geometry->setDrawingMode(QSGGeometry::DrawLineStrip);
node->setGeometry(geometry);
node->setFlag(QSGNode::OwnsGeometry);

QSGFlatColorMaterial *material = new QSGFlatColorMaterial;
material->setColor(QColor(255, 0, 0));
node->setMaterial(material);
node->setFlag(QSGNode::OwnsMaterial);

} else {
node = static_cast<QSGGeometryNode *>(oldNode);
geometry = node->geometry();
geometry->allocate(m_segmentCount);
}

QSizeF itemSize = size();
QSGGeometry::Point2D *vertices = geometry->vertexDataAsPoint2D();
for (int i = 0; i < m_segmentCount; ++i) {
qreal t = i / qreal(m_segmentCount - 1);
qreal invt = 1 - t;

QPointF pos = invt * invt * invt * m_p1
+ 3 * invt * invt * t * m_p2
+ 3 * invt * t * t * m_p3
+ t * t * t * m_p4;

float x = pos.x() * itemSize.width();
float y = pos.y() * itemSize.height();

vertices[i].set(x, y);
}
node->markDirty(QSGNode::DirtyGeometry);

return node;
}

那么,有没有可能设计出既和底层平台隔离,有具有良好开发者体验的显示模块API呢?

这里基于现代C++的丰富特性支持,提供一种声明式的显示模块API,来达成上述目标.

显示场景与声明式API

针对显示场景来讲,显示的图形都是由一些基本要素构成的,例如矩形路径,可以由四条线构成.从这个角度来讲,API设计应当是组合式的,提供一些基本图形,然后开发者面临复杂图形时将这些基本图形拼合起来.以矩形为例:

//基本的API定义
struct Line{
Point start;
Point end;
};

void drawLine(Line line);

//用户端的绘制矩形API
void CustomDrawRect(double x,double y,double w,double h){
std::array<Line,4> lines;
//创建出四条线
for(auto& line:lines){
drawLine(line);
}
}

当然,上述实现依然依赖于drawLine这个底层API.如果希望解耦,则可以考虑将绘制动作存储起来,统一传递给某个抽象API来实现,例如:

enum class GraphicsType{
Line,
Text,
Group
};
class IGraphics{
public:
virtual ~IGraphics()=default;
virtual GraphicsType type()=0;
};

class LineGraphcis:public IGraphics{
public:
Line line;
GraphicsType type() override{
return GraphicsType::Line
}
};

class GroupGraphics:public IGraphics{
public:
std::vector<std::unique_ptr<IGraphics>> units;
};

class IPainter{
public:
virtual void draw(IGraphics * graphics) = 0;
};

这时对于底层平台,譬如Qt,其实现为:

class PainterImpl{
QPainter* painter;
public:
void draw(IGraphics* graphics) override{
switch(graphics->type()){
case GraphicsType::Line:{
auto impl = dynamic_cast<LineGraphics*>(graphics);
//仅作为示意,可能还需类型转换
painter->drawLine(impl->line);
};
case GraphicsType::Group:{
auto impl = dynamic_cast<GroupGraphics*>(graphics);
for(auto& g:impl.units){
this->draw(g.get());
}
}
}
}
};

通过上述方式能够达成解耦和应对需求场景的目标.但是创建出对应的Graphics却依然需要书写大量代码.既然可以把显示场景转换成IGraphics派生对象,那么这个问题就变成了构造复杂对象,这时声明式API就派上了用场.

首先来看一下原始的写法:

GroupGraphics build(double x,double y,double w,double h){
auto result = GroupGraphics();
result.units.empack_back(std::make_unique<LineGraphics>(Line{
Point{x,y},Point{x+w,y}}));
result.units.empack_back(std::make_unique<LineGraphics>(Line{
Point{x+w,y},Point{x+w,y+h}}));
result.units.empack_back(std::make_unique<LineGraphics>(Line{
Point{x+w,y+h},Point{x,y+h}}));
result.units.empack_back(std::make_unique<LineGraphics>(Line{
Point{x,y+h},Point{x,y}}));
return std::move(result);
}

对比以下声明式写法:

//这里假设Graphics即为最终那个GroupGraphics
Graphics build(double x,double y,double w,double h){
return Graphics(
Line(
Point{x,y},Point{x+w,y}
),
Line(
Point{x+w,y},Point{x+w,y+h}
),
Line(
Point{x+w,y+h},Point{x,y+h}
),
Line(
Point{x,y+h},Point{x,y}
),
)
};

如果某一条线的颜色要单独设定,两种写法分别为:

void normalAPI()
{
auto line = std::make_unique<LineGraphics>(Line{
Point{x,y},Point{x+w,y}});
line->setColor(QColor(Qt::blue));
}

Graphics declarativeAPI(){
return Graphics(
Line(
Point{x,y},Point{x+w,y},
Pen(QColor(Qt::blue))
),
//其它线
);
}

下面来看以下如何实现.

设计目标/效果

这里为了简单起见,把一些基本类型(intdoublestd::stringboolstd::vector<double>)作为图形,并提供print实现,来组合出各种场景,例如:

int main(int argc, char** argv) {
//图形由int、double、std::string和另外一个符合Graphics构成,
//Pen修改的是std::string这个图形的画笔信息
auto result = Graphics(
1024,
3.1415926,
std::string("liff.engineer@gmail.com"),
Pen{ 10 },
Graphics(
true,
1.414,
std::vector<double>{1, 2, 3, 4, 5},
Graphics(
false,
std::string("engineer"),
4096
)
)
);
//打印出来,层级为0
result.print(0);
return 0;
}

输出为:

>>>>1024
>>>>3.14159
>>>>liff.engineer@gmail.com
>>>>>>>>1
>>>>>>>>1.414
>>>>>>>>{1,2,3,4,5,}
>>>>>>>>>>>>0
>>>>>>>>>>>>engineer
>>>>>>>>>>>>4096

可以看到,Graphics支持由一些基本元素组合而成,也支持添加组合的Graphics,形成多级嵌套.

设计思路

API层面暴露的类如下:

说明
画笔Pen
画刷Brush
字体Font
图形单元GraphicsUnit 所有图形都派生自该类,提供画笔、画刷、字体配置
图形Graphics 复合图形,用来构建出包含所有绘制信息的可绘制对象
具体图形类 譬如Line等,不需要派生自GraphcisUnit,表达基本的图形

图形单元提供如下接口:

class GraphicsUnit
{

public:
//以下是每个图形单元都需要的配置,直接实现即可
std::optional<Pen> pen;
std::optional<Brush> brush;
std::optional<Font> font;
std::optional<Transform> transform;

virtual ~GraphicsUnit() = default;
//示例用的打印实现
virtual void print(std::size_t prefix) = 0;
protected:
GraphicsUnit() = default;
};

图形提供如下接口:

class Graphics :public GraphicsUnit
{
std::vector<std::unique_ptr<GraphicsUnit>> units;
public:
Graphics() = default;
template<typename... Args>
explicit Graphics(Args&&... args)
{
//声明式构造实现的关键
}

void print(std::size_t prefix) override {
for (auto& g : units) {
g->print(prefix + 4);
}
}
};

GraphicsUnit及其派生类

对于基本图形来讲,GraphicsUnit中已经存储了必要的画笔、画刷等信息,派生类的职责就是提供基本图形的存储,例如线:

class LineGraphics:public GraphicsUnit{
Line line;
public:
LineGraphics(Line arg)
:line(arg){};

void print(std::size_t prefix) override
{
std::cout << std::string(prefix,'>') << line << "\n";
}
};

上述代码对于任何基本图形都一样,可以用模板类来解决:

template<typename T>
class GraphicsUnitImpl:public GraphicsUnit
{
T impl;
public:
GraphicsUnitImpl(T&& arg)
:impl(std::move(arg)) {};

T& value()& {
return impl;
}

void print(std::size_t prefix) override
{
std::cout << std::string(prefix,'>') << impl << "\n";
}
};

这时,如果用户希望创建LineGraphics,可以要求GraphicsUnit提供如下接口:

class GraphicsUnit
{

public:
std::optional<Pen> pen;
std::optional<Brush> brush;
std::optional<Font> font;
std::optional<Transform> transform;
virtual ~GraphicsUnit() = default;
virtual void print(std::size_t prefix) = 0;
protected:
GraphicsUnit() = default;
protected:
//根据Line创建出对应的LineGraphics
std::unique_ptr<GraphicsUnit> make(Line&& v);
};

其实现为:

std::unique_ptr<GraphicsUnit> GraphicsUnit::make(Line&& v){
return std::make_unique<GraphicsUnitImpl<Line>>(std::move(v));
}

对于支持的基本类型都可以提供出相应的make实现.

Graphics声明式构造

这里提供辅助类Builder来支持可变参数构造:

class Graphics :public GraphicsUnit
{
//Graphics由Unit组合而成
std::vector<std::unique_ptr<GraphicsUnit>> units;

struct Builder {
Graphics& obj;
GraphicsUnit* unit;
public:
//刚开始时设置Graphics上的画笔、画刷、字体
Builder(Graphics& arg) :obj(arg), unit(&arg) {};

//处理画笔、画刷、字体等绘制配置
void operator()(Pen&& pen) {
unit->pen = std::move(pen);
}

void operator()(Brush&& brush) {
unit->brush = std::move(brush);
}

void operator()(Font&& font) {
unit->font = std::move(font);
}

//用户传递Graphics过来,首先构造出GraphicsUnit
//然后更新unit
void operator()(Graphics&& g) {
obj.units.emplace_back(std::make_unique<Graphics>(std::move(g)));
unit = obj.units.back().get();
}

//如果是基本的图形,则调用基类的make函数构造出图形单元
template<typename T>
void operator()(T&& v)
{
obj.units.emplace_back(obj.make(std::move(v)));
unit = obj.units.back().get();
}
};
public:
Graphics() = default;
};

然后Graphics的构造函数使用Builder实现:

class Graphics :public GraphicsUnit
{
std::vector<std::unique_ptr<GraphicsUnit>> units;
struct Builder {
//见上文
};
public:
Graphics() = default;
template<typename... Args>
explicit Graphics(Args&&... args)
{
Builder op(*this);
(op(std::forward<Args>(args)), ...);
}
};

可扩展API设计

上述设计要求如果是基本的图形元素,譬如Line,则GraphicsUnit必须提供对应的make函数,如果扩充基本图形,必然要修改原始API,是否可以不修改支持扩展?

这里可以将GraphicsUnit::make接口修改为一个,来应对各种场景需求.相应技术在之前的文章中讲过,这里不再赘述,示例实现如下:

struct Pen {
double width;
};
struct Brush {};
struct Font {};
struct Transform {};

class GraphicsUnit
{

public:
std::optional<Pen> pen;
std::optional<Brush> brush;
std::optional<Font> font;
std::optional<Transform> transform;
virtual ~GraphicsUnit() = default;
virtual void print(std::size_t prefix) = 0;
protected:
GraphicsUnit() = default;
protected:
struct Maker;

class IValue {
public:
virtual ~IValue() = default;
virtual std::size_t typeCode() const noexcept = 0;

template<typename T>
bool is() const noexcept
{
return typeid(T).hash_code() == this->typeCode();
}
};

template<typename T>
class Value :public IValue {
public:
T impl;
Value(T&& v) :impl(std::move(v)) {};

std::size_t typeCode() const noexcept override {
return typeid(T).hash_code();
}
};
std::unique_ptr<GraphicsUnit> make(IValue* v);
};

class Graphics :public GraphicsUnit
{
std::vector<std::unique_ptr<GraphicsUnit>> units;
struct Builder {
Graphics& obj;
GraphicsUnit* unit;
public:
Builder(Graphics& arg) :obj(arg), unit(&arg) {};

void operator()(Pen&& pen) {
unit->pen = std::move(pen);
}

void operator()(Brush&& brush) {
unit->brush = std::move(brush);
}

void operator()(Font&& font) {
unit->font = std::move(font);
}

void operator()(Graphics&& g) {
obj.units.emplace_back(std::make_unique<Graphics>(std::move(g)));
unit = obj.units.back().get();
}

template<typename T>
void operator()(T&& v)
{
obj.units.emplace_back(obj.make(&Value<T>(std::forward<T>(v))));
unit = obj.units.back().get();
}
};
public:
Graphics() = default;
template<typename... Args>
explicit Graphics(Args&&... args)
{
Builder op(*this);
(op(std::forward<Args>(args)), ...);
}

void print(std::size_t prefix) override {
for (auto& g : units) {
g->print(prefix + 4);
}
}
};

std::ostream& operator<<(std::ostream& os, const std::vector<double>& values) {
os << "{";
for (auto v : values) {
os << v << ",";
}
os << "}";
return os;
}

template<typename T>
class GraphicsUnitImpl:public GraphicsUnit
{
T impl;
public:
GraphicsUnitImpl(T&& arg)
:impl(std::move(arg)) {};

T& value()& {
return impl;
}

void print(std::size_t prefix) override
{
std::cout << std::string(prefix,'>') << impl << "\n";
}
};


int main(int argc, char** argv) {
auto result = Graphics(
1024,
3.1415926,
std::string("liff.engineer@gmail.com"),
Pen{ 10 },
Graphics(
true,
1.414,
std::vector<double>{1, 2, 3, 4, 5},
Graphics(
false,
std::string("engineer"),
4096
)
)
);

result.print(0);
return 0;
}


struct GraphicsUnit::Maker
{
std::unique_ptr<GraphicsUnit> result;

template<typename T>
bool try_make(IValue* v)
{
if (!v->is<T>()) return false;
result = std::move(std::make_unique<GraphicsUnitImpl<T>>(
std::move(static_cast<Value<T>*>(v)->impl)
));
return true;
}

template<typename... Ts>
bool make(IValue* v)
{
return (try_make<Ts>(v) || ...);
}
};


std::unique_ptr<GraphicsUnit> GraphicsUnit::make(IValue* v)
{
Maker op;
if (op.make<int,double,bool,std::string,std::vector<double>>(v)) {
return std::move(op.result);
}
throw std::invalid_argument("invalid append argument");
return nullptr;
}


以上是关于基于现代C++的声明式显示模块API的主要内容,如果未能解决你的问题,请参考以下文章

现代C++笔记

现代C++函数式编程

C++ 引用在函数式编程中的应用

Spring全注解开发---声明式事务模块

OpenHarmony - ArkUI(TS)声明式开发之画板

实现无入侵式C++代码mock工具