用 TikZ 绘制技术图形

每当我制作技术文档(演示文稿、出版物、技术演讲...)时,制作其中的图形曾经是我最头疼的事情。

你知道我在说什么吗?我会试图将箭头对齐到框的边缘,或者稍微居中一个标签,或者因为我不太会用鼠标而消除这些小瑕疵。无论我多么努力想让它完美,总是差那么一点点,刚好能看出来。

这一切在我发现矢量图形以及如何轻松地用数学术语指定技术图示后改变了。

我邀请你走我走过的路:停止在draw.io、Visio或Inkscape中绘制图表。编写描述你图表的代码。

1、使用图形语言的好处

像大多数文章一样,我喜欢从提供一些动机开始。为什么有人会费劲地为他们的图形编写代码?为什么要在一个本质上是视觉的东西上浪费时间写抽象描述?

我能想到很多原因:

  • 图表变成纯文本,因此可以通过代码自动化并通过版本控制软件跟踪。
  • 你需要经常绘制的重复部分可以抽象成函数,甚至可以参数化。
  • 无限分辨率和控制,可以精确对齐图表的各个部分。
  • 更容易回收和调整旧图形。
  • 如果你遵循我的方法,LaTeX数学集成在你的图形中,与文档其余部分的质量相匹配。
  • 如果你已经在使用某种编程环境来制作文档(参见我在下面文章中的想法),你不必切换到不同的程序来制作图形。

那么我最终选择的语言是什么?它叫TikZ,是LaTeX生态系统的一部分。

2、我使用的语言:TikZ

TikZ是一个名为PGF(便携式图形格式)的高级前端,两者都由Beamer的作者、无与伦比的Till Tantau创建。

PGF/TikZ作为LaTeX包在CTAN上分发,并有一个令人难以置信的手册,前100页左右就能让你上手(通过非常实用的教程),但后面还有1300页关于PGF和TikZ所有功能的详细说明。

如上所述,作为LaTeX的一部分意味着TikZ图形与我在LaTeX中制作的任何文档都非常紧密地集成在一起。图形中的任何标签都与主文档的字体相匹配,数学符号也具有相同的质量。

话虽如此,你总是可以使用TikZ和LaTeX引擎来制作不嵌入任何文档的图形(参见standalone文档类),并将其包含在你正在处理的任何其他类型的文档中。

TikZ的另一个优势是它有许多很棒的支持库,提供了现成的图形供重用。一些特别值得注意的例子:

  • tikz-uml用于UML图表
  • circuitikz用于绘制电路图,包括逻辑门和晶体管
  • PGFPlots用于创建方程或来自外部文件的原始数据的2D和3D图
  • 内置的思维导图库

你可能想知道是否还有其他选择。确实有。我知道至少还有两个:

  1. 你可以用PostScript语言编写图形,LaTeX有PSTricks包来帮助实现。
  2. 你可以使用Asymptote,这是一种独立的语言,感觉很像C/C++,比PGF/TikZ性能更好,对3D图形有更好的支持。还有一个LaTeX用于在文档中嵌入Asymptote图形。

我自己从Asymptote开始,但后来切换到了TikZ,因为它更容易使用,而且有现成的库为我节省了很多时间。在这个意义上,我用性能换取了便利。

如果你仍然持怀疑态度,我邀请你看看这个示例画廊,体验一下TikZ的所有功能。

3、TikZ的基本概念

TikZ基本上是一支"笔",你可以通过数学操作引导它在画布上移动。在过程中,你可以告诉笔改变颜色、粗细、线条样式,甚至从画布上的一个点跳到另一个点而不绘制它们之间的路径。

到目前为止,这与一般的图形语言概念没有太大不同。TikZ的优势在于它的简洁性和它提供的高级结构,使创建图表更容易。

从一个非常基础的例子可以看出这一点:绘制矩形。

假设我们想画一个宽度为5个单位、高度为2个单位的矩形。有很多种方法可以做到。让我们来探索它们。

本文中的所有代码示例都假设你在导言区有\usepackage{tikz},并将代码嵌入到tikzpicture环境中。

% 直接指定坐标:
\draw (0, 0) -- (5, 0) -- (5, 2) -- (0, 2) -- (0, 0);

% 稍微好一点,通过将最后一个点连接到第一个点来闭合路径:
\draw (0, 0) -- (5, 0) -- (5, 2) -- (0, 2) -- cycle;

% 指定相对坐标:++(5, 0) 表示在当前点右侧5个单位:
\draw (0, 0) -- ++(5, 0) -- ++(0, 2) -- ++(-5, 0) -- cycle;

% 如果使用单个+,它计算相对坐标但不使新点成为当前点
% 所以所有坐标都是相对于我们指定的第一个点计算的:
\draw (0, 0) -- +(5, 0) -- +(5, 2) -- +(0, 2) -- cycle;

% 在许多场景中另一个方便的方法是通过水平和垂直线连接两个点:
\draw (0, 0) -| (5, 2) -| (0, 0);

% 当然,对于像矩形这样的基本形状,已经有更方便的方式:
\draw (0, 0) rectangle (5, 2);
简单的5x2矩形

我应该更多地解释倒数第二个例子。你看到命令也告诉TikZ在绘制时如何在两点之间移动。两个破折号只是表示沿直线移动。但当我们使用像(0, 0) -| (5, 2)这样的形式时,我们表示从(0, 0)水平向外移动,直到在(5, 2)正下方,然后垂直移动到(5, 2)。

还有一个等效的|-指令。它在上面的例子中同样有效(尽管线段会以不同的顺序绘制)。

样式呢?非常简单,只需提供适当的选项:

\draw [red, fill=blue] (0, 0) rectangle (5, 2);
带样式的矩形

节点

除了在点之间绘制路径,你还可以在给定点创建节点。节点是图片的一个元素,它有与之关联的形状、可选的文本标签和若干锚点。

这相当抽象。让我具体一点。

我认为节点的原始目的是说明树形图中的节点或流程图中的步骤。我之所以这么认为是因为默认节点形状是矩形。以下是当我要求TikZ绘制默认节点形状时发生的情况:

\node [draw] (my node) at (0, 0) {This is my node};

带标签的节点(作者截图)

注意语法的几个要点:你可以提供可选的节点名称(我在上面使用了"my node"),以便在后面的代码中引用该节点。你还可以提供它的位置(这是可选的,默认为当前点)。花括号之间的文本是节点标签。这是强制性的。你可以通过空的一对花括号来拥有空标签。

那么什么是节点锚点?注意节点不是单个点。当我为节点指定位置时,节点的锚点就是被放置在那里的位置。默认情况下,这是节点的中心,但我可以更改它:

\node [circ] at (0, 0) {}; % 标记 (0, 0)
\node [circ] at (2, 0) {}; % 标记 (2, 0)
\node [draw] at (0, 0) {A}; % 锚定在中心
\node [draw, anchor=south west] at (2, 0) {B}; % 锚定在左下角

锚定节点(作者截图)

节点也有形状。你可以为节点指定不同的形状:

\node [draw,circle] at (0, 0) {AA};
\node [draw, anchor=south west,ellipse] at (2, 0) {BB};
节点形状可以更改

有很多库定义了许多(相当复杂的)节点形状。例如,circuitikz库将所有电路元件定义为节点形状。你也可以定义自己的节点形状及其自定义锚点。需要注意的是,你必须用PGF来做这件事,它比TikZ低级得多,所以语法没有那么友好。

节点如果不能连接就没多大用处。这里有一些不同的连接方式:

\node [draw,regular polygon, regular polygon sides=3] (A) at (0, 0) {A};
\node [draw, anchor=south west, regular polygon, regular polygon sides=5] (B) at (2, 0) {B};
\draw (A.east) -- (B.west)
      (A.north) to [out=90, in=90] (B.north);
不同的节点连接方式

注意第二种方式给了你更多的灵活性:你指定线条从源点(在这种情况下是A的北锚点)出来的角度,以及它进入目标(B的北锚点)的角度。TikZ自动为你绘制曲线路径。

还有一个有用的命令允许你定义命名坐标并在后面按名称引用它们:

\coordinate (A) at (0, 0);
\coordinate (B) at (1, 2.5);
\coordinate (C) at (2, -3);

\draw (A) -- (B) -- (C) -- cycle;
\node [circ] at (A) {A};
\node [circ] at (B) {B};
\node [circ] at (C) {C};
定义命名坐标

最后,为了将这些概念结合在一起,注意你可以在绘制路径时定义节点和坐标:

\draw (0,0) coordinate (A) -- ++(1, 2) coordinate (B) -- node [draw, right] (C) {C} ++(0, -2);
\draw [->] (C.north) to [out=30, in=60] (B);
在绘制路径时定义节点和坐标

4、更高级的概念

当然,一篇文章不可能涵盖TikZ的所有功能(请参阅手册!),但我想介绍几个我经常利用的有用概念。

坐标计算

编写绘图程序当然涉及计算大量坐标。TikZ通过允许你内联指定一些坐标计算使之变得简单。这是一个例子:

\coordinate (A) at (0, 0);
\coordinate (B) at (1, 2);
\coordinate (C) at ($(A)+(2, 0)$);
\draw (A) node [left] {A} -- (B) node [above] {B} -- (C) node [right] {C} -- cycle;
\coordinate (midpoint) at ($(A)!0.5!(C)$);
\draw (midpoint) -- (B); % 高度
绘制高的等腰三角形

注意在中点的计算中,我们使用0.5表示中间点。我们可以使用任何分数。例如,如果我们使用0.2,我们会计算一个位于从A出发AC长度0.2倍处的点。

宏和数学

当内联坐标计算不够用时,PGF有自己的数学引擎,可以进行更复杂的计算。你还可以用它来定义宏,用于参数化你的图形以便于维护。

\pgfmathsetmacro{\width}{3}
\pgfmathsetmacro{\height}{2}
\pgfmathsetmacro{\mydistance}{3}
\pgfmathsetmacro{\myangle}{60}
\pgfmathsetmacro{\newx}{\mydistance * cos(\myangle)}
\pgfmathsetmacro{\newy}{\mydistance * sin(\myangle)}
\draw [red] (0, 0) rectangle ++(\width, \height);
\draw [blue] (\newx, \newy) rectangle ++(0.5*\width, 0.5*\height);
使用PGF数学引擎进行高级计算

循环

什么编程语言会没有循环和条件语句?TikZ提供这些功能,让你在绘图中有更大的灵活性:

\foreach \rowIdx/\rowLabel in {0/A,1/B,2/C}{
  \foreach \colIdx in {0,...,3}{
      \def\nodenamelabel{\rowLabel-\colIdx};
      \node [draw, circle, shading=ball, ball color=blue] (\nodenamelabel)
      at (\colIdx, \rowIdx) {\color{white}{\nodenamelabel}};
  }
}
使用循环避免重复

你也可以使用条件语句,但我不经常使用它们,所以请参考手册。

作用域

你可以在tikzpicture中使用scope环境来临时改变图形的选项和样式。这非常方便,例如,如果你想重复一个图形但将原点移动到不同的点:

  \coordinate (A) at (0, 0);
  \coordinate (B) at (1, 2);
  \coordinate (C) at ($(A)+(2, 0)$);
  \draw (A) node [left] {A} -- (B) node [above] {B} -- (C) node [right] {C} -- cycle;
  \coordinate (midpoint) at ($(A)!0.5!(C)$);
  \draw (midpoint) -- (B); % 高度
  \begin{scope}[shift={(3, 3)}, rotate=30, color=red]
    \coordinate (A) at (0, 0);
    \coordinate (B) at (1, 2);
    \coordinate (C) at ($(A)+(2, 0)$);
    \draw (A) node [left] {A} -- (B) node [above] {B} -- (C) node [right] {C} -- cycle;
    \coordinate (midpoint) at ($(A)!0.5!(C)$);
    \draw (midpoint) -- (B); % 高度
  \end{scope}
使用作用域移动坐标

注意我们必须用花括号括住shift,因为它的值包含逗号,逗号用于分隔传递给大多数命令的选项。

在我们传递给scope的选项中,我们还可以改变图形的比例,更改颜色选项(如上所示),并添加更多的本地自定义。

一个涵盖一切的节点

最后,有一个简单的技巧可以把一堆图形包围在一个大框里用于说明目的。你指定一个节点并告诉它必须包含某些其他节点:

\foreach \rowIdx/\rowLabel in {0/A,1/B,2/C}{
  \foreach \colIdx in {0,...,3}{
      \def\nodenamelabel{\rowLabel-\colIdx};
      \node [draw, circle, shading=ball, ball color=blue] (\nodenamelabel)
      at (\colIdx, \rowIdx) {\color{white}{\nodenamelabel}};
  }
}
\node [draw, rectangle, rounded corners, fit=(A-0) (C-3),fill=red, opacity=0.3] {};
轻松地将图形包围在大节点中

注意,在fit选项中只指定两个球形节点就足够了,因为如果矩形适合对角的那两个,它会自动适合所有其他节点。

5、结束语

我希望这趟PGF/TikZ的旋风之旅能激起你对它所有可能性的兴趣。当然,我只是触及了表面。TikZ比我这里展示的功能要多得多。

如果你从这篇文章中得到一个要点,那就是用图形语言指定技术图形比"手动"绘制要好得多,原因如下:

  • 更好的精度
  • 矢量图形的无限分辨率
  • 更大的灵活性和自动化性
  • 更少的上下文切换

最后,TikZ的另一个优势是它是由与LaTeX中Beamer演示文稿类的同一人编写的,所以它们配合得很好,可以创建真正令人惊叹的演示文稿。

请停止"手动"绘制图形,开始编写代码吧。


原文链接: I Don't Draw Technical Graphics Anymore

汇智网翻译整理,转载请标明出处