VC++5.0による巡回セールス問題プログラミング

 

1998.9.17

ワタベ

 

本報告は,Microsoft Visual C++ 5.0(以下VC)を用いたWindowsアプリケーションの作成方法を述べたものである.とは言っても,すべての機能の解説をするのが目的ではなく,具体的な問題(巡回セールスマン問題)を通して,VCの使い方の概要を掴むのが目的である.ただし,筆者自身VCについては初心者であるため試行錯誤的にプログラミングを行っている.たぶん,もっとエレガントな開発の仕方があるだろうと思う.しかし,何とか作ったプログラムでも後から見るとどうやって作ったがわからなくなることが多く,特にVCの場合はなおさらである.そこで,一通りのプログラミングの記録を取っておけば,後でまた違うプログラムを作ろうとするときの参考になるのではないか,という思いでこの記録を残している.いうなれば,自分自身に対するドキュメントである.

 

VC++5.0での作業の流れ

VC++5.0でのプログラミングは,大きくは以下の2つの作業から構成されると思われる.

スケルトンの生成は,アプリケーションの雛形を作るものであり,質問形式で答えていくことで簡単に行える.しかし,できあがったスケルトンはアプリケーションソフトとしての最低限の機能は備えているものの,あたりまえであるが,実際には何の役にも立たない.やはり,次のコーディングを行って初めて役に立つアプリケーションを作成できる.ところが,このコーディング作業が非常にわかりづらく,プログラミング初心者にとっては大きな壁となっている.

 それでは,以下でスケルトンの生成とコーディングに分けて,その作業例を述べていく.

 

[スケルトン(雛形)の生成]

  1. Developer Studio を起動し「新規作成」
    ・「プロジェクト」タブで,
    MFC Applicatio Wizard(exe)を選択
    ・「プロジェクト名」に適当なプロジェクト名(以下
    Test)を記入
    ・「位置」に適当なフォルダを指定(そのフォルダにすべてのファイルが作成される)
  2. 「ステップ1」
    ・作成するアプリケーションの種類(
    SDI, MDI,ダイアログベースのどれかを選択)
     以下,
    MDIを選択したことにして,進める
  3. 「ステップ2〜6」
    ・すべてデフォルトで進める
  4. 「完了」すると自動的にスケルトンが作成される
    自動的にできるクラスは,以下の通り.
    CAboutDlg...ヘルプなどのダイアログボックス
    CChildFrame...
    CMainFrame...
    CTestApp...
    CTestDoc...ドキュメントクラス(文書・データのセーブ・ロードなど)
    CTestView...ビュークラス(ドキュメントの表示など)
     各クラスは,主に2つのファイルから構成される.一つは
    C++のソースファイル(*.cpp)で,もう一つはヘッダーファイル(*.h)である.ソースファイルやヘッダーファイルのファイル名はクラス名の先頭文字Cを除いた名前になっている.たとえば,ビュークラスのファイルは,TestView.hTestView.cppである.
     以下のプログラミング作業中に直接触る可能性のあるのは「ドキュメントクラス」と「ビュークラス」ならびに「自分で追加するクラス」である.
  5. この段階で,「ビルド」して,「実行」すると,まだ何もプログラムしていないのに,Windowsアプリケーションが動く.
    起動したアプリケーションのタイトルバーには,プロジェクト名で指定した文字列(この場合
    Test)が表示されており,「ファイル」,「編集」,「表示」,「ウインドウ」,「ヘルプ」といったお馴染みのメニューがあり,また,メニューの下には「ツールバー」がありいくつかのボタンが並んでいる.そしてウィンドウの右上には,最小化・最大化・終了ボタンがあり,これらは正常に機能する.さらに,ウィンドウ下部には「ステータスバー」がある.ウィンドウ本体にはサブウィンドウが一つあり,「新規作成」メニューで新たなサブウィンドウが開ける.ただし,当然のことながら,このアプリケーションは何の目的も果たさない.目的のアプリケーションを作成するには,ここから多くの作業を必要とする.

[コーディング]

次に,実際にコーディングを行いアプリケーションを作ってみる.

ここで作成するアプリケーションは,MDI(マルチドキュメントインターフェース)型のもので,TSP(巡回セールスマン問題)のデータを設定し問題を解くものである.

その機能は次のようになる.

  1. ドキュメントウィンドウ上でマウスクリックすると,小さな円が表示される.この作業を任意回実行でき,各円は各都市の位置を表すものとする.
  2. 都市の個数・各都市の位置座標をファイルに読み書きできるようにする.
  3. 新たなメニュー(「TSP」−「Line」)で,各都市を入力した順に直線で結んで表示する.
  4. TSPを解くためのパラメータを入力するダイアログボックスをメニュー(「TSP」−「パラメータ」)で起動する.
  5. メニュー(「TSP」−「実行」)で,TSPを解き,その結果をドキュメントウィンドウに表示(都市間を結ぶ直線を表示)する.

以下,順にプログラムを作成する.

 

<ステップ1>

ドキュメントクラスとビュークラスに都市数と都市のx-y座標を表す変数を定義し,両クラス間で連動させる.

都市の数:m_Num

都市のx-y座標:m_PosX[], m_PosY[]

なるメンバー変数をビュークラスとドキュメントクラスに作成する.ビュークラスはドキュメントウィンドウへの表示を担当し,ドキュメントクラスはファイルとの読み書きを担当する.画面に表示されたデータをファイルに書き込んだり,逆にファイルから読み込んだデータを画面に表示できるように,この2つのクラスのデータは互いに連動していなければならない.

それでは,具体的にコードを記述する.

まず,ドキュメントクラスにメンバ変数を宣言する.TestDoc.hを開き,class CTestDocの中に

                       

// アトリビュート

public:

int m_Num;

int m_PosX[10000];

int m_PosY[10000];

                       

を記述する(上2行はあらかじめ記述されている).

同様に,ビュークラスにメンバー変数を定義する.TestView.hを開きclass CTestViewの中に以下のようなコードを追加する(上3行はあらかじめ記述されている.)

                       

// アトリビュート

public:

CTestDoc* GetDocument();

int m_Num;

int m_PosX[10000];

int m_PosY[10000];

                       

つぎに,データの初期化を行う.初期化は,都市数をゼロにセットすることで行える.

ドキュメントクラスのデータの初期化は,OnNewDocument()メンバー関数で行われる.TestDoc.cppを開き,OnNewDocument()関数を探し,その中に以下のコードを追加する(上2行はあらかじめ記述されている).

                       

// TODO: この位置に再初期化処理を追加してください。

// (SDI ドキュメントはこのドキュメントを再利用します。)

m_Num = 0;

                       

OnNewDoument()関数は,プログラムを起動したときや,「新規作成」メニューなどで新しいドキュメントを作成したときに呼び出される.

ビュークラスのデータの初期化は,OnInitialUpdate()関数で行われる.ドキュメントクラスと違って,OnInitialUpdate()関数はスケルトンでは定義されていない.そこで,新たにこの関数を定義する必要があるが,ここでは,Class Wizardを使って新たな関数を定義することにする.(もちろん,TestView.cppを直接開いてコードを記述しても良いが,かなり複雑になるので,Class Wizardを使って作成するのが便利である.)

Class Wizardを起動し,「メッセージマップ」タブを選択した状態で,以下のように各項目を選択する.

クラス名:CTestView

オブジェクトIDCTestView

メッセージ:OnInitialUpdate

そして,「関数の追加」ボタンをクリックする.(すると,.cppファイルに関数本体の外枠,.hファイルにプロトタイプ宣言が追加されている.)ついで,「コード編集」ボタンをクリックすると,OnInitialUpdate()関数定義部が表示されるので,以下のコードを追加する(実際に記述するのは太線部のみ).

                       

void CTestView::OnInitialUpdate()

{

CView::OnInitialUpdate();

// TODO: この位置に固有の処理を追加するか、または基本クラスを呼び出してください

// ドキュメントのポインタを取得する

CTestDoc* pDoc = GetDocument();

 

// ビュークラスのデータメンバを

// 対応するドキュメントクラスの値で更新する

m_Num = pDoc->m_Num;

int i;

for (i=0; i<m_Num; i++)

{

m_PosX[i] = pDoc->m_PosX[i];

m_PosY[i] = pDoc->m_PosY[i];

}

}

                       

この関数では,ドキュメントのポインタを取得し,そのポインタを介してドキュメントクラスのデータの値をビュークラスのデータに代入している.ドキュメントクラスでの初期化は都市数をゼロに設定しているだけであったが,ビュークラスの初期化では,都市数のみではなく,各都市の座標値もコピーしている.これは,ドキュメントクラスでデータをファイルからロードしたときのことを考慮しているためである.

 

<ステップ2>

画面表示用の機能を作成する.

画面表示の必要が生じると,ビュークラスのOnDraw()関数が呼び出される.そこで,何かを画面に表示するには,OnDraw()関数に表示のためのコードを記述すればよい.ここでは,「都市数(文字列)」と「各都市位置に円」を表示するものとする.ファイルTestView.cppを開いて,OnDraw()関数の中に以下のようなコードを記述する(実際の記述は太字のみ).

                       

void CTestView::OnDraw(CDC* pDC)

{

CTestDoc* pDoc = GetDocument();

ASSERT_VALID(pDoc);

 

// TODO: この場所にネイティブ データ用の描画コードを追加します。

RECT rect;

int i;

 

for (i=0; i<m_Num; i++)

{

rect.left = m_PosX[i] - 2;

rect.right = m_PosX[i] + 2;

rect.top = m_PosY[i] - 2;

rect.bottom = m_PosY[i] + 2;

pDC->Ellipse(&rect);

}

 

char buf[100];

sprintf(buf, "都市数:%d", m_Num);

pDC->TextOut(0,0,buf);

}

                       

このコードの前半では,都市位置を中心とする縦横4の正方形を定義し(rect),その正方形に内接する楕円,すなわち,円をpDCに表示している.pDCOnDraw()関数の引数で,表示用のウィンドウに対応している.後半部分では,都市数の値を組み込んだ文字列を定義し,その文字列を表示している.

この段階でプログラムを実行してみると,

都市数:0

の表示のみなされる.まだ,都市を定義していないので,都市位置を表す円は表示されない.

 

<ステップ3>

ウィンドウ内でマウスをクリックすると円を表示する機能を追加する.

ここでの処理は,都市を定義することである.都市を定義するのにマウスを用い,適当な位置でクリックするとその位置座標が変数に代入され,また,都市数を一つ増やす.そして,更新された変数の値を使って画面表示を行う.ステップ2で作成したように画面表示の関数OnDraw()はすでにできているので,各変数の値を更新したらOnDraw()関数を呼び出せば,画面に表示される.

さて,Windowsでは何かのイベントが発生すると,そのイベントに対応したメッセージがアプリケーションに送られてくる仕組みになっている.たとえば,マウスの左ボタンが押されたというイベントが発生すると,それに対応したメッセージ(WM_LBUTTONDOWN)がアプリケーションに送られる.
 したがって,ここでは,マウスの左ボタンが押されたというメッセージを受け取ったときに実行する関数を定義することになる.

具体的には,Class Wizardを起動し,「メッセージマップ」タブを選択した状態で,以下のように各項目を選択する.

クラス名:CTestView

オブジェクトIDCTestView

メッセージ:WM_LBUTTONDOWN

そして,「関数の追加」ボタンをクリックする.すると,OnLButtonDown()関数が追加され,「コード編集」ボタンをクリックすると,OnLButtonDown()関数定義部が表示されるので,以下のコードを追加する(実際に記述するのは太線部のみ).

                       

void CTestView::OnLButtonDown(UINT nFlags, CPoint point)

{

// TODO: この位置にメッセージ ハンドラ用のコードを追加するかまたはデフォルトの処理を呼び出してください

// マウスがクリックされた点のx-y座標を配列に格納し,

// 点の個数を +1 する

m_PosX[m_Num] = point.x;

m_PosY[m_Num] = point.y;

m_Num++;

 

// OnDraw()関数呼び出しを発生させる

Invalidate();

 

// 文書のポインタを取得する

CTestDoc* pDoc = GetDocument();

 

// 文書クラスのデータメンバをビュークラスの

// データメンバの新しい値で更新する

pDoc->m_PosX[pDoc->m_Num] = m_PosX[pDoc->m_Num];

pDoc->m_PosY[pDoc->m_Num] = m_PosY[pDoc->m_Num];

pDoc->m_Num = m_Num;

 

// 文書が修正されたことを通知する

pDoc->SetModifiedFlag(TRUE);

 

CView::OnLButtonDown(nFlags, point);

}

                       

この関数では,マウスの座標を都市位置変数に格納し,都市数変数を+1している.(マウスの座標はこの関数の引数pointに代入されている.)そして,Invalidate()関数を呼び出すことにより画面表示メッセージを発生させ,結果としてOnDraw()関数を実行している.ここまでで,画面表示は行われるが,マウスクリックにより更新されたのはビュークラスのデータのみなので,以下で,ドキュメントクラスのデータを更新している.また,SetModifiedFlag(TRUE)関数でドキュメントが更新されたことを通知している.こうすることで,データが更新されたにもかかわらず,データを保存せずにアプリケーションを終了しようとすると,確認のダイアログボックスが表示されるようになる.

ここまでの状態で,プログラムを実行すると,マウスクリックにより次々に都市位置が小さな円で表示されるようになり,また,都市数の値がカウントアップされるようになる.また,保存せずに終了しようとすると,保存を促すダイアログボックスがでてくる.(ただし,保存用のコードはまだ記述していないので,保存の操作は行えるが,実際には何も保存されない.)

 

<ステップ4>

ファイルへのセーブとロード機能を追加する.

スケルトンで既に,ファイルセーブ・ロードのメニューは存在する.そのメニューを選択するとセーブ・ロード作業は実行できるが,実際には何のデータも読み書きされない.これは当然のことで,まだどのデータを読み書きするのか何も書いていないからである.

ファイルへの読み書きは,ドキュメントクラスで行う.TestDoc.cppを開き,Serialize()関数を探す.そして,以下のようなコードを記述する.

                       

void CTestDoc::Serialize(CArchive& ar)

{

int i;

 

if (ar.IsStoring())

{

// TODO: この位置に保存用のコードを追加してください。

ar << m_Num;

for (i=0; i<m_Num; i++)

{

ar << m_PosX[i];

ar << m_PosY[i];

}

}

else

{

// TODO: この位置に読み込み用のコードを追加してください。

ar >> m_Num;

for (i=0; i<m_Num; i++)

{

ar >> m_PosX[i];

ar >> m_PosY[i];

}

}

}

                      

Serialize()関数は,メニューの「開く」,「上書き保存」,「名前を付けて保存」などを選択すると自動的に実行される.ここで,追加したコードは,都市数変数,都市位置座標配列の値をファイル(ar)に保存(上),読み込み(下)するものである.

 

<ステップ5>

新たなメニューを追加する.

都市の巡回経路の初期値(入力された順番に都市を結ぶ)の表示を「Line」メニューを選択することで行わせたい.そこで,アプリケーションに新たなメニューを追加し,そのメニューが選択されたときの動作を記述する.

MDIアプリケーションに新たなメニューを追加するには,インターフェイス部分は「Resource View」でグラフィカルに設計できる. まず,「Resource View」タブをクリックし,「Testリソース」をダブルクリックし,「Menu」をダブルクリックし,「IDR_TESTTYPE」をダブルクリックする.すると,現在のメニューが表示される.新たなメニュー項目を追加するには,既存のメニューの横に並んでいる四角い枠をダブルクリックする.すると「メニューアイテムプロパティ」ダイアログボックスが出てくるので,その「キャプション」欄にそのメニューに付ける名前を入力し(ここでは,「TSP」とする),ダイアログボックスを閉じる.さらに,今追加したメニューをクリックすると,その下に四角い枠が出てくるので,それをダブルクリックし,「ID」と「キャプション」を入力する(ここでは,IDID_TSP_LINE,キャプション:&Line).キャプションはそのメニューの名前であり,IDは,Class WizardのオブジェクトIDに対応している.そのオブジェクトに関連づけて,プログラムの中の変数や関数を定義することになる.

さて,次に今定義したメニューLineIDID_TSP_LINE)に関連づけて,そのメニューが選択されたときの動作を記述する.具体的にはLineメニューが選択されたときに実行する関数を定義する.Class Wizardを起動し,「メッセージマップ」タブを選択した状態で,以下のように各項目を選択する.

クラス名:CTestView

オブジェクトIDID_TSP_LINE

メッセージ:COMMAND

COMMANDは,メニューが選択されたときに発せられるメッセージである.次に,「関数の追加」ボタンをクリックすると,「メンバ関数の追加」ダイアログボックスが表示され,「メンバ関数名」がオブジェクトIDから機械的に生成されている(ここでは,OnTspLine).そこで,「OK」ボタンをクリックすると,OnTspLine()関数の雛形が生成される.「コード編集」ボタンをクリックすると,TestView.cppが開き,OnTspLine ()関数定義部が表示されるので,以下のコードを追加する(実際に記述するのは太線部のみ).

                      

void CTestView::OnTspLine()

{

// TODO: この位置にコマンド ハンドラ用のコードを追加してください

int i;

for (i=0; i<m_Num; i++)

{

m_Line[i] = i;

}

m_Line[m_Num] = 0;

m_LineNum = m_Num + 1;

 

// OnDraw()関数呼び出しを発生させる

Invalidate();

}

                      

この関数は,m_Line[]配列に都市の順序を入力された順番に格納し,さらに,最後に最初の都市へ戻るために,最後の都市としてゼロ番の都市を追加している.

 ここで,まだ宣言していない以下の変数が使われている.

m_Line[]

m_LineNum;

これらの変数は,とりあえず,ビュークラスでのみ使用するので,(ファイルへの読み書きは行わないのでドキュメントクラスでは必要ない),ビュークラスのヘッダーファイル(TestView.h)で型宣言を行う.TestView.hを開いて,ステップ1で記述した

int m_PosY[10000];

の下に,以下のコードを追加する.

                      

int m_LineNum;

int m_Line[10000];

                      

さらに,OnTspLine()関数の最後では,Invalidate()関数を呼び出して再描画(OnDraw()関数の起動)をしている.しかし,OnDraw()関数には,まだ,都市間を結ぶ直線を描画するコードを記述していないので,以下のコードをOnDraw()関数に追加する必要がある.

                      

// 巡回ツアー(直線)の表示

pDC->MoveTo(m_PosX[0], m_PosY[0]);

for (i=0; i<m_LineNum; i++)

{

pDC->LineTo(m_PosX[m_Line[i]], m_PosY[m_Line[i]]);

}                       

                      

また,m_LineNum変数の初期値を0にセットしておく必要があるので,OnInitialUpdate()関数に以下のコードを追加する.

                      

// 直線数を初期化する

m_LineNum = 0;

                      

 

<ステップ6>

メニューからダイアログボックスを起動する.

 TSPを解かせるために色々なパラメータを設定する必要があるが,ここでは,メニューからダイアログボックスを起動し,そこでパラメータ値を設定するものとする.

まず,ダイアログボックスを定義する.「Resource View」で「Dialog」を選択し,マウスの右ボタンメニューで「Dialogの挿入」を選択する.すると,ダイアログボックスの設計ウィンドウが表示されるので,そのウィンドウをクリックして右ボタンメニューでプロパティを選択し,「ID」と「キャプション」を設定する.ここでは,

IDIDD_TSP_DIALOG

キャプション:パラメータの設定

とする.次に,ダイアログに以下の項目を追加する.各項目はスタティックテキストとエディットボックスの対で設定する.具体的には,以下を必要回繰り返すことになる.

設定するキャプションとIDは以下のようにする.

個体数:IDC_PSIZE

最大世代数:IDC_MAXGEN

エリート数:IDC_NELITE

逆位率:IDC_RREV

最大逆位長:IDC_MAXLREV

最大逆位回数:IDC_MAXNREV

以上の設定が完了したら,Class Wizardを起動する.(設計中のダイアログをダブルクリックしてもClass Wizardは起動する.)すると,クラスの追加ダイアログが表示されるので,「新規クラスの作成」を選択する.「クラスの新規作成」ダイアログが表示されたら,クラス名,基本クラス,ダイアログIDを設定する.(ここでは,クラス名をCTspDialogに設定する.するとファイル名が自動的にTspDialog.cppに設定される.基本クラスはCDialogにダイアログIDIDD_TSP_DIALOGになっているのでそのまま利用する.)

次に,Class Wizardの「メンバ変数」タブで各エディットボックスに付けたIDを選択して「変数の追加」を行う.ここでは以下のようにメンバ変数を定義する.

IDC_PSIZE : int : m_PSize

IDC_MAXGEN : long : m_MaxGen

IDC_NELITE : int : m_NElite

IDC_RREV : double : m_RRev

IDC_MAXLREV : int : m_MaxLRev

IDC_MAXNREV : int : m_MaxNRev

以上で,パラメータ設定用のダイアログボックスの設定がとりあえずできたので,次にメニューを追加して,そのメニューでダイアログボックスを起動するための設定を行う.

Resource View」で「Menu」の「IDR_TESTTYPE」を選択し,前に追加したTSPメニューの項目に「パラメータ」を追加する.キャプションをパラメータとし,IDをパラメータ設定用ダイアログボックスに付けたIDIDD_TSP_DIALOG)に設定する.

さて,ダイアログボックスは新規クラス(CTspDialog)として定義されている.メニューはビュークラスにある.したがって,メニューからダイアログボックスを起動するには,ビュークラスからCTspDialogクラスを呼び出さなくてはならない.つまり,ビュークラスでCTspDialogクラスのオブジェクト(変数)を定義し,そのオブジェクト経由でダイアログボックスの表示を行うことになる.

具体的には,TspView.hに以下のコードを追加する.

まず,先頭部分に次のコードを記述する.

                      

#include "TspDialog.h"

                      

そして,アトリビュートの部分に,前に記述したコードの下に,以下のコードを記述する(追加部分は太字の1行のみ).

                      

int m_Line[10000];

 

CTspDialog m_dlg;

                      

ついで,Class Wizardを起動し,「メッセージマップ」タブを選択して,以下のように各項目を設定して,「関数の追加」を行う.

クラス名:CTestView

オブジェクトIDIDD_TSP_DIALOG

メッセージ:COMMAND

すなわち,「TSP」−「パラメータ」メニューが選択されたときに実行する関数を定義しようとしているのである.「コード編集」ボタンを押したときに出てくる位置に以下のコードを追加する(実際の追加は太字部分1行のみ).

                      

void CTestView::OnTspDialog()

{

// TODO: この位置にコマンド ハンドラ用のコードを追加してください

m_dlg.DoModal();

}

                      

以上で,メニューからダイアログボックスを起動できるようになった.エディットボックスに数値を入力することもできる.しかし,エディットボックスに数値を入力しただけでは各エディットボックスに対応するメンバ変数の値は書き変わっていない.エディットボックスに入力された値でメンバ変数の値を更新するには,UpdateData(TRUE)関数を呼び出す必要がある.それでは,いつどこで呼び出すべきか.作成したダイアログボックスには,初めから[OK]ボタンと[キャンセル]ボタンが付いている.そこで,この[OK]ボタンにメンバ変数を更新するためのコードを割り付けることにする.ついでに[OK]ボタンを押すと,各メンバ変数の値を表示するようにする.

実際の作業は,Class Wizardを起動し「メッセージマップ」タブを選択して,各項目を以下のように設定し,「関数の追加」ボタンを押す.

クラス名:CTspDialog

オブジェクトIDIDOK

メッセージ:BN_CLICKED

そして,「コード編集」ボタンで,TspDialog.cppファイルのOnOK()関数部を開き以下のようなコードを追加する.

                      

void CTspDialog::OnOK()

{

// TODO: この位置にその他の検証用のコードを追加してください

UpdateData(TRUE);

 

char buf[200];

sprintf(buf, "個体数:%d 最大世代数:%d エリート数:%d\n逆位率:%lf 最大逆位長:%d 最大逆位回数:%d"

, m_PSize, m_MaxGen, m_NElite, m_RRev, m_MaxLRev, m_MaxNRev);

MessageBox(buf);

 

CDialog::OnOK();

}

                      

UpdateData(TRUE)関数で,エディットボックス内に入力された値でメンバ変数の値を更新し,後の3行で各メンバ変数の値をメッセージボックスを使って表示している.

現状では,パラメータの設定ダイアログボックスの各エディットボックスの初期値はすべて0になっているが,適当な初期値が設定されている方が便利である.そこで,各変数の初期値を設定することにする.初期値の設定はダイアログボックスクラスの初期値設定関数(OnInitDialog())ではなく,ビュークラスの初期値設定関数(OnInitialUpdate())で行うことにする.なぜならば,もし,ダイアログボックスクラスの初期値設定関数で初期値を設定すると「パラメータの設定」ダイアログボックスが開くたびに初期化が行われることになり,これでは,パラメータの値を変更しても次にダイアログボックスを開いたときにはその値がまた初期値に戻ってしまうので具合が悪いからである.

それでは,ビュークラスのファイル(TestView.cpp)を開いて,OnInitialUpdate()関数に以下のコードを追加する.

                      

 // GAのパラメータの初期値を設定する

 m_dlg.m_PSize = 50;

 m_dlg.m_MaxGen = 1000;

 m_dlg.m_NElite = 1;

 m_dlg.m_RRev = 0.5;

 m_dlg.m_MaxLRev = 5;

 m_dlg.m_MaxNRev = 3;

                      

 

<ステップ7>

TSPを解き,結果をドキュメントウィンドウに表示する.

以上で,都市の設定,GAのパラメータの設定(ここでは,TSPを解くための手法としてGA:遺伝的アルゴリズムを想定している)が終わったので,最後にそれらのデータを使ってTSPを解き,結果を表示する.TSPを解くプログラムはインターフェイスから独立させた方が便利なので,TSPを解くクラス(SolveTSP)を作成し,ビュークラスからそれを呼び出して計算させ,結果をもらって表示するものとする.

TSPを解くのに必要なデータは以下の通りである.

都市の数:Num

都市のx-y座標:PosX[], PosY[]

GAのパラメータ:

  個体数:PSize

  最大世代数:MaxGen

  エリート数:NElite

  逆位率:RRev

  最大逆位長:MaxLRev

  最大逆位数:MaxNRev

 都市の巡回順序:Line[]

これらのうちLine[]は計算結果として得られる値である.

 それでは,以下SolveTSPクラスの作成とビュークラスからのSolveTSPクラスの呼び出しをコーディングしていく.

1.「Resource View」で「Dialog」の下に「IDD_SOLVE_TSP」を作成する.

2.Class Wizardで「クラスの追加」を行い,SolveTSPクラスを作成する.

クラス名:SolveTSP
基本クラス:
CDialog

(基本クラスをCDialogとしたが,何にするのが良いのかはよくわからない.とりあえず,メッセージボックスなどが使えそうなので,そうした.)
作成されるファイルは,
SolveTSP.h
SolveTSP.cpp
である.

3.SolveTSP.hファイルに以下のコードを追加する.

                      

class SolveTSP : public CDialog

{

// コンストラクション

public:

SolveTSP(CWnd* pParent = NULL); // 標準のコンストラクタ

 

// メンバ変数

int Num;

int PosX[10000], PosY[10000];

int PSize;

long MaxGen;

int NElite;

double RRev;

int MaxLRev;

int MaxNRev;

int Line[10000];

                      

4.「Resource View」でビュークラスのメニューの「TSP」に「実行」メニューを追加する.

5.TestView.hファイルに以下のコードを追加する.

先頭部に,

                      

#include "SolveTSP.h"

                      

アトリビュート部に,

                      

SolveTSP m_tsp;

                      

  1. Class Wizardで,
  2. クラス名:CTestView

    オブジェクトIDID_SOLVE_TSP

    メッセージ:COMMAND

    に「関数を追加」する.

    TestView.cppファイルを開いて以下の関数を追加する.

                          

    void CTestView::OnSolveTsp()

    {

    // TODO: この位置にコマンド ハンドラ用のコードを追加してください

    m_tsp.Exec(m_Num,

             m_PosX,

    m_PosY,

    m_dlg.m_PSize,

    m_dlg.m_MaxGen,

    m_dlg.m_NElite,

    m_dlg.m_RRev,

    m_dlg.m_MaxLRev,

    m_dlg.m_MaxNRev,

    m_Line);

     

       // OnDraw()関数呼び出しを発生させる

       Invalidate();

    }

                          

    すなわち,ビュークラスでメニュー「TSP」−「実行」を選択すると,SolveTSPクラスのExec()関数が呼び出され,Exec()関数での計算結果(m_Line)がドキュメントウィンドウに表示される.

  3. SolveTSPクラスで,SolveTSP.hファイルに以下のコードを追加し,

                      

   int Line[10000];

 

   // メンバ関数

void Exec(int, int [], int [], int, long, int, double, int, int, int []);

                      

SolveTSP.cppに以下の関数を追加する.

                      

void SolveTSP::Exec(int Num, int PosX[], int PosY[],

           int PSize, long MaxGen, int NElite,

           double RRev, int MaxLRev, int MaxNRev,

           int Line[])

{

char buf[200];

sprintf(buf, "Num=%d, PosX[0]=%d, PosY[0]=%d",

Num, PosX[0], PosY[0]);

MessageBox(buf);

sprintf(buf, "PSize=%d, MaxGen=%d, NElite=%d",

PSize, MaxGen, NElite);

MessageBox(buf);

sprintf(buf, "RRev=%f, MaxLRev=%d, MaxNRev=%d",

RRev, MaxLRev, MaxNRev);

MessageBox(buf);

 

// Line[1]Line[2]を交換する

int dummy;

dummy = Line[1]; Line[1] = Line[2]; Line[2] = dummy;

 

sprintf(buf, "Line[0]=%d, Line[1]=%d, Line[2]=%d",

Line[0], Line[1], Line[2]);

MessageBox(buf);

}

                      

ただし,このコードはExec()関数に正しく引数の値が渡っているかとビュークラスに結果が正しく返っているかを見るためのものである.Line[1]Line[2]を交換している(都市1と都市2の訪問順序を逆にする)ので,「TSP」−「実行」メニューを選択した後,ドキュメントウィンドウの線のつなぎ方が変わるはずである.

実際にはここにTSPを解くためのコードを記述する必要があるが,ここでは省略する.(C++の文法に則って,SolveTSPクラスにコードを記述すればよい.)