画布控件
画布控件和画布控件渲染器是强大的多功能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)”,让使用变得简单,可自动计算内存需求和自动分配内存。
Available Canvas Widgets in TouchGFX Designer:
通过TouchGFX Designer使用这些控件时,可通过显示控件在运行时的状态,使得放置和大小调整非常简单。
存储空间分配和使用
为了生成反锯齿效果良好的复杂几何图形,需要额外的存储空间。 为此,CWR必须具有专门分配的存储缓冲区,以便在渲染过程中使用。 CWR与TouchGFX的其余部分一样,没有动态存储空间分配。
TouchGFX Designer中的存储空间分配
在向屏幕的画布添加控件时,会自动生成存储缓冲区。 缓冲区大小基于屏幕宽度,计算公式为 (宽度 × 3) × 5。 但是,这并非所有情况下的理想缓冲区大小。 因此,可以重写缓冲区大小,如下图所示。
Note
用户代码中的存储空间分配
If you don't use TouchGFX Designer to allocate a memory buffer for screens that uses Canvas Widgets, you must manually setup a buffer. 建议在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中可用的绘画工具。 When you use Canvas Widgets with TouchGFX Designer, it will select the correct painter, but if you write code yourself that uses Canvas Widgets, you must select a suitable painter.
帧缓存格式 | 彩色画笔 | 位图画笔 |
---|---|---|
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);
Tiling can not be enabled in TouchGFX Designer.
定制绘图器
尽管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);