準備完了です。 これまでに使った事柄を応用して, Lesson 8の冒頭で一度概略を説明した「採点機能付き計算練習プログラム」を作りましょう。 これまでに紹介したさまざまな機能を利用した長いプログラムになるので, ひとつひとつ, しっかり理解していってください。
このLessonでは, まず最終的に作るプログラムの「仕様」を決めておいて, それを目指して今までのプログラムを改良していきましょう。 これまでは, 最初に小さなプログラムを作り「こう変えた方がいい」という改良をどんどん追加していく形で作ってきました。 しかし, 多くのプログラムは, 発注者から依頼を受けてその要求通りに動作するように作るので, このLessonでみるように, 最初に仕様を決めて, それを目指して開発するという手順の方が一般的です。 ここでは, その手順を踏んでみることにしましょう。
これから作るプログラムは通常のアプリケーションプログラムなどに比べれば小規模なものなので, ラフなレイアウトとそれぞれの部品に対するちょっとした説明で「仕様」に代えることとします。
一般に, プログラム(システム)を開発する際には, 発注者と開発者が合意した書式に則って「仕様書」を作成し, その仕様書に合うようプログラムを開発します。 大規模なプログラム(システム)になると, 何百ページにも渡る仕様書を書き, 両者が詳細に合意してから, 開発を始めることになります。 印刷すると, プログラムそのものよりも, 仕様書の方がページ数が多くなることも珍しくはありません。
これから作る「採点機能付き計算練習」ページ(「計算練習ページ」と略します)は次のようなレイアウトを持ちます。
|
| 図13-1 採点機能付き計算練習ページのレイアウト |
仕様を書く段階ではまだプログラムはできていないので, 普通はこのようなレイアウトは手で書いたり, 描画ソフトや図形作成機能をもったワープロなどを使って書いたりすることになります。 ただ, JavaScriptを使えばレイアウトは比較的簡単にできるので, レイアウトを出力する部分のプログラムだけを最初に作ってしまうという手もあります。
このレイアウトでわかるように, 「計算練習ページ」は次の3つの部分に分けられます。
「タイトル部分」は「けいさんれんしゅう」という文字を中央に大きめの文字で表示します。
「問題部分」は「タイトル部分」の下に置かれ, 20題の計算問題が縦に10行, 横に2列に並びます。 「問題部分」はさらに「問題」と「解答欄」にわかれ, 「解答欄」には利用者が答を入力できるようになっています。 マウスをクリックして次の問題に移動するのは大変なので, 「解答欄」でタブ(Tab)キーを押すと次の問題に移動することにします。 ただし, 「次の問題」を次のように決める(定義する)ことにします。
そして, 次の機能も追加しましょう。
さらに, 次の条件も満たすことにしましょう。
「問題」には以下のような条件があります。
一番下の「採点部分」は次の4つの部分から成り, それぞれ次のような機能を持ちます。
最後に次の条件を課すことにします。
以上のような要求を満たす計算練習のページを作るのが最終目標です。
まず全体のレイアウトを考えましょう。 前に作成した「計算プリント」のページもそうでしたが, いつも同じ問題を使うわけにはいかないので, HTMLだけを使っては書けません。 JavaScriptを使って, 問題部分が実行のたびに変化するHTMLのコードを生成し, これをブラウザに表示してもらうことになります。 このため, まずどのようなコードを生成する必要があるかを検討しましょう。
生成すべき部分として「タイトル部分」「問題部分」「採点部分」の3つがありました(仕様を参照)。 まず, 「タイトル部分」は簡単です。 「けいさんれんしゅう」と中央に大きめの文字で表示するので, これはJavaScriptを使うにしろ使わないにしろ, 次のようにすればよいでしょう。
<h1 align="center">けいさんれんしゅう</h1>
「採点部分」もそれほど難しくありません。 単純に, 1行に並べてもよいですが, 表機能を使った方が細かな制御ができるので, たとえば次のような感じにしましょう。
<form ...>
<table align="center">
<tr>
<td><input type="button" name="saiten" value="さいてん"
onclick="saitenFunc()">
</td>
<td>せいかい<input type="text" name="seikai" size="3"
readonly="1">
</td>
<td>まちがい<input type="text" name="machigai" size="3"
readonly="1">
</td>
<td align="right"><input type="button" name="again" value="つぎへ"
onclick="location.reload();"></td>
</tr>
</form>
全体を1行に納めるので, 表の1行を表す<tr>...</tr>は1組だけで, 「さいてん」および「つぎへ」のボタンと「せいかい」および「まちがい」の数を表示する欄を, それぞれ<td>...</td>で囲んでいます。
ここで<form ...>と属性を指定しなかったのにはわけがあります。 「採点部分」だけでなく, この上に書くことになる「問題部分」でも, 解答の入力用にフォームが必要です。 1ページに<form>...</form>のペアを複数を使うこともできますが, ここではその必要はないので, 「問題部分」と「採点部分」は同じ<form>...</form>の中に書くことにしましょう。 したがって, <from>タグとその属性の指定は, 「問題部分」の始まりに書くことになります。
[さいてん]ボタンをクリックするとsaitenFunc()が呼ばれるので, この関数を定義して採点をする必要があります。 この関数の具体的な内容については, 後で考えましょう。
[つぎへ]ボタンのonclick属性にはlocation.reload()という関数が指定されています。 これは, 今見ているページを再ロード(再読込)するという関数です。 再ロードすると, 問題生成用の関数が再度呼ばれて別の問題が生成され, 表示されることになります。
「せいかい」と「まちがい」の右に置かれる2つのテキスト入力欄には「readonly="1"」が指定されています。 これによって, この欄のテキストは読み込み専用(Read Only)になり, 利用者からは変更できなくなります。 採点欄を利用者がさわる必要はないのでこうしておいた方がよいでしょう。
古いブラウザだとreadonlyの指定が有効にならないことがあり, この場合, 利用者が値を変更できてしまいます(単に無視されるのでエラーにはなりません)。 したがって, 仕様にはこの値を変更できないようにせよとは書かれていないので(あまり本質的に重要な問題ではないので), 単に対応しているブラウザではこの機能が有効になるというだけのことにしておきましょう。
さて, 問題なのは「タイトル部分」と「採点部分」の間に入れる「問題部分」です。 「前にやった『計算プリント』のページ(Lesson 7)と同じようにやればいいのでは?」と思った人もいるでしょうが, 実はそうはいきません。 タブによるフォーカス移動の順序を考えなければならないのです。 実際にやってみるとわかりますが, 単に10行2列の表を図13-2のように作成すると, タブによる移動で左から右が優先されてしまって, 上から下には移動してくれません。
<form> <table> <tr><td>1<input type="text"></td><td>2<input type="text"></td></tr> <tr><td>3<input type="text"></td><td>4<input type="text"></td></tr> ... <tr><td>19<input type="text"></td><td>20<input type="text"></td></tr> </table> </form>
|
| 図13-2 10行×2列の表をひとつだけ使った場合のタブ順 |
どうしたらよいでしょうか? いくつかの方法が考えられますが, ひとつの方法は表を入れ子にするというものです。 つまり, 一番外側の表は2つのセル(A, Bとする)をもつ1行だけのものにして, セルAとセルBの中でそれぞれ1列10行の表を描きます。 わかりやすいように, 図示してみましょう。 単純に縦2列横10行の表にすると図13-2のような順番の「タブ順」になってしまいますが, 2つの表を使って入れ子にすると図13-3のようなタブ順にできます。
こうすれば, 左側のセルが上から下に描かれて行くので, タブを押したときに下方向に先に移動することになります。 HTMLのコードで書くと次のような形になります。 わかりやすくするために, (border="1"を指定して)枠線を書いて, (cellpadding="8"を指定して)枠線とセルの中身との間隔を広めにとってみました(cellpaddingで, セルの中身と枠線との間の間隔を指定できます。 この値を大きくすると, 中に書かれる文字と枠線との間が広くなります)。
<table border="1" align="center"> <tr> <td> <!-- 外側の表の左側のセル --> <table width="100%" border="1" cellpadding="8"> <!-- 内側の左の表の始まり --> <tr><td>1<input type="text"></td></tr> <tr><td>2<input type="text"></td></tr> ... <tr><td>10<input type="text"></td></tr> </table> </td> <td> <!-- 外側の表の右側のセル --> <table width="100%" border="1"> <!-- 内側の右の表の始まり --> <tr><td>11<input type="text"></td></tr> <tr><td>12<input type="text"></td></tr> ... <tr><td>20<input type="text"></td></tr> </table> </td> </tr> </table>
この部分だけをブラウザで表示すると上の図13-3のようになります。
|
| 図13-3 表を入れ子にした場合のタブ順 |
あとは, 内側の表の各セルに問題を揃えて並べればよいでしょう。 (例によって)話を簡単にするために, 2×2の4問題だけを表示することにすると, たとえば次のような感じのHTMLコードを出力すればよいことになります。
<form ...> <table width="600" border="0" align="center"> <tr> <td> <table width="100%"> <tr><td align="right">8 - 8</td><td>= <input type="text" size="8"></td></tr> <tr><td align="right">5 + 9</td><td>= <input type="text" size="8"></td></tr> </table> </td> <td> <table width="100%"> <tr><td align="right">10 - 4</td><td>= <input type="text" size="8"></td></tr> <tr><td align="right">6 + 9</td><td>= <input type="text" size="8"></td></tr> </table> </td> </tr> </table> ... </form>
さて, これで出力すべきものはだいたい決まりました。 もう一度全体を確認してみましょう(図13-4)。
|
| 図13-4 各部分の実現方法 |
まず「タイトル部分」はいつも同じ次のようなHTMLコードを出力すれば問題ありません。
<h1 align="center">けいさんれんしゅう</h1>
「問題部分」と「採点部分」はひとつの<form>...</form>の中に入り, 問題部分では表を入れ子にして, 左側と右側の問題を別の表を使って出力します。 そして, 「採点部分」では, 「さいてん」「つぎへ」のボタンや, 「せいかい」と「まちがい」の表示欄を, 1行4列の表を使って並べます。
これで, 残っているのは次の2つです。
HTMLコードの生成は前にも経験しているので, これはあとでJavaScriptのコード全体を見るときに少し説明することにして, 残りのsaitenFunc()の処理について考えてみましょう。
saitenFunc()では次のようなことをする必要があります。
1. と 2. は, 最初の問題から順番に見ていき, それぞれの欄ごとに処理すればよいでしょう。 すべての問題についての次のようなループがひとつ必要になります。
for (var i=0; i<gMondaiSu; i++) {
【問題ごとの処理】
}
【問題ごとの処理】の部分は大きく次のように分けられます。
if (【正解】) {
・○を付加する
}
else {
・×を付加する
}
3. については, 正解と間違いを数える変数を用意しておいて, 最初に0に設定して, forループの中で答を合わせてながら正解あるいは間違いの数を増やしていくことにしましょう。 すべての問題についてチェックが終わったら(つまりforループを抜けたら), 所定のテキスト欄に記入します。 (4)のフォーカスの移動は一番最後にすればよいでしょう。
まとめると, 関数saitenFunc()の定義は次のようにすればよいことになります。
function saitenFunc()
var seikai = 0;
var machigai = 0;
for (var i=0; i<gMondaiSu; i++) {
if (【正解】) {
・○を付加する
seikai++;
}
else { // 間違いのとき
・×を付加する
machigai++;
}
}
・正解欄にseikaiの値を記入
・まちがい欄にmachigaiの値を記入
・フォーカスを[つぎへ]に移動
}
JavaScriptのコードではなく普通の言葉で書いたところをJavaScriptで書くと次のようになります。
function saitenFunc() {
var seikai = 0;
var machigai = 0;
for (var i=0; i<gMondaiSu; i++) {
if (document.keisanMondai.elements[i].value != "" &&
document.keisanMondai.elements[i].value == eval(gMondaiKioku[i]) ) {
// 正しい解のとき
document.keisanMondai.elements[i].value += " ○";
seikai++;
}
else { // まちがい
document.keisanMondai.elements[i].value += " ×";
machigai++;
}
}
document.keisanMondai.seikai.value = seikai;
document.keisanMondai.machigai.value = machigai;
document.keisanMondai.again.focus();
}
見慣れないのはeval()とdocument.keisanMondai.again.focus()の2つです。
eval()は引数に指定してある文字列をJavaScriptのコードとして「評価(evaluate)」してくれるものです。 たとえば,
x = eval("3+4");
とするとxには数値の7が代入されます。 gMondaiKioku[i]には(0から数えて)i番目の問題が入っているのでこれをeval()に渡すことにより問題の答が返ってくることになります。 返ってきた問題の答の値がdocument.keisanMondai.elements[i].valueと等しいかどうかをチェックすれば正解かどうかがわかるというわけです。
問題を作るときに, 答えを, もうひとつ別の配列(たとえばgSeikaiKiokuという名前の配列)を使って記憶しておけば, eval()を使わずにすませることもできます。 採点の時に, i番目の問題の正誤を判断する際に, eval()を呼ばずに, gSeikaiKioku[i]とgMondaiKioku[i]が等しいかを見ればよいのです。
前のLessonで見たように, テキスト欄の要素は名前を使ってもアクセスできますが, このようにelements[]という配列を使ってもアクセスできるのでした。 このテキスト欄には名前は付けなかったのでこの配列を使ってアクセスするしか方法がありません。 同じような要素をいくつも使うような場合は, 名前ではなく配列を使ってアクセスした方が便利なのです。
ついでながら, eval()の前にあるf.elements[i].value != ""のチェックはなぜあるかおわかりでしょうか? 答えが0の問題(たとえば「9-9」)のとき, このチェックがないと, 答えが書かれてなかった場合でも正解になってしまうのです。 JavaScriptでは, 空文字列("")と0を「==」で比べると等しくなってしまうのです。
さて, もうひとつのdocument.keisanMondai.again.focus()はフォーカス(操作の対象)を移動する関数(メソッド)です。 フォームのすべての要素について, focus()というメソッドが用意されていて, これを呼ぶとその要素にフォーカスが移動します。 document.keisanMondai.againは[つぎへ]ボタンを表しているので, このボタンのメソッドfocus()を実行すると[つぎへ]ボタンにフォーカスが移動することになります。
これでこの関数は一応完成ですが, このプログラムにはdocument.keisanMondaiという文字列が繰り返し繰り返し登場します。 document.keisanMondaiというのはフォーム全体を表している「オブジェクト」で, じつはこれを次のように変数に代入することができます。
var f = document.keisanMondai;
こうしておけば, たとえばdocument.keisanMondai.againを, 短くf.againとして参照できます(名前fは, formのfです)。 このようにしておくと記述が簡潔になり, プログラムが読みやすくもなります。
このあたりの詳しい仕組みは付録で説明してあります。 このLessonでは, このようにしても大丈夫だということだけを覚えておいてください。
こうして次のsaitenFunc()ができあがります。
function saitenFunc() {
var seikai = 0;
var machigai = 0;
var f = document.keisanMondai; // form
for (var i=0; i<gMondaiSu; i++) {
if (f.elements[i].value != "" &&
f.elements[i].value == eval(gMondaiKioku[i]) ) { // 正しい解
f.elements[i].value += " ○"; // 正解に○を追加
seikai++;
}
else { // まちがい
f.elements[i].value += " ×"; // まちがいに×を追加
machigai++;
}
}
f.seikai.value = seikai; // 「せいかい」欄へ表示
f.machigai.value = machigai; // 「まちがい」欄へ表示
f.again.focus(); // フォーカスを「つぎへ」に移動
}
では, 全体のコードを見ましょう。 まだ, 計算問題のHTMLコードの生成については細かく見ていないので, その当たりを詳しく見ていきます。
9-10行目では, 問題生成部分のスクリプトを別のファイルから読み込んでいます。 この部分については, 前のLessonで説明しました。
9 <script type="text/javascript" src="keisansub.js">
10 </script>
13-15行目のグローバル変数の宣言です。 デバッグ時には, 次のようにしておいた方がよいでしょう。
13 var gDebugging = true; // デバッグが終わったらfalseに
14 var gMondaiSu = 2*2; // 問題数。 デバッグが終わったら10*2に
15 var gMondaiKioku = new Array(); //これまでに出した問題を記憶
keisansub.jsでもグローバル変数を使っています。 2つのファイルで同じグローバル変数を使わないように注意する必要があります。 まちがって, 同じ変数を使ってしまうと, 想定どおりに変数の値が変化せずに, プログラムが動かなくなってしまいます。 このため, 前のLessonの最後で, keisansub.jsのグローバル変数を自分のファイル名を頭に付けて長くしておいたのでした。
このように, グローバル変数を使うと, プログラム全体でその変数について注意を配る必要が生じます。 この程度の長さのプログラムならば, 全体に気を配るのも難しくないので, 話を簡単にするために用いましたが, できるならば使わない方がよいのです。
19行目からの関数keisanRenshuPage()が表示を担当する部分です。 22〜24行目で, 問題を生成し, gMondaiKiokuに記憶しています。
22 for (var i=0; i< gMondaiSu; i++) { // 問題の生成
23 gMondaiKioku[i] = mondaiSeisei(kotaeNoSaidaiChi);
24 }
問題の生成を, 問題を書き出す直前(HTMLコードの生成の途中)で行ってもかまいませんが, こうして別にしておいた方がわかりやすくなります。 後で答え合わせをするので, どこで生成するにしても問題をすべて記憶しておく必要があるので, 他の処理と別にここで生成しておきましょう。
問題の生成が終わったので, 25行目からが表示用のHTMLコードの生成です。 まずは, タイトル部分。
25 // HTMLコードの生成
26 var code = // タイトル部分のコード
27 '<h2 align="center">けいさんれんしゅう</h2>\n';
28行目から「問題部分」と「採点部分」を囲む<form>タグを書いています。
28 code += // 問題部分のコード
29 '<form name="keisanMondai" action="">\n' +
30 '<table width="600" border="0" align="center">\n' +
31 '<tr>\n<td>\n' +
321 ' <table width="100%">\n';
name属性に指定したkensanMondaiはフォームの名称で, これを使ってあとで解答欄の値を見たり「せいかい」や「まちがい」の欄へ値を書き入れたりします。
この<form>タグにaction属性が指定されていますが, これはフォームに(「送信」あるいは「提出」)ボタンを付ける場合に, その際の動作を指定するものです。 これは主にCGIという機構を利用するときに必要になります。 ここでは, submitボタンを使わないので, action=""と指定しておきます。
ひとつの計算問題に相当する部分のコードは, たとえば次のようなものになります。 この例では「9 - 1 =」という問題を表示します。
<tr><td align="right">9 - 1</td><td>= <input type="text" size="8"></td></tr>
これを34行目からのforループを使って出力しています。
34 for (var i=0; i<gMondaiSu; i++) { // 問題ごとのループ
35 code += ' <tr><td align="right">' + gMondaiKioku[i] + '</td>' +
36 '<td>= <input type="text" size="8"></td></tr>\n';
37 if (i==gMondaiSu/2-1) { //半分終わったら右の表に移るコードを追加
38 code += ' </table>\n'; // 左の列の表の終わり
39 code += '</td>\n'; // 外側の表の1つ目の要素の終わり
40 code += '<td>\n'; // 外側の表の2つ目の要素の始まり
41 code += ' <table width="100%">\n';
42 }
43 }
このforループで注意しなければいけないのは, 半分の問題を出力したときをif文で判定して, 「左側の表を閉じるコード」と「右側の表の始まりのコード」を出す必要があることです。
上のforループの部分は次のようにも書くことができます。
for (var i=0; i<gMondaiSu/2; i++) {
code += ' <tr><td align="right">' + mondai[i] + '</td>' +
'<td>= <input type="text" size="8"></td></tr>\n';
}
code += ' </table>\n'; // 左の列の表の終わり
code += '</td>\n'; // 外側の表の1つ目の要素の終わり
code += '<td>\n'; // 外側の表の2つ目の要素の始まり
code += ' <table width="100%">\n'; // 内側の2つめの表の始まり
for (var i=0; i<gMondaiSu/2; i++) {
code += ' <tr><td align="right">' + mondai[i] + '</td>' +
'<td>= <input type="text" size="8"></td></tr>\n';
}
つまり, 左側の半分を最初のforループで出し, ひとつのforループを抜けます。 続いて, 左の表から右の表に移る部分を書き, そして2つ目のforループで右側の部分を書くわけです。 これだとif文を使う必要がなくて素直な感じもしますが, 欠点もあります。
まずコードが少し長くなります。 わかりやすくなればコードが少々長くなってもかまわないのですが, 問題なのは次の点です。 それはforの内部の処理を変更したときに2カ所の修正をしなければならないということです。 たとえば, レイアウトを少し変えたいのでalign属性に別の値を指定する場合, 上のループと下のループを同時にまちがいなく直す必要があります。 最初に見たようにforループをひとつにしておけば, 1カ所のコードを直すだけですみます。 修正の際に間違いが入る可能性があることを考えると, forループをひとつにまとめておいた方がよいでしょう。
36行目の<input>タグにname属性を指定していませんが, これは上で見たように, この欄には名前ではなく配列elementsを使って, document.kensanMondai.elements[i]のようにアクセスするので, 名前はいりません。
44行目で, 入れ子にした内側と外側の2つの表を閉じています。
44 code += ' </table>\n</td>\n</tr>\n</table>\n';
46行目で, 区切りの横線を入れています。
43 //区切り線
44 code += '<hr width="600" align="center" />\n';
47〜62行目の採点部分のコードはすでに見ました。
63行目でここまで作ったHTMLコードを出力します。
63 document.write(code); // コードを出力
65行目から67行目ですべての解答欄を空文字列("")にして, 解答欄をクリアしています。
65 for (var i=0; i<gMondaiSu; i++) { // 解答をクリア
66 document.keisanMondai.elements[i].value = "";
67 }
じつは, ページを再読込したときにフォームに記入した値がクリアされるかされないかはブラウザによってまちまちのようです。 Internet Explorerでは前の値は消えてしまいます。 Netscape Navigatorのバージョン6以降は, クリアされずに前に記入した値が残ります(再読込しても, 前に入力した値を忘れないでおいてくれるのは, 確かにうれしい場合もあるかもしれません)。 同じNetscapeでも6より前のバージョンでは消されてしまうようです。 このように, 同じブラウザであっても以前のバージョンとは動作が変わってしまうこともあるので, 注意が必要です。 ともかく, 上のプログラムのようにいつもクリアすることにしておけば安心です。
69-74行目はデバッグ用のコードです。
69 if (gDebugging) { // デバッグ用
70 var debugMojiretsu = "<form>" +
71 "<textarea name='debug' rows='20' cols='80'>" + code +
72 "</textarea>\n</form>";
73 document.write(debugMojiretsu);
74 }
80行目からのsaiutenFunc()はすでに見た部分です。
最後に102行目からのHTMLの本体部分(<body>...</body>の間)を見ましょう。
102 <body> 103 <script type="text/javascript"> 104 keisanRenshuPage(); 105 document.keisanMondai.elements[0].focus(); // 最初の問題に移動 106 </script> 107 </body> 108 </html>
本体部分はJavaScriptのコードだけで, まず関数keisanRenshuPage()を呼び出してタイトルや問題, それに「採点部分」を表示し, 続いてフォーカスを最初の問題の回答欄に移動するためのメソッドを呼んでいます。
これで完成です。 最初に書いた「仕様」を満たすものになっていることを実行して確認してみてください。
このLessonではこれまでに学んだことを生かして, 採点機能付きの計算練習のページを作りました。
この講座では, プログラミングに関する主要な基本概念をJavaScriptという言語を例にして紹介してきました。 世の中には100を超えるプログラミング言語があり, それぞれの言語がさまざまな場面で使われています。 余力のある方は, この講座で学んだことをベースにして, 書籍などでいろいろな言語に挑戦してみてください。
JavaScriptについてさらに学びたい方には, 『初めてのJavaScript 第2版』(オライリー・ジャパン, こちらのサポートページで, 内容の紹介や例題をご覧いただけます)をおすすめします。
手軽に始められるウェブページとして, とほほのJavaScriptリファレンスをご紹介しておきます。
つづく付録では, オブジェクト指向とJavaScriptについて, これまでのLessonで触れることができなかった補足的な事柄を説明しています。
DHC-オンライン講座
文系の人にもわかる プログラミング入門
Lesson 13
Copyright (C) 2006- DHC Corporation & Marlin Arms Corporation.
当サイトでは、第三者配信による広告サービスを利用しています。このような広告配信事業者は、ユーザーの興味に応じた商品やサービスの広告を表示するため、当サイトや他サイトへのアクセスに関する情報(氏名、住所、メール アドレス、電話番号は含まれません) を使用することがあります。この処理の詳細やこのような情報が広告配信事業者に使用されないようにする方法については、ここをクリックしてください。