Skia 使用 skparagraph 和 icu 等库来辅助开发者渲染段落文本。
skparagraph 是 Skia 提供的一个高级文本布局和绘制工具。它简化了文本排版的过程,使开发者可以更容易地处理复杂的文本布局需求,例如多行文本、自动换行、对齐方式等。
icu (International Components for Unicode) 库是一个成熟的、功能全面的国际化开发库,被广泛用于处理 Unicode 文本,比如:字体的布局、文本的形状化、语言断词(用于自动换行)等功能。
大段文本渲染
来看一下绘制段落文本的代码:
// #include "include/core/SkFontMgr.h"
// #include "include/ports/SkTypeface_win.h"
// #include "modules/skparagraph/include/Paragraph.h"
// #include "modules/skparagraph/src/ParagraphBuilderImpl.h"
void drawMutiText(SkCanvas* canvas)
{
std::u16string text(uR"(醉里挑灯看剑,梦回吹角连营。
八百里分麾下炙,五十弦翻塞外声。
沙场秋点兵。
马作的卢飞快,弓如霹雳弦惊。
了却君王天下事,赢得生前身后名。
可怜白发生!)");
sk_sp<skia::textlayout::FontCollection> fontCollection = sk_make_sp<skia::textlayout::FontCollection>(); //字体容器
auto fontMgr = SkFontMgr_New_GDI();
fontCollection->setDefaultFontManager(fontMgr); //默认字体管理器
skia::textlayout::ParagraphStyle paraStyle; //段落样式
std::unique_ptr<skia::textlayout::ParagraphBuilder> builder = skia::textlayout::ParagraphBuilder::make(paraStyle, fontCollection); //段落构建器
skia::textlayout::TextStyle defaultStyle; //文本样式
defaultStyle.setFontFamilies({ SkString{"Microsoft YaHei"} });
defaultStyle.setColor(0xff00ffff);
defaultStyle.setFontSize(38);
builder->pushStyle(defaultStyle);
builder->addText(text);
std::unique_ptr<skia::textlayout::Paragraph> paragraph = builder->Build(); //构建段落对象
paragraph->layout(w); //设置段落宽度
paragraph->paint(canvas, 10, 10); //绘制段落文本
}
这段代码有以下几点需要注意:
-
用
std::u16string类型的变量存储文本(中文),这是一段多行文本(包括换行符)。 -
FontCollection用于存储段落中需要用到的字体。 -
ParagraphStyle用于设置段落的总体样式(这部分内容稍后再介绍) -
ParagraphBuilder用于构建和配置复杂文本段落,它可以为段落设置样式(比如行高)。 -
TextStyle用于设置文本样式,这里设置了字体,颜色,字号等。 -
ParagraphBuilder对象的Build方法生成一个段落对象。 -
段落对象的
layout方法用于设置段落的宽度(此处设置为窗口宽度,超出窗口宽度的文本将自动换行) -
段落对象的
paint方法用于把段落文本绘制到画布上。
运行程序得到如下结果:

改变窗口大小,文本会自动换行。
段落文本样式
通过 ParagraphStyle 设置段落样式,如下代码所示:
skia::textlayout::ParagraphStyle paraStyle;
skia::textlayout::StrutStyle strutStyle;
strutStyle.setFontFamilies({ SkString{"Microsoft YaHei"} }); //使用字体名称设置字体
strutStyle.setStrutEnabled(true);
strutStyle.setFontSize(38); //字体大小
strutStyle.setHeightOverride(true);
strutStyle.setHeight(1.6); //行高
paraStyle.setStrutStyle(strutStyle);
这段代码设置段落文本 1.6 倍行高。
还可以在一段文本中应用多个文本样式(TextStyle),如下代码所示:
skia::textlayout::TextStyle defaultStyle; //默认样式
defaultStyle.setFontFamilies({ SkString{"Microsoft YaHei"} });
defaultStyle.setColor(0xff00ffff);
defaultStyle.setFontSize(38);
builder->pushStyle(defaultStyle);
builder->addText(u"醉里挑灯看剑,梦回吹角连营。\n");
skia::textlayout::TextStyle newStyle = defaultStyle; //新样式
newStyle.setFontStyle(SkFontStyle::BoldItalic()); //粗体+斜体
newStyle.setLetterSpacing(8); //字间距
newStyle.setColor(0xFFFFFF00); //颜色
newStyle.setFontSize(26); //文字大小
newStyle.setDecoration(skia::textlayout::TextDecoration::kUnderline); //文本下划线
builder->pushStyle(newStyle);
builder->addText(u"八百里分麾下炙,五十弦翻塞外声。\n");
builder->pop(); // 移除最后一个样式元素
builder->addText(u"沙场秋点兵。\n");
在这段代码中,先设置了段落的默认文本样式(defaultStyle),用这个样式绘制第一行文本。
接着设置第二个文本样式(newStyle),用这个样式绘制第二行文本。
随后执行 ParagraphBuilder 对象的 pop 方法(推出最后一个样式元素),放弃第二个文本样式( newStyle ),仍旧使用第一次设置的文本样式( defaultStyle )绘制第三行文本。
应用程序执行后,界面显示如下图所示:

使用多个字体绘制文本
有的时候,一个段落中的文本需要多个字体来渲染。比如:段落中既有中文文本,又有 Emoji 表情图标(或者阿拉伯文本)。
如果你使用的字体不能兼容段落中的所有文本,那么渲染结果就会有问题。假设用本章第一个示例,渲染这行文本:
std::u16string text(uR"(轻舟已过万重山。😊)");
渲染的结果如下图所示:

如果让文本中的 Emoji 表情正常渲染,只要给文本样式添加一个字体即可,如下代码所示:
skia::textlayout::TextStyle defaultStyle;
defaultStyle.setFontFamilies({ SkString{"Microsoft YaHei"},SkString{"Segoe UI Emoji"} });
Segoe UI Emoji是 Windows 操作系统内置的一个字体,专门用于渲染Emoji表情。
设置此字体后,运行程序时,Skia 会帮我们选择合适的字体渲染段落文本中的字符。
再次运行程序得到的结果如下图所示:

文本阴影
可以使用如下代码给段落文本增加阴影效果:
skia::textlayout::TextShadow shadow(0xFFFFFFFF, SkPoint::Make(2, 2), 2);
defaultStyle.addShadow(shadow);
创建 skia::textlayout::TextShadow 对象的第一个参数为阴影颜色,第二个参数为阴影偏移量,最后一个参数为模糊半径。
运行程序得到的结果如下图所示:

文本编辑器
Skia 库可以渲染各种各样的文本,但并不直接提供接收文本输入的支持。
下面介绍几个需要注意的知识点:
下面介绍几个需要注意的知识点:
-
把 Skia 示例源码文件引入到自己的工程

-
定义几个预处理器
SK_EDITOR_GO_FAST SK_UNICODE_ICU_IMPLEMENTATION SK_SHAPER_HARFBUZZ_AVAILABLE SK_SHAPER_UNICODE_AVAILABLE这都是 Skia 提供的代码中用到的预处理器.
-
通过一个定时器来绘制输入光标,并让它闪烁
bool fBlink = false; #define WM_REFRESH (WM_APP+100) //定义一个Windows消息,这是一个整形数字 SetTimer(hwnd, WM_REFRESH, 600, (TIMERPROC)NULL);SetTimer是一个Windows API,这个方法负责启动一个 600 毫秒的定时器。此方法执行后,操作系统每隔 600 毫秒会向 hwnd 指向的窗口发送WM_TIMER消息。 处理此消息的代码如下所示://处理定时器消息 LRESULT CALLBACK wndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam) { switch (message) { case WM_TIMER: { switch (wParam) { case WM_REFRESH: { fBlink = !fBlink; //用此变量控制光标是否显示 InvalidateRect(hWnd, nullptr, false); //发送重绘消息 break; } default: { break; } } return 0; } } // ......此处省略了很多代码 return DefWindowProc(hWnd, message, wParam, lParam); }收到
WM_TIMER消息后,判断wParam的值是否为WM_REFRESH,如果是,则重置fBlink变量的值(这个变量用于控制文本输入光标是否显示),之后请求重绘窗口。
4, 接收文本输入(兼容 CJK 字符)
case WM_CHAR: {
if ((wParam >= 32 && wParam <= 126) || // 可打印的ASCII字符范围
(wParam >= 160 && wParam <= 55295) || // 可打印的Unicode字符范围
(wParam >= 57344 && wParam <= 65535)) // 高位可打印的Unicode字符范围
{
std::wstring word{ (wchar_t)wParam }; //宽字符文本
auto text2 = wideStrToStr(word); //转码为 utf8 文本
fEditor.insert(fTextPos, text2.data(), text2.length()); //在光标处插入文本
//移动光标
auto moveType = getMoveType(VK_RIGHT);
auto pos = fEditor.move(moveType, fTextPos);
move(pos, false);
return 0;
}
break;
}
这段代码把用户输入的内容转码成 utf8 文本再插入编辑器中。
5, 移动光标时,定位中文输入法提示框的位置:
//x,y是中文输入法提示框的位置
void activeKeyboard(long x,long y) {
if (HIMC himc = ImmGetContext(hwnd))
{
COMPOSITIONFORM comp = {};
comp.ptCurrentPos.x = x;
comp.ptCurrentPos.y = y;
comp.dwStyle = CFS_FORCE_POSITION;
ImmSetCompositionWindow(himc, &comp);
CANDIDATEFORM cand = {};
cand.dwStyle = CFS_CANDIDATEPOS;
cand.ptCurrentPos.x = x;
cand.ptCurrentPos.y = y;
ImmSetCandidateWindow(himc, &cand);
ImmReleaseContext(hwnd, himc);
}
}
6, 处理回车按键,换行文本。
case WM_KEYDOWN: {
switch (wParam)
{
case VK_RETURN: {
char ch = (char)'\n'; //文本中的换行符就是 \n
fEditor.insert(fTextPos, &ch, 1); //插入换行符
//移动光标
auto moveType = getMoveType(VK_RIGHT);
auto pos = fEditor.move(moveType, fTextPos);
move(pos, false);
return 0;
}
}
}
803

被折叠的 条评论
为什么被折叠?



