TouchGFX之画布

一、画布渲染器

画布控件渲染器(Canvas Widget Renderer,以下简称CWR)是强大的多功能TouchGFX插件,在使用相对较小的存储空间的同时保持高性能,可提供平滑、抗锯齿效果良好的几何图形绘制。TouchGFX使用CWR可绘制复杂的几何图形。 通过画布控件(Canvas Widget)定义几何图形。

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()函数会将圆心放置在给定像素的中心,也就是说,从指定的坐标再向右和向下移动半个像素.

#ifndef TOUCHGFX_RASTERIZER_HPP
#define TOUCHGFX_RASTERIZER_HPP
#include <touchgfx/canvas_widget_renderer/Outline.hpp>
#include <touchgfx/widgets/canvas/AbstractPainter.hpp>

namespace touchgfx
{
/* 多边形光栅化器,用于渲染具有高质量消除混叠的填充多边形 */
class Rasterizer
{
public:
	/* 确定子像素精度,更确切地说,是坐标的小数部分的位数 */ 
	enum
	{
		POLY_BASE_SHIFT = 5,                   ///< 为小数部分保留的位数
		POLY_BASE_SIZE = 1 << POLY_BASE_SHIFT, ///< 转换为此格式时要除以或乘以的值
		POLY_BASE_MASK = POLY_BASE_SIZE - 1    ///< 用于屏蔽小数的值
	};

	/* 确定区域精度,更确切地说,是在计算扫描线时区域的小数部分的位数 */
	enum
	{
		AA_SHIFT = 8,           ///< 计算区域时为小数部分保留的位数
		AA_NUM = 1 << AA_SHIFT, ///< 转换为此格式时要除以或乘以的值
		AA_MASK = AA_NUM - 1,   ///< 用于屏蔽小数的值
		AA_2NUM = AA_NUM * 2,   ///< 两个区域数相乘时的小数位数
		AA_2MASK = AA_2NUM - 1  ///< 两个区域数相乘时用于屏蔽小数的值
	};

	/* 表示填充规则的值 */
	enum FillingRule
	{
		FILL_NON_ZERO, ///< 填充规则,填充轮廓最外边界内的任何内容
		FILL_EVEN_ODD  ///< 填充规则,使用异或规则在轮廓内进行填充
	};

	/* 构造函数 */
	Rasterizer() : outline(), fillingRule(FILL_NON_ZERO), offsetX(0), offsetY(0), width(0)
	{
	}

	/* 重置此对象。基本上是通过重置Outline来完成的 */
	void reset(int16_t offset_x, int16_t offset_y)
	{
		offsetX = offset_x;	///< 设置X偏移量
		offsetY = offset_y;	///< 设置Y偏移量
		outline.reset();		///< 重置轮廓
	}

	/* 设置渲染轮廓时要使用的填充规则 */
	void setFillingRule(FillingRule rule)
	{
		fillingRule = rule;
	}

	/* 获取渲染轮廓时使用的填充规则 */  
	FillingRule getFillingRule() const
	{
		return fillingRule;
	}

	/* 移动到指定坐标 */
	void moveTo(int x, int y)
	{
#ifndef SIMULATOR
		if (!outline.wasOutlineTooComplex())	// 如果轮廓不太复杂
#endif
		{
			outline.moveTo(x, y);	// 移动到指定坐标
		}
	}

	/* 画线到指定坐标 */ 
	void lineTo(int x, int y)
	{
#ifndef SIMULATOR
		if (!outline.wasOutlineTooComplex())
#endif
		{
			outline.lineTo(x, y);
		}
	}

	/* 计算透明度(alpha值) */
	unsigned calculateAlpha(int area) const
	{
		int cover = area >> (POLY_BASE_SHIFT * 2 + 1 - AA_SHIFT);

		if(cover < 0)
		{
			cover = -cover;
		}
		
		if (fillingRule == FILL_EVEN_ODD)
		{
			cover &= AA_2MASK;
			if (cover > AA_NUM)
			{
				cover = AA_2NUM - cover;
			}
		}
		
		if (cover > AA_MASK)
		{
			cover = AA_MASK;
		}
		
		return cover;
	}

	/**  
	 * 渲染当前对象到给定的绘图缓冲区。  
	 *   
	 * 这个函数会处理对象的轮廓数据,并将其渲染到指定的绘图缓冲区中。轮廓数据被组织成  
	 * 一系列的cell,这些cell会被混合并绘制到缓冲区,形成最终的图像。  
	 *   
	 * @param painter      指向AbstractPainter对象的指针,该对象定义了绘制图形的基本操作。  
	 * @param buf          指向绘图缓冲区的指针,渲染的结果将存储在这里。  
	 * @param stride       缓冲区的跨度(stride),即缓冲区中每行像素所占的字节数。  
	 * @param xAdjust      用于调整渲染位置在x轴上的偏移量。  
	 * @param global_alpha 渲染时使用的全局透明度值,它会影响所有像素的透明度。  
	 *   
	 * @return 如果成功渲染对象,并且没有遇到内存不足的问题,则返回true;否则返回false。  
	 */  
	bool render(const AbstractPainter* const painter, uint8_t* buf, int16_t stride, uint8_t xAdjust, uint8_t global_alpha)  
	{  
    // 关闭当前轮廓,并对其进行排序以优化渲染过程。  
    const Cell* curCell = outline.closeOutlineAndSortCells();  
  
    // 获取排序后轮廓中的cell数量。  
    unsigned numCells = outline.getNumCells();  
    if (numCells == 0)  
    {  
        // 如果没有cell需要渲染,直接返回成功。  
        return true;  
    }  
  
    // 检查在处理轮廓时是否发生了内存不足的情况。  
    if (outline.wasOutlineTooComplex())  
    {  
        // 如果内存不足,则无法继续渲染,返回失败。  
        return false;  
    }  
  
    // 初始化渲染时的变量。  
    int cover = 0;          // 当前覆盖值(用于抗锯齿和透明度计算)。  
    int old_y = curCell->y; // 上一个cell的y坐标,用于检测换行。  
    int widget_y = old_y + offsetY; // 当前行号(考虑了对象的偏移)。  
    uint8_t* buf_ptr = buf + old_y * stride; // 指向当前cell开始位置的缓冲区指针。  
    numCells--; // 减少cell数量,因为for循环会先处理一个cell。  
  
    // 渲染循环,处理所有cell。  
    for (;;)  
    {  
        // 从当前cell开始混合直到遇到不同x或y坐标的cell。  
        const Cell* startCell = curCell;  
  
        // 记录当前cell的起始x坐标和y坐标。  
        const int start_x = curCell->x;  
        int x = start_x;  
        const int y = curCell->y;  
          
        // 检查是否需要换行到新的y坐标。  
        if (y != old_y)  
        {  
            old_y = y; // 更新上一个y坐标。  
            widget_y = old_y + offsetY; // 重新计算当前行号。  
            buf_ptr = buf + old_y * stride; // 更新缓冲区指针到新的行。  
        }  
  
        // 混合相同位置的cell。  
        int area = startCell->area; // 当前混合区域的面积。  
        cover += startCell->cover; // 累加覆盖值。  
        // 循环直到遇到不同x或y坐标的cell,或者处理完所有cell。  
        while (numCells-- > 0)  
        {  
            curCell++; // 移动到下一个cell。  
            // 检查是否到达了新的x或y坐标,如果是则跳出循环。  
            if (curCell->x != start_x || curCell->y != y)  
            {  
                break;  
            }  
            // 累加当前cell的面积和覆盖值到混合区域。  
            area += curCell->area;  
            cover += curCell->cover;  
        }  
  
        // 如果混合区域有有效面积,则进行绘制。  
        if (area)  
        {  
            // 确保x坐标在渲染范围内。  
            if (x >= 0 && x < width)  
            {  
                // 计算混合后的透明度值,并乘以全局透明度。  
                const int8_t alpha = LCD::div255(calculateAlpha((cover << (POLY_BASE_SHIFT + 1)) - area) * global_alpha);  
                if (alpha) // 如果透明度非零,则进行绘制。  
                {  
                    painter->paint(buf_ptr, x + xAdjust, x + offsetX, widget_y, 1, alpha); // 绘制当前像素。  
                }  
            }  
            x++; // 移动到下一个x坐标。  
        }  
  
        // 检查是否处理完了所有cell。  
        if (numCells == unsigned(-1)) // 如果减到了-1,说明所有cell都已处理。  
        {  
            break; // 退出循环。  
        }  
  
        // 处理当前cell与下一个cell之间的空白区域(如果有的话)。  
        int count = curCell->x - x; // 计算空白区域的宽度。  
        if (count > 0)  
        {  
            // 如果当前x坐标小于0,则减少空白区域宽度以补偿。  
            if (x < 0)  
            {  
                count += x; // 减少宽度。  
                x = 0; // 重置x坐标。  
            }  
            // 如果还有空白区域需要处理,则进行绘制。  
            if (count > 0)  
            {  
                // 确保绘制不会超出渲染范围。  
                if (x + count >= width)  
                {  
                    count = width - x; // 裁剪空白区域宽度。  
                }  
                // 如果还有空白区域需要绘制,则计算透明度并绘制。  
                if (count > 0)  
                {  
                    const int8_t alpha = LCD::div255(calculateAlpha(cover << (POLY_BASE_SHIFT + 1)) * global_alpha); // 计算透明度。  
                    if (alpha) // 如果透明度非零,则进行绘制。  
                    {  
                        painter->paint(buf_ptr, x + xAdjust, x + offsetX, widget_y, count, alpha); // 绘制空白区域。  
                    }  
                }  
            }  
        }  
    }  
      
    // 所有cell处理完毕,渲染完成。  
    return true;  
	}

	/**  
	 * 设置最大渲染y坐标。这将传递给Outline,以避免注册任何y坐标小于0或大于给定y的Cell。  
	 *  
	 * 注意:函数名可能有些误导,因为它实际上设置了宽度和高度,而不仅仅是y坐标。  
	 *  
	 * @param w 宽度。  
	 * @param h 高度。  
	 */
	void setMaxRender(int16_t w, int16_t h)
	{
		width = w;
		outline.setMaxRender(w, h);
	}

	/**  
	 * 确定轮廓是否太复杂而无法完全绘制。  
	 *  
	 * @return 如果太复杂,则返回true;否则返回false。  
	 */
	FORCE_INLINE_FUNCTION bool wasOutlineTooComplex()
	{
		return outline.wasOutlineTooComplex();
	}

private:
	/* 拷贝构造函数 */
	Rasterizer(const Rasterizer&);
	/* 赋值运算符重载 */
	const Rasterizer& operator=(const Rasterizer&);

	Outline outline;         ///< 轮廓对象,用于存储和处理图形的轮廓信息
	FillingRule fillingRule; ///< 填充规则,用于确定如何填充图形的内部

	int16_t offsetX;	///< x坐标的偏移量
	int16_t offsetY;	///< y坐标的偏移量
	int16_t width;		///< 宽度,可能用于限制渲染的范围或优化性能
};

}

#endif

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

#ifndef TOUCHGFX_CANVASWIDGETRENDERER_HPP
#define TOUCHGFX_CANVASWIDGETRENDERER_HPP
#include <touchgfx/canvas_widget_renderer/Cell.hpp>
#include <touchgfx/hal/Types.hpp>

namespace touchgfx
{
/**  
 * 用于图形绘制的类。此类管理由底层算法使用的内存。  
 * CanvasWidget不会动态分配内存,而是使用传递给CanvasWidgetRenderer的缓冲区中的内存。  
 * 当使用TouchGFX模拟器时,还可以获取关于使用CanvasWidgetRenderer进行绘制时实际使用的内存量的报告,以帮助调整缓冲区大小。  
 */
class CanvasWidgetRenderer
{
public:
	/**  
	 * 设置CanvasWidget使用的缓冲区。  
	 *  
	 * @param [in] buffer     为CanvasWidget保留的缓冲区。  
	 * @param      bufferSize 缓冲区的大小。  
	 */
	static void setupBuffer(uint8_t* buffer, unsigned bufferSize);

	/**  
	 * 移除之前通过setupBuffer()设置的缓冲区的引用。  
	 *  
	 * @see setupBuffer  
	 */
	static void resetBuffer();

	/**  
	 * 查询CanvasWidgetRenderer是否已使用缓冲区进行初始化。  
	 *  
	 * @return 如果已设置缓冲区,则返回true。  
	 */ 
	static bool hasBuffer();

	/**  
	 * 获取用于Outline中Cell对象的内存的指针。  
	 *  
	 * @return 内部由Outline使用的内存的指针。  
	 */
	static Cell* getOutlineBuffer();

	/**  
	 * 获取用于Outline中Cell对象的内存区域的大小。  
	 *  
	 * @return 内部由Outline使用的内存区域的大小。  
	 */
	static unsigned int getOutlineBufferSize();

private:
	static Cell *memoryBuffer;						//存储指向缓冲区的指针,该缓冲区用于保存绘图数据
	static unsigned int memoryBufferSize;	//存储缓冲区的大小
};

}

#endif

二、画布缓冲区

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

如果不通过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);

 具体操作如下:

#include <platform/hal/simulator/sdl2/HALSDL2.hpp>
#include <touchgfx/hal/NoDMA.hpp>
#include <common/TouchGFXInit.hpp>
#include <gui_generated/common/SimConstants.hpp>
#include <platform/driver/touch/SDL2TouchController.hpp>
#include <touchgfx/lcd/LCD.hpp>
#include <stdlib.h>
#include <simulator/mainBase.hpp>
#include <touchgfx/canvas_widget_renderer/CanvasWidgetRenderer.hpp>

using namespace touchgfx;

#ifdef __linux__
int main(int argc, char** argv)
{
#else
#include <shellapi.h>
#ifdef _UNICODE
#error Cannot run in unicode mode
#endif
int CALLBACK WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nCmdShow)
{
    int argc;
    char** argv = touchgfx::HALSDL2::getArgv(&argc);
#endif

    touchgfx::NoDMA dma; //For windows/linux, DMA transfers are simulated
    LCD& lcd = setupLCD();
    touchgfx::SDL2TouchController tc;

    touchgfx::HAL& hal = touchgfx::touchgfx_generic_init<touchgfx::HALSDL2>(dma, lcd, tc, SIM_WIDTH, SIM_HEIGHT, 0, 0);
	
    setupSimulator(argc, argv, hal);

    // Ensure there is a console window to print to using printf() or
    // std::cout, and read from using e.g. fgets or std::cin.
    // Alternatively, instead of using printf(), always use
    // touchgfx_printf() which will ensure there is a console to write
    // to.
    //touchgfx_enable_stdio();
	CanvasWidgetRenderer::setWriteMemoryUsageReport(true);
	
    touchgfx::HAL::getInstance()->taskEntry(); //Never returns

    return EXIT_SUCCESS;
}

三、画笔

TouchGFX支持许多画布控件,但是就像普通控件一样,您可以创自定义画布控件来满足您的需求。 画布控件定义要通过CWR绘制的图形的几何形状,而图形中每个像素的实际颜色则由相关Painter类定义。 同样地,TouchGFX自带许多Painter,但是您也可以创建自定义Painter来满足您的需求。

#ifndef TOUCHGFX_ABSTRACTPAINTER_HPP
#define TOUCHGFX_ABSTRACTPAINTER_HPP
#include <string.h>
#include <touchgfx/Bitmap.hpp>
#include <touchgfx/hal/HAL.hpp>
#include <touchgfx/hal/Types.hpp>
#include <touchgfx/lcd/LCD.hpp>
#include <touchgfx/transforms/DisplayTransformation.hpp>

namespace touchgfx
{
/* 绘制canvas widget的画笔类的抽象接口类。
 * 所有的canvas widgets都需要一个画笔来填充由CanvasWidgetRenderer绘制的形状。  
 * 画笔必须提供在给定坐标上的像素颜色,这将根据canvas widget的位置和给定像素的透明度混合到帧缓冲区中。  
 */
class AbstractPainter
{
public:
	/* 构造函数 */
	AbstractPainter() : widgetWidth(0)
	{
	}

	/* 析构函数 */
	virtual ~AbstractPainter()
	{
	}

	/* 画笔适用的渲染方法 */ 
	virtual HAL::RenderingMethod getRenderingMethod() const
	{
		return HAL::SOFTWARE;
	}

	/* 在实际开始任何绘制操作之前,都会调用此函数。画笔初始化变量和可选的dma队列 */
	virtual bool setup(const Rect& widgetRect) const
	{
		widgetWidth = widgetRect.width;
		return true;
	}

	/* 在所有绘制操作完成后,都会调用此函数。画笔关闭dma队列等 */
	virtual void tearDown() const
	{
		return;
	}

	/**  
	 * 将帧缓冲区widget坐标转换为显示widget坐标。这在创建自定义画笔并需要在与显示上的widget相同的坐标系中获取X,Y时非常有用。  
	 *  
	 * @param [in,out] widgetX widget的x坐标。  
	 * @param [in,out] widgetY widget的y坐标。  
	 *  
	 * @see AbstractPainter::paint  
	 */
	void framebufferToDisplay(int16_t& widgetX, int16_t& widgetY) const
	{
		if(HAL::DISPLAY_ROTATION == rotate0)
		{
			return;
		}
		const int16_t tmpX = widgetX;
		widgetX = (widgetWidth - 1) - widgetY;
		widgetY = tmpX;
	}

	/**  
	 * 在帧缓冲区中绘制一系列具有相同alpha值的像素。要绘制的第一个像素相对于'destination'在'offset'位置(为了支持1bpp,2bpp和4bpp)。  
	 * 要绘制的第一个像素位于给定的'widgetX','widgetY'坐标。要绘制的像素数量是'count',要应用的alpha是'alpha'。  
	 * 请注意,widgetX,widgetY相对于帧缓冲区中的widget,而不是显示。要将坐标转换为显示widget坐标,请使用framebufferToDisplay()。  
	 *  
	 * @param [in] destination 如果非空,则为目标指针。  
	 * @param      offset      要添加到目标的偏移量。  
	 * @param      widgetX     widget的x坐标。  
	 * @param      widgetY     widget的y坐标。  
	 * @param      count       要绘制的像素数。  
	 * @param      alpha       像素的alpha值。  
	 *  
	 * @see AbstractPainter::setup, AbstractPainter::tearDown, AbstractPainter::framebufferToDisplay  
	 */  
	virtual void paint(uint8_t* destination, int16_t offset, int16_t widgetX, int16_t widgetY, int16_t count, uint8_t alpha) const = 0;

protected:
	/**  
	 * 帮助函数,用于检查提供的位图格式是否与当前帧缓冲区格式匹配。  
	 *  
	 * @param  format 位图格式。  
	 *  
	 * @return 如果格式与帧缓冲区格式匹配,则返回true;否则返回false。  
	 */
	FORCE_INLINE_FUNCTION static bool compatibleFramebuffer(Bitmap::BitmapFormat format)
	{
		bool compat = HAL::lcd().framebufferFormat() == format;
		if (HAL::getInstance()->getAuxiliaryLCD())
		{
			compat |= HAL::getInstance()->getAuxiliaryLCD()->framebufferFormat() == format;
		}
		return compat;
	}

	mutable int16_t widgetWidth; ///< 屏幕上widget的宽度,由framebufferToDisplay()使用
};

}

#endif
 RGB565颜色画笔
#ifndef TOUCHGFX_ABSTRACTPAINTERCOLOR_HPP
#define TOUCHGFX_ABSTRACTPAINTERCOLOR_HPP
#include <touchgfx/hal/Types.hpp>

namespace touchgfx
{
/* 颜色画笔抽象接口类 */
class AbstractPainterColor
{
public:
	/* 构造函数 */
	AbstractPainterColor(colortype color = 0)
	{
		setColor(color);	//颜色
	}

	/* 析构函数 */
	virtual ~AbstractPainterColor()
	{
	}

	/* 设置颜色 */
	virtual void setColor(colortype color)
	{
		painterColor = color;
	}

	/* 获取颜色 */
	colortype getColor() const
	{
		return painterColor;
	}

protected:
	colortype painterColor; //颜色
};

}

#endif
#ifndef TOUCHGFX_ABSTRACTPAINTERRGB565_HPP
#define TOUCHGFX_ABSTRACTPAINTERRGB565_HPP
#include <touchgfx/Bitmap.hpp>
#include <touchgfx/hal/Types.hpp>
#include <touchgfx/lcd/LCD.hpp>
#include <touchgfx/widgets/canvas/AbstractPainter.hpp>

namespace touchgfx
{
/*  
 * AbstractPainterRGB565类是为在RGB565显示屏上使用CanvasWidgetRenderer绘图创建画笔的抽象类。  
 * 它继承自AbstractPainter。  
 */
class AbstractPainterRGB565 : public AbstractPainter
{
public:
	// RGB565格式的颜色掩码  
	static const uint16_t RMASK = 0xF800; ///< 红色掩码   (1111100000000000)  
	static const uint16_t GMASK = 0x07E0; ///< 绿色掩码   (0000011111100000)  
	static const uint16_t BMASK = 0x001F; ///< 蓝色掩码   (0000000000011111)

	// 构造函数  
	AbstractPainterRGB565()  
			: AbstractPainter()  
	{  
		// 断言检查:确保当前画笔只与RGB565显示屏一起工作  
		assert(compatibleFramebuffer(Bitmap::RGB565) && "The chosen painter only works with RGB565 displays");  
	}

	/*    
	 * 使用给定的alpha混合新像素和缓冲区像素的颜色。  
	 *   
	 * 参数:  
	 *      newpix: 新像素的值。  
	 *      bufpix: 缓冲区像素的值。  
	 *      alpha: 要应用于新像素的alpha值。  
	 *   
	 * 返回:  
	 *      混合两种颜色后的新颜色值。  
	 */
	FORCE_INLINE_FUNCTION uint16_t alphaBlend(uint16_t newpix, uint16_t bufpix, uint8_t alpha) const
	{
		return alphaBlend(newpix & RMASK, newpix & GMASK, newpix & BMASK, bufpix, alpha);
	}

	/*    
	 * 使用给定的alpha混合新像素和缓冲区像素的颜色(重载版本)。  
	 *   
	 * 参数:  
	 *      R: 红色颜色值(0-31,需要左移到RMASK的位置)。  
	 *      G: 绿色颜色值(0-63,需要左移到GMASK的位置)。  
	 *      B: 蓝色颜色值(0-31,需要左移到BMASK的位置)。  
	 *      bufpix: 缓冲区像素的值。  
	 *      alpha: R、G、B的alpha值。  
	 *   
	 * 返回:  
	 *      混合两种颜色后的新颜色值。  
	 */
	FORCE_INLINE_FUNCTION uint16_t alphaBlend(uint16_t R, uint16_t G, uint16_t B, uint16_t bufpix, uint8_t alpha) const
	{
		const uint8_t ialpha = 0xFF - alpha;
		return (((R * alpha + (bufpix & RMASK) * ialpha) / 255) & RMASK) |
					 (((G * alpha + (bufpix & GMASK) * ialpha) / 255) & GMASK) |
					 (((B * alpha + (bufpix & BMASK) * ialpha) / 255) & BMASK);
	}
};

}

#endif
#ifndef TOUCHGFX_PAINTERRGB565_HPP
#define TOUCHGFX_PAINTERRGB565_HPP
#include <platform/driver/lcd/LCD16bpp.hpp>
#include <touchgfx/hal/Types.hpp>
#include <touchgfx/widgets/canvas/AbstractPainterColor.hpp>
#include <touchgfx/widgets/canvas/AbstractPainterRGB565.hpp>

namespace touchgfx
{
/**  
 * PainterRGB565 类允许使用给定的颜色和透明度填充形状。  
 * 这使得可以绘制透明和反锯齿的元素。  
 *  
 * @see AbstractPainter  
 */
class PainterRGB565 : public AbstractPainterRGB565, public AbstractPainterColor
{
public:
	/* 构造函数  
	 * @param color (可选) 颜色,默认为黑色。这里的颜色应该是RGB565格式。  
	 */
	PainterRGB565(colortype color = 0) : AbstractPainterRGB565(), AbstractPainterColor(color)
	{
	}

	/* 设置画笔的颜色 */ 
	virtual void setColor(colortype color)
	{
		AbstractPainterColor::setColor(color);
		color565 = LCD16bpp::getNativeColor(painterColor);
	}

	/**  
	 * 在目标缓冲区上绘制具有指定颜色、透明度和像素数量的线条。  
	 *  
	 * @param destination 目标缓冲区的指针。  
	 * @param offset 目标缓冲区中的偏移量。  
	 * @param widgetX (未使用) 小部件的x坐标(此实现中忽略)。  
	 * @param widgetY (未使用) 小部件的y坐标(此实现中忽略)。  
	 * @param count 要绘制的像素数量。  
	 * @param alpha 透明度值,用于混合。  
	 */
	virtual void paint(uint8_t* destination, int16_t offset, int16_t widgetX, int16_t widgetY, int16_t count, uint8_t alpha) const;

	/* 释放画笔资源 */
	virtual void tearDown() const;

	/* 获取此画笔使用的渲染方法
	 * @return 渲染方法(硬件或软件)。  
	 */
	virtual HAL::RenderingMethod getRenderingMethod() const
	{
		return HAL::getInstance()->getDMAType() == DMA_TYPE_CHROMART ? HAL::HARDWARE : HAL::SOFTWARE;
	}

protected:
	uint16_t color565; ///< 以565格式存储的本地颜色
};

}

#endif
#include <touchgfx/hal/Paint.hpp>
#include <touchgfx/widgets/canvas/PainterRGB565.hpp>

namespace touchgfx
{
// paint 函数的实现  
void PainterRGB565::paint(uint8_t* destination, int16_t offset, int16_t /*widgetX*/, int16_t /*widgetY*/, int16_t count, uint8_t alpha) const  
{  
	// 将目标缓冲区指针转换为16位指针,并加上偏移量  
	uint16_t* const framebuffer = reinterpret_cast<uint16_t*>(destination) + offset;  
	// 调用 paint::rgb565::lineFromColor 函数来绘制线条,传入转换后的帧缓冲区、像素数量、颜色、透明度和本地存储的565格式颜色  
	paint::rgb565::lineFromColor(framebuffer, count, painterColor, alpha, color565);  
}  

// tearDown 函数的实现  
void PainterRGB565::tearDown() const  
{  
	// 调用 paint 命名空间的 tearDown 函数来释放资源(这里假设有这样的函数存在)  
	paint::tearDown();  
}

}
RGB565位图画笔
#ifndef TOUCHGFX_ABSTRACTPAINTERBITMAP_HPP
#define TOUCHGFX_ABSTRACTPAINTERBITMAP_HPP
#include <touchgfx/Bitmap.hpp>
#include <touchgfx/hal/HAL.hpp>
#include <touchgfx/hal/Types.hpp>
#include <touchgfx/transforms/DisplayTransformation.hpp>

namespace touchgfx
{
/* 位图画笔抽象接口类 */
class AbstractPainterBitmap
{
public:
	/* 构造函数 */
	AbstractPainterBitmap(const Bitmap& bmp = Bitmap(BITMAP_INVALID))
			: bitmap(bmp), bitmapFormat(), bitmapRect(), bitmapData(0), xOffset(0), yOffset(0), isTiled(false)
	{
		setBitmap(bmp);
	}

	/* 析构函数 */
	virtual ~AbstractPainterBitmap()
	{
	}

	/* 设置位图 */
	virtual void setBitmap(const Bitmap& bmp)
	{
		bitmap = bmp;
		bitmapFormat = bitmap.getFormat();
		bitmapRect = bitmap.getRect();
		DisplayTransformation::transformDisplayToFrameBuffer(bitmapRect);
		bitmapData = bitmap.getData();
	}

	/* 获取位图 */
	Bitmap getBitmap() const
	{
		return bitmap;
	}

	/* 设置是否平铺 */
	virtual void setTiled(bool tiled)
	{
		isTiled = tiled;
	}

	/* 获取是否平铺 */
	bool getTiled() const
	{
		return isTiled;
	}

	/* 设置位图偏移 */
	virtual void setOffset(int16_t x, int16_t y)
	{
		xOffset = x;
		yOffset = y;
	}

	/* 获取位图偏移 */
	void getOffset(int16_t& x, int16_t& y)
	{
		x = xOffset;
		y = yOffset;
	}

protected:
	Bitmap bitmap;                     //位图
	Bitmap::BitmapFormat bitmapFormat; //位图格式
	Rect bitmapRect;                   //位图尺寸和位置(在framebuffer中)
	const uint8_t *bitmapData;         //位图数据
	int16_t xOffset;                   //位图中X偏移
	int16_t yOffset;                   //位图中Y偏移
	bool isTiled;                      //是否平铺

	// 根据widgetWidth更新位图偏移量,这些偏移量由位图画笔使用,以调整坐标位置在控件内部
	void updateBitmapOffsets(int16_t widgetWidth) const
	{
		xDelta = (HAL::DISPLAY_ROTATION == rotate0) ? xOffset : yOffset;
		yDelta = (HAL::DISPLAY_ROTATION == rotate0) ? yOffset : -(xOffset + (widgetWidth - bitmap.getWidth()));
	}

	// 调整位图的x和y。对于平铺的位图画笔,坐标被固定在位图内部,对于非平铺的位图画笔,  
	// 如果整个范围(考虑偏移量和计数)都在位图外部,则返回false - 否则,参数将被调整,  
	// 以确保可以从给定的坐标和计数像素前方的位图中读取所请求的像素数。  
	//  
	// @param [in,out] bitmapX 位图的x坐标。  
	// @param [in,out] bitmapY 位图的y坐标。  
	// @param [in,out] offset  指针的x坐标。  
	// @param [in,out] count   数量。  
	//  
	// @returns 如果应该绘制任何内容,则返回true,否则返回false。  
	bool adjustBitmapXY(int16_t& bitmapX, int16_t& bitmapY, int16_t& offset, int16_t& count) const
	{
		bitmapX += xDelta;
		bitmapY += yDelta;
		if (isTiled)
		{
			bitmapX %= bitmapRect.width;
			if (bitmapX < 0)
			{
					bitmapX += bitmapRect.width;
			}
			bitmapY %= bitmapRect.height;
			if (bitmapY < 0)
			{
					bitmapY += bitmapRect.height;
			}
			return true;
		}

		if (bitmapX < 0)
		{
			if (bitmapX + count <= 0)
			{
					return false;
			}
			count += bitmapX;
			offset -= bitmapX;
			bitmapX = 0;
		}
		
		if ((bitmapX >= bitmapRect.width) || (bitmapY < 0) || (bitmapY >= bitmapRect.height))
		{
			return false;
		}
		
		if (bitmapX + (int)count > bitmapRect.width)
		{
			count = bitmapRect.width - bitmapX;
		}
		
		return true;
	}

private:
	mutable int16_t xDelta; //可变的x方向上的差值,用于调整位图坐标  
	mutable int16_t yDelta; //可变的y方向上的差值,用于调整位图坐标
};

}

#endif
#ifndef TOUCHGFX_PAINTERRGB565BITMAP_HPP
#define TOUCHGFX_PAINTERRGB565BITMAP_HPP
#include <touchgfx/Bitmap.hpp>
#include <touchgfx/hal/Types.hpp>
#include <touchgfx/transforms/DisplayTransformation.hpp>
#include <touchgfx/widgets/canvas/AbstractPainterBitmap.hpp>
#include <touchgfx/widgets/canvas/AbstractPainterRGB565.hpp>

namespace touchgfx
{
/* PainterRGB565Bitmap类将从位图中获取给定点的颜色 */ 
class PainterRGB565Bitmap : public AbstractPainterRGB565, public AbstractPainterBitmap
{
public:
	/* 构造函数 */
	PainterRGB565Bitmap(const Bitmap& bmp = Bitmap(BITMAP_INVALID))
			: AbstractPainterRGB565(), AbstractPainterBitmap(bmp)
	{
	}

	/* 设置位图 */  
	virtual void setBitmap(const Bitmap& bmp);

	/* 设置绘制环境,包括检查位图是否有效等
	 * @param widgetRect widget的矩形区域。  
	 * @return 如果设置成功则返回true,否则返回false。  
	 */ 
	virtual bool setup(const Rect& widgetRect) const
	{
		if (!AbstractPainterRGB565::setup(widgetRect))
		{
				return false;
		}
		updateBitmapOffsets(widgetWidth);
		return bitmap.getId() != BITMAP_INVALID;
	}

	/**  
	 * 在给定的目标缓冲区上绘制位图。  
	 *   
	 * @param destination 目标缓冲区。  
	 * @param offset 目标缓冲区中的偏移量。  
	 * @param widgetX widget的X坐标。  
	 * @param widgetY widget的Y坐标。  
	 * @param count 要绘制的像素数量。  
	 * @param alpha 透明度值(0-255)。  
	 */
	virtual void paint(uint8_t* destination, int16_t offset, int16_t widgetX, int16_t widgetY, int16_t count, uint8_t alpha) const;
  
	/* 清理绘制环境,例如释放资源等 */ 
	virtual void tearDown() const;

	/* 获取渲染方法(硬件渲染或软件渲染) */
	virtual HAL::RenderingMethod getRenderingMethod() const
	{
		return HAL::getInstance()->getDMAType() == DMA_TYPE_CHROMART ? HAL::HARDWARE : HAL::SOFTWARE;
	}

protected:
	const uint8_t* bitmapExtraData; ///< 指向位图额外数据的指针
};

}

#endif
#include <platform/driver/lcd/LCD16bpp.hpp>
#include <touchgfx/Color.hpp>
#include <touchgfx/hal/Paint.hpp>
#include <touchgfx/lcd/LCD.hpp>
#include <touchgfx/widgets/canvas/PainterRGB565Bitmap.hpp>

namespace touchgfx
{
/* 设置位图 */
void PainterRGB565Bitmap::setBitmap(const Bitmap& bmp)
{
	AbstractPainterBitmap::setBitmap(bmp);
	assert((bitmap.getId() == BITMAP_INVALID || bitmapFormat == Bitmap::RGB565 || bitmapFormat == Bitmap::ARGB8888) && "PainterRGB565Bitmap only works with RGB565 and ARGB8888 bitmaps");
	assert(bitmap.getId() == BITMAP_INVALID || bitmapData);
	bitmapExtraData = bitmap.getExtraData();
}

/**  
 * 在给定的目标缓冲区上绘制位图。  
 *   
 * @param destination 目标缓冲区。  
 * @param offset 目标缓冲区中的偏移量。  
 * @param widgetX widget的X坐标。  
 * @param widgetY widget的Y坐标。  
 * @param count 要绘制的像素数量。  
 * @param alpha 透明度值(0-255)。  
 */
void PainterRGB565Bitmap::paint(uint8_t* destination, int16_t offset, int16_t widgetX, int16_t widgetY, int16_t count, uint8_t alpha) const  
{  
	// 调整位图的坐标和偏移,如果调整失败则直接返回  
	if (!adjustBitmapXY(widgetX, widgetY, offset, count))  
	{  
		return;  
	}  

	// 转换目标地址为16位指针,并计算行的结束地址  
	uint16_t* RESTRICT framebuffer = reinterpret_cast<uint16_t*>(destination) + offset;  
	const uint16_t* const lineEnd = framebuffer + count;  

	// 计算跳过的行数和位图在当前行的可用宽度  
	const int32_t rowSkip = widgetY * bitmapRect.width;  
	int16_t bitmapAvailable = bitmapRect.width - widgetX;  

	// 如果位图格式为RGB565  
	if (bitmapFormat == Bitmap::RGB565)  
	{  
		// 计算位图数据的起始地址和当前行的指针  
		const uint16_t* const bitmapLineStart = reinterpret_cast<const uint16_t*>(bitmapData) + rowSkip;  
		const uint16_t* bitmapPointer = bitmapLineStart + widgetX;  

		// 如果存在额外的alpha通道数据  
		if (bitmapExtraData)  
		{  
			// 计算alpha数据的起始地址和当前行的指针  
			const uint8_t* const alpha_linestart = bitmapExtraData + rowSkip;  
			const uint8_t* alphaPointer = alpha_linestart + widgetX;  

			// 逐行绘制位图  
			do  
			{  
				// 计算当前行要绘制的像素数量  
				const int16_t length = MIN(bitmapAvailable, count);  
				count -= length;  

				// 逐像素绘制  
				do  
				{  
					// 计算alpha值  
					const uint8_t a = LCD::div255(alpha * (*alphaPointer++));  

					// 如果alpha值为255,则直接复制位图数据  
					if (a == 0xFF)  
					{  
						*framebuffer = *bitmapPointer;  
					}  
					// 如果alpha值不为0,则进行alpha混合  
					else if (a)  
					{  
						*framebuffer = alphaBlend(*bitmapPointer, *framebuffer, a);  
					}  

					// 移动位图指针和帧缓冲区指针  
					bitmapPointer++;  
				} while (++framebuffer < chunkend);  

				// 重置位图和alpha的指针到行的起始位置  
				bitmapPointer = bitmapLineStart;  
				alphaPointer = alpha_linestart;  
				bitmapAvailable = bitmapRect.width;  

			} while (framebuffer < lineEnd);  
		}  
		else // 如果没有额外的alpha通道数据  
		{  
			// 逐行绘制位图  
			do  
			{  
				// 计算当前行要绘制的像素数量  
				const int16_t length = MIN(bitmapAvailable, count);  
				count -= length;  

				// 调用外部函数绘制一行RGB565数据  
				paint::rgb565::lineFromRGB565(framebuffer, bitmapPointer, length, alpha);  

				// 移动帧缓冲区指针和位图指针  
				framebuffer += length;  
				bitmapPointer = bitmapLineStart;  
				bitmapAvailable = bitmapRect.width;  

			} while (framebuffer < lineEnd);  
		}  
	}  
	else // 如果位图格式为ARGB8888  
	{  
		// 计算位图数据的起始地址和当前行的指针  
		const uint32_t* const bitmapLineStart = reinterpret_cast<const uint32_t*>(bitmapData) + rowSkip;  
		const uint32_t* bitmapPointer = bitmapLineStart + widgetX;  

		// 逐行绘制位图  
		do  
		{  
			// 计算当前行要绘制的像素数量  
			const int16_t length = MIN(bitmapAvailable, count);  
			count -= length;  

			// 调用外部函数绘制一行ARGB8888数据到RGB565  
			paint::rgb565::lineFromARGB8888(framebuffer, bitmapPointer, length, alpha);  

			// 移动帧缓冲区指针和位图指针  
			framebuffer += length;  
			bitmapPointer = bitmapLineStart;  
			bitmapAvailable = bitmapRect.width;  
		} while (framebuffer < lineEnd);  
	}  
}

/* 清理绘制环境,例如释放资源等 */ 
void PainterRGB565Bitmap::tearDown() const
{
	paint::tearDown();
}

}

 四、画布类

 一个便于使用CanvasWidgetRenderer进行渲染的类。Canvas类将使得实现一个新的CanvasWidget变得非常简单。它提供了几个简单的图形基元, 允许移动一个“画笔”并绘制一个形状的轮廓,然后可以进行渲染。  

#ifndef TOUCHGFX_CANVAS_HPP
#define TOUCHGFX_CANVAS_HPP
#include <touchgfx/hal/Types.hpp>
#include <touchgfx/widgets/canvas/CWRUtil.hpp>
#include <touchgfx/widgets/canvas/CanvasWidget.hpp>

namespace touchgfx
{
/**  
 * 一个便于使用CanvasWidgetRenderer进行渲染的类。  
 *   
 * Canvas类将使得实现一个新的CanvasWidget变得非常简单。它提供了几个简单的图形基元,  
 * 允许移动一个“画笔”并绘制一个形状的轮廓,然后可以进行渲染。  
 *   
 * Canvas类已经被优化,以消除在当前无效矩形外部绘制不必要的线条。  
 */
class Canvas
{
public:
	/**  
	 * Canvas构造函数。锁定帧缓冲区,并准备只在允许的区域内进行绘制,该区域已被标记为无效。  
	 * 同时会考虑LCD的颜色深度。  
	 *   
	 * @param  painter              用于在画布上绘制的画笔对象。  
	 * @param  canvasAreaAbs        画布在绝对坐标中的尺寸。  
	 * @param  invalidatedAreaRel   在画布区域的相对坐标中应更新的区域。  
	 * @param  globalAlpha          在画布上绘制时使用的alpha值。
	 *   
	 * @note 会锁定帧缓冲区。  
	 */
	Canvas(const AbstractPainter* const painter, const Rect& canvasAreaAbs, const Rect& invalidatedAreaRel, uint8_t globalAlpha);

	/**  
	 * Canvas析构函数。负责清理和释放Canvas对象在其生命周期中分配的资源。  
	 *   
	 * @note 会解锁帧缓冲区。  
	 */
	virtual ~Canvas()
	{
	}

	/**  
	 * 设置渲染轮廓时要使用的填充规则。  
	 *   
	 * @param  rule 填充规则。  
	 *   
	 * @see getFillingRule  
	 */
	void setFillingRule(Rasterizer::FillingRule rule)
	{
		rasterizer.setFillingRule(rule);
	}

	/**  
	 * 获取渲染轮廓时正在使用的填充规则。  
	 *   
	 * @return 填充规则。  
	 *   
	 * @see setFillingRule  
	 */
	Rasterizer::FillingRule getFillingRule() const
	{
		return rasterizer.getFillingRule();
	}

	/**  
	 * 将当前画笔位置移动到(x, y)。如果画笔在绘制区域之外,则不执行任何操作,但会保存坐标,  
	 * 以防下一个操作是lineTo一个位于(或在相反侧)绘制区域内的坐标。  
	 *   
	 * @param  x 画笔位置的x坐标,采用CWRUtil::Q5格式。  
	 * @param  y 画笔位置的y坐标,采用CWRUtil::Q5格式。  
	 */
	void moveTo(CWRUtil::Q5 x, CWRUtil::Q5 y);

	/**  
	 * 从当前的(x, y)绘制一条线到新的(x, y)作为正在绘制的形状的一部分。与moveTo一样,完全在绘制区域外的lineTo命令将被丢弃。  
	 *   
	 * @param  x 画笔位置的x坐标,采用CWRUtil::Q5格式。  
	 * @param  y 画笔位置的y坐标,采用CWRUtil::Q5格式。  
	 *   
	 * @see CWRUtil::Q5, moveTo  
	 */
	virtual void lineTo(CWRUtil::Q5 x, CWRUtil::Q5 y);

	/**  
	 * 通过x1,y1绘制一个二次贝塞尔曲线到x2,y2。  
	 *   
	 * @param  x0 起始x坐标。  
	 * @param  y0 起始y坐标。  
	 * @param  x1 'via'点的x坐标。  
	 * @param  y1 'via'点的y坐标。  
	 * @param  x  结束x坐标。  
	 * @param  y  结束y坐标。  
	 */ 
	void quadraticBezierTo(float x0, float y0, const float x1, const float y1, const float x, const float y)
	{
		recursiveQuadraticBezier(x0, y0, x1, y1, x, y, 0);
		lineTo(CWRUtil::toQ5<float>(x), CWRUtil::toQ5<float>(y));
	}

	/**  
	 * 通过x1,y1和x2,y2绘制一个三次贝塞尔曲线到x3,y3。  
	 *   
	 * @param  x0 起始x坐标。  
	 * @param  y0 起始y坐标。  
	 * @param  x1 第一个'via'点的x坐标。  
	 * @param  y1 第一个'via'点的y坐标。  
	 * @param  x2 第二个'via'点的x坐标。  
	 * @param  y2 第二个'via'点的y坐标。  
	 * @param  x  结束x坐标。  
	 * @param  y  结束y坐标。  
	 */
	void cubicBezierTo(float x0, float y0, float x1, float y1, float x2, float y2, float x, float y)
	{
		recursiveCubicBezier(x0, y0, x1, y1, x2, y2, x, y, 0);
		lineTo(CWRUtil::toQ5<float>(x), CWRUtil::toQ5<float>(y));
	}

	/**  
	 * 关闭当前形状,以便可以使用Painter填充内部。  
	 *   
	 * @return 如果有足够的内存来计算形状轮廓,则返回true;如果内存不足,则返回false。  
	 */
	bool close();

		/**  
		 * 将当前笔位置移动到(x, y)。如果笔在绘图区域的外部(上方或下方),  
		 * 则不执行任何操作,但会保存坐标,以防下一个操作是lineTo一个位于绘图区域内  
		 * (或在相对的另一侧)的坐标。  
		 *   
		 * @tparam T 类型为int或float。  
		 * @param  x 笔位置的x坐标。  
		 * @param  y 笔位置的y坐标。  
		 */
	template <typename T>
	void moveTo(T x, T y)
	{
		moveTo(CWRUtil::toQ5<T>(x), CWRUtil::toQ5<T>(y));
	}

	/**  
	 * 从当前的(x, y)绘制线条到新的(x, y)作为正在绘制的形状的一部分。对于  
	 * moveTo,完全在绘图区域外部的lineTo命令将被丢弃。  
	 *   
	 * @tparam T 类型为int或float。  
	 * @param  x 笔位置的x坐标。  
	 * @param  y 笔位置的y坐标。  
	 */ 
	template <typename T>
	void lineTo(T x, T y)
	{
		lineTo(CWRUtil::toQ5<T>(x), CWRUtil::toQ5<T>(y));
	}

	/**  
	 * 使用给定的Painter渲染使用moveTo()和lineTo()绘制的图形形状。  
	 * 形状会自动闭合,即自动插入一个lineTo()连接初始moveTo()命令中给定的  
	 * 初始笔位置与当前笔位置。  
	 *   
	 * @param  customAlpha (可选)应用于整个画布的Alpha。如果画布是  
	 *                      更复杂容器设置的一部分,需要进行淡入淡出处理,那么这将非常有用。  
	 *                      默认是实心的。  
	 *   
	 * @return 如果小部件已渲染,则为true;如果内存不足无法渲染小部件,则为false。  
	 */
	bool render(uint8_t customAlpha = 255);

	/**  
	 * 确定轮廓是否过于复杂而无法绘制完整。  
	 *   
	 * @return 如果过于复杂,则为True;否则为False。  
	 */
	FORCE_INLINE_FUNCTION bool wasOutlineTooComplex()
	{
		return rasterizer.wasOutlineTooComplex();
	}

private:
	// 用于绘制的变量  
	const AbstractPainter *const canvasPainter;	// 指向画笔的指针,用于实际的绘制操作  
	int16_t canvasAreaWidth;  									// 画布区域的宽度  
	uint8_t canvasAlpha;												// 画布的透明度  
	Rect dirtyAreaAbsolute;											// 需要重绘的绝对区域  
	Rasterizer rasterizer;											// 光栅化器,用于将形状转换为可在屏幕上绘制的像素  

	// 无效区域在Q5坐标中的变量  
	CWRUtil::Q5 invalidatedAreaX;  			// 无效区域的x坐标  
	CWRUtil::Q5 invalidatedAreaY;  			// 无效区域的y坐标  
	CWRUtil::Q5 invalidatedAreaWidth;		// 无效区域的宽度  
	CWRUtil::Q5 invalidatedAreaHeight;	// 无效区域的高度  

	// 用于优化绘图算法的变量  
	bool isPenDown;							// 当前笔是否按下(即是否正在绘制)  
	bool wasPenDown;						// 上一次笔是否按下  
	CWRUtil::Q5 previousX;			// 上一个点的x坐标  
	CWRUtil::Q5 previousY;			// 上一个点的y坐标  
	uint8_t previousOutside;		// 上一个点是否在画布外部的标记  
	uint8_t penDownOutside;			// 笔按下时是否在画布外部的标记  
	CWRUtil::Q5 initialMoveToX;	// 初始moveTo命令的x坐标  
	CWRUtil::Q5 initialMoveToY;	// 初始moveTo命令的y坐标  

	// 用于标记点在画布外部的枚举  
	enum  
	{  
			POINT_IS_ABOVE = 1 << 0,  // 点在画布上方  
			POINT_IS_BELOW = 1 << 1,  // 点在画布下方  
			POINT_IS_LEFT = 1 << 2,   // 点在画布左侧  
			POINT_IS_RIGHT = 1 << 3   // 点在画布右侧  
	};  

	// 检查点是否在给定宽度和高度的画布外部的函数  
	FORCE_INLINE_FUNCTION uint8_t isOutside(const CWRUtil::Q5& x, const CWRUtil::Q5& y, const CWRUtil::Q5& width, const CWRUtil::Q5& height) const  
	{  
			// 使用位操作确定点的位置,并返回相应的标记组合  
			return ((y < 0) ? (POINT_IS_ABOVE) : (y >= height ? POINT_IS_BELOW : 0)) |  
						 ((x < 0) ? (POINT_IS_LEFT) : (x >= width ? POINT_IS_RIGHT : 0));  
	}  

	// 将帧缓冲区坐标转换为显示坐标的函数(此函数在代码段中未给出实现)  
	void transformFrameBufferToDisplay(CWRUtil::Q5& x, CWRUtil::Q5& y) const;  

	// 用于递归绘制二次和三次贝塞尔曲线的函数(这些函数在代码段中未给出实现)  
	void recursiveQuadraticBezier(const float x1, const float y1, const float x2, const float y2, const float x3, const float y3, const unsigned level);  
	void recursiveCubicBezier(const float x1, const float y1, const float x2, const float y2, const float x3, const float y3, const float x4, const float y4, const unsigned level);
};

}

#endif
#include <math.h>
#include <touchgfx/Bitmap.hpp>
#include <touchgfx/canvas_widget_renderer/CanvasWidgetRenderer.hpp>
#include <touchgfx/canvas_widget_renderer/Rasterizer.hpp>
#include <touchgfx/hal/HAL.hpp>
#include <touchgfx/transforms/DisplayTransformation.hpp>
#include <touchgfx/widgets/canvas/Canvas.hpp>

namespace touchgfx
{
// Canvas类的构造函数  
Canvas::Canvas(const AbstractPainter* const painter, const Rect& canvasAreaAbs, const Rect& invalidatedAreaRel, uint8_t globalAlpha)  
    : canvasPainter(painter),  // 初始化canvasPainter成员变量,它是一个指向AbstractPainter的指针,用于绘制  
      canvasAreaWidth(canvasAreaAbs.width),  // 初始化canvasAreaWidth成员变量,它表示画布的宽度  
      canvasAlpha(globalAlpha),  // 初始化canvasAlpha成员变量,它表示全局的透明度  
      rasterizer(),  // 初始化rasterizer成员变量,它是一个Rasterizer对象,用于光栅化(将图像转换为位图)  
      // 以下变量用于跟踪和管理画布上的无效区域  
      invalidatedAreaX(0),  
      invalidatedAreaY(0),  
      invalidatedAreaWidth(0),  
      invalidatedAreaHeight(0),  
      // 以下变量用于跟踪和管理画笔的状态  
      isPenDown(false),  
      wasPenDown(false),  
      previousX(0),  
      previousY(0),  
      previousOutside(0),  
      penDownOutside(0),  
      initialMoveToX(0),  
      initialMoveToY(0)  
{  
	// 确保CanvasWidgetRenderer已经为绘制分配了缓冲区  
	assert(CanvasWidgetRenderer::hasBuffer() && "No buffer allocated for CanvasWidgetRenderer drawing");  
	// 确保Rasterizer的POLY_BASE_SHIFT设置与CanvasWidget的期望一致  
	assert(Rasterizer::POLY_BASE_SHIFT == 5 && "CanvasWidget assumes Q5 but Rasterizer uses a different setting");  

	// 计算绝对无效区域  
	Rect dirtyArea = invalidatedAreaRel;  
	dirtyAreaAbsolute = Rect(canvasAreaAbs.x + invalidatedAreaRel.x,  
													 canvasAreaAbs.y + invalidatedAreaRel.y,  
													 invalidatedAreaRel.width,  
													 invalidatedAreaRel.height);  

	// 如果显示屏与帧缓冲区相比有旋转,则需要转换矩形以匹配帧缓冲区的坐标  
	DisplayTransformation::transformDisplayToFrameBuffer(dirtyArea, canvasAreaAbs);  
	DisplayTransformation::transformDisplayToFrameBuffer(dirtyAreaAbsolute);  

	// 根据无效区域重新调整缓冲区大小  
	rasterizer.reset(dirtyArea.x, dirtyArea.y);  

	// 将无效区域的坐标和尺寸转换为Q5格式(固定点数)  
	invalidatedAreaX = CWRUtil::toQ5<int>(dirtyArea.x);  
	invalidatedAreaY = CWRUtil::toQ5<int>(dirtyArea.y);  
	invalidatedAreaWidth = CWRUtil::toQ5<int>(dirtyArea.width);  
	invalidatedAreaHeight = CWRUtil::toQ5<int>(dirtyArea.height);  

	// 设置rasterizer的最大渲染尺寸  
	rasterizer.setMaxRender(dirtyAreaAbsolute.width, dirtyAreaAbsolute.height);  
}  
  
// 将画笔移动到指定的坐标位置  
void Canvas::moveTo(CWRUtil::Q5 x, CWRUtil::Q5 y)  
{  
	// 如果画笔当前处于按下状态  
	if (isPenDown)  
	{  
		// 尝试关闭当前路径  
		if (!close())  
		{  
			return;  
		}  
	}  

	// 将坐标从帧缓冲区转换到显示屏  
	transformFrameBufferToDisplay(x, y);  
	// 将坐标转换为相对于无效区域的偏移量  
	x -= invalidatedAreaX;  
	y -= invalidatedAreaY;  

	// 检查点是否在无效区域之外  
	const uint8_t outside = isOutside(x, y, invalidatedAreaWidth, invalidatedAreaHeight);  

	// 如果点在无效区域之外,则抬起画笔  
	if (outside)  
	{  
		isPenDown = false;  
	}  
	else  
	{  
		// 记录画笔按下的状态和外部标志  
		penDownOutside = outside;  
		rasterizer.moveTo(x, y);  // 在rasterizer中移动画笔到指定位置  
		isPenDown = true;  // 设置画笔为按下状态  
		wasPenDown = true;  // 记录画笔曾经被按下过  
	}  

	// 记录初始移动到的位置  
	initialMoveToX = x;  
	initialMoveToY = y;  

	// 更新前一个点的坐标和外部标志  
	previousX = x;  
	previousY = y;  
	previousOutside = outside;  
}  
  
// 从当前位置画一条线到指定的坐标位置  
void Canvas::lineTo(CWRUtil::Q5 x, CWRUtil::Q5 y)  
{  
	// 将坐标从帧缓冲区转换到显示屏  
	transformFrameBufferToDisplay(x, y);  
	// 将坐标转换为相对于无效区域的偏移量  
	x -= invalidatedAreaX;  
	y -= invalidatedAreaY;  

	// 检查点是否在无效区域之外  
	uint8_t outside = isOutside(x, y, invalidatedAreaWidth, invalidatedAreaHeight);  

	// 如果前一个点不在无效区域之外  
	if (!previousOutside)  
	{  
		rasterizer.lineTo(x, y);  // 在rasterizer中从当前位置画一条线到指定位置  
	}  
	else  
	{  
		// 如果当前点不在无效区域之外,或者当前点和前一个点不在无效区域的同一侧  
		if (!outside || !(previousOutside & outside))  
		{  
			// 如果画笔当前未按下,则需要将画笔移动到前一个点,并按下画笔  
			if (!isPenDown)  
			{  
				penDownOutside = previousOutside;  
				rasterizer.moveTo(previousX, previousY);  
				isPenDown = true;  
				wasPenDown = true;  
			}  
			else  
			{  
				// 如果画笔已经按下,则需要先将线画到前一个点  
				rasterizer.lineTo(previousX, previousY);  
			}  
			// 然后从前一个点画线到当前点  
			rasterizer.lineTo(x, y);  
		}  
		else  
		{  
			// 限制“外部”状态与前一个点保持在同一侧  
			outside &= previousOutside;  
		}  
	}  
	// 更新前一个点的坐标和外部标志  
	previousX = x;  
	previousY = y;  
	previousOutside = outside;  
}  
  
// 尝试关闭当前路径,如果路径太复杂则返回false  
bool Canvas::close()  
{  
	// 如果画笔当前处于按下状态  
	if (isPenDown)  
	{  
		// 如果前一个点和画笔按下的点在同一侧外部区域  
		if (previousOutside & penDownOutside)  
		{  
			// 不需要关闭路径,CWR会自动处理这种情况(注释中的代码已被省略)  
			//   lineTo(penDownX, penDownY);  
		}  
		else  
		{  
			// 如果前一个点在外部区域,则需要先将线画到前一个点  
			if (previousOutside)  
			{  
				rasterizer.lineTo(previousX, previousY);  
			}  
			// 然后将线画回画笔按下的点,以关闭路径  
			rasterizer.lineTo(initialMoveToX, initialMoveToY);  
		}  
	}  
	// 抬起画笔  
	isPenDown = false;  
	// 返回是否路径过于复杂而无法关闭的标志  
	return !rasterizer.wasOutlineTooComplex();  
}

// Canvas类的render方法,用于渲染画布。参数customAlpha表示自定义的透明度。  
bool Canvas::render(uint8_t customAlpha)  
{  
	// 计算最终的透明度值,它是canvasAlpha和customAlpha的乘积,再除以255的结果。  
	const uint8_t alpha = LCD::div255(canvasAlpha * customAlpha);  
		
	// 如果最终的透明度为0或者之前没有按下画笔,则直接返回true,表示没有需要渲染的内容。  
	if (alpha == 0 || !wasPenDown)  
	{  
		return true; // Nothing. Done  
	}  

	// 如果需要渲染的区域过宽,超过了为连续写入(CWR)分配的缓冲区,则重绘将不会起作用。  
	// 这种情况下,CanvasWidget需要知道这种情况,并可能尝试垂直划分区域,但这还没有实现。  
	if (!close())  
	{  
		return false;  
	}  

	// 创建渲染缓冲区,并获取其指针。  
	uint8_t* RESTRICT framebuffer = reinterpret_cast<uint8_t*>(HAL::getInstance()->lockFrameBufferForRenderingMethod(canvasPainter->getRenderingMethod()));  
		
	// 获取帧缓冲区的跨度(stride)。  
	const int stride = HAL::lcd().framebufferStride();  
		
	// 根据帧缓冲区的格式,调整framebuffer指针的位置,并计算x方向的调整值xAdjust。  
	uint8_t xAdjust = 0;  
	switch (HAL::lcd().framebufferFormat())  
	{  
		case Bitmap::BW: //... 各种情况的处理,省略了具体的代码注释,因为它们都是基于帧缓冲区格式的调整。  
		//...  
		case Bitmap::ARGB8888:  
			framebuffer += dirtyAreaAbsolute.x * 4 + dirtyAreaAbsolute.y * stride;  
			break;  
		case Bitmap::BW_RLE:  
		case Bitmap::A4:  
		case Bitmap::CUSTOM:  
			assert(false && "Unsupported bit depth"); // 对于不支持的位深度,触发断言。  
	}  
		
	// 调用rasterizer的render方法进行渲染,并获取渲染结果。之后拆卸画布画笔并解锁帧缓冲区。  
	const bool result = rasterizer.render(canvasPainter, framebuffer, stride, xAdjust, alpha);  
	canvasPainter->tearDown();  
	HAL::getInstance()->unlockFrameBuffer();  
	return result; // 返回渲染结果。  
}  
  
// Canvas类的transformFrameBufferToDisplay方法,用于将帧缓冲区坐标转换为显示坐标。参数x和y是Q5格式的坐标值。  
void Canvas::transformFrameBufferToDisplay(CWRUtil::Q5& x, CWRUtil::Q5& y) const  
{  
	// 如果显示旋转设置为90度,则交换x和y的值,并对x进行取反调整。  
	if (HAL::DISPLAY_ROTATION == rotate90)  
	{  
		CWRUtil::Q5 const tmpY = y;  
		y = CWRUtil::toQ5<int>(canvasAreaWidth) - x;  
		x = tmpY;  
	}  
}  
  
// 以下是一些用于二次贝塞尔曲线递归渲染的常量和方法的定义。其中curve_recursion_limit定义了递归的最大深度。  
#define curve_collinearity_epsilon 1e-10f // 定义共线性判断的阈值。  
#define curve_recursion_limit 8 // 定义递归的最大深度。  
#define m_distance_tolerance 0.25f // 定义距离容忍度的阈值。  
#define m_angle_tolerance 0.1f // 定义角度容忍度的阈值(注意:此处的定义实际上被注释掉了,可能需要在实际代码中进行定义或调整)。  

// Canvas类的recursiveQuadraticBezier方法,用于递归地渲染二次贝塞尔曲线。参数x1,y1,x2,y2,x3,y3定义了曲线的控制点和终点,level表示当前的递归深度。  
void Canvas::recursiveQuadraticBezier(const float x1, const float y1, const float x2, const float y2, const float x3, const float y3, const unsigned level)  
{  
	// 如果递归深度超过了定义的最大深度,则直接返回。  
	if (level > curve_recursion_limit)  
	{  
		return;  
	}  

	// 计算曲线上的中点坐标。  
	const float x12 = (x1 + x2) / 2; //... 省略了具体的代码注释,因为它们都是基于中点坐标的计算。  
	const float y123 = (y12 + y23) / 2;  

	// 计算曲线的起点和终点之间的差值。  
	float dx = x3 - x1;  
	float dy = y3 - y1;  
	// 计算曲线的“曲率”,如果曲率小于某个阈值,则认为曲线接近于直线。  
	const float d = abs(((x2 - x3) * dy - (y2 - y3) * dx));  

	// 如果曲率大于某个阈值,则进行正常的曲线处理;否则,进行共线性情况的处理。  
	if (d > curve_collinearity_epsilon) //... 省略了具体的代码注释,因为它们都是基于曲率和角度的判断以及递归调用。  
	//...  
	// 继续递归细分曲线。  
	recursiveQuadraticBezier(x1, y1, x12, y12, x123, y123, level + 1);  
	recursiveQuadraticBezier(x123, y123, x23, y23, x3, y3, level + 1);  
}

//#define m_cusp_limit 0.0f

// 递归绘制三次贝塞尔曲线  
// 参数:  
//   x1, y1: 第一个控制点的坐标  
//   x2, y2: 第二个控制点的坐标  
//   x3, y3: 第三个控制点的坐标  
//   x4, y4: 第四个控制点的坐标(终点)  
//   level: 递归的层级
void Canvas::recursiveCubicBezier(const float x1, const float y1, const float x2, const float y2, const float x3, const float y3, const float x4, const float y4, const unsigned level)
{
    if (level > curve_recursion_limit)
    {
        return;
    }

    // Calculate all the mid-points of the line segments
    //----------------------
    const float x12 = (x1 + x2) / 2;
    const float y12 = (y1 + y2) / 2;
    const float x23 = (x2 + x3) / 2;
    const float y23 = (y2 + y3) / 2;
    const float x34 = (x3 + x4) / 2;
    const float y34 = (y3 + y4) / 2;
    const float x123 = (x12 + x23) / 2;
    const float y123 = (y12 + y23) / 2;
    const float x234 = (x23 + x34) / 2;
    const float y234 = (y23 + y34) / 2;
    const float x1234 = (x123 + x234) / 2;
    const float y1234 = (y123 + y234) / 2;

    if (level > 0) // Enforce subdivision first time
    {
        // Try to approximate the full cubic curve by a single straight line
        //------------------
        float dx = x4 - x1;
        float dy = y4 - y1;

        const float d2 = abs(((x2 - x4) * dy - (y2 - y4) * dx));
        const float d3 = abs(((x3 - x4) * dy - (y3 - y4) * dx));

        float da1, da2;

        if (d2 > curve_collinearity_epsilon && d3 > curve_collinearity_epsilon)
        {
            // Regular care
            //-----------------
            if ((d2 + d3) * (d2 + d3) <= m_distance_tolerance * (dx * dx + dy * dy))
            {
                // If the curvature doesn't exceed the distance_tolerance value
                // we tend to finish subdivisions.
                //----------------------

                // FGC: lint does not like this
                // if (m_angle_tolerance < curve_angle_tolerance_epsilon)
                // {
                //     lineTo(x1234, y1234);
                //     return;
                // }

                // Angle & Cusp Condition
                //----------------------
                const float a23 = atan2f(y3 - y2, x3 - x2);
                da1 = abs(a23 - atan2f(y2 - y1, x2 - x1));
                da2 = abs(atan2f(y4 - y3, x4 - x3) - a23);
                if (da1 >= PI)
                {
                    da1 = 2 * PI - da1;
                }
                if (da2 >= PI)
                {
                    da2 = 2 * PI - da2;
                }

                if (da1 + da2 < m_angle_tolerance)
                {
                    // Finally we can stop the recursion
                    //----------------------
                    lineTo(x1234, y1234);
                    return;
                }

                // FGC: lint does not like this
                // if (m_cusp_limit != 0.0f)
                // {
                //     if (da1 > m_cusp_limit)
                //     {
                //         lineTo(x2, y2);
                //         return;
                //     }

                //     if (da2 > m_cusp_limit)
                //     {
                //         lineTo(x3, y3);
                //         return;
                //     }
                // }
            }
        }
        else
        {
            if (d2 > curve_collinearity_epsilon)
            {
                // p1,p3,p4 are collinear, p2 is considerable
                //----------------------
                if (d2 * d2 <= m_distance_tolerance * (dx * dx + dy * dy))
                {
                    // FGC: lint does not like this
                    // if (m_angle_tolerance < curve_angle_tolerance_epsilon)
                    // {
                    //     lineTo(x1234, y1234);
                    //     return;
                    // }

                    // Angle Condition
                    //----------------------
                    da1 = abs(atan2f(y3 - y2, x3 - x2) - atan2f(y2 - y1, x2 - x1));
                    if (da1 >= PI)
                    {
                        da1 = 2 * PI - da1;
                    }

                    if (da1 < m_angle_tolerance)
                    {
                        lineTo(x2, y2);
                        lineTo(x3, y3);
                        return;
                    }

                    // FGC: lint does not like this
                    // if (m_cusp_limit != 0.0f)
                    // {
                    //     if (da1 > m_cusp_limit)
                    //     {
                    //         lineTo(x2, y2);
                    //         return;
                    //     }
                    // }
                }
            }
            else if (d3 > curve_collinearity_epsilon)
            {
                // p1,p2,p4 are collinear, p3 is considerable
                //----------------------
                if (d3 * d3 <= m_distance_tolerance * (dx * dx + dy * dy))
                {
                    // FGC: lint does not like this
                    // if (m_angle_tolerance < curve_angle_tolerance_epsilon)
                    // {
                    //     lineTo(x1234, y1234);
                    //     return;
                    // }

                    // Angle Condition
                    //----------------------
                    da1 = abs(atan2f(y4 - y3, x4 - x3) - atan2f(y3 - y2, x3 - x2));
                    if (da1 >= PI)
                    {
                        da1 = 2 * PI - da1;
                    }

                    if (da1 < m_angle_tolerance)
                    {
                        lineTo(x2, y2);
                        lineTo(x3, y3);
                        return;
                    }

                    // FGC: lint does not like this
                    // if (m_cusp_limit != 0.0f)
                    // {
                    //     if (da1 > m_cusp_limit)
                    //     {
                    //         lineTo(x3, y3);
                    //         return;
                    //     }
                    // }
                }
            }
            else
            {
                // Collinear case
                //-----------------
                dx = x1234 - (x1 + x4) / 2;
                dy = y1234 - (y1 + y4) / 2;
                if (dx * dx + dy * dy <= m_distance_tolerance)
                {
                    lineTo(x1234, y1234);
                    return;
                }
            }
        }
    }

    // Continue subdivision
    //----------------------
    recursiveCubicBezier(x1, y1, x12, y12, x123, y123, x1234, y1234, level + 1);
    recursiveCubicBezier(x1234, y1234, x234, y234, x34, y34, x4, y4, level + 1);
}

}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/495547.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

Redis命令-String命令

4.3 Redis命令-String命令 String类型&#xff0c;也就是字符串类型&#xff0c;是Redis中最简单的存储类型。 其value是字符串&#xff0c;不过根据字符串的格式不同&#xff0c;又可以分为3类&#xff1a; string&#xff1a;普通字符串int&#xff1a;整数类型&#xff0…

【正点原子FreeRTOS学习笔记】————(14)事件标志组

这里写目录标题 一、事件标志组简介&#xff08;了解&#xff09;二、事件标志组相关API函数介绍&#xff08;熟悉&#xff09;三、事件标志组实验&#xff08;掌握&#xff09; 一、事件标志组简介&#xff08;了解&#xff09; 事件标志位&#xff1a;用一个位&#xff0c;来…

香港高才通计划abc类的申请材料各不同,附官方续签攻略!

香港高才通计划abc类的申请材料各不同&#xff0c;附官方续签攻略&#xff01; 作为香港“史上最快获批签证”&#xff0c;高才通计划受到内地精英的热烈欢迎&#xff0c;香港高才通于2022年12月28日开放申请&#xff0c;截至2023年12月31日&#xff0c;香港高才通计划已接获62…

如何应对Android面试官->Android中的类加载机制,手写热修复框架(上)

前言 本章主要介绍 Android 中的类加载机制和 Java 中的类加载机制有什么不同&#xff1f; ClassLoader Android 中的顶层抽象基类也是 java.lang.ClassLoader&#xff1b;可以进入 Android SDK 中提供的 ClassLoader 看一下 可以看到&#xff0c;这个 ClassLoader 是一个抽象…

短视频矩阵系统新的ui迭代上线

短视频矩阵系统是一个复杂的系统&#xff0c;需要保证其稳定性以确保用户的使用体验。以下是一些关键的方法和步骤来确保开发的系统稳定性&#xff1a; 1. **设计合理的架构**&#xff1a;架构的合理性对于系统的稳定性至关重要。系统应设计为可扩展的&#xff0c;并具有良好的…

【JavaWeb】Day17.前端工程化——Vue项目开发流程

Vue项目开发流程 上个文章展示了建立新的Vue项目并打开网页&#xff0c;今天展示项目是如何形成的&#xff0c;即项目的开发流程。 用VScode打开新建的项目 &#xff0c;render函数创建了虚拟DOM元素&#xff0c;在app.vue中定义。 Vue的组件文件以 .vue结尾&#xff0c;每个组…

TLSR8258 MTU、DLE(PDU)

本文基于泰凌微TLSR8258 M1S1 demo。 1.DLE&#xff1a;LE Data Packet Length Extension 中文全称&#xff1a;低功耗蓝牙数据包长度扩展。 这是一个在2014年12月2日正式发布的bluetooth for BLE添加的新特性&#xff0c;以支持在LL Data PDU更长的数据&#xff0c;最大支持…

echarts 3D示例 echart, echarts-gl

echarts官网有很多的炫酷的3D模型 来尝试实现下&#xff0c;使用原本的柱状图或者折线图代码创建echarts示例,使用cdn的方式引入echarts <!DOCTYPE html> <html lang"en"><head><meta charset"UTF-8" /><meta name"viewp…

共享WiFi贴码真能赚钱还只是骗局?

最近WiFi贴码的风真的很大&#xff0c;想做的人很多&#xff0c;那么自然怕被骗的人也比比皆是。WiFi贴码真的如大家所说很赚钱&#xff1f;本期就来解答一下WiFi贴码到底能不能挣钱以及它分析是不是骗局的套路。 什么是WiFi贴码&#xff1f; 这边我们从共享wifi的鼻祖微火那了…

PADS导出元器件的值,并且自动摆放在相对的位置上显示

PADS导出元器件的值&#xff0c;并且自动摆放在相对的位置上显示 1. pads 显示出器件信息 点击右键鼠标&#xff0c;点击选择元器件&#xff0c;鼠标框选全部器件 在器件上右击&#xff0c;点击特性 PS&#xff1a;操作的时候&#xff0c;需要保证 器件有相关值的参数存在…

docker在线安装centos7(windows版)

目录 1、docker本地安装2、拉取centos7镜像3、启动容器4、配置SSH以访问centos7 1、docker本地安装 windows安装docker比较简单&#xff0c;官网搜索有个docker desktop装上就完事。 2、拉取centos7镜像 可以登录到docker hub上拉&#xff0c;也可以搜出来对应的centos7镜像…

前端学习<二>CSS基础——06-CSS盒模型详解

盒子模型 前言 盒子模型&#xff0c;英文即box model。无论是div、span、还是a都是盒子。 但是&#xff0c;图片、表单元素一律看作是文本&#xff0c;它们并不是盒子。这个很好理解&#xff0c;比如说&#xff0c;一张图片里并不能放东西&#xff0c;它自己就是自己的内容。…

大会邀请 |北京智慧城市时空信息大会

2024年3月27-28日&#xff0c;2024中关村论坛系列活动一一首届智慧城市时空信息大会将在北京国际财富中心举办&#xff0c;大势智慧作为协办单位出席大会&#xff0c;重点展出公司自主研发的实景三维全自主、全流程系列软硬件产品&#xff0c;并为您带来公司在智慧城市、智慧应…

Linux的VirtualBox中USB设备无法选择USB3.0怎么办?

在VirtualBox中&#xff0c;如果遇到USB设备无法选择 USB 3.0 的问题&#xff0c;可以尝试按照以下步骤来解决&#xff1a; 确保VirtualBox版本支持USB 3.0&#xff1a;首先&#xff0c;你需要确认你的VirtualBox版本是否支持USB 3.0。一些较旧的版本可能不支持&#xff0c;因此…

MappedByteBuffer VS FileChannel:从内核层面对比两者的性能差异

本文基于 Linux 内核 5.4 版本进行讨论 自上篇文章《从 Linux 内核角度探秘 JDK MappedByteBuffer》 发布之后&#xff0c;很多读者朋友私信我说&#xff0c;文章的信息量太大了&#xff0c;其中很多章节介绍的内容都是大家非常想要了解&#xff0c;并且是频繁被搜索的内容&…

vue多语言包i18n

1.安装 如果是vue2直接安装8.2.1版本&#xff0c;否则会出现版本不匹配的错误 npm install vue-i18n8.2.1 --save2.文件编辑 在src目录下创建文件 en.js export const h {system: "Background management system",loginOut:"LoginOut",LayoutSet:Layout …

SQL104 返回产品名称和每一项产品的总订单数(left join..on.. ,group by)

select prod_name,count(order_num) as orders from Products P left join OrderItems OI on OI.prod_id P.prod_id group by prod_name order by prod_name;left join一个数据条多的表 count&#xff08;order_num&#xff09;,group by 另一个字段

解决PATH变量污染的问题

文章目录 解决PATH变量污染的问题概述笔记清空PATH变量之后的系统设置在命令行查看清空后的PATH变量以 gitea-1.17.1-gogit-windows-4.0-amd64.exe 为例以系统命令 where为例备注 - 批处理的后缀最好是batEND 解决PATH变量污染的问题 概述 随着不断安装新软件, 可能多个软件中…

flowable-ui后台显式非中文

把flowable-ui的war包发布后&#xff0c;后台界面显示的是非中文 用的是6.7.2版本&#xff0c;经过了解该版本是有国际化配置文件的&#xff0c;支持中文 猜测可能是浏览器语言导致未显示中文&#xff0c;在控制台输入navigator.language&#xff0c;查看到果然是英文 解决方案…

C++ 之多态虚函数原理及应用

文章目录 多态基本概念和原理虚函数的基本原理和概念虚析构和纯虚析构多重继承中的虚函数小结 多态基本概念和原理 多态的基本概念 **多态是C面向对象三大特性之一** 多态的定义 多态是一种面向对象编程概念&#xff0c;指同一个行为&#xff08;方法&#xff09;在不同的对象上…