Skia如何渲染文本段落

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); //绘制段落文本
}

这段代码有以下几点需要注意:

  1. 用 std::u16string 类型的变量存储文本(中文),这是一段多行文本(包括换行符)。

  2. FontCollection 用于存储段落中需要用到的字体。

  3. ParagraphStyle 用于设置段落的总体样式(这部分内容稍后再介绍)

  4. ParagraphBuilder 用于构建和配置复杂文本段落,它可以为段落设置样式(比如行高)。

  5. TextStyle 用于设置文本样式,这里设置了字体,颜色,字号等。

  6. ParagraphBuilder 对象的 Build 方法生成一个段落对象。

  7. 段落对象的 layout 方法用于设置段落的宽度(此处设置为窗口宽度,超出窗口宽度的文本将自动换行)

  8. 段落对象的 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 )绘制第三行文本。

应用程序执行后,界面显示如下图所示:

段落样式.png

使用多个字体绘制文本

有的时候,一个段落中的文本需要多个字体来渲染。比如:段落中既有中文文本,又有 Emoji 表情图标(或者阿拉伯文本)。

如果你使用的字体不能兼容段落中的所有文本,那么渲染结果就会有问题。假设用本章第一个示例,渲染这行文本:

std::u16string text(uR"(轻舟已过万重山。😊)");

渲染的结果如下图所示:

文本与Emoji.png

如果让文本中的 Emoji 表情正常渲染,只要给文本样式添加一个字体即可,如下代码所示:

skia::textlayout::TextStyle defaultStyle;
defaultStyle.setFontFamilies({ SkString{"Microsoft YaHei"},SkString{"Segoe UI Emoji"} });

Segoe UI Emoji是 Windows 操作系统内置的一个字体,专门用于渲染Emoji表情。

设置此字体后,运行程序时,Skia 会帮我们选择合适的字体渲染段落文本中的字符。

再次运行程序得到的结果如下图所示:

文本与Emoji2.png

文本阴影

可以使用如下代码给段落文本增加阴影效果:

skia::textlayout::TextShadow shadow(0xFFFFFFFF, SkPoint::Make(2, 2), 2);
defaultStyle.addShadow(shadow);

创建 skia::textlayout::TextShadow 对象的第一个参数为阴影颜色,第二个参数为阴影偏移量,最后一个参数为模糊半径。

运行程序得到的结果如下图所示:

 

文本编辑器

Skia 库可以渲染各种各样的文本,但并不直接提供接收文本输入的支持。

下面介绍几个需要注意的知识点:

下面介绍几个需要注意的知识点:

  1. 把 Skia 示例源码文件引入到自己的工程

    拷贝编辑器源码.png

  2. 定义几个预处理器

    SK_EDITOR_GO_FAST
    SK_UNICODE_ICU_IMPLEMENTATION
    SK_SHAPER_HARFBUZZ_AVAILABLE
    SK_SHAPER_UNICODE_AVAILABLE

    这都是 Skia 提供的代码中用到的预处理器.

  3. 通过一个定时器来绘制输入光标,并让它闪烁

    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;
        }
    }
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

liulun

如果文章真帮到了你,谢谢您打赏

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值