Closed6

Flutterで系統図を作成し表示する

snova301snova301

きっかけ

QCで出てくる系統図をFlutterの中で使いたいが、dart packageで調べても系統図を作成できるライブラリがぱっと見つからない。
自作するためにスクラップで確認します。

環境構築

まずはアプリを初期化します。
系統図の英語はsystem_diagramなのかな?

flutter create system_diagram
cd system_diagram
snova301snova301

図を作成

作図用のツールとしてはCustomPainterが一番に思い立ったので実験してみます。
https://api.flutter.dev/flutter/rendering/CustomPainter-class.html

目標は以下のような感じ。

Flutterで実装。

class _TestPainter extends CustomPainter {
  
  void paint(Canvas canvas, Size size) {
    /// paintの定義
    final paint = Paint();

    /// Firstの箱
    paint.color = Colors.green;
    canvas.drawRect(const Rect.fromLTWH(0, 0, 40, 100), paint);

    /// FirstとSecondの間の線
    paint.strokeWidth = 3;
    paint.color = Colors.grey;
    canvas.drawLine(const Offset(40, 50), const Offset(150, 50), paint);

    /// Secondの箱
    paint.color = Colors.green;
    canvas.drawRect(const Rect.fromLTWH(150, 0, 40, 100), paint);

    /// FirstとThirdの間の線
    paint.color = Colors.grey;
    canvas.drawLine(const Offset(40, 50), const Offset(150, 170), paint);

    /// Thirdの箱
    paint.color = Colors.green;
    canvas.drawRect(const Rect.fromLTWH(150, 120, 40, 100), paint);
  }

  /// Repaintはfalse
  
  bool shouldRepaint(covariant CustomPainter oldDelegate) {
    return false;
  }
}

出来たことはできました。

ただし、以下の課題があります。

  • サイズや位置の指定が手間
  • 文字が表示できない
  • ボタンの表示が出来ない

うーん。他の方法を考えます。

参考サイト

https://dev.classmethod.jp/articles/flutter_custom_paint/

snova301snova301
ここまでのコード全文
import 'package:flutter/material.dart';

void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({Key? key}) : super(key: key);

  
  Widget build(BuildContext context) {
    return MaterialApp(
      title: '系統図テスト',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: const MyHomePage(title: '系統図テスト'),
      debugShowCheckedModeBanner: false,
    );
  }
}

class MyHomePage extends StatefulWidget {
  const MyHomePage({Key? key, required this.title}) : super(key: key);

  final String title;

  
  State<MyHomePage> createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(widget.title),
      ),
      body: ListView(
        /// 水平方向に伸びるListview
        scrollDirection: Axis.horizontal,
        padding: const EdgeInsets.all(10),
        children: <Widget>[
          /// Container系のWidgetでCustomPaintをラップする
          SizedBox(
            width: 400,
            height: 400,
            child: CustomPaint(
              painter: _TestPainter(),
            ),
          ),
        ],
      ),
    );
  }
}

class _TestPainter extends CustomPainter {
  
  void paint(Canvas canvas, Size size) {
    /// paintの定義
    final paint = Paint();

    /// Firstの箱
    paint.color = Colors.green;
    canvas.drawRect(const Rect.fromLTWH(0, 0, 40, 100), paint);

    /// FirstとSecondの間の線
    paint.strokeWidth = 3;
    paint.color = Colors.grey;
    canvas.drawLine(const Offset(40, 50), const Offset(150, 50), paint);

    /// Secondの箱
    paint.color = Colors.green;
    canvas.drawRect(const Rect.fromLTWH(150, 0, 40, 100), paint);

    /// FirstとThirdの間の線
    paint.color = Colors.grey;
    canvas.drawLine(const Offset(40, 50), const Offset(150, 170), paint);

    /// Thirdの箱
    paint.color = Colors.green;
    canvas.drawRect(const Rect.fromLTWH(150, 120, 40, 100), paint);
  }

  /// Repaintはfalse
  
  bool shouldRepaint(covariant CustomPainter oldDelegate) {
    return false;
  }
}
snova301snova301

いくつかエラーが出ていたので、修正して実行。

Tree View(BuchheimWalker)を選択。

いい感じですね。
ソースコードを見るとWidgetを指定する場所もあったので、おそらくElevatedButtonなども配置できるのではないでしょうか?

結論 : 簡単な系統図ならgraphviewで十分

snova301snova301

WidgetとCustomPaintを組み合わせる

簡単な系統図ならgraphviewでも十分ですが、オリジナルで作成したいとき、WidgetとCustomPaintを組み合わせる方法もあります。

Widget

body: InteractiveViewer(
  boundaryMargin: const EdgeInsets.all(10),
  child: Center(
    child: Column(
      children: <Widget>[
        /// 1番目のボタン
        ElevatedButton(
          onPressed: () {},
          child: const Text('first'),
        ),

        /// 線
        SizedBox(
          width: screenSize.width,
          height: 50,
          child: CustomPaint(
            painter: _LinePainter(),
            // painter: _TestPainter(),
          ),
        ),

        /// 2番目と3番目
        Row(
          mainAxisAlignment: MainAxisAlignment.spaceEvenly,
          children: <Widget>[
            /// 2番目のボタン
            ElevatedButton(
              onPressed: () {},
              child: const Text('second'),
            ),

            /// 3番目のボタン
            ElevatedButton(
              onPressed: () {},
              child: const Text('third'),
            ),
          ],
        ),
      ],
    ),
  ),
),

CustomPainter

class _LinePainter extends CustomPainter {
  
  void paint(Canvas canvas, Size size) {
    /// paintの定義
    final paint = Paint()
      ..color = Colors.grey
      ..strokeWidth = 2;

    /// Lineを描く
    canvas.drawLine(
        Offset(size.width / 2, 0), Offset(size.width / 3, size.height), paint);
    canvas.drawLine(Offset(size.width / 2, 0),
        Offset(size.width * 2 / 3, size.height), paint);
  }

  /// Repaintはfalse
  
  bool shouldRepaint(covariant CustomPainter oldDelegate) {
    return false;
  }
}

画面はこんな感じ。

個人的にはカスタマイズしやすいので、いいなあと思ってます。
ただし、数が増えると複雑なアルゴリズムになります。

このスクラップは2022/06/17にクローズされました