本文翻译整理自:Text Layout Programming Guide(更新日期:2014-02-11
https://developer.apple.com/library/archive/documentation/Cocoa/Conceptual/TextLayout/TextLayout.html#//apple_ref/doc/uid/10000158i
文章目录
- 一、文本布局编程指南简介
- 1、谁应该阅读此文档
- 2、本文件的组织
- 3、另见
- 二、布局管理器
- 1、线程安全
- 2、布局过程
- 3、字形绘图
- 三、Typesetters
- 1、填充线片段矩形
- 2、排字机行为和版本
- 3、NSTypesetter的设计
- 四、线段生成
- 五、沿任意路径布置文本
- 六、绘制字符串
- 1、使用字符串绘制便利方法
- 2、使用NSCell绘制文本
- 3、使用NSLayoutManager绘制文本
- 4、字符串绘制和排字机行为
- 七、计算文本高度
- 八、计算文本行数
- 九、使用文本表
- 1、添加文本表面板
- 2、以编程方式支持文本表
- 3、文本表模型
- 4、控制文本块外观
- 5、表格布局过程
一、文本布局编程指南简介
文本布局编程指南描述了Cocoa文本系统如何布局文本。
文本布局是将一串文本字符、字体信息和页面规范转换为放置在页面特定位置的字形行的过程,适合显示和打印。
1、谁应该阅读此文档
如果您需要了解文本系统布局机制是如何工作的,以及如何直接使用NSLayoutManager
对象来实现文章中描述的编程目标,您应该阅读本文档。
要理解本文档中的信息,您应该阅读 Cocoa文本体系结构指南 。
您还应该了解基本的Cocoa编程约定,例如委托。
2、本文件的组织
本编程主题包含以下文章:
- 布局管理器介绍了
NSLayoutManager
类,描述了它的特性并解释了它如何执行文本布局。 - Typesetters描述了typesetter对象的职责,该对象从
NSTypesetter
的具体子类实例化,该子类代表布局管理器生成行片段和字形位置。 - 行片段生成解释了排字器和文本容器如何协同工作以创建行片段矩形。
- Drawing Strings解释了如何使用布局管理器而不是
NSString
便捷方法来高效地绘制文本字符串。 - 沿任意路径布局文本展示了如何在没有文本视图的情况下使用布局管理器沿计算路径布局字形。
- 计算文本高度显示了如何确定在固定宽度区域中布局的文本块的高度。
- Counting Lines of Text解释了如何以编程方式计算文本字符串中的行数,无论这些行是由硬换行符定义还是在文本容器中布局。
- 使用文本表说明了如何在OS X 10.4及更高版本中为应用程序添加文本表支持。
3、另见
如需进一步阅读,请参阅以下文件:
- 文本系统存储层概述 讨论了Cocoa文本系统用来存储用于文本布局的文本和几何图形信息的工具。
- 文本属性编程主题 描述了Cocoa文本系统维护的文本相关属性,这些属性为段落和文档提供富文本和其他格式信息的区别特征。
二、布局管理器
布局管理器类NSLayoutManager
为Cocoa文本系统中的文本显示提供了中央控制对象。
一个NSLayoutManager
对象执行以下操作:
- 控制文本存储和文本容器对象
- 从字符生成字形
- 计算字形位置并存储信息
- 管理字形和字符的范围
- 在视图请求时在文本视图中绘制字形
- 管理用于段落样式控制的标尺
- 计算文本行的边框矩形
- 控制断字
- 操作字符和字形属性
在model-view-controller范式中,NSLayoutManager
是控制器。
NSTextStorage
是NSMutableAttributedString
的子类,它提供了模型的一部分,保存了一串文本字符,这些字符具有字体、样式、颜色和大小等属性。
NSTextContainer
也可以被认为是模型的一部分,因为它对文本布局所在页面的几何布局进行建模。
NSTextView
(或另一个NSView
对象)提供了显示文本的视图。
NSLayoutManager
充当文本系统的控制器,因为它指示字形生成器将文本存储对象中的字符转换为字形,指示排字器根据一个或多个文本容器对象的尺寸将它们排成行,并协调一个或多个文本视图对象中的文本显示。
图1说明由布局管理器协调的文本显示的组成。
图1 文本显示的组成
您可以将文本系统配置为具有多个布局管理器,如果您需要以多种方式布局同一个NSTextStorage
对象中的文本。
例如,您可能希望文本在一个视图中显示为连续的厨房,在另一个视图中分割成页面。
有关文本对象不同排列的更多信息,请参阅 通用配置。
1、线程安全
一般来说,一个给定的布局管理器(和相关的对象)不应该一次在多个线程上使用。
大多数布局管理器用于主线程,因为它是显示其文本视图的主线程,并且因为后台布局发生在主线程上。
但是,只要对象图包含在单个线程中,您就可以使用NSLayoutManager
在辅助线程上布局和渲染文本。
如果必须在辅助线程上使用布局管理器,则应用程序有责任确保不会从其他线程同时访问对象。
首先,通过禁用后台布局和自动显示,确保在辅助线程上使用布局管理器时不显示与该布局管理器关联的NSTextView
对象(如果有)。
例如,您可以发送文本视图lockFocusIfCanDraw
以阻止主线程显示(完成后发送unlockFocus
)。
其次,通过发送setBackgroundLayoutEnabled:
withNO
来关闭该布局管理器在辅助线程上使用时的后台布局。
2、布局过程
布局管理器在两个单独的步骤中执行文本布局:字形生成和字形布局。
布局管理器懒惰地执行这两个布局步骤,即按需执行。
因此,一些NSLayoutManager
方法会导致字形生成,而其他方法不会,字形布局也是如此。
在生成字形并计算其布局位置后,布局管理器会缓存信息以提高后续调用的性能。
布局管理器缓存字形、属性和布局信息。
它跟踪因更改文本存储中的字符而失效的字形范围。
有两种方法可以自动使字符范围失效:如果需要生成字形或需要布局字形。
如果您愿意,您可以手动使字形或布局信息无效。
当布局管理器收到要求了解无效范围内的字形或布局的消息时,它会根据需要生成字形或重新计算布局。
NSLayoutManager
使用NSTypesetter
对象来执行实际的字形布局。
有关详细信息,请参阅Typesetters。
图2说明了布局过程中涉及的对象的交互。
图2 文本布局过程
以下步骤(编号与图2中的数字相关联)解释了布局管理器如何控制文本布局:
- 文本存储中的文本发生更改,使字形或其布局位置或两者无效。
例如,由于用户在文本视图中编辑文本,文本视图导致文本存储内容的更改。
或者另一个对象可以以编程方式更改文本。 - 文本存储通过发送消息来通知其关联的布局管理器(或多个管理器)无效的字符范围
textStorage:edited:range:changeInLength:invalidatedRange:
该消息指定更改是否影响字符、属性或两者;更改的字符范围;以及属性修复后影响的范围。 - 布局管理器更新其内部数据结构以反映无效范围。
属性更改可能会也可能不会影响字形生成和布局。
例如,更改文本的颜色不会影响它的布局方式。 - 为了通知其关联的文本视图需要重新显示无效区域,布局管理器发送消息
setNeedsDisplayInRect:avoidAdditionalLayout:
。
此时可能会发生以下任何事情。
如果文本视图的无效部分可见,文本视图会向布局管理器询问任何需要的字形及其位置。
如果无效区域当前不可见,视图不会立即调用布局。
但是,当应用程序没有要处理的事件时,可能会发生后台(空闲时间)布局。
默认情况下,后台布局是打开的,尽管您可以为任何单独的布局管理器关闭它。 - 当文本视图向布局管理器询问字形和位置时,布局过程开始。
(其他消息也可以调用布局。
布局管理器头文件和引用留档指定哪些方法导致字形生成和布局发生。) - 布局管理器从新编辑的字符范围生成字形流并缓存字形。
字形生成是将特定字体中的字符快速首次转换为字形。
(没有字体信息,就无法生成字形。)
布局过程的后期阶段可以对字形流进行更改。 - 生成所需字形后,布局管理器调用其排字器将字形布局成一个或多个行片段,向排字器发送
layoutGlyphsInLayoutManager:startingAtGlyphIndex:maxNumberOfLineFragments:nextGlyphIndex:
消息。
在此过程中,排字器可以执行字形替换;例如,它可以替换一个连字符字形来代替两个或多个单字符字形。 - 排字器生成与文本容器通信的行片段矩形,并确定每个字形的位置,如排字器和行片段生成中所述。
- 排字器将带有字形和位置的行片段矩形发送到布局管理器,布局管理器将其内部数据结构中的信息提交为有效布局。
3、字形绘图
除了生成字形和执行布局之外,布局管理器还会绘制文本视图中的字形。
当文本视图要求布局管理器找出给定视图矩形中的字形并显示它们时,就会发生绘图。
布局管理器有绘制字形及其背景的方法。
这些方法通过调用Quartz图形层来完成所有必要的绘图。
它们绘制背景,设置字体和颜色,绘制字形、下划线和任何临时属性。
大多数NSLayoutManager
方法使用容器坐标,而不是视图坐标。
文本系统期望视图坐标被翻转,就像NSTextView
一样。
如果您有一个视图坐标需要转换为容器坐标,减去文本视图的textContainerOrigin
值以获得容器坐标。
字形位置相对于它们的线片段边界矩形的原点表示。
图3显示了这些坐标系之间的关系。
图3 视图、容器和线段坐标
属性是布局管理器在排版和布局过程中应用于字符的品质,例如字体、大小和颜色。
文本存储在与字符串一起存储的字典中保留了许多属性,但其他属性是临时的,仅由布局管理器在布局过程中维护。
临时属性取代与字体或段落相关的属性。
绘图方法还处理与文本视图相关的其他属性——例如,不同的背景颜色——当在文本视图中选择正在绘制的字形时。
绘图方法调用一些其他公共NSLayoutManager
方法,例如drawUnderlineForGlyphRange:underlineType:baselineOffset:lineFragmentRect:lineFragmentGlyphRange:containerOrigin:
,如果你想做不同的事情,你可以覆盖它。
请参阅 文本属性编程主题 了解更多信息。
布局管理器还处理附件的表示在字形绘制期间。
文本系统将附件存储为特殊字符的属性。
典型的附件是文件,但也可以是内存中的数据。
文件附件通常通过绘制图标来处理。
然而,如果您实现不同的行为,附件可以做的远不止这些。
在布局期间,附件单元格(NSTextAttachmentCell
)告诉布局管理器它的大小,因此它可以像字形一样布局。
相应地,文本的行高和字符位置被调整以适应附件单元格。
在绘制期间,布局管理器要求附件单元格自行绘制。
有关详细信息,请参阅 文本附件编程主题 。
布局管理器保留和重用尽可能多的布局信息,以最小化重新计算字形位置。
例如,如果已经为需要布局的无效字符范围生成了字形,布局管理器会尝试优化布局过程。
在最好的情况下,布局中的这些漏洞可以通过移动文本容器中的行片段位置来填补。
NSLayoutManager
提供了一个公共API,用于获取字符中的字形。
但是,这个过程很复杂:您不能简单地将单个字符转换为字形,因为字符和字形之间的关系是多对多的。
也就是说,文本存储中的一个字符可以映射到多个字形,反之亦然。
因此,您可以使用NSLayoutManager
方法glyphRangeForTextContainer:
获取文本容器中所有字符的字形,或者glyphRangeForCharacterRange:actualCharacterRange:
获取一系列字符的字形。
三、Typesetters
布局管理器使用称为排版器的辅助对象来布局行片段中的字形。
排版器对象是从NSTypesetter
的具体子类实例化的。
与Cocoa文本系统中的其他对象一起工作,排字器创建行片段矩形,将字形放置在行片段中,通过换行和断字确定换行符,并处理制表符定位。
排字器还确定行间距、段落行间距和双向字形的从右到左定位。
1、填充线片段矩形
排字器对象生成行片段通过与文本容器通信,如行片段生成中所述。
排字器确定合适的行片段大小和位置,并以容器坐标返回。
创建行片段矩形后,排字人员根据layoutGlyphsInLayoutManager:startingAtGlyphIndex:maxNumberOfLineFragments:nextGlyphIndex:
来自布局管理器的消息确定字形在其中的位置。
排字人员报告相对于其行片段边界矩形原点的字形位置。
排字人员填充行片段,直到它超出行片段的宽度。
然后它通过包装文本或连字符连接最后一个单词来创建换行符。
在此步骤中,排字人员执行字形替换,如果需要,并且可以添加字形到字形流中。
例如,排字人员可以将连字符字形替换为一个或多个单字符字形,或者可以在字形流中添加连字符。
NSTypesetter
子类可以通过覆盖shouldBreakLineByWordBeforeCharacterAtIndex:
方法来控制单词边界的换行。
同样,子类可以通过覆盖shouldBreakLineByHyphenatingBeforeCharacterAtIndex:
方法来干预断字。
每当布局行的宽度除以行矩形的宽度超过断字阈值由布局管理器维护,排字器调用一个内部断字器对象,该对象试图在行中的最后一个单词中找到断字点。
如果断字器找到一个好的点,排字器会在行片段矩形的末尾插入一个连字符号。
断字由称为断字因子的阈值控制,该阈值由布局管理器维护。
您可以使用NSLayoutManager
方法setHyphenationFactor:
.断字因子是一个介于0.0和1.0之间的浮点数。
默认情况下,它的值为0.0,这意味着断字是关闭的。
将断字因子设置为1.0会导致排字程序总是尝试断字。
2、排字机行为和版本
文本系统使用一个共享的、可重入的排字器实例,该实例由NSLayoutManager
方法typesetter
提供。
NSLayoutManager
方法setTypesetterBehavior:
在OS X 10.2版之前附带的原始默认排字器中选择一个封装Apple Type Services(ATS)的排字器OS X 10.2版附带的、OS X 10.3版附带的基于ATS的排字器的增强版本,以及OS X 10.4版中引入的排字器行为。
NSTypesetterBehavior
枚举定义了相关常量。
实现原始排字行为的NSTypesetter
子类NSSimpleHorizontalTypesetter
,在NSTypesetter.h
头文件中定义。
NSSimpleHorizontalTypesetter
仅支持从左到右扫描和向下移动的字形布局。
NSSimpleHorizontalTypesetter
在OS X 10.4及更高版本中已弃用。
OS X版本10.2中引入的排字器行为由NSATSTypesetter
类实现,该类定义在NSATSTypesetter.h
头文件中。
NSATSTypesetter
提供了增强的行间距和字符行间距精度,并支持更多的语言,包括双向语言,比原来的NSSimpleHorizontalTypesetter
。
OS X版本10.3引入了NSATSTypesetter
的新版本,它声明了NSATSTypesetter
和NSGlyphGenerator
的公共API。
这些API打开了排字器,以便与具有不同于传统Cocoa文本系统设计的自定义布局引擎一起使用,如NSTypesetter的设计中所述。
在OS X版本10.4中,这些API转移到NSTypesetter
。
除非您需要早期排字器版本的特定行为,否则您应该使用最新版本的NSATSTypesetter
或对其进行子类化。
在测量和呈现文本时使用相同的排版行为非常重要,以避免段落行间距、行距和头部缩进处理的差异。
有关排版行为不匹配的更多信息,请参阅字符串绘制和排版行为。
3、NSTypesetter的设计
在Cocoa文本系统中,布局管理器拥有排字器和字形生成器作为私有对象,并维护文本容器数组,如布局管理器中所述。
排字器概念与布局管理器和文本容器概念紧密耦合。
排字器的职责是用字形生成器提供的字形填充数组中的文本容器。
默认情况下,NSATSTypesetter
以这种方式工作。
然而,NSTypesetter
旨在使开发人员能够将其与Cocoa文本系统的其他组件分离。
NSTypesetter
的设计将原始的、核心的排字器与Cocoa文本系统的其余部分隔离开来,如图1所示。
NSTypesetter
具有核心排版引擎、布局阶段接口和字形存储接口层,与文本系统通信并驱动布局引擎。
核心排版引擎通过简化的API提供高级排版功能。
核心排版引擎在无限的水平线中布置字形,对文本容器或文本方向一无所知。
字形存储接口层调用文本系统生成行片段矩形,并确保它们正确地适合页面。
图1 NSTypesetter的设计
针对NSTypesetter
的API设计有两个主要目标。
首先是打破两个类和NSLayoutManager
之间的联系,允许开发人员在不使用NSLayoutManager
的情况下深入挖掘Cocoa的排版功能。
第二个目标是提供覆盖点,允许开发人员扩展排版过程的各个方面。
此外,对这些类的直接访问使得将带有自己布局引擎的Carbon、Windows或UNIX应用程序移植到Cocoa变得更加容易。
NSTypesetter
将其方法分类如下:
- 字形存储接口(
NSGlyphStorageInterface
)声明了与字形存储设施(Cocoa中的NSLayoutManager
)接口的所有原始方法。
通过覆盖所有这些方法,应用程序可以实现一个与自定义字形存储设施和布局管理器交互的NSTypesetter
子类。
这些方法的默认实现调用NSLayoutManager
。
NSTypesetter
从布局管理器复制当前正在处理的行片段的字形,并对副本执行布局、替换、插入和删除。
作为布局过程的最后一步,它将生成的字形移动到字形存储中。
大多数NSGlyphStorageInterface
方法包括insertGlyph:atGlyphIndex:characterIndex:
用于将结果复制回字形存储的最后一步。
由于字形索引和字符索引在布局过程中是标称的,因此您应该等到最后一个过程再修改NSLayoutManager
。 - 布局阶段接口(
NSLayoutPhaseInterface
)声明在文本布局期间调用的控制点(如果实现)。
这些方法调用充当布局过程中发生的事件的通知。
如果需要,NSTypesetter
子类可以覆盖这些方法中的任何一个,以修改布局过程的各个方面。
例如,排字器调用willSetLineFragmentRect:forGlyphRange:usedRect:baselineOffset:
在它调用之前setLineFragmentRect:forGlyphRange:usedRect:baselineOffset:
将实际的行片段矩形位置存储在布局管理器中。 - 其余的
NSTypesetter
方法是原始的排字器方法,自定义布局管理器可以调用这些方法来直接控制排字器。
通过其分层设计,NSTypesetter
可以实例化并在Cocoa文本系统的标准配置中使用,或者子类化并适应于与另一个文本系统一起工作,即使是一个对如何执行页面布局有完全不同概念的系统。
四、线段生成
一个NSTypesetter
对象在NSTextContainer
对象中以字形行的形式放置文本。
这些行在NSTextContainer
对象中的布局由其形状决定。
例如,如果文本容器的某些部分比其他部分窄,则这些部分中的行必须缩短;如果区域中有孔,则某些行必须被分割;如果整个区域都有间隙,则必须移动与之重叠的行以进行补偿。
文本系统当前提供的内置排字机仅支持水平文本布局。
然而,文本系统可以支持排字机沿水平或垂直以及任何方向排列文本。
这种类型的移动称为扫描方向,由Objective-C中的NSLineSweepDirection
类型和Java中的扫描方向常量表示。
线条然后前进的方向称为线条移动方向,并由Objective-C中的NSLineMovementDirection
类型和线条移动Java常量表示。
每个都以不同的方式影响线段矩形的调整:矩形可以沿扫描方向移动或缩短,并在线移动方向上移动(但不能调整大小)。
排字器对象为给定的行提出一个矩形,然后要求NSTextContainer
对象调整矩形以适应。
提出的矩形通常跨越文本容器的边界矩形,但它可以更窄或更宽,也可以部分或完全位于边界矩形之外。
排字器发送文本容器以调整提出的矩形的消息是lineFragmentRectForProposedRect:sweepDirection:movementDirection:remainingRect:
,它返回可用于提出的矩形的最大矩形,基于文本布局的方向。
它还返回一个包含任何剩余空间的矩形,例如留在文本容器中的孔或间隙的另一侧。
图1中说明了这个过程。
图1 不规则文本容器中的行片段拟合
对于图1中的三个示例,扫描方向是NSLineSweepRight
,线移动方向是NSLineMovesDown
。
在第一个示例中,建议的矩形跨越区域的边界矩形,并被文本容器缩短以适合沙漏形状,没有余数。
在第二个例子中,提议的矩形穿过一个孔,因此文本容器必须返回一个较短的矩形(左边的白色矩形)和一个余数(右边的白色矩形)。
排字人员提议的下一个矩形将是这个余数矩形,文本容器将原封不动地返回它。
在第三个例子中,一个间隙穿过整个文本容器。
这里,文本容器将建议的矩形向下移动,直到它完全位于容器的区域内。
如果这里的行移动方向是NSLineDoesntMove
,文本容器将必须返回NSRect.ZeroRect
,表明该行根本不合适。
在这种情况下,排字人员可以提出不同的矩形或移动到不同的容器。
当文本容器移动行片段矩形时,布局管理器会将此考虑到后续行。
排字器在实际将文本放入矩形时进行最后一次调整。
这种调整是由NSTextContainer
对象固定的少量调整,称为行片段填充,它定义了行片段矩形留空的每一端的部分。
文本在行片段矩形中插入这个量(矩形本身不受影响)。
填充允许对文本容器边缘和任何孔周围的区域进行小规模调整,并防止文本直接与该区域附近显示的任何其他图形相邻。
您可以使用setLineFragmentPadding:
方法更改填充的默认值。
请注意,行片段填充不是表示边距的合适方法;您应该为文档边距设置NSTextView
对象的位置和大小,或者为文本边距设置段落边距属性。
除了行片段矩形本身之外,排字器还返回一个名为已使用矩形的矩形。
这是行片段矩形中实际包含要绘制的字形或其他标记的部分。
按照惯例,这两个矩形都包括行片段填充和根据字体的行高度量和段落的行距参数计算的行间空间。
但是,段落行间距(前后)和文本周围添加的任何空间,例如由中心间距文本引起的空间,仅包含在行片段矩形中,而不包含在已使用的矩形中。
有关文本容器的更多信息,请参见布局几何:NSTextContainer类。
有关布局过程的更多信息,请参见布局管理器。
五、沿任意路径布置文本
Cocoa文本系统通常在文本视图中以水平线排列文本。
然而,也可以只使用存储字符和生成字形所需的文本对象,同时手动计算最终字形位置并自己绘制字形。
要沿着任意路径布局文本,您需要使用三个基本的非视图文本对象:NSTextStorage
保存文本,NSTextContainer
建模文本布局区域,NSLayoutManager
生成字形和布局信息。
最后,您在自定义NSView
对象中绘制字形。
首先,创建并初始化文本存储、文本容器和布局管理器的实例。
使用要布局的文本字符串初始化文本容器。
然后将这些对象连接在一起:文本存储对象保留对布局管理器的引用,布局管理器保留对文本容器的引用。
例1可以驻留在显示文本的自定义NSView
对象的初始化方法中,它说明了这个过程。
例1 创建和配置非视图文本对象
NSTextStorage *textStorage;
NSLayoutManager *layoutManager;
NSTextContainer *textContainer;
textStorage = [[NSTextStorage alloc] initWithString:@"This is the string of text in the text storage."];
layoutManager = [[NSLayoutManager alloc] init];
textContainer = [[NSTextContainer alloc] init];
[layoutManager addTextContainer:textContainer];
[textContainer release];
[textStorage addLayoutManager:layoutManager];
[layoutManager release];
您“添加”这些引用而不是“设置”它们的原因是因为布局管理器可以有多个文本容器,而一个文本存储对象可以有多个布局管理器。
还要注意内存管理此过程的含义:因为布局管理器保留文本容器,而文本存储保留布局管理器,所以您可以在连接对象时立即释放它们。
但是,您应该在dealloc
方法中显式释放文本存储对象。
告诉布局管理器不要使用屏幕字体,因为它们不能正确缩放或旋转(默认情况下允许使用屏幕字体):
[layoutManager setUsesScreenFonts:NO];
接下来,强制布局管理器为文本存储对象中的字符生成字形,并让它计算字形在一个简单的矩形容器中布局的位置。
然后转换位置并调用布局管理器来绘制字形。
这可以在视图的drawRect:
方法中完成。
以下消息强制布局并返回文本存储对象中字符串的字形:
NSRange glyphRange = [layoutManager glyphRangeForTextContainer:textContainer];
例2中的代码完成了实际的绘图。
例2 绘制字符串
NSGraphicsContext *context = [NSGraphicsContext currentContext];
NSAffineTransform *transform = [NSAffineTransform transform];
[transform rotateByDegrees:30.0];
[context saveGraphicsState];
[transform concat];
[self lockFocus];
[layoutManager drawGlyphsForGlyphRange:glyphRange
atPoint:NSMakePoint(50.0, 50.0)];
[self unlockFocus];
[context restoreGraphicsState];
这个片段只是在要求布局管理器绘制字形之前将图形上下文逆时针旋转30度,但是可以使用另一种算法来计算更复杂的布局路径。
这个讨论简化了这项技术,以便专注于与布局管理器的交互。
这个 CircleView 示例提供了一个应用程序的源代码,说明了该技术,但以更健壮的方式完成。
CircleView计算一个位置并单独绘制每个字形。
六、绘制字符串
在Cocoa中以编程方式绘制文本的方法有三种:使用NSString
或NSAttributedString
的方法,使用NSCell
的方法,以及直接使用NSLayoutManager
。
NSLayoutManager
是最有效的。
1、使用字符串绘制便利方法
该NSString
类有两个方便的方法用于直接在NSView
对象中绘制字符串对象:drawAtPoint:withAttributes:
和drawInRect:withAttributes:
。
对于具有与范围和单个字符相关联的多个属性的字符串,必须使用NSAttributedString
。
您可以使用drawAtPoint:
或drawInRect:
方法绘制字符串(在焦点集中的NSView
中)。
这些方法设计用于绘制少量文本或必须很少绘制的文本。
每次调用它们时,它们都会创建和处理各种支持文本对象,包括NSLayoutManager
。
然而,对于文本的重复绘制,字符串绘制便利方法效率不高,因为它们在幕后做了很多工作。
例如,要绘制Unicode文本,您必须首先将字符转换为字形,即字体的元素。
字形生成很复杂,因为几个字符可能会产生一个字形,反之亦然,这取决于上下文和其他因素。
此外,系统为字形转换做了大量设置工作,字符串绘制便利方法每次绘制字符串时都会做这项工作。
使用布局管理器直接提供了显着的性能改进,因为它缓存了字形布局和大小信息。
2、使用NSCell绘制文本
此外,NSCell
类还提供了用于显示和编辑文本的原语。
NSBrowser
和NSTableView
使用NSCell
文本绘制方法。
NSCell
的文本绘制比使用字符串方便方法更有效,因为它缓存了一些信息,例如文本矩形的大小。
因此,对于重复显示相同的文本,NSCell
效果很好,但要最有效地显示任意文本字符串,请直接使用NSLayoutManager
。
3、使用NSLayoutManager绘制文本
如果您使用NSTextView
类来显示文本,或者从Interface Builder数据面板拖动文本视图对象,或者使用NSTextView
方法initWithFrame:
方法以编程方式创建文本视图,Cocoa会自动创建一个NSLayoutManager
实例来绘制文本。
但是,如果您使用NSTextView
initWithFrame:textContainer:
方法创建文本视图,或者如果您需要将文本直接绘制到不同类型的NSView
对象中,则必须显式创建NSLayoutManager
。
要使用NSLayoutManager
将文本字符串直接绘制到视图中,您必须创建并初始化文本系统的三个基本非视图组件。
首先创建一个NSTextStorage
对象来保存字符串。
然后创建一个NSTextContainer
对象来描述文本的几何区域。
然后创建NSLayoutManager
对象,并通过将布局管理器添加到文本存储对象并将文本容器添加到布局管理器来将三个对象挂钩在一起。
例1中的代码可以驻留在视图的initWithFrame:
方法中,说明了这个过程。
例1 创建和配置非视图文本对象
NSTextStorage *textStorage = [[NSTextStorage alloc]
initWithString:@"This is the text string."];
NSLayoutManager *layoutManager = [[NSLayoutManager alloc] init];
NSTextContainer *textContainer = [[NSTextContainer alloc] init];
[layoutManager addTextContainer:textContainer];
[textContainer release];
[textStorage addLayoutManager:layoutManager];
[layoutManager release];
您可以释放文本容器,因为布局管理器保留了它,您可以释放布局管理器,因为文本存储对象保留了它。
要想直接在视图中绘制字形,可以在视图的drawRect:
方法中使用NSLayoutManager
方法drawGlyphsForGlyphRange:
。
但是,必须先将要绘制的字符范围转换为字形范围。
如果需要在文本存储对象中选择文本的子范围,可以使用glyphRangeForCharacterRange:actualCharacterRange:
方法。
如果要在文本存储对象中绘制整个字符串,可以使用glyphRangeForTextContainer:
方法,如例2所示(它使用例1中的layoutManager
变量名)。
例2 直接在视图中绘制字形
NSRange glyphRange = [layoutManager
glyphRangeForTextContainer:textContainer];
[self lockFocus];
[layoutManager drawGlyphsForGlyphRange: glyphRange atPoint: rect.origin];
[self unlockFocus];
4、字符串绘制和排字机行为
Cocoa在排字行为方面绘制文本的三种方法之间存在差异,排字行为和版本中有描述。
默认情况下,应用程序套件提供的字符串绘制便利方法和NSCell
对象使用NSTypesetterBehavior_10_2_WithCompatibility
,而NSLayoutManager
对象使用NSTypesetterLatestBehavior
。
在测量和渲染文本时使用相同的排字行为非常重要,以避免段落行间距、行距和头部缩进处理的差异。
如果您必须以一种方式测量文本并以另一种方式呈现文本,请使用NSLayoutManager
和NSTypesetter定义的setTypesetterBehavior:
方法NSTypesetter
以匹配。
例如,如果您需要使用NSLayoutManager
对象来测量文本并使用便利字符串绘制方法来绘制它,请将布局管理器的排字程序行为更改为NSTypesetterBehavior_10_2_WithCompatibility
。
七、计算文本高度
有时您可能需要知道文本字符串在固定宽度区域中布局后形成的文本块的高度。
NSLayoutManager
类可以非常简单地做到这一点。
本文说明了在单个函数中实现的技术。
注意 : 您不需要使用此技术来查找单行文本的高度。
NSLayoutManager
方法defaultLineHeightForFont:
返回该值。
默认行高是字体最高升序的总和,加上其最深降序的绝对值,加上其前导。
计算文本高度的基本技术使用文本系统的三个基本非视图组件:NSTextStorage
、NSTextContainer
和NSLayoutManager
。
文本存储对象保存要测量的字符串;文本容器指定布局区域的宽度;布局管理器进行布局并返回高度。
要为计算设置文本系统,您需要测量文本字符串、字符串的字体和文本容器建模的区域的宽度。
您可以将这些值传递给具有如下声明的函数:
float heightForStringDrawing(NSString *myString, NSFont *myFont,
float myWidth);
函数声明中的参数名称在定义方法体的以下代码片段中显示为变量。
首先,您实例化所需的文本对象并将它们挂钩在一起。
您为文本存储对象使用指定的初始化器,它将字符串指针作为参数。
同样,文本容器的指定初始化器将容器大小作为其参数。
您将容器宽度设置为所需的宽度,并将高度设置为任意大的值,如以下代码片段所示:
NSTextStorage *textStorage = [[[NSTextStorage alloc]
initWithString:myString] autorelease];
NSTextContainer *textContainer = [[[NSTextContainer alloc]
initWithContainerSize: NSMakeSize(myWidth, FLT_MAX)] autorelease];
NSLayoutManager *layoutManager = [[[NSLayoutManager alloc] init]
autorelease];
创建文本对象后,您可以将它们挂钩在一起:
[layoutManager addTextContainer:textContainer];
[textStorage addLayoutManager:layoutManager];
您不需要释放文本容器和布局管理器,因为您在初始化时将它们添加到自动释放池中。
接下来,通过将字体属性添加到文本存储对象中整个字符串的范围来设置字体。
将行片段填充设置为0
以获得准确的宽度测量。
(填充用于页面布局,以防止文本容器中的文本与页面上的其他元素(如图形)紧靠。)
[textStorage addAttribute:NSFontAttributeName value:myFont
range:NSMakeRange(0, [textStorage length])];
[textContainer setLineFragmentPadding:0.0];
最后,因为布局管理器懒惰地执行布局,所以根据需要,您必须强制它布局文本,即使您不需要此函数返回的字形范围。
然后,您可以简单地向布局管理器询问布局文本所占矩形的高度,并假设此代码在函数实现中,返回值:
(void) [layoutManager glyphRangeForTextContainer:textContainer];
return [layoutManager
usedRectForTextContainer:textContainer].size.height;
八、计算文本行数
此任务展示了如何以编程方式计算文本块中的行数。
行可以由文本字符串中的硬换行符定义,也可以是文本布局机制在包装文本以适应文本容器时生成的行。
由硬换行符定义的文本行,如回车符和换行符,被认为是段落。
也就是说,文本布局引擎生成大小适合文本容器的行片段,直到它到达硬换行符,因此最后一个片段通常短于容器宽度。
但是,如果行被布局到文本容器中,比硬换行符之间最长的字形宽,那么每个段落都是单行。
要计算文本字符串中硬换行符的数量,可以使用NSString
方法getLineStart:end:contentsEnd:forRange:
和lineRangeForRange:
。
这些方法将字符范围作为输入,并返回包含该范围的行。
它们将行定义为以回车、换行符、回车和换行符一起结束的字符范围(按此顺序,通常称为CRLF),以及用于行分隔符和段落分隔符的Unicode字符。
例如,例1中的代码将NSString
变量string
中的行数放入numberOfLines
。
例1 计算硬换行符
NSString *string;
unsigned numberOfLines, index, stringLength = [string length];
for (index = 0, numberOfLines = 0; index < stringLength; numberOfLines++)
index = NSMaxRange([string lineRangeForRange:NSMakeRange(index, 0)]);
这个紧凑的代码片段首先创建一个仅包含字符串中第一个字符的范围。
lineRangeForRange:
方法将包含该字符的行作为包含硬换行符(或多个字符)的范围返回。
NSMaxRange
函数返回下一行中第一个字符的索引,即分配给index
的值。
numberOfLines
变量递增,for
循环重复,直到index
大于string
的长度,此时numberOfLines
包含string
中的行数,由硬换行符定义。
要计算文本布局机制在包装文本以适应文本容器时生成的行数,您可以从布局管理器获取信息,如例2所示。
例2 包装文本的计数行
NSLayoutManager *layoutManager = [textView layoutManager];
unsigned numberOfLines, index, numberOfGlyphs =
[layoutManager numberOfGlyphs];
NSRange lineRange;
for (numberOfLines = 0, index = 0; index < numberOfGlyphs; numberOfLines++){
(void) [layoutManager lineFragmentRectForGlyphAtIndex:index
effectiveRange:&lineRange];
index = NSMaxRange(lineRange);
}
此代码假定您对配置有布局管理器、文本存储和文本容器的文本视图有引用。
文本视图返回对布局管理器的引用,然后该管理器返回其关联文本存储中所有字符的字形数,并在必要时执行字形生成。
然后for
循环开始布局文本并计算生成的行片段。
NSLayoutManager
方法lineFragmentRectForGlyphAtIndex:effectiveRange:
强制在传递给它的索引处布局包含字形的行。
该方法返回行片段占用的矩形(此处忽略),并通过引用返回布局后行中字形的范围。
方法计算出一行后,NSMaxRange
函数返回大于范围内最大值的索引1,即下一行中第一个字形的索引。
numberOfLines
变量递增,for
循环重复,直到index
大于文本中的字形数,此时numberOfLines
包含布局过程产生的行数,由换行定义。
此策略会对文本存储对象中包含的整个文本进行布局,并计算布局所需的行数,而不管填充的文本容器数量或显示它所需的文本视图数量。
要获取单个页面(由文本容器建模)中的行数,您可以使用NSLayoutManager
方法glyphRangeForTextContainer:
并将行计数for
循环限制在该范围内,而不是{0, numberOfGlyphs}
范围,该范围包括所有文本。
九、使用文本表
Cocoa文本系统支持OS X 10.4及更高版本中的文本表。
涉及的主要类是NSTextTable
,它代表一个表,NSTextTableBlock
,它代表一个在表中显示为单元格的文本块,以及它的超类NSTextBlock
。
本文解释了如何为您的应用程序添加表支持。
1、添加文本表面板
NSTextView
内置了对文本表格的支持,它提供了向文本视图添加表格支持的最简单方法。
此表格支持采用action方法的形式orderFrontTablePanel:
.此方法将表格插入到文本视图中,并打开漂浮在应用程序窗口上的无模式实用程序窗口。
此表格面板使用户能够在光标或选择位于表格中时操作表格的属性。
表格面板显示在文本表格面板中。
图1 文本表格面板
用户可以通过直接使用光标操作来更改表的其他方面,例如单元格大小和内容。
要使文本表格面板在文本视图中可用,请使用Interface Builder将orderFrontTablePanel:
action方法添加到急救人员,并将其连接到菜单项,如连接操作方法中所示。
图2 连接action方法
NSTextView
为打开列表、链接和段落行间距面板定义了类似的操作方法。
2、以编程方式支持文本表
如果您不想使用文本表格面板,您可以通过直接使用NSTextTable
和相关类以编程方式支持表格。
该组中的基本类是NSTextBlock
,它表示在文本容器的子区域中布置的文本块。
使用表格时,您可以使用它的子类NSTextTableBlock
,它表示在表格中显示为单元格的文本块。
表格本身由一个单独的类NSTextTable
表示表格中单元格的所有 NSTextTableBlock
对象引用NSTextTable
对象,该对象控制它们的大小和位置。
文本块显示为段落的属性,作为段落样式的一部分。
NSParagraphStyle
对象可以有一个表示包含段落的表格单元格的文本块数组。
段落样式使用数组,因为表格单元格可以嵌套,文本块在数组中从最外到最内排序。
例如,如果block 1包含四个段落,中间两个也在内部block 2中,那么第一和第四段的文本块数组是(block 1),第二和第三段的数组是(block 1,block 2)。
使用NSMutableParagraphStyle
方法setTextBlocks:将文本块添加到段落样式对象setTextBlocks:
而NSParagraphStyle
方法textBlocks
返回数组。
要以编程方式实现文本表,请使用以下步骤序列:
- 为表创建属性字符串。
- 创建表对象,设置列数。
- 为行的第一个单元格创建文本表格块,引用表格对象。
- 设置文本块的属性。
- 为单元格创建段落样式对象,将文本块设置为属性(以及任何其他段落属性,例如对齐方式)。
- 为单元格创建属性字符串,添加段落样式作为属性。
单元格字符串必须以段落标记结尾,例如换行符。 - 将单元格字符串附加到表格字符串。
- 对表中的每个单元格重复步骤3-7。
在表格创建方法和表格单元格创建方法中显示的方法执行前面列表中的步骤。
(本文中的所有示例方法都在基于文档的应用程序的NSDocument
子类中定义,但它们可以很容易地属于另一个对象,例如文本视图。)
表格单元格创建方法对表格中的每个单元格执行步骤3-6,使用胖边框和对比色进行说明。
例1 表创建方法
- (NSMutableAttributedString *) tableAttributedString
{
// tableString is an ivar declared in the header file as NSMutableAttributedString *tableString;
tableString = [[NSMutableAttributedString alloc] initWithString:@"\n\n"];
NSTextTable *table = [[NSTextTable alloc] init];
[table setNumberOfColumns:2];
[tableString appendAttributedString:[self tableCellAttributedStringWithString:@"Cell1\n"
table:table
backgroundColor:[NSColor greenColor]
borderColor:[NSColor magentaColor]
row:0
column:0]];
[tableString appendAttributedString:[self tableCellAttributedStringWithString:@"Cell2\n"
table:table
backgroundColor:[NSColor yellowColor]
borderColor:[NSColor blueColor]
row:0
column:1]];
[tableString appendAttributedString:[self tableCellAttributedStringWithString:@"Cell3\n"
table:table
backgroundColor:[NSColor lightGrayColor]
borderColor:[NSColor redColor]
row:1
column:0]];
[tableString appendAttributedString:[self tableCellAttributedStringWithString:@"Cell4\n"
table:table
backgroundColor:[NSColor cyanColor]
borderColor:[NSColor orangeColor]
row:1
column:1]];
[table release];
return [tableString autorelease];
}
例2 表格单元格创建方法
- (NSMutableAttributedString *) tableCellAttributedStringWithString:(NSString *)string
table:(NSTextTable *)table
backgroundColor:(NSColor *)backgroundColor
borderColor:(NSColor *)borderColor
row:(int)row
column:(int)column
{
NSTextTableBlock *block = [[NSTextTableBlock alloc]
initWithTable:table
startingRow:row
rowSpan:1
startingColumn:column
columnSpan:1];
[block setBackgroundColor:backgroundColor];
[block setBorderColor:borderColor];
[block setWidth:4.0 type:NSTextBlockAbsoluteValueType forLayer:NSTextBlockBorder];
[block setWidth:6.0 type:NSTextBlockAbsoluteValueType forLayer:NSTextBlockPadding];
NSMutableParagraphStyle *paragraphStyle =
[[NSParagraphStyle defaultParagraphStyle] mutableCopy];
[paragraphStyle setTextBlocks:[NSArray arrayWithObjects:block, nil]];
[block release];
NSMutableAttributedString *cellString =
[[NSMutableAttributedString alloc] initWithString:string];
[cellString addAttribute:NSParagraphStyleAttributeName
value:paragraphStyle
range:NSMakeRange(0, [cellString length])];
[paragraphStyle release];
return [cellString autorelease];
}
在表格创建方法和表格单元格创建方法中的代码产生如Table输出所示的表格。
图3 表输出
要在文本视图中显示的文本中插入表格,请实现一个操作方法,如 例 3中所示。该方法会在文本视图中插入 表格创建方法 中构建的表格字符串,替换当前选区(如果没有选区,则插入点),并确保发送适当的通知和委托消息。
Listing 3 Table insertion action method
- (void) insertMyTable:(id)sender
{
NSRange charRange = [myTextView rangeForUserTextChange];
NSTextStorage *myTextStorage = [myTextView textStorage];
if ([myTextView isEditable] && charRange.location != NSNotFound)
{
NSMutableAttributedString *attrStringToInsert = [self tableAttributedString];
if ([myTextView shouldChangeTextInRange:charRange replacementString:nil])
{
[myTextStorage replaceCharactersInRange:charRange
withAttributedString:attrStringToInsert];
[myTextView setSelectedRange:NSMakeRange(charRange.location, 0)
affinity:NSSelectionAffinityUpstream stillSelecting:NO];
[myTextView didChangeText];
}
}
}
NSAttributedString
具有以下方便的方法,您可以使用这些方法来确定文本块或表格所覆盖的字符串范围:
rangeOfTextBlock:atIndex:
rangeOfTextTable:atIndex:
如果给定位置不在指定的块或表中,则这些方法返回一个范围(NSNotFound, 0)
。
3、文本表模型
Cocoa文本表模型主要源自由超文本标记语言和CSS定义的表模型,其中表是从单元格行构建的。
有关CSS表模型的描述,请参阅以下URL:
http://www.w3.org/TR/CSS21/tables.html
这种关联性提供了另一种在文本中创建表格的方法。
您可以用超文本标记语言定义表格,并使用该数据初始化属性字符串。
然后,该字符串的属性定义由Cocoa文本系统呈现的表格。
为此,您可以使用以下NSAttributedString
初始化方法:
initWithHTML:documentAttributes:
initWithHTML:options:documentAttributes:
initWithHTML:baseURL:documentAttributes:
initWithData:options:documentAttributes:error:
4、控制文本块外观
文本块的位置由其文本容器或包含块决定。对于表示表中单元格的文本表块,大小和位置由文本表以及该块与表中其他块的关系控制。当初始化NSTextTableBlock
对象时,您将其行和列位置指定为其表中的单元格,还指定它是跨越多行还是多列。NSTextTableBlock
初始化方法是:
initWithTable:startingRow:rowSpan:startingColumn:columnSpan:
表格单元格创建方法显示了此方法的使用。
此外,您可以为每个块指定多个维度的值,作为绝对值或包含块的百分比。这些维度包括以下内容:
- 宽度
- 高度
- 最小宽度
- 最小高度
- 最大宽度
- 最大高度
- 四个边中每个边的填充宽度。填充是包围块内容区域的空间,延伸到边界。
- 四个边中每个边的边框宽度。边框是填充和边距之间的空间,通常着色以呈现可见的边界。
- 四个边的边距宽度。边距是边界周围的空间。
这些维度的默认值为0
,表示没有填充、边框或边距,以及自然宽度和高度。单个文本块的自然宽度和高度延伸到其包含块(或文本容器)的宽度和高度;多个块的自然宽度和高度均匀地划分其包含块的空间。
以下方法指定或返回与这些维度关联的值:
setValue:type:forDimension:
valueForDimension:
valueTypeForDimension:
setWidth:type:forLayer:
setWidth:type:forLayer:edge:
widthForLayer:edge:
widthValueTypeForLayer:edge:
在这些方法中,值类型是指绝对值或百分比值。维度是指块的最小、最大和全宽和高。层是指填充、边框和边距。这些参数由NSTextBlock中描述的常量指定NSTextBlock
NSTextBlock
提供以下方法来指定和返回块的背景和边框颜色:
backgroundColor
setBackgroundColor:
borderColorForEdge:
setBorderColor:forEdge:
setBorderColor:
默认情况下,颜色值为nil
,表示没有颜色。请注意,没有颜色的边框是不可见的。
5、表格布局过程
在文本布局期间,排字器使用NSTextBlock
来确定文本块的布局矩形。
如果文本块是NSTextTableBlock
的实例,它调用其包含的NSTextTable
实例来执行计算。
排字器将这些计算的结果存储在其布局管理器中。
NSTextBlock
、NSTextTable
和NSLayoutManager
中有特定于此布局过程的方法,如果需要干预该过程,可以使用这些方法。
为了开始文本块布局过程,排字者提出一个大矩形,文本块应该适合这个矩形。
对于最外面的块,这是由文本容器决定的;对于内部块,它是由包含块确定的。
然后,块对象决定它应该在提议的矩形中实际占据什么区域。
文本块实际上决定了两个矩形:第一,布局矩形,块中的文本将在其中布局;第二,边界矩形,它包含用于填充、边框、边框装饰和边距的额外空间。
文本块在排字机布置第一个字形之前立即计算布局矩形,因为它是块中所有后续文本布局所必需的。
布局矩形通常相当高,因为此时要布置的文本的高度尚未确定。
文本块在块中最后一个字形布置后立即计算边界矩形,它基于块中文本使用的实际矩形。
在某些情况下,随着同一表中的附加块的布局,边界矩形可能会随后进行调整。
要查找布局和边界矩形,排字器调用以下NSTextBlock
方法:
rectForLayoutAtPoint:inRect:textContainer:characterRange:
boundsRectForContentRect:inRect:textContainer:characterRange:
反过来,NSTextTableBlock
对象使用以下方法调用其NSTextTable
对象来执行这些计算:
rectForBlock:layoutAtPoint:inRect:textContainer:characterRange:
boundsRectForBlock:contentRect:inRect:textContainer:characterRange:
排字器使用以下方法将这些方法的结果存储在布局管理器中:
setLayoutRect:forTextBlock:glyphRange:
setBoundsRect:forTextBlock:glyphRange:
排字器在需要确定先前铺设的文本块使用的空间时使用以下NSLayoutManager
方法:
layoutRectForTextBlock:glyphRange:
layoutRectForTextBlock:atIndex:effectiveRange:
boundsRectForTextBlock:glyphRange:
boundsRectForTextBlock:atIndex:effectiveRange:
前面的方法导致字形生成,但不强制布局。
这避免了在布局期间调用方法时的无限递归。
出于同样的原因,现有NSLayoutManager
方法的以下变体具有防止它们导致布局的选项:
lineFragmentRectForGlyphAtIndex:effectiveRange:withoutAdditionalLayout:
lineFragmentUsedRectForGlyphAtIndex:effectiveRange:withoutAdditionalLayout:
textContainerForGlyphAtIndex:effectiveRange:withoutAdditionalLayout:
如果未设置矩形,则前面的方法返回NSZeroRect
。
在显示时,像往常一样绘制文本,如“布局管理器”中所述,除了文本块在绘制字形背景时绘制背景和边框装饰之外,使用以下方法:
drawBackgroundWithFrame:inView:characterRange:layoutManager:
如果文本块是一个NSTextTableBlock
对象,它为此目的调用其文本表,使用以下NSTextTable
方法:
drawBackgroundForBlock:withFrame:inView:characterRange:layoutManager:
2024-06-18(二)