<戻る

ソフトウェア編
その2 ボタン操作をフラグ化するクラス


 パラレルポートのデータアドレスには8bitの情報を出力する必要があります。プログラムから複数のボタンを操作する時、ボタン情報を8bit情報に変換するクラスがあると楽ですね。ここでは手始めにボタン情報を格納して出力できるクラスを作ることにしましょう。

 作成は例のごとくテスト主導型開発(TDD)とYAGNIの原則で行きます。もちろん、オブジェクト指向です。TDDやYAGNIの原則に馴染みが無い方は、こちらをささっとご覧下さい。オブジェクト指向に馴染みが無い方は・・・さすがに説明し切れませんので、巷に溢れんばかりにあるオブジェクト指向の本をこれまたささっとご覧になってみてください。オブジェクトの意味と「継承」「多態性」辺りを理解してしまえば、後はなんとなくできます。



@ テクニクビートで使うコントローラオブジェクト

 絶対に必要なことから考える。これがYAGNIの原則です。今回の自動操作において絶対に必要なのは、どのボタンが押されているかを判断し、それを8bitの情報として保持し出力することです。そのために、まずテクニクビート用のコントローラオブジェクトの作成から始めてみます。テクニクビートは方向ボタンと○、×、△ボタンだけで操作可能です。

 やることリスト、いわゆる「ToDoリスト」は次のような感じでしょう。

ToDoリスト
・ 何も押さないと、(00000000))2=0が出力
・ デフォルトで○、△、□、×、上、下、右、左のbit番号がそれぞれ1,2,3,4,5,6,7,8と取得
・ ○のbit番号を2番に設定し、○のビット番号を取得すると2が返る
・ △のbit番号を5番に設定し、△を押すと、(00010000)2=16が出力
・ ○と△のbitをそれぞれ3に設定し、○と△を押すと、(00000100)=4が出力

 使用する側から立ったテストプログラムを先に立てる。これがTDDの手法です。1段目のテストコードは次のような感じになるでしょう。

TecnicContTest.h
class CTecnicContTest : TestCase
{
public:
   // 何も押さないと0を返すテスト
   void testDefaultOutput()
   {
      sp<CTecnicCont> spCont( new CTecnicCont);   // 動的生成
      assertEquals(0, spCont->GetCurState());   // ボタンが押されていないので0を返す
   }
};

 細かく説明します。CTecninContTestはCTecninContクラスをテストする専門のクラスです。親クラスのCTestCaseクラスにはテスト専門のメンバ関数が格納されています。この親クラスにはassertEquals関数という比較関数が定義されていて、2つの引数が等しければ「OK」、だめなら「だめ〜」っとデバッガに返します。sp<...>というテンプレートクラスはスマートポインタと呼ばれるもので、動的に作成したオブジェクトを自動的に削除する機能を持っています。どちらも事前に独自に用意したクラスで大変便利なものです。中身が気になる方は、CTestCaseクラスについてはこちらで、スマートポインタについてはこちらをご覧下さい。

 テストは、CTecninContというまだ定義すらされていないクラスのオブジェクトを生成し、そのクラスのGetCurState関数を呼び出してbitの情報を得ると0を返すだろう、という内容です。このテストプログラムをコンパイルし、出てくる沢山のコンパイルエラーを潰していけば、いつの間にか仕様を満たす事になる。これがTDDのやり方です。

 このテストを通す最も簡単なクラスは次のように定義されるでしょう。

TecnicCont.cpp
BYTE CTecnicCont::GetCurSatate()
{
   return 0;
}

TDDではコンパイルエラーをまずなくすプログラムを書きます。湧き上がる「最適化」をぐっと抑えて、最小限のプログラムを書くことで、仕様を満たす機能を最短で作り上げることが出来るわけです。これで、コンパイラエラーは無くなりますし、実行時エラー(assertEquals関数による比較)も無くなります。これを「グリーンシグナル」と呼ぶようです。


A デフォルトで○、△、□、×、上、下、右、左のbit番号をそれぞれ1,2,3,4,5,6,7,8と取得

 ToDoリスト2段目です。これは、ボタンとビット番号の対応をテストします。テストコードです。

TecnicContTest.h
void testDefaultBit()
{
   // デフォルトのビットを次のように取得
   // ボタン:○、△、□、×、上、下、右、左
   // ビット:1、2、3、4、5、6、7、8

   sp<CTecnicCont> spCont( new CTecnicCont);   // 動的生成
   assertEquals(1, spCont->GetBit(BTN_CIRCLE));       // ○
   assertEquals(2, spCont->GetBit(BTN_TRIANGLE));   // △
   assertEquals(3, spCont->GetBit(BTN_RECT));         // □
   assertEquals(4, spCont->GetBit(BTN_CROSS));       // ×
   assertEquals(5, spCont->GetBit(BTN_UP));            // 上
   assertEquals(6, spCont->GetBit(BTN_DOWN));       // 下
   assertEquals(7, spCont->GetBit(BTN_RIGHT));       // 右
   assertEquals(8, spCont->GetBit(BTN_LEFT));        // 左
}

 GetBit関数を新たに定義しています。また、各ボタンをあらわすマクロ定数を次のように考えています。

#define BTN_CIRCLE       1
#define BTN_TRIANGLE   2
#define BTN_RECT         4
#define BTN_CROSS       8
#define BTN_UP             16
#define BTN_DOWN        32
#define BTN_RIGHT        64
#define BTN_LEFT          128

 このテストを満たす最小限の実装は、GetBit関数内で各ボタンに対するswitch-case文を作ることでしょう。

TecnicCont.cpp
unsigned int CTecnicCont::GetBit(BYTE btnflag)
{
   switch(btnflag)
   {
   case BTN_CIRCLE;   // ○
      return 1;
   case BTN_TRIANGLE;   // △
      return 2;
   // ...以下延々と...
   }

   return 0;
}

 もちろん、これがダメなのはわかっています。しかし、今はテストを通すのが先です。これでコンパイルエラーも回避され、実行テストもグリーンシグナルです。



B ○のbit番号を2番に設定し、○のビット番号を取得すると2が返る

 ToDoテスト3段目です。ここでは○ボタンのビット番号を変更して、それがビット番号取得に反映されるかをテストします。テストコードは次の通り。

TecnicContTest.h
void testChangeCirBitTo2t()
{
   // ○のビット番号を2にセット
   // ビット番号を取得すると2になるテスト

   sp<CTecnicCont> spCont( new CTecnicCont);   // 動的生成

   // ○のビットを2に変更
   spCont->SetBitNum( BTN_CIRCLE , 2);

   // 状態取得
   assertEquals(2, spCont->GetBit( BTN_CIRCLE ) );
}

 SetBitNum関数が新しく定義されています。名前の通りビット番号をセットする関数です。ここで○のビット番号を2にセットして、すぐその状態を取得します。

 コンパイラエラーを最速に回避するコードはSetBitNum関数を空に定義することです。定義後コンパイルすると次のような実行時エラーが返ってきます。

デバッグ内容
■■ 期待値は 2 でしたが、引数は 1 となり異なっています。

 これは変更が反映されていません。これを回避するように実装していきます。
 まず、SetBitNum関数内で「○」のビット番号を変更すると言うことは、ボタンに対応するビット番号の配列を定義しなければなりません。そのメンバ変数名をm_BtnBitAryとしましょう。SetBitNum関数は次のように実装されます。

TecnicCont.cpp
void CTecnicCont::SetBitNum(BYTE btncode, unsigned int bitnum)
{
   m_BtnBitAry[ BtnElemHash(btncode) ] = bitnum;
}

 BtnElemHash関数はボタンのコードを見て配列の要素番号を返します。これはもう一意に決めてしまってよいでしょう。やることはswitch-case文です。

TecnicCont.cpp
unsigned int CTecnicCont::BtnElemHash(BYTE btncode)
{
   switch(btncode)
   {
   case BTN_CIRCLE:   // ○
      return 1;
   case BTN_TRIANGLE:   // △
      return 2;
   // ...以下延々と...
   }

   return 0;   // 不正コードの場合0を返す
}

 今はこれでよいと思います。ボタンコードが無い場合、不正なコードなので要素番号0番を返すことにします。つまり、m_BtnBitAry配列の0番目には使われないボタンのビット番号0番が入っていることになります。

 これでコンパイルしても、もちろん実行時エラーは回避されません。GetBit関数が固定されているからです。今一度この関数を訂正します。すでにm_BtnBitAry配列の中に指定のビット番号が入っているのですから、この関数の実装はとても簡単になります。

TecnicCont.cpp
unsigned int CTecnicCont::GetBit(BYTE btnflag)
{
   return m_BtnBitAry[ BtnElemHash(btnflag) ];
}

 これで実行テストをしてみると、このテストはOKなんですが、前のテストがおかしなことになります。

デバッグ内容
□□ testDefaultOutput: Good !
■■ testDefaultBit: 期待値は 1 でしたが、引数は -842150451 となり異なっています。
■■ testDefaultBit: 期待値は 2 でしたが、引数は -842150451 となり異なっています。
■■ testDefaultBit: 期待値は 3 でしたが、引数は -842150451 となり異なっています。
■■ testDefaultBit: 期待値は 4 でしたが、引数は -842150451 となり異なっています。
■■ testDefaultBit: 期待値は 5 でしたが、引数は -842150451 となり異なっています。
■■ testDefaultBit: 期待値は 6 でしたが、引数は -842150451 となり異なっています。
■■ testDefaultBit: 期待値は 7 でしたが、引数は -842150451 となり異なっています。
■■ testDefaultBit: 期待値は 8 でしたが、引数は -33686019 となり異なっています。

 これは明らかに「配列を初期化していない」というのが原因です。初期化はコンストラクタで行ってしまいましょう。

TecnicCont.cpp
#define BTN_NUM 9

CTecnicCont::CTecnicCont()
{
   // ボタンの初期ビット番号の設定
   for(int i=0; i<BTN_NUM; i++)
      m_BtnBitAry[i] = i;
}

ボタンの数は9つにします(使用しないボタン分)。これで実行時エラーも回避され、グリーンシグナルとなります。


C △のbit番号を5番に設定し、△を押すと、(00010000)2=16が出力

 ToDoテスト4段目。ここでは、△のビット番号を5に設定した後△を押し、ビットを立たせてその結果16を得るテストを行います。テストコードです。

TecnicContTest.h
void testChangeTriBitTo5AndGetState16()
{
   // △のビット番号を5にセット
   // △ボタンを押し、状態16を取得

   sp<CTecnicCont> spCont( new CTecnicCont);   // 動的生成

   // △のビットを5に変更
   spCont->SetBitNum( BTN_TRIANGLE , 5);

   // △ボタンを押す
   spCont->Push( BTN_TRIANGLE );

   // 状態取得
   assertEquals(16, spCont->GetCurState() );
}

 ここまでのテストで、やたらとメンバ関数名が長いのにお気づきでしょうか?これは、メンバ関数名を見ただけでどのようなテストを行っているかがわかるようにするためです。このクラスは一般に使用する目的が無いので、とにかくテスト内容を素早く知れることが大切です。よって、コメントも具体的に書きます。
 テストの内容は問題ないでしょう。新たにPush関数が追加されています。機能はそのまんまで、指定のボタンフラグを立てます。

TecnicCont.cpp
void CTecnicCont::Push( BYTE btnflag )
{
   // ボタンのフラグを立てる
   // 0番が使われていないことに注意
   unsigned int bit = m_BtnBitAry[ BtnElemHash(btnflag) ];
   if(bit)
      m_BtnFlags |= (0x1 << (bit-1) );
}

bitにはそのボタンコードのビット番号が返ります。このテストの場合は「5」です。使用されていない0番のボタンでなければビット番号を0基点に変換し(bit-1の部分)、左にシフトさせて、論理和を取ります。m_BtnFlagsは現在のボタン押し下げ情報(BYTEコード)が入っているとしましょう。

 これでコンパイルは通ります。しかし実行時エラーは出ます。GetCurState関数が固定されているからです。この関数では先に新しく設けたm_BtnFlagsを返すだけに変更します。

TecnicCont.cpp
BYTE CTecnicCont::GetCurState()
{
   return m_BtnFlags;
}

もちろんm_BtnFlagsはコンストラクタで0に初期化します。これで、実行時エラーも回避です。さらにいえば、これまでのテストも通ります。ある部分の変更が今までの仕様に影響するか否かを自動化されたテストで即座に判断できる。これがTDDの最大の魅力です。



D ○と△のbitをそれぞれ3に設定し、○と△を押すと、(00000100)=4が出力

 ToDoテスト5段目。これは、○と△を同じビット番号に設定すると、両方のボタンが同じ機能になるというテストです。テストコードを等はもう問題ないでしょう。新たに追加する関数もありませんし、実はもうテストは一発で通ります。よって、ここは割愛します!



E ○、△、□、×を押した後に○、△を離すと、(00001100)2=12が出力

 最後のテストです。○、△、□、×を押した後に○と△を離すことでフラグが降りて、12が出力されてくるというテストです。テストコードを示します。

TecnicContTest.h
void testPushCirTRCrAndCirTRelease()
{
   // ○、△、□、×を押した後に○と△を離すことでフラグが降りて、12が出力

   sp<CTecnicCont> spCont( new CTecnicCont);   // 動的生成

   // ○、△、□、×を押す
   spCont->Push( BTN_CIRCLE );
   spCont->Push( BTN_TRIANGLE );
   spCont->Push( BTN_RECT );
   spCont->Push( BTN_CROSS );

   // ○、△ボタンを離す
   spCont->Release( BTN_CIRCLE );
   spCont->Release( BTN_TRIANGLE );

   // 状態取得
   assertEquals(12, spCont->GetCurState() );
}

Release関数が新設されます。これはPush関数の逆で、フラグを降ろせば良いので、メンバ変数m_BtnFlagsと反転ビット論理積(NAND)を取ります。

TecnicCont.cpp
void CTecnicCont::Release(BYTE btncode)
{
   // ボタンのフラグを降ろす
   // 0番が使われていないことに注意
   unsigned int bit = m_BtnBitAry[ BtnElemHash(btnflag) ];
   if(bit)
      m_BtnFlags &= ~(0x1 << (bit-1) );
}

 これで、テストはグリーンシグナルです。

 ビット演算が苦手な方のためにちょっと2進数の説明。btncodeとして例えばBTN_TRIANGLE=2=00000010が入ってきたとします。「~」によってビットを反転させると11111101。m_BtnFlagsのビットとこれの論理積(AND)は、

btncode 1 1 1 1 1 1 0 1
m_BtnFlags 0 1 0 1 0 0 1 0
AND 0 1 0 1 0 0 0 0

となります。btncodeでビットが立っている部分に対応するm_BtnFlagsはそのままですが、btncodeでビットが降りている部分はm_BtnFlagsが何であろうと必ず0になります。よって、「降ろしたいフラグのNAND」を取ると、指定のフラグを降ろせるのです。

 まだ多少テストは足りないのですが、これでボタン情報を1バイトのbit列に変換するコントローラオブジェクトはできました。次は、1つのボタンに注目します。ボタンを任意の時間で押したり離したりする自動ボタン操作オブジェクトを作成します。時間を扱うオブジェクト。これはなかなかに面倒なのです。