MyTetra Share
Делитесь знаниями!
Использование graphviz для отрисовки синтаксического дерева
Время создания: 13.05.2021 11:05
Раздел: Компьютер - Программирование - Теория программирования - Теория компиляции
Запись: xintrea/mytetra_syncro/master/base/1620893102dvkb4hg4ux/text.html на raw.github.com

Graphviz — утилита командной строки для генерации схем графов на основе текстовых описаний узлов и рёбер. Из статьи вы узнаете, как и языка C++ программно вызвать graphiz с заданными параметрами, прочитать вывод и код возврата вызванной утилиты. Способ можно использовать в отрыве от Graphviz, здесь он взят для примера.

Установка graphviz

Чтобы получить graphviz на Windows, вам нужно:


  • установить Graphviz из MSI-пакета с сайта graphviz.org
  • добавить в системную переменную PATH путь к Graphviz (полный путь к поддиректории Graphviz2.38\bin внутри Program Files)


Для Ubuntu потребуется:

  • установить пакет dot


После установки проверьте работу graphviz на простом примере:


digraph G {

0 [shape="circle"label="S0(a)"];

1 [shape="box"label="S1(b)"];

2 [shape="box"label="S2(c)"];

0->1 [label=" a "];

0->2 [label=" b "];

1->1 [label=" b "];

2->1 [label=" a "];

}


Автоматизируем вызов Graphviz

Для автоматизации нам потребуется кроссплатформенный способ запуска внешнего процесса из C++. Например, функции popen/pclose из стандарта POSIX.

На Windows название функции popen изменено на _popen. Это легко исправить с помощью препроцессора:

макрозамена popen/pclose

#ifdef _WIN32

#define popen _popen

#define pclose _pclose

#endif


Используя пример из документации (msdn.microsoft.com), можно реализовать функцию для запуска произвольной команды, печати вывода в консоль и возврата true/false при успешном/неуспешном выполнении.

объявление класса CUtils

#pragma once


#include <string>


class CUtils

{

public:

CUtils() = delete;


static bool RunProcess(std::string const& command);

};


запуск внешней команды в CUtils::RunProcess

bool CUtils::RunProcess(const std::string &command)

{

CPipeHandle pipe(popen(command.c_str(), "r"));

if (!pipe)

{

return false;

}


std::vector<char> buffer(256);


while (std::fgets(buffer.data(), int(buffer.size()), pipe))

{

std::fputs(buffer.data(), stderr);

}


if (std::feof(pipe))

{

return true;

}


// process run failed, print error.

std::fputs(std::strerror(errno), stderr);

return false;

}


Класс CPipeHandle — это простейшая безопасная обёртка над FILE*, в деструкторе которой автоматически вызывается закрытие канала через pclose.

RAII-обёртка для FILE*

namespace

{

class CPipeHandle

{

public:

CPipeHandle(FILE *pipe)

: m_pipe(pipe)

{

}


~CPipeHandle()

{

if (m_pipe)

{

pclose(m_pipe);

}

}


operator FILE*()const

{

return m_pipe;

}


private:

FILE *m_pipe = nullptr;

};

}


Чтобы воспользоваться API, нужно указать полную команду со всеми аргументами. Если путь к исполняемому файлу команды не добавлен в переменную окружения PATH, то придётся указать полный путь к .exe. Вот пример вызова Graphviz для преобразования диаграммы fsm.dot в изображение fsm.png

CUtils::RunProcess("dot -Tpng -ofsm.png fsm.dot");


Класс генерации кода для Graphviz

Допустим, нам захочется сделать сериализацию дерева или графа в язык Graphviz, чтобы автоматизировать их визуализацию. Допустим, мы хотим визуализировать конечный автомат:



На выходе мы должны получить примерно такой файл *.dot:


digraph G {

0 [shape="circle"label="S0(a)"];

1 [shape="box"label="S1(b)"];

2 [shape="box"label="S2(c)"];

0->1 [label=" a "];

0->2 [label=" b "];

1->1 [label=" b "];

2->1 [label=" a "];

}


В файле указаны надписи на вершинах и рёбрах графа, где под вершинами подразумеваются состояния, а под рёбрами — переходы. Для вершин указана форма: для начального состояния круг (circle), для конечных состояний (терминалов) двойной круг (doublecircle), для промежуточных состояний (нетерминалов) прямоугольник (box).

Вспомогательный класс для сериализации назовём CDotWriter. Определение класса:

enum class StateType

{

Initial,

Nonterminal,

Terminal

};


class CDotWriter

{

public:

CDotWriter(std::ostream & out);

~CDotWriter();


void PrintVertex(size_t index, std::string const& label, StateType type = StateType::Nonterminal);

void PrintEdge(size_t from, size_t to, std::string const& label);


private:

std::string GetShape(StateType type)const;


std::ostream & m_out;

};


Реализации методов выглядят так:

CDotWriter::CDotWriter(std::ostream &out)

: m_out(out)

{

m_out << "digraph G {\n";

}


CDotWriter::~CDotWriter()

{

m_out << "}\n";

}


void CDotWriter::PrintVertex(size_t index, const std::string &label, StateType type)

{

m_out << index << " [";

m_out << "shape=\"" << GetShape(type) << "\"";

m_out << "label=\"" << label << "\"];\n";

}


void CDotWriter::PrintEdge(size_t from, size_t to, const std::string &label)

{

m_out << from << "->" << to << " [label=\" " << label << " \"];\n";

}


std::string CDotWriter::GetShape(StateType type) const

{

switch (type)

{

case StateType::Initial:

return "circle";

case StateType::Nonterminal:

return "box";

case StateType::Terminal:

return "doublecircle";

}

return std::string();

}


Так же в этом разделе:
 
MyTetra Share v.0.59
Яндекс индекс цитирования