PAYATHON 2026

如何计算字符串节点树的布局并绘制到 CGContext?

数据小许

可以先计算树形布局,再交给 CGContext 绘制

可以先根据树结构和文字尺寸计算节点位置,再用 CGContext 绘制连线、节点和文字。CGContext 负责绘图,树的层级排列、节点间距和重叠处理则需要布局算法或布局库来完成。

如果要绘制一棵从上到下排列的树,可以先实现基于子树宽度的布局。这种方法容易理解,也能处理字符串长度不同的节点。

如何计算节点的位置

以下方案假设数据是一棵没有环的树,每个非根节点只有一个父节点。所有节点高度相同,文字保持单行,宽度随字符串长度变化。

先设置几个参数:

  • paddingX:文字左右留白。
  • nodeHeight:节点统一高度,需要容纳字体和上下留白。
  • siblingGap:相邻子树之间的水平间距。
  • levelGap:相邻层节点之间的垂直间距。

布局分两遍进行。

第一遍:从叶子向根节点计算子树宽度

先用实际绘制时使用的字体测量字符串:

nodeWidth = textWidth + 2 * paddingX

叶子节点的子树宽度等于自身宽度。有子节点时,先把各棵子树的宽度和间距相加:

childrenWidth = sum(child.subtreeWidth)
                + siblingGap * (childCount - 1)

subtreeWidth = max(nodeWidth, childrenWidth)

这样可以为整棵子树预留水平空间。如果只根据当前节点的文字宽度排列兄弟节点,更下层的后代可能互相覆盖。

第二遍:从根节点向下分配坐标

将父节点放在其子树区域的水平中央,再把各棵子树依次放入下方区域。以下是布局伪代码,measureText、Rect 等名称用于说明算法,并非可直接调用的 Swift API:

function measure(node):
    node.width = measureText(node.text) + 2 * paddingX

    for child in node.children:
        measure(child)

    if node.children is empty:
        node.childrenWidth = 0
    else:
        node.childrenWidth = sum(child.subtreeWidth)
        node.childrenWidth += siblingGap * (count(node.children) - 1)

    node.subtreeWidth = max(node.width, node.childrenWidth)

function place(node, left, depth):
    centerX = left + node.subtreeWidth / 2
    top = depth * (nodeHeight + levelGap)

    node.frame = Rect(
        x = centerX - node.width / 2,
        y = top,
        width = node.width,
        height = nodeHeight
    )

    childLeft = left + (node.subtreeWidth - node.childrenWidth) / 2

    for child in node.children:
        place(child, childLeft, depth + 1)
        childLeft += child.subtreeWidth + siblingGap

measure(root)
place(root, 0, 0)

这套算法为每棵子树保留独立的水平区域,在上述假设下可以避免节点重叠。代价是树较深或分支较多时,图形可能比较宽。

在 CGContext 中绘制

得到节点的矩形后,建议先画全部连线,再画节点和文字,避免连线盖住文字。

如果布局采用左上角为原点、Y 轴向下的逻辑坐标,可以将父节点的底边中心连接到子节点的顶边中心。假设 parentFrame、childFrame 均为已经计算好的 CGRect,连线代码如下:

import CoreGraphics

func drawEdge(
    in context: CGContext,
    parentFrame: CGRect,
    childFrame: CGRect
) {
    context.beginPath()
    context.move(to: CGPoint(
        x: parentFrame.midX,
        y: parentFrame.maxY
    ))
    context.addLine(to: CGPoint(
        x: childFrame.midX,
        y: childFrame.minY
    ))
    context.strokePath()
}

这段代码要求绘图上下文的坐标方向与布局一致。不同来源的 CGContext 初始坐标系可能不同,不能直接假设 Y 轴总是向下。

文字可以通过 Core Text 绘制到 CGContext。测量和绘制时必须使用一致的字体、字号及换行规则。文字基线和坐标变换也需要单独处理,否则容易出现位置偏移或文字倒置。

查找算法或布局库时关注什么

可以从这些关键词入手:tree layout、tidy tree layout、variable node size tree layout、Swift tree layout。

评估库时,先确认它能否接收节点尺寸并输出布局坐标。如果库只负责输出坐标,可以自行将结果绘制到 CGContext;如果库绑定了自己的视图或画布,还需要确认是否方便导出位置和连线信息。

对于静态、规模较小的树,上面的子树宽度方案可以作为起点。如果需要更紧凑的排列,或者支持折叠、缩放、拖拽,可以再根据需求选择更完整的布局和交互方案。

备注:内容仅供参考。