t-hom’s diary

主にVBAネタを扱っているブログ…とも言えなくなってきたこの頃。

VBA 数値をふりがなに変換するマクロ

今回のマクロは、数値をふりがなに変換するマクロの紹介。
例えば1なら「いち」、290なら「に / ひゃく / きゅう / じゅう」という具合だ。

これの何が難しいかというと、単純に数値の読み「1~9」と位の読み「じゅう・ひゃく・せん」を組み合わせればできるというものでもないってところ。

たとえば8は「はち」。
800は?

はちひゃくと呼んでも意味は伝わるけど、普通は「は / ゃく」だ。
2000は、「に / せん」。
3000は?

「さん & ぜん」と読む。

色々整理したところ、例外も含めた規則が分かってきたのでコードに落とし込んでみた。

規則

  • 1の位は普通に読む。
  • 1の位以外の位に「1」が出てきたら「いち」は読まずに位だけを読む。
  • ある位に「0」が出てきたら、読まずに次の位に移る。
  • 百の位が3の場合、位は「びゃく」と読む
  • 百の位が6の場合、6は「ろっ」と読み、位は「ぴゃく」と読む。
  • 百の位が8の場合、8は「はっ」と読み、位は「ぴゃく」と読む。
  • 千の位が3の場合、位は「ぜん」と読む。
  • 千の位が8の場合、8は「はっ」と読む。
  • 万より上の位は普通。ただし一万を超える値において千の位に「1」が来た場合、「いっ」と読んでも良い。

コード

出力シートのオブジェクト名はOutputSheetに変更したうえで、標準モジュールに次のコードを書いて実行する。

Option Explicit
Sub NumberToPronounciation()
    Dim numWord: numWord = Split("ぜろ いち に さん よん ご ろく なな はち きゅう")
    
    Dim i
    For i = 0 To 9999
        Dim numAsText: numAsText = Format(i, "0000")
        Dim thousandsPlace: thousandsPlace = Int(Mid(numAsText, 1, 1))
        Dim hundredsPlace: hundredsPlace = Int(Mid(numAsText, 2, 1))
        Dim tensPlace: tensPlace = Int(Mid(numAsText, 3, 1))
        Dim onesPlace: onesPlace = Int(Mid(numAsText, 4, 1))
        
        Dim c As Collection: Set c = New Collection
        Select Case thousandsPlace
        Case 0
            'Do nothing
        Case 1
            c.Add "せん"
        Case 3
            c.Add "さん"
            c.Add "ぜん"
        Case 8
            c.Add "はっ"
            c.Add "せん"
        Case Else
            c.Add numWord(thousandsPlace)
            c.Add "せん"
        End Select
        
        Select Case hundredsPlace
        Case 0
            'Do nothing
        Case 1
            c.Add "ひゃく"
        Case 3
            c.Add "さん"
            c.Add "びゃく"
        Case 6
            c.Add "ろっ"
            c.Add "ぴゃく"
        Case 8
            c.Add "はっ"
            c.Add "ぴゃく"
        Case Else
            c.Add numWord(hundredsPlace)
            c.Add "ひゃく"
        End Select

        Select Case tensPlace
        Case 0
            'Do nothing
        Case 1
            c.Add "じゅう"
        Case Else
            c.Add numWord(tensPlace)
            c.Add "じゅう"
        End Select
        
        Select Case onesPlace
        Case 0
            If c.Count = 0 Then
                c.Add "ぜろ"
            End If
        Case Else
            c.Add numWord(onesPlace)
        End Select
        
        Dim result As String: result = ""
        Dim j
        For j = 1 To c.Count
            result = result & c(j)
        Next
        OutputSheet.Cells(i + 1, 1).Value = i
        OutputSheet.Cells(i + 1, 2).Value = result
    Next
End Sub

実行結果

こんな感じで出力される。
f:id:t-hom:20191013215453p:plain
※ブログ用に画像加工で横に並べてますが出力はA列とB列だけです。

実際にやりたかったこと

実際にやりたいのは、Raspberry Piを使った数値の音声読み上げである。1.wav~9.wav, jyu.wav, hyaku.wav, sen.wavのように予め発話用のwavファイルを用意しておき、それらを組み合わせて数値を発音させる。

「体重データをもとに、必要エネルギーを計算します。あなたの体重は」
「はち」
「じゅう」
「よん」
「てん」
「に」
「キロです。」
「1カ月あたり1キロやせたい場合、本日の摂取エネルギーの上限は」
「に」
「せん」
「さん」
「びゃく」
「ろく」
「じゅう」
「なな」
「キロカロリーです。」

そんなことしなくてもLinuxにはフリーの音声合成もあるので、リアルタイムで数字そのまま読み上げさせることもできるんだけど、どうにも声が気に入らないし、購入済のVOICEROIDで作ったwavファイルと連続で読み上げさせるので、数値だけ別の声というのも違和感がある。それで検討した結果、wav組み合わせ方式を採用することにした。

一応、得意なVBAでの検証が済んだのでこのあとPythonコードに翻訳し、「いち」や「に」等の出力の代わりに「1.wav」「2.wav」を再生するように改変して出来上がりの予定。

VBA 減量のためのカロリー計算

ちょっと最近太りぎみなのでそろろそマジでダイエットでもしようかと思い立ったところ、世の中には種々様々なダイエット法が存在していて、一体どうすれば良いのか混乱する。

ただ基本的には、シンプルにこういう式で表すことができる。
 摂取エネルギー > 消費エネルギー →太る
 摂取エネルギー < 消費エネルギー →痩せる
 摂取エネルギー = 消費エネルギー →維持

それで、変なダイエット法に当たるくらいなら、ちゃんとカロリー計算した方が良いなと思って、マクロにしてみた。

データを集める

厚労省の資料から

WHOとかの研究データをもとに書かれてるので信頼できる。
https://www.mhlw.go.jp/file/05-Shingikai-10901000-Kenkoukyoku-Soumuka/0000083871.pdf

P54 3─2─3.目標とする BMI の範囲

ここには、統計データから最も死亡率の低いBMI値が掲載されている。
対象年齢ごとに目標BMIが異なるが、全年齢をカバーするのが21.5~24.93であることが分かった。

よって、その中間値 23.215 を目標に定めることにした。

達成後も21.5~24.93からはみ出ないように23.215に向けて調整し続ければ良い。

P56・P57

ここにとても重要なことが書かれている。

なお、脂肪細胞 1 gが7 kcal を有すると仮定すれば、100 kcal/日のエネルギー摂取量の減少は14.3 g/日の体重減少、つまり、5.21 kg/年の体重減少が期待できるが、上記のようにそうはならない。これは、主として、体重の減少に伴ってエネルギー消費量も減少するためであると考えられる。体重の変化(減少)は徐々に起こるため、それに呼応してエネルギー消費量も徐々に減少する。そのため、時間経過に対する体重の減少率は徐々に緩徐になり、やがて、体重は減少しなくなる。

~中略~

f:id:t-hom:20191012194257p:plain

つまり、体重変化の度に、必要エネルギー量は計算し直さないといけないのだ。

P65 4─2.基礎代謝基準値

ここには基礎代謝を求める計算式が書かれている。

年齢、性別、身長、体重を用いた下記の日本人の基礎代謝量の推定式 170)は、BMI が 30 kg/m2 程度までならば体重による系統誤差を生じないことが示されており 35)、BMI が 25~29.9 kg/m2 の肥満者では、この推定式で基礎代謝量の推定が可能である。

基礎代謝(kcal/日)=〔0.0481×体重(kg)+0.0234×身長(cm)-0.0138×年齢(歳)-定数(男性:
0.4235、女性:0.9708)〕×1000/4.186

P67~69

ここには活動レベルⅠ~Ⅲの定義と年齢を考慮した活動量がある。
ただ成人してからはそんなに変わらないので、Ⅰが1.5、Ⅱが1.75、Ⅲが2と覚えればよさげ。
私はデスクワーク中心なので、意識的に運動しない日は1.5、運動する日は1.75ということにした。

P70

最後に、推定エネルギー必要量(kcal/日)=基礎代謝量(kcal/日)×身体活動レベルという計算式が出てくる。
これでPDFから取得する材料はおしまい。

その他の情報

ググってみると、脂肪1キロあたりのカロリーは7200kcalとのこと。
また、痩せるスピードとしては1月で1キロくらいがベストらしい。
つまり30日で7200kcalのマイナスなので、推定エネルギー必要量よりも1日あたり240kcal少ない食事量にすれば、1キロ/月ペースで痩せることができる。

コーディング

シートモジュール

まずシミュレーション結果を表示させるシートのオブジェクト名をSimulationSheetに変更する。
f:id:t-hom:20191012195622p:plain

コードは次のとおり。

Private Cursor As Long
Private Enum Col
    日付 = 1
    想定体重
    活動レベル
    摂取カロリー目安
    BMI
End Enum
Private Const HEADER_ROW = 1

Sub Init()
    Me.Cells.Delete
    Cursor = HEADER_ROW + 1
    Me.Cells(HEADER_ROW, Col.日付) = "日付"
    Me.Cells(HEADER_ROW, Col.想定体重) = "想定体重"
    Me.Cells(HEADER_ROW, Col.活動レベル) = "活動レベル"
    Me.Cells(HEADER_ROW, Col.摂取カロリー目安) = "摂取カロリー目安"
    Me.Cells(HEADER_ROW, Col.BMI) = "BMI"
End Sub

Sub WriteRecord(day, weight, activity_level, cal_intake, bmi_)
    Me.Cells(Cursor, Col.日付) = day
    Me.Cells(Cursor, Col.想定体重) = weight
    Me.Cells(Cursor, Col.活動レベル) = activity_level
    Me.Cells(Cursor, Col.摂取カロリー目安) = cal_intake
    Me.Cells(Cursor, Col.BMI) = bmi_
    Cursor = Cursor + 1
End Sub

標準モジュール

標準モジュールは次のとおり。

'パラメーター(自分のデータを入れる)
Const 初期体重 = 85
Const 身長 = 176.8
Const 運動頻度 = 3  ' 日に1回
Const 減量目標 = 30 ' 日ごとに1キロ
Const 性別 = "男"
Const 開始日 = #10/12/2019#

'固定係数(基本さわらない)
Const 体重係数 = 0.0481
Const 身長係数 = 0.0234
Const 年齢係数 = 0.0138
Const 男性係数 = 0.4235
Const 女性係数 = 0.9708
Const 理想BMI = 23.215
Const 脂肪1に対するCal = 7200

Sub Main()
    Dim 体重 As Double: 体重 = 初期体重
    Dim 経過日数 As Long: 経過日数 = 0
    SimulationSheet.Init
    Do
        Dim 身体活動レベル As Double: 身体活動レベル _
            = IIf(経過日数 Mod 運動頻度 = 0, 1.75, 1.5)
        
        Dim 体重維持エネルギー As Double: 体重維持エネルギー _
            = 基礎代謝(体重, 身長, #6/28/1984#, 性別) * 身体活動レベル
            
        Dim BMI As Double: BMI = 体重 / (身長 / 100) ^ 2
        SimulationSheet.WriteRecord _
            day:=開始日 + 経過日数, _
            weight:=Round(体重, 1), _
            activity_level:=身体活動レベル, _
            cal_intake:=Round(体重維持エネルギー - (脂肪1に対するCal / 減量目標)), _
            bmi_:=Round(BMI, 1)

        体重 = 体重 - (1 / 減量目標)
        経過日数 = 経過日数 + 1
    Loop While BMI > 理想BMI
End Sub

Function 基礎代謝(体重 As Double, 身長 As Double, 生年月日 As Date, 性別 As String)
    Dim 年齢 As Double
    年齢 = CLng(Date - 生年月日) / 365
    
    Dim 性別補正 As Double
    Select Case 性別
        Case "男"
            性別補正 = 男性係数
        Case "女"
            性別補正 = 女性係数
    End Select
    
    基礎代謝 = ((体重 * 体重係数) + (身長 * 身長係数) - (年齢 * 年齢係数) - 性別補正) * 1000 / 4.186
End Function

使い方

標準モジュールのMainを実行するとシミュレーションシートにこんな感じで出力される。
f:id:t-hom:20191012200558p:plain

活動レベルが1.75の日は運動するっていう意味。運動したらその分は食うというコードになってるので、このとおりに食べた場合、運動による消費カロリーが直接ダイエットスピードを速めることにはならない。ただ筋力が付くのでその分基礎代謝は上がるだろうけど。

摂取カロリー目安の減少を見るにはややこしいので、殆ど運動しない設定に変えてみる。
※パラメータの運動頻度を999とかにすると最初の1回だけ1.75であとは1.5になる。

Mainを実行すると次のように出た。最初の1回は活動レベル1.75になるので無視したとして、明日から月末にかけて摂取カロリー目安が11キロカロリー減少しているのがわかる。
f:id:t-hom:20191012201319p:plain

つまりダイエット成功のためには、食う量を、固定で減らすんじゃなくて、減らし続けなければならないということがこのシミュレーションから分かる。

今後の応用

個人的な話で恐縮だけど、最近体重計に乗るだけでCSVに体重データを蓄積できるシステムを作った。
構成図としてはこんな感じで、できてるのはCSV書き込みのweght_watcher.pyまで。
f:id:t-hom:20191012202211p:plain

今回調べた計算式を用いれば、図のcalorie_adviser.pyが作れるので、デイリーのカロリー計算に役立つかなと思っている。

VBA 電波の図形を作成するマクロ

今回は電波の図形を作成するマクロを作った。
といってもサイン派みたいなのではなく、イラストでよくある電波でてますよーというアイコンみたいなもの。
f:id:t-hom:20191009121201p:plain

下図のように基本図形の「円弧」を使って作画するのだが、手動でやるとどうにも綺麗にできないのでプログラムで作ることにした。
f:id:t-hom:20191009122549p:plain

完成したシェイプはパワポにコピーして使う。

コード

以下コードのMainを実行すると、アクティブシートに電波が綺麗に描画される。
実行するとアクティブシートの既存シェイプが消えるので注意。このコードはまっさらなシートで実行する想定。

Sub Main()
    Call DeleteAllShape
    Call DrawRadioWave( _
        directivity:=40, _
        color:=RGB(255, 100, 100), _
        frequency:=10, _
        acceleration:=1.2, _
        intensity:=1)
    ActiveSheet.Shapes.SelectAll
    Selection.ShapeRange.Group
End Sub

Sub DrawRadioWave(directivity, color, frequency, acceleration, intensity)
    Dim sh As Shape, sh2 As Shape
    pos = 10 * acceleration ^ (freqency - 1)
    Set sh = ActiveSheet.Shapes.AddShape(msoShapeArc, pos, pos, 10, 10)
    sh.Adjustments.Item(1) = -90 - directivity / 2
    sh.Adjustments.Item(2) = -90 + directivity / 2
    For t = 1 To frequency - 1
        sh.Line.Weight = intensity
        sh.Line.ForeColor.RGB = color
        Set sh2 = sh.Duplicate
        sh2.Top = sh.Top
        sh2.Left = sh.Left
        sh2.ScaleWidth acceleration, msoFalse, msoScaleFromMiddle
        sh2.ScaleHeight acceleration, msoFalse, msoScaleFromMiddle
        Set sh = sh2
    Next
End Sub

Sub DeleteAllShape()
    For Each sh In ActiveSheet.Shapes
        sh.Delete
    Next
End Sub

パラメータの解説

DrawRadioWave関数に渡すパラメータ(引数)によって描画される電波が変化する。

    Call DrawRadioWave( _
        directivity:=40, _
        color:=RGB(255, 100, 100), _
        freqency:=10, _
        acceleration:=1.2, _
        intensity:=1)

directivity(指向性)

電波の描画角度を表す。角度は向きじゃなくて、幅の意味なので注意。
f:id:t-hom:20191009123639p:plain

color(色)

色。そのまま。

frequency(周波数)

なんちゃって周波数。単に線の数。
これを増やす場合、accelerationを下げないとでかくなる。
でかくなると描画位置がだいぶ右にズレる。なぜなら円弧は表示範囲だけでなく円周分の幅を取るので。

acceleration(加速度)

電波が広がる度にサイズがどれくらい大きくなるかを元の倍率で表す。
1.2~1.5くらいが適正。これを上げる場合、frequencyを下げないとでかくなる。

intensity(強度)

電波強度。単に線の太さ。
1~3くらいが適正。

以上

PythonでGUI版 筋トレメトロノームを作成 その2

前回は筋トレ用メトロノームをGUI化した。
thom.hateblo.jp

なるべく紹介済の機能に絞って書いたのでコードがとても冗長だ。
前回GUIパーツをそれぞれ変数に入れて扱っていたため、今回はPythonのデータ型(リスト、タプル、ディクショナリ)を使ってもう少しスッキリさせてた。

Pythonの基礎でこれらのデータ型が出てくるので、「何に使うねんこれ」と思うかもしれないけど、そのイメージを掴んでもらうためにも良いと思う。

コード

import tkinter as tk
import winsound
from time import sleep

root_window = tk.Tk()
root_window.geometry("200x160")

def start_clicked():
    replay = int(items["Replay"][1].get())
    count = int(items["Count"][1].get())
    high_tone = int(items["High tone"][1].get())
    low_tone = int(items["Low tone"][1].get())
    duration = int(items["Duration"][1].get())
    for i in range(replay):
        for j in range(count):
            winsound.Beep(55*2**low_tone,duration)
            sleep(1)
        winsound.Beep(55*2**high_tone,duration*2)
        sleep(1)

item_names = ["Replay", "Count", "High tone", "Low tone", "Duration"]
initials = [5, 3, 5, 3, 200]

items = {}

for item_name in item_names:
    items[item_name] = (
            tk.Label(
                master=root_window,
                text=item_name),
            tk.Entry(
                master=root_window,
                width=10))

btn = tk.Button(
    master=root_window,
    text="Start",
    command=start_clicked
    )

for i in range(5):
    lbl, ent = items[item_names[i]]
    lbl.place(x=5,y=5+(25*i))
    ent.place(x=70,y=5+(25*i))
    ent.insert(tk.END,initials[i])

btn.place(x=70,y=130)

root_window.mainloop()

リストについて

リストはVBAでいうコレクションのようなものだが、Pythonの場合[]の中にカンマ区切りで書くことで初期値を設定できる。

VBAの場合

初期値を設定する方法が無いので以下のように一つずつ要素を追加する。

Dim item_names As Collection
Set item_names = New Collection
item_names.Add "Replay"
item_names.Add "Count"
item_names.Add "High tone"
item_names.Add "Low tone"
item_names.Add "Duration"

Pythonの場合

このように一つずつ追加しても良いが、

item_names = list()
item_names.append("Replay")
item_names.append("Count")
item_names.append("High tone")
item_names.append("Low tone")
item_names.append("Duration")

以下のように単に[]内にカンマ区切りで同じリストを作成できる。

item_names = ["Replay", "Count", "High tone", "Low tone", "Duration"]

また、空のリストを作りたければitem_names=list()とも書けるし、item_names=[]とも書ける。

アンパックについて

Pythonではリストの内容を個別の変数に入れたいとき、次のようにインデックス指定で入れることもできるが、

a = item_names[0]
b = item_names[1]
c = item_names[2]
d = item_names[3]
e = item_names[4]

アンパックという記述方法があり、次のように書くとリストのそれぞれの要素が変数に入る。

a, b, c, d, e = item_names

これ、とても楽。

ディクショナリについて

VBAのディクショナリと同じく、キーと値の対応付けデータである。
空のディクショナリを作っているのがこちら。

items = {}

文字列でキーを指定して値を入れることで追加・更新ができる。

items["キー"] = 値

タプルについて

基本的にリストみたいなもの。ただし追加や削除などの操作ができない。
リストが主に同種データを扱うのに対し、タプルは異なる型のデータを纏める用途でよく使われるらしい。
ということで、ラベルとテキストボックスを纏めてみた。

コード中に登場する以下のループ内では、item_namesリストから取り出したitem_nameをキーとしてディクショナリitemsに、ラベルとテキストボックスのタプルを格納している。

for item_name in item_names:
    items[item_name] = (
            tk.Label(
                master=root_window,
                text=item_name),
            tk.Entry(
                master=root_window,
                width=10))

そして配置する際は、ディクショナリitemsからitem_namesリストのi番目のキーでタプルを取り出し、lblとentにアンパックしている。

for i in range(5):
    lbl, ent = items[item_names[i]]
    lbl.place(x=5,y=5+(25*i))
    ent.place(x=70,y=5+(25*i))
    ent.insert(tk.END,initials[i])

一方でテキストボックス読み出しの際は、ラベルを取り出す必要はないのでアンパックではなく[1]を指定してゲット。
※タプルの[0]にラベル、[1]にテキストボックスが入っている。

def start_clicked():
    replay = int(items["Replay"][1].get())
    count = int(items["Count"][1].get())
    high_tone = int(items["High tone"][1].get())
    low_tone = int(items["Low tone"][1].get())
    duration = int(items["Duration"][1].get())

使用したデータの組み合わせ図

f:id:t-hom:20190929133438p:plain

おわりに

データ型を覚えるとコードの冗長性が無くなってコーディングが楽になる。
今回は前回の半分くらいのコード量になった。
また、変数名を大量に管理する必要がなくなり、変更にも強くなる。

今回の記事で、あまりついてこれなかった方も、基礎をしっかり学ぶとより短い労力でコードを書けるということは伝わったかと思う。

ということで、学習がんばりましょう。

やさしいPython (「やさしい」シリーズ)

やさしいPython (「やさしい」シリーズ)

PythonでGUI版 筋トレメトロノームを作成

前回の記事ではPythonで筋トレ用メトロノームを作成した。
thom.hateblo.jp

前々回ではTkinterを使ったGUIのサンプルを紹介した。
thom.hateblo.jp

今回は、これらを組み合わせて、GUI版の筋トレ用メトロノームを作成してみよう。

動作イメージ

動作は前回のCUI版と同じだが、設定用のテキストボックスとスタートボタンを配置した。

ちょっと右の余白がダサいんだけど、これ以上横幅を削るとタイトルバーで掴める場所がなくなって、ウインドウ移動に支障をきたすので。。

コード

import tkinter as tk
import winsound
from time import sleep

root_window = tk.Tk()
root_window.geometry("200x160")

def start_clicked():
    replay = int(ent_replay.get())
    count = int(ent_count.get())
    high_tone = int(ent_high_tone.get())
    low_tone = int(ent_low_tone.get())
    duration = int(ent_duration.get())
    for i in range(replay):
        for j in range(count):
            winsound.Beep(55*2**low_tone,duration)
            sleep(1)
        winsound.Beep(55*2**high_tone,duration*2)
        sleep(1)

lbl_replay = tk.Label(
    master=root_window,
    text="Replay")

lbl_count = tk.Label(
    master=root_window,
    text="Count")

lbl_high_tone = tk.Label(
    master=root_window,
    text="High tone")

lbl_low_tone = tk.Label(
    master=root_window,
    text="Low tone")

lbl_duration = tk.Label(
    master=root_window,
    text="Duration")

ent_replay = tk.Entry(
    master=root_window,
    width = 10)

ent_count = tk.Entry(
    master=root_window,
    width = 10)

ent_high_tone = tk.Entry(
    master=root_window,
    width = 10)

ent_low_tone = tk.Entry(
    master=root_window,
    width = 10)

ent_duration = tk.Entry(
    master=root_window,
    width = 10)

btn = tk.Button(
    master=root_window,
    text="Start",
    command=start_clicked
    )


lbl_replay.place(x=5,y=5)
ent_replay.place(x=70,y=5)

lbl_count.place(x=5,y=30)
ent_count.place(x=70,y=30)

lbl_high_tone.place(x=5,y=55)
ent_high_tone.place(x=70,y=55)

lbl_low_tone.place(x=5,y=80)
ent_low_tone.place(x=70,y=80)

lbl_duration.place(x=5,y=105)
ent_duration.place(x=70,y=105)

btn.place(x=70,y=130)

ent_replay.insert(tk.END,"5")
ent_count.insert(tk.END,"3")
ent_high_tone.insert(tk.END,"5")
ent_low_tone.insert(tk.END,"3")
ent_duration.insert(tk.END,"200")

root_window.mainloop()

解説

GUIとビープ音の解説は前回・前々回で説明が終わっているので省略。
今回新登場した要素は、テキストボックスだ。

tk.Entryというクラスで作成することができる。
プログラムで文字を入力するには、insert命令で、最初の引数 tk.END はテキストボックスの最後に挿入するという意味で、2番目の引数が実際に入力する値。

startボタンを押したときにはgetメソッドで内容を取り出して、int関数で整数に直してから変数に格納し、あとはループ中で利用している。

テキストボックスの使い方はこちらのサイトが分かりやすいのでご参考までに。
pg-chain.com

今回のコードは実際にはかなり冗長なので、次回もう少しスマートにしてみようと思う。

Pythonで筋トレ用メトロノームを作成

前回の記事ではPythonでGUIを体験するプログラムを紹介した。

今回はPythonで筋トレ用メトロノームを作成。
音がメインなので、GUIは作らないが、CUIでも十分実用的なプログラムが作れるということで紹介。
学習モチベーションUpに繋がると嬉しい。

実行イメージ

開始

ポッ・ポッ・ポッ・ピー・
ポッ・ポッ・ポッ・ピー・
ポッ・ポッ・ポッ・ピー・
ポッ・ポッ・ポッ・ピー・
ポッ・ポッ・ポッ・ピー・

終了

ポッ…低音ビープ
ピー…高温ビープ
中黒「・」…1秒の間

コード

import winsound
from time import sleep

#設定
replay = 5       #再生回数
count = 3        #カウント数
high_tone = 5    #高音:1~6の範囲で指定
low_tone = 3     #低音:1~6の範囲で指定
duration = 200   #音の長さ:100~1000がお勧め

for i in range(replay):
    for j in range(count):
        winsound.Beep(55*2**low_tone,duration)
        sleep(1)
    winsound.Beep(55*2**high_tone,duration*2)
    sleep(1)

解説

今回の主役は「winsound」モジュールだ。
※Windows専用なので環境が違う方はごめんなさい。

winsoundのBeep関数は、周波数と音の長さを引数として渡すとビープ音を鳴らす。

周波数を決めるのに参考にしたサイトはこちら。
音階周波数

「ラ」の音の周波数がキリが良くて、55×2のべき乗で表すことができるのでこれを採用した。
Pythonのべき乗は「**」である。

コードでいうと、この部分

winsound.Beep(55*2**low_tone,duration)

あとはfor文でループしながらビープサウンドを鳴らしつつ、1秒スリープを挟めばメトロノーム完成

ちなみにsleepのimport文だが、このように「from モジュール import 関数」と書くと、モジュールの特定関数だけをインポートできる。

from time import sleep

普段VBAを書いている方はPythonのfor文に戸惑うかもしれないので解説しておくと、PythonにVBAでいうFor文はなくて、全部For Eachだと思っていただくと良い。

よく分からない?
それこそが、Pythonを本格的に学習する動機になる。

私のオススメはこちら。

VBA経験者向け、Python入門用 GUIサンプル

今回は、ちょっとPythonを触ってみたいVBA経験者に向けて、簡単なGUIサンプルを紹介する。
Excel VBAではマクロの実行結果がシートに反映されるので、ある意味それ自体がGUIプログラミングと言えるが、殆どの言語では黒い画面に"Hello, World!"から入門するのが通例で、GUIの話は入門書の後半に差し掛かるあたりでちょろっと紹介される程度だ。

一方でGUIについて詳しく書かれた専門書は、黒画面での修行が終わったことを前提にしているので難しい。

黒画面 → 華がないのでやる気がでない。
GUI → 難しいのでやる気がでない。

つまり、ちょっとPythonを触ってみて、GUIが使えることを実感して、モチベーションをあげてから本格的に入門したいという方に向けた体験用コードで、しかもVBA経験者向けというのがなかなか無いので、今回書いてみた次第。

作るもの

サンプルなのでシンプルに。
ボタンを押すとラベルのテキストが変わるだけのプログラム。

コード

私にしては珍しく、逐一コメントを書いてみた。
後程、この#1~#8のフェーズごとに解説していく。

#1 tkinterモジュールをtkという名前でインポート
import tkinter as tk

#2 ウインドウの生成と変数への格納
root_window = tk.Tk()
root_window.geometry("200x100")

#3 可変文字列の生成と変数への格納
label_text = tk.StringVar()

#4 可変文字列の初期化
label_text.set("Please click the button.")

#5 ボタンが押された時用の関数を予め定義しておく。
def button_clicked():
    label_text.set("Button clicked.")

#6 GUIパーツの生成と変数への格納
lbl = tk.Label(
    master=root_window,
    textvariable=label_text)

btn = tk.Button(
    master=root_window,
    text="Button",
    command=button_clicked
    )

#7 GUIパーツの配置
lbl.place(x=5,y=5)
btn.place(x=5,y=30)

#8 ウインドウ表示
root_window.mainloop()

解説

#1 tkinterモジュールをtkという名前でインポート

import tkinter as tk

まずはこちら、VBAでいうところの「参照設定」に該当する。
あちらはメニューからGUI操作でモジュール名にチェックを入れるのに対し、こちらはコード自体に記述する方式。

tkinterというのはpythonの標準GUIモジュールである。長いのでここでtkという名前をつけて利用しやすくしている。
慣例的にtkという名前にしているが、ただの識別子なので別名でも動作する。
別名にした場合、コード各部のtkはその名前に置き換える必要がある。

なお、自分で書いた「ナントカ.py」も同じようにimport文で取り込むことができる。その場合拡張子「.py」は記述しない。

また、pythonではpipという同梱プログラムで外部モジュールをインターネットからお取り寄せできる。
tkinterはインストールした覚えがないので最初から入ってたと思うが、無いモジュールは「pip install モジュール名」というコマンドでインストール可能。
※pipが使えない場合、pip.exeの入ったパスを環境変数Pathに登録が必要

#2 ウインドウの生成と変数への格納

root_window = tk.Tk()
root_window.geometry("200x100")

Pythonではクラス名(VBAでいうクラスモジュール名に相当)を関数のように()を付けて呼び出すことで、インスタンスが生成される。
先ほどインポートしたtkinterにTkというウインドウを生成するクラスが定義されているので、tk.Tk()でインスタンスを生成して、root_window変数に代入している。
VBAとちがって、オブジェクトの代入にSetを使うなどの特殊な方法はない。普通にイコール記号だけで代入される。
また、VBAのDimのような変数宣言専用の書式は存在しないので、識別子に対していきなり代入する。Pythonの場合は作法的にもこれで合ってる。

geometryメソッドはウインドウのサイズを変更する命令である。
文字列で横x縦のサイズを引き渡すとそのサイズになる。
掛け算記号がアスタリスクではなく小文字のエックスなのが面白い。

さて、ここではウインドウは内部的に生成されただけで、まだ表示はされておらず、#8のmainloopメソッドで表示される。

#3 可変文字列の生成と変数への格納

label_text = tk.StringVar()

Pythonでは文字列は基本的に固定で、ラベル等に直接文字列を書いてしまうとプログラムで動的に変更ができなくなる。
そこで、tkモジュールのStringVarクラスから可変文字列のインスタンスを生成し、label_textという変数に格納している。
label_textはただの変数名なので、この時点でラベルにセットされたわけではないことに注意。

#4 可変文字列の初期化

label_text.set("Please click the button.")

先ほど生成した可変文字列に対し、具体的な文字列をsetメソッドでセット。

#5 ボタンが押された時用の関数を予め定義しておく。

def button_clicked():
    label_text.set("Button clicked.")

これはボタンがクリックされた時用に予め用意した関数。
さきほど作った可変長文字列label_textに新しい文字列をセットするコードだ。

関数と書いてるが、戻り値を持たないのでVBAでいうとSubプロシージャである。
Pythonでは呼び分けはせず、戻り値を持たなくても関数と呼ぶ。
defが「Sub」や「Function」に相当する宣言文で、button_clickedが関数名。

VBAだとEnd SubやEnd Functionでプロシージャブロックの終わりを表すが、Pythonではインデントでブロックを表す。これについてはいろんなところで解説があるのでここでは省略する。

ちなみになぜボタンもまだ作ってないのに、押された後のことを先に書くかというと、Pythonコードは呼び出し先コードは呼び出し元よりも前に定義しおく必要があるから。

たとえばこういうコードを書くと、

hoge()

def hoge():
    print("Hi")

こういうエラーが出る。

だから、呼ぶ前に教えてあげる必要がある。

def hoge():
    print("Hi")

hoge()

VBAだとモジュール内でプロシージャの順番を気にしなくても一旦全部探してくれるけど、Pythonだと順番を意識する必要がある。

#6 GUIパーツの生成と変数への格納

lbl = tk.Label(
    master=root_window,
    textvariable=label_text)

btn = tk.Button(
    master=root_window,
    text="Button",
    command=button_clicked
    )

ここではtkモジュールのLabelクラスからインスタンスを生成している。
VBAと違って殆どのオブジェクト指向言語はインスタンス生成時に引数を渡すことができる。
master=とかtextvariable=といった表記は、名前付き引数である。VBAでいうところの「:=」 と同じ。
第一引数がmasterなので、他の解説ではmaster=は省略して単にウインドウを格納した変数を書くケースが多いが、tkinter初見だと意味が分からないと思うので明示的に書いた。

これはつまり、ラベルやボタンを作るときに、マスターを指定して親子関係の構造を作っているということ。

サンプルではルートウインドウを指定していることが多いが、GUIパーツを纏めるフレームを使用する場合などは、フレームに配置するパーツのマスターをフレームに指定したり、複雑な構造を作ることができる。

マスターの指定では論理的な構造を定義しているだけで、実際にパーツを配置しているわけではない。
マスター以外の引数は、読むとなんとなくわかると思うので省略。

今回ラベルの仮引数textvariableにlabel_textをセットしたので、label_textの変更があれば都度ラベルに反映される仕組みになっている。
なお、ラベルの値を変更する予定が無ければボタンと同じように、textvariableの代わりにtextにテキストを設定する。逆にボタンにtextvariableを指定することも可能である。

#7 GUIパーツの配置

lbl.place(x=5,y=5)
btn.place(x=5,y=30)

ここでは先ほど作ったGUIパーツを実際に配置している。
パーツの配置方法にはpack、grid、placeの3つのメソッドがあり、基本的に同じレイヤーで混在させることはできない。
(フレームを使うと、フレーム内を別メソッドでレイアウトすることが可能)

packは最も簡便であるが基本的に直列・並列に並べる用なので応用して自由なレイアウトをしたいと思うと難しい。
これを改造して何か作りたいと思ったときに難しいと思うので、ここでの紹介は割愛する。

gridは綺麗にレイアウトできてフレキシブルだけど、これも直感的にレイアウトするのはそれなりに難しい。
同じく、これを改造して何か作りたいと思ったときに難しいと思うので、ここでの紹介は割愛する。

placeは愚直にxyでの座標指定。面倒くさいけど、理解は簡単。改造も簡単なので今回はこれにした。

なお、lblとbtnは配置されたが、まだウインドウが表示されているわけではない。

#8 ウインドウ表示

root_window.mainloop()

mainloopメソッドを実行するとウインドウが表示される。

なんでloopという名前がついてるのかというと。。
基本的にプログラムって順次・反復・分岐で出てきているので、ウインドウにボタンが配置されてそれを押したらどうなるみたいなGUIプログラムも、「順次・反復・分岐」の応用でしかない。
つまりウインドウを表示するプログラムって、反復(つまりloop)の中で絶えず入力を監視して、入力(クリック・キー入力など)があったら分岐して処理し、さらに反復(入力待ち)にもどる。専門的にはメッセージループという。
メインループというのはそれを開始するって意味。

メインループについて知りたい場合、以下の本が参考になる。もはやPython関係ないけど。
「なか見"検索」でその辺りも読めるので興味があれば。

一見単純に見える何もないウインドウが裏で色々とやってるのが分かって面白い。

当ブログは、amazon.co.jpを宣伝しリンクすることによってサイトが紹介料を獲得できる手段を提供することを目的に設定されたアフィリエイト宣伝プログラムである、 Amazonアソシエイト・プログラムの参加者です。