
Core Text 的段落样式对象 CTParagraphStyle 是控制多行文本垂直布局的关键。它通过一组设置值来影响行高、行间距以及段落之间的空白。在 NSAttributedString 中使用 NSParagraphStyle 时,底层映射的就是 CTParagraphStyle。但对于需要精细排版、自定义文字引擎,或直接与 Core Text 交互的场景,我们必须直面 C 级别的 CTParagraphStyleRef,自行拼装 CTParagraphStyleSetting 数组来完成初始化。
CTParagraphStyle 的结构与设置方法
CTParagraphStyle 是一个不透明类型,创建时需通过 CTParagraphStyleCreate 函数,传入一组 CTParagraphStyleSetting 结构体。每个 CTParagraphStyleSetting 包含三个字段:spec 表示要设置的属性规格,valueSize 表示值的大小,value 则是指向数据的指针。常用的规格常量以 kCTParagraphStyleSpecifier 开头,如 kCTParagraphStyleSpecifierLineSpacing、kCTParagraphStyleSpecifierParagraphSpacing 等。
在设置行间距时,所有与垂直方向相关的规格都会有标注“以点为单位的距离”。但 C 结构体的设置非常原始,不像 Swift 或 Objective-C 的 NSParagraphStyle 那样有属性语法糖,因此开发者需要时刻注意类型转换。例如,要设置一个 CGFloat 类型的行间距,可以像这样构造设置项:
CGFloat lineSpacing = 4.0;
CTParagraphStyleSetting lineSetting;
lineSetting.spec = kCTParagraphStyleSpecifierLineSpacing;
lineSetting.valueSize = sizeof(CGFloat);
lineSetting.value = &lineSpacing;
// 如果有多个设置,组合成数组
CGFloat paragraphSpacing = 12.0;
CTParagraphStyleSetting paragraphSetting;
paragraphSetting.spec = kCTParagraphStyleSpecifierParagraphSpacing;
paragraphSetting.valueSize = sizeof(CGFloat);
paragraphSetting.value = ¶graphSpacing;
CTParagraphStyleSetting settings[] = { lineSetting, paragraphSetting };
CTParagraphStyleRef paragraphStyle = CTParagraphStyleCreate(settings, 2);
创建得到的 paragraphStyle 可以附加到属性字符串中,作为 kCTParagraphStyleAttributeName 的值。之后在使用 CTFramesetter 或 CTLine 进行排版时,这些样式就会生效。
行间距与段间距的核心属性辨析
在宏观上,段落样式提供的垂直控制参数可以分为三类:行间距调整、行高倍数/约束,以及段落前后间距。第一类包括 kCTParagraphStyleSpecifierLineSpacing(通常称为行间距),它指定了在两行之间额外添加的空白量,单位是点。这个值不改变行本身的高度,只是在行与行的空隙上叠加。例如,默认情况下两行之间的基线偏移大约为字体大小加内置的上下行空白,设置 lineSpacing 为 4 点意味着在这个基础上再加 4 点间距,每行都向下推移,通常使得整个段落高度增加。
段落间距方面,kCTParagraphStyleSpecifierParagraphSpacing 用于在段落末尾添加额外空白,类似于段后间距。kCTParagraphStyleSpecifierParagraphSpacingBefore 则在段落开始之前添加额外空白。这两个值仅当段落之间有分隔符(如换行符 \n)时才起作用,会独立于行间距施加在段落的上下边界。这两项对于分段排版极为重要,例如,若希望列表项之间有更大的视觉分隔,或者标题与正文间有固定的空白区,就可以分别指定这两项,而不会影响普通行距。
行高倍数是另一个容易混淆的概念。设置 kCTParagraphStyleSpecifierLineHeightMultiple 时,指定的是一个浮点倍数,Core Text 会用这个倍数乘以当前字体的点大小,得到目标行高(即基线到基线的距离)。如果该目标行高大于字体的自然行高,多出的空间会被均匀分配到行的上方和下方,使行的视觉高度增大;如果小于自然行高,则会被最小行高(如果设置了 kCTParagraphStyleSpecifierMinimumLineHeight)或字体固有属性约束,一般不会真的缩小行高。关键在于 lineHeightMultiple 和 lineSpacing 可以共存:前者确定基础的行框高度,后者在此基础上追加硬性的额外间距。正确的理解顺序是,先由字体本身加上 lineHeightMultiple(和最大最小行高约束)确定每一行的“原始”行高,然后,在两行之间再插入由 lineSpacing 指定的间隙。当两者配合不当时,容易出现“最后一行的底部留白过大”或“行间距视觉不均匀”的问题,因为段落后间距也会参与底部空白的计算。
完整的设置示例与效果对比
下面通过一个完整的 Core Text 绘制流程来展示多个属性叠加后的视觉效果。我们创建两段文本,第一段使用默认段落样式(仅用作对比),第二段应用复杂的间距设置:行间距 5pt,段前间距 10pt,段后间距 10pt,行高倍数 1.5,同时限制最小行高 18pt,最大行高 24pt。代码使用 Objective-C 编写,在 drawRect: 中完成绘制。
- (void)drawRect:(NSRect)dirtyRect {
CGContextRef context = [[NSGraphicsContext currentContext] CGContext];
CGContextSetTextMatrix(context, CGAffineTransformIdentity);
CGContextSetTextDrawingMode(context, kCGTextFill);
// 创建默认样式属性字符串
NSDictionary *defaultAttributes = @{
(id)kCTFontAttributeName: (__bridge id)CTFontCreateWithName(CFSTR("Helvetica"), 14.0, NULL),
(id)kCTForegroundColorAttributeName: (id)[NSColor blackColor].CGColor
};
NSMutableAttributedString *attrString = [[NSMutableAttributedString alloc] initWithString:@"第一段文本。n第二段文本。" attributes:defaultAttributes];
// 为第二段单独设置段落样式
CGFloat lineSpacing = 5.0;
CGFloat paragraphSpacing = 10.0;
CGFloat paragraphSpacingBefore = 10.0;
CGFloat lineHeightMultiple = 1.5;
CGFloat minLineHeight = 18.0;
CGFloat maxLineHeight = 24.0;
CTParagraphStyleSetting settings[] = {
{kCTParagraphStyleSpecifierLineSpacing, sizeof(CGFloat), &lineSpacing},
{kCTParagraphStyleSpecifierParagraphSpacing, sizeof(CGFloat), ¶graphSpacing},
{kCTParagraphStyleSpecifierParagraphSpacingBefore, sizeof(CGFloat), ¶graphSpacingBefore},
{kCTParagraphStyleSpecifierLineHeightMultiple, sizeof(CGFloat), &lineHeightMultiple},
{kCTParagraphStyleSpecifierMinimumLineHeight, sizeof(CGFloat), &minLineHeight},
{kCTParagraphStyleSpecifierMaximumLineHeight, sizeof(CGFloat), &maxLineHeight}
};
CTParagraphStyleRef paraStyle = CTParagraphStyleCreate(settings, 6);
// 将段落样式应用到第二段(即将整个属性字符串分两段属性)
[attrString addAttribute:(id)kCTParagraphStyleAttributeName
value:(__bridge id)paraStyle
range:NSMakeRange(7, attrString.length - 7)];
CFRelease(paraStyle);
// 创建CTFrame并绘制
CTFramesetterRef framesetter = CTFramesetterCreateWithAttributedString((CFAttributedStringRef)attrString);
CGPathRef path = CGPathCreateWithRect(self.bounds, NULL);
CTFrameRef frame = CTFramesetterCreateFrame(framesetter, CFRangeMake(0, 0), path, NULL);
CTFrameDraw(frame, context);
CFRelease(frame);
CFRelease(path);
CFRelease(framesetter);
// 注意: 属性字符串如果使用 CFRelease 方式管理,需要记得释放,此处省略
}
运行这段代码后,可以观察到“第一段文本”使用默认间距,而“第二段文本”的行高明显增大(因为 1.5 倍的行高倍数),且行与行之间还叠加了 5pt 的 lineSpacing,显得更加疏朗。同时,该段与前一段之间有 10pt 的 paragraphSpacingBefore,在第二段结尾之后又有 10pt 的 paragraphSpacing,因此两段落之间以及文本底部会留有清晰的空白。另外,由于设置了最小行高 18pt,即便字体较小,行高也不会低于这个值,最大行高 24pt 则限制了高度上限,避免因行高倍数过大导致行距失控。
通过调整这些参数,我们可以实现对任意文本布局的精确控制。例如,如果想让行间距更为紧凑,只增大 lineHeightMultiple 并取消 lineSpacing;如果希望段落间距严格固定,则完全依赖 paragraphSpacing 和 paragraphSpacingBefore,并将 lineSpacing 置 0。在实际项目中,这些属性往往被抽象成 UI 调节滑块,由用户实时预览。
常见问题与最佳实践
在 Core Text 中设置垂直间距时,开发者最常踩的坑有:第一,混淆 lineSpacing 和 paragraphSpacing 的作用域。paragraphSpacing 仅在段落后添加空白,对段内行不影响;而 lineSpacing 会作用于所有行,包括段内和段间(只要行间有换行就会叠加)。如果同时设置了较大的 paragraphSpacing 和 lineSpacing,段间行距会变得比预期大得多。第二,忽略 lineHeightMultiple 与 lineSpacing 的叠加效应。正确的调试方式是,先关闭 lineSpacing,将行高倍数调整到位,再逐步添加行间距;第三,没有设置最小/最大行高,导致在动态字体大小下出现异常行距;第四,在处理文本末尾时,最后一行下面的空白可能是 paragraphSpacing 或 lineSpacing 共同造成的,若想要使底部边界紧贴最后一行文本,必须将这两项合理设置为 0。
实际开发中,推荐将段落样式定义封装成可配置的模型对象,并通过工厂方法生成 CTParagraphStyleRef,以方便缓存。因为 CTParagraphStyleCreate 每次都会分配新的不可变对象,重复创建会影响性能。在需要频繁更换样式的场景中,可以预先创建多个样式实例并复用。此外,Core Text 和 NSParagraphStyle 之间存在桥接关系,可以直接将 CTParagraphStyleRef 转换为 NSParagraphStyle,或者通过 NSParagraphStyle 的 mutableCopy 生成 NSMutableParagraphStyle 再转回 Core Text 样式。这种互操作性使得我们能充分利用 UILabel、NSTextView 等高级控件的同时,仍可对底层距离进行精细干预。
理解这些属性之后,再去阅读 Core Text 的文档,你会发现那些看似枯燥的 kCTParagraphStyleSpecifier 描述变得非常清晰。无论是生成 PDF、自定义文本渲染引擎,还是实现 WYSIWYG 编辑器,精准的行间距与段间距控制都是不可或缺的一环。希望本文的解析和代码示例能让你在实际编码中少走弯路。
Core_TextCTParagraphStyle行间距修改时间:2026-08-12 12:01:28