跳转到主要内容

画布控件

画布控件和画布控件渲染器是强大的多功能TouchGFX插件,在使用相对较小的存储空间的同时保持高性能,可提供平滑、抗锯齿效果良好的几何图形绘制。 但是,渲染几何图形必然是成本非常高的操作,如果使用不小心,很容易对微控制器资源造成浪费。

画布控件渲染器(Canvas Widget Renderer,以下简称CWR)是一种通用图形API,为图元提供优化绘制,自动消除最多余的绘制。 TouchGFX使用CWR可绘制复杂的几何图形。 通过画布控件(Canvas Widget)定义几何图形。 TouchGFX支持许多画布控件,但是就像普通控件一样,您可以创自定义画布控件来满足您的需求。 画布控件定义要通过CWR绘制的图形的几何形状,而图形中每个像素的实际颜色则由相关Painter类定义。 同样地,TouchGFX自带许多Painter,但是您也可以创建自定义Painter来满足您的需求。

使用画布控件

TouchGFX中其他控件的大小是自动设置的。 例如,可以自动获取位图控件的宽度和高度。 因此,在位图控件上使用 setXY() 将位图放置在显示屏上就足够了。

画布控件没有默认大小,其值既可以自动确定,也可以一开始就设置。 不仅要注意位置,还要正确地确定控件的大小,否则画布控件的宽度和高度将为零,并且不会在显示屏上绘制任何内容。

因此,不要使用 setXY(),而是使用 setPosition() 来放置和调整画布控件的大小。 关于如何创建和使用自定义画布控件的示例另请参见下文Custom Canvas Widget部分。

在设置了画布空间的位置和大小后,可以在其内部绘制几何图形。 坐标系将使 (0, 0) 位于控件(不是显示屏)的左上角,X轴向右延伸且Y轴向下延伸。

TouchGFX设计器也支持“画布控件(Canvas Widget)”,让使用变得简单,可自动计算内存需求和自动分配内存。

TouchGFX 设计器中可用的画布控件:

通过TouchGFX Designer使用这些控件时,可通过显示控件在运行时的状态,使得放置和大小调整非常简单。

存储空间分配和使用

为了生成反锯齿效果良好的复杂几何图形,需要额外的存储空间。 为此,CWR必须具有专门分配的存储缓冲区,以便在渲染过程中使用。 CWR与TouchGFX的其余部分一样,没有动态存储空间分配。

TouchGFX Designer中的存储空间分配

在向屏幕的画布添加控件时,会自动生成存储缓冲区。 缓冲区大小基于屏幕宽度,计算公式为 (宽度 × 3) × 5。 但是,这并非所有情况下的理想缓冲区大小。 因此,可以重写缓冲区大小,如下图所示。

在屏幕属性中重写画布缓冲区大小

Note
通过使用上述覆盖功能,还可为不使用Canvas Widgets(画布控件)的屏幕生成内存缓冲区。 这在User Code(用户代码)中创建Canvas Widgets(画布控件)时非常有用。

用户代码中的存储空间分配

如果不通过TouchGFX 设计器为使用Canvas Widgets(画布控件)的屏幕分配内存缓冲区,则必须手动设置缓冲区。 建议在Screen::setupScreen方法中执行此操作。

将其作为私有项添加到Screen类定义中:

private:
static const uint16_t CANVAS_BUFFER_SIZE = 3600;
static uint8_t canvasBuffer[CANVAS_BUFFER_SIZE]

然后,在ScreenView.cppsetupScreen()方法中,可以添加以下缓冲区设置行。

void ScreenView::setupScreen()
{
...
CanvasWidgetRenderer::setupBuffer(canvasBuffer, CANVAS_BUFFER_SIZE);
...
}

ScreenView.hpp析构函数~ScreenView()中,可添加以下重置缓冲区行。

virtual ~ScreenView()
{
touchgfx::CanvasWidgetRenderer::resetBuffer();
}

需要的CWR存储空间的量取决于要在应用中绘制的最大图形大小。 但是,您可以保留比最复杂形状所需内存空间更少的内存。 为了应对这种情况,CWR将图形绘制分割成较小的帧缓存部分,在这种情况下,由于有时需要不止一次地渲染图像,因此渲染时间稍长。 在模拟器模式下运行时,可以更细致地查看存储空间消耗并进行微调。 只需向main.cpp中添加以下函数:

CanvasWidgetRenderer::setWriteMemoryUsageReport(true);

现在,无论绘制操作何时结束,CWR都将报告(在控制台上打印)所需存储空间的大小。 对于canvas_widget_example,可以是“CWR需要3604字节”(第一个绘制操作),然后是“CWR需要7932字节(4328字节缺失)”(第二个绘制操作)。 尽管显示CWR没有足够存储空间(本例中为4328字节缺失),应用仍正常运行。 这是因为CWR检测到可用于完成一次运行中复杂绘制操作的存储空间太少。 为此,它将绘制操作分割成两项独立的绘制操作,可以很好地绘制图形,但需要更多时间渲染。

因此,设置正确的存储缓冲区大小以便在存储空间与性能(渲染时间)之间取得平衡。 好的起始值通常约为3000,但使用上述技巧通常可以确定更优值。 如果图形过于复杂且分配的存储缓冲区过小,将不绘制部分图形(一些垂直像素线会被跳过),并且有可能不绘制任何内容。 在任何情况下,渲染时间都会增加许多。

这意味着如果您希望应用以最快速度渲染CWR绘图,您需要满足分配请求的存储空间大小。 但是,如果能够接受更长的渲染时间,完全可以缩小存储缓冲区。

CWR坐标系

TouchGFX中的坐标系通常用于寻址像素,以便在显示屏上定位位图。 位图、文本和其他图形元素都位于坐标系中,其中 (0,0) 是左上角像素,X轴向右延伸,Y轴向下延伸。 在CWR中,能够使用整数寻址像素是不够的,尽管在特殊情况下已经足够,在一般情况下远远不够。 为了证明这一点,假设有一个线宽为1的圆,必须被精确地嵌入一个5x5像素的方块中。 该圆的中心必须位于(2.5,2.5),半径必须为2(直线从圆周的两侧画出0.5),因此中心坐标需为分数。 类似地,如果圆应嵌入一个6x6像素的方块,则中心必须位于 (3, 3),半径必须是2.5,因此半径需为小数。

这种新的图形绘制坐标寻址方式意味着 (0,0) 处像素的中心的CWR坐标为 (0.5, 0.5)。 因此,包含屏幕左上角像素的方块的轮廓如下:(0,0) -> (1,0) -> (1,1) -> (0,1) -> (0,0)。

(0,0) 处像素的CWR坐标系

尽管最初看起来令人困惑,但很快会发现这是十分自然的。 位图的坐标系寻址像素,画布控件的同一坐标系寻址像素之前有间隙。

因为圆形通常需要移动半个像素才能正确放置圆心,所以Circle::setPixelCenter()函数会将圆心放置在给定像素的中心,也就是说,从指定的坐标再向右和向下移动半个像素.

自定义画布控件

实现自定义画布控件需要用下列函数实现新类:

virtual bool drawCanvasWidget(const Rect& invalidatedArea) const;
virtual Rect getMinimalRect() const;

drawCanvasWidget() 必须绘制自定义控件需要绘制的任何内容,并且 getMinimalRect() 应该返回 Widget 中包含几何形状的实际矩形。

Note
使用 getMinimalRect() 的原因在于可以在其控件内部到处移动几何图形,并且当形状变为仅使最小可能区域无效时,改变控件的大小和重新定位控件通常都不切实际。

函数 getMinimalRect() 的虚拟实现可能只 返回rect;这是控件的大小,但这会导致被画布控件覆盖的整个区域的重新绘制,而不只是包含几何图形的画布控件的一部分。 几何图形通常只占据画布控件的一小部分。

画布控件全部使用Canvas类,它如上文所述压缩Canvas Widget Renderer。 CWR有许多自动应用的优化,尽管知道几何图形与无效区域有关,避免无效区域之外的不必要绘制,始终是一种提升性能的好方法。

在10x10方块内部粗略实现一个菱形块,方式可能像这样:

#include <touchgfx/widgets/canvas/CanvasWidget.hpp>
#include <touchgfx/widgets/canvas/Canvas.hpp>

using namespace touchgfx;

class Diamond10x10 : public CanvasWidget
{
public:
virtual Rect getMinimalRect() const
{
return Rect(0,0,10,10);
}
virtual bool drawCanvasWidget(const Rect& invalidatedArea) const
{
Canvas canvas(this, invalidatedArea);
canvas.moveTo(5,0);
canvas.lineTo(10,5);
canvas.lineTo(5,10);
canvas.lineTo(0,5);
return canvas.render(); // Shape is automatically closed
}
};
Note
同样地,注意 getMinimalRect() 返回到正确矩形,否则屏幕上的图形可能是错误的。

为了在显示器上看到Diamond10x10,必须通过向方块传递画笔来设置颜色。 有关画笔的详细内容见下节。 另外,必须正确地放置Diamond10x10并调整其大小。 方式可能像这样:

在头文件中声明

Diamond10x10 box;
PainterRGB565 myPainter; // For 16bpp displays

在代码中,setupScreen()中应有这样的内容:

myPainter.setColor(Color::getColorFromRGB(0xFF, 0x0, 0x0));
box.setPosition(100,100,10,10);
box.setPainter(myPainter);
add(box);

Painter

绘图器定义一个配色方案,用于填充‘Canvas Widget’对象,因此绘图器需要使形状可见。 绘图器可以为所有像素提供单一颜色(例如PainterRGB565),或者从提供的位图中复制每个像素(例如PainterRGB565Bitmap)。 由于画笔直接将像素写入帧缓存,因此所选的画笔必须匹配帧缓存或动态位图的格式。 TouchGFX提供的绘图器面向所有支持的显示屏,专门用于纯色或位图绘制。

画笔类

下表列出了TouchGFX中可用的绘画工具。 当您在TouchGFX 设计器中使用画布控件时,设计器将选择正确的画笔,但是如果您自己编写画布控件代码,则必须选择合适的画笔。

帧缓存格式彩色画笔位图画笔
BWPainterBWPainterBWBitmap
GRAY2PainterGRAY2PainterGRAY2Bitmap
GRAY4PainterGRAY4PainterGRAY4Bitmap
ABGR2222PainterABGR2222PainterABGR2222Bitmap
ARGB2222PainterARGB2222PainterARGB2222Bitmap
BGRA2222PainterBGRA2222PainterBGRA2222Bitmap
RGBA2222PainterRGBA2222PainterRGBA2222Bitmap
RGB565PainterRGB565PainterRGB565Bitmap, PainterRGB565L8Bitmap
RGB888PainterRGB888PainterRGB888Bitmap, PainterRGB888L8Bitmap
ARGB8888PainterARGB8888PainterARGB8888Bitmap, PainterARGB8888L8Bitmap
XRGB8888PainterXRGB8888PainterXRGB8888Bitmap, PainterXRGB8888L8Bitmap

位图画笔支持各种位图格式:

画笔支持的位图格式
PainterBWBitmapBW, BW_RLE
PainterGRAY2BitmapGRAY2
PainterGRAY4BitmapGRAY4
PainterABGR2222BitmapABGR2222
PainterARGB2222BitmapARGB2222
PainterBGRA2222BitmapBGRA2222
PainterRGBA2222BitmapRGBA2222
PainterRGB565BitmapRGB565, ARGB8888
PainterRGB565L8BitmapL8_RGB565, L8_RGB888, L8_ARGB8888
PainterRGB888BitmapRGB888, ARGB8888
PainterRGB888L8BitmapL8_RGB565, L8_RGB888, L8_ARGB8888
PainterARGB8888BitmapRGB565, RGB888, ARGB8888
PainterARGB8888L8BitmapL8_RGB565, L8_RGB888, L8_ARGB8888
PainterXRGB8888BitmapRGB565(无透明度), RGB888, ARGB8888
PainterXRGB8888L8BitmapL8_RGB565, L8_RGB888, L8_ARGB8888

平铺位图

从位图绘制像素的画笔将位图放置在画布控件的左上角。 不绘制位图维度之外的形状像素。

位图画笔可配置为重复控件(平铺)以覆盖整个形状。

通过调用画笔上的setTiled(bool)方法来启用平铺:

    PainterRGB888Bitmap bitmapPainter;
...
bitmapPainter.setBitmap(touchgfx::Bitmap(BITMAP_BLUE_LOGO_TOUCHGFX_LOGO_ID));
bitmapPainter.setTiled(true);

无法在设计器中启用平铺。

在显示图像的应用程序中,使用位图画笔绘制圆,使用平铺位图画笔绘制圆。

定制绘图器

尽管TouchGFX提供一组预定义的画笔类,涵盖了大多数用例场景,但也可实现定制画笔。

本节中,我们将提供一些可以作为灵感的示例。 示例仅适用于16bpp RGB565。 其它帧缓存格式必须稍加修改。

定制画笔只是AbstractPainter类的一个子类。 16bpp(RGB565)帧缓存的画笔可使用AbstractPainterRGB565类作为超级类。 用AbstractPainterRGB565类作为超级类。 24bpp(RGB888)帧缓存的画笔可使用AbstractPainterRGB888作为超级类。

这些超级类为抽象类。 定制画笔类必须实现以下方法:

    virtual void paint(uint8_t* destination, int16_t offset, int16_t widgetX, int16_t widgetY, int16_t count, uint8_t alpha) const = 0;

目标指向帧缓存中的起始位置(控件左边缘)。
偏移量是从该起始位置到放置第一个像素的像素数。
widgetXwidgetY是第一个像素相对于控件的坐标(在帧缓存坐标系中给出)。
count是用指定的alpha绘制的像素数。

画布控件会多次调用这个方法,所以画笔的实现速度要快,这一点非常重要。 如果画布控件不经常更新,则不重要。

彩色画笔

最简单的画笔只需要将固定的颜色写入帧缓存。 以下为实现方法:

#include <touchgfx/widgets/canvas/AbstractPainterRGB565.hpp>
using namespace touchgfx;
class RedPainter : public AbstractPainterRGB565
{
public:
virtual void paint(uint8_t* destination, int16_t offset, int16_t widgetX, int16_t widgetY, int16_t count, uint8_t alpha) const
{
uint16_t* framebuffer = reinterpret_cast<uint16_t*>(destination) + offset; // Address of first pixel to paint
const uint16_t* const lineend = framebuffer + count; // Address of last pixel to paint
const uint16_t redColor565 = 0xF800; // Full red in RGB565
do
{
*framebuffer = redColor565;
} while (++framebuffer < lineend);
}
};

创建一个画笔实例,并将其分配给您的画布控件。 将一种画笔类型添加至您的类别:

Circle myCircle;
RedPainter myPainter;

在代码中,setupScreen()中应有这样的内容:

...
myCircle.setPainter(myPainter);
...

RedPainter绘制圆。 右边是放大的部分。

上面的RedPainter类忽略了alpha参数。 这会使边缘变得粗糙(无alpha混合),因为所有像素都变成了红色。 我们可以对代码进行略微更新来改善这一点,如果需要,可以使用alpha参数进行混合:

#include <touchgfx/widgets/canvas/AbstractPainterRGB565.hpp>
using namespace touchgfx;
class AlphaRedPainter : public AbstractPainterRGB565
{
public:
virtual void paint(uint8_t* destination, int16_t offset, int16_t widgetX, int16_t widgetY, int16_t count, uint8_t alpha) const
{
uint16_t* framebuffer = reinterpret_cast<uint16_t*>(destination) + offset; // Address of first pixel to paint
const uint16_t* const lineend = framebuffer + count;
const uint16_t redColor565 = 0xF800; // Full red in RGB565
do
{
if (alpha == 0xFF)
{
*framebuffer = redColor565; // Write red to framebuffer
}
else
{
*framebuffer = alphaBlend(redColor565, *framebuffer, alpha); // Blend red with the framebuffer color
}
} while (++framebuffer < lineend);
}
};

函数alphaBlend混合两个RGB565像素,第一个像素具有给定的alpha。 该函数由超级类AbstractPainterRGB565提供。 使用以下代码,圆现在具有平滑的边缘:

RedAlphaPainter绘制一个圆。 右侧显示alpha混合的放大部分。

WidgetXWidgetY参数可用于将图形限制在特定区域。 例如,有一个画笔,每次都隔一条水平线进行绘制。 WidgetY用于控制:

#include <touchgfx/widgets/canvas/AbstractPainterRGB565.hpp>
using namespace touchgfx;
class StripePainter : public AbstractPainterRGB565
{
public:
virtual void paint(uint8_t* destination, int16_t offset, int16_t widgetX, int16_t widgetY, int16_t count, uint8_t alpha) const
{
if ((widgetY & 2) == 0)
{
return; // Do not draw anything on line 0,1, 4,5, 8,9, etc.
}
uint16_t* framebuffer = reinterpret_cast<uint16_t*>(destination) + offset;
const uint16_t* const lineend = framebuffer + count;
if (alpha == 0xFF)
{
do
{
*framebuffer = 0xF800;
} while (++framebuffer < lineend);
}
else
{
do
{
*framebuffer = alphaBlend(0xF800, *framebuffer, alpha);
} while (++framebuffer < lineend);
}
}
};

StripePainter绘制圆。 右边是放大的部分。

更改帧缓存

本节中的画笔不会将特定内容绘制到帧缓存,而是将帧缓存更改为灰度级。 通过读取帧缓存中的像素值(由圆背景中的控件写入),提取绿色成分,使用该成分创建灰色(红色、绿色、蓝色的值相同),然后将其写回帧缓存来实现这一点。

利用读取和修改帧缓存的原理,可以开发许多类似的技术。

#include <touchgfx/widgets/canvas/AbstractPainterRGB565.hpp>
#include <touchgfx/Color.hpp>
using namespace touchgfx;
class GrayscalePainter : public AbstractPainterRGB565
{
public:
virtual void paint(uint8_t* destination, int16_t offset, int16_t widgetX, int16_t widgetY, int16_t count, uint8_t alpha) const
{
uint16_t* framebuffer = reinterpret_cast<uint16_t*>(destination) + offset;
const uint16_t* const lineend = framebuffer + count;
do
{
const uint8_t green = Color::getGreenFromRGB565(*framebuffer) & 0xF8; // Keep only 5 bits of the green
const uint16_t color565 = LCD16bpp::getNativeColorFromRGB(green, green, green);
if (alpha == 0xFF)
{
*framebuffer = color565;
}
else
{
*framebuffer = alphaBlend(color565, *framebuffer, alpha);
}
} while (++framebuffer < lineend);
}
};

左侧是原始背景。 右侧,圆形画笔将圆的内部像素改为灰度。

旋转显示的定制容器

如果您的应用程序使用旋转显示,当其在绘制过程中使用坐标时,自定义容器代码必须考虑这一点。

以下是与旋转显示一起使用的StripePainter:

StripePainter绘制圆。 右边是放大的部分。

图像、文本和按钮已被TouchGFX引擎旋转,但我们看到条纹现在垂直于文本,但应该是平行的。 直线没有旋转。
问题是帧缓存没有旋转,所以当画笔在连续的地址(帧缓存中的像素) 中绘画时,线条的方向和以前一样(没有旋转)。

这个问题我们可以通过使用WidgetX决定是否正在绘制予以解决。 widgetXwidgetY参数在帧缓存坐标系中给出。 这意味着当我们在显示器上向下移动时,widgetX会增长,并对应显示器坐标系中的y。

#include <touchgfx/widgets/canvas/AbstractPainterRGB565.hpp>
#include <touchgfx/Color.hpp>
using namespace touchgfx;
class StripePainterRotate90 : public AbstractPainterRGB565
{
public:
virtual void paint(uint8_t* destination, int16_t offset, int16_t widgetX, int16_t widgetY, int16_t count, uint8_t alpha) const
{
uint16_t* framebuffer = reinterpret_cast<uint16_t*>(destination) + offset;
const uint16_t* const lineend = framebuffer + count;
if (alpha == 0xFF)
{
do
{
if (widgetX++ & 2)
{
*framebuffer = 0xF800;
}
} while (++framebuffer < lineend);
}
else
{
do
{
if (widgetX++ & 2)
{
*framebuffer = alphaBlend(0xF800, *framebuffer, alpha);
}
} while (++framebuffer < lineend);
}
}
};

条纹现已正确定向:

StripePainterRotate90绘制圆。

填充规则

Shape(形状)控件上,可在两个填充规则之间进行选择:Fill-Non-Zero(填充-非零)Fill-Even-Odd(填充-奇偶)。 Fill-Non-Zero(填充-非零)规则为默认规则。 下图显示了两种填充规则之间的差异:

使用Fill-Even-Odd(填充-奇偶)规则绘制起始形状

使用Fill-Non-Zero(填充-非零)规则绘制起始形状

Even-Odd(奇偶)规则不会绘制穿过偶数(此处为零或二)边从外部到达的像素。

Non-Zero(非零)规则计算像素路径上从左到右的边数,并减去从右到左的边数。 如果计数为非零,则绘制像素。

填充规则可以用代码轻松设置:

    touchgfx::Shape<5> shape1;
....
shape1.setFillingRule(Rasterizer::FILL_EVEN_ODD);
Further reading