65.9K
CodeProject 正在变化。 阅读更多。
Home

WPF TikZ 编辑器(TikzEdt)

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.95/5 (15投票s)

2011 年 2 月 6 日

GPL3

8分钟阅读

viewsIcon

69616

downloadIcon

2267

用于 TikZ 矢量图形语言的文本/所见即所得组合编辑器。

引言

TikZ/PGF 是用于创建矢量图形的两种广泛使用的语言,尤其是在 Latex 文档中。有几个编辑器可以帮助创建 TikZ 代码。但是,大多数情况下,我们只能在带有预览的文本编辑器(没有所见即所得功能)和具有 TikZ 导出功能的真正所见即所得编辑器(没有直接代码访问)之间进行选择。结合直接代码编辑和所见即所得功能是相当复杂的,因为它需要一个能够“理解”TikZ 代码的解析器和解释器,以便以所见即所得的方式进行渲染和编辑。我们最近编写了一个这样的编辑器,TikzEdt。本文档描述了我们面临的主要编程挑战以及我们为克服这些挑战所做的设计决策。此外,我们程序的某些组件可以用于类似的应用程序。

代码的最新版本和一些进一步的文档可以在 此处找到。

出于篇幅原因,我们在此不对 TikzEdt 的源代码进行全面记录。相反,我们将主要限制在用人类语言描述我们所做的设计决策,并仅提供偶尔的代码示例。

必备组件

本文档应适合熟悉 C# 的读者。但是,了解 LaTeX 和 TikZ/PGF 编程以及 Antlr 解析器生成器的基本知识将有助于读者理解示例。

TikzEdt 的功能

在开始之前,让我们通过查看上面的屏幕截图简要了解一下编辑器。左侧是一个文本框,用户可以在其中自由输入和编辑 LaTeX/TikZ 代码。右侧是已编译的 TikZ 图形的预览,即 LaTeX 编译器在接收左侧代码时生成的 PDF。预览的顶部显示了一个叠加层(红色 X)。用户可以以所见即所得的方式编辑此叠加层,例如,用鼠标拖放一个红色 X。当他/她这样做时,左侧文本框中的 TikZ 代码会相应地更新,然后再次编译,以便右侧显示的预览会发生变化。

文本编辑器

文本编辑器(屏幕截图的左半部分)基于 AvalonEdit 组件。它具有语法高亮显示和基本代码补全功能。AvalonEdit 在 这篇 CodeProject 文章中有详细描述。我们添加了以下功能:

查找/替换对话框

它包含了查找/替换对话框的标准功能。

可自定义的代码补全

代码补全存储在外部 XML 文件(CodeCompletions.xml)中。每个代码补全都属于一个特定的环境,该环境由开始和结束标签标识,可以在 CodeCompletions.xml 中指定为正则表达式。CodeCompleter 类用于读取 XML 文件并将适当的补全填充到 AvalonEdit CompletionWindow 中。显示补全的代码如下所示:

private void ShowCodeCompletionsCommandHandler(object sender, ExecutedRoutedEventArgs e)
{
    // Open code completion window
    completionWindow = new CompletionWindow(txtCode.TextArea);
    IList<ICompletionData> data = completionWindow.CompletionList.CompletionData;
    codeCompleter.GetCompletions(txtCode.Document, txtCode.CaretOffset, data);

    ...

    completionWindow.Show();
    ...
}

备注:我们希望将查找/替换对话框和 CodeCompleter 作为可重用库提供。但是,由于时间原因,我们需要一些帮助进行更严格的测试和维护。如果您有兴趣,请联系作者之一。

代码片段库

TikzEdt 提供了一个可配置的代码片段库。其功能大致是标准的,不值得详细介绍,除了一点:每个代码片段都附带一个缩略图,该缩略图直接从 LaTeX/TikZ 代码编译而成。因此,插入自定义代码片段的用户不必生成单独的图像文件来显示,只需插入一些示例代码,这些代码会被编译以生成缩略图。

TikZ 解析器

TikzEdt 的技术核心是 TikZ 解析器。必须考虑以下问题:

  • LaTeX/TikZ 不是一个“干净”的语言,特别是它具有歧义且不是 LL*。
  • 我们不想重新实现完整的 TikZ 语言,而只实现与所见即所得编辑相关的部分。因此,解析器应该具有容错性,即也能接受它无法“理解”的语言结构。
  • 我们没有 TikZ 的一手规格。因此,部分规格必须通过逆向工程获得。

为了构建解析器,我们使用了 Antlr 解析器生成器。生成的解析器从 TikZ 代码生成一个抽象语法树(AST),该树仅表示 TikzEdt 可以理解的 TikZ 文件的一部分。然后,AST 在第二步被翻译成一个 Tikz_ParseTree,其中包含每种 TikZ 对象的自定义节点。例如,这是表示 TikZ 节点对象的 C# 类:

public class Tikz_Node : Tikz_XYItem
{
    public static Tikz_Node FromCommonTree(CommonTree t, 
                            CommonTokenStream tokens)
    {
        ...
    }
    public void SetName(string tname)
    {
        ...
    }
    public override bool GetAbsPos(out Point ret, bool OnlyOffset = false) 
    {
        ...
    }
    public override void SetAbsPos(Point p)
    {
        ...
    }
    ...
}

在这里,静态方法 FromCommonTree() 用于从作为参数 t 传递的抽象 AST 节点生成 Tikz_Node 实例。方法 GetAbsPos() 返回节点的坐标。

解释器和坐标计算

TikZ/PGF 具有一个广泛的坐标变换系统,以及指定相对坐标或极坐标的方法。例如,以下 TikZ 代码会生成一个旋转并拉伸的矩形:

\draw[rotate=30, xscale=3] (1,1) rectangle (3,3);

为了准确理解节点的位置,这些坐标变换必须在 TikzEdt 中进行解析和表示。如果查询某个对象的位置,则必须将正确的坐标变换应用于从 TikZ 代码中解析出的原始坐标(例如,上面示例中的 (1,1))。这主要在 Tikz_ParseTree 节点的 GetCurrentTransformAt() 方法中实现。

public bool GetCurrentTransformAt(TikzParseItem childtpi, out TikzMatrix M)
{
    if (parent != null)
    {
        if (!parent.GetCurrentTransformAt(this, out M))
            return false;
    }
    else
        M = new TikzMatrix(); // identity matrix

    foreach (TikzParseItem tpi in Children)
    {
        if (tpi == childtpi && childtpi != null)
        {
            break;
        }
        else if (tpi is Tikz_Options)
        {
            TikzMatrix MM;
            if ((tpi as Tikz_Options).GetTransform(out MM))
                M = M * MM;
            else
                return false;
        }
    }
    return true;
}

该方法接受解析树中的某个项,并计算该项位置的当前变换。之后,可以使用 TikzMatrix.Transform() 对原始坐标(例如 (1,1))进行变换。

实时 PDF 预览

TikzEdt 没有 TikZ 渲染引擎。所见即所得编辑器中显示的预览是编译的 PDFLaTeX 输出。这种方法只有在编译和显示速度非常快的情况下才可行,以免在用户所见即所得地编辑叠加层和预览跟随更改之间引入显著延迟。我们通过使用预编译的头文件来加快 PDFLaTeX 的编译速度。此外,为了显示 PDF,我们使用包装了 Xpdf/muPDF 库的 PDFLibNet,速度非常快。实际上,对于小型 TikZ 文件,所见即所得编辑器几乎没有“滞后”。

确定边界框

据我们所知,没有直接的方法可以从已编译的 .pdf 或 LaTeX .aux 文件中读取 TikZ 图形的边界框。但是,我们需要此边界框才能在所见即所得编辑时将显示的叠加层与底层 PDF 预览正确对齐。我们用来确定边界框的方法是,将以下代码段写入 TeX 文件,以便在 LaTeX 编译期间读取边界框并将其写入单独的文本文件。

\pgftransformreset
...
\newwrite\metadatafile
\immediate\openout\metadatafile=\jobname_BB.txt
\path
  let
    \p1=(current bounding box.south west),
    \p2=(current bounding box.north east)
  in
  node[inner sep=0pt,outer sep=0pt,minimum size=0pt,line width=0pt,
       text width=0pt,text height=0pt,draw=white] at (current bounding box) {
\immediate\write\metadatafile{\p1,\p2}
};
\immediate\closeout\metadatafile

叠加层和所见即所得功能

为了实现所见即所得编辑,会在 PDF 预览的顶部显示一个叠加层。例如,对于以下 TikZ 代码段:

\begin{tikzpicture}
\draw(-4,3) .. controls (-3,5) and (-1,6) .. (-1,3);
\draw (1,5) rectangle (4,3);
\begin{scope}[]
\draw(6,3) -- (9,5);
\end{scope}
\end{tikzpicture}

原始输出和叠加层看起来像这样:

可以看到,为了编辑贝塞尔控制点、存在的各种坐标以及 TikZ 作用域,显示了叠加层项。用户可以像在任何所见即所得编辑器中一样用鼠标拖放叠加层项,并且 TikZ 源代码会相应地更新。

PdfOverlay 类和显示树

所有所见即所得功能都在 PdfOverlay 类中实现。内部,该类将 Tikz_Parsetree 转换为另一种树结构,即显示树(从而遵循标准的模型视图模式)。例如,这是表示显示树中坐标的类的部分定义:

public class OverlayNode : OverlayShape
{
    public Tikz_XYItem tikzitem;
    public override TikzParseItem item { get { return tikzitem; } }

    public delegate void PositionChangedHandler(OverlayNode sender);
    public event PositionChangedHandler PositionChanged;

    public override bool AdjustPosition(double Resolution)
    {
        ...
    }
    ...
}

字段 tikzitem 包含显示树节点所表示的 Tikz_ParseTree 中的项。调用 AdjustPosition() 方法来将屏幕上 OverlayNode 的位置与底层 tikzitem 中编码的位置对齐。

坐标系和光栅化器

用户通常喜欢对齐多个对象。因此,用鼠标移动的对象坐标应该被光栅化。TikzEdt 中的 rasterControl 类完成了这项工作,它还在屏幕上显示了视觉光栅。这个类比想象的要复杂一些,因为它必须支持坐标变换和极坐标。例如,下面是一个旋转的、各向异性缩放的极坐标光栅的图像:

在这种情况下,底层的 TikZ 代码是这样的:

\begin{tikzpicture}
  \draw[rotate=30, xscale=1, yscale=4] (0,0)--(90:2)--+(180:5);
\end{tikzpicture}

可扩展的工具系统

可以使用多种工具以所见即所得的方式编辑图片。我们希望工具系统尽可能模块化,允许将来添加工具或改进现有工具,而无需担心程序其余部分的实现细节。每个工具都包含在一个单独的类中,该类从 OverlayTool 基类派生。通过 OverlayInterface 接口授予对 TikZ 图片和用户界面的访问权限。

class OverlayTool
{
    /// <summary>
    /// Access to the PdfOverlay. It will be set
    /// before the first call to OnActivate().
    /// </summary>
    public OverlayInterface overlay;

    /// <summary>
    /// This method is called when the tool is selected by the user.
    /// For example, the cursor shape should be set here.
    /// </summary>
    public virtual void OnActivate() { }
    public virtual void OnDeactivate() { }

    public virtual void OnLeftMouseButtonDown(OverlayShape item, 
                        Point p, MouseButtonEventArgs e) { }
    ...
}

最后 remarks 和待办事项

我们在此概述了一个用于矢量图形的组合文本/所见即所得编辑器。据我们所知,在开源社区中没有类似的项。目前,TikzEdt 仍在开发中,作者的愿望清单上还有许多功能。目前,我们欢迎一切帮助,无论是测试还是编码,或者只是对我们工作的评论。(如果您有兴趣成为贡献者,请联系作者。)

谢谢

没有社区的先前努力,我们的工作将不可能实现。特别是,我们使用了以下组件:

© . All rights reserved.