決済つきの予約システムが3,940円〜/月

Raspberry Pi 覚え書き

小型PC、ラズベリーパイ3を使って遊んだプロセスを記録するページ

Contents

1.Raspberry Pi 3でNAS-ファイルサーバーを作る
2.WindowsPCとファイル共有する
3.Raspberry Pi 3にポータブルSSDをつなぐ
4.Raspberry Pi 3に3.5インチタッチパネルをつなぐ
5.Raspberry Pi 3でAmazon Echoを作る
6.MP3を再生する
7.Python-Pygameを使ってみる
8.WindowsにPython-Pygame開発環境を構築する
9.Python-Pygameで漢字を扱う
10.Tkinterを使ってみる
11.pyaudioを使ってみる
12.TkinterでJPEG画像を表示する
13.Tkinter.Canvasを使ってみる
14.Tkinter.Canvasを使って電卓を作る
15.Orange Pi ZeroをWiFiに接続する
16.Raspberry Pi Zero WHを使う
17.Raspberry Pi 3にローカルWebサーバーを構築する
18.リネームアプリを作る
19.PHPでホームページ管理画面を作る
20.ページング処理を加える
21.MySQLでお知らせ画面を作る20240413修正
22.PDF.jsを使ってみる
23.summernoteを使ってみる
24.Ajaxを使ってみる
25.Bootstrapを使ってみる
26.Raspberry Pi OS Busterを使ってみる
27.シェルスクリプトを使ってみる
28.OLED display HATを使う
29.PyQtを使う
30.監視カメラを作る

1.Raspberry Pi 3でNAS-ファイルサーバーを作る

1.1.Raspbain Stretch with Dasktopをインストールする

Windows10-PCを使ってラズベリーパイを設定する手順を解説する。
用意するもの
・SD Formatter 5.0
   https://www.sdcard.org/jp/index.html
SDメモリーカードフォーマッター5.0アソシエーションのページ、ダウンロードのタブの次、SDメモリーカードフォーマッターWindows用のボタンを押す。「同意します」ボタンを押すとインストーラのダウンロード開始。
・Win32 Disk Imager 1.0
   https://ja.osdn.net/projects/sfnet_win32diskimager/
ダウンロードファイル一覧のボタンの先、win32diskimager-1.0.0-install.exeを選んでインストーラをダウンロードする。窓の杜からも入手できる。窓の杜で"Win32 Disk Imager"を検索すれば見つけられるだろう。

以上の2つのフリーソフトをあらかじめWindowPCにインストールしておく。

ラズベリーパイ公式サイト>DOWNLOADS>RASPBIANのページ*から
RASPBIAN STRETCH WITH DESKTOP-"2017-11-29-raspbian-stretch.zip"をダウンロード。
ZIP形式になっているので解凍してイメージファイル"2017-11-29-raspbian-stretch.img"を取り出す。
*https://www.raspberrypi.org/downloads/raspbian/
※このページ執筆時点の最新RASPBIANが"2017-11-29-raspbian-stretch.zip"であった。
※torrent形式かzip形式どちらかを選ぶことができる。torrentユーザーならtorrent経由でダウンロードした方が速い。torrentを持っていない/何のことかわからない人はzip形式をダウンロードすべし。

■MicroSDカードをフォーマットする
PCにMicroSDカードをセットし、SD Formatter 5.0を開く。
"カードの選択"でドライブを間違えないように。"クイックフォーマット"も可能だが、トラブルを未然に防ぎたいのなら"上書きフォーマット"を選択する。"ボリュームラベル"は空欄でもよい。
※MicroSDカードは16GB以上を推奨する。
※MicroSDカードとラズベリーパイには相性がある。詳しくは下記のサイト参照。
*http://elinux.org/RPi_SD_cards

■MicroSDカードにイメージファイルを展開する
Win32 Disk Imager 1.0を開く。
"Image File"は"2017-11-29-raspbian-stretch.img"を選択、"Device"でMicroSDカードが挿入されているドライブを選択したのち、"Write"を押す。

イメージファイルを展開したMicroSDカードをラズベリーパイに挿し、電源を入れる。問題なければ、RASPBIAN STRETCH WITH DESKTOPのデスクトップ画面が表示されるはずである。

■RASPBIAN STRETCH WITH DESKTOPの初期設定をする
メニューバー>Preferences>Raspberry Pi Cofigurationを開く。
"Interfaces"のタブの下、SSHがDisabeleになっているのでEnableをオンにする。以後、リブートのたびにパスワード変更を促す警告メッセージが出る。パスワードを変更するかしないかはご自身の判断で。
→SSHで接続しないのなら、当然Enableにする必要はない。後述するTera TermでラズベリーパイにアクセスするためにはSSHをオンにする必要がある。しかし、SSHをオンにしなくてもWindows標準のリモートデスクトップ接続を使ってアクセスすることができる。
"Lacarization"のタブの下、"Local"の"Language"を"ja(Japanese)"に、"Country"を"JA(Japan)"に、
"Timezone"を"Japan"に、"Keyboard"を"Japanese(PC-98xx Series)"に、
"WiFi Country"を"JP Japan"に設定。
※Timezoneを変更してもすぐに時計はアジャストされない。
※"Local"をデフォルトのままにしておくとメニューが日本語化されない。英語のメニューがお好きならここはそのまま。最低限"Keyboard"だけは"Japanese(PC-98xx Series)"に変えておいたほうがいいと思う。

■WiFiの設定をする
画面右上、メニューバーの中、Bluetoothの右の記号をクリック。
自宅のWiFiのSSIDを選んで"Pre Shared key"にパスワードを入力。
すぐにSSIDが表示されないときは、いったん"Turn off Wi-Fi"を選んだのち、再び"Turn On Wi-Fi"を選ぶと表示が正常化される。

■とりあえずアップデートする
以下の日本語IMEをはじめ、いくつかのアプリをインストールする時は事前にOSをアップデートしておいた方がいい。インストールに失敗しないためのおまじないだ。
左から4つ目のアイコン"LXTerminal"ターミナルを開いて以下を入力。
$ sudo apt update
※apt-getコマンドじゃなくてaptだけでもいいみたい。

■心配ならアップグレードもしてみる
$ sudo apt upgrade

最新のRASPBIAN STRETCH WITH DESKTOPは日本語フォントを内蔵しているので、別途日本語フォントをインストールする必要はない。しかし、日本語入力メソッドをインストールしないと日本語の入力ができない。

■日本語入力メソッドをインストールする
uim uim-mozcは2018-06-27 Raspbianとの相性がよくないようだ。2枚目のターミナルが開かない、Sambaが起動しないなどのトラブルを確認した。
mozcをインストールする。ターミナルを開いて以下を入力。
$ sudo apt install uim uim-mozc
途中、続行しますか?[Y/n]が出たらyを押してEnter。

※"Language"が"en(English)"のままだと日本語入力できない。
再起動後にmozcが有効になる。
ちなみに再起動は
$ reboot
あるいは

$ sudo reboot
※SSHによるリモート接続からリブートを実行するときはsudoから書き始めないといけないようだ。

■ディスプレイ解像度を設定する
私のモニタはHDMI-1920 x 1080(スピーカ付き)だが、デフォルトでは画面いっぱいに表示されず黒い非表示領域が残る。そこでディスプレイ解像度を設定して表示領域をモニタにフィットさせる。
ターミナルを開いて以下を入力。
$ sudo nano /boot/config.txt
長い文字列が表示されるが、ずーっと下まで送って、最後の行に以下のコマンドを追記する。
hdmi_drive=2
hdmi_group=2 # DMT
hdmi_mode=82 # 1080p 60Hz
その後、Ctrl+O(上書き)を押す。Config.txtを上書きするか尋ねてくるのでEnterを押す。
Ctrl+Xを押して完了。
→hdmi_drive=2でスピーカ付きHDMIモニタから音が出るようになる。
→#に続く文字はコメントなので省略可。
→hdmi_mode=82は1920x1080/60Hzディスプレイに対応した設定である。それ以外の解像度の設定については以下のページを参照。
https://www.raspberrypi.org/documentation/configuration/config-txt/video.md
※メニューバー>Preferences>Raspberry Pi Cofiguration>Systemタブの下に"Set Resolution"ボタンがあるが、この中に"DMT mode 82 1920x1080 60Hz 16:9"が見当たらないので、/boot/config.txtを直接編集する。

以上で汎用PCとしての体裁が整った。

1.2.HDDをマウントしてファイルサーバーにする

■ntfs-3gをインストール
NTFS形式のHDDをマウントするためにntfs-3gをインストールする。
ターミナルを開いて以下を入力。
$ sudo apt install ntfs-3g
そしてリブート

■HDDを接続する
今回はSeagate(3TB)HDDを用意した。これをUSBでラズベリーパイに接続する。
ターミナルを開いてdf(disk filesystem)コマンドを入力。
$ df
以下のような文字列が表示される。
ファイルシス       1K-ブロック         使用          使用可 使用% マウント位置
/dev/root            29320680  3822304     23985904  14% /
devtmpfs                469688             0         469688   0% /dev
tmpfs                     474004             0         474004   0% /dev/shm
tmpfs                     474004      12596         461408   3% /run
tmpfs                         5120             4            5116   1% /run/lock
tmpfs                     474004             0          474004  0% /sys/fs/cgroup
/dev/mmcblk0p6       64366      19974           44392 32% /boot
/dev/sda2          930134012    290184    929843828  1% /media/pi/Seagate
tmpfs                       94804            0            94804   0% /run/user/1000

Seagate HDDのデバイスネームとして/dev/sda2が与えられている。
Seagate HDDは臨時に/media/piディレクトリにマウントされている。
以上を確認。※デバイスネームはシステムが勝手に割り当てるもので状況により変わる。
$ df -h
と入力するともう少しわかりやすい単位で表示してくれる。

■新規にディレクトリ(フォルダ)を作る
ターミナルを開いて以下を入力。
$ sudo mkdir /mnt/usbhdd
mkdir(make directory)コマンドでmntフォルダの下にusbhddフォルダを作成した。

■HDDのマウント位置を変える
/media/piにマウントしたままでは以下の設定ができないのでマウント位置を変える。
ターミナルを開いて以下を入力。/dev/sda2をいったんアンマウントする。
$ sudo umount /dev/sda2
次に以下を入力。
$ sudo mount -t auto /dev/sda2 /mnt/usbhdd
これで先ほど作成した/mnt/usbhdd直下にマウントされる。

■ラズベリーパイ起動時に自動マウントされるようにする
上記の設定のままでは再起動すると設定がリセットされてしまう。再起動後も自動マウントされるようシステムファイル/etc/fstabに追記する。
ターミナルを開いて以下を入力。
$ sudo nano /etc/fstab
最後の行(# a swapfile is not swap partition,...の前)に以下を追記。
※長い空白はTabキーを入力。
/dev/sda2     /mnt/usbhdd     ntfs-3g     defaults     0     0
その後、Ctrl+O(上書き)を押す。fstabを上書きするか尋ねてくるのでEnterを押す。
Ctrl+Xを押して完了。
※書く場所を間違えると次回の起動でつまずくので注意。

■Sambaをインストールする
ファイルサーバを作成するためのプログラム、sambaをインストールする。
ターミナルを開いて以下を入力。
$ sudo apt install samba

■Sambaの設定をする。
ターミナルを開いて以下を入力。
$ sudo nano /etc/samba/smb.conf
最後の行に以下を追加
[RaspNAS]
comment = USBHDD
path = /mnt/usbhdd
public = Yes
read only = No
writable = Yes
guest ok = Yes
force user = pi

その後、Ctrl+O(上書き)を押す。smb.confを上書きするか尋ねてくるのでEnterを押す。
Ctrl+Xを押して完了。
※[RaspNAS]はWindowsPCで閲覧する時のサーバー名である。任意の文字を名称として設定して差し支えないが、Sambaが予約している単語は避ける。どのような単語が予約されているかはsmb.confを読めばわかる。
※comment =に続く文字はコメント文である。自分のための覚え書きなので何を書いてもよい。
$ reboot
でリブートするとSambaが起動する。
以上で、ファイルサーバ完成。

1.3.ラズベリーパイのローカルIPアドレスを知る

ターミナルを開いて以下を入力。
 $ ifconfig
以下のような文字列が表示される。
eth0 
Link encap:イーサネット ハードウェアアドレス x0:00:xx:00:00:x0
inet6アドレス: xx00::0x00:0xx0:0000:x0xx/64 範囲:リンク
UP BROADCAST MULTICAST MTU:1500 メトリック:1
RXパケット:0 エラー:0 損失:0 オーバラン:0 フレーム:0
TXパケット:0 エラー:0 損失:0 オーバラン:0 キャリア:0
衝突(Collisions):0 TXキュー長:1000
RXバイト:0 (0.0 B) TXバイト:0 (0.0 B)

lo 
Link encap:ローカルループバック
inetアドレス:127.0.0.1 マスク:255.0.0.0
inet6アドレス: ::1/128 範囲:ホスト
UP LOOPBACK RUNNING MTU:65536 メトリック:1
RXパケット:140 エラー:0 損失:0 オーバラン:0 フレーム:0
TXパケット:140 エラー:0 損失:0 オーバラン:0 キャリア:0
衝突(Collisions):0 TXキュー長:0
RXバイト:11712 (11.4 KiB) TXバイト:11712 (11.4 KiB)

wlan0 
Link encap:イーサネット ハードウェアアドレス x0:00:xx:00:x0:x0
inetアドレス:192.168.0.5 ブロードキャスト:192.168.0.255 マスク:255.255.255.0
inet6アドレス: xx00::xx00:xxxx:xx00:x0x0/64 範囲:リンク
UP BROADCAST RUNNING MULTICAST MTU:1500 メトリック:1
RXパケット:599 エラー:0 損失:490 オーバラン:0 フレーム:0
TXパケット:194 エラー:0 損失:0 オーバラン:0 キャリア:0
衝突(Collisions):0 TXキュー長:1000
RXバイト:94274 (92.0 KiB) TXバイト:30525 (29.8 KiB)

→注目すべきはwlan0の2行目、inetアドレス:192.168.0.5 
これがWiFiのローカルIPアドレスである。
192.168.0.Xとなる数列で、末尾の1桁はルータが勝手に割り当てる。

■ラズベリーパイのローカルIPアドレスを知る別の手段。
私のWiFiルータはバッファロー製である。バッファロー製ルータの付属ユーティリティ"エアステーション設定ツール"の管理画面トップページ、"ネットワークサービス一覧を表示"を押すと、ローカルWiFi接続機器の一覧が表示される。そこからラズベリーパイのIPアドレスを知ることもできる。
■iPhoneユーザーならばFingというiPhoneアプリが利用できる。ローカルWiFiをスキャンし、接続しているデバイスとIPアドレスのリストを表示してくれる。
※iOS11よりMACアドレスが読めなくなったとかでFingが使いにくくなった。新規のデバイス名が不明と表示される。何らかのデバイスがIPアドレスを取得したのはわかるがデバイスの詳細はわからない。
■WindowsならAdvanced IP Scannerというフリーソフトが利用可能だ。窓の杜で入手できる。窓の杜ライブラリ>インターネット・セキュリティ>サーバー・ネットワーク>ネットワーク調査*~に分類されている。*https://forest.watch.impress.co.jp/library/nav/genre/inet/servernt_netanlz.html
ローカルネットワーク内のデバイスを検出してリスト化してくれる。
※このフリーソフトを起動するとユーザーフォルダ内にadvanced_ip_scanner_Aliases.bin他3つのジャンクファイルを勝手に保存するところがちょっとうざい。このジャンクファイルはその都度削除しても大丈夫。

■ルータを再起動するとローカルIPアドレスの割り当てがリセットされる。おそらく末尾1桁の数字が変わる。

■WindowsPCからファイルサーバにアクセスする
WindowsPCでエクスプローラを開き、検索窓に"\\192.168.0.5"を入力する。末尾の1桁は環境により変わる。
ネットワーク資格情報の入力欄
ユーザー名:pi
パスワード:raspberry
でアクセス可能。

上記の方法でアクセスすると以後、PCの電源を落とすまでエクスプローラ>ネットワーク経由でもアクセス可能となる。

1.4.ラズベリーパイをリモート接続する

WindowsPC、ラズベリーパイそれぞれに別々のモニタ、キーボード、マウスを用意できればよいが、通常はリモート接続によってひとつのモニタ、キーボード、マウスを共用することになろう。

■ラズベリーパイにSSHでリモート接続する
WindowsPCにTera Termをインストールする。ラズベリーパイ側はSSHをオンにしておく。
Tera Term
https://ja.osdn.net/projects/ttssh2/

Tera Teamを起動し、"ホスト"にローカルIPアドレス"192.168.0.X"を入力。
ユーザ名:pi
パスフレーズ:raspberry
でOKを押す。
ターミナル画面が開き、コマンド入力待ち状態となる。
SSHで開くことができるのはターミナル画面のみである。しかし、この画面からすべてのことが行えるので問題ない。

■ラズベリーパイにリモートデスクトップ接続する(xrdp)
ラズベリーパイにリモートデスクトップ接続する方法は二つある。ひとつはxrdp経由で行うもの。Windows標準のリモートデスクトップ接続になる。Windowsスタート画面、Windowsアクセサリの中にある"リモートデスクトップ接続"で接続できる。後述するVNCとは同時に利用できない。
ラズベリーパイ側の準備。リモートデスクトップ接続を有効にするために以下のふたつのプログラムをインストールする。ターミナルを開いて以下を入力。
$ sudo apt install tightvncserver
$ sudo apt install xrdp
WindowsPC側でリモートデスクトップ接続を起動。
"コンピュータ(C):"にローカルIPアドレス"192.168.0.X"を入力。
USERNAME:pi
PASSWORD:raspberry
でOKを押す。

■ラズベリーパイにリモートデスクトップ接続する(REALVNC)
ラズベリーパイに標準で組み込まれているリモートデスクトップ接続サービスはREALVNC経由によるものだ。そのクライアントソフトVNC ViewerはWindows、Mac、iOS、Andoroid...様々なホストをサポートしている。VNCを利用するためにはメニューバー>Preferences>Raspberry Pi Cofiguration "Interfaces"のタブの下、VNCをオンにする。リブートするとメニューバーにVNCアイコンが現れる。このVNCアイコンをクリックするとラズベリーパイのローカルIPアドレス"192.168.0.X"が表示される。
■WindowPCにVNC Viewerをインストールする。
VNC Viewerのダウンロードページ*にアクセスしてWindouws用VNC Viewerアプリをダウンロードする。
*https://www.realvnc.com/download/viewer/
ダウンロードしたVNC-Viewer-6.0.2-Windows-64bit.exeを好きなフォルダに置いてダブルクリックする。
空白部分で右クリック""New Connection"を選ぶ。"VNC Server"にローカルIPアドレス"192.168.0.X"を記入。"Name"は”RaspberryPi"とでもしておく。"Continue"を押して、USERNAME:pi、PASSWORD:raspberry、"Remember Password"にチェックを入れておく。OKボタンで接続を始める。
※iOSもサポートしているのでiPhoneにVNC Viewerを入れておけば、iPhone からもリモートデスクトップ接続できる。

1.5.ラズベリーパイをNAS-ファイルサーバーにする-横着編

上記はとあるページを参考にしたやり方、でもよく考えたらわざわざHDDのマウント位置を変える必要なくねということで、マウント位置は/media/piのまま、そのディレクトリに対して共有かけてみた。

■Sambaをインストールする
最新のRaspberry Pi OSにはntfs-3gが最初から組み込まれているようなので新たにインストールする必要はない。Sambaだけをインストールする。
$ sudo apt install samba

■Sambaの設定をする。
ターミナルを開いて以下を入力。
$ sudo nano /etc/samba/smb.conf

最後の行に以下を追加
[RaspNAS]
path = /media/pi
public = Yes
read only = No
writable = Yes
guest ok = Yes
force user = pi

その後、Ctrl+O(上書き)を押す。smb.confを上書きするか尋ねてくるのでEnterを押す。
Ctrl+Xを押して完了。そしてリブート。
これでラズベリーパイにつないだHDDはすべてWindowsPCと共有される。

■WindowsPCからファイルサーバにアクセスする
WindowsPCでエクスプローラを開き、検索窓に"\\192.168.X.X"を入力する。\\に続けてラズベリーパイのローカルIPアドレスを入力する。
ネットワーク資格情報の入力欄
ユーザー名:
パスワード:
はRaspberry Pi OSをインストールした時のユーザー名/パスワードに従う。

2.WindowsPCとファイル共有する

2.1.Sambaを使ってWindowsPCとファイル共有する

■ntfs-3gとSambaをインストールする
前述の通り
$ sudo apt install ntfs-3g
$ sudo apt install samba

■Sambaの設定をする
ターミナルを開いて以下を入力。
$ sudo nano /etc/samba/smb.conf
最後の行に以下を追加
[RaspSHARE]
comment = HomePi
path = home/pi
public = Yes
read only = No
writable = Yes
guest ok = Yes
force user = pi

その後、Ctrl+O(上書き)を押す。smb.confを上書きするか尋ねてくるのでEnterを押す。
Ctrl+Xを押して完了。
※[RaspSHARE]はWindowsPCで閲覧する時のサーバー名である。任意の文字を名称として設定して差し支えないが、Sambaが予約している単語は避ける。どのような単語が予約されているかはsmb.confを読めばわかる。
※comment =に続く文字はコメント文である。自分のための覚え書きなので何を書いてもよい。
※前述のファイルサーバ制作の時path =に続く文はpath = /mnt/usbhddで"mnt"の前に"/"があった。しかし今回、"home"の前に"/"はない。結論から言うとこの"/"はあってもなくてもどっちでもいいらしい。

■Sambaを起動する
ターミナルを開いて以下を入力。
$ sudo service samba-ad-dc restart
または、リブート。

■WindowsPCからRaspberry Piにアクセスする
WindowsPCでエクスプローラを開き、検索窓に"\\192.168.0.X"を入力する。末尾の1桁は環境により変わる。
ネットワーク資格情報の入力欄
ユーザー名:pi
パスワード:raspberry
でアクセス可能。

■ファイルサーバとファイル共有を同時に実行する

[RaspNAS]
comment = USBHDD
path = /mnt/usbhdd
public = Yes
read only = No
writable = Yes
guest ok = Yes
force user = pi

[RaspSHARE]
comment = HomePi
path = home/pi
public = Yes
read only = No
writable = Yes
guest ok = Yes
force user = pi

というように前述のファイルサーバの構文に続けて、ファイル共有の構文を書き足すことができる。この場合、WindowsPCから"\\192.168.0.X"にアクセスすると[RaspNAS][RaspSHARE]ふたつのフォルダが見える。

3.Raspberry Pi 3にポータブルSSDをつなぐ

3.1.ポータブルSSD(USB給電タイプ)をつなぐ

Sandisk製外付けポータブルSSDを入手したのでラズベリーパイにつないでみる。USB経由で給電を受けるタイプである。
■USB給電のアンペア数を上げる
ラズベリーパイ側からの出力電流は0.6Aに制限されているらしいので、これを1.2Aに引き上げる。
ターミナルを開いて以下を入力。
$ sudo nano /boot/config.txt
最後の行に以下を追加
safe_mode_gpio=4
その後、Ctrl+O(上書き)を押す。config.txtを上書きするか尋ねてくるのでEnterを押す。
Ctrl+Xを押して完了。

■exFAT形式のドライバをインストールする
Sandiskの外付けSSDはexFATとかいうフォーマット形式らしい。これもそのままではラズベリーパイで扱えないのでドライバをインストールする。
$ sudo apt-get install exfat-fuse
そして再起動。

ラズベリーパイに外付けSSDをつなぐと臨時に/media/piフォルダの中にマウントされる。マウントポイントを変えたい時は上記「HDDをマウントしてファイルサーバーにする」の項を参照。SSDを外す時は必ずデスクトップ画面右上のイジェクトボタンを押してマウントを解除する。安全に取り外すためのおまじない。

4.Raspberry Pi 3に3.5インチタッチパネルをつなぐ

4.1.機器を接続する

ラズベリーパイのGPIOに3.5インチLCDを挿す。挿す場所は取説などに従う。HDMI接続のLCDの場合、二股のHDMI端子が付属するが、まだLCDドライバを入手していないので、今はこれを使わない。ラズベリーパイのHDMI端子には一般のモニタをつなげておく。

4.2.GitHubからドライバを入手し、インストールする

ここ*に行くと最新のタッチパネルディスプレイドライバが入手できるらしい。
*https://github.com/goodtft/LCD-show
このURLを覚える必要はない。「GitHub LCD-show」くらいのキーワードでググればたどりつける。
このページを見れば丁寧にドライバの入手方法が書いてある。以下はその覚え書き。
ターミナルを開いて以下を入力。
$ sudo rm -rf LCD-show
$ git clone https://github.com/goodtft/LCD-show.git
$ chmod -R 755 LCD-show
$ cd LCD-show
$ sudo ./LCD35-show


1行目は先に古いLCD-showフォルダがあった場合、それを消すコマンド。初めてのイントールならばLCD-showフォルダは存在しないはずなので、1行目は省略可。
2行目でドライバを入手。piフォルダにLCD-showフォルダができているはずだ。
3行目でLCD-showフォルダのアクセス権変更
4行目cd(change directory)コマンドでLCD-showフォルダの中に入る。
5行目でシェルスクリプトLCD35-showの実行、自動的に再起動して、3.5インチLCDにデスクトップが表示される。もしあなたのLCDが3.5インチでなく5インチだったら、5行目はsudo ./LCD5-showとなる。詳しくは上記URLを参考にしていただきたい。

■一般のHDMIモニタに戻したい時
ターミナルを開いて以下を入力。
$ cd LCD-show
$ ./LCD-hdmi

■再び3.5インチLCDに戻したい時
ターミナルを開いて以下を入力。
$ cd LCD-show
$ ./LCD35-show

ラズベリーパイゼロWHにElecrowの3.5インチタッチパネルを挿して、GitHubのドライバをインストールしたところ、タッチ動作のX軸とY軸が入れ替わる不具合が生じた。(指を横に動かすとポインタは縦に動く現象)
そこで下記、3.ドライバを直接ダウンロードし、インストールする方法を試したところ不具合は解消した。

4.3.ドライバを直接ダウンロードし、インストールする方法

GitHubから入手した方が楽である。以下の方法はお勧めしない。
■waveshare.com-LCDの記事*にアクセス
*http://www.waveshare.com/wiki/3.5inch_RPi_LCD_(A)
文中のリンクからLCD-show-170703.tar.gzファイルをダウンロードする。リンクをクリックするとダウンロードを開始する。

ダウンロードしたファイルは/home/pi/Downloadsフォルダに保存されているはずである。このフォルダから出して/home/piフォルダに移動する。WindowsPCと同様、右クリックで「切り取り」→「コピー」でファイルを移動できる。
※上記リンクにはLCDパネル設定手順が英文で書いてある。以下はその覚え書き。
※3.5inch_RPi_LCD_(A)と3.5inch_RPi_LCD_(B)がある。(B)を使うと表示がネガポジ反転する。

■初期設定
ターミナルを開いて以下を入力。
$ sudo raspi-config
初期設定画面が開く。
下↓カーソルキーを2回叩いて"3 Boot Options"をハイライトさせてEnter。
"B1 Desktop/CLI"をハイライトさせてEnter。
下↓カーソルキー3回たたいて"B4 Desktop Autologin"をハイライトさせてEnter。
Tabキー2回叩いて"Finish"をハイライトさせてEnter。
"Yes"をハイライトさせてEnter。自動でリブートする。

■tarファイルを解凍(tarファイルはzipのようなもので複数のフォルダ/ファイルをまとめて圧縮している)
ターミナルを開いて以下を入力。
$ tar xvf LCD*.tar.gz
/home/piフォルダの中にLCD-showフォルダが現れる。
※LCD-show-170703.tar.gzをダブルクリック、LCD-showフォルダが見えるのでこれを1回クリックで選択、アクションメニューから[展開]、次に開くダイアログボックスの設定は変えずにそのまま[展開]ボタンを押しても結果は同じ。ターミナルからコマンドを打ちたくない人はこちらの手順で。

■3.5インチタッチパネルディスプレイ用ドライバを起動
ターミナルを開いて以下を入力。
$ cd LCD-show
$ ./LCD35-show

■HDMIに戻したい時
ターミナルを開いて以下を入力。
$ cd LCD-show
$ ./LCD-hdmi

5.Raspberry Pi 3でAmazon Echoを作る

5.1.準備

Amazon EchoはAlexa(アレクサ)と呼びかけると反応する音声コントロールによる人工知能である。開発者向けのボイスサービスサンプルプログラムを使うことで、ラズベリーパイからも利用できる。

まずはラズベリーパイを使用可能な状態にするための最低限の下ごしらえをしておく。
■SDカードをフォーマットし、Rasipbian Jessie with Pixelを書き込む。
■キーボード、マウス、モニタを接続、そしてSDカードをラズベリーパイに挿し、起動する。
■WiFiを使えるようにしておく。
■"Locarization"の設定をしておく。
■OSをアップデートする。心配ならアップグレードもしておく。
■SSH経由でリモート接続するつもりなら、SSHをオンにしておく。
■WindowsPCからリモートデスクトップ接続するつもりならtightvncserverとxrdpもインストールしておく。

以上、詳細は前項に記載済みなのでそちらを参照。
さらに今回はUSBマイクと3.5mmイヤホンジャック入力のスピーカーも接続する。AlexaはUSBスピーカーに対応していない。
※HDMIモニタを接続していると音声出力が自動的にHDMIにセットされる。強制的に3.5mmイヤホンジャックに出力させるには
$ sudo raspi-config
7 Advance Optionsを>A4 Audio>1 Force 3.5mm ('headphone')jackをハイライトさせてTabキー<Ok>を選んでenter。Tabキー2回押して<Finish>選んでenter。

なお、ラズベリーパイでAlexaを利用する手順を詳細に説明するウェブページ*が既にある。私の説明はここに書かれていることとほぼ同じ内容である。
*https://github.com/alexa/alexa-avs-sample-app/wiki/Raspberry-Pi

5.2.Amazon Developerアカウントの登録

開発者向けのウェブサービス、Amazon Developerアカウントを取得し、ラズベリーパイで利用可能なセキュリティIDを発行してもらう。
まずはAmazon Developer Serviceのウェブページ*にアクセスする。
*https://developer.amazon.com/

言語設定から"Japanese(日本語)"も選べるが"English"のままにしておく。"Japanese(日本語)"を選択するとAlexaに関するリンクが非表示になってしまう。
上段のメニュー、Alexa>Alexa Voice Srevice>Stert Buildindのリンクをたどる。
Getting Started with the Alexa Voice Serviceのページ、"Sign up for a free Amazon developer account"のリンク*をクリック。
*https://developer.amazon.com/home.html

Sign Inのページ
■E-mail or mobile numberの欄にEメールアドレス
■初回登録時は◎I am a new costomer.に丸をつけて次へ
1.Profile Infomation-個人情報を偽りなく記入。日本の国番号は+81。
2.App Distribution Agreement-合意して次へ
3.Payments-開発したアプリで金儲けする意思があるか聞いている。両方とも◎No
※次回以降はパスワードを入力して// DEVELOPER CONSOLEポータルサイトにリンクする。

登録が完了すると// DEVELOPER CONSOLE(開発者のためのポータルサイト)にアクセスできるようになる。ここで上段ALEXA>Alexa Voice Service>Get Startedのリンクをだどる
[Register a Product Type]のメニューで"Device"を選択。

Create a new Device Typeのページ
Device Type Info
Company Nameのところにあなたの登録した名前が入っている。
Device Type IDは"my_device"とすること。他の名前では先へ進めない。
Display Nameは"My Device"
次へ

[Security Profile]ドロップダウンリストから"Create a new profile" を選択。
[General]タブ
Security Profile Nameに"Alexa Voice Service Sample App Security Profile"と記入。
Security Profile Descriptionに"Alexa Voice Service Sample App Security Profile Description"と記入。
次へ

・Security Profile ID
・Client ID
・Client Secret
が発行される。このうちClient IDとClient Secretは後々使用するのでコピーしておく。

次に、[Web Settings]タブをクリック。
[Alexa Voice Service Sample App Security Profile]のドロップダウンリストはそのまま
[Edit]をクリック。
Allowed Origins [Add Another]クリック、ブランクに"https://localhost:3000"と記入。
Allowed Return URLs[Add Another]クリック、ブランクに"https://localhost:3000/authresponse"と記入。
次へ

Devise Details
横142x縦130pixels以内のJPEG画像を登録できる。画像はリストに表示される。画像はあってもなくてもよい。
[Category]ドロップダウンリストから"Other" を選択。
Descriptionに"Alexa Voice Service sample app test"と記入。
Do you have plans to make your product available to the general public? ◎Noを選択。
次へ

Amazon Music
Apply for access to Amazon Music?◎Noを選択。
[Submit]をクリック。

Security Profileを有効にする
ここへ行く
https://developer.amazon.com/lwa/sp/overview.html

[Select a Security Profile] ドロップダウンリストから"Alexa Voice Service Sample App Security Profile" を選択。
[Confirm]をクリック。

Consent Privacy Notice URLに"http://example.com"と記入。
[Save]をクリック。

"Show Client ID and Client Secret"のリンクをクリック。
Client ID と Client Secretが表示されていることを確認する。

なお、セキュリティプロファイルを作成する手順を詳細に説明するウェブページ*が既にある。私の説明はここに書かれていることとほぼ同じ内容である。
*https://github.com/alexa/alexa-avs-sample-app/wiki/Create-Security-Profile

5.3.ラズベリーパイのセットアップ

■alexa-avs-sample-appをコピーする
ターミナルを開いて以下を入力。
$ cd Desktop
$ git clone https://github.com/alexa/alexa-avs-sample-app.git
デスクトップに"alexa-avs-sample-app"という名のフォルダが作成されている。このフォルダを開いて内容を確認する。Rasipbian Jessieのファイルマネージャーがファイルの存在に気づいていないことがあるので、フォルダを開いて確かにファイルがあることを確認する。

■alexa-avs-sample-appのインストールスクリプトを編集する
ターミナルを開いて以下を入力。
$ cd ~/Desktop/alexa-avs-sample-app
$ sudo nano automated_install.sh

ProductID=my_device
ClientID=amzn.xxxxx.xxxxxxxxx
ClientSecret=4e8cb14xxxxxxxxxxxxxxxxxxxxxxxxxxxxx6b4f9

Amazonから発行されたClientIDとClientSecretをそれぞれ記入する。
その後、Ctrl+O(上書き)を押す。automated_install.shを上書きするか尋ねてくるのでEnterを押す。
Ctrl+Xを押して完了。

■インストールスクリプトを実行する
ターミナルを開いて以下を入力。
$ cd ~/Desktop/alexa-avs-sample-app
$ . automated_install.sh
これでAlexaインストール完了。

5.4.Alexaの起動

3つのターミナルウィンドウを別々に開いて3つのコマンドを実行する。
■ターミナル1
以下を入力。
$ cd ~/Desktop/alexa-avs-sample-app/samples
$ cd companionService && npm start

■ターミナル2
以下を入力。
$ cd ~/Desktop/alexa-avs-sample-app/samples
$ cd javaclient && mvn exec:exec
Webブラウザを起動する旨の警告と[Yes/No]のボタンが出るので[Yes]をクリック。直後に[OK]ボタンが出るがすぐにこれを押してはいけない。
Webブラウザが起動し、Amazon Developer Serviceにログインする。
JessieではここでしばしばHTTPS警告「この接続ではプライバシーが保護されません」が出ることが確認されている。左下[詳細設定]ボタンを押してこれを回避する。
認証が通ると"device tokens ready"と表示される。ここで先ほどの[OK]ボタンを押す。
[Listen]ボタンが表示される。このボタンを押してAlexaを呼び出すことができる。

■ターミナル3
以下を入力。
$ cd ~/Desktop/alexa-avs-sample-app/samples
$ cd wakeWordAgent/src && ./wakeWordAgent -e sensory
これでWake Wordが使えるようになる。[Listen]ボタンを押さなくても"Alexa"と呼びかけるだけでポン♪という音とともにAlexaを呼び出すことができる。
"Good morning."と呼びかけてみよう。

6.MP3を再生する

6.1.VLCをインストールする

定番のメディアプレイヤー"VLC Media Player"をインストールする。
$ sudo apt-get install vlc
メニューバー、ラズベリーのプルダウンメニューに"Sound & Video"カテゴリが作られる。その中に"VLC Media Player"がある。
"VLC Media Player"を選んで起動する。初期画面はオレンジのパイロン。Media>Open File...のウィンドウから聴きたいmp3ファイルを選ぶ。
※デフォルトのRaspbianにmp3ファイルは存在しない。前述のファイル共有機能等を使ってあらかじめ他所からコピーしておく必要がある。/home/pi/Musicフォルダにでも置いておけばよいだろう。
※/home/pi/python_gameフォルダの中にいくつかのビープ音が収録されている。何でもいいから音を出したい時に便利。音符の形のアイコンなのですぐに見つけられるだろう。

6.2.Bluetoothスピーカーを接続する

音楽をBluetouthスピーカーから流してみる。Bluetouthスピーカーの電源を入れ、どこかのボタン(スピーカーの仕様によるので取説参照)を長押ししてペアリングモードにする。
ラズベリーパイのBluetouthアイコンをクリック。Add Device...のウィンドウからBluetouthデバイス名を選んで"Pair"を押す。
Bluetouthアイコンの2つ右、スピーカアイコンを右クリック。Bluetouthデバイス名を選んでチェックを入れる。これでBluetoothスピーカーから音が出るようになる。
※Bluetoothスピーカーは安定しない。時折音飛び、音切れが発生する。
※ブラウザでYouTubeを開いてミュージックビデオ等を再生しようとするとハングアップする。正直Bluetoothスピーカーは使い物にならない。

6.3.USBスピーカーを接続する

スピーカアイコンを右クリック。Audio Advantage MicroIIを選ぶ。Analogを選べば3.5mmイヤホンジャックから、HDMIを選べばHDMIモニタから、Bluetoothスピーカーを選べばBluetoothスピーカーから、Audio Advantage MicroIIを選べばUSBスピーカーから音が出る仕組み。

7.Python-Pygameを使ってみる

7.1.Pygameについて

Pygameはラズベリーパイにあらかじめインストールされているプログラム言語Pythonのためのモジュールである。ラズベリーのメニューの下、Gameの中にPython Gamesがあり、たくさんのゲームが収録されている。こんな立派なゲームを作ることはできないが、簡単なプログラムなら書けそうなので挑戦してみた。

7.2.Pygameを使ってMP3を再生する

Pythonでmp3を再生するスクリプトを書き、実行する。
まず、/home/piフォルダの中に再生したいmp3ファイルを置いておく。
ターミナルを開いて以下を入力。
$ nano musicplay.py
白紙の画面が表示されるので、下記のスクリプトを記述。
# -*- coding:utf-8 -*-

import pygame.mixer
import time

# mixerモジュールの初期化
pygame.mixer.init()
# 音楽ファイルの読み込み
pygame.mixer.music.load("ファイル名.mp3")
# 音楽再生、および再生回数の設定(-1はループ再生)
pygame.mixer.music.play(-1)
# 60秒間だけ再生
time.sleep(60)
# 再生の終了
pygame.mixer.music.stop()

その後、Ctrl+O(上書き)を押す。musicplay.pyを上書きするか尋ねてくるのでEnterを押す。
Ctrl+Xを押して完了。
/home/piにmusicplay.pyファイルが作成されている。その隣にmp3ファイルが置かれている。
※上記スクリプトの"ファイル名.mp3"の部分には隣に置いてあるmp3ファイル名を書いておく。

ターミナルを開いて以下を入力。
$ python musicplay.py
指定した"ファイル名.mp3"が60秒間だけ再生される。
musicplayというファイル名は私が勝手につけたファイル名である。もちろんこの名前は好き勝手に変更していただいてかまわない。

7.3.Pygameを使ってデジタル時計を作る

Pythonでデジタル時計を表示するスクリプトを書き、実行する。
ターミナルを開いて以下を入力。
$ nano clock.py
白紙の画面が表示されるので、下記のスクリプトを記述。
# -*- coding:utf-8 -*-

import pygame
from pygame.locals import *
import datetime
import sys

# Pygameの初期化
pygame.init()
# 大きさ300*200の画面を生成
screen = pygame.display.set_mode((300, 200))
# タイトルバーに表示する文字
pygame.display.set_caption("clock")
# フォントの設定(25px)
font = pygame.font.Font(None, 25)

# ループを作る
while True:
    # 画面を黒色に塗りつぶし
    screen.fill((0,0,0))
    # 変数clocktimeを定義、現在の年月日時分秒を代入
    clocktime = datetime.datetime.now().strftime('%Y/%m/%d %H:%M:%S')
    # アンチエイリアスあり、文字色を白に設定
    text = font.render(clocktime, True, (255,255,255))
    # 文字列の表示位置x方向:75ドット、y方向:85ドット
    screen.blit(text, [75, 85])
    # 画面を更新
    pygame.display.update()

    # イベント待ち受け
    for event in pygame.event.get():
        # 閉じるボタンが押されたら終了
        if event.type is pygame.QUIT:
            # Pygameの終了(画面閉じられる)
            pygame.quit()
            sys.exit()


その後、Ctrl+O(上書き)を押す。clock.pyを上書きするか尋ねてくるのでEnterを押す。
Ctrl+Xを押して完了。
/home/piにclock.pyファイルが作成されている。

ターミナルを開いて以下を入力。
$ python clock.py
デジタル時計が表示される。
もう一度言うがclockというファイル名は私が勝手につけたファイル名である。好き勝手に変更してかまわない。

■Pythonの決まり事
screen =
font =
clocktime =
text =
=(イコール)の前にある文字列はたいてい変数である。変数ということはユーザーが勝手に定義した仮名称のようなもので、これまた勝手に別の文字に置き換えられる。もちろん置き換えたら置き換えたで、その後の行でその変数を引用している箇所が必ずあるので、そこもいっしょに置き換えなければならない。
そうはいっても"font ="や"text ="を別の文字に置き換えたところでわかりづらくなるだけだ。"screen ="もまた慣例的にみんなが使っている変数名にすぎない。人によっては"display ="とか別の言い方に変えているのを見かける。

7.4.python_gamesフォルダについて

/home/piフォルダの中にpython_gamesフォルダがある。この中にはpygameで作られたゲームがスクリプトのまま収められている。例えばpython_gamesフォルダの中のflippy.py(オセロゲーム)をダブルクリックで開くとそのスクリプトを読むことができる。そしてこれを実行して実際にゲームをプレイすることができる。
ゲームをプレイするためにはまずカレントディレクトリをpython_gamesフォルダに移す。

ターミナルを開いて以下を入力。
$ cd python_games
続けてflippy.pyを実行。
$ python flippy.py
これでオセロゲームの盤面が開く。

8.WindowsにPython-Pygame開発環境を構築する

8.1.WindowsにPythonをインストールする

■Pythonの入手
Python.orgのDownloadページ*に行って最新Pythonを入手する。
*https://www.python.org/downloads/
本稿執筆時点の最新バージョンはPython3.6.4であった。"Download Python 3.6.4"のボタンを押すとすぐに32ビット用Python3.6.4実行形式のインストーラがダウンロードされる。これを使ってインストールしてかまわない。"Downloads">"Windows"のタブをたどるとインストーラにも複数の種類があることを知る。
Windows x86-64 xxxxxは64ビット用、Windows x86 xxxxxは32ビット用。私のPCはWindows10の64ビットだが、ここはあえて動作の安定している32ビット用を選ぶ。もちろんここで64ビット用を選んでもかまわない。さらにweb-based、embeddable zip fileなど様々な形態が用意されているが、ここは実行形式:Windows x86 executable installerを選ぶべきである。様々な処理を自動で行ってくれるので楽である。
■Pythonのインストール
Python3.6.4インストーラをダブルクリックして起動。途中□Add Python 3.6 to PATHのチェックボックスが表示されるので、チェックを入れて続行。「環境変数の編集」とか「パスを通す」とか意味不明の手続きを自動で行ってくれる。最後、Disable path length limitの記述のところがボタンになっているのでこれを押し、ファイル名に関する制限項目を解除して完了。
なお、Python-32bit-3.6.4の本体は隠しフォルダの中にインストールされる。
■IDELを試す
IDELはPythonに付属するエディタである。IDELはWindowsメニュー>"最近追加されたもの"の中に入っているはずである。これを起動すると確かにPython3.6.4の記述がある。プロンプトに続けて
print ("Hello World")
と入力。Enterを押してHello Worldが返ってくればインストール成功だ。
FileメニューからNew Fileを選択すると新規pyファイルを作ることができる。そのまま保存すると例の隠しフォルダの中に保存してしまうので、保存先は変えた方がいいだろう。

8.2.WindowsにPygameをインストールする

ラズベリーパイなら最初から入っているが、Windowsの場合、Pygameも自力でインストールしなければならない。
■Pygameの入手
Pygame.orgのページ*に行って最新Pygameを入手する。
*https://www.pygame.org/
ページの中ほどにpygame 1.9.3 released — 16 Jan, 2017の記事がある。Downloads: PyPIのリンクをたどるとインストーラの一覧が表示される。これまたたくさんの種類がある。Windwows用Python-32bit-3.6.1に適合したPygameはpygame-1.9.3-cp36-cp36m-win32.whl (md5)なので、このリンクをクリックしてpygame-1.9.3-cp36-cp36m-win32.whlをダウンロードする。これはwheel形式といって残念ながら実行形式ではない。
Windows用Python拡張パッケージまとめサイト*もある。
*https://www.lfd.uci.edu/~gohlke/pythonlibs/
ここでもpygame-1.9.3-cp36-cp36m-win32.whlを見つけられるだろう。
■Pygameのインストール
pygame-1.9.3-cp36-cp36m-win32.whlファイルをC:\Users\自分の名前のフォルダに置く。エクスプローラーでたどるなら > PC > Windows(C:) > ユーザー > 自分の名前のフォルダの中だ。
よそのチュートリアルでは、この時環境変数にパスが通っているか確認せよと忠告するところもあるがその必要はない。パスは自動で通っているはずだ。
次にスタートメニューを右クリック、Windows PowerShellを起動。管理者でない方を選ぶ。かつてコマンドプロンプトと呼ばれたシェルプログラムはいつの間にかこんな名前に変わっていた。旧コマンドプロンプトもスタートメニューの中をさがせばある。
C:\Users\自分の名前>のプロンプトが表示されているはずである。pygame-1.9.3-cp36-cp36m-win32.whlファイルが別の場所にあるならここからcd(Change Directory)コマンドで.whlファイルが置いてある場所に移動することもできる。
プロンプト(>)に続けて以下を入力。
pip install pygame-1.9.3-cp36-cp36m-win32.whl
これでインストール完了。
■Pygameを用いたプログラムを実行する
前述のclock.pyをC:\Users\自分の名前のフォルダに置く。あるいはIDELで新規pyファイルを作り、前述のスクリプトをコピーし、clock.pyのファイル名でC:\Users\自分の名前のフォルダに保存してもよい。このスクリプトをWindows PowerShellまたはコマンドプロンプトから呼び出して実行する。
スタートメニューを右クリック、Windows PowerShell(管理者でない方)を起動。プロンプトに続けて以下を入力。
python clock.py
これでclock.pyの画面が開く。
■ダブルクリックでpyファイルを実行する
clock.pyファイルをダブルクリックするとそのまま実行する。これは便利。ただし、シェルプログラムの黒い画面が後ろで同時に開いてしまう。これが残念だなと思う方はclock.pywというように拡張子を.pywに変えるとよい。黒い画面を開かなくなる。

8.3.PythonスクリプトをWindows実行形式(.exeファイル)にする

PythonスクリプトをWindows PowerShellから実行できるが、このままではPython-PygameがインストールされたPCでしかプログラムを動かせない。EXEファイルすなわち1個のアプリケーションとして配布でき誰でも実行できるプログラムとしたい。その方法を紹介する。
■cx_Freezeの入手
Pythonスクリプトを実行形式にするアプリはいくつかあるがここではcx_Freezeを使う。
Anthony Tuiningaさんのページ*に行って最新のcx_Freezeを入手する。
*https://github.com/anthony-tuininga
download directly from PyPIのリンクが見つかるだろうか。ここからpython.orgのcx_Freeze保存場所にたどり着くことができる。cx_Freeze-5.1.1-cp36-cp36m-win32.whl (md5)のリンクをクリックしてcx_Freeze-5.1.1-cp36-cp36m-win32.whlをダウンロードする。これはWindwows用Python-32bit-3.6.Xに適合したバージョンとなる。
Windows用Python拡張パッケージまとめサイト*もある。
*https://www.lfd.uci.edu/~gohlke/pythonlibs/
ここでもcx_Freeze-5.1.1-cp36-cp36m-win32.whlを見つけられるだろう。
■cx_Freezeのインストール
cx_Freeze-5.1.1-cp36-cp36m-win32.whlファイルをC:\Users\自分の名前のフォルダに置く。
次にWindows PowerShellを起動。プロンプトに続けて以下を入力。
pip install cx_Freeze-5.1.1-cp36-cp36m-win32.whl
これでインストール完了。
■cx_Freezeの使い方
まずsetup.pyというファイル名のpyファイルを作成、cx_Freezeはこのファイルの中でモジュールとして呼ばれる。さらにこの中にビルドすなわち実行形式に変換するための手順を記述。このpyファイルにbuildという引数を与えて実行すると、exeファイルが組み立てられる仕組み。わかりにくい。
さきほどのclock.pyを例にとって説明する。最終的にclock.pyがclock.exeという名の時計アプリに変換(コンパイル)される。
■setup.pyの記述
IDELで新規pyファイルを作成し、以下を記述。
# coding: utf-8

import sys
from cx_Freeze import setup, Executable

includes = ["pygame", "datetime"]

base = None

if sys.platform == "win32":
   base = "Win32GUI"

exe = Executable(script = 'clock.py', base = base)

setup(name = 'sample',
   version = '0.1',
   description = 'converter',
   executables = [exe])

以上。
4行目includes = ["", "", "", "" ....]で呼び出されるのはclocl.pyに使用したモジュール。clock.pyはPygameとDatetime、sysの3つのモジュールを使用している。使用したモジュールはすべてここに書き出す。ただしsysモジュールとかosモジュールとかは記述不要。
8行目exe = Executable(script = 'clock.py',の行で変換したいpyファイルを記述。
10行目setup(name = 'sample',で名前がsampleのままでいいのか不安になるが、これはこのままでも影響ない。
なお、このsetup.pyの記述はGUIを含むアプリ用である。例えばprint("Hello World")のようなGUIのないアプリの場合、また記述の仕方が少し異なってくるらしい。
■setup.pyの実行
setup.py、clock.pyともにC:\Users\自分の名前のフォルダに置いておく。
次にWindows PowerShellを起動。プロンプトに続けて以下を入力。
python setup.py build
成功すればbuildフォルダができあがる。中にclock.exeと大量の参照ファイルが入っている。この参照ファイルが20MBもある。import pygameというようにざっくりpygameをimportするとpygameのフル機能に対応する参照ファイルが呼び出されるみたいである。
■.exeファイルの配布
大量の参照ファイルを含むbuildフォルダとなってしまったが、clock.exeだけを取り出してダブルクリックしても起動しない。参照ファイルといっしょに配布しなければならない。(全部必要ではないと思うが)これで、pythonを持っていない友人に自作のアプリケーションソフトとして配布することが可能になるが、ひとつ問題点がある。
自作の.exeファイルは他人のPCの下では発行元不明の怪しい実行ファイルとみなされWindowsのセキュリティ機能SmartScreenが発動する。起動しようとすると実行が阻止され「WindowsによってPCが保護されました」画面が表示される。もちろんこれを無視して実行することは可能だが、心臓に悪い。
これを回避するためには専門の機関に連絡して健全な発行元であることを証明するデジタル署名を発行してもらわなければならない。その話はまたPythonとはずいぶん離れた話なので割愛する。

8.4.アイコンを追加する

上記の手順で.exeファイルを作成できるが、完成した.exeファイルにアイコンがない。アイコンを追加する手続きを紹介する。
■PNGファイルを作る
32x32pixelのPNGファイルを作成する。Windows標準-大アイコンは128x128pixelだが、Python で扱えるのは32x32pixelのみである。
■icoファイルに変換する
PNGファイルをicoファイルに変換してくれるWebサービスがいくつかある。ここではアイコンコンバータ*を紹介する。
*http://app.tree-web.net/icon_converter/
JEPG/PNG/GIFファイルをico形式に変換してくれる。ここではとりあえずicon.icoというファイル名で保存し、clock.pyと同じフォルダに置いておく。
■setup.pyの記述
setup.pyを以下のように記述する。icon='icon.ico'が追記されている。
# coding: utf-8

import sys
from cx_Freeze import setup, Executable

includes = ["pygame", "datetime"]

base = None

if sys.platform == "win32":
   base = "Win32GUI"

exe = Executable(script = 'clock.py', base = base, icon='icon.ico')

setup(name = 'sample',
   version = '0.1',
   description = 'converter',
   executables = [exe])

以上。
これでpython setup.py buildを実行すればclock.exeにアイコンが付与される。

9.Python-Pygameで漢字を扱う

9.1.Raspbianで使える日本語フォントを調べる

Pythonで漢字を扱う練習のため、漢字を表示する簡単なアプリを制作してみようと思う。漢字を扱う上で気を付けなければいけないことは以下の2つ。
・日本語のフォントを使用する
・日本語の前にはuの字をつけてunicodeであることを宣言する(Raspbianの場合)

そこでRaspbianにプレインストールされている日本語フォントにどんなものがあるか調べてみる。
ターミナルを開いて以下を入力。
$ python
Pythonが起動し、対話モードになる。プロンプトに続けて以下を入力。
import pygame
pygame.font.get_fonts()
インストール済み使用可能なフォントの一覧を表示する。今回はdroidsansjapaneseを使ってみる。Andoroid端末用日本語フォントらしい。

9.2.新規フォントのインストール

好きなフォントをインストールすることもできる。
メニューバー>Preferences>Add/Remove Softwareを開く。Fontの項目をクリックするとインストール済み/未インストールフォントの一覧を表示する。ここから梅フォント、東風フォントといった有名フリーフォントを追加できる。
もちろんapt-get install...と手入力で好きなフォントパッケージをインストールすることもできる。

9.3.くじびきアプリを作る

漢字利用の例としてくじびきアプリを作ってみる。起動後、画面をクリックするとルーレットがスタートし、1都9県の県名がループする。もう一度クリックすると停止するというシンプルなアプリ。会社などで会議の書記担当をくじ引きで決めるケースもあるだろうと思う。県名のところを人名に書き換えれば、人名ルーレットができあがる。ループの項目の数は増減しても問題ない。
なお、Pythonを覚え始めて半年の初心者が作るプログラムである。プログラムとして不備不適当な箇所があることは承知の上である。とはいえこのレベルのプログラムなら初心者でもサッと書けるところがPythonの魅力だ。

新規.pyファイルを作って以下を入力。(コメントごとコピペしても多分大丈夫。)
#!/usr/bin/env python
# -*- coding:utf-8 -*-
import pygame
from pygame.locals import *
import time
import sys
# Pygameの初期化
pygame.init()
# 大きさ300*200の画面を生成
screen = pygame.display.set_mode((300,200))
# タイトルバーに表示する文字
pygame.display.set_caption('くじびき')
# loopリストを定義、日本語の前にはuをつけてunicodeであることを宣言
loop=[u'茨城',u'栃木',u'群馬',u'埼玉',u'千葉',u'東京',u'神奈川',u'山梨',u'長野',u'新潟']

# 関数main()を定義
def main():
    # 変数nに初期値0を代入
    n = 0
    # 変数xに初期値-1を代入
    x = -1
    # while構文の開始、PygameGUIは必ずwhile構文の形をとる
    while True:
        # 画面色にR,G,B=255,255,255(白)を設定
        screen.fill((255,255,255))
        # fontの定義、プレインストールの日本語フォントdroidsansfallbackを設定、大きさは50pix
        font = pygame.font.SysFont('droidsansfallback',50)
        # loopリストの[n]番目を呼び出してtextオブジェクトを生成、文字色はR,G,B=0,0,0(黒)
        text = font.render(loop[n], True, (0,0,0))
        # text.get_rect()でtextを内包する矩形を取得、変数textrectに代入
        textrect = text.get_rect()
        # textrect(textを内包する矩形)を画面中央にセンタリング
        textrect.center = (300/2),(200/2)
        # 以上はtextの定義、blit()を実行してtextを画面に配置、そして描画
        screen.blit(text, textrect)
        # for構文の開始、イベント待ち受け
        for event in pygame.event.get():
            # 閉じるボタンが押されたら終了、今回はQキー押下でも終了
            if event.type == pygame.QUIT or (event.type == KEYDOWN and event.key == K_q):
                # Pygameの終了
                pygame.quit()
                sys.exit()
            # もしマウスボタンが押されたら変数x(初期値:-1)に-1を乗じる
            if event.type == pygame.MOUSEBUTTONDOWN:
                # 変数xに-1を乗じるたびxの値は+1⇔-1と入れ替わる、これをトグルボタンに利用する
                x = x*-1
        # もしxが+1すなわちx>0の時
        if x>0:
            # nに1を足してインクリメント、loopリストの[n]番目の値が繰り上がる
            n = n+1
            # len(loop)でloopリストの個数を数える、今回の例ではlen(loop)=10になる
            if n >= len(loop):
                # nの値が10に達したら次の値は11でなく0にする、これでループがぐるぐる回る
                n = 0
        # 途中でマウスボタンが押されるとxの値は-1すなわちx<0になる
        if x<0:
            # インクリメントを停止する、ルーレットが止まる
            n = n
        # 画面の再描画、while構文で始めてpygame.display.update()で結ぶのがPygameGUIの基本
        pygame.display.update()
        # ルーレットの速度の調整、毎秒20歯に設定
        pygame.time.Clock().tick(20)

# ここまでの行は関数main()の定義、最後の行で関数main()を実行
main()

以上。

9.4.Windowsにくじびきアプリを移植する

移植というのは大げさだが、このプログラムをWindows用アプリとしてコンパイルすることは簡単だ。ただしWindowsにはdroidsansjapaneseフォントがないのでフォントだけ差し替えなければならない。
そこでWindowsにインストールされている日本語フォントにどんなものがあるか調べてみる。
Windows PowerShellを開いて以下を入力。
python
Pythonが起動し、対話モードになる。プロンプトに続けて以下を入力。
import pygame
pygame.font.get_fonts()
インストール済み使用可能なフォントの一覧を表示する。今回はmeiryomeiryomeiryouimeiryouiitalicを使うことにする。いわゆるメイリオフォントだ。
上記スクリプトの17行目
font = pygame.font.SysFont('droidsansjapanese',50)
これを以下の文章に差し替える。
font = pygame.font.SysFont('meiryomeiryomeiryouimeiryouiitalic',50)
これでWindows上でも漢字で表示される。なお、漢字の前に追記していたuの字はWindows環境の場合、あってもなくてもどっちでもいい。
一応、差し替え後のスクリプト全文を掲載する。
#!/usr/bin/env python
# -*- coding:utf-8 -*-
import pygame
from pygame.locals import *
import time
import sys
pygame.init()
screen = pygame.display.set_mode((300,200))
pygame.display.set_caption('くじびき')
clock = pygame.time.Clock()
loop=['茨城','栃木','群馬','埼玉','千葉','東京','神奈川','山梨','長野','新潟']

def main():
    n = 0
    x = -1
    while True:
        screen.fill((255,255,255))
        font = pygame.font.SysFont('meiryomeiryomeiryouimeiryouiitalic',50)
        text = font.render(loop[n], True, (0,0,0))
        textrect = text.get_rect()
        textrect.center = (300/2),(200/2)
        screen.blit(text, textrect)
        for event in pygame.event.get():
            if event.type == pygame.QUIT or (event.type == KEYDOWN and event.key == K_q):
                pygame.quit()
                sys.exit()
            if event.type == pygame.MOUSEBUTTONDOWN:
                x = x*-1
        if x>0:
            n = n+1
            if n >= len(loop):
                n = 0
        if x<0:
            n = n
        pygame.display.update()
        clock.tick(20)

main()

以上。
前述の手続きに従ってビルドすればWindows用.exeファイルに変換される。

9.5.外部ファイルにリストを置く

上記のプログラムだと、人名/県名を入れ替えるたびにスクリプトから書き直さなければならない。たいしたプログラムではないが、外部のテキストファイルにリストを記述し、それを利用するパターンで書き直してみた。
まず「名前リスト.txt」というテキストファイルを用意し、
茨城,栃木,群馬,埼玉,千葉,東京,神奈川,山梨,長野,新潟
というようにカンマ区切りでルーレットの項目を記述する。
その上でスクリプトを以下のように修正。Windowsは2バイト文字に対する制限がゆるゆるなのでファイル名にも普通に漢字ひらがなが使える。
#!/usr/bin/env python
# -*- coding:utf-8 -*-
import pygame
from pygame.locals import *
import time
import sys
import os
pygame.init()
screen = pygame.display.set_mode((300,200))
pygame.display.set_caption('くじびき')
clock = pygame.time.Clock()

# 名前リスト.txtを開いて中身を変数namesに転記
names = open('名前リスト.txt','r')
# for構文でnamesの項目をひとつひとつ読み出す
for name in names:
    # カンマ区切りで分割してloopリストに取り込む。
    loop = name.split(',')

def main():
    n = 0
    x = -1
    while True:
        screen.fill((255,255,255))
        font = pygame.font.SysFont('meiryomeiryomeiryouimeiryouiitalic',50)
        text = font.render(loop[n], True, (0,0,0))
        textrect = text.get_rect()
        textrect.center = (300/2),(200/2)
        screen.blit(text, textrect)
        for event in pygame.event.get():
            if event.type == pygame.QUIT or (event.type == KEYDOWN and event.key == K_q):
                names.close()
                pygame.quit()
                sys.exit()
            if event.type == pygame.MOUSEBUTTONDOWN:
                x = x*-1
        if x>0:
            n = n+1
            if n >= len(loop):
                n = 0
        if x<0:
            n = n
        pygame.display.update()
        clock.tick(20)

main()

以上。
前述の手続きに従ってビルドすればWindows用.exeファイルに変換される。完成した.exeファイルの隣に名前リスト.txtを置くのを忘れずに。

10.Tkinterを使ってみる

10.1.Tkinterを使ってデジタル時計を作る

以前Pygameを使ってデジタル時計を作った。しかし実際に動かしてみると発熱が大きい。PygameモジュールはCPUへの負荷が大きいらしくCPU占有率もすぐに80%を超える。やはりPygameはゲーム用モジュールなのか。そこで動作の軽そうなもうひとつのGUI用モジュール、Tkinterを使ってデジタル時計を作り直してみた。以下にサンプルスクリプトを掲載する。素人が書くスクリプトである。おそらく冗長でお手本としては不適切だと思う。

新規pyファイルを作り、以下を記述する。
# -*- coding:utf-8 -*-

#Tkinterとdatetimeをインポートする
import Tkinter
import datetime

#メイン画面rootを生成、フォントサイズを12ポイントに設定
root = Tkinter.Tk()
root.option_add('*font','12')

#ラベルを生成、textに現在の年月日時分秒を代入、pack()で画面に配置
label = Tkinter.Label(text=datetime.datetime.now().strftime('%Y/%m/%d %H:%M:%S'))
label.pack()

#関数timecount()を定義
def timecount():
    #Tkinterは勝手に画面を更新してくれないのでroot.after()で強制的に画面更新
    root.after(1000,timecount)
    #1000ミリ秒(1秒)ごとにtextの中の年月日時分秒を書き換え
    label.configure(text=datetime.datetime.now().strftime('%Y/%m/%d %H:%M:%S'))

#関数timecount()を実行
timecount()
#root = Tkinter.Tk()で始めてroot.mainloop()で結ぶのがTkinterGUIの基本
root.mainloop()
exit()

pyファイルに名前をつけて保存する。tkclock.pyとでもしておこうか。

■Pythonの決まり事
Pygameの項でも書いたが
root =
label =
のように=(イコール)の前にある文字列はたいてい変数だ。これはユーザーが勝手に定義しているもので別の文字列に差し替え可能だ。ややこしい言い方をすると変数にクラスを代入してインスタンス化している作業ということになるらしい。とにかくただの変数なのでrootという言い方が気に入らなければ別の言い方に変更してかまわない。

10.2.TkinterとPygameを使って音楽再生アプリを作る

mp3ファイルを再生するためにはPygame.mixerを使わなければならない。以前Pygameを使ってmp3ファイルを再生したが、今回はそのGUIをTkinterで作る。PlayボタンとStopボタンを備えたGUIとする。
新規pyファイルを作り、以下を記述する。
# -*- coding:utf-8 -*-

#TkinterとPygame.mixerをインポートする
import Tkinter
import pygame.mixer

root = Tkinter.Tk()
root.option_add('*font','12')

#関数play()を定義
def play():
    #mixerモジュールの初期化
    pygame.mixer.init()
    #音楽ファイルの読み込み
    pygame.mixer.music.load("ファイル名.mp3")
    #音楽再生
    pygame.mixer.music.play(1)

#関数stop()を定義
def stop():
    #再生の終了
    pygame.mixer.music.stop()

#Playボタンを生成、コマンドに関数playを割り当て、pack()で画面配置
button = Tkinter.Button(text='Play',command=play)
button.pack()
#Stopボタンを生成、コマンドに関数stopを割り当て、pack()で画面配置
button = Tkinter.Button(text='Stop',command=stop)
button.pack()

root.mainloop()
exit()

pyファイルに名前をつけて保存する。playmp3.pyとでもしておこうか。
playmp3.pyと同じ場所にmp3ファイルも置いておく。
$ python playmp3.py
で起動すると、PlayボタンとStopボタンだけの画面が現れる。Playボタンを押せば音楽を再生し、Stopボタンを押せば音楽を終了する。

10.3.上記ふたつのスクリプトを合体させて目覚まし時計を作る

新規pyファイルを作り、以下を記述する。
# -*- coding:utf-8 -*-

#TkinterとdatetimeとPygame.mixerをインポートする
import tkinter
import datetime
import pygame.mixer

#メイン画面rootを生成、フォントサイズを12ポイントに設定
root = tkinter.Tk()
root.option_add('*font','12')

#関数play()を定義
def play():
    #mixerモジュールの初期化
    pygame.mixer.init()
    #音楽ファイルの読み込み
    pygame.mixer.music.load("ファイル名.mp3")
    #音楽再生
    pygame.mixer.music.play(1)

#関数stop()を定義
def stop():
    #再生の終了
    pygame.mixer.music.stop()

#ラベルを生成、textに現在の年月日時分秒を代入、pack()で画面に配置
label = tkinter.Label(text=datetime.datetime.now().strftime('%Y/%m/%d %H:%M:%S'))
label.pack()
#Playボタンを生成、コマンドに関数playを割り当て、pack()で画面配置
button = tkinter.Button(text='Play',command=play)
button.pack()
#Stopボタンを生成、コマンドに関数stopを割り当て、pack()で画面配置
button = tkinter.Button(text='Stop',command=stop)
button.pack()

#関数timecount()を定義
def timecount():
    #Tkinterは勝手に画面を更新してくれないのでroot.after()で強制的に画面更新
    root.after(1000,timecount)
    #1000ミリ秒(1秒)ごとにtextの中の年月日時分秒を書き換え
    label.configure(text=datetime.datetime.now().strftime('%Y/%m/%d %H:%M:%S'))
    #if構文、もし現在時刻が06時00分00秒なら関数play()を実行して音楽再生
    if datetime.datetime.now().strftime('%H%M%S') == '060000':
        play()

#関数timecount()を実行
timecount()
#root = tkinter.Tk()で始めてroot.mainloop()で結ぶのがTkinterGUIの基本
root.mainloop()
exit()

pyファイルに名前をつけて保存する。今回はmezamashi.pyとする。
if構文に続く'060000'=06時00分00秒の値を書き換えれば目覚まし時刻のセットができる。時刻セットのためにはスクリプトから書き直さなければならないが、まあ最初はこんなもんから。
今回はPython3の記法に従って記述してみた。すなわちTkinterでなくtkinterと小文字で書き出してみた。ラズベリーパイでこれを実行する際は
$ python3 mezamashi.py
と、あえてpython3であることを指定しないとうまく動かない。さきほど、WindowsPCにはPython3.6.4をインストールした。なのでWindowsPC上ではPython3に従った記法でないとうまく動かない。しかし、WindowsPCで上記pyファイルを実行する時は、コマンドプロンプトに
python mezamashi.py
と打ち込む。"3"はいらない。

10.4.ttkを使ってみる

ttkはTkinterの機能を拡張するモジュールだ。Pythonのパッケージに最初から含まれているものなので、後から何かをインストールすることなく、だたimportで呼び出すだけですぐに使える。しかし、その呼び出し方がPython3とPython2で異なるのでややこしい。

■Python3でのttkの呼び出し方
import tkinter
from tkinter import ttk

■Python2でのttkの呼び出し方
import Tkinter
import ttk

なぜ異なるのか。Tkinteおよびttkのモジュールとしての作りこみに起因するらしい。詳しいことはわからない。(理解できない)
ttkを使ってみることに意義はある。Notebookというウィジェットを使うとタブつきのウィンドウを生成できるのである。先ほどの目覚まし時計アプリにttk.Notebookを追記して、アラーム時刻をセットするウィンドウを追加する。

# -*- coding:utf-8 -*-

import tkinter
from tkinter import ttk
import datetime
import pygame.mixer

#まずはalarmtimeという変数を用意する
alarmtime = "060000"

root = tkinter.Tk()
root.option_add('*font','12')

#ttk.Notebookを使ってタブ1とタブ2を作る
note = ttk.Notebook(root)
tab1 = ttk.Frame(note)
tab2 = ttk.Frame(note)
note.add(tab1,text="Alarm Clock")
note.add(tab2,text="Set Alarm")
note.pack()

def play():
    pygame.mixer.init()
    pygame.mixer.music.load("ファイル名.mp3")
    pygame.mixer.music.play(1)

def stop():
    pygame.mixer.music.stop()

label = tkinter.Label(tab1,text=datetime.datetime.now().strftime('%Y/%m/%d %H:%M:%S'))
label.pack()
button = tkinter.Button(tab1,text='Play',command=play)
button.pack()
button = tkinter.Button(tab1,text='Stop',command=stop)
button.pack()

#タブ2にエントリー(1行入力ボックス)を置く
EditBox = tkinter.Entry(tab2)
EditBox.insert(tkinter.END,alarmtime)
EditBox.pack()

def timecount():
    root.after(1000,timecount)
    label.configure(text=datetime.datetime.now().strftime('%Y/%m/%d %H:%M:%S'))
    #エントリーの6桁の数字を取得してアラーム時刻を設定する
    alarmtime = EditBox.get()
    if datetime.datetime.now().strftime('%H%M%S') == alarmtime:
        play()

timecount()
root.mainloop()
exit()

10.5.threadを使ってみる

この目覚まし時計を改良して、鳴り始めは小さな音で演奏し、徐々に音量が大きくなる仕様に変更したい。そこで関数tone()を定義し、10秒ごとに段々音量が大きくなる仕組みを追記した。18段階、3分で最大音量に達する。
pygame.mixer.music.set_volume(引数)は0~1を引数に取り、0で最小、1で最大音量となる。
def tone():
    global volume_up
    for i in range(18):
        i+=1
        if volume_up is False:
           
#音量を元に戻す
            pygame.mixer.music.set_volume(1)
            break
        else:
            #音量を10秒ごとに18段階上げる
            pygame.mixer.music.set_volume(i/18)
            time.sleep(10)
Stopボタンが押されるとグローバル変数volume_upがFalseになり、for文を抜ける→演奏をストップする という仕組みとする予定だったが、for文実行中はパソコン自体がビジーになり、マウス入力すら受け付けないことを知った。これではStopボタンを押せない。for文を止めるボタンを作りたい。
そこでthreadというライブラリを使ってみる。マルチスレッド(並列処理)を実現するものだ。for文の処理を本体と並行して走るマルチスレッドとすることで、for文実行中もStopボタンが有効になる。
前述のプログラムを以下のように書き換えた。

# -*- coding:utf-8 -*-

import tkinter
from tkinter import ttk
import datetime
import pygame.mixer
import threading
import time

alarmtime = "060000"
#グローバル変数volume_upを初期化
volume_up = False


root = tkinter.Tk()
root.option_add('*font','12')

note = ttk.Notebook(root)
tab1 = ttk.Frame(note)
tab2 = ttk.Frame(note)
note.add(tab1,text="Alarm Clock")
note.add(tab2,text="Set Alarm")
note.pack()


def play():
    global volume_up
    pygame.mixer.init()
    pygame.mixer.music.load("ファイル名.mp3")
    pygame.mixer.music.play(1)
    volume_up = True
    #tone()関数を並列して走るマルチスレッドとして実行する

    thread = threading.Thread(target=tone)
    thread.start()


def tone():
    global volume_up
    for i in range(18):
        i+=1
        if volume_up is False:
            pygame.mixer.music.set_volume(1)
            break
        else:
            pygame.mixer.music.set_volume(i/18)
            time.sleep(10)

def stop():
    global volume_up
    volume_up = False
    pygame.mixer.music.stop()

label = tkinter.Label(tab1,text=datetime.datetime.now().strftime('%Y/%m/%d %H:%M:%S'))
label.pack()
button = tkinter.Button(tab1,text='Play',command=play)
button.pack()
button = tkinter.Button(tab1,text='Stop',command=stop)
button.pack()

EditBox = tkinter.Entry(tab2)
EditBox.insert(tkinter.END,alarmtime)
EditBox.pack()

def timecount():
    root.after(1000,timecount)
    label.configure(text=datetime.datetime.now().strftime('%Y/%m/%d %H:%M:%S'))
    alarmtime = EditBox.get()
    if datetime.datetime.now().strftime('%H%M%S') == alarmtime:
        play()

timecount()
root.mainloop()
exit()

これで徐々に音量を上げる優しい目覚まし時計が完成した。演奏時間が3分未満の曲の場合、最大音量に達する前に演奏が終了するので注意。

10.6.unixtimeを使ってみる

3分かけて最大音量に達する目覚まし時計ができたわけだが、やはり3分では起きられない。せめて10分鳴らし続けたい。しかし一般的な楽曲はたいてい3~4分で終わってしまう。ならばリピート演奏して10分以上鳴り続ける仕組みを作ってしまおう。
時間の計算にunixtimeを利用する。unixtimeとは過去のある時点から1秒ずつ加算し続けている数値だ。ただしこの仕組みが利用できるのはPytgon3.3以上のdatetimeモジュールからとなる。
仕組みはこうだ。演奏終了時にイベントキューを発生させる。イベントキューはpygameを代表する機能、KEYDOWNやMOUSEBUTTONDOWNなどさまざまなアクションにイベントを発生させ、それを次のアクションに関連づけられる。
イベントキューを待ち受ける構文は以下の通り。
for event in pygame.event.get():
    if event.type == イベント名:
前回の時計アプリ、くじびきアプリの演習の中でも出てきた。一定時間おき(今回は1秒おき)にイベントを監視し、イベントキューが発生したら受け取る仕組み。
演奏終了時にイベントキューを発生させる構文は以下の通り。
pygame.mixer.music.set_endevent(イベント名
この仕組みを利用してPLAY_ENDという名前のイベントを発生させる。
一方、unixtimeを受け取る構文は以下の通り。
datetime.datetime.now().timestamp()
ここから得られる数値をint型(整数値型)に変換して600秒を足せば10分後のunixtimeとなる。

今回は演奏終了時にPLAY_ENDという名のユーザー定義イベントを設定し、そのタイミングで演奏開始時と終了時のunixtimeを比較する。10分((600秒)以内だったらリピート演奏。以上だったら曲の最後まで演奏させて終了。
ついでに、ウィンドウが小さくてつかみづらいので200x120の画面を定義した。

# -*- coding:utf-8 -*-

import tkinter
from tkinter import ttk
import datetime
import pygame.mixer
import threading
import time

alarmtime = "060000"
volume_up = False
PLAY_END = pygame.USEREVENT
unixplaytime = 0

root = tkinter.Tk()
root.option_add('*font','12')
root.geometry("200x120")

note = ttk.Notebook(root)
tab1 = ttk.Frame(note)
tab2 = ttk.Frame(note)
note.add(tab1,text="Alarm Clock")
note.add(tab2,text="Set Alarm")
note.pack()


def play():
    pygame.mixer.init()
    pygame.mixer.music.load("ファイル名.mp3")
    pygame.mixer.music.play(1)

def tone():
    global volume_up
    for i in range(60):
        i+=1
        if volume_up is False:
            pygame.mixer.music.set_volume(1)
            break
        else:
            #今回は音量を10秒ごとに60段階上げる
            pygame.mixer.music.set_volume(i/60)
            time.sleep(10)

def alarm():
    play()
    global unixplaytime
    unixplaytime=int(datetime.datetime.now().timestamp())+600
    pygame.mixer.music.set_endevent(PLAY_END)
    global volume_up
    volume_up = True
    thread = threading.Thread(target=tone)
    thread.start()

def stop():
    global unixplaytime
    pygame.mixer.music.stop()
    unixplaytime=0
    global volume_up
    volume_up = False

label = tkinter.Label(tab1,text=datetime.datetime.now().strftime('%Y/%m/%d %H:%M:%S'))
label.pack()
button = tkinter.Button(tab1,text='Play',command=play)
button.pack()
button = tkinter.Button(tab1,text='Stop',command=stop)
button.pack()

EditBox = tkinter.Entry(tab2)
EditBox.insert(tkinter.END,alarmtime)
EditBox.pack()

def timecount():
    global PLAY_END
    global unixplaytime
    pygame.init()
    root.after(1000,timecount)
    label.configure(text=datetime.datetime.now().strftime('%Y/%m/%d %H:%M:%S'))
    alarmtime = EditBox.get()
    if datetime.datetime.now().strftime('%H%M%S') == alarmtime:
        alarm()
    for event in pygame.event.get():
        if event.type == PLAY_END:
            if int(datetime.datetime.now().timestamp())<unixplaytime:
                play()
            else:
                unixplaytime=0

timecount()
root.mainloop()
exit()


10.7.複数の楽曲を日替わりで演奏する

こんなつまらない目覚ましアプリをしつこく拡張してみる。今度は複数のmp3ファイルを日替わりで演奏させてみる。Musicフォルダに複数のmp3ファイルを入れておけば、それをソートして日替わり再生する仕組みを作った。

# -*- coding:utf-8 -*-

#osモジュールをインポート
import os
import tkinter
from tkinter import ttk
import datetime
import pygame.mixer
import threading
import time


alarmtime = "060000"
volume_up = False
PLAY_END = pygame.USEREVENT
unixplaytime = 0
#Musicフォルダの中身をリスト化してmusiclistに代入
musiclist = os.listdir('Music')
#ソートする

musiclist.sort()
#先頭の曲をnextmusicに代入

n = 0
nextmusic = musiclist[n]

root = tkinter.Tk()
root.option_add('*font','12')
root.geometry("200x120")

note = ttk.Notebook(root)
tab1 = ttk.Frame(note)
tab2 = ttk.Frame(note)
note.add(tab1,text="Alarm Clock")
note.add(tab2,text="Set Alarm")
note.pack()


def play():
    pygame.mixer.init()
    #nextmusicを演奏
    pygame.mixer.music.load("Music/"+nextmusic)
    pygame.mixer.music.play(1)

def tone():
    global volume_up
    for i in range(60):
        i+=1
        if volume_up is False:
            pygame.mixer.music.set_volume(1)
            break
        else:
            pygame.mixer.music.set_volume(i/60)
            time.sleep(10)

def alarm():
    play()
    global unixplaytime
    unixplaytime=int(datetime.datetime.now().timestamp())+600
    pygame.mixer.music.set_endevent(PLAY_END)
    global volume_up
    volume_up = True
    thread = threading.Thread(target=tone)
    thread.start()

def stop():
    global unixplaytime
    pygame.mixer.music.stop()
     #stopボタンを押すたびchangemusic()を実行
    changemusic()
    unixplaytime=0
    global volume_up
    volume_up = False

#関数changemusic()を定義、nextmusicの曲を差し替える
def changemusic():
    global n
    global musiclist
    global nextmusic
    num = len(musiclist)
    n+=1
    nextmusic = musiclist[n]
    if n > (num-2):
        n = -1

label = tkinter.Label(tab1,text=datetime.datetime.now().strftime('%Y/%m/%d %H:%M:%S'))
label.pack()
button = tkinter.Button(tab1,text='Play',command=play)
button.pack()
button = tkinter.Button(tab1,text='Stop',command=stop)
button.pack()

EditBox = tkinter.Entry(tab2)
EditBox.insert(tkinter.END,alarmtime)
EditBox.pack()

def timecount():
    global PLAY_END
    global unixplaytime
    pygame.init()
    root.after(1000,timecount)
    label.configure(text=datetime.datetime.now().strftime('%Y/%m/%d %H:%M:%S'))
    alarmtime = EditBox.get()
    if datetime.datetime.now().strftime('%H%M%S') == alarmtime:
        alarm()
    for event in pygame.event.get():
        if event.type == PLAY_END:
            if int(datetime.datetime.now().timestamp())<unixplaytime:
                play()
            else:
                unixplaytime=0
   #毎日12時00分00秒に関数changemusic()を実行
    if datetime.datetime.now().strftime('%H%M%S') == '120000':

        changemusic()

timecount()
root.mainloop()
exit()

10.8.mp3ファイルだけを選別する

Musicフォルダに複数の楽曲があればそれを日替わりで演奏する仕組みができた。しかし、Musicフォルダの中にmp3ではないファイルがあってもそれをリストアップし、演奏しようと試みる。当然音が出ない。Musicフォルダ内のファイルの拡張子を読み取り、mp3ファイルだけを選別する仕組みが必要だ。
musiclist = os.listdir('Music')を消し去り、代わりに以下の青字を記述する。

# -*- coding:utf-8 -*-

import os
#globモジュールをインポート
import glob
import tkinter
from tkinter import ttk
import datetime
import pygame.mixer
import threading
import time


alarmtime = "060000"
volume_up = False
PLAY_END = pygame.USEREVENT
unixplaytime = 0
#glob.glob()関数でMusicフォルダ内mp3ファイルのリストを得る
files = glob.glob('./Music/*.mp3')
musiclist=[]
#glob.glob()関数で得られるリストはフルパス名なのでos.path.basename()でファイル名だけを取り出し、musiclistリストに登録する

for i in files:
    title = os.path.basename(i)
    musiclist.append(title)
musiclist.sort()
n = 0
nextmusic = musiclist[n]

root = tkinter.Tk()
root.option_add('*font','12')
root.geometry("200x120")

note = ttk.Notebook(root)
tab1 = ttk.Frame(note)
tab2 = ttk.Frame(note)
note.add(tab1,text="Alarm Clock")
note.add(tab2,text="Set Alarm")
note.pack()


def play():
    pygame.mixer.init()
    pygame.mixer.music.load("Music/"+nextmusic)
    pygame.mixer.music.play(1)

def tone():
    global volume_up
    for i in range(60):
        i+=1
        if volume_up is False:
            pygame.mixer.music.set_volume(1)
            break
        else:
            pygame.mixer.music.set_volume(i/60)
            time.sleep(10)

def alarm():
    play()
    global unixplaytime
    unixplaytime=int(datetime.datetime.now().timestamp())+600
    pygame.mixer.music.set_endevent(PLAY_END)
    global volume_up
    volume_up = True
    thread = threading.Thread(target=tone)
    thread.start()

def stop():
    global unixplaytime
    pygame.mixer.music.stop()
    changemusic()
    unixplaytime=0
    global volume_up
    volume_up = False

def changemusic():
    global n
    global musiclist
    global nextmusic
    num = len(musiclist)
    n+=1
    nextmusic = musiclist[n]
    if n > (num-2):
        n = -1

label = tkinter.Label(tab1,text=datetime.datetime.now().strftime('%Y/%m/%d %H:%M:%S'))
label.pack()
button = tkinter.Button(tab1,text='Play',command=play)
button.pack()
button = tkinter.Button(tab1,text='Stop',command=stop)
button.pack()

EditBox = tkinter.Entry(tab2)
EditBox.insert(tkinter.END,alarmtime)
EditBox.pack()

def timecount():
    global PLAY_END
    global unixplaytime
    pygame.init()
    root.after(1000,timecount)
    label.configure(text=datetime.datetime.now().strftime('%Y/%m/%d %H:%M:%S'))
    alarmtime = EditBox.get()
    if datetime.datetime.now().strftime('%H%M%S') == alarmtime:
        alarm()
    for event in pygame.event.get():
        if event.type == PLAY_END:
            if int(datetime.datetime.now().timestamp())<unixplaytime:
                play()
            else:
                unixplaytime=0
    if datetime.datetime.now().strftime('%H%M%S') == '120000':
        changemusic()

timecount()
root.mainloop()
exit()

11.pyaudioを使ってみる

11.1.pyaudioをインストールする

ターミナルを開いて
sudo apt install python-pyaudio
と入力。

11.2.録音アプリを作る

新規pyファイルを作成し、以下を記述。
#!/usr/bin/env python
# -*- coding: utf-8 -*-
import pyaudio
import wave
import Tkinter

CHUNK = 512
FORMAT = pyaudio.paInt16
CHANNELS = 1
RATE = 44100
RECORD_SECONDS = 10
WAVE_OUTPUT_FILENAME = "output.wav"

root = Tkinter.Tk()
root.option_add('*font','12')

p = pyaudio.PyAudio()

def record():
    stream = p.open(format=FORMAT,
                                channels=CHANNELS,
                                rate=RATE,
                                input=True,
                                frames_per_buffer=CHUNK)

    print("* recording")

    frames = []

    for i in range(0, int(RATE / CHUNK * RECORD_SECONDS)):
         data = stream.read(CHUNK)
         frames.append(data)

    print("* done recording")

    stream.stop_stream()
    stream.close()
    p.terminate()

    wf = wave.open(WAVE_OUTPUT_FILENAME, 'wb')
    wf.setnchannels(CHANNELS)
    wf.setsampwidth(p.get_sample_size(FORMAT))
    wf.setframerate(RATE)
    wf.writeframes(b''.join(frames))
    wf.close()

button = Tkinter.Button(text='Record',command=record)
button.pack()

root.mainloop()
exit()

pyファイルに名前をつけて保存する。record.pyとでもしておこうか。
$ python record.py
で起動すると"Record"と書かれたボタンが現れる。これを押すと10秒間マイクの音を録音する。録音された音は"output.wav"という音声ファイルとして書き出される。
RECORD_SECONDS = 10の値を書き換えることで録音時間を変更できる。
自分で書いたように語るがpyaudio公式Webページ*のコードサンプルを改造しTkinterでボタンをくっつけただけである。
*https://people.csail.mit.edu/hubert/pyaudio/

12.TkinterでJPEG画像を表示する

12.1.Python Imaging Library(PIL)をインストールする

TkinterにはGIF画像を表示する機能がある。しかし、そのままではJPEG画像を扱えない。TkinterでJPEG画像を表示できるようにするためPython Imaging Library(PIL)をインストールする。
ターミナルを開いて以下を入力。
$ sudo apt install python-pil.imageTk
※新たなプログラムをインストールする時はその直前にsudo apt updateでアップデートをしておくクセをつけておいた方がよい。
これでImageTk.PhotoImageコマンドが使えるようになった。

■Python3用PIL
以下のように入力するとPython3用PILもインストールできる。
$ sudo apt install python3-pil.imageTk

12.2.JPEG画像を表示するスクリプトを書く

新規pyファイルを作成し、以下を記述。
#!/usr/bin/env python
# -*- coding: utf-8 -*-
import Tkinter
from PIL import ImageTk, Image

#開始の決まり文句
root = Tkinter.Tk()

#画像をロード、インスタンス化して変数imgに代入
image = Image.open("yourfile.jpg")
img = ImageTk.PhotoImage(image)

#imgをラベルに添付、pack()で画面に配置
label = Tkinter.Label(root, image = img)
label.pack()

#結びの決まり文句
root.mainloop()

以上。
yourfile.jpgはもちろんあなたのjpg画像のファイル名に書き換える。

12.3.画面サイズを指定し、その中央にJPEG画像を置く

画面サイズを指定し、その中央にJPEG画像を配置するパターンで書いてみた。root.geometry("600x400")で画面サイズを600x400にしている。さらにタイトルも記入する。
#!/usr/bin/env python
# -*- coding: utf-8 -*-
import Tkinter
from PIL import ImageTk, Image

root = Tkinter.Tk()
root.title("Image")
root.geometry("600x400")

image = Image.open("yourfile.jpg")
img = ImageTk.PhotoImage(image)
label = Tkinter.Label(root, image = img)
label.pack(side = "bottom", fill = "both", expand = "yes")

root.mainloop()

以上。

12.4.画面枠いっぱいに広げる

画面枠いっぱいに背景を広げ、その中央にJPEG画像を配置するパターンで書いてみた。PowerPointのスライドショーのようにタイトルバーも消してしまう。タイトルバーが消えてしまうと[閉じる]ボタンも消えてしまうため、プログラムを終了できない。終了するためにはラズベリーパイの電源を抜くより方法がなくなる。それではまずいので、[Exit](終了)ボタンをつける。
#!/usr/bin/env python
# -*- coding: utf-8 -*-
import Tkinter
from PIL import ImageTk, Image

root = Tkinter.Tk()
sw, sh = root.winfo_screenwidth(), root.winfo_screenheight()
root.overrideredirect(1)
root.geometry("{0}x{1}+0+0".format(sw,sh))

image = Image.open("yourfile.jpg")
img = ImageTk.PhotoImage(image)
label = Tkinter.Label(root, image = img)
label.pack(side = "bottom", fill = "both", expand = "yes")

button = Tkinter.Button(text='Exit',command=root.destroy)
button.place(x=0,y=0)

root.mainloop()

以上。
※button.place(x=0,y=0)で[Exit]ボタンを左上角に配置している。(x=0,y=0)の値を書き換えれば好きな位置に再配置できる。

12.5.文字の省略

import Tkinter
でなく
from Tkinter import*
と書き出すことで、以下Tkinterの文字を省略できる。
#!/usr/bin/env python
# -*- coding: utf-8 -*-
from Tkinter import*
from PIL import ImageTk, Image

root = Tk()
image = Image.open("yourfile.jpg")
img = ImageTk.PhotoImage(image)

label = Label(root, image = img)
label.pack()

root.mainloop()

12.6.Tkinterを使ってGIF画像を表示する

最後にTkinterを使ってGIF画像を表示する演習をしてみる。TkinterにはもともとGIF画像を扱う機能が備わっている。なのでPILや後述するCanvasを使わなくてもGIF画像を表示できる。
import Tkinter

root = Tkinter.Tk()
img = Tkinter.PhotoImage(file = 'yourfile.gif')
label = Tkinter.Label(root, image = img)
label.pack()

root.mainloop()

12.7.Tkinterのスクリプトをオブジェクト指向で書いてみる

TkinterでGIF画像を表示するスクリプトは上記の通り、たった6行で書けてしまう。今度はこの6行をオブジェクト指向で書き直したらどうなるかという演習。3通りの書き方ができてしまうらしい。自分で書いたように語るがStack Overflowからのコピペである。
■初期化メソッドのみのパターン
import Tkinter

class MyCustomWindow(Tkinter.Frame):
    def __init__(self, parent):
        Tkinter.Frame.__init__(self, parent)
        Tkinter.Label(self, image=img).pack()
        self.pack()

root = Tkinter.Tk()
img = Tkinter.PhotoImage(file='yourfile.gif')
MyCustomWindow(root)
root.mainloop()

■初期化メソッドの中で画像読み込み
import Tkinter

class MyCustomWindow(Tkinter.Frame):
    def __init__(self, parent):
        Tkinter.Frame.__init__(self, parent)
        self.img = Tkinter.PhotoImage(file='yourfile.gif')
        Tkinter.Label(self, image=self.img).pack()
        self.pack()

def main():
    root = Tkinter.Tk()
    MyCustomWindow(root)
    root.mainloop()

if __name__ == "__main__":
    main()

■main()の中で画像読み込み
import Tkinter

class MyCustomWindow(Tkinter.Frame):
    def __init__(self, parent):
        Tkinter.Frame.__init__(self, parent)
        Tkinter.Label(self, image=img).pack()
        self.pack()

def main():
    root = Tkinter.Tk()
    global img
    img = Tkinter.PhotoImage(file='yourfile.gif')
    MyCustomWindow(root)
    root.mainloop()

if __name__ == "__main__":
    main()

12.8.WindowsにPython Imaging Library(PIL)をインストールする

前述のWindows用Python拡張パッケージまとめサイト*でPILをダウンロードできる。
*https://www.lfd.uci.edu/~gohlke/pythonlibs/
Pillowなどという名前で呼ばれている。Python3.6(32ビット)用Pillow(PIL)はPillow‑4.3.0‑cp36‑cp36m‑win32.whlである。自分の環境にあった.whlファイルをダウンロードし、インストールする。.whlファイルのインストールの仕方は前述、Pygameの項参照。

12.9.Windows上でスクリプトを実行する

Windowsで上記スクリプトを記述、実行するにあたって、ひとつ注意点がある。私のWindowsの中のPythonはPython3なのでPython3系の記法に書き換えなければならない。要するにTkinterはtkinterと小文字に書き換えなければならない。

13.Tkinter.Canvasを使ってみる

13.1.Tkinter.Canvasを使って、画像をマウスドラッグで動かす

Tkinter.CanvasはGUI画面上に円、三角、四角を描くためのキャンバスとなる画面要素(ウィジェット)の一種だ。Tkinter.Canvasの中で使えるコマンド(メソッド)は実に多種で全部説明できないし、まったく理解できない。詳しくはここ*を見るといいだろう。
*http://effbot.org/tkinterbook/canvas.htm
私はJPEG画像またはPNG画像をアイコンに見立て、そのアイコンをマウスクリックすると何らかの反応を得られたリ、そのアイコン自体をマウスドラッグで移動できたり、というような一般的なGUIのデモ画面を構築したいだけだ。そのためには.Tkinter.Canvasと前述のPython Imaging Library(PIL)の力を借りる必要があった。

13.2.画像をマウスドラッグで動かすスクリプトを書く

新規pyファイルを作成し、以下を記述。
#!/usr/bin/env python
# -*- coding: utf-8 -*-
import Tkinter
from PIL import ImageTk, Image

#400x300のキャンバスを生成
root = Tkinter.Tk()
canvas = Tkinter.Canvas(width=400, height=300)
canvas.pack(fill="both", expand=True)

#3つの変数を初期化
item_ID=None
current_x=0
current_y=0

#たい焼きの画像(アイコン)をロード、キャンバスに配置
image1 = Image.open("taiyaki.png")
img1 = ImageTk.PhotoImage(image1)
canvas.create_image(100,150, image=img1, tags="icon")

#桜餅の画像(アイコン)をロード、キャンバスに配置
image2 = Image.open("sakuramochi.png")
img2 = ImageTk.PhotoImage(image2)
canvas.create_image(300, 150, image=img2, tags="icon")

#アイコンをマウスプレスした時の処理、アイコンのIDを取得、マウスの現在位置座標取得
def on_icon_press(event):
    global item_ID
    global current_x
    global current_y
    item_ID = canvas.find_closest(event.x, event.y)[0]
    current_x = event.x
    current_y = event.y

#アイコンを離した時の処理、3つの変数を初期化
def on_icon_release(event):
    global item_ID
    global current_x
    global current_y
    item_ID = None
    current_x = 0
    current_y = 0

#マウスドラッグした時の処理、マウスの移動量とアイコンの位置座標を同期
def on_icon_motion(event):
    global item_ID
    global current_x
    global current_y
    delta_x = event.x - current_x
    delta_y = event.y - current_y
    canvas.move(item_ID, delta_x, delta_y)
    current_x = event.x
    current_y = event.y

#3種類のイベントと3種類の関数をバインド(関連付け)
canvas.tag_bind("icon", "<ButtonPress-1>", on_icon_press)
canvas.tag_bind("icon", "<ButtonRelease-1>", on_icon_release)
canvas.tag_bind("icon", "<B1-Motion>", on_icon_motion)

#結びの決まり文句
root.mainloop()

以上。
アイコン画像に見立てたのは「いらすとや」から引用したたい焼きと桜餅のPNG画像。PILをインポートすればJPEG画像のほかにPNG画像も利用可能だ。PNG画像ならば透明色を設定でき、背景を透過させられる。
canvas.create_image(...の次に来るのは画像の座標だが、画像の中央点の位置を指定する。左上ではない。
さて、とりあえずスクリプトは書けたが、item_ID、current_x、current_yの3つの変数を3回グローバル変数として宣言するのはいかにもやぼったい。そこでもっとスマートな記述をしている他人のスクリプトをまねてみた。

13.3.画像をマウスドラッグで動かすスクリプトを書く(その2)

上記スクリプトを以下の通り修正。
#!/usr/bin/env python
# -*- coding: utf-8 -*-
import Tkinter
from PIL import ImageTk, Image

root = Tkinter.Tk()
canvas = Tkinter.Canvas(width=400, height=300)
canvas.pack(fill="both", expand=True)

image1 = Image.open("taiyaki.png")
img1 = ImageTk.PhotoImage(image1)
canvas.create_image(100, 150, image=img1, tags="icon")

image2 = Image.open("sakuramochi.png")
img2 = ImageTk.PhotoImage(image2)
canvas.create_image(300, 150, image=img2, tags="icon")

def on_icon_press(event):
    current["item_ID"] = canvas.find_closest(event.x, event.y)[0]
    current["x"] = event.x
    current["y"] = event.y

def on_icon_release(event):
    current["item_ID"] = None
    current["x"] = 0
    current["y"] = 0

def on_icon_motion(event):
    delta_x = event.x - current["x"]
    delta_y = event.y - current["y"]
    canvas.move(current["item_ID"], delta_x, delta_y)
    current["x"] = event.x
    current["y"] = event.y

current = {"x": 0, "y": 0, "item_ID": None}

canvas.tag_bind("icon", "<ButtonPress-1>", on_icon_press)
canvas.tag_bind("icon", "<ButtonRelease-1>", on_icon_release)
canvas.tag_bind("icon", "<B1-Motion>", on_icon_motion)

root.mainloop()

以上。
3つの変数をディクショナリにまとめた。こういう使い方ができるのか。

14.Tkinter.Canvasを使って電卓を作る

14.1.電卓の背景画像を作る

Python-Tkinterを用いて電卓を作る演習は他の人が色々やっている。同じものを作るのでは能がない。私は背景画像をGUIに見立て、そこに透明なボタンを配置したいと考えた。背景画像を差し替えればまるでスキンを入れ替えるようにGUIのイメージを変えられる。まずは240x320のJPEG画像を用意した。これをbg.jpgという名で保存し、スクリプトから呼び出すようにした。
本来のTkinterにボタンを透明化するオプションは備わっていない。そこで(event.x, event,y)からマウスクリックした座標を取り出し、それがある領域(x1, y1, x2, y2)内にある時、特定のボタンが押されたと認識するプログラムとした。
結果としてTkinterの演習でありながら、Button()もLabel()も使わないプログラムとなった。
■bg.jpg

14.2.電卓のスクリプトを書く

スクリプトは以下の通り。たくさんのif構文で複雑に条件分岐する内容となった。多分お手本としては不適切だと思う。いいじゃないかしろうとだもの。

#!/usr/bin/env python
# -*- coding: utf8 -*-
from tkinter import*
from PIL import ImageTk, Image

def calc(event):
    global buff
    if 80<event.y<=140:
        if 0<event.x<=60: #7
            if buff[0]=='=' or buff==['0']:
                buff=['7']
            else:
                buff.append('7')
        if 60<event.x<=120: #8
            if buff[0]=='=' or buff==['0']:
                buff=['8']
            else:
                buff.append('8')
        if 120<event.x<=180: #9
            if buff[0]=='=' or buff==['0']:
                buff=['9']
            else:
                buff.append('9')
        if 180<event.x<=240: #/
            if buff[-1] in ('+','-','*','/'):
                buff[-1]='/'
            elif buff[0]=='=':
                buff.pop(0)
                buff.append('/')
            else:
                buff.append('/')
    if 140<event.y<=200:
        if 0<event.x<=60: #4
            if buff[0]=='=' or buff==['0']:
                buff=['4']
            else:
                buff.append('4')
        if 60<event.x<=120: #5
            if buff[0]=='=' or buff==['0']:
                buff=['5']
            else:
                buff.append('5')
        if 120<event.x<=180: #6
            if buff[0]=='=' or buff==['0']:
                buff=['6']
            else:
                buff.append('6')
        if 180<event.x<=240: #*
            if buff[-1] in ('+','-','*','/'):
                buff[-1]='*'
            elif buff[0]=='=':
                buff.pop(0)
                buff.append('*')
            else:
                buff.append('*')
    if 200<event.y<=260:
        if 0<event.x<=60: #1
            if buff[0]=='=' or buff==['0']:
                buff=['1']
            else:
                buff.append('1')
        if 60<event.x<=120: #2
            if buff[0]=='=' or buff==['0']:
                buff=['2']
            else:
                buff.append('2')
        if 120<event.x<=180: #3
            if buff[0]=='=' or buff==['0']:
                buff=['3']
            else:
                buff.append('3')
        if 180<event.x<=240: #-
            if buff[-1] in ('+','-','*','/'):
                buff[-1]='-'
            elif buff[0]=='=':
                buff.pop(0)
                buff.append('-')
            else:
                buff.append('-')
    if 260<event.y<=320:
        if 0<event.x<=60: #0
            if buff[0]=='=' or buff==['0']:
                buff=['0']
            elif buff[-1]=='/':
                pass
            else:
                buff.append('0')
        if 60<event.x<=120: #C
            buff = ['0']
        if 120<event.x<=180: #=
            if buff[-1] in ('+','-','*','/'):
                pass
            elif buff[0]=='=':
                pass
            else:
                ans=eval(''.join(buff))
                buff=['=']
                buff.append(str(ans))
        if 180<event.x<=240: #+
            if buff[-1] in ('+','-','*','/'):
                buff[-1]='+'
            elif buff[0]=='=':
                buff.pop(0)
                buff.append('+')
            else:
                buff.append('+')
    else:
        pass
    display = ''.join(buff)
    canvas.itemconfig(item,text=display)

root = Tk()
buff = ['0']
display = ''.join(buff)
canvas = Canvas(width=240,height=320)
image = Image.open("bg.jpg")
img = ImageTk.PhotoImage(image)
canvas.create_image(120,160,image=img)
canvas.bind("<ButtonPress-1>",calc)
item = canvas.create_text(20,20,text=display,font=('',24),fill='black',anchor=NW)

canvas.pack()
root.mainloop()

今回はfrom tkinter import*という書き出しで始めて見た。スクリプト本文中からtkinterの文字を省略できる。しかもtkinterは小文字である。Python3の記法に従った。

15.Orange Pi ZeroをWiFiに接続する

ラズベリーパイゼロがいつまでたっても買えないので勢いでオレンジパイゼロを買ってみた。同じようなもんだろと思ったが全然違った。私と同様興味本位でうっかり買ってしまった人のためにオレンジパイゼロを起動するまでの顛末を記述する。
なお、私が買ったのはOrange Pi Zero 512MBメモリモデルだ。オレンジパイゼロにはたくさんの種類があり、Orange Pi Zero 2+ H5とか拡張ボード付きタイプとか色々あるが、私が買ったのは無印のオレンジパイゼロだ。

15.1.Armbianをインストールする

オレンジパイゼロではUbuntuやAndroidが扱えると公式マニュアルには書いてあるがそれは幻想ではなかろうか。事実上使えるのはArmbianというOSだけだ。これはGUIがない。初心者には敷居が高い。
■用意するもの
・圧縮解凍ソフト7-ZIP
窓の杜*で入手できる。「圧縮・解凍」ライブラリのトップで紹介されている。圧縮・解凍を行うフリーソフト。オレンジパイゼロ用OS「Armbian」が7-ZIPで圧縮されているため、解凍にはこのソフトが必須となる。
*https://forest.watch.impress.co.jp/library/nav/genre/arc/archive_archiver.html
・SSH接続ソフトTera Term
https://ja.osdn.net/projects/ttssh2/

■Armbianを入手する
ここ*に行ってオレンジパイゼロ用Armbianのイメージファイルを入手する。
*https://www.armbian.com/orange-pi-zero/
Ubuntu server - legacy kanelのボタンをクリック。7zファイルのダウンロードが始まる。完了までに10分ほど時間がかかる。2017年10月29日時点、"Armbian_5.30_Orangepizero_Ubuntu_xenial_default_3.4.113.7z"というファイルが入手できる。Armbianはバージョンによって振る舞いが違うので注意されたい。
7-ZIPがインストールされていれば、圧縮ファイルを右クリックした時7-Zipのサブメニューが表示されるはずだ。7-Zipのサブメニューから「ここに展開」を選ぶ。同じフォルダに7zファイルが解凍され複数のファイルが展開されるが、必要なのは"Armbian_5.30_Orangepizero_Ubuntu_xenial_default_3.4.113"ディスクイメージファイルだけだ。
このディスクイメージファイルをWin32 Disk Imager 0.9.5を使ってMicroSDカードに展開する。私は16GBのMicroSDカードしか持っていないので16GBのカードを使用したが4GB以上なら大丈夫な気がする。(確認はしていない)
Armbianのディスクイメージを展開したMicroSDカードをオレンジパイゼロに挿す。さらに有線LANコネクタを接続、そして電源ON。1分ほどするとArmbianの初期動作が完了し、SSHで接続可能となる。
ここで問題。どうやってオレンジパイゼロのIPアドレスを知ることができようか。そこで前述の「ラズベリーパイのローカルIPアドレスを知る」の項を参照。2通りの手段がある。
・バッファロー製ルータを使用している場合。付属ユーティリティ"エアステーション設定ツール"の管理画面トップページ、"ネットワークサービス一覧を表示"を開くとLAN内のデバイス一覧を表示する。ここから新規に追加されたデバイスを探る。
・Windouwsの場合、Advanced IP Scannerというフリーソフトが利用可能だ。LANに接続するデバイスを検出し一覧表示する。ここから新規に追加されたデバイスを探る。

■SSH接続ソフト"Tera Term"を使う
Tera Teamを起動し、"ホスト"にローカルIPアドレス"192.168.0.X"を入力。
ユーザ名:root
パスフレーズ:1234
でOKを押す。
※バッファロー製ルータを使用の場合、IPアドレスは192.168.0.Xという数列になる。

以下のような起動画面が表示される。

You are required to change your password immediately (root enforced)
___ ____ _ _____
/ _ \ _ __ __ _ _ __ __ _ ___ | _ \(_) |__ /___ _ __ ___
| | | | '__/ _` | '_ \ / _` |/ _ \ | |_) | | / // _ \ '__/ _ \
| |_| | | | (_| | | | | (_| | __/ | __/| | / /| __/ | | (_) |
\___/|_| \__,_|_| |_|\__, |\___| |_| |_| /____\___|_| \___/
|___/崩れているが本当はASCII文字でOrangePiZeroと書かれている

Welcome to ARMBIAN 5.30 stable Ubuntu 16.04.2 LTS 3.4.113-sun8i
System load: 0.25 0.32 0.16 Up time: 5 min
Memory usage: 6 % of 494MB IP: 192.168.0.X
CPU temp: 47°C
Usage of /: 9% of 15G

[ General system configuration: armbian-config ]
Changing password for root.
(current) UNIX password:
現在のパスワード"1234"を入力してEnterを押す。カーソルは動かない
Enter new UNIX password:
新しいパスワードを入力。私は"orangepi"とした。Enterを押す。カーソルは動かない。
Retype new UNIX password:
もう一度新しいパスワードを入力。Enterを押す。カーソルは動かない。


Thank you for choosing Armbian! Support: www.armbian.com

Creating a new user account. Press <Ctrl-C> to abort

Please provide a username (eg. your forename): orangepi
新しいユーザーネームを入力。私は"orangepi"とした。Enterを押す。
Trying to add user orangepi
Adding user `orangepi' ...
Adding new group `orangepi' (1000) ...
Adding new user `orangepi' (1000) with group `orangepi' ...
Creating home directory `/home/orangepi' ...
Copying files from `/etc/skel' ...
Enter new UNIX password:
パスワードを入力。Enterを押す。カーソルは動かない。
Retype new UNIX password:
もう一度パスワードを入力。Enterを押す。カーソルは動かない。
passwd: password updated successfully
Changing the user information for orangepi
Enter the new value, or press ENTER for the default
Full Name []: Enterを押す。
Room Number []: Enterを押す。
Work Phone []: Enterを押す。
Home Phone []: Enterを押す。
Other []: Enterを押す。
Is the information correct? [Y/n]  yを押す。

Dear orangepi, your account orangepi has been created and is sudo enabled.
Please use this account for your daily work from now on.

root@orangepizero:~#
これで初回の手続き完了。
sudo apt-get updateしてみるもよし。sudo shutdown -h nowでいきなりシャットダウンするもよし。

15.2.WiFiの設定をする

日本の技適を通過していないのでWiFiの利用は自己責任になる。
プロンプトに続けて以下を入力。
nmtui
Networkmanagerの画面を開く
↓下矢印キーを叩いてActivate a coonectionを選択、Enterを押す。
ネットワークの一覧が表示されるので自宅のルータを選んで、Enterを押す。
パスワード入力画面が表示されるのでパスワードを入力、Enterを押す。
上下左右キーで<OK><Quit>を選んでEnterを押す。画面を抜ける。
■新しいIPアドレスを知る
有線LAN接続時とは異なる新しいIPアドレスが割り当てられる。新しいIPアドレスを知るには前述の「ラズベリーパイのローカルIPアドレスを知る」の項を参照。
ifconfig
を入力して、wlan0の数行下の数列を確認する。

16.Raspberry Pi Zero WHを使う

16.1.Rasupbian Stretch Liteをインストールする

Raspberry Pi Zero WHを入手したので起動するまでの顛末を記録する。Raspberry Pi Zero / Raspberry Pi Zero WHはMini HDMIx1、Micro USB タイプBメスx2という構成となっている。変換ケーブルなしにはHDMIモニタもキーボード/マウスも接続できない。しかし、後述する方法は最初からSSHによるリモート接続を行うので、モニタ、マウス、キーボードを直結する必要はない。Micro USB タイプBの電源用ケーブルさえあればいい。
Raspberry Pi Zero WHの場合、ふたつ並んだMicro USB端子のうち、外側が電源用端子で内側がUSB機器接続用端子とされている。

用意するもの
・SD Formatter 5.0
https://www.sdcard.org/jp/index.html
・Win32 Disk Imager 1.0
https://ja.osdn.net/projects/sfnet_win32diskimager/

手順は前項「Raspbian Stretch with Desktopをインストールする」を参照。RASPBIAN STRETCH WITH DESKTOP-"2017-11-29-raspbian-stretch.zip"でなくRASPBIAN STRETCH LITE"2017-11-29-raspbian-stretch-lite.zip"の方をダウンロードする。
Raspberry Pi ZeroにRasupbian Stretch with Desktopが入らないわけではなかろうが、なぜかみんなPi ZeroにはRasupbian Stretch Liteをインストールしたがるようだ。
■MicroSDカードをフォーマットする
SD Formatter 5.0を使って、MicroSDカードをフォーマットする。手順は前項の通り。
■MicroSDカードにイメージファイルを展開する
Win32 Disk Imager 1.0を使って、"2017-11-29-raspbian-stretch-lite.img"をMicroSDカードに展開する。手順は前項の通り。

OSをインストールしたMicroSDカードをRaspberry Pi Zero WHにすぐ挿したいところだが、その前に下ごしらえがある。

16.2.SSHを使えるようにする

OSをインストールしたMicroSDカードをWindowsエクスプローラーで開いてみる。bootという名のディレクトリとなっている。この中にSSHという名の空ファイルを作る。
まず、エクスプローラー;[表示]タブの中、□ファイル名拡張子にチェックを入れる。拡張子って何?って人はいるまいな。Windowsの基本から勉強したまえ。
この状態でbootフォルダの中で右クリック、[新規作成>]から何かファイルを作る。例えばテキストドキュメントを選んでみる。「新しいテキストドキュメント.txt」が作られる。ファイル名を「SSH.txt」としたのち、「.txt」を消す。「SSH」という名の拡張子のないファイルが出来上がる。不思議なことにこれでSSHが使えるようになる。

16.3.WiFiを使えるようにする

WiFi設定も事前の下ごしらえでプリセットできる。起動時に自動でWiFi接続する設定ファイルを仕込んでおくだけでいい。
まずIDLEを起動する。IDLEはWindows用Pythonに付属するエディタだ。まだPythonをインストールしていないのであれば前項「WindowsにPythonをインストールする」を参照してインストールしておく。
IDLEの場所を覚えているだろうか。スタートメニュー[P]の項目、Python3.6の中にある。
IDLE起動後、FileメニューからNew Fileを選ぶ。開かれた白紙画面に以下を入力。

country=JP
ctrl_interface=DIR=/var/run/wpa_supplicant GROUP=netdev
update_config=1
network={
                ssid="xxxxxxxx"
                psk="yyyyyyyy"
}

以上。

ssid="xxxxxxxx"のところにSSIDを記入する。psk="yyyyyyyy"のところにパスワードを記入する。SSIDって何?って人はいるまいな。WiFiの基本から勉強したまえ。

上記を記述後、ファイル名をwpa_supplicant.confとして保存する。Fileメニュー→Saveを選ぶ。続くダイアログに[ファイル名]wpa_supplicant.conf、[ファイルの種類]All files(*.*)とする。そのまま保存すると隠しフォルダの中に保存してしまうので、保存場所は変えたほうがいいだろう。デスクトップにでも保存しておいて、後でbootフォルダの中にコピーする。
さて、SSHファイルとwpa_supplicant.confファイルをbootフォルダの中に作ったら下ごしらえ完了。MicroSDカードを抜いてRaspberry Pi Zeroに挿す。電源ケーブルを挿して起動処理が完了するまで1分程度かかる。ゆっくり待ってSSHを起動しよう。

16.4.SSHで接続する

前項「1.4.ラズベリーパイをリモート接続する」を参照。Pi ZeroのローカルIPアドレスを知る手段は前項「1.3.ラズベリーパイのローカルIPアドレスを知る」を参照。
無事、Pi Zeroを起動できたが、GUIのないOSはつまらんな。

16.5.初期設定をする

ターミナルを開いて以下を入力。
$ sudo raspi-config
初期設定画面が開く。
下↓カーソルキーを3回叩いて"4 Localisation Options"をハイライトさせてEnter。またはTabキーを1回叩いて<Select>をハイライトさせてEnter。
"I1 Change Locals"をハイライトさせてEnter。次の画面が出てくるまでちょっと間がある。
下↓カーソルキー押し続け"[ ] ja_JP.UTF-8 UTF-8"をハイライトさせてスペースキー押下。[ ]にアスタリスク[*]を入れてEnter。続く画面で"ja_JP.UTF-8"をセレクトしEnter。
同様に"I2 Change Timezone"をAsia→Tokyoに。"I4 Change Wi-fi Country"を"JP Japan"にセットしてEnter。最後にトップ画面でTabキー2回叩いて<Finish>をハイライトさせてEnter。
確認画面で"Yes"をハイライトさせてEnter。自動でリブートする。

16.6.GUI環境を構築する

最初からRasupbian Stretch with Desktopを入れれば済むことだが、Raspbian Stretch Liteの上にGUI環境を構築していこうと思う。
まず新たなプログラムをインストールする前にupdateとupgradeを済ませておく。
$ sudo apt update
$ sudo apt upgrade
次に日本語フォントをインストール。
$ sudo apt install fonts-vlgothic
そしてGUI環境をインストール。
$ sudo apt install raspberrypi-ui-mods
最後にVNCをインストールして、WindowsPCからVNCでリモート接続できるようにしておこうと思う。
再び初期設定画面を開く。
$ sudo raspi-config
下↓カーソルキーを4回叩いて"5 Internet Options"をハイライトさせてEnter。"P3 VNC"を選んでEnter。続く画面で<Ok>をハイライトさせてEnter。確認画面で"Yes"をハイライトさせてEnter。自動でVNCのインストールが始まる。
$ sudo apt install tightvncserver
と入力して手動でVNCをインストールする方がたやすいかもしれない。インストール後は再起動する。

16.7.WindowsPCからVNC経由でリモート接続する

WindowsのVNCを開く。RealVNCをインストールしていない人は前項「ラズベリーパイをリモート接続する」を参照。WindowPCにVNC Viewerをインストールする手順を記述している。
WindowsのVNCを開いたら空白部分で右クリック。"New Connection"を選ぶ。"VNC Server"にローカルIPアドレス"192.168.0.X"を記入。"Name"はご自由に。"Continue"を押して、USERNAME:pi、PASSWORD:raspberry、"Remember Password"にチェックを入れ、OKボタンで接続を始める。おなじみのデスクトップ画面が表示される。起動時はUSERNAME:pi、PASSWORD:raspberryを記入させられる。

16.8.画面サイズを修正する

WindowsのVNC Viewerから接続すると初期状態では画面サイズが656x416となっており、極めて小さい。これを修正する。ラズベリーパイメニューからターミナルを開いて以下を入力。
$ sudo nano /boot/config.txt
長い文字列が表示されるが、ずーっと下まで送って、最後の行に以下のコマンドを追記する。
hdmi_ignore_edid=0xa5000080
hdmi_group=2
hdmi_mode=47
その後、Ctrl+O(上書き)を押す。Config.txtを上書きするか尋ねてくるのでEnterを押す。
Ctrl+Xを押して完了。
→hdmi_mode=47は1440x900/60HzでRaspbianのデフォルトとなっている解像度である。それ以外の解像度の設定については以下のページを参照。
https://www.raspberrypi.org/documentation/configuration/config-txt/video.md

16.9.TkinterとPygameをインストールする

Raspbian Stretch with Desktopなら最初から入っているTkinterとPygameもStretch Liteには付属しないようだ。
ターミナルを開いて以下を入力。Python2用とPython3用それぞれインストールする。
$ sudo apt install python-tk
$ sudo apt install python3-tk
$ sudo apt install python-pygame
$ sudo apt install python3-pygame
ここまでするのならいよいよ最初からRaspbian Stretch with Desktopを入れていた方がよさそうだ。

16.10.スクリーンセーバーを無効にする

こうしてインストールしたGUI付きRaspbianにはどういうわけがスクリーンセーバーが付属していてデフォルトでONになっている。眺めているのは楽しいが、無駄機能なのでOFFにする。ラズベリーパイメニューから設定>スクリーンセーバーを選ぶ。モード→[セーバーを無効にする]を選ぶ。

17.Raspberry Pi 3にローカルWebサーバーを構築する

17.1.ラズベリーパイにLAMP環境を構築する

Pythonを一通り習得したので今度はPHPを学習しようと思う。そこでPHPの学習環境となるLAMP環境をラズベリーパイに構築する。

■LAMP環境をインストールする
Apach、PHP、MySQL、phpMyAdminをまとめてインストールする。
$ sudo apt-get install apache2 php mysql-server phpmyadmin
途中phpMyAdminで使用するWebサーバをapache2とするかlighttpdとするか聞いてくる。apache2が選択されているのでそのままEnter。あるいはTabキーで<了解>を選択してEnter.
途中phpmyadmin のデータベースを dbconfig-common で設定しますか?と聞いてくる。<はい>を選択してEnter。
途中phpMyAdminのパスワードを聞いてくる。任意のパスワードを設定してEnter。
これで完了。
WindowsPCのWebブラウザからラズベリーパイのIPアドレスにアクセスしてみる。
http://192.168.0.X ※IPアドレスの値は環境により変わる。
「It works」のページが表示されたらOK。
この「It works」ページの本体となるHTMLファイルは/var/www/html/の中にある。

■PHPの動作確認をする
ターミナルを開いて以下を入力。
$ sudo nano /var/www/html/phpinfo.php
空のファイルに以下のテキストを記述して保存。
<?php phpinfo(); ?>

WindowsPC側のブラウザにて以下のURLにアクセス。
http://192.168.0.x/phpinfo.php
紫色のPHP情報画面が表示されたらOK。

■varフォルダを書き込み可能にする
そのままでは/var/www/html/フォルダの中身をいじることができない。それでは不便なので自由自在にフォルダの中身をいじれるようにしたい。
ターミナルを開いて以下を入力。
sudo chmod -R 777 /var/www/
これでvar/www/フォルダ以下をいじれる。
なお、後から追加したファイル/フォルダに対しては777のパーミッションすなわちすべてのユーザーが自由に読み書き実行できる権限が適用されない。var/www/フォルダ以下のディレクトリに後からファイル/フォルダを追加したらその都度sudo chmod -R 777 /var/www/を実行する。

17.2.MySQLを使う

ターミナルを開いて以下を入力。
$ sudo nano /etc/mysql/my.cnf

[client-server]の下に以下の一文を挿入。文字コードをUTF-8に設定する。
default-character-set=utf8mb4
その後、Ctrl+O(上書き)を押す。上書きするか尋ねてくるのでEnterを押す。
Ctrl+Xを押して完了。

ターミナルを開いて以下を入力。
$ sudo mysql
以下の応答があれば成功。
Welcome to the MariaDB monitor. Commands end with ; or \g.
Your MariaDB connection id is 15
Server version: 10.1.23-MariaDB-9+deb9u1 Raspbian 9.0

Copyright (c) 2000, 2017, Oracle, MariaDB Corporation Ab and others.

Type 'help;' or '\h' for help. Type '\c' to clear the current input statement.

MariaDB [(none)]>

MariaDBはMySQLの非商用互換データベースだ。それはともかくMariaDBがコマンド待ち受け状態で止まっている。とりあえずexit;コマンドで脱出。

MariaDB [(none)]> exit;

Bye
が返ってくる。

17.3.データベースを作成する

では以下のようなデータベースを作成してみる。
データベース名:hoge_db1
データベースのパスワード:2018hogePW

データベースにテーブルを配置する。テーブルとはExcel表のようなもので、テーブルの集まりがデータベースである。1個のデータベースの中にいくつでもテーブルを配置できる。ここでは特定のユーザーに対する特定のパスワードを記録するテーブルを用意する。パスワードはsha1形式で暗号化する。
テーブル名:members、ユーザー名:000member、パスワード:pokopen
                      
id                         name               password
Auto Increment     000member      pokopen 

こんなイメージのテーブルを作成してみる。
SQL構文ではコマンドの末尾に必ず ; を入力する。

ターミナルを開いて以下を入力。
$ sudo mysql
Welcome to the MariaDB monitor. Commands end with ; or \g.
Your MariaDB connection id is 15
Server version: 10.1.23-MariaDB-9+deb9u1 Raspbian 9.0

Copyright (c) 2000, 2017, Oracle, MariaDB Corporation Ab and others.

Type 'help;' or '\h' for help. Type '\c' to clear the current input statement.

MariaDB [(none)]> CREATE DATABASE hoge_db1;
Query OK, 1 row affected (0.00 sec)
MariaDB [(none)]> USE hoge_db1;
Database changed
MariaDB [hoge_db1]> CREATE TABLE members
-> (
-> id INT(11) NOT NULL AUTO_INCREMENT,
-> name VARCHAR(255) NOT NULL,
-> password VARCHAR(255) NOT NULL,
-> PRIMARY KEY (id)
-> );
Query OK, 0 rows affected (2.80 sec)
MariaDB [hoge_db1]> INSERT INTO members (name, password) VALUES
-> ('000member', sha1('pokopen'));
Query OK, 1 row affected (0.04 sec)
MariaDB [hoge_db1]> GRANT ALL ON hoge_db1.* to 'hoge_db1'@'localhost'
-> IDENTIFIED BY '2018hogePW';
Query OK, 0 rows affected (0.02 sec)
MariaDB [hoge_db1]> FLUSH PRIVILEGES;
Query OK, 0 rows affected (1.22 sec)
MariaDB [hoge_db1]> exit;
Bye

INSERT構文でnameとpasswordをどんどん追記していけば、複数のユーザー名とパスワードを管理するデータベースとなる。

17.4.Apache設定ファイルを編集する

画像をアップロードして一覧表示するWebサイトをローカルに構築する予定だ。しかし、その前にApache設定ファイルを修正する必要がありそうだ。デフォルトの設定を前述のPHP情報画面から確認できる。それによるとアップロード可能なファイルサイズは上限2MBにセットされているらしい。今時2MBでは到底足りない。そこでApache設定ファイルを編集してアップロード可能なファイルサイズの上限を増やす。
ターミナルを開いて以下を入力。
$ sudo nano /etc/apache2/apache2.conf
最後の行に以下を入力。赤字のところは好きな値をセットしてよい。
<Directory "/var/www">
php_value max_execution_time 100
php_value memory_limit 256M
php_value post_max_size 28M
php_value upload_max_filesize 25M
</Directory>

その後、Ctrl+O(上書き)を押す。apache2.confを上書きするか尋ねてくるのでEnterを押す。
Ctrl+Xを押して完了。そしてリブート。

■Apache設定変更を確認する。
WindowsPC側のブラウザにて先ほどのphpinfo.phpにアクセス
http://192.168.0.x/phpinfo.php

左側Local Valueと右側Master Valueの値を比べてみる。
max_execution_time
memory_limit
post_max_size
upload_max_filesize
の各項目がLocalでは修正されていることがわかるだろう。

17.5.画像アップロード用Webページを作る

前述の通り、var/www/html/フォルダの中に置いたファイルがWindowsPC側のブラウザから閲覧可能となる。ここにindex.htmlまたはindex.phpという名のファイルを置き、WindowsPC側のブラウザからラズベリーパイのローカルIPアドレスhttp://192.168.0.Xにアクセスすればindexファイルの中身が表示される仕組み。

■画像アップロード用ページを作る
var/www/html/フォルダの中に新規「空のファイル」を作り、以下を記述する。
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">

<title>Upload</title>
</head>
<body>
<form action="index.php" method="post" enctype="multipart/form-data">
<input type="file" name="fname">
<input type="submit" value="アップロード">
</form>
</body>
</html>

<?php
$temporaryname = $_FILES['fname']['tmp_name'];
$originalname = $_FILES['fname']['name'];
if (is_uploaded_file($temporaryname)){
    if(move_uploaded_file($temporaryname, './' . $originalname)){
    echo $originalname . "をアップロードしました。";
    }
}
?>

ファイル名を"index.php"として保存する。保存の際のオプションとして文字コードは"UTF-8"、改行コードは"CR+LF"としておく。
これで必要最低限の画像アップロード用ページが完成する。WindowsPC側のブラウザからhttp://192.168.0.Xにアクセスし、指示に従い画像を選択→アップロードすれば、var/www/html/フォルダの中に画像ファイルが次々転送されていく。フリーのGalleryページ用PHPファイルを転記すれば画像アップロード兼ギャラリーページを作成することも可能だろう。
画像アップロードと書いたが実は画像以外のファイルでも何でも選べば転送できてしまう。画像だけに限定したいのなら、もう1段if構文を重ねて、JPGファイル以外をふるい落とす仕組みが必要だろう。
何でも転送できるが唯一、自分と同名"index.php"という名のファイルだけは転送できない。当たり前だ。"index.php"という名のファイルを転送したかったら、アップロード用ファイルの方の名前をhogehoge.phpとか別の名前にしておけばよい。その際、8行目<form action="index.php" ~<form action="hogehoge.php" ~に変えておく。そしてWindowsPC側のブラウザからhttp://192.168.0.X/hogehoge.phpにアクセスすればよい。
よく考えたらSambaなんかインストールしなくてもこれでファイル共有というかファイル転送の仕組みが構築できたことになる。

17.6."選択されていません"を消す

<input type="file"~という通常のファイル選択フォームで書き出すと、"ファイル選択/選択されていません"といったボタン/ラベルが表示され、アップロードの手順が2ボタンに分かれる。"ファイル選択/選択されていません"を消して、1ボタンでアップロードを完了したい。
前述のPHPファイルを下記のように修正する。
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Upload</title>

<style>
@media screen { #filename { display: none; } }

.uploadbtn {
    background-color: #ff7f50;
    padding: 6px;
    border-radius: 8px;
    font-weight: bold;
    cursor: pointer;
    cursor: hand;
}

.uploadbtn:hover {
    background-color: #ff0000;
    border:2px solid #ff0000;
}
</style>

</head>
<body>
<div align="center">
<p></p>
<form action="index.php" method="post" enctype="multipart/form-data">
<label for="filename">
<span class="uploadbtn">アップロード</span>
<input type="file" name="fname" style="display:none;" onchange="this.form.submit()" id="filename">

</label>
</form>
</div>
</body>
</html>

<?php
$temporaryname = $_FILES['fname']['tmp_name'];
$originalname = $_FILES['fname']['name'];
if (is_uploaded_file($temporaryname)){
    if(move_uploaded_file($temporaryname,'./'.$originalname)){
   echo $originalname."をアップロードしました。";
    }
}
?>

<style></style>でスタイルシートを追記してアップロードボタンを修飾した。

18.リネームアプリを作る

18.1.EXIF情報から撮影日を取得する

わけあって他人からもらった大量の写真ファイルを管理することとなった。他人からもらう写真ファイルにはIMG_8984.JPGだのDSC07891.JPGだのメーカーごとに異なるファイル名がつく。このままでは管理しづらいので撮影日順にソートしやすいよう先頭に撮影日がつくファイル名にリネームしたい。窓の杜あたりに行けばそんなフリーソフトなどごまんとありそうだが、それを自分で作ってみる。
まず、EXIF情報から撮影日を取得するスクリプトを書く。PILをインポートするとEXIF情報が扱えるみたいだ。

EXIF情報から撮影日を取得するだけのスクリプト。これを記述しgetexif.pyなどというファイル名で保存しておく。getexif.pyの隣にターゲットファイルとなる"MyImageFile.JPG"を置いておいて、このスクリプトを実行すれば"MyImageFile.JPG"の撮影日情報をprintして返す。さらに撮影年月日を8桁の数列「20180814」として返す。

# -*- coding:utf-8 -*-
import os
from PIL import Image
from PIL.ExifTags import TAGS, GPSTAGS

#ファイル名とexif情報属性名を引数にとる関数を定義
def get_exif(file,field):

    #imageオブジェクトからexif情報を抽出
    img = Image.open(file)
    exif = img._getexif()
    #exif_dataリストを生成
    exif_data = []

    for id, value in exif.items():
        if TAGS.get(id) == field:
            tag = TAGS.get(id, id),value
            exif_data.extend(tag)
    #引数に与えられた属性名から値を返す
    return exif_data

#ターゲットとなる画像ファイル、ここではMyImageFile.JPGを設定
f = "MyImageFile.JPG"
#DateTimeOriginal属性の値を取得してprint
var=get_exif(f,"DateTimeOriginal")
print (var)
#上記の値を整形して年月日を示す8桁数列としてprint
fdate = var[1].replace(":", "")[:8]
print (fdate)

18.2.連番を追記するリネームアプリを作る

目標とするアプリのもうひとつの機能はファイル名の末尾に3桁の連番をつけるもの。ターゲットフォルダ内の複数のJPGファイルに対して適当なファイル名と連番を付与するスクリプト。これを記述してrenamefiles.pyなどというファイル名で保存しておく。renamefiles.pyの隣にターゲットフォルダとなる"Rename"フォルダを置いておく。このスクリプトを実行すればRenameフォルダの中にある複数のJPGファイルを「_任意のタイトル_001.JPG」というファイル名にリネームする。

# -*- coding:utf-8 -*-
import glob
import os
#/Rename/フォルダ内JPG画像のリストを生成
files = glob.glob('./Rename/*.jpg')

#リネームを実行する関数を定義
def rename_files():

    #enumerate関数で連番を生成してiに代入、初期値は1
    for i, f in enumerate(files,1):
        #リネーム前のファイル名fから拡張子を分離
        ftitle, fext = os.path.splitext(f)
        #[_任意のタイトル_+3桁の連番+拡張子]にリネームして/Rename/フォルダに保存
        os.rename(f, "./Rename/" + "_任意のタイトル_" + '{0:03d}'.format(i) + fext)

rename_files()
exit()

18.3.上記ふたつのスクリプトを合体させてリネームアプリを作る

さらにTkinterを用いてGUIを付与する。一行エントリーに「_任意のタイトル_」の文字をあらかじめ入れておき、これを編集することでタイトルを入れ替えられる仕様とする。もうひとつ一行エントリーを配置し、連番の初期値も変更できる仕様とする。
新規pyファイルを作り、以下を記述する。

# -*- coding:utf-8 -*-
import glob
import os
import tkinter
from PIL import Image
from PIL.ExifTags import TAGS, GPSTAGS

#ファイル名とexif情報属性名を引数にとる関数を定義
def get_exif(file,field):

    img = Image.open(file)
    exif = img._getexif()
    exif_data = []

    for id, value in exif.items():
        if TAGS.get(id) == field:
        tag = TAGS.get(id, id),value
        exif_data.extend(tag)
    return exif_data

files = glob.glob('./Rename/*.jpg')

#リネームを実行する関数を定義
def rename_files():

    #EditBox2から連番の初期値を取得
    num = EditBox2.get()

    for i, f in enumerate(files,int(num)):
        ftitle, fext = os.path.splitext(f)
        var=get_exif(f,"DateTimeOriginal")
        fdate = var[1].replace(":", "")[:8]
        #EditBox1から任意のタイトル(ユーザー入力値)を取得
        title = EditBox1.get()
        #[年月日を示す8桁数列+任意のタイトル+3桁の連番+拡張子]にリネーム
        os.rename(f, "./Rename/" + fdate + title + '{0:03d}'.format(i) + fext)
    exit()

#tkinterによるGUI生成
root = tkinter.Tk()
Label = tkinter.Label(text="20180102(撮影日) + 任意のタイトル + 001(連番).JPG")
Label.pack(anchor=tkinter.W, padx=5, pady=5)
EditBox1 = tkinter.Entry(width=50)
EditBox1.insert(tkinter.END,"_任意のタイトル_")
EditBox1.pack(padx=5, pady=5)
Label = tkinter.Label(text="連番の初期値")
Label.pack(side=tkinter.LEFT, padx=5, pady=5)
EditBox2 = tkinter.Entry(width=5)
EditBox2.insert(tkinter.END,"001")
EditBox2.pack(side=tkinter.LEFT, padx=5, pady=5)
Button = tkinter.Button(text='Rename',command=rename_files)
Button.pack(side=tkinter.RIGHT, padx=5, pady=5)
root.mainloop()
exit()

19.PHPでホームページ管理画面を作る

所属するチームのホームページを作ることになった。ページは数人で管理する。私もしろうとだが、メンバーはさらに輪をかけてしろうとだ。彼らにエディタとFFFTPを使って管理してねと言っても無理な話だ。彼らはホームページなんかスマホで更新できるものと思っている。そこでスマホからでも更新できるホームページ管理画面を作る。
実際のホームページはレンタルサーバ上に置くが、ラズベリーパイに構築したローカルサーバで動作確認をする。/var/www/html/の中にHTMLファイルやPHPファイルを置き、ローカルIPアドレスにアクセスすれば、あたかもレンタルサーバ上にあるかのごとくブラウザに表示される。

ただしここでひとつ注意が必要だ。Linux上に新たに作成されたファイル/フォルダはなんであれ自由に書き込みできる権限が設定されていない。/var/www/html/の中に何かファイル/フォルダを新規作成したらその都度ターミナルから
sudo chmod -R 777 /var/www/
を実行してパーミッションを再設定する

19.1ホームページ用素材リストを作る

ホームページ上には複数の写真やPDFがある。これらを別画面で一元管理したい。写真やPDFをアップロードし、一覧表示する画面を作る。前項「17.5.画像アップロード用Webページを作る」「17.6."選択されていません"を消す」で作ったアップロード画面に一覧表示のためのスクリプトを追記する。
なお、このアップローダーは4つのPHPファイルから構成される。
□sozai_list.php
□upload.php
□execute.php
□rename.php
アップロードしたファイルを特定のフォルダに収めて管理したいという人はそのためのフォルダも作っておく。フォルダの名前は「sozai」とでもしておこうか。このフォルダはあってもなくてもいい。フォルダがなければ、ルートディレクトリ(/var/www/html/)にアップロードファイルをどんどん置いていくだけだ。

■sozai_list.php
アップロードボタンとサムネイルの一覧表示領域を備えた画面。$src_dir = './';となっているが、特定のフォルダ、例えばsozaiという名のフォルダを作ってそこにアップロードファイルを収めたいという人はここを$src_dir='sozai/';に書き換える。
ところで、$src_dir = './';の変数は4つのPHPファイル全てに記述してあるので、これを書き換えるなら4か所とも書き換える必要がる。

<?php
$src_dir = './'; // ex. $src_dir='sozai/'; $src_dir='./';(current_dir)
$thumb_width = '100'; // width of thumbnails
$thumb_height = '100'; // height of thumbnails
$src_files = scandir($src_dir , 1);
$exclusions = array("html","HTML","htm","HTM","php","PHP","exe","EXE","");
$extensions = array("jpg","JPG","jpeg","JPEG","png","PNG","gif","GIF");
?>

<!DOCTYPE html>
<html>
<head>
<title>素材リスト</title>
<style>
@media screen { #filename { display: none; } }

.uploadbtn {
    background-color: skyblue;
    padding: 6px;
    border-radius: 8px;
    font-weight: bold;
    cursor: pointer;
    cursor: hand;
}

.uploadbtn:hover {
    background-color: deepskyblue;
    border:2px solid deepskyblue;
}

ul {
    list-style: none;
    margin-left: 0px;
    padding-left: 0px;
}
</style>
</head>
<body>
<h2>素材リスト</h2>
<form action="upload.php" method="post" enctype="multipart/form-data">
<label for="filename">
<span class="uploadbtn">アップロード</span>
<input type="file" name="uploadfile" onchange="this.form.submit()" id="filename";>
</label>
</form>
<BR>
<ul>
<?php

//make directry 'thums' folder
if (!is_dir($src_dir.'thumbs')) {
    mkdir($src_dir.'thumbs');
    chmod($src_dir.'thumbs', 0777);
}

//make thumnails into 'thums' folder
foreach ($src_files as $file){
   if ($file){
       $ext = pathinfo($file, PATHINFO_EXTENSION);
        if(!in_array($ext,$exclusions)){
            if(in_array($ext,$extensions)){
                list($width, $hight, $type) = getimagesize($src_dir.$file);
                switch ($type) {
                case IMAGETYPE_JPEG:
                    $baseImage = imagecreatefromjpeg($src_dir.$file);
                    break;
                case IMAGETYPE_PNG:
                    $baseImage = imagecreatefrompng($src_dir.$file);
                    break;
                case IMAGETYPE_GIF:
                    $baseImage = imagecreatefromgif($src_dir.$file);
                    break;
                default:
                }
            $image = imagecreatetruecolor($thumb_width, $thumb_height);
            imagecopyresampled($image, $baseImage, 0, 0, 0, 0, $thumb_width, $thumb_height, $width, $hight);
            imagejpeg($image , $src_dir.'thumbs/'.$file);
            imagedestroy($image);
            echo '<li><img src="'.$src_dir.'thumbs/'.$file.'">';
            }else{
            echo'<li><div style=" display:inline-block; width:100px; height:100px; background:lightgray;"></div>';
            }
        echo '<a href="rename.php?name='.$file.'"style="padding:10px;">'.$file.'</a></li>';
        echo "<HR>";
        }
    }
}
?>
</ul>
</body>
</html>

前半は前項「17.6."選択されていません"を消す」と同じ内容。form action=の転送(POST)先が異なる。今回は確認画面を挟みたいのでupload.phpに転送する。

19.2.アップロードされたファイルの確認画面を作る

■upload.php
アップロードされたファイルの確認画面、それが画像ならプレビューを表示する。必要に応じてファイル名をリネームできる。

<?php
$src_dir = './'; // ex. $src_dir='sozai/'; $src_dir='./';(current_dir)
$extensions = array("jpg","JPG","jpeg","JPEG","png","PNG","gif","GIF");

//make directry 'temporary' folder
if (!is_dir($src_dir.'temporary')) {
    mkdir($src_dir.'temporary');
    chmod($src_dir.'temporary', 0777);
}

//save uploaded files into 'temporary' folder
$temp_name = $_FILES['uploadfile']['tmp_name'];
$filename = $_FILES['uploadfile']['name'];
if (is_uploaded_file($temp_name)){
    move_uploaded_file($temp_name,$src_dir.'temporary/'.$filename);
}
?>

<!DOCTYPE html>
<html>
<head>
<title>Upload</title>
<style>
.block{
    width:360px;
    line-height:360px;
    font-size:30px;
    font-weight:bold;
    text-align:center;
    background-color:lightgray;
    color:white;
}
</style>
</head>
<body>
<?php
$ext = pathinfo($filename, PATHINFO_EXTENSION);
if(in_array($ext,$extensions)){
    echo'<img src="'.$src_dir.'temporary/'.$filename.'" height="400">';
}else{
    echo '<div class="block">No Preview</div>';
}
?>

<form action="execute.php" method="post" >
<p><input type="hidden" name="originalname" value="<?php echo $filename; ?>"></p>
<p>ファイル名:<input type="text" name="newname" value="<?php echo $filename; ?>"size=40></p>
<input type="submit" value="アップロード">
<p><a href="sozai_list.php">キャンセル</a></p>
</body>
</html>

アップロードされたファイルはいったん'temporary'フォルダに保存される。そして再びform action=で今度はexecute.phpに転送する。
$src_dir = './'; や$extensions = array();といった変数をここでも宣言している。毎回同じスクリプトを引用するならinclude関数を使えばよいではないかという指摘もあろう。もっともだ。普通はそうするだろう。includeを多用すると後から読みづらいのが嫌なのだ。

■execute.php
upload.phpからこのexecute.phpを開いて初めてアップロードが完了する。'temporary'フォルダに仮置きされたファイルは'$src_dir'フォルダに移される。

<?php
$src_dir = './'; // ex. $src_dir='sozai/'; $src_dir='./';(current_dir)
?>
<!DOCTYPE html>
<html>
<head>
</head>
<body>

<?php
// processing from upload.php
$originalname = $_POST['originalname'];
$newname = $_POST['newname'];

if(isset($originalname)){
    if($newname == ''){
        header('Location: sozai_list.php');
        exit();
   }else{
        rename($src_dir.'temporary/'.$originalname, $src_dir.$newname);
        unlink($src_dir.'temporary/'.$originalname);
        echo '<h1>アップロードされました</h1>';
    }
}

// rename processing from rename.php
$previousname = $_POST['previousname'];
$rename = $_POST['rename'];

if(isset($previousname)){
    if($rename == ''){
        header('Location: sozai_list.php');
        exit();
    }else{
        rename($src_dir.$previousname, $src_dir.$rename);
        unlink($src_dir.'thumbs/'.$previousname);
        echo '<h1>リネームされました</h1>';
    }
}

// remove processing from rename.php
$removename = $_GET['name'];

if(isset($removename)){
    unlink($src_dir.$removename);
    unlink($src_dir.'thumbs/'.$removename);
    echo '<h1>削除されました</h1>';
}
?>

<p><a href="sozai_list.php">戻る</a></p>
</body>
</html>

upload.phpを開いてプレビューを確認したあと、アップロードボタンを押さずにキャンセルすると'temporary'フォルダに仮置きファイルがゴミとして残る。これはこのプログラムの欠陥である。それを消す(unlinkする)プロセスを挟むことも可能だが、保存しようとしてやっぱりやめたファイルが何なのか気になるのであえてこの欠陥は残してある。

19.3.リネーム画面を作る

■rename.php
一覧表示のリストsozai_list.phpからファイル名をクリックするとこのrename.phpに遷移する。この画面ではファイル名のリネームと削除が行える。リネームも削除もプロセスをexecute.phpに渡して実行している。

<?php
$src_dir = './'; // ex. $src_dir='sozai/'; $src_dir='./';(current_dir)
$extensions = array("jpg","JPG","jpeg","JPEG","png","PNG","gif","GIF");
$filename = $_GET['name'];
?>
<!DOCTYPE html>
<html>
<head>
<title>Rename</title>
<style>
.block{
    width:360px;
    line-height:360px;
    font-size:30px;
    font-weight:bold;
    text-align:center;
    background-color:lightgray;
    color:white;
}
</style>
</head>
<body>
<?php
$ext = pathinfo($filename, PATHINFO_EXTENSION);
if(in_array($ext,$extensions)){
    echo '<img src="'.$src_dir.$filename.'" height="400">';
}else{
    echo '<div class="block">No Preview</div>';
}
?>

<form action="execute.php" method="post">
<input type="hidden" name="previousname" value="<?php echo $filename; ?>">
<p>ファイル名:<input type="text" name="rename" id="rename" value="<?php echo $filename; ?>" size=40></p>
<input type="submit" value="リネーム">
</form>
<?php
echo '<p><a href="execute.php?name='.$filename.'">'."削除".'</a></p>';
?>
<p><a href="<?php echo $src_dir.$filename; ?>" download="<?php echo $filename; ?>">ダウンロード</a></p>
<p><a href="sozai_list.php">戻る</a></p>
</body>
</html>

19.4.ホームページ編集画面を作る

例えばここにhoge.htmlという名のhtmlファイルがある。外部からこのhtmlファイルを編集するもっとも簡単な方法はfopen関数でhtmlファイルを直接read/writeしてしまう方法である。edit_html.phpという名でそのプログラムを書いてみる。

■edit_html.php

<?php
$filename = 'hoge.html';
$fp = fopen($filename, 'r');
$row = array();
if ($fp){
    while(!feof($fp)){
        $row[] = fgets($fp);
    }
    fclose($fp);
}
?>

<!DOCTYPE HTML>
<html>
<head>
<title>htmlエディタ</title>
</head>
<body>
<form action="execute.php" method="POST">
<p>ファイル名:<input type="text" name="filename" value="<?php echo $filename ?>"></p>
<input type="submit" value="保存"><BR>
<textarea name="content" rows="80" cols="100" wrap="soft">
<?php
foreach ($row as $i => $text){
    echo $text;
}
?>
</textarea>
<BR>
</form>
<a href="sozai_list.php">戻る</a>
</body>
</html>

hoge.htmlの中身がまるごとテキストボックスの中に流し込まれる。自由に書き換えて保存ボタンを押せばそのまま保存される仕組み。かなり横着なツール。なお、保存ボタンを押した後の処理は例によってexecute.phpに送る。execute.phpに// processing from edit_html.php以下を追記した。

■execute.php

<?php
$src_dir = './'; // ex. $src_dir='sozai/'; $src_dir='./';(current_dir)
?>
<!DOCTYPE html>
<html>
<head>
</head>
<body>

<?php
// processing from upload.php
$originalname = $_POST['originalname'];
$newname = $_POST['newname'];

if(isset($originalname)){
    if($newname == ''){
        header('Location: sozai_list.php');
        exit();
    }else{
        rename($src_dir.'temporary/'.$originalname, $src_dir.$newname);
        unlink($src_dir.'temporary/'.$originalname);
        echo '<h1>アップロードされました</h1>';
    }
}

// rename processing from rename.php
$previousname = $_POST['previousname'];
$rename = $_POST['rename'];

if(isset($previousname)){
    if($rename == ''){
        header('Location: sozai_list.php');
        exit();
    }else{
        rename($src_dir.$previousname, $src_dir.$rename);
        unlink($src_dir.'thumbs/'.$previousname);
        echo '<h1>リネームされました</h1>';
    }
}

// remove processing from rename.php
$removename = $_GET['name'];

if(isset($removename)){
unlink($src_dir.$removename);
unlink($src_dir.'thumbs/'.$removename);
echo '<h1>削除されました</h1>';
}

// processing from edit_html.php
$filename = $_POST['filename'];
$line = $_POST['content'];

if(isset($filename)){
    header('X-XSS-Protection: 0');
    $fp = fopen($filename, 'w');
    if ($fp){
        fwrite($fp, $row);
    }
    fclose($fp);
    echo $line;
}
?>

<p><a href="sozai_list.php">戻る</a></p>
</body>
</html>

Chromeブラウザはクロスサイトスクリプティングエラーを出しやすいのでheader('X-XSS-Protection: 0');でそれを抑制している。
なお、この例文はセキュリティ上の配慮が一切ない。もし本当にこのままウェブに公開してしまうと、誰でも自由にホームページの改変ができることになってしまう。実際にはデータベースを設置し、パスワードでログイン/ログアウトする仕組みを取り入れなければならない。その仕組みについてここでは詳細記述しない。

19.5.バナー管理画面を作る

さて、htmlファイルを直接編集させる上記の仕組みは案の定チームメンバーにすこぶる不評であった。もっと簡単に管理・更新ができるものでなければ使えそうにない。そこでホームページをいくつかのエリアに分割、タイトルエリア、メニューエリア、バナーエリア、テキストエリア、写真エリアと大きく5つのブロックに分け、それぞれをインラインフレームで構成した。タイトルとメニューは基本不変でいじらない。バナーとテキストと写真のエリアはチームで分担して随時更新する。バナー担当はバナーの入れ替えだけに注力すればよい。バナーと聞くだけで昭和の香り漂うイメージだが、チームメンバーはみな昭和世代なんだから仕方ない。

バナーのブロックはバナーだけで構成されるhtmlファイルである。赤字は仮のファイル名だ。

■banner.html

<!DOCTYPE html>
<html>
<head>
</head>
<body>
<a href="http://hoge.hoge" target="_top">
<img src="hoge.jpg" width="200"></a>
<BR><BR>
<a href="fuga.pdf" target="_top">
<img src="fuga.jpg" width="200"></a>
<BR><BR>
<a href="piyo.pdf" target="_top">
<img src="piyo.jpg" width="200"></a>
<BR><BR>
</body>
</html>

3段のバナー専用コラム、3つのJPEGファイルが縦に並ぶ。それぞれのJEPGファイルをクリックすると特定のウェブページにジャンプしたり、PDFファイルを開いたりする。バナーの幅は200pixelとする。

上記のhtmlを少し加工する。ファイル名の前後に改行を入れる。わかりやすく行番号を入れてみた。

  0| <!DOCTYPE html><html><head></head><body>
  1| <a href="
  2| http://hoge.hoge
  3| " target="_top"><img src="
  4| hoge.jpg
  5| " width="200"></a>
  6| <BR><BR>
  7| <a href="
  8| fuga.pdf
  9| " target="_top"><img src="
10| fuga.jpg
11| " width="200"></a>
12| <BR><BR>
13| <a href="
14| piyo.pdf
15| " target="_top"><img src="
16| piyo.jpg
17| " width="200"></a>
18| <BR><BR>
19| </body></html>

このhtmlのうち、2行目、4行目、8行目、10行目、14行目、16行目を差し替えるphpファイルを作る。

■edit_banner.php

バナー編集用のコントロールパネル。
・バナー画像選択用のアンカー
・バナーをクリックした時に開くファイル(主にPDFファイル)を選択するアンカー
 テキストボックスにURLを記入すると、そこがバナーのリンク先になる。
・2つのテキストボックスをクリアにする[消去]ボタン。
上記3つの部品で1ユニットを構成する。これが3段並ぶ。

<?php
$filename = 'banner.html';
$fp = fopen($filename, 'r');
$row = array();
if ($fp){
    while(!feof($fp)){
    $row[] = fgets($fp);
}
    fclose($fp);
}
?>

<!DOCTYPE html>
<html>
<head>
<script type="text/javascript" src="https://ajax.googleapis.com/ajax/libs/jquery/1.9.1/jquery.min.js"></script>
</head>
<script>

//消去ボタン
$(function () {
    $("#clearbtn0").click( function() {
        $("#slot0").val("");
        $("#slot1").val("");
    });
});
$(function () {
    $("#clearbtn1").click( function() {
        $("#slot2").val("");
        $("#slot3").val("");
    });
});
$(function () {
    $("#clearbtn2").click( function() {
        $("#slot4").val("");
        $("#slot5").val("");
    });
});

// 子窓を開く
function openWindow0() {
    window.open('subwindow.php?slot0', 'child', 'width=500,height=800,scrollbars=yes')
}
function openWindow1() {
    window.open('subwindow.php?slot1', 'child', 'width=500,height=800,scrollbars=yes')
}
function openWindow2() {
    window.open('subwindow.php?slot2', 'child', 'width=500,height=800,scrollbars=yes')
}
function openWindow3() {
    window.open('subwindow.php?slot3', 'child', 'width=500,height=800,scrollbars=yes')
}
function openWindow4() {
    window.open('subwindow.php?slot4', 'child', 'width=500,height=800,scrollbars=yes')
}
function openWindow5() {
    window.open('subwindow.php?slot5', 'child', 'width=500,height=800,scrollbars=yes')
}

</script>
<body>

<form name="form1" action="write_banner.php" method="POST">
<input type="submit" value="保存"><BR>
1段目<BR>
▼バナー画像 <a href="javascript:void(0)" onClick="openWindow0()">リストから選択</a><BR>
<input type="text" id="slot0" name="slot[]" value="<?php echo $row[4]; ?>" style="width:600px;"><BR>
▼リンク先  <a href="javascript:void(0)" onClick="openWindow1()">リストから選択</a><BR>
<input type="text" id="slot1" name="slot[]" value="<?php echo $row[2]; ?>" style="width:600px;"><BR>
<p><input type="button" id="clearbtn0" value="消去"></p>
<hr>
2段目<BR>
▼バナー画像 <a href="javascript:void(0)" onClick="openWindow2()">リストから選択</a><BR>
<input type="text" id="slot2" name="slot[]" value="<?php echo $row[10]; ?>" style="width:600px;"><BR>
▼リンク先  <a href="javascript:void(0)" onClick="openWindow3()">リストから選択</a><BR>
<input type="text" id="slot3" name="slot[]" value="<?php echo $row[8]; ?>" style="width:600px;"><BR>
<p><input type="button" id="clearbtn1" value="消去"></p>
<hr>
3段目<BR>
▼バナー画像 <a href="javascript:void(0)" onClick="openWindow4()">リストから選択</a><BR>
<input type="text" id="slot4" name="slot[]" value="<?php echo $row[16]; ?>" style="width:600px;"><BR>
▼リンク先  <a href="javascript:void(0)" onClick="openWindow5()">リストから選択</a><BR>
<input type="text" id="slot5" name="slot[]" value="<?php echo $row[14]; ?>" style="width:600px;"><BR>
<p><input type="button" id="clearbtn2" value="消去"></p>
<hr>

</form>
</div>
</body>

1~11行目までは前述edit_html.phpと同じ。fopenでhtmlを開いてその中身を配列$rowに代入する。
jQueryを使用するのでGoogleのライブラリを読み込む一文を挿入している。
40~58行目、window.openメソッドでsubwindow.phpを開く。subwindow.php側でクリックした値(ファイル名)を親ウィンドウに戻す。

■subwindow.php

前述のsozai_list.phpと似ているが少し違う。[アップロード]ボタンがない。

<?php
$src_dir = './'; // ex. $src_dir='sozai/'; $src_dir='./';(current_dir)
$src_files = scandir($src_dir , 1);
$exclusions = array("html","HTML","htm","HTM","php","PHP","exe","EXE","");
$extensions = array("jpg","JPG","jpeg","JPEG","png","PNG","gif","GIF");
?>

<!DOCTYPE html>
<html>
<head>
<script type="text/javascript" src="https://ajax.googleapis.com/ajax/libs/jquery/1.9.1/jquery.min.js"></script>
<style>
ul {
    list-style: none;
    margin-left: 0px;
    padding-left: 0px;
}
</style>
</head>

<script>
var str = location.search;
var get = str.substring(1,str.length);

$(function(){
    $('li').click(function(){
        if (!window.opener || window.opener.closed){
            window.alert('メインウィンドウが見当たりません。');
        }else{
            switch(get){
                case 'slot0':
                    window.opener.document.form1.slot0.value = $(this).text();
                    window.close();
                    break;
                case 'slot1':
                    window.opener.document.form1.slot1.value = $(this).text();
                    window.close();
                    break;
                case 'slot2':
                    window.opener.document.form1.slot2.value = $(this).text();
                    window.close();
                    break;
                case 'slot3':
                    window.opener.document.form1.slot3.value = $(this).text();
                    window.close();
                    break;
                case 'slot4':
                    window.opener.document.form1.slot4.value = $(this).text();
                    window.close();
                    break;
               case 'slot5':
                    window.opener.document.form1.slot5.value = $(this).text();
                    window.close();
                    break;
                default:
            }
        }
    });
});
</script>
<body>

<h2>素材リスト</h2>
<ul>
<?php
foreach ($src_files as $file){
if ($file){
    $ext = pathinfo($file, PATHINFO_EXTENSION);
        if(!in_array($ext,$exclusions)){
            if(in_array($ext,$extensions)){
                echo '<li><img src="'.$src_dir.'thumbs/'.$file.'">';
            }else{
                echo '<li><div style=" display:inline-block; width:100px; height:100px; background:lightgray;"></div>';
            }
            echo '<a href="rename.php?name='.$file.'"style="padding:10px;">'.$file.'</a></li>';
            echo "<HR>";
        }
    }
}
?>
</ul>
</body>
</html>

PHPでは<a href="subwindow.php?hoge=huga">でリンクを開き、そのリンク先に$_GET['hoge']変数を送ることができる。javascriptでも似たことを実現できるらしい。window.open('subwindow.php?slot0');でリンクを開く。リンク先には以下のスクリプトを書いておく。
<script>
var str = location.search;
var get = str.substring(1,str.length);
</script>
これで?以降の値が変数getに代入される。

edit_banner.phpでバナー画像を入れ替えたら、write_banner.phpに送って変更内容を確定する。

■write_banner.php

<?php
$filename = 'banner.html';
$fp = fopen($filename, 'r');
$row = array();
if ($fp){
    while(!feof($fp)){
       $row[] = fgets($fp);
    }
}

$slot= $_POST['slot'];
header('X-XSS-Protection: 0');
$fp = fopen($filename, 'w');
if ($fp){
    if (flock($fp, LOCK_EX)){ //ファイルロック処理(多重書き込みによる、ファイル破損回避)
        fwrite($fp, $row[0]); //<!DOCTYPE html><html><head></head><body>
        if($slot[0]){
            fwrite($fp, $row[1]); //<a href="
            fwrite($fp, $slot[1]."\n"); //http://hoge.hoge
            fwrite($fp, "\" target=\"_top\"><img src=\""."\n");
            fwrite($fp, $slot[0]."\n"); //hoge.jpg
            fwrite($fp, $row[5]); //" width="200"></a>
            fwrite($fp, "<BR><BR>"."\n");
        }else{
            fwrite($fp, $row[1]); //<a href="
            fwrite($fp, $slot[1]."\n"); //http://hoge.hoge
            fwrite($fp, ""."\n");
            fwrite($fp, $slot[0]."\n");
            fwrite($fp, $row[5]); //" width="200"></a>
            fwrite($fp, ""."\n");
        }
        if($slot[2]){
            fwrite($fp, $row[7]); //<a href="
            fwrite($fp, $slot[3]."\n"); //fuga.pdf
            fwrite($fp, "\" target=\"_top\"><img src=\""."\n");
            fwrite($fp, $slot[2]."\n"); //fuga.jpg
            fwrite($fp, $row[11]); //" width="200"></a>
            fwrite($fp, "<BR><BR>"."\n");
        }else{
            fwrite($fp, $row[7]); //<a href="
            fwrite($fp, $slot[3]."\n"); //fuga.pdf
            fwrite($fp, ""."\n");
            fwrite($fp, $slot[2]."\n");
            fwrite($fp, $row[11]); //" width="200"></a>
            fwrite($fp, ""."\n");
        }
        if($slot[4]){
            fwrite($fp, $row[13]); //<a href="
            fwrite($fp, $slot[5]."\n"); //piyo.pdf
            fwrite($fp, "\" target=\"_top\"><img src=\""."\n");
            fwrite($fp, $slot[4]."\n"); //piyo.jpg
            fwrite($fp, $row[17]); //" width="200"></a>
            fwrite($fp, "<BR><BR>"."\n");
        }else{
            fwrite($fp, $row[13]); //<a href="
            fwrite($fp, $slot[5]."\n"); //piyo.pdf
            fwrite($fp, ""."\n");
            fwrite($fp, $slot[4]."\n");
            fwrite($fp, $row[17]); //" width="200"></a>
            fwrite($fp, ""."\n");
        }
        fwrite($fp, $row[19]); //</body></html>
        flock($fp, LOCK_UN);
    }
}
fclose($fp);
?>

<!DOCTYPE html>
<html>
<head>
</head>
<body>
<h1>バナー登録されました</h1>
<p><a href="banner.html">確認</a></p>
</body>
</html>

1~11行目までは前述edit_banner.phpと同じ。fopenでhtmlを開いてその中身を配列$rowに代入する。fwrite($fp, $row[])でもって配列$rowの中身を1行ずつ書き戻すのだが、$slot[]の内容いかんによって一部差し換える。
バナーは常に3段なくてもいい。中抜けとなったら自動で詰まるようにしている。バナー画像のあるなしを判別しif構文で処理を変えている。<img src="">を空欄のまま残すとIEやEdgeで不都合な空白が生じるので画像がない時は<img src=タグごと消去するようにしている。
これでボタン操作だけでバナーの入れ替えができるようになった。

19.6.お知らせ画面を作る

バナーの入れ替えが簡単になったところで、いよいよホームページ本体の編集画面を作成する。我々のホームページ内でもっとも頻繁に更新するエリアは「お知らせ」ページだ。月に1~2度ほどイベントの案内を発信する。だが、逆に言うとそれ以外のページはまず更新の必要がない。何年も前に作ったページがそのまま放置されているが全く問題にならない。要するに「お知らせ」ページの更新さえ簡単にできれば、それ以上は望まれないということだ。

お知らせページの概要は以下の通り。

■oshirase.html

<!DOCTYPE HTML>
<html>
<head>
<title>お知らせ</title>
</head>
<body>
<h3>預かり保育実施のお知らせ</h3>
時間 8:00~18:00<BR>
春・夏・冬休み期間中も預かり保育実施します<BR>
土日祝日、年末年始は預かり保育休止します<BR>
<hr>
<h3>新入園児追加募集・二次募集のお知らせ</h3>
年少・年中・年長追加募集中<BR>
お電話で問い合わせください<BR>
<hr>
<h3>読み聞かせ読書会のお知らせ</h3>
○月○日、△△先生による読み聞かせ読書会を開催<BR>
保護者、卒園生のお母さんもご参加ください<BR>
</body>
</html>

お知らせといってもせいぜいこの程度の内容だ。ここに順次追記していく。最新のお知らせが常に先頭に表示される。過去のお知らせはそのまま残る。という日記スタイルのページとなる。
前項「19.4.ホームページ編集画面を作る」で示したedit_html.phpを使ってoshirase.htmlを開いても編集可能だが、HTMLを直接記述するスタイルは全く望まれていない。

■edit_oshirase.php

お知らせ追記用のコントロールパネル


<?php
$filename = 'oshirase.html';
$fp = fopen($filename, 'r');
$row = array();
if ($fp){
  while(!feof($fp)){
    $row[] = fgets($fp);
  }
  fclose($fp);
}
?>

<!DOCTYPE HTML>
<html>
<head>
<script type="text/javascript" src="https://ajax.googleapis.com/ajax/libs/jquery/1.9.1/jquery.min.js"></script>
</head>
<style>
textarea {
width: 600px;
height: 10em;
}
</style>

<script>
// 子窓を開く
function openWindow0() {
    window.open('subwindow.php?slot6', 'child', 'width=500,height=800,scrollbars=yes')
}
function openWindow1() {
    window.open('subwindow.php?slot7', 'child', 'width=500,height=800,scrollbars=yes')
}
function openWindow2() {
    window.open('subwindow.php?slot8', 'child', 'width=500,height=800,scrollbars=yes')
}
function openWindow3() {
    window.open('subwindow.php?slot9', 'child', 'width=500,height=800,scrollbars=yes')
}
function openWindow4() {
    window.open('subwindow.php?slot10', 'child', 'width=500,height=800,scrollbars=yes')
}
function openWindow5() {
    window.open('subwindow.php?slot11', 'child', 'width=500,height=800,scrollbars=yes')
}
</script>
<body style="float: left">
<form name="form2" id="fwrite" action="write_oshirase.php" method="POST">
<input type="button" id="submit-btn" value="保存"><BR>

<textarea name="zenhan" style="display:none;">
<?php
foreach ($row as $i => $text){
    if ($i <= 5){
      echo $text;
    }
}
?>
</textarea >
<table border="1" cellspacing="0">
<tr>
<td>
▼タイトル
</td>
</tr>
<tr>
<td>
<input type="text" name="title" id="title" value="" style="font-size:1em;width:600px;">
</td>
</tr>
<tr>
<td>
▼本文1
</td>
</tr>
<tr>
<td>
<textarea name="honbun1"></textarea>
</td>
</tr>
<tr>
<td>
▼画像1 <a href="javascript:void(0)" onClick="openWindow0()">リストから選択</a>
</td>
</tr>
<tr>
<td>
<input type="text" id="slot6" name="gazo1" value="" style="font-size:1em;width:600px;">
</td>
</tr>
<tr>
<td>
▼画像1リンク先PDF <a href="javascript:void(0)" onClick="openWindow1()">リストから選択</a>
</td>
</tr>
<tr>
<td>
<input type="text" id="slot7" name="link1" value="" style="font-size:1em;width:600px;">
</td>
</tr>
<tr>
<td>
▼文字1
</td>
</tr>
<tr>
<td>
<input type="text" name="moji1" value="" style="font-size:1em;width:600px;">
</td>
</tr>
<tr>
<td>
▼文字1リンク先PDF <a href="javascript:void(0)" onClick="openWindow2()">リストから選択</a><BR>
</td>
</tr>
<tr>
<td>
<input type="text" id="slot8" name="link2" value="" style="font-size:1em;width:600px;">
</td>
</tr>
<tr>
<td>
▼本文2
</td>
</tr>
<tr>
<td>
<textarea name="honbun2"></textarea>
</td>
</tr>
<tr>
<td>
▼画像2 <a href="javascript:void(0)" onClick="openWindow3()">リストから選択</a>
</td>
</tr>
<tr>
<td>
<input type="text" id="slot9" name="gazo2" value="" style="font-size:1em;width:600px;">
</td>
</tr>
<tr>
<td>
▼画像2リンク先PDF <a href="javascript:void(0)" onClick="openWindow4()">リストから選択</a>
</td>
</tr>
<tr>
<td>
<input type="text" id="slot10" name="link3" value="" style="font-size:1em;width:600px;">
</td>
</tr>
<tr>
<td>
▼文字2
</td>
</tr>
<tr>
<td>
<input type="text" name="moji2" value="" style="font-size:1em;width:600px;">
</td>
</tr>
<tr>
<td>
▼文字2リンク先PDF <a href="javascript:void(0)" onClick="openWindow5()">リストから選択</a>
</td>
</tr>
<tr>
<td>
<input type="text" id="slot11" name="link4" value="" style="font-size:1em;width:600px;">
</td>
</tr>
<tr>
<td>
▼過去のお知らせ<BR>
</td>
</tr>
<tr>
<td>
<textarea name="kouhan">
<?php
foreach ($row as $i => $text){
if ($i > 5){
    echo $text;
    }
}
?>
</textarea>
</td>
</tr>
</table>
</form>

<script>
$(function(){
  $("#submit-btn").click(function(){
    if($("#title").val() == ""){
      alert("タイトルが未設定です");
      return;
    }
    $("#fwrite").submit();
  });
});
</script>

一般的な手法ではあるがフォームを作り、その空欄を埋めることでコンテンツを記述できるような仕組みとした。
フォームの構成は以下の通り
[前半]行番号0~5を収録、
<!DOCTYPE HTML>
<html>
<head>
<title>お知らせ</title>
</head>
<body>
までの6行を収めている。ここはチームメンバーには触って欲しくない箇所なので
style="display:none;"
のスタイルを加えて非表示にしている。
[タイトル]お知らせのタイトル部分
[本文1]お知らせの本文その1
[画像1]画像その1
[画像1のリンク先]画像をクリックした時のジャンプ先、園児募集のポスター画像をクリックしたら募集要項のPDFが表示されるというような使い方を想定している。
[文字1]1行テキスト
[文字1のリンク先]文字1のハイパーリンク先、「ここをクリック」みたいなテキストをクリックしたらイベントの詳細を記述するPDFや別URLにジャンプするという使い方を想定している。
[本文2]
[画像2]
[画像2のリンク先]
[文字2]
[文字2のリンク先]
同じフォームが2回続く、こうすることで本文の前に画像を置いたり、画像を2つ並べたりといったレイアウトの自由度が生まれると考えた。使わないフォームは空欄のまま残してよい。
[過去のお知らせ]過去のお知らせがHTMLタグ付きの状態で表示される。滅多にないが過去のお知らせを訂正したい時はここから編集する。画像をfloat にしたり、文字色を変えたり、HTMLの再編集を行うことも想定している。

[保存]ボタンを押してフォームの内容をwrite_oshirase.phpにPOSTするその前にJavaScriptによってタイトルの有無を判定し、タイトルが空欄ならば「タイトルが未設定です」のアラートを表示する。
以下の行(最後から11行)でタイトルの有無の判定を行っている。これはJavaScriptでボタンのクリックを処理する時の一般的な構文である。
<script>
$(function(){
  $("#submit-btn").click(function(){
    if($("#title").val() == ""){
      alert("タイトルが未設定です");
      return;
    }
    $("#fwrite").submit();
  });
});
</script>
ここは以下のように書き換えることもできる。結果は同じだ。色々な書き方ができるというのは覚える身にとっては不幸だ。結局両方覚えなければならない。
<script>
$(document).ready(function(){
  $("#submit-btn").on("click", function(){
    if($("#title").val() == ""){
      alert("タイトルが未設定です");
      return;
    }
    $("#fwrite").submit();
  });
});
</script>

JavaScriptはしばしばfunction()の中にfunction()を入れ子にするという変なことをする。こうすることでfunction()文をHTML内のどこに置いてもよいという状況が生まれる。

さて、小窓を開くスクリプトを追加したので、これに対応するためにsubwindow.phpの方も修正しなければならない。

■subwindow.php

<?php
$src_dir = './'; // ex. $src_dir='sozai/'; $src_dir='./';(current_dir)
$src_files = scandir($src_dir , 1);
$exclusions = array("html","HTML","htm","HTM","php","PHP","exe","EXE","");
$extensions = array("jpg","JPG","jpeg","JPEG","png","PNG","gif","GIF");
?>

<!DOCTYPE html>
<html>
<head>
<script type="text/javascript" src="https://ajax.googleapis.com/ajax/libs/jquery/1.9.1/jquery.min.js"></script>
<style>
ul {
  list-style: none;
  margin-left: 0px;
  padding-left: 0px;
}
</style>
</head>

<script>
var str = location.search;
var get = str.substring(1,str.length);

$(function(){
    $('li').click(function(){
      if (!window.opener || window.opener.closed){
        window.alert('メインウィンドウが見当たりません。');
    }else{
        switch(get){
            case 'slot0':
                window.opener.document.form1.slot0.value = $(this).text();
                window.close();
                break;
            case 'slot1':
                window.opener.document.form1.slot1.value = $(this).text();
                window.close();
                break;
            case 'slot2':
                window.opener.document.form1.slot2.value = $(this).text();
                window.close();
                break;
            case 'slot3':
                window.opener.document.form1.slot3.value = $(this).text();
                window.close();
                break;
            case 'slot4':
                window.opener.document.form1.slot4.value = $(this).text();
                window.close();
                break;
            case 'slot5':
                window.opener.document.form1.slot5.value = $(this).text();
                window.close();
                break;
            case 'slot6':
                window.opener.document.form2.slot6.value = $(this).text();
                window.close();
                 break;
            case 'slot7':
                window.opener.document.form2.slot7.value = $(this).text();
                window.close();
                break;
            case 'slot8':
                window.opener.document.form2.slot8.value = $(this).text();
                window.close();
                break;
            case 'slot9':
                window.opener.document.form2.slot9.value = $(this).text();
                window.close();
                break;
            case 'slot10':
                window.opener.document.form2.slot10.value = $(this).text();
                window.close();
                break;
            case 'slot11':
                window.opener.document.form2.slot11.value = $(this).text();
                window.close();
                break;
            default:
           }
        }
    });
});
</script>
<body>

<h2>素材リスト</h2>
<ul>
<?php
foreach ($src_files as $file){
    if ($file){
        $ext = pathinfo($file, PATHINFO_EXTENSION);
        if(!in_array($ext,$exclusions)){
            if(in_array($ext,$extensions)){
               echo '<li><img src="'.$src_dir.'thumbs/'.$file.'">';
            }else{
                echo '<li><div style=" display:inline-block; width:100px; height:100px;      background:lightgray;"></div>';
            }
                 echo '<a href="rename.php?name='.$file.'"style="padding:10px;">'.$file.'</a></li>';
                echo "<HR>";
        }
    }
}
?>
</ul>
</body>
</html>

switch文で受けるcaseの数が増えた。

■write_oshirase.php

普通、定期的に更新する日記スタイルやブログスタイルのホームページを管理・運用する時はデータベースを使う。めんどくさそうだが、後から訂正したり引用したりといった作業が発生するなら結局データベース化した方が簡単だ。
だが我々の場合後からの訂正は基本ない。それよりもレイアウトの自由度が優先する。急に大量の写真をアップしたいと言い出したり、動画を置きたいと言い出したり、想定外の要求を想定しなければならない。きっとデータベースによる管理では対応し切れない。結局都度都度HTMLやCSSでゴリゴリ手書きした方がよいということになる。そのため、やはりHTML直接編集の系は残しておきたい。
そこでフォームに入力された内容にHTMLタグを自動挿入するプログラムとした。後から訂正するためにはHTMLを直接編集しなければならないが、そもそもそのニーズはない。

<?php
header('X-XSS-Protection: 0');
$zenhan    = $_POST['zenhan'];
$title         = $_POST['title'];
$honbun1 = $_POST['honbun1'];
$gazo1      = $_POST['gazo1'];
$link1        = $_POST['link1'];
$moji1       = $_POST['moji1'];
$link2        = $_POST['link2'];
$honbun2  = $_POST['honbun2'];
$gazo2       = $_POST['gazo2'];
$link3         = $_POST['link3'];
$moji2        = $_POST['moji2'];
$link4         = $_POST['link4'];
$kouhan     = $_POST['kouhan'];
$fp = fopen('oshirase.html', 'w');

if ($fp){
    if (flock($fp, LOCK_EX)){ //ファイルロック処理(多重書き込みによる、ファイル破損回避)
        fwrite($fp, $zenhan."\n");
        if($title){fwrite($fp, "<h3>".$title."</h3>\n");}
        if($honbun1){fwrite($fp, nl2BR($honbun1, false)."<BR>\n");}
        if($gazo1){
            if($link1){fwrite($fp, "<a href=\"".$link1."\" target=\"_top\"><img src=\"".$gazo1."\"></a><BR>\n");}
            else{fwrite($fp, "<img src=\"".$gazo1."\"><BR>\n");}}
        if($moji1){
            if($link2){fwrite($fp, "<a href=\"".$link2."\" target=\"_top\">".$moji1."</a><BR>\n");}
            else{fwrite($fp, $moji1."<BR>\n");}}
        if($honbun2){fwrite($fp, nl2br($honbun2, false)."<BR>\n");}
        if($gazo2){
            if($link3){fwrite($fp, "<a href=\"".$link3."\" target=\"_top\"><img src=\"".$gazo2."\"></a><BR>\n");}
            else{fwrite($fp, "<img src=\"".$gazo2."\"><BR>\n");}}
        if($moji2){
            if($link4){fwrite($fp, "<a href=\"".$link4."\" target=\"_top\">".$moji2."</a><BR>\n");}
           else{fwrite($fp, $moji2."<BR>\n");}}

        fwrite($fp, "<hr>\n");
        fwrite($fp, $kouhan."\n");
    }
    flock($fp, LOCK_UN);
}
fclose($fp);
?>

<!DOCTYPE html>
<html>
<head>
</head>
<body>
<h1>お知らせ更新しました</h1>
<p><a href="oshirase.html">確認</a></p>
</body>
</html>

本文1のスロットに何も記載がない時、すなわち$honbun1=""の時、if(isset($honbun1)){ で条件分岐しようとするとTrueが返ってきてしまう。
$honbun1=""なのにif(isset($honbun1)){ がTrue!
本文1に記載があってもなくてもTrue!
これでは条件分岐にならないので当初if(strlen($honbun1)>1){ で分岐していた。if(strlen($honbun1)>1){ すなわち変数$honbun1の文字数が1バイト以上の時True。これなら$honbun1=""の時確実にFalseが返ってくる。
でもなんとなく素人臭いような美しいスクリプトでないような気がしていた。
調べてみたらif($honbun1){ とすれば解決することがわかった。$honbun1=""の時、if($honbun1){ はFalseを返す。$honbun1に何らかの文字が入っていればTrueを返す。なんだこんなんでよかったのか。ということで「19.5.バナー管理画面を作る」の■write_banner.phpスクリプトの方も書き直しておいた。

19.7.ローディングGIFアニメを設定する

ホームページ編集画面を運用して数日、しばしば画像のアップロードに失敗するという事案が発生した。原因を究明してみると、多くのメンバーが画像アップロード途中で画面を閉じたり別画面に遷移していることが判明した。みんな「アップロード」ボタンを押した瞬間にアップロードが完了するものだと思っているようだ。転送には時間がかかるという概念がない。Chromeの場合、ファイル転送中、左下のステータスバーに進捗度合が%で表示される。だが、我がチームメンバーは全くそれに気がついていないようだ。困ったものだ。ファイル転送時、完了までに時間がかかることを何かしらの方法で伝える必要がある。
そこでアップロードの進捗をプログレスバーで表示する方法を調べてみた。HTML5からは<progress>タグも使えるようになりプログレスバーを記述しやすくなった。
だが、やっぱりプログレスバーは敷居が高い。しろうとが簡単にマネできるものではなさそうだ。
そこでくるくる回るローディングGIFアニメを表示させることにした。sozai_list.phpを修正し、アップロードボタンを押した瞬間にローディングGIFアニメが表示されるよう改良した。

■sozai_list.php

<?php
$src_dir = './'; // ex. $src_dir='sozai/'; $src_dir='./';(current_dir)
$thumb_width = '100'; // width of thumbnails
$thumb_height = '100'; // height of thumbnails
$src_files = scandir($src_dir , 1);
$exclusions = array("html","HTML","htm","HTM","php","PHP","exe","EXE","");
$extensions = array("jpg","JPG","jpeg","JPEG","png","PNG","gif","GIF");
?>

<!DOCTYPE html>
<html>
<head>
<script type="text/javascript" src="https://ajax.googleapis.com/ajax/libs/jquery/1.9.1/jquery.min.js"></script>
<title>素材リスト</title>
<style>
    @media screen { #filename { display: none; } }

.uploadbtn {
    background-color: skyblue;
    padding: 6px;
    border-radius: 8px;
    font-weight: bold;
    cursor: pointer;
    cursor: hand;
}

.uploadbtn:hover {
    background-color: deepskyblue;
    border:2px solid deepskyblue;
}

ul {
    list-style: none;
    margin-left: 0px;
    padding-left: 0px;
}
</style>
</head>
<body>
<h2>素材リスト</h2>
<form action="upload.php" method="post" enctype="multipart/form-data">
<label for="filename">
<span class="uploadbtn">アップロード</span>
<input type="file" name="uploadfile" onchange="this.form.submit()" id="filename";>
</label>
</form>

<p id="loadgif"></p>
<BR>
<ul>
<?php

//make directry 'thums' folder
if (!is_dir($src_dir.'thumbs')) {
    mkdir($src_dir.'thumbs');
    chmod($src_dir.'thumbs', 0777);
}

//make thumnails into 'thums' folder
foreach ($src_files as $file){
    if ($file){
        $ext = pathinfo($file, PATHINFO_EXTENSION);
        if(!in_array($ext,$exclusions)){
            if(in_array($ext,$extensions)){
                list($width, $hight, $type) = getimagesize($src_dir.$file);
                switch ($type) {
                case IMAGETYPE_JPEG:
                    $baseImage = imagecreatefromjpeg($src_dir.$file);
                    break;
                case IMAGETYPE_PNG:
                    $baseImage = imagecreatefrompng($src_dir.$file);
                    break;
                case IMAGETYPE_GIF:
                    $baseImage = imagecreatefromgif($src_dir.$file);
                    break;
                default:
            }
                $image = imagecreatetruecolor($thumb_width, $thumb_height);
                imagecopyresampled($image, $baseImage, 0, 0, 0, 0, $thumb_width, $thumb_height, $width, $hight);
                imagejpeg($image , $src_dir.'thumbs/'.$file);
                imagedestroy($image);
                echo '<li><img src="'.$src_dir.'thumbs/'.$file.'">';
            }else{
                echo '<li><div style=" display:inline-block; width:100px; height:100px;             background:lightgray;"></div>';
            }
            echo '<a href="rename.php?name='.$file.'"style="padding:10px;">'.$file.'</a></li>';
            echo "<HR>";
        }
    }
}
?>
</ul>
</body>
</html>

<script>
$(function(){
   $("#filename").change(function(){

       $('#loadgif').html('<img src="./imgsrc/loadgif.gif">');
    });
});

</script>

前述のsozai_list.phpスクリプトに青字部分を追記した。
ローディングGIFアニメのファイルとしてloadgif.gifを用意した。ローディングGIFアニメはそれを作る専用のジェネレータサイトも色々あったりして誰でも簡単に作ることができる。imgsrcフォルダを作り、GIFファイルをその中に入れた。ルートディレクトリvar/www/html/に置いてしまうと、素材リストの中に表示されてしまう。それでもいいっちゃいいかもしれないが、今回はimgsrcフォルダを新たに作り、その中に隠した。だから新規に追加したスクリプトも'<img src="./imgsrc/loadgif.gif">'となっている。ここで自慢気に紹介するのもはばかれるほど簡単なスクリプトだ。
これでひとまずアップロード未完了問題は解決しそうだ。
だが、何かコレジャナイ感が漂う。こんなかっこ悪い表示の仕方でよいのか。もっとおしゃれな画面にできないものか。そこでここ*(https://webllica.com/jquery-now-loading/)のスクリプトを丸パクリしてsozai_list.phpを書き直した。

■sozai_list.php

<?php
$src_dir = './'; // ex. $src_dir='sozai/'; $src_dir='./';(current_dir)
$thumb_width = '100'; // width of thumbnails
$thumb_height = '100'; // height of thumbnails
$src_files = scandir($src_dir , 1);
$exclusions = array("html","HTML","htm","HTM","php","PHP","exe","EXE","");
$extensions = array("jpg","JPG","jpeg","JPEG","png","PNG","gif","GIF");
?>

<!DOCTYPE html>
<html>
<head>
<script type="text/javascript" src="https://ajax.googleapis.com/ajax/libs/jquery/1.9.1/jquery.min.js"></script>
<title>素材リスト</title>
<style>
    @media screen { #filename { display: none; } }

.uploadbtn {
    background-color: skyblue;
    padding: 6px;
    border-radius: 8px;
    font-weight: bold;
    cursor: pointer;
    cursor: hand;
}

.uploadbtn:hover {
    background-color: deepskyblue;
    border:2px solid deepskyblue;
}

ul {
    list-style: none;
    margin-left: 0px;
    padding-left: 0px;
}

#loading {
    display: table;
    width: 100%;
    height: 100%;
    position: fixed;
    top: 0;
    left: 0;
    background-color: #fff;
    opacity: 0.8;
}

#loading .loadingMsg {
    display: table-cell;
    text-align: center;
    vertical-align: middle;
    padding-top: 140px;
    background: url("./imgsrc/loadgif.gif") center center no-repeat;
}
</style>
</head>
<body>
<h2>素材リスト</h2>
<form action="upload.php" method="post" enctype="multipart/form-data">
<label for="filename">
<span class="uploadbtn">アップロード</span>
<input type="file" name="uploadfile" onchange="this.form.submit()" id="filename";>
</label>
</form>
<BR>
<ul>
<?php

//make directry 'thums' folder
if (!is_dir($src_dir.'thumbs')) {
    mkdir($src_dir.'thumbs');
    chmod($src_dir.'thumbs', 0777);
}

//make thumnails into 'thums' folder
foreach ($src_files as $file){
    if ($file){
        $ext = pathinfo($file, PATHINFO_EXTENSION);
        if(!in_array($ext,$exclusions)){
            if(in_array($ext,$extensions)){
                list($width, $hight, $type) = getimagesize($src_dir.$file);
                switch ($type) {
                case IMAGETYPE_JPEG:
                    $baseImage = imagecreatefromjpeg($src_dir.$file);
                    break;
                case IMAGETYPE_PNG:
                    $baseImage = imagecreatefrompng($src_dir.$file);
                    break;
                case IMAGETYPE_GIF:
                    $baseImage = imagecreatefromgif($src_dir.$file);
                    break;
                default:
            }
                $image = imagecreatetruecolor($thumb_width, $thumb_height);
                imagecopyresampled($image, $baseImage, 0, 0, 0, 0, $thumb_width, $thumb_height, $width, $hight);
                imagejpeg($image , $src_dir.'thumbs/'.$file);
                imagedestroy($image);
                echo '<li><img src="'.$src_dir.'thumbs/'.$file.'">';
            }else{
                echo '<li><div style=" display:inline-block; width:100px; height:100px;             background:lightgray;"></div>';
            }
            echo '<a href="rename.php?name='.$file.'"style="padding:10px;">'.$file.'</a></li>';
            echo "<HR>";
        }
    }
}
?>
</ul>
</body>
</html>

<script>
var dispMsg = "<div class='loadingMsg'>" + "ロード中" + "</div>";

$(function(){
   $("#filename").change(function(){
   $("body").append("<div id='loading'>" + dispMsg + "</div>");
    });
});
</script>

画面全体を半透明の白地で覆い、中央にGIFアニメを表示する。誰もが思い描くローディング画面である。これでちょっとかっこよくなった。くるくる回転するGIFアニメでなく5秒くらいで満タンに達する疑似プログレスバー風のGIFアニメでもいいかもしれない。本物の進捗を反映するものではないが、多分誰も本物との区別がつかない。

20.ページング処理を加える

「素材リスト」に素材を追加していたらダラダラ長くなった。適当なアイテム数ごとにページを区切りたい。特にiPadでの操作を考えると8~10アイテムくらいでページ割をした方がよさそうだ。そこで今度はページング処理に挑戦する。参考にしたHPはここ*
*https://www.sejuku.net/blog/70234

「19.1ホームページ用素材リストを作る」の項で作った「素材リスト」のスクリプトを改造する。ローディングGIFアニメをつける前のものだ。まずおさらいとして前回のsozai_list.phpをもう一度ここに貼る。

■sozai_list.php

<?php
$src_dir = './'; // ex. $src_dir='sozai/'; $src_dir='./';(current_dir)
$thumb_width = '100'; // width of thumbnails
$thumb_height = '100'; // height of thumbnails
$src_files = scandir($src_dir , 1);
$exclusions = array("html","HTML","htm","HTM","php","PHP","exe","EXE","");
$extensions = array("jpg","JPG","jpeg","JPEG","png","PNG","gif","GIF");
?>

<!DOCTYPE html>
<html>
<head>
<title>素材リスト</title>
<style>
@media screen { #filename { display: none; } }

.uploadbtn {
    background-color: skyblue;
    padding: 6px;
    border-radius: 8px;
    font-weight: bold;
    cursor: pointer;
    cursor: hand;
}

.notimage {
    display:inline-block;
    width:<?php echo $thumb_width ?>px;
    height:<?php echo $thumb_height ?>px;
    background:lightgray;
}

.uploadbtn:hover {
    background-color: deepskyblue;
    border:2px solid deepskyblue;
}

ul {
    list-style: none;
    margin-left: 0px;
    padding-left: 0px;
}
</style>
</head>
<body>
<h2>素材リスト</h2>
<form action="upload.php" method="post" enctype="multipart/form-data">
<label for="filename">
<span class="uploadbtn">アップロード</span>
<input type="file" name="uploadfile" onchange="this.form.submit()" id="filename";>
</label>
</form>
<BR>
<ul>
<?php

//make directry 'thums' folder
if (!is_dir($src_dir.'thumbs')) {
mkdir($src_dir.'thumbs');
chmod($src_dir.'thumbs', 0777);
}

//make thumnails into 'thums' folder
foreach ($src_files as $file){
    if ($file){
        $ext = pathinfo($file, PATHINFO_EXTENSION);
        if(!in_array($ext,$exclusions)){
            if(in_array($ext,$extensions)){
                list($width, $hight, $type) = getimagesize($src_dir.$file);
                switch ($type) {
                    case IMAGETYPE_JPEG:
                        $baseImage = imagecreatefromjpeg($src_dir.$file);
                        break;
                    case IMAGETYPE_PNG:
                        $baseImage = imagecreatefrompng($src_dir.$file);
                        break;
                    case IMAGETYPE_GIF:
                        $baseImage = imagecreatefromgif($src_dir.$file);
                        break;
                    default:
                }
                $image = imagecreatetruecolor($thumb_width, $thumb_height);
                imagecopyresampled($image, $baseImage, 0, 0, 0, 0,
                    $thumb_width,$thumb_height, $width, $hight);

                imagejpeg($image , $src_dir.'thumbs/'.$file);
                imagedestroy($image);
                echo '<li><img src="'.$src_dir.'thumbs/'.$file.'">';
            }else{
                echo'<li><div class="notimage"></div>';
            }
            echo '<a href="rename.php?name='.$file.'"style="padding:10px;">
                '.$file.'</a></li>';

            echo "<HR>";
        }
    }
}
?>
</ul>
</body>
</html>

といいながら、前回と全く同じではない。下から11行目、echo'<li><div style=" display:inline-block; width:100px; height:100px; background:lightgray;"></div>';の部分はあまりに1行が長ったらしいので<div class="notimage"></div>としてその中身は前半のCSSスタイルシート部分に記述した。青色文字色が変更箇所。ブラウザから開いて見た時の表示に変化はないはずだ。

20.1.素材リストをページング処理する

上記のスクリプトにページングの処理を加える。赤文字色が変更箇所。

■sozai_list.php

<?php
$src_dir = './'; // ex. $src_dir='sozai/'; $src_dir='./';(current_dir)
$thumb_width = '100'; // width of thumbnails
$thumb_height = '100'; // height of thumbnails
$src_files = scandir($src_dir , 1);
$exclusions = array("html","HTML","htm","HTM","php","PHP","exe","EXE","");
$extensions = array("jpg","JPG","jpeg","JPEG","png","PNG","gif","GIF");
$itemsPerPage=10; //1ページあたりのアイテム数
?>

<!DOCTYPE html>
<html>
<head>
<title>素材リスト</title>
<style>
@media screen { #filename { display: none; } }

.uploadbtn {
    background-color: skyblue;
    padding: 6px;
    border-radius: 8px;
    font-weight: bold;
    cursor: pointer;
    cursor: hand;
}

.notimage {
    display:inline-block;
    width:<?php echo $thumb_width ?>px;
    height:<?php echo $thumb_height ?>px;
    background:lightgray;
}

.uploadbtn:hover {
    background-color: deepskyblue;
    border:2px solid deepskyblue;
}

ul {
    list-style: none;
    margin-left: 0px;
    padding-left: 0px;
}

a {
    text-decoration: none;
}

b3 {
    font-size:1.2em;
    font-weight:bold;
}
</style>
</head>
<body>
<h2>素材リスト</h2>
<form action="upload.php" method="post" enctype="multipart/form-data">
<label for="filename">
<span class="uploadbtn">アップロード</span>
<input type="file" name="uploadfile" onchange="this.form.submit()" id="filename";>
</label>
</form>
<BR>
<ul>
<?php

//make directry 'thums' folder
if (!is_dir($src_dir.'thumbs')) {
    mkdir($src_dir.'thumbs');
    chmod($src_dir.'thumbs', 0777);
}

//make thumnails into 'thums' folder
$files = array();
foreach ($src_files as $file){
    $ext = pathinfo($file, PATHINFO_EXTENSION);
    if(!in_array($ext,$exclusions)){
        if(in_array($ext,$extensions)){
            list($width, $hight, $type) = getimagesize($src_dir.$file);
            switch ($type) {
                case IMAGETYPE_JPEG:
                    $baseImage = imagecreatefromjpeg($src_dir.$file);
                    break;
                case IMAGETYPE_PNG:
                    $baseImage = imagecreatefrompng($src_dir.$file);
                    break;
                case IMAGETYPE_GIF:
                    $baseImage = imagecreatefromgif($src_dir.$file);
                    break;
                default:
            }
            $image = imagecreatetruecolor($thumb_width, $thumb_height);
            imagecopyresampled($image, $baseImage, 0, 0, 0, 0,
                    $thumb_width, $thumb_height, $width, $hight);

            imagejpeg($image , $src_dir.'thumbs/'.$file);
            imagedestroy($image);
        }
        array_push($files, $file);
    }
}

$numPages = ceil(count($files)/$itemsPerPage);

if(!isset($_GET['page'])){
    $currentPage=1;
}else{
    $currentPage = $_GET['page'];
}

$start = ($currentPage -1) * $itemsPerPage;

for($i=$start; $i<$start + $itemsPerPage; $i++ ){
    if(isset($files[$i])&& is_file($src_dir.'thumbs/'.$files[$i])){
        echo '<li><a href="rename.php?name='.$files[$i].'">
        <img src="'.$src_dir.'thumbs/'.$files[$i].'"> '.$files[$i].'</a></li>';
        echo '<HR>';
    }
    if( isset($files[$i])&& !is_file( $src_dir.'thumbs/'.$files[$i] )) {
        echo '<li><a href="rename.php?name='.$files[$i].'">
        <div class="notimage"></div> '.$files[$i].'</a></li>';
        echo '<HR>';
    }
}

if($currentPage > 1){ // リンクをつけるかの判定
    echo '<a href=sozai_list.php?page='.($currentPage - 1).'>前へ</a>'. ' ';
} else {
    echo '前へ'. ' ';
}

for($i = 1; $i <= $numPages; $i++){
    if ($i == $currentPage ) {
        echo '<b3>'.$currentPage. '</b3> ';
    } else {
        echo '<a href=sozai_list.php?page='. $i. '>'. $i. '</a>'. ' ';
    }
}

if($currentPage < $numPages){ // リンクをつけるかの判定
    echo '<a href=sozai_list.php?page='.($currentPage + 1).'>次へ</a>'. ' ';
} else {
    echo '次へ';
}
?>
</ul>
</body>
</html>

ページング処理で必ず必要なのは全アイテム数の数を数えるプロセスだ。そのために配列$filesにリストアイテムのファイル名$fileを追加し、
$files = array();
...
array_push($files, $file);
後工程で数を数えている。
count($files)
私のリストに登録できるアイテムは"html","HTML","htm","HTM","php","PHP","exe","EXE"以外のファイルだ。もし対象アイテムを画像だけに絞るのならthumbフォルダ内のファイル数を数えるだけでもよいかも知れない。とはいえ、指定フォルダ内のファイル数を数えるプロセスだってそれなりに面倒だ。foreachでファイルをひとつひとつ探索して配列に加えて...と思ったらここ*(https://codeday.me/jp/qa/20181208/56373.html)で簡単なファイル数の数え方を紹介していた。
<?php
$fi = new FilesystemIterator(__DIR__, FilesystemIterator::SKIP_DOTS);
printf("There were %d Files", iterator_count($fi));
?>
なんと2行で数えている。__DIR__のところを指定フォルダに書き換えれば、たとえば
<?php
$fi = new FilesystemIterator($src_dir.'thumbs/', FilesystemIterator::SKIP_DOTS);
$numOfFiles = (iterator_count($fi));
?>
で$src_dir.'thumbs/'フォルダ内のファイル数を取得できるらしい。

20.2.リネームページの戻るボタンを修正する

素材リストからリンクするリンク先は2つ。アップロードボタンからリンクするupload.phpと各リストアイテムをクリックしてリンクするrename.php。
これらのページには戻るボタンがあり、戻るを押すと素材リストに戻る。素材リストに複数のページがある場合、戻るボタンを押した時、直前まで閲覧していたページに戻ってほしい。そこで戻り先を修正する。ブラウザの履歴を1個戻る仕様とする。upload.phpとrename.php、両方修正する必要があるがめんどくさいのでrename.phpの方だけ記述する。

■rename.php

<?php
$src_dir = './'; // ex. $src_dir='sozai/'; $src_dir='./';(current_dir)
$extensions = array("jpg","JPG","jpeg","JPEG","png","PNG","gif","GIF");
$filename = $_GET['name'];
?>
<!DOCTYPE html>
<html>
<head>
<title>Rename</title>
<style>
.block{
    width:360px;
    line-height:360px;
    font-size:30px;
    font-weight:bold;
    text-align:center;
    background-color:lightgray;
    color:white;
}
</style>
</head>
<body>
<?php
$ext = pathinfo($filename, PATHINFO_EXTENSION);
if(in_array($ext,$extensions)){
    echo '<img src="'.$src_dir.$filename.'" height="400">';
}else{
    echo '<div class="block">No Preview</div>';
}
?>

<form action="execute.php" method="post">
<input type="hidden" name="previousname" value="<?php echo $filename; ?>">
<p>ファイル名:<input type="text" name="rename" id="rename" value="<?php echo $filename; ?>" size=40></p>
<input type="submit" value="リネーム">
</form>
<?php
echo '<p><a href="execute.php?name='.$filename.'">'."削除".'</a></p>';
?>
<p><a href="<?php echo $src_dir.$filename; ?>" download="
    <?php echo $filename; ?>">ダウンロード</a></p>

<p><a href=""  onclick="history.back()">戻る</a></p>
</body>
</html>

21.MySQLでお知らせ画面を作る

前回、「19.6.お知らせ画面を作る」の項でPHPだけで強引にお知らせ画面編集プログラムを書いたが、後から修正が困難な仕様ではやっぱりメンバーに不評であった。心を入れ替えてデータベース管理によるお知らせ画面編集画面を作る。
CodeCampusさんのブログを参考にさせていただいた。
*https://blog.codecamp.jp/php-mysq

21.1.テーブルを作成する

OT「17.3.データベースを作成する」でデータベースはもう作ってある。
データベース名:hoge_db1
データベースのパスワード:2018hogePW
前回、membersというテーブルも作った。今回はそのhoge_db1の中にoshiraseというテーブルを新規に作り、お知らせ記事を書き込んでいこうと思う。

ターミナルを開いて以下を入力。
$ sudo mysql
Welcome to the MariaDB monitor. Commands end with ; or \g.
Your MariaDB connection id is 17
Server version: 10.1.44-MariaDB-0+deb9u1 Raspbian 9.11
Copyright (c) 2000, 2018, Oracle, MariaDB Corporation Ab and others.
Type 'help;' or '\h' for help. Type '\c' to clear the current input statement.
MariaDB [(none)]> USE hoge_db1;
Database changed
MariaDB [hoge_db1]> CREATE TABLE oshirase
-> (
-> id INT(11) NOT NULL AUTO_INCREMENT,
-> title mediumblob NOT NULL,
-> kiji1 mediumblob NOT NULL,
-> gazo1 mediumblob NOT NULL,
-> moji1 mediumblob NOT NULL,
-> link1 mediumblob NOT NULL,
-> PRIMARY KEY (id)
-> );
Query OK, 0 rows affected (0.29 sec)
MariaDB [hoge_db1]> exit;
Bye
                                                                                          
id                      title                 kiji1                  gazo1                  moji1                  link1
Auto Increment                                                                                                             

こんなイメージのテーブルが完成

注意■赤字部分は2024年4月13日に修正した。mediumblobにしておかないと画像ファイルなどのバイナリファイルがうまく保存できないようだ。

21.2.サブウィンドウを作り直す

「19.PHPでホームページ管理画面を作る」の項で素材アップローダーを作ったが、あれはそのまま利用する。
□sozai_list.php
□upload.php
□execute.php
□rename.php
この4つのファイルが既にある前提で話をすすめる。
「19.6.お知らせ画面を作る」で作成したsubwindow.phpはなんかごちゃごちゃしてきたので作り直す。ファイル名はsubwindow2.phpとでもしておく。

■subwindow2.php

<?php
$src_folder = './';
$src_files = scandir($src_folder, 1);
$exclusions = array("html","HTML","htm","HTM","php","PHP","exe","EXE","");
$exceptions = array("jpg","JPG","jpeg","JPEG","png","PNG","gif","GIF");
?>

<!DOCTYPE html>
<html>
<head>
<script type="text/javascript" src="https://ajax.googleapis.com/ajax/libs/jquery/1.9.1/jquery.min.js"></script>
<script>
var str = location.search;
var get = str.substring(1,str.length);
$(function(){
    $('li').click(function(){
        if (!window.opener || window.opener.closed){
            window.alert('メインウィンドウが見当たりません。');
        }else{
            switch(get){
                case 'slot0':
                    window.opener.document.form.slot0.value = $(this).text();
                    window.close();
                    break;
                case 'slot1':
                    window.opener.document.form.slot1.value = $(this).text();
                    window.close();
                    break;
                default:
            }
        }
    });
});
</script>
<style>
ul {
    list-style: none;
    margin-left: 0px;
    padding-left: 0px;
}
.notimage {
    display:inline-block;
    background:lightgray;
}
</style>
</head>
<body>
<h2>素材リスト</h2>
<ul>
<?php
foreach ($src_files as $file){
    if ($file){
        $ext = pathinfo($file, PATHINFO_EXTENSION);
        if(!in_array($ext,$exclusions)){
            if(in_array($ext,$exceptions)){
                echo '<li><img src="thumbs/'.$file.'">';
            }else{
                echo'<li><div class="notimage"></div>';
            }
            echo '<a href="">'.$file.'</a></li>';
            echo "<HR>";
        }
    }
}
?>
</ul>
</body>
</html>

21.3.お知らせ編集画面を作る

次にデータベースからデータを読み込んでテキストボックスに流し込むプログラムを作る。
SELECT構文でIDごとにデータを取得する。

■select_oshirase.php

<?php
if(isset($_GET['id'])&&($_GET['id']>=1)){
    $id = $_GET['id'];
    $link = mysqli_connect('localhost', 'hoge_db1', '2018hogePW', 'hoge_db1');

    // 接続状況をチェック
    if (mysqli_connect_errno()) {
        die("データベースに接続できません:" . mysqli_connect_error() . "\n");
    }

    $query = 'SELECT * FROM oshirase WHERE id = "'.$id.'"';
    $result = mysqli_query($link, $query);
    foreach ($result as $record) {
    }
   
    // 接続を閉じる
    mysqli_close($link);
}else{
    $link = mysqli_connect('localhost', 'hoge_db1', '2018hogePW', 'hoge_db1');

    // 接続状況をチェック
    if (mysqli_connect_errno()) {
        die("データベースに接続できません:" . mysqli_connect_error() . "\n");
    }

    $query = 'SELECT * FROM oshirase ORDER BY id DESC LIMIT 1';
    $result = mysqli_query($link, $query);
    foreach ($result as $record) {
    }
    
    // 接続を閉じる
    mysqli_close($link);
}
?>
<!DOCTYPE HTML>
<html>
<head>
<style>
input{font-size:1em;width:600px;}
textarea{font-size:1.2em;width:600px;height:100px;}
</style>
<script>
// 子窓を開く
function openWindow0() {
    window.open('subwindow2.php?slot0', 'child', 'width=500,height=800,scrollbars=yes')
}
function openWindow1() {
    window.open('subwindow2.php?slot1', 'child', 'width=500,height=800,scrollbars=yes')
}
</script>
</head>
<body>
<form method="post" name="form" action="execute_oshirase.php">
<a href="select_oshirase.php?id= <?php echo $record['id']-1; ?>" >
    ひとつ前の記事</a>

<a href="select_oshirase.php?id= <?php echo $record['id']+1; ?>" >
    ひとつ後の記事</a>

<a href="insert_oshirase.php">
    新規記事作成</a>

<input type="submit" value="送信" 
    style="width: 120px;position:  absolute;top: 0px;left: 600px;">
<input type="hidden" name="update" value="update">

<table border="1" cellspacing="0">
<tr>
<td>番号</td>
<td>
<input type="text" name="id" value="<?php echo $record['id']; ?>" readonly>
</td>

</tr>
<tr>
<td>タイトル</td>
<td>
<input type="text" name="title" value="<?php echo $record['title']; ?>">
</td>

</tr>
<tr>
<td>本文</td>
<td>
<textarea name="kiji1"><?php echo $record['kiji1']; ?></textarea>
</td>

</tr>
<tr>
<td>
画像 <a href="javascript:void(0)" onClick="openWindow0()">リストから選択</a>
</td>

<td>
<input type="text" id="slot0" name="gazo1" value="<?php echo $record['gazo1']; ?>">
</td>

</tr>
<tr>
<td>文字(PDFのリンク元)</td>
<td>
<input type="text" name="moji1" value="<?php echo $record['moji1']; ?>">
</td>

</tr>
<tr>
<td>
PDF <a href="javascript:void(0)" onClick="openWindow1()">リストから選択</a><BR>
</td>

<td>
<input type="text" id="slot1" name="link1" value="<?php echo $record['link1']; ?>">
</td>

</tr>
</table>
</form>
</body>
</html>

21.4.新規記事入力画面を作る

前項のselect_oshirase.phpとほぼ同じ体裁だがこちらは新規記事入力用のフォーム。

■insert_oshirase.php

<!DOCTYPE HTML>
<html>
<head>
<style>
input{font-size:1em;width:600px;}
textarea{font-size:1.2em;width:600px;height:100px;}
</style>
<script>
// 子窓を開く
function openWindow0() {
    window.open('subwindow2.php?slot0', 'child', 'width=500,height=800,scrollbars=yes')
}
function openWindow1() {
    window.open('subwindow2.php?slot1', 'child', 'width=500,height=800,scrollbars=yes')
}
</script>
</head>
<body>
<form method="post" name="form" action="execute_oshirase.php">
<input type="submit" value="送信"
    style="width: 120px;position:  absolute;top: 0px;left: 600px;">

<input type="hidden" name="insert" value="insert">

<table border="1" cellspacing="0">
<tr>
<td>番号</td>
<td>
<input type="text" name="id" value="新規" readonly>
</td>

</tr>
<tr>
<td>タイトル</td>
<td>
<input type="text" name="title" value="">
</td>

</tr>
<tr>
<td>本文</td>
<td><textarea name="kiji1"></textarea></td>
</tr>
<tr>
<td>
画像 <a href="javascript:void(0)" onClick="openWindow0()">リストから選択</a>
</td>

<td>
<input type="text" id="slot0" name="gazo1" value="">
</td>

</tr>
<tr>
<td>文字(PDFのリンク元)</td>
<td>
<input type="text" name="moji1" value="">
</td>

</tr>
<tr>
<td>
PDF <a href="javascript:void(0)" onClick="openWindow1()">リストから選択</a><BR>
</td>

<td>
<input type="text" id="slot1" name="link1" value="">
</td>

</tr>
</table>
</form>
</body>
</html>

21.5.UPDATEとINSERTを行う画面を作る

SELECT、UPDATE、INSERT、DELETEがデータベース操作の基本となる。しかし、今回DELETEは使わない。IDを自動連番で取得しているので、DELETEしてしまうとIDが中抜けになってしまう。

■execute_oshirase.php

<?php
if($_POST['update']){
    $id = $_POST['id'];
    $title= $_POST['title'];
    $kiji1= $_POST['kiji1'];
    $gazo1= $_POST['gazo1'];
    $moji1= $_POST['moji1'];
    $link1= $_POST['link1'];

    $link = mysqli_connect('localhost', 'hoge_db1', '2018hogePW', 'hoge_db1');

    // 接続状況をチェック
    if (mysqli_connect_errno()) {
        die("データベースに接続できません:" . mysqli_connect_error() . "\n");
    }
    echo "データベースの接続に成功しました。\n";

    $query = 'UPDATE oshirase SET
        title = "'.$title.'", kiji1 = "'.$kiji1.'", gazo1 = "'.$gazo1.'"
        ,moji1 = "'.$moji1.'" , link1 = "'.$link1.'" WHERE id = "'.$id.'"';
    $result = mysqli_query($link, $query);
    echo 'UPDATE に成功しました。';
    echo '<a href="oshirase.php">確認する。</a>';
    

    // 接続を閉じる
    mysqli_close($link);
}

if($_POST['insert']){
    $title= $_POST['title'];
    $kiji1= $_POST['kiji1'];
    $gazo1= $_POST['gazo1'];
    $moji1= $_POST['moji1'];
    $link1= $_POST['link1'];

    $link = mysqli_connect('localhost', 'hoge_db1', '2018hogePW', 'hoge_db1');

    // 接続状況をチェック
    if (mysqli_connect_errno()) {
        die("データベースに接続できません:" . mysqli_connect_error() . "\n");
    }
    echo "データベースの接続に成功しました。\n";

    $query = 'INSERT INTO oshirase
        (title,kiji1,gazo1,moji1,link1) VALUES
        ("'.$title.'","'.$kiji1.'","'.$gazo1.'","'.$moji1.'","'.$link1.'")';
    $result = mysqli_query($link, $query);
    echo 'INSERT に成功しました。';
    echo '<a href="oshirase.php">確認する。</a>';

    // 接続を閉じる
    mysqli_close($link);
}
?>

21.6.お知らせ画面本体

そしてこれがお知らせ画面の本体。これを<iframe>を使ってindex.html内に配置すれば、我がチームのホームページが完成する。

■oshirase.php

<!DOCTYPE HTML>
<html>
<head>
</head>
<body>
<?php
$link = mysqli_connect('localhost', 'hoge_db1', '2018hogePW', 'hoge_db1');
// 接続状況をチェック
if (mysqli_connect_errno()) {
    die("データベースに接続できません:" . mysqli_connect_error() . "\n");
}

$query = 'SELECT * FROM oshirase ORDER BY id DESC;';
$result = mysqli_query($link, $query);
    foreach ($result as $record) {
        if($record['title']){
            echo '<h3>'.$record['title'].'</h3>';
            if($record['kiji1']){echo nl2br($record['kiji1'].'<BR>', false);}
            if($record['gazo1']){echo '<img src="'.$record['gazo1'].'"><BR>';}
            if($record['moji1']){
                if($record['link1']){
                    echo '<a href="'.$record['link1'].'" target="_top">'.$record['moji1'].'
                        </a><BR>';

                }else{
                    echo $record['moji1'].'<BR>';
                }
            }
                echo '<HR>';
        }
    }


// 接続を閉じる
mysqli_close($link);
?>
</body>
</html>

21.7.PHPのデバッグ

PHPは簡単なミスでも「このページは動作していません」というつれないエラーを返す。
<?php
ini_set('display_errors',1);

の一文を加えるとエラーの箇所と原因を表示してくれる。

22.PDF.jsを使ってみる

我がチームのホームページを管理していて困った問題に突き当たった。どうやら我がホームページ管理メンバーはJPEGとPDFの区別がつかないらしい。これをトップ画面に貼り付けてくださいと言って送ってよこすファイルがPDF。しかも複数ページもの。これじゃ貼り付けられないよと言うと、ああごめんなさいと言って今度は1ページごとに分割されたPDFをよこす。違う。そうじゃない。PDFはそのままではWebページの画面の一部として貼り付けられないのだ。ああPDFもJPEGのようにそのまま画像として貼り付けられたらいいのに。
そこでPDF.js。PDF.jsはブラウザの中でPDFのレンダリングを行うJavaScriptライブラリ。これを使えばPDFの画像イメージそのままにWebページに貼り付けられるかもしれない。

22.1.PDF.jsをダウンロードする

「PDF.js」でググってトップに表示される↓このページ
「PDF.js - Mozilla on GitHub」*https://mozilla.github.io/pdf.js/
に行って、「Download」ボタンを押す。次のページにはビルド済ベータ版とビルド済安定版のボタンが並び、選べと言う。とりあえず安定版(当項目執筆時点でv2.3.200)をダウンロード。
zipファイルを開いて中身をラズベリーパイ側/var/www/htmlフォルダ内の適当なところに置く。
もちろん我がチームのホームページ本体はレンタルサーバ上にある。ラズベリーパイは練習台だ。
ファイル構造はこんな感じ。

├── build/
│ ├── pdf.js                                                          - display layer
│ ├── pdf.js.map                                                   - display layer's source map
│ ├── pdf.worker.js                                                - core layer
│ └── pdf.worker.js.map                                         - core layer's source map
├── web/
│ ├── cmaps/                                                        - character maps (required by core)
│ ├── compressed.tracemonkey-pldi-09.pdf             - PDF file for testing purposes
│ ├── debugger.js                                                  - helpful debugging features
│ ├── images/                                                       - images for the viewer and annotation icons
│ ├── locale/                                                         - translation files
│ ├── viewer.css                                                    - viewer style sheet
│ ├── viewer.html                                                  - viewer layout
│ ├── viewer.js                                                      - viewer layer
│ └── viewer.js.map                                               - viewer layer's source map
└── LICENSE

buildフォルダとwebフォルダ、ライセンスファイルからなる。
ブラウザからwebフォルダの中のviewer.htmlを開くとサンプルのPDFファイルを表示する。viewer.htmlのURL、http://192.168.0.x/~/~/web/viewer.htmlに続けて、?file=PDFファイルの場所、を記述するとそのPDFファイルを表示する。例えば、webフォルダの外にhoge.pdfというファイルがあったなら、http://192.168.0.x/~/~/web/viewer.html?file=../hoge.pdf
となる。

22.2.viewer.htmlをカスタマイズする

viewer.htmlは高機能である。サムネイルから目的のページへジャンプできるし栞も挟める。印刷もできる。しかしそんな機能はいらない。ただPDFの中身を表示してくれればよかったのにこの高機能なツールバーが邪魔だ。そこでツールバーを非表示にする。やり方はこのブログ*を参考にした。
*http://www.chaordic.co.jp/memorandum/2020/01/pdfjspdf.html
まずviewer.htmlのコピーを作り、別名で保存、viewer_custom.htmlとでもしておく。
エディタで開いて32行目、
<link rel="stylesheet" href="viewer.css">の下の行に
<link rel="stylesheet" href="viewer_custom.css">を挿入。
次に、viewer_custom.cssを作り、webフォルダの中に置く。中身は以下の通り。
.toolbar{
display: none;
}
これでまずツールバーが消える。
妙に暗い背景色も気に入らない。白にしてみよう。ツールバーの跡地もつめてしまえ。
.toolbar{
display: none;
}
body {
background-color: white;
background-image: none;
}
.pdfViewer .page {
-o-border-image: none;
     border-image: none;
}
#viewerContainer {
position: absolute;
top: 0px;
right: 0;
bottom: 0;
left: 0;
}

これで背景色が白になる。しかし待った。PDFの余白の白とと背景色の白がボーダーレスになってしまってなんか変。
.toolbar{
display: none;
}
body {
background-color: #eee;
background-image: none;
}
.pdfViewer .page {
-o-border-image: none;
     border-image: none;

}
#viewerContainer {
position: absolute;
top: 0px;
right: 0;
bottom: 0;
left: 0;
}

背景色を薄いグレーにしてみた。これなら落ち着く。
.toolbar{
display: none;
}
body {
background-color: white;
background-image: none;
}
.pdfViewer .page {
border: 2px solid transparent;
-o-border-image: url(images/shadow.png) 9 9 repeat;
     border-image: url(images/shadow.png) 9 9 repeat;
     border-image-width: 2px
}
#viewerContainer {
position: absolute;
top: 0px;
right: 0;
bottom: 0;
left: 0;
}
背景色は白としておいてボーダーに2pxのグレーを配置した例、これでもいい。
なお、PDF.jsはいちいち直前の操作のパラメータをクッキーに保存するらしく、ズーム倍率をいじると次回起動時も直前のズーム設定が継承される。余計なことをしてくれるものだ。自動ズームに戻したいときはクッキーを削除しなければならない。

22.3.iframeでホームページに貼り付ける

<iframe width="700" height="1000" src="http://192.168.0.x/~/~/web/viewer_custom.html?file=../hoge.pdf" scrolling="no" frameborder="0"></iframe>てな感じでホームページに貼り付ける。
自動ズームによって横幅はうまい調子にフィットしてくれる。縦幅はPDFの縦幅に合わせて調整が必要だろう。余裕をもって長めにしておけばまあ大丈夫か。おおこれでPDFが貼り付けられるぞ。しかし、PDFが重いとそれなりに表示に時間がかかる。軽めのPDF限定かな。

23.summernoteを使ってみる

どうやら我がチームメンバーが望んでいるのは本物のWYSIWYGらしい。WORDをいじるようにホームページのコンテンツを編集できなければ使えないらしい。だったらホームページビルダー買えよということになるが、我がメンバーのITリテラシーではホームページビルダーすら使いこなせるかどうか心配だ。
そこでsummernoteを使ってみる。summernoteはオンラインWYSIWYGエディタを実装するJavascriptライブラリらしい。

23.1.summernoteをダウンロードする

「summernote」でググってトップに表示されるページ。
「Summernote - Super Simple WYSIWYG editor」*https://summernote.org/
ここへ行って「Getting started」のタブをクリック。「Dowmload compiled」を押してZIPファイルをダウンロードし、/var/www/htmlフォルダ内のどこかにsummernoteという名のフォルダを作って中身を保存。

今回、
□wysiwygeditor.php
□editordata.php
□content.txt
□goannai.php
という4つのファイルを作る。我がチームホームページののトップ画面を構成する「ご案内」ページ(goannai.php)をwysiwygeditor.phpによってWYSIWYGで編集しようという試み。

ファイル構造は以下の通り

├── summernote/ (フォルダ)
│ ├── font/ (フォルダ)
│ ├── lang/ (フォルダ)
│ ├── plugin/ (フォルダ)
│ ├── summernote.css
│ ├── summernote.js
│ │ ・
│ │ ・
│ │ ・
│ ├── summernote-lite.min.js.LICENSE.txtfiles
│ └── summernote-lite.min.js.map
├── wysiwygeditor.php
├── editordata.php
├── content.txt
└── goannai.php

23.2.WYSIWYG編集画面を作る

wysiwygeditor.phpという名のファイルを作る。
「Getting started」に作り方が書いてあるのでほぼその通りにコピペ。「Customization」の項を参照して、タブの内容を若干カスタマイズした。
snmmernote画面内によそからコピーしてきた文章をペーストするとスタイル情報もいっしょにペーストしてしまうのが若干うっとおしい。stack over flow*にその解決策が載っていたのでそれもコピペした。
*https://stackoverflow.com/questions/30993836/paste-content-as-plain-text-in-summernote-editor

■wysiwygeditor.php

<!DOCTYPE html>
<?php
$file="content.txt";
$content=file_get_contents($file);
?>
<html>
<head>
<link href="https://stackpath.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css"
    rel="stylesheet">

<script src="https://code.jquery.com/jquery-3.4.1.min.js"></script>
<script src="https://stackpath.bootstrapcdn.com/bootstrap/3.3.7/js/bootstrap.min.js">
</script>

<link href="summernote/summernote.css" rel="stylesheet">
<script src="summernote/summernote.min.js"></script>
<script src="summernote/lang/summernote-ja-JP.js"></script>
</head>
<body>
<form method="post" action="editordata.php">
<textarea id="summernote" name="editordata"><?php echo $content; ?></textarea>
<input type="submit" value="送信">
</form>
<script>
$(document).ready(function() {
    $('#summernote').summernote({
        toolbar: [
        ['style', ['style']],
        ['font', ['bold', 'italic', 'underline', 'clear']],
        ['fontsize', ['fontsize']],
        ['color', ['color']],
        ['para', ['ul', 'ol', 'paragraph']],
        ['height', ['height']],
        ['table', ['table']],
        ['insert', ['link', 'picture']],
        ['view', ['fullscreen', 'codeview', 'help']]
    ],
    lang: 'ja-JP', 
    height: 300, 

    callbacks: {
    onPaste: function (e) {
        var bufferText =
            ((e.originalEvent || e).clipboardData || window.clipboardData).getData('Text');

        e.preventDefault();
        document.execCommand('insertText', false, bufferText);
        }
      }
   });
});
</script>
</body>
</html>

23.3.POSTを受け取る

wysiwygeditor.phpからのPOST送信を受け取るファイルeditordata.phpを作る。受け取ったデータのコンテナとなるcontent.txtも作成。

■editordata.php

<?php
$file="content.txt";
if($_POST['editordata']){
    $content=$_POST['editordata'];
    file_put_contents($file, $content);
}
?>
<!DOCTYPE html>
<html>
<head>
</head>
<body>
送信しました
</body>
</html>

あとはcontent.txtをincludeでご案内ページに取り込めば、ご案内ページの完成。

■goannai.php

<!DOCTYPE html>
<head>
<link href="https://stackpath.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css"
    rel="stylesheet">
<title>

ご案内
</title>

</head>
<body>
<?php include 'content.txt'; ?>
</body>
</html>

bootstrapのCSSを読みに行かないとちゃんとしたWYSIWYGにならないらしい。簡単にWYSIWYGエディタが完成した。画像も貼り付けられる。よそのブログを見るとこの画像の貼り付け方がバイナリをそのまま貼り付けているため、横着でけしからんという声が多数ある。しろうとなのでとりあえず貼り付けられればなんでもいい。

24.Ajaxを使ってみる

ホームページを訪れた客からの問い合わせにどう対応するか。これまではホームページに電話番号を記載して「ここにお電話ください」と案内するだけだったが、それではビジネスの機会を逃すのではないか。問い合わせフォームを用意してメールによる問い合わせも受け付けるようにすれば、これまで電話をかけるのを躊躇していた顧客を新規に開拓できるのではないか。
我がチームにビジネスの欲が湧いてきた。そこでお問い合わせフォームのページを作ることにした。

24.1.お問い合わせページを作る

お問い合わせページの仕様は以下の通り

お問い合わせ
お名前 (必須)
┌──────────────┐
│              │
└──────────────┘
メールアドレス (必須)
┌──────────────┐
│              │
└──────────────┘
題名
┌──────────────┐
│              │
└──────────────┘
メッセージ本文
┌──────────────┐
│              │
│              │
│              │
│              │
│              │
│              │
│              │
│              │
└──────────────┘
┌────┐
│ 送信 │
└────┘

名前とメールアドレスは必須項目とした。このフォームに入力して送信ボタンを押せば我がチームの問い合わせ受信専用メールアドレスに送信される仕組み。作り方は簡単だ。<form action="mailto:受信専用メールアドレス" method="post">とでもしておけば、簡単にメール送信フォームが作れる。だがちょっと待て。このフォームの送信ボタンを押すと勝手にOutlookが起動して、新規メール送信待ちの状態となる。この挙動が意外だった。こんな挙動は期待していない。送信ボタンを押したらダイレクトで送信完了してほしい。
action=""の先をphpファイルとすればOutlookは起動しない。しかしそれでは我々がメールで受信できない。顧客からの問い合わせは瞬時にメールで受け取りたいのだ。すぐに問い合わせ内容を確認してオンタイムでレスポンスを返したいのだ。ならばphpから自動でメール送信する仕組みを作ればよい。
仕様はこうだ。送信ボタンを押した時、お名前 (必須)、メールアドレス (必須)の欄が空白だったら「必須項目に入力してください」とメッセージを返す。そうでなければ「送信完了しました」とメッセージを返す。
送信ボタンを押した後phpファイルに画面遷移してしまうのはかっこ悪い。画面遷移をせずただメッセージのみを返すのがよくある問い合わせフォームの姿だ。フォームを画面遷移させずにSubmitするにはAjaxを使うとよいらしい。

お問い合わせ
お名前 (必須)
┌──────────────┐
│              │
└──────────────┘
必須項目に入力してください   ←空白ならメッセージ表示
メールアドレス (必須)
┌──────────────┐
│              │
└──────────────┘
必須項目に入力してください   ←空白ならメッセージ表示
題名
┌──────────────┐
│              │
└──────────────┘
メッセージ本文
┌──────────────┐
│              │
│              │
│              │
│              │
│              │
│              │
│              │
│              │
└──────────────┘
┌────┐
│ 送信 │
└────┘
┌──────────────┐
│   送信完了しました   │←送信後メッセージ表示
└──────────────┘

あまりにそっけないGUIだが、本物のお問い合わせページは多少CSSを駆使して飾り付けているので心配ご無用。ここではコードをシンプルに見せるためあえて最低限のスタイルにしている。

■contact.html

<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<script type="text/javascript" src="https://code.jquery.com/jquery-3.4.1.min.js"></script>
</head>
<body>
<h1>お問い合わせ</h1>
<form name="form" id="form" method="post">
<p>お名前 (必須)<br>
<input type="text" id="yourName" name="yourName" value="" size="40" onChange="formCheck()"></p>
<p id="notice1" style="display:none; color:red;">必須項目に入力してください</p>
<p>メールアドレス (必須)<br>
<input type="text" id="yourEmail" name="yourEmail" value="" size="40" onChange="formCheck()"></p>
<p id="notice2" style="display:none; color:red;">必須項目に入力してください</p>
<p><label> 題名<br>
<input type="text" id="yourSubject" name="yourSubject" value="" size="40"></p>
<p><label> メッセージ本文<br>
<textarea id="yourMessage" name="yourMessage" cols="40" rows="10"></textarea></p>
<p><input type="button" class="send" value="送信" ></p>
<p><input type="text" id="notice3" style="display:none;color:royalblue;border:dashed 1px royalblue;" value="送信しました" size="40"><p>
</form>

<script type="text/javascript">
function formCheck(){
   $(function(){
      if($('input[name="yourName"]').val()==""){
         $('#notice1').css('display','block');
      }else{
         $('#notice1').css('display','none');
      }
      if($('input[name="yourEmail"]').val()==""){
         $('#notice2').css('display','block');
      }else{
         $('#notice2').css('display','none');
      }
   });
}

$(function(){
   $('.send').click(function(event){
      formCheck();
      if($('#yourName').val() !="" && $('#yourEmail').val() !=""){
         event.preventDefault();
         $.ajax({
            type: 'POST',
            url: 'transmit.php',
            data: {yourName: $('#yourName').val(),
            yourEmail: $('#yourEmail').val(),
            yourSubject: $('#yourSubject').val(),
            yourMessage: $('#yourMessage').val()},
            success: function(data) {
            // Do something with the success
            },
            error: function(error) {
            // Do something with the error
            }
         });
      $('#notice3').css('display','block');
      }
   });
});

</script>
</body>
</html>

そしてSubmitを受け取りメールに転送するphpファイルがこちら

■transmit.php

<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
</head>
<body>

<?php
mb_language("Japanese");
mb_internal_encoding("UTF-8");

$to = "我がチームのメールアドレス";
$title = "Web Page Response";
$message =
   "<yourName>"
   .$_POST['yourName']
   ."<yourEmail>"
   .$_POST['yourEmail']
   ."<yourSubject>"
   .$_POST['yourSubject']
   ."<yourMessage>"
   .$_POST['yourMessage'];
$headers = "";

if(mb_send_mail($to, $title, $message, headers)){
   echo "メール送信成功です";
}else{
   echo "メール送信失敗です";
}
?>
</body>
</html>

このtransmit.phpをラズベリーパイのwww/htmlフォルダに置いてもメールは送信してくれない。ラズベリーパイにメールサーバの機能を与えていない。でもこのtransmit.phpを我々が借りているレンタルサーバ上に置くと不思議なことにちゃんとメールが届く。
Ajaxがうまく機能せず、うっかり画面遷移してしまったとしても「メール送信成功です」のメッセージが表示され、とりあえずメールが届く。

25.Bootstrapを使ってみる

25.1.ハンバーガーメニューを作る

同僚の一人が「うちのホームページはレスポンシブデザインにしないの?」と聞いてきた。おのれその言葉をどこで覚えてきた?ちょっと覚えた技術用語を使ってみたかっただけだろう。うちのホームページなんかがレスポンシブデザインなど気にする必要があるか。
とはいえ確かに我がチームのホームページはスマホでは読みにくい。レスポンシブデザインとかいうのをかじってみたい気はするがめんどくさそうだ。
どうやらBootstrapというのを利用すると簡単にレスポンシブ対応Webページが作れてしまうらしい。と聞いて調べてみたが言うほど簡単ではないことだけはよくわかった。
手始めにハンバーガーメニューというのを実現してみることにした。PCで見ると横並びのメニュー項目、スマホで見ると3本線のメニューアイコンとして表示される裏技。
とりあえずBootstrap4のホームページに書いてあるサンプルをコピペしつつも少々手直しして作ってみた。まあお手軽にハンバーガーメニューが作れるっちゃ作れるかな。

<!DOCTYPE html>
<html lang="ja">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title></title>
<!-- CSS only -->
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.5.0/css/bootstrap.min.css" integrity="sha384-9aIt2nRpC12Uk9gS9baDl411NQApFmC26EwAOH8WgZl5MYYxFfc+NcPb1dKGj7Sk" crossorigin="anonymous">

<!-- JS, Popper.js, and jQuery -->
<script src="https://code.jquery.com/jquery-3.5.1.slim.min.js" integrity="sha384-DfXdz2htPH0lsSSs5nCTpuj/zy4C+OGpamoFVy38MVBnE+IbbVYUew+OrCXaRkfj" crossorigin="anonymous"></script>
<script src="https://cdn.jsdelivr.net/npm/popper.js@1.16.0/dist/umd/popper.min.js" integrity="sha384-Q6E9RHvbIyZFJoft+2mJbHaEWldlvI9IOYy5n3zV9zzTtmI3UksdQRVvoxMfooAo" crossorigin="anonymous"></script>
<script src="https://stackpath.bootstrapcdn.com/bootstrap/4.5.0/js/bootstrap.min.js" integrity="sha384-OgVRvuATP1z7JjHLkuOU7Xw704+h835Lr+6QL9UvYjZE3Ipu6Tp75j7Bh/kR0JKI" crossorigin="anonymous"></script>
</head>
<body>
<!-- 白背景バージョン -->
<nav class="navbar navbar-expand-lg navbar-light bg-light">
<div class="container">
<a class="navbar-brand" href="#">ほげほげ幼稚園</a>
<button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarNav" aria-controls="navbarNav" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarNav">
<ul class="navbar-nav ml-auto">
<li>
<a class="nav-link" href="#">ホーム<span class="sr-only">(current)</span></a>
</li>
<li>
<a class="nav-link" href="#">保育内容</a>
</li>
<li>
<a class="nav-link" href="#">入園案内</a>
</li>
</ul>
</div>
</div>
</nav>
<!-- グレー背景バージョン -->
<nav class="navbar navbar-expand-lg navbar-dark bg-dark">
<div class="container">
<a class="navbar-brand" href="#">ほげほげ幼稚園</a>
<button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarNav" aria-controls="navbarNav" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarNav">
<ul class="navbar-nav ml-auto">
<li>
<a class="nav-link" href="#">ホーム<span class="sr-only">(current)</span></a>
</li>
<li>
<a class="nav-link" href="#">保育内容</a>
</li>
<li>
<a class="nav-link" href="#">入園案内</a>
</li>
</ul>
</div>
</div>
</nav>
</body>
</html>

25.2.画像の上にメニューをのせる

画面幅いっぱいのメイン画像に社名とメニューとキャッチコピーを重ねてのせる。WordPressのサンプルみたいなホームページも簡単に作れたりするんだろうか。簡単じゃなかったけどとりあえず作れた。
embed-responsiveクラスは本当はYouTube動画みたいなのをアスペクト比を崩さずにインラインフレームに収めるためのコンポーネントだが、ただの画像の表示にも使えそう。
キャッチコピーはそれ専用のコンポーネント、Jumbotronクラスで配置してみた。何が変わったのかよくわからん。
Styleを少々改変したが、やらなくても動く。
画像はここ*から拝借している。
*https://www.aoki-kyousei.com/sozai/yokohama/
でもこのホームページをスマホで見ると、キャッチコピーの文字が画像からあふれる。これは失敗作だ。スマホでも正しく表示されるようにするためにはメディアクエリを駆使してフォントサイズや行間スペースを切り替えてやらなければならない。そこが一番めんどくさいのに。そういうのはBootstrapさんでは用意してくれないのね。

<!DOCTYPE html>
<html lang="ja">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title></title>
<!-- CSS only -->
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.5.0/css/bootstrap.min.css" integrity="sha384-9aIt2nRpC12Uk9gS9baDl411NQApFmC26EwAOH8WgZl5MYYxFfc+NcPb1dKGj7Sk" crossorigin="anonymous">

<!-- JS, Popper.js, and jQuery -->
<script src="https://code.jquery.com/jquery-3.5.1.slim.min.js" integrity="sha384-DfXdz2htPH0lsSSs5nCTpuj/zy4C+OGpamoFVy38MVBnE+IbbVYUew+OrCXaRkfj" crossorigin="anonymous"></script>
<script src="https://cdn.jsdelivr.net/npm/popper.js@1.16.0/dist/umd/popper.min.js" integrity="sha384-Q6E9RHvbIyZFJoft+2mJbHaEWldlvI9IOYy5n3zV9zzTtmI3UksdQRVvoxMfooAo" crossorigin="anonymous"></script>
<script src="https://stackpath.bootstrapcdn.com/bootstrap/4.5.0/js/bootstrap.min.js" integrity="sha384-OgVRvuATP1z7JjHLkuOU7Xw704+h835Lr+6QL9UvYjZE3Ipu6Tp75j7Bh/kR0JKI" crossorigin="anonymous"></script>
<style>
.navbar-dark .navbar-toggler {
border-color: rgba(0,0,0,0);
}
.navbar-dark .navbar-nav .nav-link {
color: rgba(255, 255, 255, 0.9);
}
.navbar-dark .navbar-nav .nav-link:hover, .navbar-dark .navbar-nav .nav-link:focus {
color: rgba(255, 255, 255, 0.75);
}
</style>
</head>
<body>
<section class="embed-responsive">
<img src="https://www.aoki-kyousei.com/img/free/046.jpg"  class="w-100">
<div class="embed-responsive-item" >
<nav class="navbar navbar-expand-lg navbar-dark" style="padding:20px 0px;" >
<div class="container">
<a class="navbar-brand" href="#">ほげほげ幼稚園</a>
<button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarNav" aria-controls="navbarNav" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarNav">
<ul class="navbar-nav ml-auto">
<li>
<a class="nav-link" href="#">ホーム<span class="sr-only">(current)</span></a>
</li>
<li>
<a class="nav-link" href="#">保育内容</a>
</li>
<li>
<a class="nav-link" href="#">入園案内</a>
</li>
</ul>
</div>
</div>
</nav>
<br>
<br>
<div class="jumbotron-fluid">
<div class="text-center" style="color:#fff;">
<h1 class="display-4">こころのやさしいこどもをそだてる</h1>
<p class="lead">地域とともに歩むあたたかい保育を</p>
<a href="#" class="btn btn-lg btn-primary">私たちについて</a>
</div>
</div>
</div>
</section>
</body>
</html>

25.3.スクロールしても固定している画像を作る

と言葉に書くとわかりにくいが、スクロールしても背景画像だけが固定している状態を作りたい。WordPressのTwenty Seventeenみたいなやつと言っても知ってる人にしかわからんな。
千葉市美術館のホームページ*みたいなやつ。
*https://www.ccma-net.jp/
スクロールしても画像だけが「置いてけ」になる状態。Bootstrapでできるのかと思ったらそんな機能はないらしい。でもCSSだけで意外と簡単に作れるらしい。
上記の偽ホームページに「置いてけ」画像を追加した。ついでにFooterもくっつけてみた。WordPressっぽくなったかな。

<!DOCTYPE html>
<html lang="ja">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title></title>
<!-- CSS only -->
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.5.0/css/bootstrap.min.css" integrity="sha384-9aIt2nRpC12Uk9gS9baDl411NQApFmC26EwAOH8WgZl5MYYxFfc+NcPb1dKGj7Sk" crossorigin="anonymous">

<!-- JS, Popper.js, and jQuery -->
<script src="https://code.jquery.com/jquery-3.5.1.slim.min.js" integrity="sha384-DfXdz2htPH0lsSSs5nCTpuj/zy4C+OGpamoFVy38MVBnE+IbbVYUew+OrCXaRkfj" crossorigin="anonymous"></script>
<script src="https://cdn.jsdelivr.net/npm/popper.js@1.16.0/dist/umd/popper.min.js" integrity="sha384-Q6E9RHvbIyZFJoft+2mJbHaEWldlvI9IOYy5n3zV9zzTtmI3UksdQRVvoxMfooAo" crossorigin="anonymous"></script>
<script src="https://stackpath.bootstrapcdn.com/bootstrap/4.5.0/js/bootstrap.min.js" integrity="sha384-OgVRvuATP1z7JjHLkuOU7Xw704+h835Lr+6QL9UvYjZE3Ipu6Tp75j7Bh/kR0JKI" crossorigin="anonymous"></script>
<style>
.navbar-dark .navbar-toggler {
border-color: rgba(0,0,0,0);
}
.navbar-dark .navbar-nav .nav-link {
color: rgba(255, 255, 255, 0.9);
}
.navbar-dark .navbar-nav .nav-link:hover, .navbar-dark .navbar-nav .nav-link:focus {
color: rgba(255, 255, 255, 0.75);
}
.callout {
background: url(images/callout-back.jpg) no-repeat fixed 0 0 /cover;
overflow: hidden;
width: 100%;
padding: 0;
}
.callout .overlay {
position: relative;
width: 100%;
padding: 240px 0;
}

</style>
</head>
<body>
<section class="embed-responsive">
<img src="https://www.aoki-kyousei.com/img/free/046.jpg" class="w-100">
<div class="embed-responsive-item" >
<nav class="navbar navbar-expand-lg navbar-dark" style="padding:20px 0px;" >
<div class="container">
<a class="navbar-brand" href="#">ほげほげ幼稚園</a>
<button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarNav" aria-controls="navbarNav" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarNav">
<ul class="navbar-nav ml-auto">
<li>
<a class="nav-link" href="#">ホーム<span class="sr-only">(current)</span></a>
</li>
<li>
<a class="nav-link" href="#">保育内容</a>
</li>
<li>
<a class="nav-link" href="#">入園案内</a>
</li>
</ul>
</div>
</div>
</nav>
<br>
<br>
<div class="jumbotron-fluid">
<div class="text-center" style="color:#fff;">
<h1 class="display-4">こころのやさしいこどもをそだてる</h1>
<p class="lead">地域とともに歩むあたたかい保育を</p>
<a href="#" class="btn btn-lg btn-primary">私たちについて</a>
</div>
</div>
</div>
</section>
<section class="callout" style="background-image:url('https://www.aoki-kyousei.com/img/free/004.jpg');">
<div class="overlay">
<div class="container text-center" style="color:#fff;">
<h3>新入園児追加募集・二次募集のお知らせ</h3>
<p>年少・年中・年長追加募集中 お電話で問い合わせください</p>
</div>
</div>
</section>
<br>
<br>
<br>
<br>
<br>
<br>
<br>
<br>
<br>
<br>
<footer>
<div class="container">
<a href="#">ホーム</a>
<span>|</span>
<a href="#">保育内容</a>
<span>|</span>
<a href="#">入園案内</a>
</div>
</footer>
</body>
</html>

<style>の中、33行目あたり
padding: 240px 0;
の値を変えると画像の縦寸法を変えられる。

px値による指定でなく以下のようにすることもできる。
padding: 20% 0;
こうするとレスポンシブ的な感じで画面枠サイズに合わせて縦寸法も自動リサイズされる。

25.4.カルーセル(スライド)を作る

カルーセルとは背景画像が左右にスライドしていく特殊効果。
くら寿司のホームページ*みたいなやつ。
*https://www.kurasushi.co.jp/
あるいはなか卯のホームページ*みたいなやつ。
*https://www.nakau.co.jp/jp/
ドムドムバーガー*もカルーセル。
*https://domdomhamburger.com/
Bootstrap4では簡単にカルーセルが作れるコンポーネントが用意されているので使ってみる。
「25.2.画像の上にメニューをのせる」で示したサンプルを改変してみる。
<body>の2行下。
<img src="https://www.aoki-kyousei.com/img/free/046.jpg" class="w-100">
の行を消し去り、代わりに<div class="carousel slide"から始まる数行を追加する。
とりあえずFooterもくっつけてみた。

<!DOCTYPE html>
<html lang="ja">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title></title>
<!-- CSS only -->
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.5.0/css/bootstrap.min.css" integrity="sha384-9aIt2nRpC12Uk9gS9baDl411NQApFmC26EwAOH8WgZl5MYYxFfc+NcPb1dKGj7Sk" crossorigin="anonymous">

<!-- JS, Popper.js, and jQuery -->
<script src="https://code.jquery.com/jquery-3.5.1.slim.min.js" integrity="sha384-DfXdz2htPH0lsSSs5nCTpuj/zy4C+OGpamoFVy38MVBnE+IbbVYUew+OrCXaRkfj" crossorigin="anonymous"></script>
<script src="https://cdn.jsdelivr.net/npm/popper.js@1.16.0/dist/umd/popper.min.js" integrity="sha384-Q6E9RHvbIyZFJoft+2mJbHaEWldlvI9IOYy5n3zV9zzTtmI3UksdQRVvoxMfooAo" crossorigin="anonymous"></script>
<script src="https://stackpath.bootstrapcdn.com/bootstrap/4.5.0/js/bootstrap.min.js" integrity="sha384-OgVRvuATP1z7JjHLkuOU7Xw704+h835Lr+6QL9UvYjZE3Ipu6Tp75j7Bh/kR0JKI" crossorigin="anonymous"></script>
<style>
.navbar-dark .navbar-toggler {
border-color: rgba(0,0,0,0);
}
.navbar-dark .navbar-nav .nav-link {
color: rgba(255, 255, 255, 0.9);
}
.navbar-dark .navbar-nav .nav-link:hover, .navbar-dark .navbar-nav .nav-link:focus {
color: rgba(255, 255, 255, 0.75);
}
</style>
</head>
<body>
<section class="embed-responsive">
<div class="carousel slide" data-ride="carousel" data-interval="8000">
<div class="carousel-inner">
<div class="carousel-item active">
<img src="https://www.aoki-kyousei.com/img/free/045.jpg" class="d-block w-100" alt="...">
</div>
<div class="carousel-item">
<img src="https://www.aoki-kyousei.com/img/free/046.jpg" class="d-block w-100" alt="...">
</div>
<div class="carousel-item">
<img src="https://www.aoki-kyousei.com/img/free/047.jpg" class="d-block w-100" alt="...">
</div>
</div>
</div>
<div class="embed-responsive-item" >
<nav class="navbar navbar-expand-lg navbar-dark" style="padding:20px 0px;" >
<div class="container">
<a class="navbar-brand" href="#">ほげほげ幼稚園</a>
<button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarNav" aria-controls="navbarNav" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarNav">
<ul class="navbar-nav ml-auto">
<li>
<a class="nav-link" href="#">ホーム<span class="sr-only">(current)</span></a>
</li>
<li>
<a class="nav-link" href="#">保育内容</a>
</li>
<li>
<a class="nav-link" href="#">入園案内</a>
</li>
</ul>
</div>
</div>
</nav>
<br>
<br>
<div class="jumbotron-fluid">
<div class="text-center" style="color:#fff;">
<h1 class="display-4">こころのやさしいこどもをそだてる</h1>
<p class="lead">地域とともに歩むあたたかい保育を</p>
<a href="#" class="btn btn-lg btn-primary">私たちについて</a>
</div>
</div>
</div>
</section>
<br>
<br>
<br>
<br>
<br>
<br>
<br>
<br>
<br>
<br>
<footer>
<div class="container">
<a href="#">ホーム</a>
<span>|</span>
<a href="#">保育内容</a>
<span>|</span>
<a href="#">入園案内</a>
</div>
</footer>
</body>
</html>

data-interval="8000"
の8000は8000ミリ秒(8秒)の意味。この値を変えることでスライドの切り替え時間を調整できる。

25.5.カルーセル(フェード)を作る

carousel-fadeクラスを使うことでフェードイン→フェードアウトするタイプの画像切り替え効果を作ることもできる。
前述のコードに
.carousel-fade .carousel-item.active,
.carousel-fade .carousel-item-next.carousel-item-left,
.carousel-fade .carousel-item-prev.carousel-item-right {
z-index: 0;
}
のスタイルと
carousel-fadeのクラス名を追加した。

<!DOCTYPE html>
<html lang="ja">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title></title>
<!-- CSS only -->
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.5.0/css/bootstrap.min.css" integrity="sha384-9aIt2nRpC12Uk9gS9baDl411NQApFmC26EwAOH8WgZl5MYYxFfc+NcPb1dKGj7Sk" crossorigin="anonymous">

<!-- JS, Popper.js, and jQuery -->
<script src="https://code.jquery.com/jquery-3.5.1.slim.min.js" integrity="sha384-DfXdz2htPH0lsSSs5nCTpuj/zy4C+OGpamoFVy38MVBnE+IbbVYUew+OrCXaRkfj" crossorigin="anonymous"></script>
<script src="https://cdn.jsdelivr.net/npm/popper.js@1.16.0/dist/umd/popper.min.js" integrity="sha384-Q6E9RHvbIyZFJoft+2mJbHaEWldlvI9IOYy5n3zV9zzTtmI3UksdQRVvoxMfooAo" crossorigin="anonymous"></script>
<script src="https://stackpath.bootstrapcdn.com/bootstrap/4.5.0/js/bootstrap.min.js" integrity="sha384-OgVRvuATP1z7JjHLkuOU7Xw704+h835Lr+6QL9UvYjZE3Ipu6Tp75j7Bh/kR0JKI" crossorigin="anonymous"></script>
<style>
.navbar-dark .navbar-toggler {
border-color: rgba(0,0,0,0);
}
.navbar-dark .navbar-nav .nav-link {
color: rgba(255, 255, 255, 0.9);
}
.navbar-dark .navbar-nav .nav-link:hover, .navbar-dark .navbar-nav .nav-link:focus {
color: rgba(255, 255, 255, 0.75);
}
.carousel-fade .carousel-item.active,
.carousel-fade .carousel-item-next.carousel-item-left,
.carousel-fade .carousel-item-prev.carousel-item-right {
z-index: 0;
}
</style>
</head>
<body>
<section class="embed-responsive">
<div class="carousel slide carousel-fade" data-ride="carousel" data-interval="8000">
<div class="carousel-inner">
<div class="carousel-item active">
<img src="https://www.aoki-kyousei.com/img/free/045.jpg" class="d-block w-100" alt="...">
</div>
<div class="carousel-item">
<img src="https://www.aoki-kyousei.com/img/free/046.jpg" class="d-block w-100" alt="...">
</div>
<div class="carousel-item">
<img src="https://www.aoki-kyousei.com/img/free/048.jpg" class="d-block w-100" alt="...">
</div>
</div>
</div>
<div class="embed-responsive-item" >
<nav class="navbar navbar-expand-lg navbar-dark" style="padding:20px 0px;" >
<div class="container">
<a class="navbar-brand" href="#">ほげほげ幼稚園</a>
<button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarNav" aria-controls="navbarNav" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarNav">
<ul class="navbar-nav ml-auto">
<li>
<a class="nav-link" href="#">ホーム<span class="sr-only">(current)</span></a>
</li>
<li>
<a class="nav-link" href="#">保育内容</a>
</li>
<li>
<a class="nav-link" href="#">入園案内</a>
</li>
</ul>
</div>
</div>
</nav>
<br>
<br>
<div class="jumbotron-fluid">
<div class="text-center" style="color:#fff;">
<h1 class="display-4">こころのやさしいこどもをそだてる</h1>
<p class="lead">地域とともに歩むあたたかい保育を</p>
<a href="#" class="btn btn-lg btn-primary">私たちについて</a>
</div>
</div>
</div>
</section>
<br>
<br>
<br>
<br>
<br>
<br>
<br>
<br>
<br>
<br>
<footer>
<div class="container">
<a href="#">ホーム</a>
<span>|</span>
<a href="#">保育内容</a>
<span>|</span>
<a href="#">入園案内</a>
</div>
</footer>
</body>
</html>

25.6.キャッチコピーを固定する

ハンバーガーメニューを開くと中央のキャッチコピーが押されて下がる。これなんかダサくない?キャッチコピーを固定表示にしたい。せっかくBootstrap4があるんだから、Bootstrap4のコマンドで何とか処理したい。
まず<div class="embed-responsive-item" >~</div>の外にキャッチコピーを作るhtmlを出しちゃう。その上で<div class="carousel-caption" ~というクラスで包む。元々はカルーセル画像の説明文を記述するためのクラスらしい。なので、これを使えばうまい具合に画像の上に文字が載る。しかも自動で中央揃え+文字色も白にしてくれる。元々説明文のためのクラスなんで画像の下の方に文字が配置される。これはよろしくないのでstyle="top:150px;"を追加して上から150ピクセルの位置に配置するようカスタマイズ。
さらに改良を加えた。例のスクロールしても置いてけになる画像。この画像が明るくて上に載る白い文字が読みにくい。透過率20%の黒い色面を配置して少し暗くしてみた。style="background: rgba(0,0,0,0.2);"を記述するとそれが可能となる。0.2の値を1に近づけるともっと暗くなる。
その他細かい改良を加えた。詳細は以下の通り。

<!DOCTYPE html>
<html lang="ja">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title></title>
<!-- CSS only -->
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.5.0/css/bootstrap.min.css" integrity="sha384-9aIt2nRpC12Uk9gS9baDl411NQApFmC26EwAOH8WgZl5MYYxFfc+NcPb1dKGj7Sk" crossorigin="anonymous">

<!-- JS, Popper.js, and jQueryは標準タイプに変更、これでAjaxも動くよ -->
<script type="text/javascript" src="https://code.jquery.com/jquery-3.4.1.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/popper.js@1.16.0/dist/umd/popper.min.js" integrity="sha384-Q6E9RHvbIyZFJoft+2mJbHaEWldlvI9IOYy5n3zV9zzTtmI3UksdQRVvoxMfooAo" crossorigin="anonymous"></script>
<script src="https://stackpath.bootstrapcdn.com/bootstrap/4.5.0/js/bootstrap.min.js" integrity="sha384-OgVRvuATP1z7JjHLkuOU7Xw704+h835Lr+6QL9UvYjZE3Ipu6Tp75j7Bh/kR0JKI" crossorigin="anonymous"></script>
<style>
/*ハンバーガーメニューの周りにある変な輪郭線を消す*/
.navbar-dark .navbar-toggler {
border-color: rgba(0,0,0,0);
}
/*ハンバーガーメニューをクリックすると出現する変な輪郭線を消す*/
button.navbar-toggler:focus{
outline:none;
}
/*メニューアイテムの文字色を白にする*/
.navbar-dark .navbar-nav .nav-link {
color: rgba(255, 255, 255, 1);
}
/*メニューアイテムを触った時の色をやや暗い白にする。この色指示を変更して全く別の色にしてもいいね*/
.navbar-dark .navbar-nav .nav-link:hover, .navbar-dark .navbar-nav .nav-link:focus {
color: rgba(255, 255, 255, 0.75);
}
/*ハンバーガーメニューの三本線の色を白にする SVGを丸ごと書き直しているのでコマンドが長ったらしい*/
.navbar-dark .navbar-toggler-icon {
background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='30' height='30' viewBox='0 0 30 30'%3e%3cpath stroke='rgba%28255, 255, 255, 1%29' stroke-linecap='round' stroke-miterlimit='10' stroke-width='2' d='M4 7h22M4 15h22M4 23h22'/%3e%3c/svg%3e");
}
/*スクロールしても固定表示になる画像を作る呪文1*/
.callout {
background: url(images/callout-back.jpg) no-repeat fixed 0 0 /cover;
overflow: hidden;
width: 100%;
padding: 0;
}
/*スクロールしても固定表示になる画像を作る呪文2*/
.callout .overlay {
position: relative;
width: 100%;
padding: 240px 0;
}

</style>
</head>
<body>
<!-- メイン画像を配置 -->
<section class="embed-responsive">
<img src="https://www.aoki-kyousei.com/img/free/046.jpg" class="w-100">
<!-- メニューを構成するdiv -->
<div class="embed-responsive-item" >
<nav class="navbar navbar-expand-lg navbar-dark" style="padding:20px 0px;" >
<div class="container">
<a class="navbar-brand" href="#">ほげほげ幼稚園</a>
<button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarNav" aria-controls="navbarNav" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarNav">
<ul class="navbar-nav ml-auto">
<li>
<a class="nav-link" href="#">ホーム<span class="sr-only">(current)</span></a>
</li>
<li>
<a class="nav-link" href="#">保育内容</a>
</li>
<li>
<a class="nav-link" href="#">入園案内</a>
</li>
</ul>
</div>
</div>
</nav>
</div>
<!-- キャッチコピーを構成するdiv -->
<div class="carousel-caption" style="top:150px;">
<div class="jumbotron-fluid">
<h1 class="display-4">こころのやさしいこどもをそだてる</h1>
<p class="lead">地域とともに歩むあたたかい保育を</p>
<a href="#" class="btn btn-lg btn-primary">私たちについて</a>
</div>
</div>
</section>
<!-- スクロールしても固定表示になる画像を構成するdiv -->
<section class="callout" style="background-image:url('https://www.aoki-kyousei.com/img/free/004.jpg');">
<div class="overlay" style="background: rgba(0,0,0,0.2);"><!-- 画像を少し暗くする -->
<div class="container text-center" style="color:#fff;">
<h3>新入園児追加募集・二次募集のお知らせ</h3>
<p>年少・年中・年長追加募集中 お電話で問い合わせください</p>
</div>
</div>
</section>
<!-- ただの空改行 -->
<br>
<br>
<br>
<br>
<br>
<br>
<br>
<br>
<br>
<br>
<!-- フッター -->
<footer>
<div class="container">
<a href="#">ホーム</a>
<span>|</span>
<a href="#">保育内容</a>
<span>|</span>
<a href="#">入園案内</a>
</div>
</footer>
</body>
</html>

25.7.スマホ対応

なんとなくWordPressのサンプルみたいなホームページができた。だがこれはまだスマホに対応していない。このページをスマホで見るとキャッチコピーがケられ、ボタンも隠れる。スマホに対応させようとすると色々面倒が発生する。今回行った改良は以下の通り。

・メディアクエリを使って、表示幅900ピクセル以上の時と899ピクセル以下の時で表示の体裁を変える。
・表示幅899ピクセル以下の時、メイン画像の高さを600ピクセル固定、中央揃えとする。
・スクロールしても固定表示される画像をiOSに対応させる。
・スマホのバウンススクロールへの対策を仕込む。

まずメディアクエリを使って、表示幅900ピクセル以上の時と899ピクセル以下の時で表示の体裁を変える。表示幅900ピクセル以上がPC用、899ピクセル以下がスマホ用の画面構成となる想定。
表示幅900ピクセル以上の時、メイン画像は表示領域いっぱいに拡大縮小表示される。表示幅899ピクセル以下の時、メイン画像は高さ600ピクセルで固定とした。これでキャッチコピーやボタンのケられが解消する。さらに画像を中央揃え表示とした。何もしないと画像は左上角基準で表示されてしまうので基準を画像の中央に移動した。表示領域が狭くなると画像の左右端から隠れ、常に中央が残る。
前回作ったスクロールしても置いてけになる画像はiOSだと無効になる。今回はiOSでもちゃんと表示されるよう改良した。しかし、一方で新たな問題が発生した。隠しておいたものがバレてしまうのだ。
スクロールしても画像が動かないよう、position:fixed;で位置を固定している。そしてこの画像の重ね順を最背面に配置し、上を流れる要素で隠したり見せたりしているだけだ。ただスマホにはバウンススクロールという仕掛けがある。Webページの先頭/最後端でビヨヨーンと1回バウンドするようなリアクションを見せるアレ。スマホがバウンススクロールする時、隠しておいたつもりの固定画像がチラリとバレてしまうのである。これはかっこ悪い。
色々調べた。バウンススクロールを無効化する呪文もあるらしいが、効かねーぞ。
そこで考えた。headerやfooterを固定表示する小技がある。jQueryを仕込んで、少しスクロールすると背景色が変わったり透明度が変わったりする。これだ。この固定header/footerでビヨヨーンの瞬間だけ画像を隠せばいい。スクロール途中では透明度100%にして画像を隠さないようにする。これでイケると思ったが、実は下のサンプルは若干隠しきれていない。大きなスマホでも小さなスマホでも縦表示でも横表示でも、全てのケースで完全に動作させるのは難しい。中途半端な解決となったが、まあ素人の精いっぱいの悪あがきということで。多分、ネットを探せば私よりももっとうまい解決を導いている例は見つかると思う。

今回参考にしたWebページは以下の通り。
https://tnyk.jp/frontend/centering-wide-img/
https://www.nxworld.net/tips/stikcy-or-change-header-and-navigation-when-scrolling-using-jquery.html


<!DOCTYPE html>
<html lang="ja">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title></title>
<!-- CSS only -->
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.5.0/css/bootstrap.min.css" integrity="sha384-9aIt2nRpC12Uk9gS9baDl411NQApFmC26EwAOH8WgZl5MYYxFfc+NcPb1dKGj7Sk" crossorigin="anonymous">

<!-- JS, Popper.js, and jQueryは標準タイプに変更、これでAjaxも動くよ -->
<script type="text/javascript" src="https://code.jquery.com/jquery-3.4.1.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/popper.js@1.16.0/dist/umd/popper.min.js" integrity="sha384-Q6E9RHvbIyZFJoft+2mJbHaEWldlvI9IOYy5n3zV9zzTtmI3UksdQRVvoxMfooAo" crossorigin="anonymous"></script>
<script src="https://stackpath.bootstrapcdn.com/bootstrap/4.5.0/js/bootstrap.min.js" integrity="sha384-OgVRvuATP1z7JjHLkuOU7Xw704+h835Lr+6QL9UvYjZE3Ipu6Tp75j7Bh/kR0JKI" crossorigin="anonymous"></script>
<style>
/*ハンバーガーメニューの周りにある変な輪郭線を消す*/
.navbar-dark .navbar-toggler {
border-color: rgba(0,0,0,0);
}
/*ハンバーガーメニューをクリックすると出現する変な輪郭線を消す*/
button.navbar-toggler:focus{
outline:none;
}
/*メニューアイテムの文字色を白にする*/
.navbar-dark .navbar-nav .nav-link {
color: rgba(255, 255, 255, 1);
}
/*メニューアイテムを触った時の色をやや暗い白にする。この色指示を変更して全く別の色にしてもいいね*/
.navbar-dark .navbar-nav .nav-link:hover, .navbar-dark .navbar-nav .nav-link:focus {
color: rgba(255, 255, 255, 0.75);
}
/*ハンバーガーメニューの三本線の色を白にする SVGを丸ごと書き直しているのでコマンドが長ったらしい*/
.navbar-dark .navbar-toggler-icon {
background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='30' height='30' viewBox='0 0 30 30'%3e%3cpath stroke='rgba%28255, 255, 255, 1%29' stroke-linecap='round' stroke-miterlimit='10' stroke-width='2' d='M4 7h22M4 15h22M4 23h22'/%3e%3c/svg%3e");
/*margin-right:15px;*/
}
/*スクロールしても固定表示になる画像を作る呪文1*/
.callout:before{
content:"";
display:block;
position:fixed;
top:0;
left:0;
z-index:-2;
width:100%;
height:100vh;
background:url(https://www.aoki-kyousei.com/img/free/004.jpg) center no-repeat;
background-size:cover;
}
/*スクロールしても固定表示になる画像を作る呪文2*/
.callout .overlay {
position: relative;
width: 100%;
padding: 240px 0;
}
/*バウンススクロール対策Top端目隠し*/
.blindTop {
position: fixed;
top: 0;
left: 0;
z-index: 0;/*重ね順メイン画像の背面に移動*/
width: 100%;
height: 300px;
background: #5F8EC6;/*空の色に合わせてみた*/
}
/*バウンススクロール対策Bottom端目隠し*/
.blindBottom {
position: fixed;
bottom: 0;
left: 0;
z-index: -1;/*重ね順footerの背面に移動*/
width: 100%;
height: 100px;/*本当は300px欲しいけど被るので*/
background: #fff;
}
/*目隠しが画像に被る時だけ透明化*/
.is-animation {
height: 100px;
background: rgba(0,0,0,0);
}
/* 900px以上の幅の場合に適応されるメディアクエリ */
@media screen and (min-width: 900px) {
    .embed-responsive img{
    width:100%;
    }

}
/* 899pxまでの幅の場合に適応されるメディアクエリ */
@media screen and (max-width: 899px) {
/*メイン画像の高さを固定*/
    .embed-responsive {
    width: auto;
    height: 600px;
    }
/*画像を高さ固定中央揃えにする*/
    .embed-responsive img{
    height:600px;
    position: absolute;
    top: 50%; /* トップを基準に中央配置 */
    left: 50%; /* 左を基準に中央配置 */
    width: 900px;
    margin-top: -300px;
    margin-left: -450px;
    }
/*三本線メニューアイコン15px左へ移動*/
    .navbar-dark .navbar-toggler-icon {
    margin-right:15px;
    }
/*ほげほげ幼稚園タイトルを15px右へ移動*/
    .navbar-brand, .nav-link{
    margin-left:15px;
    }
}

</style>
</head>
<body>
<!-- バウンススクロール対策Top端目隠し -->
<div class="blindTop"></div>
<!-- メイン画像を配置 -->
<section class="embed-responsive">
<img src="https://www.aoki-kyousei.com/img/free/046.jpg">
<!-- メニューを構成するdiv -->
<div class="embed-responsive-item" >
<nav class="navbar navbar-expand-lg navbar-dark" style="padding:20px 0px;" >
<div class="container">
<a class="navbar-brand" href="#">ほげほげ幼稚園</a>
<button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarNav" aria-controls="navbarNav" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarNav">
<ul class="navbar-nav ml-auto">
<li>
<a class="nav-link" href="#">ホーム<span class="sr-only">(current)</span></a>
</li>
<li>
<a class="nav-link" href="#">保育内容</a>
</li>
<li>
<a class="nav-link" href="#">入園案内</a>
</li>
</ul>
</div>
</div>
</nav>
</div>
<!-- キャッチコピーを構成するdiv -->
<div class="carousel-caption" style="top:150px;">
<div class="jumbotron-fluid">
<h1 class="display-4">こころのやさしいこどもをそだてる</h1>
<p class="lead">地域とともに歩むあたたかい保育を</p>
<a href="#" class="btn btn-lg btn-primary">私たちについて</a>
</div>
</div>
</section>

<!-- スクロールしても固定表示になる画像を構成するdiv -->
<section class="callout">
<div class="overlay" style="background: rgba(0,0,0,0.2);"><!-- 画像を少し暗くする -->
<div class="container text-center" style="color:#fff;">
<h3>新入園児追加募集・二次募集のお知らせ</h3>
<p>年少・年中・年長追加募集中 お電話で問い合わせください</p>
</div>
</div>
</section>
<!-- ただの空改行 -->
<div style="background: rgba(255,255,255,1);"><!-- 背景色を白にする -->
<br>
<br>
<br>
<br>
<br>
<br>
<br>
<br>
<br>
<br>
</div>
<!-- フッター -->
<footer style="background: rgba(255,255,255,1);"><!-- 背景色を白にする -->
<div class="container">
<a href="#">ホーム</a>
<span>|</span>
<a href="#">保育内容</a>
<span>|</span>
<a href="#">入園案内</a>
</div>
</footer>
<!-- バウンススクロール対策Bottom端目隠し -->
<div class="blindBottom"></div>

<script>
/*バウンススクロール対策Top端目隠し発動*/
$(function() {
    var $win = $(window),
       $blindTop = $('.blindTop'),
       animationClass = 'is-animation';

    $win.on('load scroll', function() {
        var value = $(this).scrollTop();
        /*100pxスクロールしたら目隠しを透明に*/
        if ( value > 100 ) {
            $blindTop.addClass(animationClass);
        } else {
            $blindTop.removeClass(animationClass);
        }
    });
});
/*バウンススクロール対策Bottom端目隠し発動*/
$(function() {
    var $win = $(window),
       $blindBottom = $('.blindBottom'),
       animationClass = 'is-animation';

    $win.on('load scroll', function() {
        var value = $(this).scrollTop();
        /*700pxスクロールしたら目隠しを不透明に*/
        if ( value > 700 ) {
            $blindBottom.removeClass(animationClass);
        } else {
            $blindBottom.addClass(animationClass);
        }
    });
});
</script>
</body>
</html>

26.Raspberry Pi OS Busterを使ってみる

久しぶりにラズベリーパイをイチからセットアップしようとしてとまどった。Raspberry Piのホームページhttps://www.raspberrypi.org/がずいぶん様変わりしてしまった。Raspbianはどこにあるのか。上のメニューのSoftwareを開いてみる。Raspberry Pi OSと書いてある。WindowsやMac、x86系CPUのためのimageファイルが置いてある。違うそれじゃない。Manually install an operating system imageにSee all optionsと書かれたボタンがある。この先でいわゆるRaspberry Pi用の Raspberry Pi OSが入手できるようだ。Raspberry Pi OS with desktop and recommended software、これがいわゆる「全部入り」らしい。これをダウンロードすることにする。
どうやらRaspbianはRaspberry Pi OSという名前に替わったようだ。
いつもの手順に従ってSDカードにimageファイルを移し、Raspberry Piに挿入して起動。初回はセットアップウィザードが走る。
途中、Set Up Screenの画面
□The screen shows a black border around the desktop(画面の周囲に黒い枠が見えている)をチェック。後で画面サイズを最適化してくれるようだ。便利になったものだ。
自宅Wi-Fiをサーチして暗号化キーを入力すると勝手にUpdateを始める。一定時間操作がないとRaspberry Piの画面が暗転する。スリープ機能を覚えたようだ。
全部入りとはいえ何が最初から入っているのか。
ntfs-3gは最初から入っているようだ。NTFS形式のHDDを簡単に認識した。VLCも最初から入っているようだ。MP3ファイルをダブルクリックするとVLCが起動して音を出す。初歩的なところでいちいちつまづくことがない。
sambaは入っていないようだ。WindowsPCとファイル共有したいのでsambaをインストールする。
sudo apt install samba
途中「DHCP から WINS 設定を使うよう smb.conf を変更しますか? 」と聞かれ「いいえ」が選択されている。そのままEnterを押して継続。
続けてラズベリーパイにLAMP環境を構築する。
前回の手法(17.1.ラズベリーパイにLAMP環境を構築する)ではエラーが出るようになった。mysql-serverはmariadb-server-10.0に置き換えられたという。
sudo apt install apache2
でapache2をインストール。
sudo apt install php
でphpをインストール。最新のphp7.3がインストールされる。
sudo apt install mariadb-server
でmariadb-serverをインストール。
sudo apt install phpmyadmin
途中「phpMyAdmin を動作させるために自動再設定を行う web サーバを選んでください」と聞かれ「apache2」が選択されている。そのままEnterを押して継続。
途中「phpmyadmin 用のデータベースを dbconfig-common で設定しますか? 」と聞かれ「はい」が選択されている。そのままEnterを押して継続。
さらに「phpmyadmin 用のパスワードを入力してください」と聞かれ任意のパスワード入力してEnter。確認のため再度パスワード入力してEnter。
これでインストール完了。
ついでにApache設定ファイルを編集する(17.4.Apache設定ファイルを編集する参照)
ターミナルを開いて以下を入力。
$ sudo nano /etc/apache2/apache2.conf
最後の行に以下を入力。赤字のところは好きな値をセットしてよい。
<Directory "/var/www">
php_value max_execution_time 100
php_value memory_limit 256M
php_value post_max_size 256M
php_value upload_max_filesize 256M
</Directory>
そしてリブート。
文字コードはデフォルトでutf8mb4に設定されているようなのでいじらない。
最後に/var/www/フォルダのパーミッションを変更する。
sudo chmod -R 777 /var/www/

27.シェルスクリプトを使ってみる

実現したかったのは特定のファイルをダブルクリックすることでPythonプログラムが実行される環境。いちいちターミナルを開いてpyhon3 xxxx.pyと打ち込む作業が面倒だった。前回作ったクソPythonプログラムmezamashi.pyを自動実行するシェルスクリプトを書く。
まず、mezamashi.pyのおさらい。後に書くがこのスクリプトには2か所の罠がある。

#!/usr/bin/env python
# -*- coding:utf-8 -*-

import os
import glob
import tkinter
from tkinter import ttk
import datetime
import pygame.mixer
import threading
import time

alarmtime = "060000"
volume_up = False
PLAY_END = pygame.USEREVENT
unixplaytime = 0
files = glob.glob('./Music/*.mp3')
musiclist=[]
for i in files:
    title = os.path.basename(i)
    musiclist.append(title)
musiclist.sort()
n = 0
nextmusic = musiclist[n]

root = tkinter.Tk()
root.option_add('*font','12')
root.geometry("200x120")

note = ttk.Notebook(root)
tab1 = ttk.Frame(note)
tab2 = ttk.Frame(note)
note.add(tab1,text="Alarm Clock")
note.add(tab2,text="Set Alarm")
note.pack()


def play():
    pygame.mixer.init()
    pygame.mixer.music.load("Music/"+nextmusic)
    pygame.mixer.music.play(1)

def tone():
    global volume_up
    for i in range(60):
        i+=1
        if volume_up is False:
            pygame.mixer.music.set_volume(1)
            break
        else:
            pygame.mixer.music.set_volume(i/60)
            time.sleep(10)

def alarm():
    play()
    global unixplaytime
    unixplaytime=int(datetime.datetime.now().timestamp())+600
    pygame.mixer.music.set_endevent(PLAY_END)
    global volume_up
    volume_up = True
    thread = threading.Thread(target=tone)
    thread.start()

def stop():
    global unixplaytime
    pygame.mixer.music.stop()
    changemusic()
    unixplaytime=0
    global volume_up
    volume_up = False

def changemusic():
    global n
    global musiclist
    global nextmusic
    num = len(musiclist)
    n+=1
    nextmusic = musiclist[n]
    if n > (num-2):
        n = -1

label = tkinter.Label(tab1,text=datetime.datetime.now().strftime('%Y/%m/%d %H:%M:%S'))
label.pack()
button = tkinter.Button(tab1,text='Play',command=play)
button.pack()
button = tkinter.Button(tab1,text='Stop',command=stop)
button.pack()

EditBox = tkinter.Entry(tab2)
EditBox.insert(tkinter.END,alarmtime)
EditBox.pack()

def timecount():
    global PLAY_END
    global unixplaytime
    pygame.init()
    root.after(1000,timecount)
    label.configure(text=datetime.datetime.now().strftime('%Y/%m/%d %H:%M:%S'))
    alarmtime = EditBox.get()
    if datetime.datetime.now().strftime('%H%M%S') == alarmtime:
        alarm()
    for event in pygame.event.get():
        if event.type == PLAY_END:
            if int(datetime.datetime.now().timestamp())<unixplaytime:
                play()
            else:
                unixplaytime=0
    if datetime.datetime.now().strftime('%H%M%S') == '120000':
        changemusic()

timecount()
root.mainloop()
exit()


これを/home/piフォルダの中に置いて起動するとMusicフォルダ内のmp3ファイルを鳴らす目覚まし時計になるというもの。
/home/piフォルダを開いて右クリック、New File...で新規にstartmezamashi.shファイルを作り以下を記述。

#!/usr/bin/bash
sudo python3 mezamashi.py

そして保存。次にstartmezamashi.shのパーミッションを変更する。startmezamashi.shの上で右クリック。「ファイルのプロパティ」を選択、パーミッションのタブを開く。「実行」が「なし」になっていたらこれを「なし」以外のどれかにする。
パーミッションの変更はターミナルからでもできる。ターミナルを開いて
chmod 777 stratmezamashi.shと入力。
これで stratmezamashi.shをダブルクリック→「実行」を選択するとmezamashi.pyが起動する。
では stratmezamashi.shをデスクトップに置いたらどうなるか。
起動しない。
(★エクスプローラーといっていいかどうかわからないが、エクスプローラー風フォルダビューの中でDesktopにドラッグアンドドロップすると「移動」ではなくデスクトップにshファイルのショートカットが作成される。これもショートカットといってよいものか、Raspberry Pi OSではこのショートカットファイルをシンボリックリンクと呼んでいる。shファイルを選択して、「編集」メニューを開き、「シンボリック・リンクを作成する」を選んでも結果は同じ。)
実は3つの罠がある。
まず1つ目の罠。stratmezamashi.shの内容を絶対パスに書き換える必要がある。

#!/usr/bin/bash
sudo python3 /home/pi/mezamashi.py

2つ目と3つ目の罠はmezamashi.pyのスクリプトの中にある。相対パスで記述された2か所を絶対パスに書き換える必要がある。
15行目、files = glob.glob('./Music/*.mp3')files = glob.glob('/home/pi/Music/*.mp3')
44行目、pygame.mixer.music.load("Music/"+nextmusic)pygame.mixer.music.load("/home/pi/Music/"+nextmusic)
そもそもスクリプト内の記述が相対パスのままではmezamashi.pyの置き場所がpiフォルダの中に限定される。
スクリプト内に相対パスを記述すると色々罠にはまりやすいことがわかった。
★後でわかったことだが、スクリプトの先頭付近に以下の呪文を書いておくと、以下相対パスのままでも問題ないらしい。通常はshファイルの置いてある場所が実行環境の起点となるため、相対パスが崩れるのだが、この呪文を書いておけば、それを回避できるらしい。
import os
os.chdir('/home/pi/')

あと、スクリプトの先頭に書くこの宣言文はやっぱりpython3としておくべきらしい。

#!/usr/bin/env python3

28.OLED display HATを使う

1.3inch OLED display HATというと表示デバイス、ラズベリーパイのGPIOに挿して使う。1.3インチのモノクロ有機ELディスプレーと上下左右キー、3つのタクトスイッチが一体となった基板。詳細はこちら↓
*https://www.waveshare.com/product/displays/lcd-oled/lcd-oled-3/1.3inch-oled-hat.htm
今日はこれを使って遊んでみる。とりあえずラズベリーパイのGPIOに1.3inch OLED display HATを挿す。

28.1.ドライバをインストールする

上記Webページの一番下にDevelopment Resourcesというリンクがある。こちらに(不親切な)マニュアルやデモコードが用意されている。まずはマニュアルに従いドライバをインストールしていく。
その前にラズベリーパイに最新のRaspberry Pi OSをインストールする。
*https://www.raspberrypi.org/software/operating-systems/#raspberry-pi-os-32-bit
↑ここに行って「全部入り」Raspberry Pi OS with desktop and recommended softwareをダウンロードする。Bit torrentが使える人は「Download torrent」を利用した方が多分ちょっとだけ速くダウンロードできる。

Raspberry Pi OSのインストールにかかせないアプリ、Win32 DiskImagerのホームページ*の印象が変わっていたので追記。
*https://ja.osdn.net/projects/sfnet_win32diskimager/
最新ダウンロードファイルとかいってdiskimager_ja.qm (日付: 2018-06-08, サイズ: 9.12 KB)みたいなものを勧めてくる。qmファイルについて私は何も知らない。「ダウンロードファイル一覧」を開くと一覧の中にwin32diskimager-1.0.0-install.exeを発見できる。必要なのはこれこれ。こっちは初心者なんだから最初からexeファイルのインストーラーを勧めてくれないと困る。

全部入りとは言いつつ何が入っているのか?インストール後に
$ sudo pip list
を実行して中身を調べてみる。
機械学習で使う数値計算拡張モジュールNumPyが入っている。JPEGファイルの描画に必須なPillowも入っている。

ではマニュアルに従い作業を進める。まずRaspberry PiのSPI機能をONにする。
$ sudo raspi-config
を実行、下キー2回たたいて
3 Interface Optionsを選択
Tabキー1回たたいて<Select>を選択してEnter
さらに下キー3回たたいて
4 SPIを選択
Tabキー1回たたいて<Select>を選択してEnter
Would you like the SPI interface to be enable?
と聞いてくるのでTabキー2回または左キー1回で<はい>を選択してEnter
The SPI interface is enabled
<了解>を確認してEnter
Tabキー2回または左キー1回で<Finish>を選択してEnter
raspi-configを抜ける。

そしてドライバのインストール
■Python2用
$ sudo apt-get install python-dev python-pip libfreetype6-dev libjpeg-dev
$ sudo -H pip install --upgrade luma.oled
■Python3用
$ sudo apt-get install python3-dev python3-pip libfreetype6-dev libjpeg-dev
$ sudo -H pip3 install --upgrade luma.oled
両方インストールしておく。

28.2.デモコードを試す

デモコードとはデモのプログラム。上記Development Resourcesのページ、●Demo codeのリンク先にデモコードが置いてある。1.3inch-OLED-HAT-Code.7z、なんと7-Zipの圧縮ファイルだ。開発者は7-Zipがお好きね。面倒だがこれを取り出すには7-Zipが必要だ。
ファイルを解凍するとJetsonNano、RaspberryPiのふたつのフォルダ。その先にC、python2、python3のフォルダが現れる。今回はpython3用コードを試そうと思う。python3フォルダをRaspberry Piのホームフォルダの中にコピーする。
$ cd python3
でpython3フォルダの中に移動。
$ sudo python3 main.py
pic.bmpを表示するだけのプログラム。コードの中のpic.bmpをpic.jpgに書き換えても同じ画像が表示される。
もうひとつのデモコードも試してみる。
$ sudo python3 key_demo.py
押されたキー名称をプリントするだけのプログラム。
以上、ドライバをインストールして使える状態にするまでのプロセスを記録した。

29.PyQtを使う

PyQtはTkinterと並ぶGUI用モジュール。PyQtyで前回「10.Tkinterを使ってみる」で作成した目覚まし時計プログラムを作り直してみる。

29.1.PyQtをインストールする

WindowsPC上で製作している。WindowsPCにPyQtをインストールする。
pip install SIP
pip install PyQt5
SIPモジュールも同時にインストールした方がいいらしい。なんのためかは知らない。
ラズベリーパイにインストールする時はターミナルを開いて以下を入力
sudo apt-get install python3-pyqt5
最新のRaspberry Pi OS(全部入り)には最初から入っているようだ。

29.2.目覚まし時計プログラムを作る

「10.4.ttkを使ってみる」の項で作ったものをPyQtに置き換える。タブを備え、「Set Alarm」タブの中にあるテキストボックスに6桁の数列(hhmmss)を入力して目覚まし時刻をセットする。セットした時刻になると同じディレクトリに置いてあるMP3ファイルを再生する。MP3ファイルがなければエラーとなる。例外処理なんていう凝ったことはしていない。

#!/usr/bin/env python3
import sys
from PyQt5.QtWidgets import (QMainWindow, QApplication,
                                                    QPushButton, QWidget,
                                                    QTabWidget, QLabel,
                                                    QLineEdit)
from PyQt5.QtCore import QTimer
import datetime
import pygame.mixer

alarmtime = "060000"

#Define play function
def play():
    pygame.mixer.init()
    pygame.mixer.music.load("your_music_file.mp3")
    pygame.mixer.music.play(1)

#Define stop function
def stop():
    pygame.mixer.music.stop()

#Define timecount function
def timecount():
    dt = datetime.datetime.now()
    dt_str = dt.strftime('%Y/%m/%d %H:%M:%S')
    label1.setText(dt_str)
    alarmtime = textbox.text()
    if datetime.datetime.now().strftime('%H%M%S') == alarmtime:
        play()

app = QApplication(sys.argv)

# Initialize tab screen
tabs = QTabWidget()
tab1 = QWidget()
tab2 = QWidget()

# Add tabs
tabs.addTab(tab1,"Alarm Clock")
tabs.addTab(tab2,"Set Alarm")

# Create play button on tab1
pushButton1 = QPushButton(parent=tab1, text='Play')
pushButton1.move(135, 50)
pushButton1.resize(130, 40)

pushButton1.clicked.connect(play)

# Create stop button on tab1
pushButton2 = QPushButton(parent=tab1, text='Stop')
pushButton2.move(135, 100)
pushButton2.resize(130, 40)

pushButton2.clicked.connect(stop)

# Create timer label on tab1
label1 = QLabel(tab1)
label1.move(125, 10)
label1.resize(300, 20)

# Create textbox label on tab2
textbox = QLineEdit(tab2)
textbox.move(50, 60)
textbox.resize(300, 40)
textbox.setText(alarmtime)

# Initialize timer
timer = QTimer()
timer.timeout.connect(timecount)
timer.start(1000) # 1000ミリ秒

tabs.setWindowTitle('Alarm Clock')
tabs.setGeometry(100, 100, 400, 200)
tabs.show()
sys.exit(app.exec_())

29.3.Style Sheetを使ってみる

PyQtはスタイルシートというものを挿入してボタン類の装飾ができる。しかも、そのやり方はCSS(カスケードスタイルシート)と類似していて、CSSをかじった人には理解しやすい。
上記のプログラムにスタイルシートを追記して、UI画面を黒地に白文字としてみる。画面内におけるボタンの位置はスタイルシートで指定できないらしい。

上記プログラムの
app = QApplication(sys.argv)

tabs.show()
の間のどこかに、以下の文を挿入する。

style = """
    QWidget{color: white; background-color: black;}
    QPushButton{border: 1px solid white; height: 40px; width: 130px}}
    """
app.setStyleSheet(style)

Windowsではタブの背景色が白く飛んで表示されてしまうがRaspberry Piだと問題なく表示される。
スタイルシートでボタンサイズを指示しているので、プログラム中の以下の2文は消してよい。
pushButton1.resize(130, 40)
pushButton2.resize(130, 40)

30.tkinterで監視カメラを作る

資源ごみの回収場所に不燃ごみを放置していく不届き者を特定するため監視カメラを作ろうと思う。買えば早いがラズベリーパイを使えば安く作れるのではないかと考えた。しかし、その前にtkinterのおさらい。

30.1.tkinterのラベルとボタンのおさらい

TkinterはPython3ではtkinterと呼ばれるようになった。大文字が小文字に変化しただけだが、プログラムでは大文字小文字を区別するので気を付けないといけない。さて、ラベルとボタンはtkinterの中でも重要なウィジェットだ。どうやらtkinterのラベルとボタンにはそれぞれ3種類の形があるらしいので、それをおさらいする。

# coding: utf-8
import tkinter

def change_lb1(lb1_text):
    if lb1_text.get() == "piyo":
        lb1_text.set("hoge")
    else:
        lb1_text.set("piyo")

def bt2_click(event):
    if lb2["text"] == "hoge":
        lb2["text"] = "piyo"
    else:
        lb2["text"] = "hoge"

def change_lb3():
    if lb3.cget("text") == "hoge":
        lb3.configure(text = "piyo")
    else:
        lb3.configure(text = "hoge")

root = tkinter.Tk()

lb1_text = tkinter.StringVar()
lb1_text.set("hoge")

lb1 = tkinter.Label(root,textvariable=lb1_text)
lb1.grid(row=0, column=0, padx=10, pady=10)
bt1 = tkinter.Button(root, text="btn1",
        command=lambda: change_lb1(lb1_text))
bt1.grid(row=0, column=1, padx=10, pady=10)

lb2 = tkinter.Label(text="hoge")
lb2.grid(row=1, column=0, padx=10, pady=10)
bt2 = tkinter.Button(root, text="btn2")
bt2.bind("<Button-1>", bt2_click)
bt2.grid(row=1, column=1, padx=10, pady=10)

lb3 = tkinter.Label(text="hoge")
lb3.grid(row=2, column=0, padx=10, pady=10)
bt3 = tkinter.Button(root, text="btn3",
        command=change_lb3)
bt3.grid(row=2, column=1, padx=10, pady=10)

root.mainloop()

3つのラベルと3つのボタンが並ぶ。各ボタンを押すと各ラベルがhoge→piyoへチェンジする。しかし、3つのラベルの文字の差し替え方、3つのボタンの発火の仕方はそれぞれ違う。
1番目のラベルはtextvariableとかStringVar()とかいう入れ物を使って、その中身を入れ替える方式
1番目のボタンはlambdaを使って引数つき関数をボタンに関連づけるやり方
2番目のラベルはラベル自体がディクショナリの構造を持つことに着目して["text"]キーの中身を入れ替える方式
2番目のボタンは<Button-1>すなわちマウス左クリックのアクションに関数を紐づけるやり方
3番目のラベルはconfigureメソッドでtextを入れ替える方式
3番目のボタンはcommandにそのまま関数を割り当てるやり方
今回ラベルとボタンの配置はpackでなくgridを使ってみた。

30.2.ラズパイ用カメラを接続する

AliExpressでラズベリーパイ3用カメラモジュールを買った。300円台で買える。解像度は1080x720p。カメラからフレキケーブルが伸びている。これをラズベリーパイ3のイヤホンジャックとHDMIポートの間にあるコネクタに挿す。

30.3.カメラを動作させる

picamera2というモジュールを使ってカメラを動作させる。まずpicamera2のGitHub*に行ってマニュアルをもらう。
*https://github.com/raspberrypi/picamera2?tab=readme-ov-file

You can find documentation here which should help you to get started.

というところからマニュアルをゲットする。
マニュアルによれば、カメラで録画してMP4動画ファイルを得るまでのプログラムはたった3行。

from picamera2 import Picamera2
picam2 = Picamera2()
picam2.start_and_record_video("test.mp4", duration=5)

録画のためのメソッドはふたつの引数を要求する。ひとつは録画ファイルのファイル名、例では"test.mp4"。もうひとつは録画時間、例では5秒。簡単である。
要するにtkinterでUI作って、このふたつの引数を渡すようなプログラムを組めば監視カメラ完成である。

30.4.任意の録画時間を選択するspinboxを作る

spinboxとは▲▼のボタンを押して選択肢の中からひとつを選ばせるウィジェット。10分から180分(3時間)までを10分刻みで任意に選ばせるUIを作ろうと思う。初期値は10分でなく30分にしておこう。どうやって作る? ここ*のプログラムをそのままパクる。
*https://tomtom-stock.com/2023/02/12/tkinter-spinbox/

import tkinter

def select_val(val):
    now_value = val.get()
    print(now_value)

root = tkinter.Tk()

val = tkinter.IntVar()
val.set('30')

lb1 = tkinter.Label(text="Recording time (minutes):")
lb1.grid(row=0, column=0, padx=10, pady=10)
sp1 = tkinter.Spinbox(
        textvariable=val,
        from_=10,
        to=180,
        increment=10,
        command=lambda: select_val(val))
sp1.grid(row=0, column=1, padx=10, pady=10)

root.mainloop()

なるほどこうするのか。

30.5.監視カメラを作る

0前出のプログラムにpicamera2の処理を合体する。
最後はシャットダウンして終わる仕様にした。万一ラズベリーパイとカメラを盗まれても用意にログインできないようにするためだ。もちろん自動ログインのチェックは外す。

#!/usr/bin/env python3
import tkinter
from datetime import datetime
from picamera2 import Picamera2
import time
import subprocess

def select_val(val):
    now_value = val.get()
    print(now_value)

def start(val):
    label_text.set("Recording")
    root.update()
    now_value = val.get()
    for i in range(int(now_value / 10)):
        output=datetime.now().strftime('%Y%m%d_%H%M%S') + ".mp4"
        picam2.start_and_record_video(output, duration = 600)
        print(output + " was saved")
        label_text.set(output + " was saved")
        root.update()
    label_text.set("Finished")
    time.sleep(2)
    subprocess.run(('/sbin/shutdown', '-h', '1'))

root = tkinter.Tk()
picam2 = Picamera2()
val = tkinter.IntVar()
val.set('30')
label_text = tkinter.StringVar()
label_text.set("Stand-by")

lb1 = tkinter.Label(text="Recording time (minutes):")
lb1.grid(row=0, column=0, padx=10, pady=10)
sp1 = tkinter.Spinbox(
        textvariable=val,
        from_=10,
        to=360,
        increment=10,
        command=lambda: select_val(val))
sp1.grid(row=0, column=1, padx=10, pady=10)
bt1 = tkinter.Button(root, text="Start",
        command=lambda: start(val))
bt1.grid(row=1, column=0, columnspan = 2, padx=10, pady=10)
lb2 = tkinter.Label(root,textvariable=label_text )
lb2.grid(row=2, column=0, columnspan = 2, padx=10, pady=10)

root.mainloop()

30.6.ラズベリーパイをモバイルバッテリーで動かす

さて監視カメラである以上、屋外でバッテリー駆動させる必要がある。今回はスマホ用のごく一般的なモバイルバッテリーに接続した。最大出力は2.1A。本来、ラズベリーパイを駆動するにはちょっと足りない電流値だけど、tkinter監視カメラプログラムを動かすだけならなんとか足りている模様。