20201026のHTMLに関する記事は16件です。

mathjaxを用いてTex記法で入力した数式をリアルタイム(非同期)で表示する方法

やりたいこと

入力した内容をリアルタイムでプレビュー欄へ表示したい
Tex記法で入力した数式は数式としてリアルタイムで表示する
通常の文章は通常の文章としてリアルタイムで表示する。

やったこと

記入欄と表示欄(プレビュー)を用意
Tex記法の入力ができるようにmathjaxを導入
リアルタイム(非同期)で表示欄(プレビュー)に出力されるようjavascript実装

コピーアンドペースト

下記文章を下図の左の蘭へコピーアンドペーストしてください。
右側のプレビュー欄にTex記法で記載した部分は数式に通常の文章はそのまま文章として表示されます。

コピーアンドペースト用の文章

オイラーの公式は下記
$ e^{i\theta} = \cos\theta + i\sin\theta $
数式は数式に文章は文章としてプレビュー欄に表示されます。

上記を貼り付けた場合、下図のようになります。
TexQiita.png

実際の画面

コピーアンドペースト用の文章を左の欄へ貼り付けてみてください。
文章を左の欄へ各人で記入していただいても動作します。

See the Pen mathjax by ShimizuKosuke19910320 (@shimizukosuke19910320) on CodePen.

所感

Tex記法で記述した数式を非同期(プレビュー)で出力するための記事が少なく悪戦苦闘したため、この記事を書きました。mathjax-railsなども調べましたが、うまくいかず、、、この方法にたどり着きました。
今後、Tex記法についても記事を書きたいと思います。

参考記事

https://gilbert.ninja-web.net/math/mathjax1.html
http://www.ic.daito.ac.jp/~mizutani/html/mathexpress.html

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

MathJaxを用いてTeX記法で入力した数式をリアルタイム(非同期)で表示する方法

やりたいこと

  • 入力した内容をリアルタイムでプレビュー欄へ表示したい
  • TeX記法で入力した数式は数式としてリアルタイムで表示する
  • 通常の文章は通常の文章としてリアルタイムで表示する

やったこと

  • 記入欄と表示欄(プレビュー)を用意
  • TeX記法の入力ができるようにMathJaxを導入
  • リアルタイム(非同期)で表示欄(プレビュー)に出力されるようJavaScript実装

コピーアンドペースト

下記文章を下図の左の欄へコピーアンドペーストしてください。
右側のプレビュー欄にTeX記法で記載した部分は数式に通常の文章はそのまま文章として表示されます。

コピーアンドペースト用の文章
オイラーの公式は下記
$ e^{i\theta} = \cos\theta + i\sin\theta $
数式は数式に文章は文章としてプレビュー欄に表示されます。

上記を貼り付けた場合、下図のようになります。
コピーアンドペーストしたときのプレビュー欄

実際の画面

コピーアンドペースト用の文章を左の欄へ貼り付けてみてください。
文章を左の欄へ各人で記入していただいても動作します。

See the Pen mathjax by ShimizuKosuke19910320 (@shimizukosuke19910320) on CodePen.

所感

TeX記法で記述した数式を非同期(プレビュー)で出力するための記事が少なく悪戦苦闘したため、この記事を書きました。mathjax-railsなども調べましたが、うまくいかず、、、この方法にたどり着きました。
今後、TeX記法についても記事を書きたいと思います。

TeX記法

https://qiita.com/kosuke_shimizu/items/66b325fd16aa6fcbdc32

参考記事

MathJaxの使い方
Webの数式表現

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

formタグは1つのまま、1つの要素クリックで複数の関連するnameだけを送信(post)させる方法

どうも7noteです。綺麗に書きたいがあまりちょっと無理やり作りました。

(※データを送信するだけならhtmlとjavascriptでできますが、受け取ってその後利用するにはphpの知識が必要です!)

例えば都道府県のように地域(関東とか)と都道府県名の2種類のデータを個別にpostさせたい時に使える方法です。

普通、複数のデータを送る時はinputのname属性を配列にする方法が一般的です。


一般的に複数のデータを送信する例)

<form action="./" method="post">
  <input type="checkbox" name="pref[]" value="tokyo">
  <input type="checkbox" name="pref[]" value="osaka" >
  <input type="submit" value="送信">
</form>

※ 受け取り側はpostを受け取れるようphpで作成してください。


この方法で、東京と大阪をpostすることはできましたが、同じ方法でエリアを送信してしまうと
pref['kanto', 'tokyo']のようにエリア名と都道府県名が1つの配列内に混ざって送信されてしまいます。

今回はこれを↓のように分けて送信する方法を書きます。

area ⇒ "kanto"
pref ⇒ "tokyo"

都道府県ごとに、エリアと都道府県情報をpostさせる方法

※jQueryを使用しています。

index.html
<form action="./" method="post" name="prefform">
  <input type="hidden" name="area" value="">
  <input type="hidden" name="pref" value="" >
</form>

<ul>
  <li area="kanto" pref="tokyo">東京</li>
  <li area="kanto" pref="kanagawa">神奈川</li>
  <li area="kansai" pref="osaka">大阪</li>
</ul>
script.js
$(function(){
  $("ul li").on("click", function(e){         // li要素がクリックされたとき
    area = $(this).attr("area");              // li要素のarea属性の値を取得
    pref = $(this).attr("pref");              // li要素のpref属性の値を取得
    $("#input_area").attr("value",area);      // inputのvalue値を変数areaに置き換え
    $("#input_pref").attr("value",pref);      // inputのvalue値を変数prefに置き換え
    document.forms.prefform.submit();         // submit(送信)する。
  });
});

とび先は、form要素のactionにURLやディレクトリを記述してください。

説明するほどのことはしていないのですが、クリックされた要素の属性値、それぞれprefとareaを取得します。
サブミット(送信)する前に、inputの中のvalue値に値を入れます。
値を入れた後、サブミットすることでエリアと都道府県をそれぞれ個別で送信することができます。
もしとび先の修正などが入った場合でも、formタグのactionを1箇所修正するだけ済むので、管理が楽になるかと。

まとめ

もっとよい方法や綺麗な書き方などあるかもしれませんが、ひとまずこれで十分の機能ははたせるかなと思います。
javascriptは使わず、htmlだけでもできなくはないですが、何度も何度もformタグやinputタグを書くとなるとソースが見にくく管理もしにくくなるので、何かしらで対応できるような作りの方がいいかなと思います。

おそまつ!

~ Qiitaで毎日投稿中!! ~
【初心者向け】HTML・CSSのちょいテク詰め合わせ

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Excel で、表の修飾を CSS で書き出すアドイン

ツール

TachyPochy/tableStyle_withContextMenu

はじめに

 同様のコンセプトをもつツールは多いと思いますが、nth-child でセルを指定するのが特徴です。

 以下のような用途で使用します。reStructuredText/Sphinx だけでなく、Markdown でも使えるかもしれません。

Sphinx、表の一部を装飾する(一部だけスタイルを変えたい) - Qiita

 Mac、Windows 両対応です。

対応

 以下の装飾に対応しています。

  • 文字色(文字色が黒以外の場合に CSS を出力)
  • 背景色(背景色が白以外の場合に CSS を出力)
  • 太字
  • イタリック
  • 下線

使い方

 スタイルを適用したセルの範囲を選択し、コンテキストメニューから CSS to Clipboad を選択してください。

図3.png

サンプルの出力結果

 クリップボードには、以下のコードが出力されます。HTML カラーコードは、可能なら色名に変換します。

    <style type='text/css'>
        #theTableBlock table tr:nth-child(1) th:nth-child(1) {
            color: Red;
            background-color: Yellow;
        }
        #theTableBlock table tr:nth-child(1) td:nth-child(2) {
            background-color: Red;
            font-weight: bold;
        }
        #theTableBlock table tr:nth-child(2) td:nth-child(3) {
            background-color: Blue;
            font-style: italic;
        }
        #theTableBlock table tr:nth-child(3) td:nth-child(4) {
            background-color: #E6B8B7;
            text-decoration: underline;
        }
    </style>
    <div id='theTableBlock'>

        <!-- TODO : write your table tag code here -->

    </div>

注意

 一行目は th として扱います。HTML を書くなら <thead>〜</thead> と <tbody>〜</tbody> が必要です。

        <table>
            <thead>
                <tr>
                    <th>Test</th>
                    <th>Test</th>
                    <th>Test</th>
                    <th>Test</th>
                </tr>
            </thead>
            <tbody>
                <tr>
                    <td>Test</td>
                    <td>Test</td>
                    <td>Test</td>
                    <td>Test</td>
                </tr>
                <tr>
                    <td>Test</td>
                    <td>Test</td>
                    <td>Test</td>
                    <td>Test</td>
                </tr>
                <tr>
                    <td>Test</td>
                    <td>Test</td>
                    <td>Test</td>
                    <td>Test</td>
                </tr>
            </tbody>
        </table>

ブラウザでのレンダリング例

図4.png

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

HTML・PHP・MySQLだけで作る間違い探しゲーム【⑥ランキングページを作る】

6. ランキング表示ページ


<< 前の記事 【⑤結果表示ページを作る】
5. 結果表示ページ
  5-1. 無効なアクセスの拒否
  5-2. 回答時間の算出
  5-3. 各変数への格納
  5-4. 保存する回答時間とカウント数の上限設定
  5-5. 正解/不正解による表示メッセージの分岐
  5-6. ランキングへの登録
   5-6-1. ランキングテーブルの構成
   5-6-2. ランキングへの登録

最後に、回答時間ランキングを表示するページを実装します。

ランキングページ.png

ranking.php
<?php

// 難易度がPOSTされている場合は変数に格納
if (isset($_POST['show_method'])) {
    $select = $_POST['show_method'];
} else {
    $select = 'all';
}

// db接続
require_once('db_connect.php');

// 選択された表示形式によってSQL文を分岐
$sql = 'SELECT * FROM rankings ';
switch ($select) {
    case 'difficult':
        $sql .= "WHERE difficulty = '難しい(漢字)' ORDER BY time LIMIT 10";
        break;
    case 'easy':
        $sql .= "WHERE difficulty = '易しい(絵文字)' ORDER BY time LIMIT 10";
        break;
    case 'all':
        $sql .= 'ORDER BY time LIMIT 10';
        break;
}
$stmt = $dbh->query($sql);
$players = $stmt->fetchAll();

?>
<!DOCTYPE html>
<html lang="ja">
    <head>
        <meta charset="UTF-8">
        <link rel="stylesheet" href="style.css">
        <title>間違い探し ランキング</title>
    </head>
    <body>
        <h1>回答時間ランキング</h1>
        <form method="POST">
            <select name="show_method" size="1">
                <option value="all" <?php if ($select === 'all') echo 'selected' ?>>全て</option>
                <option value="easy" <?php if ($select === 'easy') echo 'selected' ?>>易しい</option>
                <option value="difficult" <?php if ($select === 'difficult') echo 'selected' ?>>難しい</option>
            </select>
            <input type="submit" value="表示">
        </form>
        <div class="table">
            <table class="s-tbl">
                <thead>
                    <tr>
                        <th>順位</th>
                        <th>名前</th>
                        <th>難易度</th>
                        <th>回答時間</th>
                        <th>リセット回数</th>
                    </tr>
                </thead>
                <tbody>
                    <?php foreach ($players as $key => $player): ?>
                    <tr>
                        <td><?= ++$key; ?></td>
                        <td><?= $player['name']; ?></td>
                        <td><?= $player['difficulty']; ?></td>
                        <td><?= $player['time']; ?></td>
                        <td><?= $player['reset']; ?></td>
                    </tr>
                    <?php endforeach; ?>
                </tbody>
            </table>
        </div>
        <p>
            <button type="button" onclick="location.href='start.php'">スタートページへ</button>
        </p>
    </body>
</html>

6-1. 表示切り替え機能

ranking.php
// 難易度がPOSTされている場合は変数に格納
if (isset($_POST['show_method'])) {
    $select = $_POST['show_method'];
} else {
    $select = 'all';
}

// db接続
require_once('db_connect.php');

// 選択された表示形式によってSQL文を分岐
$sql = 'SELECT * FROM rankings ';
switch ($select) {
    case 'difficult':
        $sql .= "WHERE difficulty = '難しい(漢字)' ORDER BY time LIMIT 10";
        break;
    case 'easy':
        $sql .= "WHERE difficulty = '易しい(絵文字)' ORDER BY time LIMIT 10";
        break;
    case 'all':
        $sql .= 'ORDER BY time LIMIT 10';
        break;
}
$stmt = $dbh->query($sql);
$players = $stmt->fetchAll();

後述する<select>タグによって難易度を「全て」「易しい」「難しい」の選択肢の中から選べるようになっています。

if-else文によって選択されていた場合はそのvalueの値を、選択されていない場合(=初回訪問時)にはall$selectに格納させています。

そして、DB接続設定ファイルを呼び出し、$select内の値によってSQL文が変更されるようにswitch構文を用いています。

どの選択肢においてもSELECT * FROM rankingsは共通しているので、一旦$sqlにこの文を格納してから、条件分岐に応じたSQL文を結合代入演算子.=によって追加しています。

PHPマニュアル「PHP: 文字列演算子 - Manual

その後、query()メソッドによってSQL文を実行し、返ってきたPDOStatementオブジェクトを$stmtに格納し、次いでfetchAll()メソッドによって結果を$players配列として格納しています。

PHPマニュアル「PHP: PDOStatement::fetchAll - Manual

db_connect.php」でフェッチ形式をPDO::FETCH_ASSOCと指定したので、対象テーブルのカラム名がキーとなった配列として格納されています。

6-2. 入力値の保持 - <select>タグ

ranking.php
        <form method="POST">
            <select name="show_method" size="1">
                <option value="all" <?php if ($select === 'all') echo 'selected' ?>>全て</option>
                <option value="easy" <?php if ($select === 'easy') echo 'selected' ?>>易しい</option>
                <option value="difficult" <?php if ($select === 'difficult') echo 'selected' ?>>難しい</option>
            </select>
            <input type="submit" value="表示">
        </form>

start.php」のtextタイプやradioタイプの<input>タグと同じように、<select>タグでも入力値が保持されるようにしています。

これは$selecteの値に応じてselected属性を対象の<option>タグ内にechoさせることで実現できます。

6-3. ランキングの表示

6-3-1. テーブルの表示

ranking.php
        <div class="table">
            <table class="s-tbl">
                <thead>
                    <tr>
                        <th>順位</th>
                        <th>名前</th>
                        <th>難易度</th>
                        <th>回答時間</th>
                        <th>リセット回数</th>
                    </tr>
                </thead>
                <tbody>
                    <?php foreach ($players as $key => $player): ?>
                    <tr>
                        <td><?= ++$key; ?></td>
                        <td><?= $player['name']; ?></td>
                        <td><?= $player['difficulty']; ?></td>
                        <td><?= $player['time']; ?></td>
                        <td><?= $player['reset']; ?></td>
                    </tr>
                    <?php endforeach; ?>
                </tbody>
            </table>
        </div>

find_the_mistake.php」と同様に、foreach構文によって配列となっている$playersのデータを<td>タグ内で出力しています。

foreach構文には、

  • foreach (array_expression as $value)
  • foreach (array_expression as $key => $value)

の2種類の構文があり、今回は後者を用いることで配列のインデックス番号$keyを取得し、順位を表現しています。

PHPマニュアル「PHP: foreach - Manual

順位は1から始まりますが、配列のインデックス番号は0から始まるので、プレインクリメント(前置加算子)<?= ++$key; ?>を用いることで、+1させてから出力しています1

PHPマニュアル「PHP: 加算子/減算子 - Manual

6-3-2. テーブルデザイン

style.css
.table {
  margin-top: 10px;
}
.s-tbl {
  border-collapse: collapse;
}
.s-tbl th, .s-tbl td {
  border: 1px solid #000;
  padding: 0.5em;
}
.s-tbl tr:nth-child(even) {
  background: #eee;
}
.s-tbl tr:hover {
  background: #ffffe0;
}

テーブルのCSSに関しては下記の記事から引っ張ってきたものに、少しだけ手を加えました。

ご参考までに。

いつか誰かの役に立つかもしれないweb制作屋の備忘録
css tableで背景色を交互に変える方法

これでようやく完成です!

ここまで読んでくださりありがとうございます。

何かおかしなところや改善できる点がございましたら、コメントいただけるとありがたいです!


<< 前の記事 【⑤結果表示ページを作る】
5. 結果表示ページ
  5-1. 無効なアクセスの拒否
  5-2. 回答時間の算出
  5-3. 各変数への格納
  5-4. 保存する回答時間とカウント数の上限設定
  5-5. 正解/不正解による表示メッセージの分岐
  5-6. ランキングへの登録
   5-6-1. ランキングテーブルの構成
   5-6-2. ランキングへの登録


  1. インクリメントデクリメントにはそれぞれプレ(前置)ポスト(後置)があり、それぞれ2連続の加算子/減算子が前と後に付きます($aへの加算なら++$a$a++)。これらは「変数返す前に加算/減算する」か「変数を返した後に加算/減算する」かといった違いがあり、今回のコードでポストインクリメント($key++)を用いてしまうとechoされた後に+1されてしまい、意味をなさなくなります。 

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

HTML・PHP・MySQLだけで作る間違い探しゲーム【⑤結果表示ページを作る】

5. 結果表示ページ


<< 前の記事 【④DB接続設定ファイルを作る】
4. DB接続設定
  4-1. PDOオブジェクトの属性
   4-1-1. フェッチ形式の指定
   4-1-2. エラーモードの設定
    4-1-2-1. エラーモードの違いによるエラー文の違い
   4-1-3. エミュレーションの設定
    4-1-3-1. プリペアドステートメント
    4-1-3-2. エミュレーション
  4-2. 例外発生時の処理
   4-2-1. HTTPヘッダの送信
   4-2-2. 処理の中断とエラーメッセージの表示
  4-3. DBの切断について


次の記事 >> 【⑥ランキングページを作る】
6. ランキング表示ページ
  6-1. 表示切り替え機能
  6-2. 入力値の保持 - タグ
  6-3. ランキングの表示
   6-3-1. テーブルの表示
   6-3-2. テーブルデザイン

ここではプレイヤーの回答の正解/不正解選択した難易度回答時間リセット回数を表示するページを実装します。
結果ページ.png

result.php
<?php

// セッションの開始
session_start();

// 無効なアクセスの拒否
if (empty($_SESSION) || empty($_POST)) {
    header('Location:start.php');
    exit();
}

// 回答時間を算出
$end_time = microtime(true);
$start_time = $_SESSION['start_time'];
$time = sprintf('%05.2f', $end_time - $start_time) . '秒';

// 回答を変数に格納
$answer = $_POST['answer'];

// セッション変数を変数に格納
$name = $_SESSION['name'];
$difficulty = $_SESSION['difficulty'];
$permission = $_SESSION['permission'];
$correct = $_SESSION['correct'];
$count = sprintf('%02d', $_SESSION['count']) . '回';

// セッションの放棄
$_SESSION = [];
setcookie(session_name(), '', time() - 1, '/');
session_destroy();

// 回答時間が100秒以上の場合は値を上書き
if ($time > 100) {
    $time = '100秒以上';
}

// リセット回数が100回以上の場合は値を上書き
if ($count > 100) {
    $count = '100回以上';
}

// 正解・不正解によるメッセージの分岐
if (html_entity_decode($correct) === $answer) {
    $result = '正解です!';
} else {
    $result = '不正解です。。。';
}

// 正解かつ許可されていた場合のみDBに登録
if ($result === '正解です!' && $permission === '許可する') {

    // db接続
    require_once('db_connect.php');

    // 新規登録処理
    $sql = 'INSERT INTO rankings (name, difficulty, time, reset) VALUES (?, ?, ?, ?)';
    $stmt = $dbh->prepare($sql);
    $stmt->execute([$name, $difficulty, $time, $count]);
}

?>
<!DOCTYPE html>
<html lang="ja">
    <head>
        <meta charset="UTF-8">
        <title>間違い探し</title>
    </head>
    <body>
        <h1><small>結果は...<?= $result ?></small></h1>
        <h2><small>難易度: <?= $difficulty; ?></small></h2>
        <h2><small>回答時間: <?= $time; ?></small></h2>
        <h2><small>リセット回数: <?= $count; ?></small></h2>
        <button type="button" onclick="location.href='start.php'">スタートページへ</button>
        <button type="button" onclick="location.href='ranking.php'">ランキングページへ</button></a>
    </body>
</html>

セッションの開始や放棄はこれまでのファイルと同様です。

5-1. 無効なアクセスの拒否

result.php
// 無効なアクセスの拒否
if (empty($_SESSION) || empty($_POST)) {
    header('Location:start.php');
    exit();
}

find_the_mistake.php」ではセッション変数のみ存在確認していましたが、
ここでは押されたボタンのvalue属性の値POSTされているはずなので、
その値がない場合もスタートページへリダイレクトさせています。

5-2. 回答時間の算出

result.php
// 回答時間を算出
$end_time = microtime(true);
$start_time = $_SESSION['start_time'];
$time = sprintf('%05.2f', round($end_time - $start_time, 2)) . '秒';

find_the_mistake.php」で開始時間を記録したのと同様に、
microtime()を用いて終了時間$end_timeへ格納しています。

次いで、開始時間をセッション変数から$start_timeへ格納し、
sprintf()の第2引数として「終了時間 - 開始時間」の引き算1の結果を渡しています。

sprintf()は第2引数の文字列を、
第1引数で指定したフォーマットの文字列に変換する関数で、
%05.2fは「小数第2位まで0埋めした5桁の浮動小数点数」を意味します。

これらの各文字は指定子といい、次のような書式で記述します。
[%][符号指定子][パディング指定子][アライメント指定子][表示幅指定子][精度指定子][型指定子]

今回は符合指定子とアライメント指定子以外を下記のように指定しています。

  • %・・・フォーマットとして認識させるためのパーセント文字2
  • 0・・・パディング指定子 指定した桁数に満たない部分を埋める記号(今回は0埋め)
  • 5・・・表示幅指定子 表示する最低桁数
  • .2・・・精度指定子 小数点以下の最低表示桁数
  • f・・・型指定子 引数を浮動小数点数として表示する

詳細に関しては下記もご確認下さい。

PHPマニュアル「PHP: sprintf - Manual
Let'sプログラミング ~初心者の方を対象としたプログラミングの総合学習サイト~
指定の形式にフォーマット(sprintf) - 文字列関数 - PHP関数

変換後、最後尾には文字「秒」を連結させ、変数$timeへ格納しています。

これで回答時間が算出できました。

5-3. 各変数への格納

result.php
// 回答を変数に格納
$answer = $_POST['answer'];

// セッション変数を変数に格納
$name = $_SESSION['name'];
$difficulty = $_SESSION['difficulty'];
$permission = $_SESSION['permission'];
$correct = $_SESSION['correct'];
$count = sprintf('%02d', $_SESSION['count']) . '回';

POSTされた値はプレイヤーの回答として$answerに格納しています。

またこのページからの望ましくないページ遷移を防ぐため3
スタートページと同様にセッションを放棄させます。

問題表示ページのアクセス制限は$_SESSIONが定義されているかどうかで判定しているので、結果ページへやってきた時点でセッションを放棄させることによりよって問題表示ページヘ戻ることは出来なくなります。

これにより、PHPのスクリプト終了時点でセッション変数は初期化されているので、`タグ内で出力させるためには別の変数に格納しておく必要があります。

リセット回数は上記「回答時間の算出」で用いたsprintf()を用いて、「0埋めした2桁の小数値」のフォーマットに変換してから$countに格納しています。

5-4. 保存する回答時間とカウント数の上限設定

result.php
// 回答時間が100秒以上の場合は値を上書き
if ($time > 100) {
    $time = '100秒以上';
}

// リセット回数が100回以上の場合は値を上書き
if ($count > 100) {
    $count = '100回以上';
}

ここまでのコードでは回答時間とリセット回数に上限は存在しません。

このままでも構わないのですが、

  • ランキングページのレイアウト崩れ
  • 不必要に大きな値が表示されること

を事前に防ぐために、今回は制限を追加します。

具体的には、DBに保存される回答時間・カウント数がそれぞれ

  • 100秒以上
  • 100回以上

の場合に定型文に置き換わるようにしています。

5-5. 正解/不正解による表示メッセージの分岐

result.php
// 正解・不正解によるメッセージの分岐
if (html_entity_decode($correct) === $answer) {
    $result = '正解です!';
} else {
    $result = '不正解です。。。';
}

最も初歩的な条件分岐です。

注意点として、選択された文字が絵文字だった場合
$answerに格納された値(=POSTされた値)はブラウザへの出力を経ている為、
HTMLエンティティとしてではなく絵文字そのものとなっています。

一方、$correctに格納された値(=セッション変数として保持されていた値)は、
ブラウザへの出力はないままなのでHTMLエンティティのまま格納されています。

この為、正解かどうか(=文字列が一致しているかどうか)を判定する為には、

  • セッション変数として保持されていた値をデコードする
  • POSTされた値をHTMLエンティティ化する

のどちらかを実行する必要があります。今回は前者を採用しています。

PHPマニュアル「PHP: html_entity_decode - Manual

後者の場合は下記関数が使えます。

PHPマニュアル「PHP: htmlentities - Manual

5-6. ランキングへの登録

プレイヤーの回答が正解かつランキングへの登録が許可されている場合のみ、DBへ保存します。

result.php
// 正解かつ許可されていた場合のみDBに登録
if ($result === '正解です!' && $permission === '許可する') {

    // db接続
    require_once('db_connect.php');

    // 新規登録処理
    $sql = 'INSERT INTO rankings (name, difficulty, time, reset) VALUES (?, ?, ?, ?)';
    $stmt = $dbh->prepare($sql);
    $stmt->execute([$name, $difficulty, $time, $count]);
}

5-6-1. ランキングテーブルの構成

まず先に、データを保存するランキングテーブルを構築する必要があります。詳しい方法については「MySQL テーブル作成」などで検索すればたくさんヒットすると思いますので、ここでは割愛します。

create_rankings_table.sql
CREATE TABLE `rankings` (
  `id` int(10) UNSIGNED NOT NULL AUTO_INCREMENT,
  `name` varchar(20) NOT NULL,
  `difficulty` varchar(10) NOT NULL,
  `time` varchar(10) NOT NULL,
  `reset` varchar(15) NOT NULL,
  PRIMARY KEY(id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin;

一般的なテーブル構造となっています。各カラムの概要は以下です。

  • id・・・主キー/符号なし/オートインクリメント属性が指定されたidカラム
  • name・・・入力された名前を保存する可変長文字列/20字までのカラム
  • difficulty・・・選択された難易度を保存する可変長文字列/10字までのカラム
  • time・・・回答時間を保存する可変長文字列/10字までのカラム
  • reset・・・リセット回数を保存する可変長文字列/15字までのカラム

いわゆる「寿司ビール問題」を回避する為に、文字セットutf8mb4を、照合順序utf8mb4_binをデフォルトとしています。

Qiita「寿司ビール問題① 初心者→中級者へのSTEP20/25 - Qiita」 by @kamohicokamo さん

また脚注1.でも少し触れましたが、一部の絵文字の文字数が正常にカウントされません(2倍にカウントされる)ので、nameカラムの文字数上限はバリデーションで制限した10文字の2倍の20文字としてます。

テーブル名やカラム名の命名規則に関しては下記記事が参考になります。

Qiita「データベースオブジェクトの命名規約 - Qiita」 by @genzouw さん

5-6-2. ランキングへの登録

result.php
    // db接続
    require_once('db_connect.php');

    // 新規登録処理
    $sql = 'INSERT INTO rankings (name, difficulty, time, reset) VALUES (?, ?, ?, ?)';
    $stmt = $dbh->prepare($sql);
    $stmt->execute([$name, $difficulty, $time, $count]);

require_once()でDB接続設定用ファイル「db_connect.php」を呼び出してDB接続した後、INSERT文を用いてrankingsテーブルにデータを挿入しています。

SQL文では疑問符プレースホルダーを用いたSQL文を記述し、prepare()メソッドによってDBサーバーにプリペアドステートメントとしてSQL文を渡しています(今回のDB接続設定では静的プレースホルダーを選択している為)。

PHPマニュアル「PHP: PDO - Manual
PHPマニュアル「PHP: PDO::prepare - Manual

prepare()メソッドはPDOStatementオブジェクトを返すのでそれを$stmtに格納し、次いでPDOStatementクラスのexecute()メソッドによって、

  • execute()の引数に渡された値とプレースホルダーのバインド
  • SQL文の実行

を行っています。

PHPマニュアル「PHP: PDOStatement - Manual
PHPマニュアル「PHP: PDOStatement::execute - Manual

bindValue()メソッドを使用せずにexecute()の引数に値を配列として渡す場合の注意点として、NULL以外はすべてPDO::PARAM_STR扱いになるというのがあります。

今回のテーブルはidカラム以外はすべて可変長文字列カラムですので問題ありませんが、テーブルのカラムが文字列以外の型の場合execute()の引数はにし、prepare()execute()の間でbindValue()メソッドを使用した方がいいです。

Qiita「PHPでデータベースに接続するときのまとめ - PDO::prepare → PDOStatement::execute の2ステップでクエリを実行する - Qiita」 by @mpyw さん

結果を表示させ、ランキングに登録させることができました。
最後に、ランキングページを実装します。


<< 前の記事 【④DB接続設定ファイルを作る】
4. DB接続設定
  4-1. PDOオブジェクトの属性
   4-1-1. フェッチ形式の指定
   4-1-2. エラーモードの設定
    4-1-2-1. エラーモードの違いによるエラー文の違い
   4-1-3. エミュレーションの設定
    4-1-3-1. プリペアドステートメント
    4-1-3-2. エミュレーション
  4-2. 例外発生時の処理
   4-2-1. HTTPヘッダの送信
   4-2-2. 処理の中断とエラーメッセージの表示
  4-3. DBの切断について


次の記事 >> 【⑥ランキングページを作る】
6. ランキング表示ページ
  6-1. 表示切り替え機能
  6-2. 入力値の保持 - タグ
  6-3. ランキングの表示
   6-3-1. テーブルの表示
   6-3-2. テーブルデザイン


  1. microtime()のタイムスタンプはUnixエポックからの経過マイクロ秒数となっています。このため「新しい方(ここでは$end_time)」から「古い方($start_time)」を引いた値が経過時間のマイクロ秒数となります。 

  2. これがないと各文字が指定子として認識されなくなります。また、この文字自体は指定子ではありません。 

  3. 例えば「このページからもう一度問題表示ページへ戻る」などです。個人的には「問題表示ページから結果ページへは一方通行としたい」と考えたためこのような実装となりました。 

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

HTML・PHP・MySQLだけで作る間違い探しゲーム【④DB接続設定ファイルを作る】

4. DB接続設定


<< 前の記事 【③問題表示ページを作る】
3. 問題表示ページの実装
  3-1. 無効なアクセスの拒否
  3-2. リセット回数の計測
  3-3. 問題用文字配列の設定
   3-3-1. 文字ペア配列の選択
   3-3-2. 文字ペア配列及び文字ペアのシャッフル
   3-3-3. 正解文字と不正解文字配列の設定
    3-3-3-1. 正解文字の設定
    3-3-3-2. 不正解文字配列の設定
   3-3-4. 問題用文字配列の生成
  3-4. 開始時刻の記録
  3-5. 選択肢の描写


次の記事 >> 【⑤結果表示ページを作る】
5. 結果表示ページ
  5-1. 無効なアクセスの拒否
  5-2. 回答時間の算出
  5-3. 各変数への格納
  5-4. 保存する回答時間とカウント数の上限設定
  5-5. 正解/不正解による表示メッセージの分岐
  5-6. ランキングへの登録
   5-6-1. ランキングテーブルの構成
   5-6-2. ランキングへの登録

この記事ではDB接続設定ファイルdb_connect.php」を作成します。

必要となったところでこのファイルを呼び出すことで、いちいちPDOオブジェクトの属性などを記述しなくても済みます。

db_connect.php
<?php

// 定数定義
const PDO_DSN = 'mysql:host=localhosts;dbname=[FILTERED];charset=utf8mb4';
const USERNAME = '[FILTERED]';
const PASSWORD = '[FILTERED]';

// DB接続
try {
    $dbh = new PDO(PDO_DSN, USERNAME, PASSWORD, [
        PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
        PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
        PDO::ATTR_EMULATE_PREPARES => false,
    ]);
} catch (PDOException $e) {
    header('Content-Type: text/plain; charset=UTF-8', true, 500);
    exit('DB接続に失敗しました' . '<br>' . PHP_EOL . $e->getMessage());
}

冒頭でも述べましたが、DB構築については本記事では扱いません。
接続にはPDOオブジェクトを使用しています。

PDOのインスタンスを作成する為にはPDO()を用いて

  • 第1引数・・・DSN(必須)
  • 第2引数・・・ユーザーネーム(任意)
  • 第3引数・・・パスワード(任意)
  • 第4引数・・・PDOオブジェクトの属性(任意)

を渡す必要があります。

今回のコードでは第1〜第3引数を定数としてを定義してますが、変数定義や引数に直接渡す書き方でも問題ありません。

また、DBにはプレイヤーが入力した名前を保存します。このため絵文字を取り扱う可能性があるので、文字コードはutf8ではなくマルチバイト対応のuft8mb4を指定しています。

コピペする際は、[FILTERED]を適切な値に置き換えて下さい。

PHPマニュアル「PHP: 接続、および接続の管理 - Manual
PHPマニュアル「PHP: PDO::__construct - Manual

4-1. PDOオブジェクトの属性

属性に関する公式リファレンスは以下を参照願います。
PHPマニュアル「PHP: PDO::setAttribute - Manual

また下記Qiita記事が体系的にまとめられており、とても参考になります。ぜひ一読を!

Qiita「PHPでデータベースに接続するときのまとめ - Qiita」 by @mpyw さん
Qiita「【PHP超入門】クラス~例外処理~PDOの基礎 - Qiita」 by @7968 さん

4-1-1. フェッチ形式の指定

db_connect.php
        PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC

PDO::FETCH_ASSOC: は、結果セットに 返された際のカラム名で添字を付けた配列を返します。
引用元:PHPマニュアル「PHP: PDOStatement::fetch - Manual

SQL文で得られた結果をフェッチする際の形式を指定しています。
カラム名がキーとなった配列で返ってくるので直感的に操作しやすいです。
この形式がもっともスタンダードな気がします。

4-1-2. エラーモードの設定

db_connect.php
        PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION

エラーモードは3種類あり、PDO::ERRMODE_EXCEPTIONはエラー発生時にPDOException例外を発生させます。

また、エラーコードだけでなくその関連情報も返してくれるようにもなります。

どれに設定するかは開発する物や状況によると思いますが、今回はPDO::ERRMODE_EXCEPTIONを選択しました。

4-1-2-1. エラーモードの違いによるエラー文の違い

以下は本ゲームの「result.php」の56行目において、テーブルのカラムとは異なるカラム名を指定したINSERT文を実行した際のエラー文の比較です。

  • PDO::ERRMODE_SILENT
Fatal error: Uncaught Error: Call to a member function execute() on boolean in /path/to/result.php:58 Stack trace: #0 {main} thrown in /path/to/result.php on line 58
  • PDO::ERRMODE_WARNING
Warning: PDO::prepare(): SQLSTATE[42S22]: Column not found: 1054 Unknown column 'namae' in 'field list' in /path/to/result.php on line 57

Fatal error: Uncaught Error: Call to a member function execute() on boolean in /path/to/result.php:58 Stack trace: #0 {main} thrown in /path/to/result.php on line 58
  • PDO::ERRMODE_EXCEPTION
Fatal error: Uncaught PDOException: SQLSTATE[42S22]: Column not found: 1054 Unknown column 'namae' in 'field list' in /path/to/result.php:57 Stack trace: #0 /path/to/result.php(57): PDO->prepare('INSERT INTO ran...') #1 {main} thrown in /path/to/result.php on line 57

デフォルトのPDO::ERRMODE_SILENTでは単に「SQL文を実行するexecute()でエラーが起こった」と表示されるだけであるのに対し、PDO::ERRMODE_WARNINGPDO::ERRMODE_EXCEPTIONでは「指定されたカラムが見つからない」とより具体的なエラー文となっています。

また、PDO::ERRMODE_WARNINGではE_WARNINGメッセージが追加されており、PDO::ERRMODE_EXCEPTIONでは例外がPDOExceptionクラスになっているという違いもあります。

PHPマニュアル「エラーおよびエラー処理 - Manual

4-1-3. エミュレーションの設定

db_connect.php
        PDO::ATTR_EMULATE_PREPARES => false

プリペアドステートメントエミュレーションに関する設定です。

4-1-3-1. プリペアドステートメント

直訳すると「準備された記述」になりますが、一文で言うと
後から値を入れる部分を別の文字・単語などで仮置きしたSQL文
かなと思います。

プリペアドステートメントを用いる利点は以下の2つです。

  • 後から入れる値のみを変更しながら、何度も使用できる
  • SQLインジェクション対策になる

下記記事もご参照ください。

Qiita「【PHP超入門】クラス~例外処理~PDOの基礎 - プリペアドステートメント - Qiita」 by @7968 さん

4-1-3-2. エミュレーション

truefalse(ONかOFF)かによってプリペアドステートメントの挙動が変化します。

上述のプリペアドステートメントにおいて、仮置きする文字・単語のことをプレースホルダーといい、

  • 疑問符プレースホルダー
  • 名前つきプレースホルダー

の2種類があり、今回の記事では前者に統一しています。

さらにプレースホルダー自体にも

  • 静的プレースホルダー
  • 動的プレースホルダー

の2種類のタイプがあり、エミュレーションをfalseとすることで静的プレースホルダーが用いられるようになります。

静的プレースホルダーを選択する理由としては、よりセキュアである為です。

こちらに関しては下記記事が参考になります。

Qiita「【PHP超入門】クラス~例外処理~PDOの基礎 - 静的プレースホルダと動的プレースホルダ - Qiita」 by @7968 さん

また、エミュレーションのON/OFFによる挙動の違いに関しては下記記事が参考になります。

Qiita「PHPでデータベースに接続するときのまとめ - エミュレーションに関するまとめ - Qiita」 by @mpyw さん

更に、下記質問もご参考までに。

Teratail「`PDO::ATTR_EMULATE_PREPARES => false`は必要か?

4-2. 例外発生時の処理

db_connect.php
} catch (PDOException $e) {
    header('Content-Type: text/plain; charset=UTF-8', true, 500);
    exit('DB接続に失敗しました' . PHP_EOL . $e->getMessage() . PHP_EOL);
}

catch()を用いることで、発生した例外を捕捉することができます。

PHPマニュアル「PHP: 例外(exceptions) - Manual

PDOException発生した例外のクラス名で、$e発生した例外のクラスから作成したインスタンスを代入する変数となっています。

4-2-1. HTTPヘッダの送信

エラーメッセージを表示する際、webブラウザにエラーメッセージを「単なるテキストである」と解釈してもらうため、header()を用いてMINEタイプを設定しています。

また、第3引数にはHTTPレスポンスステータスコードを指定し、
サーバー側のエラーであることを明示しています。

MDN web docs「MIME タイプ (IANA メディアタイプ) - HTTP | MDN
MDN web docs「HTTP レスポンスステータスコード - HTTP | MDN

4-2-2. 処理の中断とエラーメッセージの表示

続くexit()によって、

  • 自作のエラー文
  • 発生した例外に関するエラーメッセージ

を出力させ、後続の処理を中断させています。

このファイルを呼び出しているということは、DBのアクセスを必要とする処理を行うはずなので、接続に失敗した場合には後続の処理も失敗する可能性が高いためです。

PHPマニュアル「PHP: exit - Manual

例外に関するエラーメッセージは、$e->getMessage()によってインスタンスのgetMessage()メソッドにアクセスすることで取得しています。

PHPマニュアル「PHP: Error::getMessage - Manual

PHP_EOLPHPの定義済み定数で、プラットフォームの行末文字を意味します(EOLは"End Of Line"の略です)。

この定数は、OSを自動判定して行末文字を選定してくれますので、サーバーのOSを気にすることなく改行して表示させることができます。

PHPマニュアル「PHP: 定義済みの定数 - Manual

4-3. DBの切断について

PDOインスタンスを格納した変数にNULLを代入することで、DBから切断させることができます(今回の場合なら$dbh = NULL)。

接続を閉じるには、他から 参照されていないことを保障することでオブジェクトを破棄する 必要があります。それには、オブジェクトを保持している変数に対して NULL を代入します。
引用元:PHPマニュアル「PHP: 接続、および接続の管理 - Manual

しかし、

明示的にこれを行わなかった場合は、スクリプトの終了時に自動的に 接続が閉じられます。
引用元:PHPマニュアル「PHP: 接続、および接続の管理 - Manual

とも書かれており、あえて記述する必要はないかと思われます。

下記もご参照下さい。

Qiita「PHPでデータベースに接続するときのまとめ - データベース接続の切断 - Qiita」 by @mpyw さん

これでDBの接続設定が完了したので、次は結果ページの実装に入ります。


<< 前の記事 【③問題表示ページを作る】
3. 問題表示ページの実装
  3-1. 無効なアクセスの拒否
  3-2. リセット回数の計測
  3-3. 問題用文字配列の設定
   3-3-1. 文字ペア配列の選択
   3-3-2. 文字ペア配列及び文字ペアのシャッフル
   3-3-3. 正解文字と不正解文字配列の設定
    3-3-3-1. 正解文字の設定
    3-3-3-2. 不正解文字配列の設定
   3-3-4. 問題用文字配列の生成
  3-4. 開始時刻の記録
  3-5. 選択肢の描写


次の記事 >> 【⑤結果表示ページを作る】
5. 結果表示ページ
  5-1. 無効なアクセスの拒否
  5-2. 回答時間の算出
  5-3. 各変数への格納
  5-4. 保存する回答時間とカウント数の上限設定
  5-5. 正解/不正解による表示メッセージの分岐
  5-6. ランキングへの登録
   5-6-1. ランキングテーブルの構成
   5-6-2. ランキングへの登録

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

HTML・PHP・MySQLだけで作る間違い探しゲーム【③問題表示ページを作る】

3. 問題表示ページの実装


<< 前の記事 【②スタートページを作る】
2. スタートページの実装
  2-1. セッション
   2-1-1. セッションの開始
   2-1-2. セッションの放棄
  2-2. バリデーション
   2-2-1. 無効な送信の拒否
   2-2-2. 名前のバリデーション
    2-2-2-1. 半角スペースのみ無効
    2-2-2-2. 文字数
    2-2-2-3. 空白文字/制御文字無効
   2-2-3. エラーメッセージの表示
   2-2-4. 入力値の保持 - タグ
  2-3. 問題表示ページへの遷移


次の記事 >> 【④DB接続設定ファイルを作る】
4. DB接続設定
  4-1. PDOオブジェクトの属性
   4-1-1. フェッチ形式の指定
   4-1-2. エラーモードの設定
    4-1-2-1. エラーモードの違いによるエラー文の違い
   4-1-3. エミュレーションの設定
    4-1-3-1. プリペアドステートメント
    4-1-3-2. エミュレーション
  4-2. 例外発生時の処理
   4-2-1. HTTPヘッダの送信
   4-2-2. 処理の中断とエラーメッセージの表示
  4-3. DBの切断について

ここではゲームのコアとなる問題を生成するページを実装します。

難易度「易しい」の問題例
問題ページ_易しい.png
難易度「難しい」の問題例
問題ページ_難しい.png

find_the_mistake.php
<?php

// セッションの再開
session_start();

// 無効なアクセスの拒否
if (empty($_SESSION)) {
    header('Location:start.php');
    exit();
}

// リセット回数の計測開始
if (isset($_SESSION['count'])) {
    $_SESSION['count']++;
} else {
    $_SESSION['count'] = 0;
}

// ターゲット配列の設定
if ($_SESSION['difficulty'] === '難しい(漢字)') {
    $chars = [
        ['猫', '描'],
        ['犬', '大'],
        ['幸', '辛'],
        ['白', '臼'],
        ['矢', '失'],
        ['力', '刀'],
        ['防', '妨'],
        ['土', '士'],
        ['卵', '卯'],
        ['巨', '臣'],
        ['寒', '塞'],
        ['旅', '族'],
        ['車', '東'],
        ['釘', '針']
    ];
} else {
    $chars = [
        ['&#x1f415;', '&#x1f408;'],
        ['&#x1f405;', '&#x1f406;'],
        ['&#x1f98e;', '&#x1f40d;'],
        ['&#x1f433;', '&#x1f42c;'],
        ['&#x1f339;', '&#x1f337;'],
        ['&#x1f34a;', '&#x1f34b;'],
        ['&#x1f34e;', '&#x1f351;'],
        ['&#x1f955;', '&#x1f336;'],
        ['&#x1f96f;', '&#x1f95e;'],
        ['&#x1f358;', '&#x1f359;'],
        ['&#x1f341;', '&#x1f342;'],
        ['&#x1f332;', '&#x1f333;'],
        ['&#x1f47b;', '&#x1f47d;'],
        ['&#x1f396;', '&#x1f3c5;']
    ];
}

// ターゲット配列のシャッフル
shuffle($chars);
for ($i = 0; $i < count($chars); $i++) {
    shuffle($chars[$i]);
}

// 正解と選択対象配列を設定
$correct = $chars[0][0];
$_SESSION['correct'] = $correct;
for ($j = 0; $j <= 19; $j++) {
    for ($k = 0; $k <= 19; $k++) {
        $targets[$j][$k] = $chars[0][1];
    }
}

// 正解のターゲットを選択対象配列に1つだけ挿入
$key = range(0, 19);
$key1 = array_rand($key);
$key2 = array_rand($key);
$targets[$key1][$key2] = $correct;

// 開始時刻を記録
$_SESSION['start_time'] = microtime(true);

?>
<!DOCTYPE html>
<html lang="ja">
    <head>
        <meta charset="UTF-8">
        <title>間違い探し</title>
    </head>
    <body>
        <h1><?= $correct; ?>を見つけよう!</h1>
        <p>
            <small>難易度: <?= $_SESSION['difficulty']; ?></small>
            <button type="button" onclick="location.href='find_the_mistake.php'">分かるか!(リセット)</button>
            <button type="button" onclick="location.href='start.php'">スタートページへ</button>
        </p>
        <form action="result.php" method="POST">
            <table>
                <?php foreach ($targets as $target): ?>
                <tr>
                    <?php for ($l = 0; $l < count($target); $l++): ?>
                    <td><input type="submit" name="answer" value="<?= $target[$l]; ?>"></td>
                    <?php endfor; ?>
                </tr>
                <?php endforeach; ?>
            </table>
        </form>
    </body>
</html>

3-1. 無効なアクセスの拒否

find_the_mistake.php
// 無効なアクセスの拒否
if (empty($_SESSION)) {
    header('Location:start.php');
    exit();
}

不正な画面遷移とエラーを防ぐために、
start.php」で設定したセッション変数がない場合は、
スタートページにリダイレクトされるようにしています。

3-2. リセット回数の計測

このゲームでは、問題が難しい場合にリセットできる機能を実装しており、
リセットボタンが押されると問題生成ページが再描写されます。

この時、セッション変数にcountをキーとしたリセット回数を記録させています。

セッション変数とすることで、結果ページでもリセット回数を取り扱えるようになります。

find_the_mistake.php
// リセット回数の計測開始
if (isset($_SESSION['count'])) {
    $_SESSION['count']++;
} else {
    $_SESSION['count'] = 0;
}
  • 既に設定されていた場合はインクリメント
  • それ以外では0を設定

とすることでリセット回数が正しくカウントされるようにしています。

3-3. 問題用文字配列の設定

find_the_mistake.php
// ターゲット配列の設定
if ($_SESSION['difficulty'] === '難しい(漢字)') {
    $chars = [
        ['猫', '描'],
        ['犬', '大'],
        ['幸', '辛'],
        ['白', '臼'],
        ['矢', '失'],
        ['力', '刀'],
        ['防', '妨'],
        ['土', '士'],
        ['卵', '卯'],
        ['巨', '臣'],
        ['寒', '塞'],
        ['旅', '族'],
        ['車', '東'],
        ['釘', '針']
    ];
} else {
    $chars = [
        ['&#x1f415;', '&#x1f408;'],
        ['&#x1f405;', '&#x1f406;'],
        ['&#x1f98e;', '&#x1f40d;'],
        ['&#x1f433;', '&#x1f42c;'],
        ['&#x1f339;', '&#x1f337;'],
        ['&#x1f34a;', '&#x1f34b;'],
        ['&#x1f34e;', '&#x1f351;'],
        ['&#x1f955;', '&#x1f336;'],
        ['&#x1f96f;', '&#x1f95e;'],
        ['&#x1f358;', '&#x1f359;'],
        ['&#x1f341;', '&#x1f342;'],
        ['&#x1f332;', '&#x1f333;'],
        ['&#x1f47b;', '&#x1f47d;'],
        ['&#x1f396;', '&#x1f3c5;']
    ];
}

// ターゲット配列のシャッフル
shuffle($chars);
for ($i = 0; $i < count($chars); $i++) {
    shuffle($chars[$i]);
}

// 正解と選択対象配列を設定
$correct = $chars[0][0];
$_SESSION['correct'] = $correct;
for ($j = 0; $j <= 19; $j++) {
    for ($k = 0; $k <= 19; $k++) {
        $targets[$j][$k] = $chars[0][1];
    }
}

// 正解のターゲットを選択対象配列に1つだけ挿入
$key = range(0, 19);
$key1 = array_rand($key);
$key2 = array_rand($key);
$targets[$key1][$key2] = $correct;

3-3-1. 文字ペア配列の選択

選択された難易度によって漢字か絵文字かを分岐させます。

find_the_mistake.php
// ターゲット配列の設定
if ($_SESSION['difficulty'] === '難しい(漢字)') {
    $chars = [
        ['猫', '描'],
        ['犬', '大'],
        ['幸', '辛'],
        ['白', '臼'],
        ['矢', '失'],
        ['力', '刀'],
        ['防', '妨'],
        ['土', '士'],
        ['卵', '卯'],
        ['巨', '臣'],
        ['寒', '塞'],
        ['旅', '族'],
        ['車', '東'],
        ['釘', '針']
    ];
} else {
    $chars = [
        ['&#x1f415;', '&#x1f408;'],
        ['&#x1f405;', '&#x1f406;'],
        ['&#x1f98e;', '&#x1f40d;'],
        ['&#x1f433;', '&#x1f42c;'],
        ['&#x1f339;', '&#x1f337;'],
        ['&#x1f34a;', '&#x1f34b;'],
        ['&#x1f34e;', '&#x1f351;'],
        ['&#x1f955;', '&#x1f336;'],
        ['&#x1f96f;', '&#x1f95e;'],
        ['&#x1f358;', '&#x1f359;'],
        ['&#x1f341;', '&#x1f342;'],
        ['&#x1f332;', '&#x1f333;'],
        ['&#x1f47b;', '&#x1f47d;'],
        ['&#x1f396;', '&#x1f3c5;']
    ];
}

正解・不正解のペア関係を維持するため1に、
多次元配列として変数$charsに格納しています。

絵文字は&XXX;という形式で記述されていますが、
これはHTMLエンティティと呼ばれるもので、
これにより見えない文字や標準キーボードでは入力が難しい文字を容易に取り扱えます

また、viエディタ上でのバグを防ぐこともできます2

MDN web docs「Entity (エンティティ) - MDN Web Docs 用語集: ウェブ関連用語の定義 | MDN


今回用いた各エンティティとwebページ上で表示される絵文字の対応はこちら
&#x1f415; => 「?」と &#x1f408; => 「?」
&#x1f405; => 「?」と &#x1f406; => 「?」
&#x1f98e; => 「?」と &#x1f40d; => 「?」
&#x1f433; => 「?」と &#x1f42c; => 「?」
&#x1f339; => 「?」と &#x1f337; => 「?」
&#x1f34a; => 「?」と &#x1f34b; => 「?」
&#x1f34e; => 「?」と &#x1f351; => 「?」
&#x1f955; => 「?」と &#x1f336; => 「?」
&#x1f96f; => 「?」と &#x1f95e; => 「?」
&#x1f358; => 「?」と &#x1f359; => 「?」
&#x1f341; => 「?」と &#x1f342; => 「?」
&#x1f332; => 「?」と &#x1f333; => 「?」
&#x1f47b; => 「?」と &#x1f47d; => 「?」
&#x1f396; => 「?」と &#x1f3c5; => 「?」

絵文字のHTMLエンティティを調べるには下記サイトが参考になります。

Let's EMOJI「Unicode 13.0 絵文字 (Unicode 13.0 Emoji) | Let's EMOJI

3-3-2. 文字ペア配列及び文字ペアのシャッフル

難易度がどちらの場合でも、正解・不正解のペアは14組あるので、
正解・不正解関係は保ったままでこれらをシャッフルし、問題にランダム性を付与します。
これによりリセットする度に異なる問題が表示されるようになります。

find_the_mistake.php
// ターゲット配列のシャッフル
shuffle($chars);
for ($i = 0; $i < count($chars); $i++) {
    shuffle($chars[$i]);
}

配列のシャッフルにはshuffle()が使えます。

shuffle()は「シャッフルされた配列」ではなく、
シャッフルが成功したかどうかの真偽値」を返しますので、
変数に格納する必要はありません。

PHPマニュアル「PHP: shuffle - Manual

まず、文字ペアの多次元配列を格納した$charsをシャッフルし、
14組の文字ペアの並び(=多次元配列の第1層)」をランダムにします。

続いて、for構文を用いて再帰的にシャッフルすることで、
文字ペアの中での並び(=多次元配列の第2層)」もランダムにします3

こうすることで「ペアの中でいつもどちらかが正解/不正解」となってしまうのを防ぎ、
ゲーム性を保てます。

正解・不正解のペアが14組あり、ペアの中でも正解・不正解が入れ替わることから、
各難易度毎に14 × 2 = 28通りの問題を生成できます(ゲーム全体では56通り)。

最初に変数$charsに格納した文字ペアの多次元配列にペアを加えることで、
更にパターンを増やすこともできます。

3-3-3. 正解文字と不正解文字配列の設定

ランダムにシャッフルされた文字ペア配列から1文字だけ選択して正解の文字とします。
次いで、「正解の文字とペアになっている文字」を用いて不正解文字の配列を生成します。

find_the_mistake.php
// 正解と選択対象配列を設定
$correct = $chars[0][0];
$_SESSION['correct'] = $correct;
for ($j = 0; $j <= 19; $j++) {
    for ($k = 0; $k <= 19; $k++) {
        $targets[$j][$k] = $chars[0][1];
    }
}

3-3-3-1. 正解文字の設定

$correct正解とする文字を代入しています。
文字ペアの多次元配列を再帰的にシャッフルしたので、
$chars[0][0]には、
28(14 × 2)文字からランダムに選択された1文字
が格納されています。

結果ページを生成するファイルで正解/不正解の判定を行うので、
セッション変数にもcorrectをキーとして正解文字を格納しています。

3-3-3-2. 不正解文字配列の設定

次いで、正解文字と対になる不正解文字を、
$targets20 × 20の多次元配列として格納しています。

対になる文字は$charsの正解文字と同じ階層にある異なるキーの値のはずなので、
今回のコードでは$chars[0][1]が該当します。

この不正解文字を、変数$j,$k0 ~ 19の範囲で2重の繰り返し処理を回す中で、
$targetsに対しキーを[$j][$k]とした値とすることで、多次元配列を生成しています4

これで正解文字と不正解文字配列の設定が完了しました。

3-3-4. 問題用文字配列の生成

不正解文字配列のランダムな1つの値のみを正解文字に置き換えることで、
問題用の文字配列を作り出します。

find_the_mistake.php
// 正解のターゲットを選択対象配列に1つだけ挿入
$key = range(0, 19);
$key1 = array_rand($key);
$key2 = array_rand($key);
$targets[$key1][$key2] = $correct;

まず、range()を用いて値が0 ~ 19の範囲の整数を値としてもつ配列を生成し、
$keyに格納しています。

その後、array_rand()によって$keyの配列からランダムにキーを取り出します。
これを2回繰り返してそれぞれ$key1$key2に格納することで、
1 ~ 19の範囲内のランダムな整数を選択しました。

後はこの$key1$key2を不正解文字配列$targetsキーとして指定することで、
多次元配列の中のランダムな1つの値」を指定しています5

PHPマニュアル「PHP: range - Manual
PHPマニュアル「PHP: array_rand - Manual

これでようやく問題用の文字配列が完成しました!

3-4. 開始時刻の記録

回答時間を計測する為に問題が表示される直前に開始時刻を記録します。

find_the_mistake.php
// 開始時刻を記録
$_SESSION['start_time'] = microtime(true);

選択された文字の正解/不正解判定と同様に、結果ページで判定するので、
セッション変数として保存しています。

microtime()現在のタイムスタンプをマイクロ秒まで返す関数で、

  • 引数を指定しない場合はmsec sec形式の文字列
  • trueを指定した場合はfloat型のマイクロ秒

を、それぞれ返します。

PHPマニュアル「PHP: microtime - Manual

結果ページでは引き算で回答時間を算出するので、
計算しやすいタイムスタンプ形式となるように引数にはtrueを指定しています。

3-5. 選択肢の描写

問題用の文字配列を、繰り返し処理によって描写し、20×20のボタンを表示させます。

find_the_mistake.php
        <form action="result.php" method="POST">
            <table>
                <?php foreach ($targets as $target): ?>
                <tr>
                    <?php for ($l = 0; $l < count($target); $l++): ?>
                    <td><input type="submit" name="answer" value="<?= $target[$l]; ?>"></td>
                    <?php endfor; ?>
                </tr>
                <?php endforeach; ?>
            </table>
        </form>

<form>タグの中でsubmitタイプの<input>タグを繰り返し表示させることで、
どのボタンを押しても「result.php」へ回答がPOSTされるようになっています。

不正解用文字配列を生成させた時と同様に、
2重の繰り返し処理によって多次元配列の値を表示させます。

まず<?php foreach ($targets as $target): ?>によって
第1層の繰り返し処理します。
繰り返しの対象を<tr>タグで囲んでおり、ボタン群の行の繰り返しに相当します。

上記foreach構文の中で更に繰り返し処理
<?php for ($l = 0; $l < count($target); $l++): ?>
を実行することで、第2層を表示させます6
こちらは<td>タグで囲っており、各行内の列の繰り返しに相当します。

これらの繰り返しにより、
「20個の<td>タグ内のsubmitボタン」

「それぞれ20個の<tr>タグで囲われた」
計400個のボタン群を表示させることができます。

これで問題を表示させることができました!
次は結果ページを実装する前に、DB接続設定ファイルを作成します。


<< 前の記事 【②スタートページを作る】
2. スタートページの実装
  2-1. セッション
   2-1-1. セッションの開始
   2-1-2. セッションの放棄
  2-2. バリデーション
   2-2-1. 無効な送信の拒否
   2-2-2. 名前のバリデーション
    2-2-2-1. 半角スペースのみ無効
    2-2-2-2. 文字数
    2-2-2-3. 空白文字/制御文字無効
   2-2-3. エラーメッセージの表示
   2-2-4. 入力値の保持 - タグ
  2-3. 問題表示ページへの遷移


次の記事 >> 【④DB接続設定ファイルを作る】
4. DB接続設定
  4-1. PDOオブジェクトの属性
   4-1-1. フェッチ形式の指定
   4-1-2. エラーモードの設定
    4-1-2-1. エラーモードの違いによるエラー文の違い
   4-1-3. エミュレーションの設定
    4-1-3-1. プリペアドステートメント
    4-1-3-2. エミュレーション
  4-2. 例外発生時の処理
   4-2-1. HTTPヘッダの送信
   4-2-2. 処理の中断とエラーメッセージの表示
  4-3. DBの切断について


  1. 「間違い探し」なので単に「ランダムに文字を選択させる」だと簡単に見分けがつく文字同士が表示される可能性があり、ゲーム性が損なわれます。この為「似た文字のペア」を設定する必要があります。 

  2. viやvimで直接絵文字を打つとカーソルの位置がおかしくなったり謎の空白が出現するなど、めちゃくちゃバグります。vimにはあまり詳しくないので、回避できる策があるなら教えて欲しいです... 

  3. 多次元配列に対してshuffle()を用いた場合、最上位層のみシャッフルされます。再帰的にシャッフルするには別途for構文やforeach構文などの繰り返し処理を用いる必要があります。 

  4. 文字ペアの多次元配列のシャッフル時にfor構文で$iを使用したので、今回はアルファベット順に$j, $kとしています。$i$j, 実際は、$kのどちらかは同じでも問題ありません(for構文の最初の式で初期化される為。$j$kは同時に使用するので分ける必要あり)が、ややこしいので筆者はこのやり方を好んでいます。 

  5. array_rand()ランダムなキーを返すことに注意してください。今回の場合、$key1$key2に格納されているのは$key値ではなくキーです。$keyの配列のキーと値が全く同じである為ややこしいですが、キーにインデックス番号以外が用いられた配列を扱う際は注意が必要です。 

  6. 脚注4.と同じように、カウンター変数はアルファベット順に$lとしています。 

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

HTML・PHP・MySQLだけで作る間違い探しゲーム【②スタートページを作る】

2. スタートページの実装


<< 前の記事 【①ゲームの概要】
1. 間違い探しゲームの概要
  1-1. ゲームの全体像
  1-2. 開発環境及びファイルの全体像
   1-2-1. 開発・動作確認済み環境
   1-2-2. 作成するファイル
    1-2-2-1. メインファイル
    1-2-2-2. サブファイル
  1-3. ソースコード
   1-3-1. メインファイルのソースコード
   1-3-2. サブファイルのソースコード
  1-4. 参考にさせて頂いた記事・サイトの一覧


次の記事 >> 【③問題表示ページを作る】
3. 問題表示ページの実装
  3-1. 無効なアクセスの拒否
  3-2. リセット回数の計測
  3-3. 問題用文字配列の設定
   3-3-1. 文字ペア配列の選択
   3-3-2. 文字ペア配列及び文字ペアのシャッフル
   3-3-3. 正解文字と不正解文字配列の設定
    3-3-3-1. 正解文字の設定
    3-3-3-2. 不正解文字配列の設定
   3-3-4. 問題用文字配列の生成
  3-4. 開始時刻の記録
  3-5. 選択肢の描写

まず、最初にプレイヤーが訪れることになるページを生成する「start.php」を実装します。

スタート画面.png

start.php
<?php

// セッションの開始
session_start();

// 送信されたデータの検証
if (isset($_POST['name'], $_POST['difficulty'], $_POST['permission'])) {

    // 変数への代入
    $name = $_POST['name'];
    $difficulty = $_POST['difficulty'];
    $permission = $_POST['permission'];

    // 名前のバリデーション
    if (empty(trim($name))) {
        $error_msg = '名前に空白は無効です';
    } elseif (mb_strlen($name, 'UTF-8') > 10) {
        $error_msg = '名前は10字以内にしてください';
    } elseif ($name !== preg_replace('/\A[\p{C}\p{Z}]++|[\p{C}\p{Z}]++\z/u', '', $name)) {
        $error_msg = '名前の前後に空白文字や制御文字を含めないで下さい';
    }

    // セッション変数への格納と問題表示ページへの遷移
    if (empty($error_msg)) {
        $_SESSION['name'] = $name;
        $_SESSION['difficulty'] = $difficulty;
        $_SESSION['permission'] = $permission;
        header('Location:find_the_mistake.php');
        exit();
    }
}

// セッションの放棄
$_SESSION = [];
setcookie(session_name(), '', time() - 1, '/');
session_destroy();

?>
<!DOCTYPE html>
<html lang="ja">
    <head>
        <meta charset="UTF-8">
        <title>間違い探し</title>
    </head>
    <body>
        <h1>間違い探し</h1>
        <p>20×20個のボタンが表示されるので、指定されたボタンを見つけ出して押しましょう!</p>
        <?php if (isset($error_msg)): ?>
        <hr>
        <ul>
            <li><?= $error_msg; ?></li>
        </ul>
        <hr>
        <?php endif; ?>
        <form method="POST">
            <label>名前
                <small> (10字以内)</small><br>
                <input type="text" name="name" required value="<?php if (isset($name)) echo $name;?>">
            </label>
            <p>
                <span>難易度</span><br>
                <label>
                    <input type="radio" name="difficulty" value="易しい(絵文字)" required <?php if (empty($difficulty) || isset($difficulty) && $difficulty === '易しい(絵文字)') echo 'checked';?>>
                    易しい(絵文字)
                </label>
                <label>
                    <input type="radio" name="difficulty" value="難しい(漢字)" <?php if (isset($difficulty) && $difficulty === '難しい(漢字)') echo 'checked';?>>
                    難しい(漢字)
                </label>
            </p>
            <p>
                <span>ランキングへの登録</span><br>
                <label>
                    <input type="radio" name="permission" value="許可しない" required <?php if (empty($permission) || isset($permission) && $permission === '許可しない') echo 'checked';?>>
                    許可しない
                </label>
                <label>
                    <input type="radio" name="permission" value="許可する" <?php if (isset($permission) && $permission === '許可する') echo 'checked';?>>
                    許可する
                </label>
            </p>
            <input type="submit" value="問題に挑戦!(時間計測が開始されます)">
        </form>
        <p>
            <button type="button" onclick="location.href='ranking.php'">ランキングページへ</button>
        </p>
    </body>
</html>

2-1. セッション

ページ間を移動しても名前や難易度などの値を保持するために、セッションを利用します。
$_SESSION定義済みのスーパーグローバル関数で、ページ間で遷移しても値を保持させることができます。

これは 'スーパーグローバル' あるいは自動グローバル変数と呼ばれるものです。 スクリプト全体を通してすべてのスコープで使用することができます。
引用元:PHPマニュアル「PHP: $_SESSION - Manual

start.php
// セッションの開始
session_start();

2-1-1. セッションの開始

セッションを利用するにはsession_start()を記述します。
注意点として、こちらの関数は「ブラウザに何かを出力する前に」呼び出す必要があります。

クッキーに基づくセッションを使用している場合、ブラウザに何か出力を行う前に session_start() をコールする必要があります。
引用元:PHPマニュアル「PHP: session_start - Manual

この為、安全を見て<?php ?>タグ内の最上部に記述しています。
また、理由については下記質問が参考になります。

Teratail「なぜsession_startより前に何も出力があってはいけない?

2-1-2. セッションの放棄

<?php ?>タグ内の後半でセッションを放棄しています。

start.php
// セッション変数の初期化
$_SESSION = [];
setcookie(session_name(), '', time() - 1, '/');
session_destroy();

後述のバリデーションに引っ掛からなかった場合には
header()で別ページに遷移させ、exit()で後続処理を停止させていますが、
このページはデフォルトでセッションを放棄させるようにしています。

これによって

  • バリデーションに引っ掛かった
  • 初回のアクセスだった

場合にはセッションが残らないようになっています。

各コードの意味は以下です。

  • $_SESSION = []
    • セッション変数を全て初期化します。
  • setcookie(session_name(), '', time() - 1, '/');
    • セッション開始時にセッションIDクッキーに保存されるので、これも削除します。
  • session_destroy()
    • セッションに登録されたデータを全て破棄します。

PHPマニュアル「PHP: session_destroy - Manual

2つ目のsetcookie()関数ですが、

  • 第1引数 : クッキーの名前
  • 第2引数 : クッキーの
  • 第3引数 : クッキーの有効期限
  • 第4引数 : サーバー上でクッキーを有効としたいパス

となっています。

第1引数にsession_name()関数によって取得した現在のセッション名(デフォルトではPHPSESSID)、
第3引数にtime()関数による現在のUnixタイムスタンプから- 1した過去のタイムスタンプ(=有効期限切れ)
第4引数に/を指定することでサーバードメイン配下の全てのパス
を指定し,cookieを実質的に削除しています。

PHPマニュアル「PHP: setcookie - Manual
PHPマニュアル「PHP: session_name - Manual
PHPマニュアル「PHP: time - Manual

これらの処理はバリデーションを通過した場合には、
先述のexit()で処理が止められるので実行されません。

2-2. バリデーション

start.php
// 送信されたデータの検証
if (isset($_POST['name'], $_POST['difficulty'], $_POST['permission'])) {

    // 変数への代入
    $name = $_POST['name'];
    $difficulty = $_POST['difficulty'];
    $permission = $_POST['permission'];

    // 名前のバリデーション
    if (empty(trim($name))) {
        $error_msg = '名前に空白は無効です';
    } elseif (mb_strlen($name, 'UTF-8') > 10) {
        $error_msg = '名前は10字以内にしてください';
    } elseif ($name !== preg_replace('/\A[\p{C}\p{Z}]++|[\p{C}\p{Z}]++\z/u', '', $name)) {
        $error_msg = '名前の前後に空白文字や制御文字を含めないで下さい';
    }

    // 中略

}

2-2-1. 無効な送信の拒否

start.php
if (isset($_POST['name'], $_POST['difficulty'], $_POST['permission'])) {

    // 中略

}

まず、$_POST存在確認がされてから、値の検証されるようにしています。
これは、開発者ツールなどでフロントのフォームが改竄された場合の送信を無効とするためです。

2-2-2. 名前のバリデーション

start.php
    // 名前のバリデーション
    if (empty(trim($name))) {
        $error_msg = '名前に空白は無効です';
    } elseif (mb_strlen($name, 'UTF-8') > 10) {
        $error_msg = '名前は10字以内にしてください';
    } elseif ($name !== preg_replace('/\A[\p{C}\p{Z}]++|[\p{C}\p{Z}]++\z/u', '', $name)) {
        $error_msg = '名前の前後に空白文字や制御文字を含めないで下さい';
    }

入力された名前に対し、

  • 半角スペースのみ無効
  • 10文字以内
  • 前後に空白文字/制御文字を含めない

のバリデーションをかけています。

2-2-2-1. 半角スペースのみ無効

trim()関数はスペースを取り除く関数で、その結果が空だった場合にエラーメッセージを格納しています。

文字列の先頭および末尾にあるホワイトスペースを取り除く
引用元:PHPマニュアル「PHP: trim - Manual

ただし、こちらの関数は全角スペースには対応していないので更にバリデーションをかけます。

2-2-2-2. 文字数

mb_strlen()によって文字数をカウントし、
カウントが10を超えている場合にエラーメッセージを設定しています。

日本語にはマルチバイト文字なので、通常のstrlen()ではなく
マルチバイト対応mb_strlen()を用いています1

2-2-2-3. 空白文字/制御文字無効

名前の前後に空白文字や制御文字が含まれている場合を検知しています。
具体的には、「対象の文字列を空文字に変換する前と後の文字列が等しくない場合」を検知しています。
全角スペースのみもこちらで弾けます。

文字数判定の後にこの判定を入れているのは、
preg_replace()正規表現を使った重い処理であり、
文字数判定を後にしてしまうと何万文字と言う文字を送りつけられた場合に、
サーバーがダウンしてしまう可能性があるためです(ReDoS攻撃対策)。

これらについては下記参考記事もご参照下さい。

Qiita「【PHP】マルチバイト(全角スペース等)対応のtrim処理 - Qiita」 by @fallout さん
Qiita「正規表現の落とし穴(ReDoS - Regular Expressions DoS) - Qiita」 by @prograti さん

2-2-3. エラーメッセージの表示

start.php
        <?php if (isset($error_msg)): ?>
        <hr>
        <ul>
            <li><?= $error_msg; ?></li>
        </ul>
        <hr>
        <?php endif; ?>

エラーメッセージがある場合のみ、
<hr>タグで囲った中で<ul>タグ<li>タグを用いて
$error_msgに格納されたエラーメッセージを出力しています。

スタート画面_エラーメッセージ.png

2-2-4. 入力値の保持 - <input>タグ

start.php
        <form method="POST">
            <label>名前
                <small> (10字以内)</small><br>
                <input type="text" name="name" required value="<?php if (isset($name)) echo $name;?>">
            </label>
            <p>
                <span>難易度</span><br>
                <label>
                    <input type="radio" name="difficulty" value="易しい(絵文字)" required <?php if (empty($difficulty) || isset($difficulty) && $difficulty === '易しい(絵文字)') echo 'checked';?>>
                    易しい(絵文字)
                </label>
                <label>
                    <input type="radio" name="difficulty" value="難しい(漢字)" <?php if (isset($difficulty) && $difficulty === '難しい(漢字)') echo 'checked';?>>
                    難しい(漢字)
                </label>
            </p>
            <p>
                <span>ランキングへの登録</span><br>
                <label>
                    <input type="radio" name="permission" value="許可しない" required <?php if (empty($permission) || isset($permission) && $permission === '許可しない') echo 'checked';?>>
                    許可しない
                </label>
                <label>
                    <input type="radio" name="permission" value="許可する" <?php if (isset($permission) && $permission === '許可する') echo 'checked';?>>
                    許可する
                </label>
            </p>
            <input type="submit" value="問題に挑戦!(時間計測が開始されます)">
        </form>

データがPOSTされた場合、

  • $name
  • $difficulty
  • $permission

に入力値が格納されているので、
条件分岐で変数が定義されていた場合

  • textタイプの<input>タグではvalue属性に変数の値
  • radioタイプの<input>タグではchecked属性

をそれぞれechoすることで、
バリデーションで弾かれてページが再読み込みされた場合でも、
入力値を保持させることができます。

また、

  • $difficulty
  • $permission

が空だった場合(=初回訪問時)は、

  • 難易度「易しい(絵文字)」
  • ランキングへの登録「許可しない」

デフォルトで選択されるようになっています。

2-3. 問題表示ページへの遷移

start.php
    // セッション変数への格納と問題表示ページへの遷移
    if (empty($error_msg)) {
        $_SESSION['name'] = $name;
        $_SESSION['difficulty'] = $difficulty;
        $_SESSION['permission'] = $permission;
        header('Location:find_the_mistake.php');
        exit();
    }

先程のバリデーションをパスした場合は、
$error_msgにエラーメッセージが格納されないので、
この変数が空の場合は「バリデーションをパスした」ものと判断しています。

以降のページで名前と難易度とランキング登録への認否は使用するので、
$_SESSIONキーを設定して代入しています。

その後、header()によって次のファイルへと遷移させ、後続の処理をexit()により停止させています2

これでスタートページが完成しました!
次は問題表示ページを実装します。


<< 前の記事 【①ゲームの概要】
1. 間違い探しゲームの概要
  1-1. ゲームの全体像
  1-2. 開発環境及びファイルの全体像
   1-2-1. 開発・動作確認済み環境
   1-2-2. 作成するファイル
    1-2-2-1. メインファイル
    1-2-2-2. サブファイル
  1-3. ソースコード
   1-3-1. メインファイルのソースコード
   1-3-2. サブファイルのソースコード
  1-4. 参考にさせて頂いた記事・サイトの一覧


次の記事 >> 【③問題表示ページを作る】
3. 問題表示ページの実装
  3-1. 無効なアクセスの拒否
  3-2. リセット回数の計測
  3-3. 問題用文字配列の設定
   3-3-1. 文字ペア配列の選択
   3-3-2. 文字ペア配列及び文字ペアのシャッフル
   3-3-3. 正解文字と不正解文字配列の設定
    3-3-3-1. 正解文字の設定
    3-3-3-2. 不正解文字配列の設定
   3-3-4. 問題用文字配列の生成
  3-4. 開始時刻の記録
  3-5. 選択肢の描写


  1. 実は少々不具合が残っており、「☺️」や「❤️」の場合、mb_stlen()は正しくカウントしてくれません(2倍にカウントされてしまう)。「?」や「?」は正しくカウントしてくれるのに謎です... 

  2. 後続の処理にセッションの放棄があるので、処理を止めないとせっかくセッションに保存した情報が初期化されてしまいます。 

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

HTML・PHP・MySQLだけで作る間違い探しゲーム【①ゲームの概要】

1. 間違い探しゲームの概要

20×20のボタンの中から指定された文字が表示されたボタンを見つける、シンプルな間違い探しゲームです!解くのが簡単とは言っていない

以下の6つの記事で解説しています。


HTML・PHP・MySQLだけで作る間違い探しゲーム【①ゲームの概要】(本記事)
HTML・PHP・MySQLだけで作る間違い探しゲーム【①ゲームの概要】

1. 間違い探しゲームの概要
  1-1. ゲームの全体像
  1-2. 開発環境及びファイルの全体像
   1-2-1. 開発・動作確認済み環境
   1-2-2. 作成するファイル
    1-2-2-1. メインファイル
    1-2-2-2. サブファイル
  1-3. ソースコード
   1-3-1. メインファイルのソースコード
   1-3-2. サブファイルのソースコード
  1-4. 参考にさせて頂いた記事・サイトの一覧


HTML・PHP・MySQLだけで作る間違い探しゲーム【②スタートページを作る】
HTML・PHP・MySQLだけで作る間違い探しゲーム【②スタートページを作る】

2. スタートページの実装
  2-1. セッション
   2-1-1. セッションの開始
   2-1-2. セッションの放棄
  2-2. バリデーション
   2-2-1. 無効な送信の拒否
   2-2-2. 名前のバリデーション
    2-2-2-1. 半角スペースのみ無効
    2-2-2-2. 文字数
    2-2-2-3. 空白文字/制御文字無効
   2-2-3. エラーメッセージの表示
   2-2-4. 入力値の保持 - タグ
  2-3. 問題表示ページへの遷移


HTML・PHP・MySQLだけで作る間違い探しゲーム【③問題表示ページを作る】
HTML・PHP・MySQLだけで作る間違い探しゲーム【③問題表示ページを作る】

3. 問題表示ページの実装
  3-1. 無効なアクセスの拒否
  3-2. リセット回数の計測
  3-3. 問題用文字配列の設定
   3-3-1. 文字ペア配列の選択
   3-3-2. 文字ペア配列及び文字ペアのシャッフル
   3-3-3. 正解文字と不正解文字配列の設定
    3-3-3-1. 正解文字の設定
    3-3-3-2. 不正解文字配列の設定
   3-3-4. 問題用文字配列の生成
  3-4. 開始時刻の記録
  3-5. 選択肢の描写


HTML・PHP・MySQLだけで作る間違い探しゲーム【④DB接続設定ファイルを作る】
HTML・PHP・MySQLだけで作る間違い探しゲーム【④DB接続設定ファイルを作る】

4. DB接続設定
  4-1. PDOオブジェクトの属性
   4-1-1. フェッチ形式の指定
   4-1-2. エラーモードの設定
    4-1-2-1. エラーモードの違いによるエラー文の違い
   4-1-3. エミュレーションの設定
    4-1-3-1. プリペアドステートメント
    4-1-3-2. エミュレーション
  4-2. 例外発生時の処理
   4-2-1. HTTPヘッダの送信
   4-2-2. 処理の中断とエラーメッセージの表示
  4-3. DBの切断について


HTML・PHP・MySQLだけで作る間違い探しゲーム【⑤結果表示ページを作る】
HTML・PHP・MySQLだけで作る間違い探しゲーム【⑤結果表示ページを作る】

5. 結果表示ページ
  5-1. 無効なアクセスの拒否
  5-2. 回答時間の算出
  5-3. 各変数への格納
  5-4. 保存する回答時間とカウント数の上限設定
  5-5. 正解/不正解による表示メッセージの分岐
  5-6. ランキングへの登録
   5-6-1. ランキングテーブルの構成
   5-6-2. ランキングへの登録


HTML・PHP・MySQLだけで作る間違い探しゲーム【⑥ランキングページを作る】
HTML・PHP・MySQLだけで作る間違い探しゲーム【⑥ランキングページを作る】

6. ランキング表示ページ
  6-1. 表示切り替え機能
  6-2. 入力値の保持 - タグ
  6-3. ランキングの表示
   6-3-1. テーブルの表示
   6-3-2. テーブルデザイン

まず、どのようなものなのか概要を説明します。
(サーバー構築やDB構築に関しては本記事群では扱いません)

1-1. ゲームの全体像

初期画面にアクセスすると、

  • 名前の入力欄
  • 難易度の選択ラジオボタン
  • ランキングへの登録認否の選択ラジオボタン

が表示されます。
スタート画面.png

後で解説しますが、名前入力欄には

  • 空白文字のみ無効
  • 10文字以内
  • 名前の前後に空白文字/制御文字無効

のバリデーションを入れており、引っかかった場合はエラーが表示されます。
スタート画面_エラーメッセージ.png
適切な名前を入力し、「送信」をクリックすると選択した難易度に応じた問題が表示されます。
問題ページ_易しい.png
問題ページ_難しい.png
正解・不正解を問わず、ボタンをクリックすると結果表示ページへ遷移します。
結果には

  • 正解か不正解か
  • 回答に要した時間
  • 難易度
  • リセット回数

が表示されます。

結果ページ.png
結果表示ページの「ランキングページへ」をクリックすると回答時間ランキング表示画面へ遷移します。
ランキングページ.png
ランキングは難易度ごとに切り替えることもできます。

1-2. 開発環境及びファイルの全体像

1-2-1. 開発・動作確認済み環境

  • PHP 7.1.29 (cli)
  • MySQL Ver 14.14 Distrib 5.6.44

1-2-2. 作成するファイル

メインとなる5つと補助的な2つの合計7つです。そこまで数が多くないのとファイル名のみ(相対パス)で参照できるようにする為、全て同一ディレクトリ内に配置します。

1-2-2-1. メインファイル

  • start.php
    • プレイヤーに名前/難易度/ランキング登録認否を選択してもらうページを生成するファイルです。
  • find_the_mistake.php
    • 問題を作成・表示するファイルです。
    • 20×20の文字が書かれたボタンを出現させ、その中に1つだけ正解を紛れ込ませます。
  • db_connect.php
    • DB接続設定用のファイルです。
    • 下記result.phpとranking.phpで使用します。
  • result.php
    • 回答が正解していたかどうかを示す結果画面です。
    • 回答時間とリセット回数をカウントする機能を実装し、その結果も示します。
  • ranking.php
    • ランキングを表示するファイルです。
    • ここでは難易度別にランキングを表示切り替えできる機能も実装します。

1-2-2-2. サブファイル

  • create_ranking_table.sql
    • ランキングデータを保存するテーブルを作成するSQL文を記述したSQLファイルです。
  • style.css
    • ランキングページのテーブルの見た目を整えるCSSファイルです。

1-3. ソースコード

GitHubで見たい方はこちらからどうぞ。

1-3-1. メインファイルのソースコード

start.php
<?php

// セッションの開始
session_start();

// 送信されたデータの検証
if (isset($_POST['name'], $_POST['difficulty'], $_POST['permission'])) {

    // 変数への代入
    $name = $_POST['name'];
    $difficulty = $_POST['difficulty'];
    $permission = $_POST['permission'];

    // 名前のバリデーション
    if (empty(trim($name))) {
        $error_msg = '名前に空白は無効です';
    } elseif (mb_strlen($name, 'UTF-8') > 10) {
        $error_msg = '名前は10字以内にしてください';
    } elseif ($name !== preg_replace('/\A[\p{C}\p{Z}]++|[\p{C}\p{Z}]++\z/u', '', $name)) {
        $error_msg = '名前の前後に空白文字や制御文字を含めないで下さい';
    }

    // セッション変数への格納と問題表示ページへの遷移
    if (empty($error_msg)) {
        $_SESSION['name'] = $name;
        $_SESSION['difficulty'] = $difficulty;
        $_SESSION['permission'] = $permission;
        header('Location:find_the_mistake.php');
        exit();
    }
}

// セッションの放棄
$_SESSION = [];
setcookie(session_name(), '', time() - 1, '/');
session_destroy();

?>
<!DOCTYPE html>
<html lang="ja">
    <head>
        <meta charset="UTF-8">
        <title>間違い探し</title>
    </head>
    <body>
        <h1>間違い探し</h1>
        <p>20×20個のボタンが表示されるので、指定されたボタンを見つけ出して押しましょう!</p>
        <?php if (isset($error_msg)): ?>
        <hr>
        <ul>
            <li><?= $error_msg; ?></li>
        </ul>
        <hr>
        <?php endif; ?>
        <form method="POST">
            <label>名前
                <small> (10字以内)</small><br>
                <input type="text" name="name" required value="<?php if (isset($name)) echo $name;?>">
            </label>
            <p>
                <span>難易度</span><br>
                <label>
                    <input type="radio" name="difficulty" value="易しい(絵文字)" required <?php if (empty($difficulty) || isset($difficulty) && $difficulty === '易しい(絵文字)') echo 'checked';?>>
                    易しい(絵文字)
                </label>
                <label>
                    <input type="radio" name="difficulty" value="難しい(漢字)" <?php if (isset($difficulty) && $difficulty === '難しい(漢字)') echo 'checked';?>>
                    難しい(漢字)
                </label>
            </p>
            <p>
                <span>ランキングへの登録<small>(正解だった場合のみ)</small></span><br>
                <label>
                    <input type="radio" name="permission" value="許可しない" required <?php if (empty($permission) || isset($permission) && $permission === '許可しない') echo 'checked';?>>
                    許可しない
                </label>
                <label>
                    <input type="radio" name="permission" value="許可する" <?php if (isset($permission) && $permission === '許可する') echo 'checked';?>>
                    許可する
                </label>
            </p>
            <input type="submit" value="問題に挑戦!(時間計測が開始されます)">
        </form>
        <p>
            <button type="button" onclick="location.href='ranking.php'">ランキングページへ</button>
        </p>
    </body>
</html>
find_the_mistake.php
<?php

// セッションの再開
session_start();

// 無効なアクセスの拒否
if (empty($_SESSION)) {
    header('Location:start.php');
    exit();
}

// リセット回数の計測開始
if (isset($_SESSION['count'])) {
    $_SESSION['count']++;
} else {
    $_SESSION['count'] = 0;
}

// ターゲット配列の設定
if ($_SESSION['difficulty'] === '難しい(漢字)') {
    $chars = [
        ['猫', '描'],
        ['犬', '大'],
        ['幸', '辛'],
        ['白', '臼'],
        ['矢', '失'],
        ['力', '刀'],
        ['防', '妨'],
        ['土', '士'],
        ['卵', '卯'],
        ['巨', '臣'],
        ['寒', '塞'],
        ['旅', '族'],
        ['車', '東'],
        ['釘', '針']
    ];
} else {
    $chars = [
        ['&#x1f415;', '&#x1f408;'],
        ['&#x1f405;', '&#x1f406;'],
        ['&#x1f98e;', '&#x1f40d;'],
        ['&#x1f433;', '&#x1f42c;'],
        ['&#x1f339;', '&#x1f337;'],
        ['&#x1f34a;', '&#x1f34b;'],
        ['&#x1f34e;', '&#x1f351;'],
        ['&#x1f955;', '&#x1f336;'],
        ['&#x1f96f;', '&#x1f95e;'],
        ['&#x1f358;', '&#x1f359;'],
        ['&#x1f341;', '&#x1f342;'],
        ['&#x1f332;', '&#x1f333;'],
        ['&#x1f47b;', '&#x1f47d;'],
        ['&#x1f396;', '&#x1f3c5;']
    ];
}

// ターゲット配列のシャッフル
shuffle($chars);
for ($i = 0; $i < count($chars); $i++) {
    shuffle($chars[$i]);
}

// 正解と選択対象配列を設定
$correct = $chars[0][0];
$_SESSION['correct'] = $correct;
for ($j = 0; $j <= 19; $j++) {
    for ($k = 0; $k <= 19; $k++) {
        $targets[$j][$k] = $chars[0][1];
    }
}

// 正解のターゲットを選択対象配列に1つだけ挿入
$key = range(0, 19);
$key1 = array_rand($key);
$key2 = array_rand($key);
$targets[$key1][$key2] = $correct;

// 開始時刻を記録
$_SESSION['start_time'] = microtime(true);

?>
<!DOCTYPE html>
<html lang="ja">
    <head>
        <meta charset="UTF-8">
        <title>間違い探し</title>
    </head>
    <body>
        <h1><?= $correct; ?>を見つけよう!</h1>
        <p>
            <small>難易度: <?= $_SESSION['difficulty']; ?></small>
            <button type="button" onclick="location.href='find_the_mistake.php'">分かるか!(リセット)</button>
            <button type="button" onclick="location.href='start.php'">スタートページへ</button>
        </p>
        <form action="result.php" method="POST">
            <table>
                <?php foreach ($targets as $target): ?>
                <tr>
                    <?php for ($l = 0; $l < count($target); $l++): ?>
                    <td><input type="submit" name="answer" value="<?= $target[$l]; ?>"></td>
                    <?php endfor; ?>
                </tr>
                <?php endforeach; ?>
            </table>
        </form>
    </body>
</html>
db_connect.php
<?php

// 定数定義
const PDO_DSN = 'mysql:host=localhost;dbname=[FILTERED];charset=utf8mb4';
const USERNAME = '[FILTERED]';
const PASSWORD = '[FILTERED]';

// DB接続
try {
    $dbh = new PDO(PDO_DSN, USERNAME, PASSWORD, [
        PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
        PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
        PDO::ATTR_EMULATE_PREPARES => false,
    ]);
} catch (PDOException $e) {
    header('Content-Type: text/plain; charset=UTF-8', true, 500);
    exit('DB接続に失敗しました' . PHP_EOL . $e->getMessage() . PHP_EOL);
}
result.php
<?php

// セッションの開始
session_start();

// 無効なアクセスの拒否
if (empty($_SESSION) || empty($_POST)) {
    header('Location:start.php');
    exit();
}

// 回答時間を算出
$end_time = microtime(true);
$start_time = $_SESSION['start_time'];
$time = sprintf('%05.2f', $end_time - $start_time) . '秒';

// 回答を変数に格納
$answer = $_POST['answer'];

// セッション変数を変数に格納
$name = $_SESSION['name'];
$difficulty = $_SESSION['difficulty'];
$permission = $_SESSION['permission'];
$correct = $_SESSION['correct'];
$count = sprintf('%02d', $_SESSION['count']) . '回';

// セッションの放棄
$_SESSION = [];
setcookie(session_name(), '', time() - 1, '/');
session_destroy();

// 回答時間が100秒以上の場合は値を上書き
if ($time > 100) {
    $time = '100秒以上';
}

// リセット回数が100回以上の場合は値を上書き
if ($count > 100) {
    $count = '100回以上';
}

// 正解・不正解によるメッセージの分岐
if (html_entity_decode($correct) === $answer) {
    $result = '正解です!';
} else {
    $result = '不正解です。。。';
}

// 正解かつ許可されていた場合のみDBに登録
if ($result === '正解です!' && $permission === '許可する') {

    // db接続
    require_once('db_connect.php');

    // 新規登録処理
    $sql = 'INSERT INTO rankings (name, difficulty, time, reset) VALUES (?, ?, ?, ?)';
    $stmt = $dbh->prepare($sql);
    $stmt->execute([$name, $difficulty, $time, $count]);
}

?>
<!DOCTYPE html>
<html lang="ja">
    <head>
        <meta charset="UTF-8">
        <title>間違い探し</title>
    </head>
    <body>
        <h1><small>結果は...<?= $result ?></small></h1>
        <h2><small>難易度: <?= $difficulty; ?></small></h2>
        <h2><small>回答時間: <?= $time; ?></small></h2>
        <h2><small>リセット回数: <?= $count; ?></small></h2>
        <button type="button" onclick="location.href='start.php'">スタートページへ</button>
        <button type="button" onclick="location.href='ranking.php'">ランキングページへ</button>
    </body>
</html>
ranking.php
<?php

// 難易度がPOSTされている場合は変数に格納
if (isset($_POST['show_method'])) {
    $select = $_POST['show_method'];
} else {
    $select = 'all';
}

// db接続
require_once('db_connect.php');

// 選択された表示形式によってSQL文を分岐
$sql = 'SELECT * FROM rankings ';
switch ($select) {
    case 'difficult':
        $sql .= "WHERE difficulty = '難しい(漢字)' ORDER BY time LIMIT 10";
        break;
    case 'easy':
        $sql .= "WHERE difficulty = '易しい(絵文字)' ORDER BY time LIMIT 10";
        break;
    case 'all':
        $sql .= 'ORDER BY time LIMIT 10';
        break;
}
$stmt = $dbh->query($sql);
$players = $stmt->fetchAll();

?>
<!DOCTYPE html>
<html lang="ja">
    <head>
        <meta charset="UTF-8">
        <link rel="stylesheet" href="style.css">
        <title>間違い探し ランキング</title>
    </head>
    <body>
        <h1>回答時間ランキング</h1>
        <form method="POST">
            <select name="show_method" size="1">
                <option value="all" <?php if ($select === 'all') echo 'selected' ?>>全て</option>
                <option value="easy" <?php if ($select === 'easy') echo 'selected' ?>>易しい</option>
                <option value="difficult" <?php if ($select === 'difficult') echo 'selected' ?>>難しい</option>
            </select>
            <input type="submit" value="表示">
        </form>
        <div class="table">
            <table class="s-tbl">
                <thead>
                    <tr>
                        <th>順位</th>
                        <th>名前</th>
                        <th>難易度</th>
                        <th>回答時間</th>
                        <th>リセット回数</th>
                    </tr>
                </thead>
                <tbody>
                    <?php foreach ($players as $key => $player): ?>
                    <tr>
                        <td><?= ++$key; ?></td>
                        <td><?= $player['name']; ?></td>
                        <td><?= $player['difficulty']; ?></td>
                        <td><?= $player['time']; ?></td>
                        <td><?= $player['reset']; ?></td>
                    </tr>
                    <?php endforeach; ?>
                </tbody>
            </table>
        </div>
        <p>
            <button type="button" onclick="location.href='start.php'">スタートページへ</button>
        </p>
    </body>
</html>

1-3-2. サブファイルのソースコード

create_rankings_table.sql
CREATE TABLE `rankings` (
  `id` int(10) UNSIGNED NOT NULL AUTO_INCREMENT,
  `name` varchar(20) NOT NULL,
  `difficulty` varchar(10) NOT NULL,
  `time` varchar(10) NOT NULL,
  `reset` varchar(15) NOT NULL,
  PRIMARY KEY(id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin;
style.css
.table {
  margin-top: 10px;
}
.s-tbl {
  border-collapse: collapse;
}
.s-tbl th, .s-tbl td {
  border: 1px solid #000;
  padding: 0.5em;
}
.s-tbl tr:nth-child(even) {
  background: #eee;
}
.s-tbl tr:hover {
  background: #ffffe0;
}

1-4. 参考にさせて頂いた記事・サイトの一覧

もちろん、公式マニュアル! RTFM!
PHPマニュアル「PHP: PHP マニュアル - Manual

PDOによるDB接続に関して体系的に書かれており非常に勉強になりました
Qiita「PHPでデータベースに接続するときのまとめ - Qiita」 by @mpyw さん

クラスの基礎から例外処理までとても丁寧に解説されておりとても参考になりました
Qiita「【PHP超入門】クラス~例外処理~PDOの基礎 - Qiita」 by @7968 さん

エミュレータのON/OFFに関する疑問が解決されました
Teratail「`PDO::ATTR_EMULATE_PREPARES => false`は必要か?

フロントに関する事なら外せないドキュメント
MDN web docs「開発者向けのウェブ技術 | MDN

ずっと疑問に思っていたことが解消されました
Teratail「なぜsession_startより前に何も出力があってはいけない?

正規表現について新たな知見を得られました
Qiita「【PHP】マルチバイト(全角スペース等)対応のtrim処理 - Qiita」 by @fallout さん

ReDoSに関してとても勉強になりました
Qiita「正規表現の落とし穴(ReDoS - Regular Expressions DoS) - Qiita」 by @prograti さん

絵文字のHTMLエンティティを調べるのに活用させて頂きました
Let's EMOJI「Unicode 13.0 絵文字 (Unicode 13.0 Emoji) | Let's EMOJI

sprintfに関してとても詳細に解説されています
Let'sプログラミング ~初心者の方を対象としたプログラミングの総合学習サイト~
指定の形式にフォーマット(sprintf) - 文字列関数 - PHP関数

文字コードと照合順序に関しとても勉強になりました
Qiita「寿司ビール問題① 初心者→中級者へのSTEP20/25 - Qiita」 by @kamohicokamo さん

この記事のおかげで命名の迷いがなくなりました
Qiita「データベースオブジェクトの命名規約 - Qiita」 by @genzouw さん

テーブルのデザインの参考にさせていただきました
いつか誰かの役に立つかもしれないweb制作屋の備忘録
css tableで背景色を交互に変える方法

どの記事・ページもとても参考になりました!
この場を借りて感謝を申し上げます?


次の記事 >> 【②スタートページを作る】
2. スタートページの実装
  2-1. セッション
   2-1-1. セッションの開始
   2-1-2. セッションの放棄
  2-2. バリデーション
   2-2-1. 無効な送信の拒否
   2-2-2. 名前のバリデーション
    2-2-2-1. 半角スペースのみ無効
    2-2-2-2. 文字数
    2-2-2-3. 空白文字/制御文字無効
   2-2-3. エラーメッセージの表示
   2-2-4. 入力値の保持 - タグ
  2-3. 問題表示ページへの遷移

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

画面で入力された注文数をグラフに反映するWebアプリ(仮)

入力された注文数をグラフに反映してみた

movie

今日の注文数を入力したらすぐ反映されますね!
DBとか使ってなくてすぐ表示させてるだけだから!

URLはこちらです!→今日までの売上を可視化しよう

使い方

 一日の終わりに、今日注文された数を入力
 →こんだけ増えたよって可視化される

苦労した点 :smile:

今回は、裏のロジックというより簡単な情報をうまく表示するところを意識しました。

・グリッドの理解をしないままbootstrapを使い始めて、表が右に行ったり、めちゃくちゃ高さが低くなったり、小さくなったり。
 それもあって、ちゃんと理解をするためにこちらのサイトで勉強させていただきました。↓
 bootstrapの構造について学ぶ

・最初Chart.jsを使おうとしていましたが、new Chart.jsをするところで、
 Vue.jsから呼び出したかったが、どう記述すればよいのかわからなかったので今回はgstaticを使いました。

今回やりたかったけどできなかったこと :astonished:

・DBを使ってデータの出し入れ
・月を選ぶ。今回のは10月にしか追加されない。。。
・「数値をクリア。」どこのだよ!と思うのを解消する。 今は10月の数値が根こそぎ消える。
・目標の数値の設定や、そことの差の描画
・描画方法の選定 絶対もっときれいでおしゃれなやつあるやろ!

環境

エディタ:codepen
CSS:bootstrap
使用ライブラリ
  vue.js
  loader.js gstatic

ソース

HTML部分はこちら↓

<!DOCTYPE html>
<html lang="en" >
<head>
  <meta charset="UTF-8">
  <title>CodePen - chart</title>
  <link rel='stylesheet' href='https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/4.5.0/css/bootstrap.min.css'>

</head>
<body>
<!-- partial:index.partial.html -->
<head>
  <script src="https://www.gstatic.com/charts/loader.js"></script>
</head>
<!-- 全体をVue.js有効にする -->
<div class="container text-center text-white bg-dark" id="app">

  <!-- タイトル行 -->
  <div class="row my-3">
    <div class="col-sm-6 mx-auto"><h1>一日の終わりに注文数を入力</h1></div>
  </div>

  <!-- タスク入力行 -->
  <div class="row my-3">
    <div class="col-sm-6 mx-auto">
      <input v-model:value="task" placeholder="注文数を入力" class="form-control"><br>
      直前に追加した注文数:{{ previous_num }} <button v-on:click="addTask" class="btn btn-primary">追加</button>
    </div>
  </div>

  <!-- 全てのタスクをクリアするボタン -->
  <div class="row my-3">
    <div class="col-sm-6 mx-auto">
      <button v-on:click="clearAll" class="btn btn-danger">数値をクリア</button>
    </div>
  </div>

  <!-- タスク追加されると表示される部分 -->
  <div class="row my-3">

    <div class="col-sm-6 mx-auto">
    </div>
  </div>
      <div class="col-lg-10 mx-auto" id="stage"></div>  

</div><!-- 全体ここまで -->
<!-- partial -->
  <script src='https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/4.5.3/js/bootstrap.min.js'></script>
<script src='https://cdnjs.cloudflare.com/ajax/libs/Chart.js/2.9.4/Chart.min.js'></script>
<script src='https://cdnjs.cloudflare.com/ajax/libs/vue/2.6.11/vue.min.js'></script>
<script src='https://cdnjs.cloudflare.com/ajax/libs/vue-chartjs/3.5.1/vue-chartjs.min.js'></script><script  src="./script.js"></script>

</body>
</html>




JS部分はこちら↓

//必要なパッケージの読み込み
google.charts.load('current', {packages: ['corechart']});
// google.charts.setOnLoadCallback(drawChart);


const app = new Vue({
  el: '#app', // Vueが管理する一番外側のDOM要素
  data: {
    // Vue内部で使いたい変数は全てこの中に定義する
    task: '',
    todoList: [['', '数量'],
        ['1月', 65],
        ['2月', 59],
        ['3月', 80],
        ['4月', 81],
        ['5月', 56],
        ['6月', 55],
        ['7月', 55],
        ['8月', 55],
        ['9月', 55],
        ['10月', 0],
        ['11月', 0],
        ['12月', 0]], // これは配列
    chart:"",
    previous_num:0,
  },
  methods: {
    // 関数はここ
    addTask: function() {
      console.log('次のタスクが追加されました:', this.task);
      // 配列の先頭に現在のタスク内容を追加する(最後尾の場合はpush)
      this.todoList[10][1] = Number(this.todoList[10][1]) + Number(this.task);
      console.log('現在のToDo一覧:', this.todoList);
      this.previous_num = this.task;
      this.drawChart();
    },
    // 以下を追加、関数名はなんでもよい
    clearAll: function() {
      this.todoList = [['', '数量'],
        ['1月', 65],
        ['2月', 59],
        ['3月', 80],
        ['4月', 81],
        ['5月', 56],
        ['6月', 55],
        ['7月', 55],
        ['8月', 55],
        ['9月', 55],
        ['10月', 0],
        ['11月', 0],
        ['12月', 0]];
      console.log('全てのToDoが消去されました');
        this.drawChart();
    },
    drawChart: function() {
      console.log('drawChart');
      //オプション設定
      var options = {
        'title': '注文数',
      };

      //月別データ
      var array = this.todoList;
      var data = google.visualization.arrayToDataTable(array);

      var stage = document.getElementById('stage');

      //グラフの種類を設定
      var chart = new google.visualization.ColumnChart(stage);

      //データとオプションを設定
      chart.draw(data, options);
    }

  },
});

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Webアプリ?作成時の描画部分のリアルタイムメモ

今回やろうとするポイント

 フロントエンドの方で簡単に描画する方法を探る

メモ

 Chart.jsでやろうとする
 →Vue.jsとの共存(vue.jsからChart.jsを呼び出す方法 newでインスタンス作ってる?)の仕方がわからん。
 →他の方法を試みる。gstaticsというgoogleの描画ライブラリがあるみたい。これを使おう。

 データ:配列のデータを作る
 描画場所:htmlのIDで指定
 グラフの種類:メソッドで指定
 でいけた。
 むしろbootstrapで困ったのでメモ。

  <div class="row my-3">
    <div class="col-sm-6 mx-auto">
      <div id="stage"></div>  
    </div>
  </div>

これだと↓

first

 ⇒こんなに小さくなくてよい。

次は、

  <div class="row my-3">
    <div class="col-sm-6 mx-auto">
    </div>
      <div id="stage"></div>  
  </div>

このパターンだと
second
 ⇒右に寄った。

外に出すと、

  <div class="row my-3">

    <div class="col-sm-6 mx-auto">
    </div>
  </div>
      <div id="stage"></div> 

third

⇒うまいサイズによしなに変わってくれた。

グラフをフルじゃなく10コラム分(bootstrapの書き方)とると、

  <div class="row my-3">

    <div class="col-sm-6 mx-auto">
    </div>
  </div>
      <div class="col-lg-10 mx-auto" id="stage"></div>  

forth

参考にしたサイト

描画の部分
bootstrapの部分

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

viewportについて解説してみた

前提

アウトプットを目的として学んだことをそのまま記事に起こしています。
間違った内容がありましたらご指摘ください。

概要

ほとんどのサイトに記述されている

<meta name="viewport" content="width=device-width">

というmetaタグ。

オンライン学習教材などでもこのタグの説明がされている教材がたくさんあると思います。

しかし、上記のコードさえ書けば特に問題は起きないので詳細まで説明されているものは少ないです。

ということで、構造を理解すればカスタマイズもできるようになるので、以下に詳細の値について1つずつ解説していきます。

目次

1.viewportとは?
2.viewportに設定できる値
3.viewportの最適値
4.まとめ

viewportとは?

viewportは「表示領域」を意味します。
ブラウザの表示の仕方を制御する役割があります。

PCの場合、viewport = ブラウザのウィンドウ
スマホ/タブレットの場合、viewport = デバイスの画面全体

モバイルのviewportを指定しない場合

デスクトップのPCで表示される横幅 = モバイル端末の横幅
として合わせてしまいます。

iPhone11 Proでの画面⬇︎
IMG_D0736AA81F77-1.jpeg
こんな感じです。

つまり、PCで見てるサイトをスマホなどで閲覧した時にそのまま縮小された非常に小さいサイズで閲覧することになります。

よって、

<meta name=“viewport” content=“width=device-width>

を設定することで、端末やブラウザに合わせた値を返すことができます。

viewportに設定できる値

① width (横幅)

  • 初期値:960px
  • 単位(px):記述不要です。
  • 指定可能範囲:200px ≦ width ≦ 10000px
  • width=device-width:様々なサイズのデバイスに応じた表示領域に設定・できる。

② height (縦幅)

  • 初期値:横幅とのアスペクト比(※)から計算される値
  • 単位(px):記述不要です。
  • 指定可能範囲:200px ≦ height ≦ 10000px
  • 補足:widthと同様、device-widthを設定できますが、そもそもheightを記述することはほとんどありません。

③ initial-scale

  • 意味:初期倍率
  • 初期値:1
  • 非対応:Internet Exproler
  • 補足:未指定の場合、自動的に「initial-scale=1.0」となります。指定した場合でも、「initial-scale=1.0」とすれば、結果は変わりません。width=divice-widthが基準となるためです。

④ minimum-scale (最小倍率) (※)

  • 意味:最小縮小倍率
  • 初期値:0.25
  • 指定可能範囲:0 < minimum-scale < 10
  • 補足:「initial-scale」と同時に指定不可

⑤ maximum-scale (最大倍率) (※)

  • 最大確率倍率
  • 初期値:1.6
  • 指定可能範囲:0 < maximum-scale < 10

⑥ user-scalable

  • 意味:ユーザーがズームすることを許可するかどうかを指定できます
  • 初期値:yes
  • 指定可能な値:yes / no または 1 / 0 (1 = yes / 0 = no)
  • 補足:デバイスによって全く効果がない場合がある

⑦ target-densitydpi

  • 指定:旧版のAndroid端末向け
  • 意味:ターゲットとなる画面密度を表します。「target-densitydpi=device-dpi」とすると、端末ごとに表示される文字サイズも大きく変わってしまいます。対策として「target-densitydpi=medium-dpi」に設定するか、記述しないかでいいと思います。

⑧ その他

  • viewport-fit
  • shrink-to-fit

etc…

他にも指定可能な値があるみたいです。スマホを横向きにした時の対応などですね。しかし指定の必要はあまりないかと…。

※アスペクト比とは…

短形における長辺と短編の比率。
アスペクト比にも色々あるので、気になる方はWikipediaの記事を参照してください。

他の記事には「アスペスト比」と書かれている記事がいくつかありますが、アスペストで検索しても「アスペクト比」としか出てこなかったのでこの記事では「アスペクト」と記述しています。

※ ④ ⑤ について

自分は正直なところ記述したことがありません。initial-scale=1.0も数値を変えたことがありません。ご自身で様々試してみてください。

viewportの最適値

<meta name=“viewport” content=“width=device-width, initial-scale=1”>

特別な理由がなければ、基本この記述だけで問題ないです。
設定などを理解していればこのコードをコピペで使いまわしていいと思います。

まとめ

  • viewportは、表示領域のこと
  • <meta name=“viewport” content=“device-width”>さえ記述すれば基本的に問題ない
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【HTML】viewportについて解説してみた

前提

アウトプットを目的として学んだことをそのまま記事に起こしています。
間違った内容がありましたらご指摘ください。

概要

ほとんどのサイトに記述されている

<meta name="viewport" content="width=device-width">

というmetaタグ。

オンライン学習教材などでもこのタグの説明がされている教材がたくさんあると思います。それくらい基本的なことではあります。

しかし、上記のコードさえ書けば特に問題は起きないので詳細まで説明されているものは少ないです。

ということで、構造を理解すればカスタマイズもできるようになるので、以下に詳細の値について1つずつ解説していきます。

目次

  1. viewportとは?
  2. viewportに設定できる値
  3. viewportの最適値
  4. まとめ

viewportとは?

viewportは「表示領域」を意味します。
ブラウザの表示の仕方を制御する役割があります。

PCの場合、viewport = ブラウザのウィンドウ
スマホ/タブレットの場合、viewport = デバイスの画面全体

モバイルのviewportを指定しない場合

デスクトップのPCで表示される横幅 = モバイル端末の横幅
として合わせてしまいます。

iPhone11 Proでの画面⬇︎
PCで表示したのをそのまま縮小したような表示
こんな感じです。

つまり、PCで見てるサイトをスマホなどで閲覧した時にそのまま縮小された非常に小さいサイズで閲覧することになります。

よって、

<meta name=“viewport” content=“width=device-width>

を設定することで、端末やブラウザに合わせた値を返すことができます。

viewportに設定できる値

① width (横幅)

  • 初期値:960px
  • 単位(px):記述不要です。
  • 指定可能範囲:200px ≦ width ≦ 10000px
  • width=device-width:様々なサイズのデバイスに応じた表示領域に設定・できる。

② height (縦幅)

  • 初期値:横幅とのアスペクト比(※)から計算される値
  • 単位(px):記述不要です。
  • 指定可能範囲:200px ≦ height ≦ 10000px
  • 補足:widthと同様、device-widthを設定できますが、そもそもheightを記述することはほとんどありません。

③ initial-scale

  • 意味:初期倍率
  • 初期値:1
  • 非対応:Internet Exproler
  • 補足:未指定の場合、自動的に「initial-scale=1.0」となります。指定した場合でも、「initial-scale=1.0」とすれば、結果は変わりません。width=divice-widthが基準となるためです。

④ minimum-scale (最小倍率) (※)

  • 意味:最小縮小倍率
  • 初期値:0.25
  • 指定可能範囲:0 < minimum-scale < 10
  • 補足:「initial-scale」と同時に指定不可

⑤ maximum-scale (最大倍率) (※)

  • 最大確率倍率
  • 初期値:1.6
  • 指定可能範囲:0 < maximum-scale < 10

⑥ user-scalable

  • 意味:ユーザーがズームすることを許可するかどうかを指定できます
  • 初期値:yes
  • 指定可能な値:yes / no または 1 / 0 (1 = yes / 0 = no)
  • 補足:デバイスによって全く効果がない場合がある

⑦ target-densitydpi

  • 指定:旧版のAndroid端末向け
  • 意味:ターゲットとなる画面密度を表します。「target-densitydpi=device-dpi」とすると、端末ごとに表示される文字サイズも大きく変わってしまいます。対策として「target-densitydpi=medium-dpi」に設定するか、記述しないかでいいと思います。

⑧ その他

  • viewport-fit
  • shrink-to-fit

etc…

他にも指定可能な値があるみたいです。スマホを横向きにした時の対応などですね。しかし指定の必要はあまりないかと…。

※アスペクト比とは…

短形における長辺と短編の比率。

アスペクト比にも色々あるので、気になる方はWikipediaの記事を参照してください。

他の記事には「アスペスト比」と書かれている記事がいくつかありますが、アスペストで検索しても「アスペクト比」としか出てこなかったのでこの記事では「アスペクト」と記述しています。

※④ ⑤ について

自分は正直なところ記述したことがありません。initial-scale=1.0も数値を変えたことがありません。ご自身で様々試してみてください。

viewportの最適値

<meta name="viewport" content="width=device-width, initial-scale=1.0">

特別な理由がなければ、基本この記述だけで問題ないです。
設定などを理解していればこのコードをコピペで使いまわしていいと思います。

まとめ

  • viewportは、表示領域のこと
  • <meta name="viewport" content="device-width">さえ記述すれば基本的に問題ない
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

2020-10-25 学習記録

やったこと

基本情報技術者試験教科書を読む
読書(ストレッチ,発達障害サバイバルブック)

うまくいかなかったこと

コーディングの学習をしなかった
あっという間に時間がすぎてしまう。

改善すること

スマホを見てダラダラする時間をへらす

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

コピぺで使える簡易的なスロットマシーン実装

コピぺで使える簡易的なスロットマシーン実装

51dfdc85977ada043ac57c858b1ee611.png

<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>スロットマシーン 1</title>
<style>
input {
    width:40px;
    font-size:30px;
    text-align:center;
}
button {
    width:50px;
}
.x1 {
    padding:10px 0;
}
.x1 div {
    float:left;
    width:60px;
    text-align:center;
}
</style>
<script>
window.onload = function() {
    var a = document.getElementById('a');
    var b1 = document.getElementById('b1');
    var b2 = document.getElementById('b2');
    var b3 = document.getElementById('b3');
    var c1 = document.getElementById('c1');
    var c2 = document.getElementById('c2');
    var c3 = document.getElementById('c3');
    var t1 = null;
    var t2 = null;
    var t3 = null;

    function start() {
        if (t1) {
            clearInterval(t1);
        }
        if (t2) {
            clearInterval(t2);
        }
        if (t3) {
            clearInterval(t3);
        }
        c1.value = Math.floor(Math.random() * 10);
        c2.value = Math.floor(Math.random() * 10);
        c3.value = Math.floor(Math.random() * 10);
        t1 = setInterval(function () {
            c1.value = (+c1.value + 1) % 10;
        }, 200);
        t2 = setInterval(function () {
            c2.value = (+c2.value + 1) % 10;
        }, 200);
        t3 = setInterval(function () {
            c3.value = (+c3.value + 1) % 10;
        }, 200);
    }

    function stop1() {
        if (t1) {
            clearInterval(t1);
            t1 = null;
        }
        check();
    }

    function stop2() {
        if (t2) {
            clearInterval(t2);
            t2 = null;
        }
        check();
    }

    function stop3() {
        if (t3) {
            clearInterval(t3);
            t3 = null;
        }
        check();
    }

    function check() {
        if (!t1 && !t2 && !t3) {
            if (c1.value == c2.value && c1.value == c3.value) {
                alert('あたり!');
            } else {
                alert('はずれ!');
            }
        }
    }

    a.addEventListener('click', start);
    b1.addEventListener('click', stop1);
    b2.addEventListener('click', stop2);
    b3.addEventListener('click', stop3);

    start();
}
</script>
</head>
<body>
    <button type="button" id="a">start</button>
    <div class="x1">
        <div>
            <input type="text" id="c1">
            <button type="button" id="b1">stop</button>
        </div>
        <div>
            <input type="text" id="c2">
            <button type="button" id="b2">stop</button>
        </div>
        <div>
            <input type="text" id="c3">
            <button type="button" id="b3">stop</button>
        </div>
    </div>
</body>
</html>

現場からは以上です!

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む