Qtを使用した迷路シミュレーターを作る (迷路情報の読み込み、壁画編)

2020年6月7日

マイクロマウス関連記事のまとめはこちら。

前回は、迷路シミュレータの外観の設定や、ボタンを押した後の処理の書き方といったことを設定しました。今回は、迷路シミュレータとしての骨組みを作っていきます。

 




迷路情報について

迷路情報の保存の仕方について

迷路情報のフォートマットは、東京理科大学Miceさんの迷路ツールを参考に、変数のビットの内容が"1″のとき壁があり、"0″の時壁がないという形で判断するようにしました。

uint8_t型の16*16の二次元配列に迷路情報をまとめることにしました。8ビットのビットは下の表のように使用しています。

* 機能を割り振りしていないビットには"-“と書きました。

7 6 5 4 3 2 1 0
西
0 0 0 0 0/1 0/1 0/1 0/1

具体的な壁情報の保存例を以下に示します。

  • 0x0A(0b00001010)の時: 南、西壁に壁があり、北、東壁がない
  • 0x0F(0b00001111)の時 : 東西南北のすべての壁がある
  • 0x00(0b00000000)の時: 東西南北のすべてに壁がない

シミュレータのプログラムに迷路情報を持ち、シミュレータ上でプログラムが持っている迷路情報を選択するという形にすることも考えましたが、迷路情報を追加するたびにプログラムの訂正を行うことに魅力が感じなかったためファイルの読み込みをできるようにするということに決めました。

 

迷路ファイル作成用プログラムについて

ファイルの作成は、別にCUIのプログラムを作成し、そこで行うことにします。

ソースコードは以下の通りです。

迷路配列をファイルに出力するだけです。
テキストファイルが完成したので、Qtによるファイルの読み込みをやっていきたいと思います。

 

ファイルの読み込み

実装方法を考える

loadボタンが押されたらダイアログボックスからファイルを読み込み、内容をactionHistoryEditに表示するようにしていきたいと思います。
Qt Documentでダイアログを扱えるクラスを探したら、QFileDialog Classを見つけることができました。static関数である、getOpenFileNameでQStringでファイルの絶対パスを取得できることがわかりました。
次に、Qtのファイルを取り扱うクラスであるQFile Classを見ていきます。コンストラクタの引数にファイルの絶対パスを入れることで、開くファイルの指定、open関数を使用することでRead・Writeで開けたかどうかをTrue・Falseで返してくれることがわかりました。
しかし、QFileクラスではファイルの中身を読み込むことはできなそうです。QTextStream Classで読み込みを行うことができることが分かりました。ここで、QTextStream Classには、readAll,readLineという関数があり、1行ずつ取得する関数とすべて取得する関数があることがわかりました。一度、迷路情報を配列に保存する前に先ほどのプログラムで思った通りにファイルが作られているか確認するために、すべて読み込んで表示してみたいと思います。
また、actionHistoryEditは文字列の表示を行うので、編集ができない方がよいと判断をたため、init関数内でReadOnlyに設定しておきました。

実装を行う

ここまでの内容を踏まえて実装すると次のようになりました。

void MainWindow::on_loadButton_clicked()
{
    QString fileName = QFileDialog::getOpenFileName(this, tr("Open File"), "", tr("Text File (*.txt);;"));

    if ( !fileName.isEmpty() ){
        QFile file(fileName);
        if ( !file.open(QIODevice::ReadOnly)){
            QMessageBox::critical(this, tr("Error"), tr("This System could not open File"));
            return;
        }

        QTextStream stream(&file);

        QString data = stream.readAll();

        ui->actionHistoryEdit->setText(data);

    }

}

実行して、loadボタンを押してダイアログからファイルを読み取ると、ActionHistoryに表示ができたと思います。

迷路情報は、数字-空白-数字の順番で並んでいると思います。

 

迷路情報を配列に入れる関数を実装する

配列に文字列を入れるためには、一行ずつ読みとって、空白で区切りそれらを一つずつの配列に入れるという実装をすることで可能であるということがActionHistoryに表示された内容から考えられます。プログラムの手順としては、readLineで一行ずつ読み取ったのち、QList Classのsplitを使用して空白ごとにデータを分け、QString ClassのtoInt関数でint型に変更を行うことでできました。

void loadMazeData(QTextStream *maze_data, int wall_data[16][16])
{
    for ( uint8_t x = 0; x < 16; x++ ){
        QString str = maze_data->readLine();
        QList<QString> list = str.split(" ");
        for ( uint8_t y = 0; y < 16; y++ ){
            wall_data[x][y] = list[y].toInt();
        }
    }
}

 

ファイル読み込みの実行結果

プログラムが正常に動作した場合は、ロードボタンを押して、ファイルダイアログからテキストファイルを読み込むと、ActionHistoryEditにテキストファイルの中身を表示されると思います。

グラフィックスビューワーに迷路を描く

グラフィックスビューワーに線と文字を表示する

迷路の表示は以下に示すことを満たしたものにしたいと考えています。

  • 区画の数字を上と下にそれぞれ入れる
  • 壁の色は赤色で引く
  • 壁のないところには灰色の線を引き、区画をわかるようにする

 

以上の点を実現するためには、どうやって線や文字を指定した座標に描くかがわからないとできないので、線や文字の追加のプログラムの書き方を書いていこうと思います。
グラフィックスビューワーに線などを描くことは、QGraphicsScene Classを使用することでできるそうです。実際にプログラムを書いて確認をしていきたいと思います。

確認は、MainWindowクラスのinit()内で行いました。

void MainWindow::init()
{
    ui->filePathEdit->setReadOnly(true);
    ui->actionHistoryEdit->setReadOnly(true);

    QGraphicsScene *scene = new QGraphicsScene;
    scene->addLine(10, 10, 100, 100 );
    ui->graphicsView->setScene(scene);

}

線を引くプログラムの実行結果

グラフィックスビューワーに線を引くことができました。

線の太さや、色を変えたい場合はQPen Classのインスタンスに設定して引数に与えてあげることでできます。

また、文字を指定した座標に表示するプログラムは次のように書くことでできます。

    QGraphicsTextItem *text = scene->addText("Maze Solver");
    text->setPos(10, 10);

 

 

迷路を描くためのクラスを作成し実装をする

迷路を描くためのクラスを作成しました。

クラスの作成方法は、左上のファイルをクリック、出てきたダイアログのファイルとクラスからC++を選択してC++クラスを選択をすると、クラスの定義という画面になると思います。クラス名は、迷路を描くクラスなので愚直にDrawMazeとしました。

決定して、プロジェクト管理に追加するというところは、追加しないのままOKを押すと、drawmaze.cpp/hファイルが生成されると思います。

壁画をするプログラムは、次のようになりました。

#ifndef DRAWMAZE_H
#define DRAWMAZE_H

#include <QtWidgets>

class DrawMaze
{
public:
    DrawMaze();

    void init(QGraphicsScene *scene);

    void drawWall(QGraphicsScene *scene, int wall[16][16]);

private:
    const int step = 43;

    QPen pen;

};

#endif // DRAWMAZE_H
void DrawMaze::init(QGraphicsScene *scene)
{

    pen.setColor(Qt::gray);
    pen.setWidth(3);

    for( int i = 1; i < 17; i++ ){
        QGraphicsTextItem *text_row_on = scene->addText(QString::number(i-1));
        text_row_on->setPos( i * step + 11, 0 );

        QGraphicsTextItem *text_row_under = scene->addText(QString::number(i-1));
        text_row_under->setPos( i * step + 11, step * 17 + 21);

        QGraphicsTextItem *text_column_left = scene->addText(QString::number(16-i));
        text_column_left->setPos( 0, i * step + 11 );

        QGraphicsTextItem *text_column_right = scene->addText(QString::number(16-i));
        text_column_right->setPos( step * 17 + 21, i * step + 11 );

    }

    for ( int i = 1; i <= 17; i++ ){
        scene->addLine(i*step, step, i*step, 17*step, pen);
        scene->addLine(step, i*step, 17*step, i*step, pen);
    }

}

void DrawMaze::drawWall(QGraphicsScene *scene, int (*wall)[16])
{
    scene->clear();
    init(scene);

    pen.setColor(Qt::red);
    pen.setWidth(3);

    for( int x = 0; x < 16; x++ ){
        for( int y = 0; y < 16; y++ ){
            int n = (wall[x][y] & 0x01);
            int e = (wall[x][y] & 0x02) >> 1;
            int w = (wall[x][y] & 0x04) >> 2;
            int s = (wall[x][y] & 0x08) >> 3;

            if( n == 1 ) scene->addLine( (x+1)*step, (16-y)*step, (x+2)*step, (16-y)*step, pen );


            if( e == 1 ) scene->addLine( (x+2)*step, (17-y)*step, (x+2)*step, (16-y)*step, pen );


            if( w == 1 ) scene->addLine( (x+1)*step, (17-y)*step, (x+1)*step, (16-y)*step, pen );


            if( s == 1 ) scene->addLine( (x+1)*step, (17-y)*step, (x+2)*step, (17-y)*step, pen );

        }
    }
}

init関数では、区画をグレーで壁画、区画の数字の挿入を行い、drawWall関数で壁のある所を赤色で上から塗りつぶすようにしました。

また、drawWasll関数では、sceneの前回の壁情報をクリアにした後に、init関数を呼び出し、迷路の壁情報から壁があるかないかをチェックしてあるところのみ赤色で線を上書きするようにしました。

DrawMazeクラスの内容を実行するために、MainWindowクラスは次のように変更しました。

void MainWindow::init()
{
    scene = new QGraphicsScene;

    ui->filePathEdit->setReadOnly(true);

    ui->actionHistoryEdit->setReadOnly(true);

    for(int i = 0; i < 16; i++) {
        for(int j =0; j < 16; j++ ){
            maze[i][j] = 0;
        }
    }

    drawmaze = new DrawMaze();

    drawmaze->init(scene);

    ui->graphicsView->setScene(scene);

}

void MainWindow::on_drawMazeButton_clicked()
{
    drawmaze->drawWall(scene, maze);

    ui->graphicsView->setScene(scene);

    QMessageBox *box = new QMessageBox;
    box->setText("You push draw Maze Button");
    box->exec();

}

MazeDrawクラスのインスタンスはprivateメンバとして定義をしています。

 

迷路壁画をするプログラムの実行結果

ここまでの内容のプログラムを書いて実行した結果です。

ファイルを読み込んで、draw Mazeボタンを押すことで迷路の表示ができるようになりました。

 

 

さいごに

これで、ファイルから迷路情報の読み込みを行い、迷路を画面上に表示ができるようにないりました。次回は、左手法を実装して動かしていきたいと思います。

最後まで一緒に頑張っていきましょう!

 

参考文献

東京理科大学Mice 迷路ツール

QtDocumentationm