Python(Timerアプリをexe化)

tkinterライブラリを利用して、自作カウントダウンタイマーを作成しました。

今回は、Windows環境であれば、誰でも利用できるツールにするには?ということを紹介します。

pythonファイルは「~.py」となり、Pythonがインストールされている環境でないと実行できませんが、実行ファイル形式の「~.exe」にすることでPythonがインストールされていない環境でもアプリケーションとして使用することができます。

そのために使用するのが「pyinstaller」です!

こちらはコマンドラインツールから使用するもので、インストールや簡単な使い方については以下のサイトで紹介されています。

Pythonのファイルをexe化する方法【初心者向け】

しかし、画像や他の実行ファイルなどをプログラムから利用する場合、紹介されている方法ではアプリを実行することができません。

外部のリソースを利用する場合は、pyinstallerを実行した場合に生成される「プログラム名.spec」というファイルの中身を書き換え、こちらを用いて再度pyinstallerを実行する必要があります。

詳しくは以下のサイトで紹介されています。

PyInstallerで実行ファイルにリソースを埋め込み

それでは、改めて今回のプログラム紹介とexe化手順、アプリケーション説明をしていきます。

Timer.py プログラムについて

import tkinter as tk
import time
import os
import sys

def resource_path(relative_path):
    if hasattr(sys, '_MEIPASS'):
        return os.path.join(sys._MEIPASS, relative_path)
    return os.path.join(os.path.abspath("."), relative_path)

class Application(tk.Frame):
    def __init__(self, master=None):
        super().__init__(master)
        self.master.title('Timer')
        self.master.geometry('500x220')
        self.target_time = None      
        self.create_widgets()

    def create_widgets(self):
        tk.Label(self, text="カウントダウンタイマー",bg="lightgreen").pack(fill=tk.X,expand=1)

        f_input = tk.Frame(self, relief=tk.RIDGE, bd=4)
        f_input.pack(fill=tk.X, expand=1)
        f_entry = tk.Frame(f_input)
        f_entry.pack(side=tk.LEFT)
        tk.Label(f_entry,text="時間(秒)").pack()
        self.entry = tk.Entry(f_entry)
        self.entry.insert(tk.END,"60")
        self.entry.pack()
        f_btn1 = tk.Frame(f_input)
        f_btn1.pack(side=tk.RIGHT)
        self.img0 = tk.PhotoImage(file=resource_path("noodle.png"))
        self.b_3m = tk.Button(f_btn1, image=self.img0, command=self.set_3m)
        self.img1 = tk.PhotoImage(file=resource_path("udon.png"))
        self.b_5m = tk.Button(f_btn1, image=self.img1, command=self.set_5m)
        self.b_5m.pack(side=tk.RIGHT,padx=5)        
        self.b_3m.pack(side=tk.RIGHT,padx=5)        

        self.svar = tk.StringVar()
        self.time_init()
        self.display = tk.Label(self, textvariable=self.svar, relief=tk.SUNKEN,
                              font=("",20),bg='white')        
        self.display.pack(fill=tk.X,expand=1)

        f_btn2 = tk.Frame(self, relief=tk.RIDGE, bd=4)
        f_btn2.pack(fill=tk.X,expand=1)
        self.image1 = tk.PhotoImage(file=resource_path("start.png"))
        self.b_start = tk.Button(f_btn2, image=self.image1, command=self.start)
        self.image2 = tk.PhotoImage(file=resource_path("stop.png"))
        self.b_stop = tk.Button(f_btn2, image=self.image2, command=self.stop, state=tk.DISABLED)
        self.image3 = tk.PhotoImage(file=resource_path("reset.png"))
        self.b_reset = tk.Button(f_btn2, image=self.image3, command=self.reset, state=tk.DISABLED)
        self.b_start.pack(side=tk.LEFT, padx=10)
        self.b_stop.pack(side=tk.LEFT, padx=10)
        self.b_reset.pack(side=tk.LEFT, padx=10)       

    def time_init(self):
        self.svar.set("セットする時間を秒単位で入力してください。")

    def time_err(self,s):
        self.svar.set(f'{s}は不正な数値です。')        

    def time_set(self):
        self.h = self.target_time // 3600
        self.m = (self.target_time % 3600) // 60
        self.s = (self.target_time % 3600) % 60        
        self.svar.set("のこり %02d時間 %02d分 %02d秒" % (self.h,self.m,self.s))

    def time_finish(self):
        self.svar.set('終了~! タイムアップ!')

    def deltatime(self):
        return (time.time() - self.pretime)

    def set_3m(self):
        self.entry.delete(0,tk.END)
        self.entry.insert(tk.END,"180")

    def set_5m(self):
        self.entry.delete(0,tk.END)
        self.entry.insert(tk.END,"300")

    def start(self):
        try:
            if not self.target_time:
                self.target_time = int(self.entry.get())                                   
        except:
            self.time_err(self.entry.get())
            return

        self.started = True
        self.pretime = time.time()
        self.time_set()
        if(0 < self.target_time <= 10):
            self.display.config(bg="red")        
        self.entry.config(state=tk.DISABLED)
        self.b_start.config(state=tk.DISABLED)
        self.b_stop.config(state=tk.NORMAL)
        self.b_reset.config(state=tk.DISABLED)
        self.b_3m.config(state=tk.DISABLED) 
        self.b_5m.config(state=tk.DISABLED)
        self.exec_timer()

    def stop(self):
        self.started = False
        self.b_start.config(state=tk.NORMAL)
        self.b_stop.config(state=tk.DISABLED)
        self.b_reset.config(state=tk.NORMAL)

    def reset(self):
        self.target_time = None
        self.time_init()
        self.display.config(bg="white")
        self.b_reset.config(state=tk.DISABLED)
        self.entry.configure(state=tk.NORMAL)
        self.b_3m.config(state=tk.NORMAL) 
        self.b_5m.config(state=tk.NORMAL) 
        self.b_start.config(state=tk.NORMAL)

    def finish(self):
        self.started = False
        self.time_finish()
        self.b_start.config(state=tk.DISABLED)
        self.b_stop.config(state=tk.DISABLED)
        self.b_reset.config(state=tk.NORMAL)

    def exec_timer(self):
        if self.started:
            self.target_time = self.target_time - self.deltatime()
            self.time_set()

            if (0 < self.target_time <= 10):
                self.display.config(bg="red")

            if self.target_time <= 0:
                self.finish()

            self.pretime = time.time()
            self.after(50, self.exec_timer)

if __name__ == '__main__':
    app = Application()
    app.pack()
    app.mainloop()

基本的な仕様については以下を確認してください。
Python(tkinterのウィジェット配置とアプリ紹介)
https://pgk.pgkids.co.jp/pages/blog/12-6

今回のプログラムのポイント

def resource_path(relative_path)
リソースファイルの参照方法を変更しています。
これは後に説明する実行ファイルでは、実行に埋め込んだリソースが実行時に別フォルダに展開されるため(例:%USERPROFILE%/AppData/Local/Temp/_MEIxxxxxx)
、これを参照できるよう関数を追加し、リソース参照時はこれを使用するようにしています。
def deltatime(self)
スタートボタンを押してから、次の更新までにかかった経過時間を返しています。のこり時間と経過時間を更新処理ごとに更新することで、一時停止処理に対応しています。
また、self.pretime = time.time()のように更新前に、timeライブラリを使用して時間を保存することで、ズレを生じにくくさせています。
更新処理はself.after(50, self.exec_timer)のようにtkinterのafterメソッドで50ミリ秒毎に行っています。

Timer.spec exe化について

  • コマンドラインツールからpyinstallerを実行します。
pyinstaller test.py

※ pyinstallerがインストールされていない場合は、コマンドラインツールからpipコマンドでインストールしてから実行してください。

pip install pyinstaller

成功すると、デフォルトでは、カレントディレクトリに「build」フォルダ、「dist」フォルダ、「ファイル名.spec」が作成されます。
「dist」フォルダ内に実行形式の「~.exe」が入っていますが、外部のリソースを利用する場合は、これをダブルクリックしても実行することができません。

  • 外部のリソースを利用するため「ファイル名.spec」を書きかえる

VSCodeなどのテキストエディタで開くと以下のような内容が書かれていると思います。

# -*- mode: python ; coding: utf-8 -*-

block_cipher = None

a = Analysis(['C:\\Users\\pcuser\\timer.py'],
             pathex=['C:\\Users\\pcuser'],
             binaries=[],
             datas=[],
             hiddenimports=[],
             hookspath=[],
             runtime_hooks=[],
             excludes=[],
             win_no_prefer_redirects=False,
             win_private_assemblies=False,
             cipher=block_cipher,
             noarchive=False)
pyz = PYZ(a.pure, a.zipped_data,
             cipher=block_cipher)
exe = EXE(pyz,
          a.scripts,
          [],
          exclude_binaries=True,
          name='timer',
          debug=False,
          bootloader_ignore_signals=False,
          strip=False,
          upx=True,
          console=True )
coll = COLLECT(exe,
               a.binaries,
               a.zipfiles,
               a.datas,
               strip=False,
               upx=True,
               upx_exclude=[],
               name='timer')

こちらを以下のように書き換えてください。

# -*- mode: python ; coding: utf-8 -*-

block_cipher = None

a = Analysis(['C:\\Users\\pcuser\\timer.py'],
             pathex=['C:\\Users\\pcuser'],
             binaries=[],
             datas=[],
             hiddenimports=[],
             hookspath=[],
             runtime_hooks=[],
             excludes=[],
             win_no_prefer_redirects=False,
             win_private_assemblies=False,
             cipher=block_cipher,
             noarchive=False)
a.datas += [('noodle.png', '.\\noodle.png', 'DATA')]
a.datas += [('udon.png', '.\\udon.png', 'DATA')]
a.datas += [('start.png', '.\\start.png', 'DATA')]
a.datas += [('reset.png', '.\\reset.png', 'DATA')]
a.datas += [('stop.png', '.\\stop.png', 'DATA')]
pyz = PYZ(a.pure, a.zipped_data,
             cipher=block_cipher)
exe = EXE(pyz,
          a.scripts,
          a.binaries,
          a.datas,
          [],
          name='timer',
          debug=False,
          bootloader_ignore_signals=False,
          strip=False,
          upx=True,
          console=False )
coll = COLLECT(exe,
               a.binaries,
               a.zipfiles,
               a.datas,
               strip=False,
               upx=True,
               upx_exclude=[],
               name='timer')

外部ファイルはdatasに記述する仕様なので
a.datas += [('image.png', '.\image.png', 'DATA')]を加えます。
複数のリソースを追加する場合は、その分だけ加えます。

※ プログラムや外部リソースのパスが異なる場合、exe化を行うことができないので、これらのファイルをカレントディレクトリに配置しておいてください。

EXE()の引数のexclude_binaries=False,を削除し、a.binaries,と a.datas,を追加します。
(console=Falseで、余計なコンソールが開かなくなります。)

  • specファイルを使ってexeを作成

コマンドラインツールで再度pyinstallerを実行しますが、そのときに「~.py」ではなく、先ほど書き換えた「~.spec」を使用してください。

pyinstaller test.spec

※ 以前実行したときに生成された「build」フォルダ、「dist」フォルダが残ったままの場合、実行することができないので、この2つを削除しておいてください。

  • distフォルダのexeを確認

単体の実行ファイル「~.exe」でリソースを使用できることを確認できます。

Timer.exe アプリケーション説明

今回作成したGUIアプリは以下からダウンロードして、Windows環境で使用することができます。
Timerアプリ
zip圧縮されていますので、ダウンロード後は解凍して「~.exe」の実行ファイルをダブルクリックしてください。

<使用方法>

  1. エントリーボックスにカウントしたい時間を秒単位で入力してください。(デフォルトは1分(60秒))
  2. noodleボタンを押すと3分(180秒)、udonボタンを押すと5分(300秒)が自動で入力されます。
  3. startボタンを押すとカウントダウンがスタートします。
  4. 中断する場合は、stopボタンを押すとカウントダウンが止まります。
    再開する場合は、再度startボタンを、リセットする場合は、Resetボタンを押下してください。
  5. 残り10秒を切ると表示背景色が赤色に、時間になると終了表示が出てカウントがストップします。
  6. 実行中、不要なボタンは無効化して、押せなくなっています。

Pythonはライブラリが充実しており、今回exe化の方法がわかったので、他にも自作ツールを作っていきたいと思います。

Python(Timerアプリをexe化)” に対して1件のコメントがあります。

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です

このサイトはスパムを低減するために Akismet を使っています。コメントデータの処理方法の詳細はこちらをご覧ください

Python

前の記事

Python(Textウィジェット)