ヤマネコでもわかるQtQuick(1)-概説と導入
今作のロボットから開発環境をUbuntuに切り替えたため,C#で開発していた経路シミュレータがお役御免となりました。
しかしWindowsで旧作マウスをいじることも考えられるため,クロスプラットフォームで動作するシミュレータをつくろうと思います。
開発環境は,Ubuntu12.04LTS, gcc4.8.4,Qt5.2です。
Windows環境はWindows7 Pro 64bit,GNU Make 3.81,Qt5.1です(近いうちにバージョンそろえた方が良いですね)。
QtはGUIアプリケーションをクロスプラットフォーム開発できる有名なライブラリですが,今回は一般的に用いられているQtCreatorを使わずに開発しようと思います。
(ただし,Windowsのmakeを利用したコンパイルがなぜか出来なかったので,コンパイル時のみQtCreatorを使用しています。まあWindowsでQtCreator使わないメリットが特に無いような気がするので良いでしょう)
Qtについてはまだ勉強中なので間違いが見つかると思いますが,その際は指摘をいただけると幸いです。
Qtって何よ?
公式サイトによると
Create connected devices, UIs and applications that run anywhere on any device, on any operating system at any time.
だそうです。
これはマルチプラットフォームで動作するGUIアプリケーションを作るためのフレームワーク,かみ砕いて言えば,Qtを使えばWindowsでもMacでもLinuxでも,はたまたAndroidでも動作するソフトが作れるようになります。
しかも同じソースコードで!
かの有名なSkypeやGoogleEarthなどもQtを利用して作られているそうですので安心して使うことができそうです。
対象読者
QMLとは
一言にQtと言っても,特にUI設計の作り方にはいくつかの方法があります。
フォームファイルでデザインする方法
VisualStudioでGUI設計することをイメージすると伝わりやすいと思います。 マウスでぽちぽちと部品を配置して,画面の設計をする方法です。 QtCreatorを使う必要があります。QMLでデザインする方法
独自のQML形式で画面設計を行います。 QMLはCSSとよく似た書式ですが,拡張性に優れています。
今回はQtCreatorを使いたくなかったので,QMLを用いた方式で開発することにしました。 QMLを構成している,QtQuickというフレームワークを利用します。
ここでQMLの優れている点をまとめておきます。
導入方法
コンパイルするために,makeとgccが必要です。
(今回の開発ではC++を用います)
当然Qtも必要です。
あわせて,Makefileを生成してくれるqmakeもインストールしておきます。
Ubuntu
gcc,makeはすでにインストールされているものとします。
sudo apt-get update sudo apt-get install qt5-default qt5-qmake
Windows
ダウンロード,インストールします。
Qt - Download
英語が読めない人は諦めましょう,と言いたいところですが,Google先生にでも頼って切り抜けてください。
正直なことを言うと,これくらいの英語が読めないとやっていけないです。
Qtのドキュメントはほとんどが英語です。頑張って慣れてください。
続いてmakeもダウンロード,インストールします。
Make for Windows
あとMinGWをインストールする必要があります。 MinGW - Minimalist GNU for Windows - Browse /Installer at SourceForge.net
次の三つのパスを通しておきます。
C:\Program Files (x86)\GnuWin32\bin C:\Program Files (x86)\MinGW\bin C:\Qt\Qt5.1.1\5.1.1\msvc2012_64\bin
拡張子
QMLを用いた開発の際,どのようなファイルが必要なのかまとめておきます。
- .cpp .h
C++のソースコードとヘッダです。 - .qml
QMLファイルです。 デザインを定義します。 - .pro
qmake用のファイルです。 Makefileを生成するためのルールを書きます。 - Makefile
qmakeにより自動生成します。
計画
以下未定
次回はコンパイルの仕方とQMLの基礎をまとめます。
壁情報の記録
マイクロマウスは壁情報を記録しながら走行し,その壁情報から最短経路を導出する競技なわけですが,さてこの壁情報はどうやって記録すべきなのでしょうか。
少なくともサークル内では次に紹介されている記録方法が主流なようです。
座標系・迷路表現の定義 | マイクロマウス委員会 関西支部
要約するならば,
迷路の各マスごとに次の8ビットの情報を持っている ・マスに隣接する壁の有無(4方向4ビット) ・マスに隣接する壁の到達情報(4方向4ビット)
といったところでしょうか。
したがってクラシックサイズの迷路を格納する場合,
8ビット × 縦16マス × 横16マス = 2048ビット(256バイト)
ものメモリ領域を食いつぶします。
今回は今作で採用している壁情報の記録法を簡単に紹介しておきます。
(そんな大げさなことじゃないですが,この方法をサークル内に普及させることが必要に感じたのでまとめています)
考え方は単純で,
外周の壁を除いた各壁1枚ずつに1ビットを割り当て,存在情報を記録する 各マスに1ビットを割り当て,到達情報をマスごとに記録する
といった感じです。
この格納法の場合,
|壁が1列に16枚 × 15列 + ー壁が1列に16枚 × 15列 + 縦16マス × 横16マス = 736ビット(92バイト)
のメモリ領域で話が済んでしまいます。
(前述した方法のおよそ35%の使用量)
具体的には次のクラスを作成しました。
(実際の実装は少し異なっています)
class Map{ private: /** * @brief 一番上の壁がMSB,一番下の壁がLSB,左から順 */ unsigned short column[15]; /** * @brief 一番左の壁がMSB,一番右の壁がLSB,下から順 */ unsigned short row[15]; /** * @brief 一番左のマスがMSB,一番右のマスがLSB,下から順 */ unsigned short reached[16]; /** * @brief マウスから見たWalldataを絶対方向に変換します * @param wall 変換元の壁情報 * @param ang マウスの向いている方向 * @return 変換後のWalldataを返します */ Walldata rotateWallToAbsolute(Walldata wall, EMouseAngle ang); /** * @brief 絶対方向のWalldataをマウスから見た方向に変換します * @param wall 変換元の壁情報 * @param ang 変換したい方向 * @return 変換後のWalldataを返します */ Walldata rotateWallToRelative(Walldata wall, EMouseAngle ang); public: Map(); /** * @brief 迷路データを初期化する */ void resetMap(); /** * @brief 到達マップを初期化する */ void resetReachedMap(); /** * @brief 壁を追加します * @param x 壁を追加する区画のx座標 * @param y 壁を追加する区画のy座標 * @param angle 今自分が向いている絶対方向 * @param wall 今見えている壁情報 */ void addWall(int x, int y, EMouseAngle angle, Walldata wall); /** * @brief 壁を設定します * @param x 壁を追加する区画のx座標 * @param y 壁を追加する区画のy座標 * @param angle 今自分が向いている絶対方向 * @param wall 今見えている壁情報 */ void setWall(int x, int y, EMouseAngle angle, Walldata wall); /** * @brief 壁を追加します。絶対方向でのみ指定が可能です。 * @param x 壁を追加する区画のx座標 * @param y 壁を追加する区画のy座標 * @param angle 今自分が向いている絶対方向 */ void addSingleWall(int x, int y, EMouseAngle angle); /** * @brief 壁を設定します。絶対方向でのみ指定が可能です。 * @param x 壁を追加する区画のx座標 * @param y 壁を追加する区画のy座標 * @param angle 今自分が向いている絶対方向 * @param wall 今見えている壁情報 */ void setSingleWall(int x, int y, EMouseAngle angle, int wall); /** * @brief 絶対方向から見て壁があるか確認します * @param x 壁を追加する区画のx座標 * @param y 壁を追加する区画のy座標 * @param ang 今自分が見ている絶対方向 * @return 壁が存在したら1,なかったら0を返します */ int isExistWall(int x, int y, EMouseAngle ang); /** * @brief 到達マップを設定します * @param x 到達設定する区画のx座標 * @param y 到達設定する区画のy座標 */ void setReached(int, int); /** * @brief 到達したか確認します * @param x 到達確認する区画のx座標 * @param y 到達確認する区画のy座標 * @return 到達していたら1,していなかったら0を返します */ int hasReached(int, int); /** * @brief =演算子のオーバーロード。Mapクラスを代入します */ Map& operator= (Map tmp); ~Map(); };
Walldata,EMouseAngle型は触れていませんが,適宜置き換えてください。
概ね上記の通りに実装すれば,それなりに使えると思います。
一応サークルの人(主に初めてプログラムを書く人)向けに補足を入れておきます。
- 今回作成したメンバ変数(グローバル変数として宣言したかもしれません)は,用意したメンバ関数(関数)以外からアクセスすべきではありません。とりわけ,今回のメンバ変数は直感的に操作できなさそうです。直感的に扱えないような変数を直感的に扱うために,メンバ関数を作成しました。したがって迷路情報を書き換えるときは必ずこのメンバ関数を利用することになります。
- このMapクラスは迷路情報の格納と操作のみを任されたクラスです。それぞれのクラス・変数・関数の担う範囲をよく考えてコードを書いてください。例えば,マウスのセンサ情報をこのクラス内で扱う必要は一切ありません。
- この設計では,迷路の外周壁データは用意していません。従って,外周壁を読み書きするように求められた場合は例外処理を書く必要があります。配列の範囲外アクセス,ダメ,絶対
- 実際に使用する際はaddWall関数を呼び出しています。しかし,3枚の壁情報を同時に格納する良い方法が思いつかなかったので,内部的にaddSingleWall関数を3回呼び出しています。
他の人がどうやって迷路情報を保存しているのか全然知らないので,意見などお待ちしています。
GooglePlayMusic->Supersonic
またまた,有名なクラウドサービスから自前クラウドサービス(?)に乗り換えるというお話です。
といっても今回は乗り換えではありません(GooglePlayMusicは関係ない)。
旅行の移動中や部室での作業中などに音楽をかけたいと思うようになったのですが,携帯はiPhone16GBモデルで,今持ってる音楽プレイヤーはBluetooth2.0しか使えないことが判明しました。
そこで最近流行のGooglePlayMusicっぽいサービスを自前で用意しようと思い立ちました。
今回使用したのは"Supersonic"なるソフトウェアです。
こいつが音楽データの管理・ストリーミング配信等を行ってくれます。
配信を聞くために,パソコンからはブラウザ,iPhoneからはiSub Music Streamerを使いました。
特に不便もなく利用できそうです。
環境
いつもどおりのUbuntuServer12.04 LTSサーバーにインストールしました。
本来であれば,
Mach5/supersonic · GitHub
このリポジトリをクローンして自前でパッケージをビルドすべきなのでしょうが,どうしてもコンパイルが通らなかったので
(プロジェクト外のファイルに対して"Not a v4.0.0 POM"というエラーが出ました),
ビルド済みのパッケージを使用しました。
Downloads · Mach5/supersonic · GitHub
cd /path/you/want/to/download sudo apt-get update sudo apt-get install lame ffmpeg sudo apt-get install default-jre-headless sudo dpkg -i supersonic-4.7.beta1.deb
インストールと同時に自動起動の設定もされるようです。
設定
ポートをあけてローカル環境からアクセスできるようにします。
デフォルト設定では4040番を使うそうなので,そのままにしておきます。
sudo ufw allow 4040
デフォルト状態ではセキュリティ上あまりよろしくないので,権限周りの変更をします。
/etc/default/supersonic SUBSONIC_ARGS="--port=4040 --https-port=4443 --max-memory=100" SUBSONIC_USER=supersonic
新規ユーザーを作成し,音楽データの保管先の所有権を変更します。
sudo useradd supersonic cd /var sudo mkdir music sudo mkdir playlists sudo chown supersonic:supersonic music sudo chown supersonic:supersonic playlists sudo service supersonic restart
ブラウザからの設定
これでとりあえずの準備はできたはずなので,早速4040ポートにアクセスします。
初期ID,パスワードはadmin, adminなのでログインし,表示されている3項目の設定変更を行います。
実際に使うことを考え,ユーザーアカウントも作成しておきます。
実はここにありがたい項目があり,ユーザーごとに最大ビットレートを変更することができます。
僕は携帯回線用と無線LAN用の2種類のアカウントを作成しています。
設定ができたらFTPなどを利用して音楽データを転送します。
僕は/var/music/以下にアーティスト名のディレクトリを作り,その下にアルバム名のディレクトリを作成しています。
データ転送後にはブラウザから音楽データのスキャンを実行する必要があります。
学内からのアクセス
本筋から少し離れるのですが,ここで少し苦戦しました。
携帯回線を使用する場合は構わないのですが,学内LANから4040ポートにアクセスできないようになっているので,リバースプロキシを利用して回避します。
リバースプロキシが何かは自分で調べてみてください。
すでにApache2を立ち上げているので,追加設定を行います。
/etc/apache2/sites-available/000-default.conf に追記 <VirtualHost *:80> ServerName music.hogehoge ProxyPass / http://localhost:4040/ ProxyPassReverse / http://localhost:4040/ </VirtualHost>
sudo a2enmod proxy
sudo a2enmod proxy_http
sudo service apache2 restart
合わせてDNSサーバーにもmusic.hogehogeを通しておき,アクセスできるようにしておきます。
以上の設定で"http://music.hogehoge:80/"または"http://music.hogehoge/"でSupersonicにアクセスできるようになりました。
他
今回ストリーミングサーバとしていくつかの環境を候補に上げました。
その中でも,良い比較記事を書かれているブログがあったので紹介しておきます。
ミュージックストリーミングWebサーバのAmpacheとSubsonicを比較してみた: boKUma
さてさて,大会前なのになんでこんなことやってるんだろうなあ。
頑張ってマウス作ります。
写真から迷路データの取得
題のとおりです。
マイクロマウスの迷路シミュレータ用にいちいち手打ちで迷路を入れるのが面倒だったので,写真を読み込ませるだけで迷路データを吐き出すというソフトウェアに挑戦してみました。
今回使用した環境は,gcc4.8.4,OpenCV2.4.8です。
今回のプログラムでは次の順に処理を行う必要がありました。
「斜めから取った写真を上から見た迷路に変換(ホモグラフィー変換)」→「扱いやすいように色空間を変換(RGBからHSV)」→「壁の上色である赤のみ抽出」→「迷路の認識」
ホモグラフィー変換
OpenCVに関数が用意されています。
・getPerspectiveTransform //変換行列の作成
・warpPerspective //画像に行列を適用
変換前の座標と変換後の座標を任意に指定することで,ホモグラフィー変換を行うことが出来ました。
Mat_<Vec3b> img1 = imread(src_image); //画像ファイルの読み込み Mat homography_matrix = getPerspectiveTransform(src_pt, dst_pt); warpPerspective(img1, img2, homography_matrix, img1.size()); imwrite(dst_image, img2); //画像ファイルの書き出し
色空間の変換
・cvtColor //色空間の変換
cvtColor(*src, colorImage, code);
おしまい。
赤色の抽出
汚いソースコードのみ掲載します。
void colorExtraction(Mat *src, Mat *dst, int code, int ch1Lower, int ch1Upper, int ch2Lower, int ch2Upper, int ch3Lower, int ch3Upper ){ Mat colorImage; int lower[3]; int upper[3]; Mat lut = Mat(256, 1, CV_8UC3); cvtColor(*src, colorImage, code); lower[0] = ch1Lower; lower[1] = ch2Lower; lower[2] = ch3Lower; upper[0] = ch1Upper; upper[1] = ch2Upper; upper[2] = ch3Upper; Mat tmp = Mat(src->rows, src->cols, CV_8UC3); uchar hsv[3]; for (int i=0; i<src->cols; i++) { for (int j=0; j<src->rows; j++) { hsv[0] = colorImage.at<Vec3b>(j, i)[0]; hsv[1] = colorImage.at<Vec3b>(j, i)[1]; hsv[2] = colorImage.at<Vec3b>(j, i)[2]; if (lower[0] <= upper[0]) { if(((lower[0] <= hsv[0]) && (hsv[0] <= upper[0])) && ((lower[1] <= hsv[1]) && (hsv[1] <= upper[1])) && ((lower[2] <= hsv[2]) && (hsv[2] <= upper[2]))){ tmp.at<uchar>(j, i*3) = 255; tmp.at<uchar>(j, i*3+1) = 255; tmp.at<uchar>(j, i*3+2) = 255; } else { tmp.at<uchar>(j, i*3) = 0; tmp.at<uchar>(j, i*3+1) = 0; tmp.at<uchar>(j, i*3+2) = 0; } } else { if(((lower[0] <= hsv[0]) || (hsv[0] <= upper[0])) && ((lower[1] <= hsv[1]) && (hsv[1] <= upper[1])) && ((lower[2] <= hsv[2]) && (hsv[2] <= upper[2]))){ tmp.at<uchar>(j, i*3) = 255; tmp.at<uchar>(j, i*3+1) = 255; tmp.at<uchar>(j, i*3+2) = 255; } else { tmp.at<uchar>(j, i*3) = 0; tmp.at<uchar>(j, i*3+1) = 0; tmp.at<uchar>(j, i*3+2) = 0; } } } } *dst = tmp; return; }
出力画像等
↓の画像を
↓になおして
↓のマスク画像にして
↓の迷路を出力させました
+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+ | | | + + + +---+---+---+---+---+---+---+---+---+---+---+---+ + | | | | | | + + + +---+ +---+---+---+---+---+---+---+---+---+ + + | | | | | | | + + + +---+ +---+ +---+---+---+---+---+---+ +---+---+ | | | | | | | | | + + + +---+ +---+ + +---+---+---+---+ +---+ + + | | | | | | | | | | + + + +---+ +---+ +---+---+ +---+ + + + + + | | | | | | | | | | | | + + + + +---+---+ + + +---+ + +---+ +---+ + | | | | | | | | + + + +---+---+---+ +---+---+ + +---+ +---+ +---+ | | | | | | | | | + + + + + +---+ + + + +---+ +---+---+ +---+ | | | | | | | | | | | | + + + + + + +---+ +---+---+ +---+---+ +---+ + | | | | | | | | | | + + + + +---+---+ +---+---+ +---+---+ +---+ + + | | | | | | | | | | | + + + + + + +---+---+ +---+---+ + +---+ + + | | | | | | | | | | | + + + + + +---+---+ +---+---+ +---+---+ + + + | | | | | | | | | | + + + + +---+---+ +---+---+ +---+ + + + + + | | | | | | | | | | | | + + + +---+ +---+ +---+ + + + + + + + + | | | | | | | | | | | + + + + +---+ +---+---+---+---+---+---+---+ + + + | | | | | +---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+
また書きたくなったら追記します(レポートと東北大会と冬コミつらい)
○参考
OpenCV 2.xでマウスコールバックなGUI - minus9d's diary
C++(OpenCV2.x)でのHomographyTransformation : とある大学院生のプログラミング
OpenCVでHSV変換して色抽出 - Qiita
ドメインを取得
初めてドメインを取得してみたので記録を残します。
今回はVALUE DOMAINでドメインを取得しました。
(広告でよく見る「お名前.com」と同じGMOが経営してるんですね。向こうと何が違うんでしょうか……)
VALUE-DOMAIN バリュードメイン
今回取得したのは".net"ドメイン。
初年度は680円、次年度以降も1280円で更新できるそうなので、これならばと思い取得しました。
DNSの簡単な設定と、サーバーの設定内容をまとめます。
VALUE DOMAINにはそこそこ使い勝手の良いコントロール画面があるので、そこからはじめの設定をしていきます。
とりあえず、DNSレコードと、DDNSパスワードの設定をしておけば問題はないと思います。
DDNS(Dynamic DNS)…個人宅で固定IPアドレスを取るのは財布にやさしくないということで、逐一IPアドレスをDNSサーバーに送信し、動的IPアドレスをドメインに紐付けることができます。今回はこれを利用しています。
DNSレコードの設定
VALUE DOMAINの場合、DDNSに対応したネームサーバは限られています。
まずはネームサーバを指定しました。
(今回の場合、NS1-5.VALUE-DOMAIN.COMを利用しています)
続いて、設定フィールドをいろいろと編集します。
とてもとても親切なことに、設定画面に懇切丁寧な説明書きがあるのでここでは詳説を省略します。
僕の場合、Webサーバー、クラウドサーバ、メールサーバに利用する予定があるので、次のとおり設定しました。
a @ IPアドレス a cloud IPアドレス a www IPアドレス mx mail.ほげほげ. 10 a mail IPアドレス
DDNSパスワードの設定
DDNSの更新通知はHTTPリクエストにより送信します。
VALUE-DOMAIN バリュードメイン
こちらのページに書かれている通りのリクエストをすると、先ほど設定したDNSレコードが変更されるというわけです。
一つ罠があって(まあ考えたら当たり前なのですが)、ダイナミックDNS設定情報の設定画面からDDNS用のパスワードを設定する必要があります。
これを任意のパスワードに設定後、ブラウザからリクエストを送信すると、レスポンスコード:0 が帰ってきました。
確認した後、サーバーの方の設定を行っていきます。
もうひとつ、僕の環境ではホスト名指定に"*(全ホスト)"を設定しても反映されませんでした。
原因はわかりませんが、www、cloud、@、mailのそれぞれに対してリクエストを送信することにしました。
サーバーの設定
サーバーからはwgetを使ってリクエストを送ります。
wget -O /dev/null http://なんやらかんやら
今回の場合、定期的にこのリクエストを送信したいので、cron用のスクリプトを作りました。
#!/bin/sh wget -O /dev/null http://なんやらかんやら > /dev/null 2>&1 wget -O /dev/null http://なんとかかんとか > /dev/null 2>&1
cronに1時間毎に設定し、無事稼働し始めたようです。
crontab -e でcronの設定、
crontab -l でcronの確認、
/var/log/syslog にログが記録されています。
さてさて、ホームページを作っていきましょうか……
OneDrive->ownCloud
Officeを購入した際,OneDrive(旧SkyDrive)との連携機能があったため,安易な気持ちでOneDriveを利用し始めました。レポートデータやマウスのデータ等はすべてここでクラウド化しています。
しかし,頻繁に起こる同期エラー(によって毎回出てくるエラーポップアップ),頻繁に起こる同期エラー(によって作成される重複データの数々),使いにくいWindows8.1用"アプリ"(なぜデスクトップクライアントがインストールできないのか),同期の遅いWindows8.1用アプリ,そしてLinuxではなかなか扱いにくい……。
大体以上の理由により,オンラインストレージを移行することにしました。
ownCloud
以前から知ってはいたのですが,今回本格的にownCloudを利用することにしました。
ownCloudとはオープンソースで開発されているオンラインストレージのソフトウェア一式のことです。
ownCloud.org
あくまで提供されているのはソフトウェアだけなので自前でサーバーを用意する必要はありますが,基本的に無料で利用できます。
今回は眠っていたサーバーを引き出してきました。
OSはUbuntu Server 12.04 LTS
こいつにownCloudを導入し,ノートPCや自宅のデスクトップから利用できるようにしました。
サーバーへの導入
今回は最新版であるownCloud8.1.0を導入します。
サーバーに必要なソフトウェアは,Apache2,PHP5.4以降,MySQL5.xです。
Ubuntu Serverの場合はOSのセットアップ時にまとめてインストールしておくのが手っ取り早いかと思います。
次のサイトが便利でした。
容量無制限のプライベートなDropBox環境を構築できるownCloud環境の構築手順をメモ
このサイトではサーバーソフトウェアを展開し,好きなディレクトリに配置するだけで導入が完了する方法を採用しています。
僕もこの手法を採用しました。
他にも,最近ではリポジトリに登録されているので楽々インストールができるみたいですが,未検証です。
クライアントの導入
公式サイトにクライアントのインストール方法がまとめてあります。
https://software.opensuse.org/download/package?project=isv:ownCloud:desktop&package=owncloud-client
各自適切なコマンドで簡単に導入をすることができます。
その後,設定画面の一般→一般設定 から自動起動するように設定しておきました。
Windowsへの導入はもっと簡単です。
公式サイトからパッケージをダウンロードし,そのままインストールするだけです。
Windows,Linuxの場合は,接続先サーバー,ユーザーを設定するだけで自動的に同期が始まりました。
モバイル
さて,パソコンで同期できるのはもちろんですが,モバイルでも使えるのがownCloudの特徴の一つです,が……
普段はiPhoneのDocumentsというアプリを用いているので,せっかくならownCloud専用アプリではなく,こちらでまとめて利用したいと思いました。
管理者アカウントからownCloudに接続して「設定」アイコンをクリックするとWebDAV用のURLを確認することができます。
自分の場合は"http://ドメイン/remote.php/webdav/"でした。
多くのファイラーはWebDAVに対応していると思われるので,これで一安心かと思います。
エスケープシーケンス
先日の講義の課題として,次のような課題が出されました。
任意の整数kを1つ発生させ,その数をあてるゲームを作成せよ
なお,予測した数が外れた場合は正解kとの大小関係を表示すること
プログラムの入門者向けの課題としてよく見るやつですね。
今回は,初めて出された「ゲーム」課題ということなので,UIにこだわってみることにしました。
ただしコンソール上で動くプログラムしか作ることが出来ないため,エスケープシーケンスというものに手を出してみました。
ゲームの計画段階では次の動作を考えました。
- ゲームクリア時・ゲームオーバー時は背景をそれっぽい色に染める →文字・背景色の変更
- 表示された大小関係のログを画面上に出す →カーソル位置の移動
エスケープシーケンス
エスケープ文字(\e, \033)に続けて文字列を記述することでコンソールの制御を行うことが出来ます。
Linuxでソフトウェアのインストールを行っている際に「 | / - \ 」がアニメーション表示されている様子を見かけるかと思いますが,恐らくこれのことです。
色の変更
"\033[x;y;z"を出力することで色の変更・属性の変更を行うことが出来ます。
x, y, zには次に示したコードが入ります。
また,これらは省略可能です。
文字色・背景色
色は数字2桁で示します "AB"
Aには,文字色変更の場合"3",背景色変更の場合"4"が入ります。
Bには,変更する色が入ります。次を参考にしてください。
0: 黒
1: 赤
2: 緑
3: 黄
4: 青
5: マゼンタ
6: シアン
7: 白
属性の変更
属性は次の通り指定できます。
1: 太字
2: 弱強調
4: 下線
5: 点滅
7: 反転
8: 非表示
カーソル位置の変更
"\033[y;xH"を出力することでカーソル位置を指定することが出来ます。
x, yにはカーソルを移動させる列,行が入ります。
画面クリア
背景色を変更した後,画面のクリアを行うことでコンソール全ての領域の背景を指定色に変更することが出来ます。
画面クリアは"\033[2J"を出力することで可能です。
サンプル
上記で書いた命令は一部ですが,今回はこれだけのエスケープシーケンスを使用して課題プログラムを作成しました。
サンプルコードを貼り付けておきます。
#include <stdio.h> #include <stdlib.h> #include <time.h> void bye(){ printf("\033[0m"); // 文字属性・文字色を初期化します。これをしないとそのまま残ってしまいます exit(1); } /* 課題として設定された関数群なので気にしないでください */ /* エラーメッセージを表示した後,終了する関数です */ void usage1(int n){ printf("Usage: k62301 a b n\na: max, b: min, n: try\nyour input was %d\n", n-1); bye(); } void usage2(int a, int b){ printf("a must be small than b\nyour input was a=%d, b=%d\n", a, b); bye(); } void usage3(int n){ printf("0 < n < 50\nyour input was n=%d", n); } void showLog(int log[], int current, int k){ int i, j; printf("\033[2J"); for(i=0; i<current; ++i){ // カーソル位置を左上の方に移動させています。 printf("\033[%d;%dH", i%20+1, (int)(i/20)*25); if(log[i]<k) printf("%2d: %d < ans\n", i+1, log[i]); else printf("%2d: ans < %d\n", i+1, log[i]); } } int main(int argc, char *argv[]){ int a, b, n; int i, j; int k; int tmp; int t; int startTime; int log[50]; startTime = (int)time(NULL); srand(startTime); if(argc != 4) usage1(argc); a = atoi(argv[1]); b = atoi(argv[2]); n = atoi(argv[3]); if(b < a) usage2(a, b); while(n < 1 || n > 49){ usage3(n); scanf("%d", &n); } k = rand()%(b-a+1); k += a; // 文字色を黄色,背景を黒色に printf("\033[33;40m"); // 画面をクリア printf("\033[2J"); i = n; while(i--){ // 23行目0列にカーソルを移動 printf("\033[23;0H"); printf("*Challenge %d\n", n-i); printf("your answer = "); scanf("%d", &tmp); log[n-i-1] = tmp; showLog(log, n-i, k); if(tmp < k){ printf("\033[23;40H"); printf("\ttrue answer is bigger than %d\n", tmp); continue; } else if(tmp > k){ printf("\033[23;40H"); printf("\ttrue answer is smaller than %d\n", tmp); continue; } printf("\033[37;43m"); printf("\033[2J"); printf("\033[15;25H"); printf("Clear!\n"); printf("\033[16;25H"); printf("The answer is %d\n", k); printf("\033[17;25H"); printf("Clear time is %d\n", (int)(((int)time(NULL)-startTime))); usleep(1000000); bye(); } printf("\033[37;41m"); printf("\033[2J"); printf("\033[15;25H"); printf("Game over\n"); printf("\033[16;25H"); printf("The answer is %d\n", k); printf("\n"); usleep(1000000); bye(); return 0; }
英語が正しいかはさておき,ひとまずこれで動作しました。
ざっとコメントを書いておいたので,真似れば使えるようになるはずです。
Windowsで使用する場合
今回紹介したエスケープシーケンスはANSIで決められたものだそうです。
Windowsのcmd.exeで使用する場合は少し小細工が必要です。
"ansicon"を使いましょう。
ダウンロードしてインストールコマンドを打てばすぐに使えるようになりました。
わかりやすくまとめてある記事があったので,紹介にとどめておきます。
Windows環境でRSpecなどの出力に色を付けるには - 萬由無事覚書
☆参考
もう一度基礎からC言語 第47回 特殊な画面制御~コンソール入出力関数とエスケープシーケンス エスケープシーケンスによる画面制御
シェル - echoで文字に色をつける その1 - Miuran Business Systems
Windows環境でRSpecなどの出力に色を付けるには - 萬由無事覚書