画布控件
画布控件和画布控件渲染器是强大的多功能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
用户代码中的存储空间分配
如果不通过TouchGFX 设计器为使用Canvas Widgets(画布控件)的屏幕分配内存缓冲区,则必须手动设置缓冲区。 建议在Screen::setupScreen
方法中执行此操作。
将其作为私有项添加到Screen类定义中:
private:
static const uint16_t CANVAS_BUFFER_SIZE = 3600;
static uint8_t canvasBuffer[CANVAS_BUFFER_SIZE]
然后,在ScreenView.cpp
的setupScreen()
方法中,可以添加以下缓冲区设置行。
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)。
尽管最初看起来令人困惑,但很快会发现这是十分自然的。 位图的坐标系寻址像素,画布控件的同一坐标系寻址像素之前有间隙。
因为圆形通常需要移动半个像素才能正确放置圆心,所以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 设计器中使用画布控件时,设计器将选择正确的画笔,但是如果您自己编写画布控件代码,则必须选择合适的画笔。
帧缓存格式 | 彩色画笔 | 位图画笔 |
---|---|---|
BW | PainterBW | PainterBWBitmap |
GRAY2 | PainterGRAY2 | PainterGRAY2Bitmap |
GRAY4 | PainterGRAY4 | PainterGRAY4Bitmap |
ABGR2222 | PainterABGR2222 | PainterABGR2222Bitmap |
ARGB2222 | PainterARGB2222 | PainterARGB2222Bitmap |
BGRA2222 | PainterBGRA2222 | PainterBGRA2222Bitmap |
RGBA2222 | PainterRGBA2222 | PainterRGBA2222Bitmap |
RGB565 | PainterRGB565 | PainterRGB565Bitmap, PainterRGB565L8Bitmap |
RGB888 | PainterRGB888 | PainterRGB888Bitmap, PainterRGB888L8Bitmap |
ARGB8888 | PainterARGB8888 | PainterARGB8888Bitmap, PainterARGB8888L8Bitmap |
XRGB8888 | PainterXRGB8888 | PainterXRGB8888Bitmap, PainterXRGB8888L8Bitmap |
位图画笔支持各种位图格式:
画笔 | 支持的位图格式 |
---|---|
PainterBWBitmap | BW, BW_RLE |
PainterGRAY2Bitmap | GRAY2 |
PainterGRAY4Bitmap | GRAY4 |
PainterABGR2222Bitmap | ABGR2222 |
PainterARGB2222Bitmap | ARGB2222 |
PainterBGRA2222Bitmap | BGRA2222 |
PainterRGBA2222Bitmap | RGBA2222 |
PainterRGB565Bitmap | RGB565, ARGB8888 |
PainterRGB565L8Bitmap | L8_RGB565, L8_RGB888, L8_ARGB8888 |
PainterRGB888Bitmap | RGB888, ARGB8888 |
PainterRGB888L8Bitmap | L8_RGB565, L8_RGB888, L8_ARGB8888 |
PainterARGB8888Bitmap | RGB565, RGB888, ARGB8888 |
PainterARGB8888L8Bitmap | L8_RGB565, L8_RGB888, L8_ARGB8888 |
PainterXRGB8888Bitmap | RGB565(无透明度), RGB888, ARGB8888 |
PainterXRGB8888L8Bitmap | L8_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;
目标指向帧缓存中的起始位置(控件左边缘)。
偏移量是从该起始位置到放置第一个像素的像素数。
widgetX、widgetY是第一个像素相对于控件的坐标(在帧缓存坐标系中给出)。
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类忽略了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提供。 使用以下代码,圆现在具有平滑的边缘:
WidgetX和WidgetY参数可用于将图形限制在特定区域。 例如,有一个画笔,每次都隔一条水平线进行绘制。 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);
}
}
};
更改帧缓存
本节中的画笔不会将特定内容绘制到帧缓存,而是将帧缓存更改为灰度级。 通过读取帧缓存中的像素值(由圆背景中的控件写入),提取绿色成分,使用该成分创建灰色(红色、绿色、蓝色的值相同),然后将其写回帧缓存来实现这一点。
利用读取和修改帧缓存的原理,可以开发许多类似的技术。
#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:
图像、文本和按钮已被TouchGFX引擎旋转,但我们看到条纹现在垂直于文本,但应该是平行的。 直线没有旋转。
问题是帧缓存没有旋转,所以当画笔在连续的地址(帧缓存中的像素) 中绘画时,线条的方向和以前一样(没有旋转)。
这个问题我们可以通过使用WidgetX决定是否正在绘制予以解决。 widgetX和widgetY参数在帧缓存坐标系中给出。 这意味着当我们在显示器上向下移动时,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);
}
}
};
条纹现已正确定向:
填充规则
Shape(形状)控件上,可在两个填充规则之间进行选择:Fill-Non-Zero(填充-非零)或Fill-Even-Odd(填充-奇偶)。 Fill-Non-Zero(填充-非零)规则为默认规则。 下图显示了两种填充规则之间的差异:
Even-Odd(奇偶)规则不会绘制穿过偶数(此处为零或二)边从外部到达的像素。
Non-Zero(非零)规则计算像素路径上从左到右的边数,并减去从右到左的边数。 如果计数为非零,则绘制像素。
填充规则可以用代码轻松设置:
touchgfx::Shape<5> shape1;
....
shape1.setFillingRule(Rasterizer::FILL_EVEN_ODD);