【C++】Graphvizを使って有向/無向グラフを描く

 Windows環境にて、Graphvizを使用して状態遷移図や人物相関図等の有向/無向グラフを描く、基本的な方法を説明します。前半はコマンド、後半はC++からの呼び出しになります。

開発環境Visual Studio 2022
言語C++
ライブラリGraphviz 5.0.0
動作確認Windows 11
Windows 10
Windows 7
Graphvizの出力例

Graphvizのインストール

 https://graphviz.org/からWindows用のインストーラーをダウンロードして、実行します。ダウンロードの際、32bit版と64bit版の選択は、C++から呼び出す場合のソリューションに合わせます。インストールの途中にて、環境変数PATHにGraphvizのbinフォルダーのパスを追加するかどうか聞かれますので、追加を選択します。

dotコマンドの使用方法

 インストール完了後、コマンドプロンプトにて動作確認します。以下の例では、Graphvizのバージョンを表示します。


> dot -V

 dotコマンドのヘルプを見る場合は、以下の通りです。


> dot -?

 冒頭の有向グラフを作成する、dotファイルの例です。文字コードは、UTF-8(BOM無し)にて保存してください。ちなみに、Microsoft Officeをインストールしている環境では、拡張子dotのファイルは「Microsoft Word 97-2003 テンプレート」と表示されますが、全く関係ありません。


digraph {
    node [fontname=Meiryo]
    edge [fontname=Meiryo]

    // node
    n0 [label=ラー]
    n1 [label=オシリス]
    n2 [label=セト shape=doublecircle]
    n3 [label=ネフティス]
    n4 [label=ホルス shape=doublecircle]
    n5 [label=ハトホル]
    n6 [label=ベック]
    n7 [label=ザヤ]
    n8 [label=トト]

    // edge
    n0 -> n1 [label=親子]
    n0 -> n2 [label=親子]
    n1 -> n4 [label=親子]
    n2 -> n1 [label=倒す]
    n2 -> n3 [label=倒す]
    n2 -> n3 [label=夫婦 arrowhead=none]
    n2 -> n8 [label=倒す]
    n4 -> n2 [label=復讐]
    n4 -> n5 [label=夫婦 arrowhead=none]
    n5 -> n4 [label=手伝う]
    n6 -> n4 [label=手伝う]
    n6 -> n7 [label=カップル arrowhead=none]
    n7 -> n4 [label=信仰]
    n8 -> n4 [label=手伝う]
}

 上記のファイルをtest.dot、出力フォーマットをPNGとして、test.pngに出力する場合、コマンドは以下の通りです。


> dot -Tpng test.dot -o test.png

 pngを、bmp、gif、jpg、pdf、svg、tifに変更すると、それぞれのフォーマットにて出力します。その際、-Tオプションのフォーマット名は小文字にて指定します。

(解説)

  • digraphは有向グラフ、graphは無向グラフ
  • fontnameは英語名にて指定する。デフォルトのTimes-Romanは、日本語表示不可
    (例)メイリオはMeiryo。同様に、MS Gothic、MS PGothic、MS Mincho、MS PMincho、Yu Gothic、Yu Mincho、等
  • fontname、labelに空白やハイフンが含まれる場合、ダブルクォートで括る
    (例)node [fontname="MS Gothic"]
  • //はコメント文
  • nodeにはIDを付与することができる(上記の例ではn0~n8)。以下の制限があるが、ダブルクォートで括ると制限は無い
    • 使用できる文字は英数字、日本語、アンダースコア。数字のみは可だが、他の文字と混在させる場合は、数字で始まるのは不可
    • 数字のみの場合、マイナスと小数点としてハイフンとピリオドの使用可
  • nodeにlabelを設定しない場合、IDが表示される
  • shape=doublecircleは2重丸
  • edgeは、digraphの場合「->」、graphの場合「--」(ハイフン2個)で指定する。「<-」は不可
  • nodeのIDやlabelを定義せず、edgeに直接書くことも可
    (例)ラー -> オシリス [label=親子]
  • arrowhead=noneは、矢印の三角無し
  • 属性の間にカンマ、行末にセミコロンを付けてもよい
    (例)n2 [label=セト, shape=doublecircle];

 他にも、色をつけたり、nodeやedgeの形状を変更したり、いろいろな表現が可能です。

C++から呼び出す

準備

 Visual Studio 2022の場合、プロジェクトのプロパティに以下の設定を追加します。インストール先のフォルダーがデフォルトと異なる場合は、そちらに合わせてください。

[C/C++]-[全般]-[追加のインクルードディレクトリ]
C:\Program Files\Graphviz\include
[リンカー]-[全般]-[追加のライブラリディレクトリ]
C:\Program Files\Graphviz\lib
[リンカー]-[入力]-[追加の依存ファイル]
cgraph.lib;gvc.lib

サンプルソース

 Graphvizのライブラリ関数は、C言語にて実装されています。ライブラリ側にてextern "C"しているので、そのままC++から呼び出しが可能です。

 以下のソースは、dotコマンドの説明と同じ有向グラフを作成します。ただし、処理の内容としては、nodeのIDを自動採番している点のみ異なります。


#include <unordered_map>

// Graphviz
#define GVDLL
#include <graphviz/gvc.h>


    const std::vector<std::string> nodes
    {
        u8"ラー",
        u8"オシリス",
        u8"セト",
        u8"ネフティス",
        u8"ホルス",
        u8"ハトホル",
        u8"ベック",
        u8"ザヤ",
        u8"トト"
    };

    const std::vector<std::tuple<int, int, std::string>> edges
    {
        {0, 1, u8"親子"},
        {0, 2, u8"親子"},
        {1, 4, u8"親子"},
        {2, 1, u8"倒す"},
        {2, 3, u8"倒す"},
        {2, 3, u8"夫婦"},
        {2, 8, u8"倒す"},
        {4, 2, u8"復讐"},
        {4, 5, u8"夫婦"},
        {5, 4, u8"手伝う"},
        {6, 4, u8"手伝う"},
        {6, 7, u8"カップル"},
        {7, 4, u8"信仰"},
        {8, 4, u8"手伝う"}
    };

    // グラフ作成

    // 2番目の引数
    // 有向グラフはAgdirected、無向グラフはAgundirected
    Agraph_t *g = agopen(nullptr, Agdirected, nullptr);

    // フォント指定
    agattr(g, AGNODE, const_cast<char *>("fontname"), "Meiryo");
    agattr(g, AGEDGE, const_cast<char *>("fontname"), "Meiryo");

    // node
    std::unordered_map<std::string, Agnode_t *> node;

    for (const std::string &n : nodes)
    {
        // 2番目の引数はID
        // 既存の場合、そのAgnode_tを返す
        // IDを自動採番する場合はnullptr

        // 3番目の引数
        // IDが存在しない場合
        // trueはAgnode_tを新規作成
        // falseはnullptrを返す
        node[n] = agnode(g, nullptr, true);

        // ラベル
        // 設定しない場合、IDを表示
        agsafeset(node[n], const_cast<char *>("label"), n.c_str(), "");
    }

    // nodeの形状の、デフォルト以外を指定
    agsafeset(node[nodes[2]], const_cast<char *>("shape"), "doublecircle", "");
    agsafeset(node[nodes[4]], const_cast<char *>("shape"), "doublecircle", "");

    // edge
    for (const std::tuple<int, int, std::string> &e : edges)
    {
        // 2番目の引数 -> 3番目の引数
        // 各nodeのAgnode_t

        // 4番目の引数はedgeを識別するキー
        // nullptrは毎回異なるキーとして扱う

        // 2,3,4番目の引数の組み合わせが既存の場合、同じedge

        // 5番目の引数
        // 組み合わせが存在しない場合
        // trueはedgeを新規作成
        // falseはnullptrを返す
        Agedge_t *edge = agedge(g, node[nodes[std::get<0>(e)]], node[nodes[std::get<1>(e)]], nullptr, true);

        // ラベル
        agsafeset(edge, const_cast<char *>("label"), std::get<2>(e).c_str(), "");

        // edgeの形状の、デフォルト以外を指定
        if (std::get<2>(e) == u8"夫婦" || std::get<2>(e) == u8"カップル")
        {
            agsafeset(edge, const_cast<char *>("arrowhead"), "none", "");
        }
    }

    // dotフォーマットにレイアウト
    GVC_t *gvc = gvContext();
    gvLayout(gvc, g, "dot");

    // デバッグのため、内部にて作成されたdotをファイル出力
    //gvRenderFilename(gvc, g, "canon", "test.dot");

    // PNGにてファイル出力
    // bmp、gif、jpg、pdf、svg、tifに変更すると、それぞれのフォーマットにて出力
    gvRenderFilename(gvc, g, "png", "test.png");

    // 解放
    gvFreeLayout(gvc, g);
    agclose(g);
    gvFreeContext(gvc);

 実際に開発する際には、何らかの処理結果から状態遷移図を作成するような、動的なラベルになるでしょう。Graphvizのライブラリ関数に渡すテキストの文字コードはUTF-8です。ただし、gvRenderFilename関数の4番目の引数に日本語を含むパスを渡す場合は、Shift_JISです。UTF-16からUTF-8やShift_JISに変換する方法は、「【C++/Win32】UTF-16とUTF-8を相互に変換する」を参照してください。

リファレンス

Documentation
https://graphviz.org/documentation/

GitLab
https://gitlab.com/graphviz/graphviz
ライブラリ関数の仕様がよくわからない場合は、libの下のソースを読みましょう。

※冒頭の人物相関図は、映画「キング・オブ・エジプト」(2016)を観て作成しました。