二进制字体
本节将介绍如何在 TouchGFX 中使用二进制字体。第一部分包含了一些关于 TouchGFX 中字体和文本系统的深入信息,在使用二进制字体时,理解这些信息会很有帮助。第二部分将解释如何使用二进制字体。
二进制字体可以作为一种替代传统方法的选择,传统方法是将字体信息编译并链接到应用程序中(the .cpp files in generated/fonts/src)。这种(使用二进制字体的)方法的主要优点是可以使应用程序的二进制文件更小,并且在为设备提供不同的字体集方面具有灵活性。例如,对于发往中国的设备,你可以打包中文字体;对于发往日本的设备,可以打包日文字体。这种方法的缺点是整个二进制字体需要加载到随机存取存储器(RAM)中(或者进行内存映射的闪存中),如果字体较大,这可能会成为一个问题。
将字体链接到应用程序的常规方法的主要优点是,应用程序将始终自动包含应用程序中使用的更新后的文本和排版。这种方法使用起来非常简单且安全。其缺点是字体可能会使应用程序体积变大。
字体和文本系统类
在默认配置下,TouchGFX 会为应用程序中使用的所有文本和字体生成 .cpp 文件。这些文件会与生成的用户界面以及你的应用程序代码一起被编译并链接到应用程序中。当你在用户界面上通过(例如)TextArea 显示文本时,你会使用 TypedTextId 来引用该文本。小部件会利用这个 TypedTextId 在文本中查找实际的字符。这些小部件将通过位于 framework/include/touchgfx/Texts.hpp 中的 touchgfx::Texts 类来访问文本。
这张表格能让 TouchGFX 在所选语言中找到指定的文本。
每当你在 TouchGFX 设计器中更改文本,或者直接在 texts.xml 文件中进行更改并生成应用程序时,这些表格都会重新生成。
在我们能够在屏幕上绘制文本之前,我们需要知道为该文本使用哪种字体。文本和字体之间的这种映射关系由 TypedTextDatabase 类控制,该类位于(生成的 texts/include/texts/TypedTextDatabase.hpp 中)。
在 TouchGFX 设计器的 “文本” 选项卡中,你可以为每个文本指定一种排版样式、书写顺序(从左到右或从右到左)以及对齐方式(左对齐、右对齐、居中对齐)。对于文本的每种翻译,排版样式、顺序和对齐方式都可以不同。这些信息会被编译成针对每种语言的特定表格。这使得 TouchGFX 能够轻松确定对于给定文本应使用哪种字体、如何对齐文本以及按何种顺序书写字符。
在上图中,TypedTextData 表格包含指向三个数组的指针。应用程序中的每种语言对应一个数组。每个数组都有 3 个元素,系统中的每个文本对应一个元素。每个元素描述了一种字体、一种阅读顺序以及一种对齐方式。我们可以看到,在这个示例中,三种语言的文本使用了相同的字体,但不同的文本使用了不同的字体(F1 或 F2)。Fonts 表格有两个指针,因为应用程序中使用了两种字体。
当 TouchGFX 要在屏幕上绘制文本时,它会查找给定文本的 TypedTextData。这些数据包含字体索引、字母顺序(从左到右或从右到左)以及在 TouchGFX 设计器或 xml 文档中指定的文本水平对齐方式(左对齐、右对齐、居中对齐)。TouchGFX 使用 TypedTextData 中的字体索引(F1 或 F2)来查找该文本对应的正确字体。
当字体被编译到应用程序中时,所有这些操作都会自动进行。
使用二进制字体
当一个应用程序使用多种不同字体且包含大量字符时,应用程序的体积可能会大幅增大。
为解决这一问题,TouchGFX 允许应用程序使用二进制字体。这些字体不会被链接到应用程序中,而是作为单独的文件与应用程序分开存储。这些文件可以在运行时由应用程序加载并提供给 TouchGFX。例如,应用程序可以从外部存储设备(如 SD 卡)加载字体,或者从互联网下载字体。
当应用程序加载了字体后,它可以请求 TouchGFX 将该二进制字体安装到字体系统中:
在此处,内置的Font2 被应用程序加载的二进制字体所替代。此后,TouchGFX 不再使用已链接的Font2。
请注意,文本表格中的内容没有变化。这些表格仍然通过索引引用相同的字体(F1 和 F2)。
配置字体转换器以生成二进制字体
必须对字体转换器进行配置,以便生成二进制字体。在 TouchGFX 设计器中,这很容易实现。转到 “配置” 选项卡,选择 “文本配置”,然后点击 “二进制字体文件”:
当你重新生成代码时,TouchGFX 会在 generated/fonts/bin/文件夹中生成二进制字体,并且会在generated/fonts/src/.文件夹中的常规文件里生成空字体。
生成的代码会配置 TouchGFX 使用空字体。应用程序需要在运行时安装二进制字体。
手动配置
如果你不使用 TouchGFX 设计器,你仍然可以生成二进制字体。在你项目的 application.config 文件的 text_configuration 部分,将 “binary_fonts” 选项改为 “yes”。
application.config
"text_configuration": {
"remap": "yes",
"a4": "yes",
"binary_translations": "no",
"binary_fonts": "yes",
"framebuffer_bpp": "16"
}
当你下次生成资源时,二进制字体将位于 generated/fonts/bin 文件夹中。
安装二进制字体
在 TouchGFX 能够使用二进制字体之前,字体数据必须存在于可寻址内存(如 RAM 或 QSPI 闪存)中(这样才能通过指针直接访问)。通常,这需要将数据从文件或块存储设备(如 eMMC 闪存)中复制出来。在生产过程中,也可以将二进制字体烧录到内存映射闪存中的预定义地址。
当应用程序将二进制字体加载到内存后(如果尚未加载),必须在 TouchGFX 中创建并安装一个引用该数据的 BinaryFont 对象。此后,TouchGFX 将使用该字体,而非编译进应用的字体。
二进制字体需要在用于绘制引用该字体的文本之前安装,但不必在启动后立即安装。可以在 FrontApplication.cpp 文件的 FrontendApplication::FrontendApplication(Model& m, FrontendHeap& heap) 构造函数中安装字体。这个构造函数会在绘制任何内容之前执行一次。
字体也可以在 setupScreen() 方法中安装。如果某个字体仅在特定屏幕中使用,这种方式会很有用。此时,可以在 tearDownScreen() 方法中卸载该字体。
下面是一个从文件系统将二进制字体加载到内部 RAM 的示例:
FrontendApplication.cpp
//read the file into this array in internal RAM
uint8_t fontdata[10000];
//binary font object using the data
BinaryFont bf;
FrontendApplication::FrontendApplication(Model& m, FrontendHeap& heap)
: FrontendApplicationBase(m, heap)
{
//read the binary font from a file
FILE* font = fopen("generated/fonts/bin/Font_verdana_20_4bpp.bin", "rb");
if (font)
{
//read data from the file
fread(fontdata, 1, 10000, font);
fclose(font);
//initialize BinaryFont object in bf using placement new
new (&bf) BinaryFont((const struct touchgfx::BinaryFontData*)fontdata);
//replace application font 'DEFAULT' with the binary font
TypedTextDatabase::setFont(DEFAULT, &bf); //verdana_20_4bpp
}
}
根据您的文件系统和操作系统的不同,打开文件并读取数据的具体代码也会有所不同。其基本步骤是将字体数据加载到内存中,使用指向该数据的指针初始化一个 BinaryFont 对象,最后将这个 BinaryFont 对象传递给 TouchGFX 的 TypedTextDatabase。
在调用 setFont 之后,TouchGFX 将使用二进制字体在屏幕上绘制文本,而非编译进应用的字体(默认字体)。
重置字体
有时,在使用二进制字体后,您可能希望恢复应用程序中编译的原始字体。例如,在更改语言后想使用默认字体。TypedTextDatabase 中的 resetFont() 函数可以将字体指针重置为内置字体:
//reset to original font
TypedTextDatabase::resetFont(DEFAULT);
调用此函数后,应用程序可以重新使用二进制字体占用的内存,将其分配给新字体或用于其他用途。
在另一个项目中生成二进制字体
在某些情况下,您可能希望在一个项目中同时使用普通字体和二进制字体。例如,您希望英文字母使用普通的编译字体,而中文字符和日文字符使用二进制字体,以便在设备中选择性地包含。这种设置在 TouchGFX 设计器中无法直接配置。
在这种情况下,建议创建两个 TouchGFX 项目。在第一个项目(即您的常规应用程序)中,包含所有应用代码和使用普通字体的 UI。在第二个项目中,仅包含足够的文本(或通配符字符)来生成二进制字体。
在第一个项目中,取消选择 “二进制字体文件”。在第二个项目中,选择 “二进制字体文件”。
当您在第二个 TouchGFX 项目中生成代码时,会生成二进制字体。然后可以将这些二进制字体复制到第一个项目(放在您方便的文件夹中),并在代码中按上述方式使用。
(我使用二进制字体的方法和步骤)
(一)生成二进制字体
(请注意:生成字体时,用Touchgfx Designer-4.23.0编译时一定要关掉KEIL项目,否则会出现意料之外的异常,比如二进制字体显示不出来等等。关键问题:字体排印中Font中(字体+字号的组合)只能出现一次,不能重复,否则在二进制字体应用时会出现显示不出来字符的现象,因为Touchgfx 在引用字体排印时,既要看Typography Id,也检查字体字号,如果重复的话,内部字体排印优先,不会从Binary font files中引用。)
(1)利用touchGFX的 Binary fongt files功能生成二进制字体:Font_SourceHanSansCN_Light_otf_20_4bpp.bin
当你重新生成代码时,TouchGFX 会在 generated/fonts/bin/文件夹中生成二进制字体,并且会在generated/fonts/src/.文件夹中的常规 文件里生成空字体。生成的代码会配置 TouchGFX 使用空字体。应用程序需要在运行时安装二进制字体。
(2)新建文件夹\KY7in_V1.0(20250428)\Drivers\font,把刚才生成的字体拷贝到这个文件夹中。并新建汇编文件:font.s,其内容如下:
AREA ExtFlashSection, DATA, READONLY
EXPORT Font_simhei_20_4bpp_bin_start
EXPORT Font_simhei_20_4bpp_bin_end
Font_simhei_20_4bpp_bin_start
INCBIN ./Font_SourceHanSansCN_Light_otf_20_4bpp.bin
Font_simhei_20_4bpp_bin_end
END
其中:Font_SourceHanSansCN_Light_otf_20_4bpp.bin为生成的二进制文件。利用编写好的下载算法文件下载到外部Flash中。
(3)生成的代码会配置 TouchGFX 使用空字体。应用程序需要在运行时安装二进制字体。把二进制字体从外部Flash中下载到RAM中并安装二进制字体的代码如下:
#include <TouchGFXHAL.hpp>
/* USER CODE BEGIN TouchGFXHAL.cpp */
#include <fonts/GeneratedFont.hpp>
#include <fonts/ApplicationFontProvider.hpp>
#include <texts/TypedTextDatabase.hpp>
#include <new>
#include "stm32f4xx.h"
#include "sdram.h"
#include "main.h"
#include "w25qxx.h"
#include <KeyController.hpp>
#include <batteryPar.hpp>
using namespace touchgfx;
static keyController kc;//实例化按键类对象kc
batteryPar bat;
static BinaryFont bf;
extern const uint8_t Font_simhei_20_4bpp_bin_start[];
extern const uint8_t Font_simhei_20_4bpp_bin_end[];
using namespace touchgfx;
void TouchGFXHAL::initialize()
{
TouchGFXGeneratedHAL::initialize();
uint32_t frameSize = DISPLAY_HEIGHT*DISPLAY_WIDTH*2;
setFrameBufferStartAddresses((void *)SDRAM_START_ADDR,(void *)(SDRAM_START_ADDR+frameSize),(void *)(SDRAM_START_ADDR+frameSize*2));
setFrameRateCompensation(true);
//设置手指大小
//setFingerSize(5);//20像素手指太大会影响按钮的响应
//20M SDRAM缓存
#define BITMAP_CACHE_SIZE 0x1400000
W25QXX_Init();
Bitmap::setCache((uint16_t*)(SDRAM_START_ADDR+ frameSize*3),BITMAP_CACHE_SIZE,3);
//Bitmap::cacheAll(); //加载图片从FLASH到SDRAM
//加载一个全中文字库 Heiti20
uint8_t * fontCacheAddr = (uint8_t *)(SDRAM_START_ADDR+frameSize*3+BITMAP_CACHE_SIZE);
W25QXX_Read(fontCacheAddr,
(uint32_t)Font_simhei_20_4bpp_bin_start-EX_FLASH_START_ADDR,
(uint32_t)(Font_simhei_20_4bpp_bin_end-Font_simhei_20_4bpp_bin_start));
//placement new
new (&bf)BinaryFont((const touchgfx::BinaryFontData *)fontCacheAddr);
TypedTextDatabase::setFont(Typography::HEITI20,&bf);//挂载字体给TouchGFX, 使用空字体名(这里是TypedTextId)来索引 HEITI20
kc.init();//初始化实体按键
bat.batteryParInit();
setButtonController(&kc);//注册实体按键到touchgfx中
HAL_GPIO_WritePin(LCD_BL_GPIO_Port,LCD_BL_Pin,GPIO_PIN_SET);//背光控制
}
(4)使用二进制字体,首先要包含头文件:#include <touchgfx/Unicode.hpp> 否则无法正确显示字体,具体代码如下:
#include <gui/startscreen_screen/startScreenView.hpp>
#include <touchgfx/Unicode.hpp>
startScreenView::startScreenView()
{
press_n = 0;
}
void startScreenView::setupScreen()
{
textProgress1.setValue(100);
startScreenViewBase::handleTickEvent();
startScreenViewBase::setupScreen();
// 其他初始化代码
#ifndef SIMULATOR
// 第一种方式
// textArea1Buffer[0] = 0x5000;
// textArea1Buffer[1] = 0x4000;
// textArea1Buffer[2] = 0;
// textArea1.invalidate();
// 第二种方式
Unicode::snprintf(textArea1Buffer,TEXTAREA1_SIZE,"%s",L"这种(使用二进制字体的)方法");
textArea1.invalidate();
//第三种方式
// Unicode::strncpy(textArea1Buffer,(Unicode::UnicodeChar*)L"让社会更安全 让生命更健康",TEXTAREA1_SIZE);
// textArea1.invalidate();
#endif
}
字体缓存
本节介绍如何在 TouchGFX 中使用字体缓存来处理二进制字体。
请先阅读关于二进制字体的内容。
字体缓存
回顾一下,使用二进制字体需要将整个字体加载到内存中。如果字体很大(例如大型中文字体),在某些情况下这可能不太理想。
字体缓存允许应用程序仅从外部内存加载显示字符串所需的字符。这意味着整个字体不需要驻留在可寻址闪存或 RAM 中,而是可以存储在更大的文件系统上。
在下图中,编译进应用的字体 Font2 已被字体缓存所取代。当 TouchGFX 绘制使用 Font2 的文本时,它会在字体表中找到指向 CachedFont 对象的指针。这个特殊的字体将在 FontCache 对象中查找字符。
CachedFont(缓存字体)在设置时会关联一个指向已链接字体(如上述的 Font2)的指针。当 TouchGFX 向 CachedFont 请求某个特定字符时,CachedFont 会首先在它所替代的常规字体(Font2)中查找。这个字体可能是一个空字体,也可能是一个包含常用字符集的 “常规” 字体。如果该字体中不包含所需的字符,CachedFont 会查看 FontCache(字体缓存),检查该字符是否已从文件系统加载。
这种机制限制了必须缓存的字符数量,因为我们无需缓存那些已存在于常规字体中的字符。
在应用代码中使用字体缓存
在应用程序安装 CachedFont 之前,必须先创建一个 FontCache、一个内存缓冲区和一个文件系统读取器对象:
Screen1View.cpp
uint8_t fontdata[5120]; //Memory buffer for the font cache, 5Kb
FontCache fontCache;
CachedFont cachedFont; //Cached Font object
FileDataReader reader; //Filesystem reader object
必须将 FontCache 与缓冲区和读取器关联起来:
Screen1View.cpp
//setup the font cache with buffer and size; and file reader object
fontCache.setMemory(fontdata, 5120);
fontCache.setReader(&reader);
现在,应用程序可以设置字体缓存、初始化 CachedFont 并将其传递给 TouchGFX。
字体缓存需要一个 TextId 来初始化 CachedFont 对象。TextId 用于查找 CachedFont 必须指向的字体。这样可以确保你替换的是显示屏上文本所使用的字体:
Screen1View.cpp
//initialize the cachedFont object to the font used by T_SINGLEUSEID1
TypedText text = TypedText(T_SINGLEUSEID1);
fontCache.initializeCachedFont(text, &cachedFont);
//replace the linked in font in TouchGFX with cachedFont
TypedTextDatabase::setFont(DEFAULT, &cachedFont);
上述代码可以放置在应用程序的任何位置。如果缓存字体仅在特定视图中使用,那么将代码插入到该视图中是个不错的选择。
缓存字符
此时字体缓存仍为空。在显示任何字符之前,必须先从字体缓存中读取它们。这可以通过将一个 Unicode 数组(即字符串)传递给字体缓存来实现。在这个示例中,我们只需传递 T_SINGLEUSEID1 对应的文本即可。
Screen1View.cpp
//cache the glyphs used by the text T_SINGLEUSEID1
Unicode::UnicodeChar* str = const_cast<Unicode::UnicodeChar*>(text.getText());
bool b = fontCache.cacheString(text, str);
字体缓存将通过读取器对象加载在 str 数组中找到的字符。读取的 Unicode 字符将与 TextId 文本参数所使用的字体相关联。
应用程序负责配置读取器对象,以便从正确的文件中进行加载。
缓存连字字符
对于那些在显示前会将 Unicode 序列转换为其他 Unicode 的语言(如阿拉伯语和天城文),上述方法并不适用。因为它缓存的是原始 Unicode,而非转换后显示的 Unicode。此时应使用以下方法对给定的 Unicode 进行转换,并缓存转换后所需的 Unicode:
Screen1View.cpp
//cache the glyphs used by the text T_SINGLEUSEID1 after conversion
Unicode::UnicodeChar* str = const_cast<Unicode::UnicodeChar*>(text.getText());
bool b = fontCache.cacheLigatures(cachedFont, text, str);
内存使用情况
字体缓存可以计算当前使用的内存量:
Screen1View.cpp
touchgfx_printf("Memory usage %d\n", fontCache.getMemoryUsage());
缓存 GSUB 表
某些字体在渲染时会使用 GSUB 表(字形替换表)。这类字体主要是东方语言字体,如天城文(Devanagari)字体。GSUB 表允许字体系统重新排列字符,并将字符序列替换为其他 “组合” 字符。
字体缓存可以从文件系统加载此 GSUB 表。如果未加载该表,文本渲染系统将无法使用 GSUB 表,从而导致字体显示不正确。
在初始化缓存字体时,可以通过提供一个额外参数来加载 GSUB 表:
Screen1View.cpp
//initialize the cachedFont and load the GSUB table
text = TypedText(T_SINGLEUSEID1);
fontCache.initializeCachedFont(text, &cachedFont, true);
实现字体文件读取器
上述示例代码中使用的 FileDataReader 类并未包含在 TouchGFX 中,因为它依赖于您所使用的操作系统。
以下是一个适用于标准 “stdio” 兼容文件系统的实现示例:
Screen1View.cpp
class FileDataReader : public FontDataReader
{
public:
virtual ~FileDataReader() { }
virtual void open()
{
fp = fopen("Font_verdana_20_4bpp.bin", "rb");
if (!fp)
{
touchgfx_printf("Unable to open font file!!!\n");
}
}
virtual void close()
{
fclose(fp);
}
virtual void setPosition(uint32_t position)
{
fseek(fp, position, SEEK_SET);
}
virtual void readData(void* out, uint32_t numberOfBytes)
{
fread(out, numberOfBytes, 1, fp);
}
private:
FILE* fp;
};
FileDataReader 类实现了 FontCache.hpp 中的 FontDataReader 接口:
FontCache.hpp
class FontDataReader
{
public:
virtual ~FontDataReader() { }
virtual void open() = 0;
virtual void close() = 0;
virtual void setPosition(uint32_t position) = 0;
virtual void readData(void* out, uint32_t numberOfBytes) = 0;
};