2022/08/14(日) [n年前の日記]
#1 [python] Pythonスクリプトの多重起動を抑止したい
Windows10 x64 21H2 + Python 3.8.10、3.9.13 上で、Pythonスクリプトの多重起動を抑止したい。
◎ 参考ページ。 :
以下のページを参考にして試してみた。
_Creating a single instance application < Python recipes < ActiveState Code
_Ptyhonでプロセス間排他を試す(Windows限定) | The only neEt thing to do.
_windowsで実行中のプロセスのフルパスを、pythonから調べたい
_多重起動禁止処理 for Windows
_benhoyt/namedmutex: namedmutex.py, a simple ctypes wrapper for Win32 named mutexes
_Windows上で多重起動を防止する方法
_Module win32event
Windowsの場合、Mutex なるものを使うと多重起動してるかどうかを判別することができる模様。キーワードとして「python CreateMutex」でググれば情報に辿り着けそう。
その Mutex を利用する方法として、以下の2つがあるようで。
_Creating a single instance application < Python recipes < ActiveState Code
_Ptyhonでプロセス間排他を試す(Windows限定) | The only neEt thing to do.
_windowsで実行中のプロセスのフルパスを、pythonから調べたい
_多重起動禁止処理 for Windows
_benhoyt/namedmutex: namedmutex.py, a simple ctypes wrapper for Win32 named mutexes
_Windows上で多重起動を防止する方法
_Module win32event
Windowsの場合、Mutex なるものを使うと多重起動してるかどうかを判別することができる模様。キーワードとして「python CreateMutex」でググれば情報に辿り着けそう。
その Mutex を利用する方法として、以下の2つがあるようで。
- win32event、win32api、winerror等を経由して呼び出す。
- ctypes モジュールで呼び出す。
◎ ソースその1。win32event版。 :
まずは win32event等を経由して使う方法を試してみた。動作には pywin32 のインストールが必要。今回は pywin32 304 がインストールされた。
_01_mutex.py
python 01_mutex.py で実行すると、1秒毎に、0 から 9 までの数字を出す。
DOS窓を複数開いておいて、どこかのDOS窓でスクリプトを動かしてから、すかさず別のDOS窓で同じスクリプトを動かしてみると、後から実行したほうは「〜 already exists」と出力して即座に終了してくれた。たしかに、多重起動を禁止するスクリプトになってくれた模様。
ちょっとよく分からなかったのが、win32event.ReleaseMutex(mtx) を呼ぶと必ずエラーになること。これは呼ばなくてもいいのだろうか…? クローズ処理として win32api.CloseHandle(mtx) か mtx.Close() は呼んでおくらしいけど…。巷のサンプルを眺めても、win32event.ReleaseMutex(mtx) を呼んでる事例は見かけなかったので、呼ばなくてもいいのかもしれない。
pip install pywin32
_01_mutex.py
import win32event import win32api import winerror import win32security import time import sys MUTEXNAME = "python_mutex_sample_01" def main(): sa = win32security.SECURITY_ATTRIBUTES() sa.SECURITY_DESCRIPTOR.SetSecurityDescriptorDacl(True, None, False) mtx = win32event.CreateMutex(sa, False, MUTEXNAME) err = win32api.GetLastError() if not mtx or err == winerror.ERROR_ALREADY_EXISTS: # Process exists print("%s already exists" % MUTEXNAME) sys.exit(0) else: # New process print("New process.") for i in range(10): print(i) time.sleep(1) if mtx: # win32event.ReleaseMutex(mtx) # win32api.CloseHandle(mtx) mtx.Close() if __name__ == '__main__': main()
- Mutex を作る際は、他のプロセスと被らないような独自の文字列を渡す。
- CreateMutex() を呼んだ直後にエラー情報を調べることで、その Mutex が既にあるかどうかが分かる模様。
- 処理が終わったら、mtx.Close() を呼んでハンドルをクローズする。
python 01_mutex.py で実行すると、1秒毎に、0 から 9 までの数字を出す。
DOS窓を複数開いておいて、どこかのDOS窓でスクリプトを動かしてから、すかさず別のDOS窓で同じスクリプトを動かしてみると、後から実行したほうは「〜 already exists」と出力して即座に終了してくれた。たしかに、多重起動を禁止するスクリプトになってくれた模様。
ちょっとよく分からなかったのが、win32event.ReleaseMutex(mtx) を呼ぶと必ずエラーになること。これは呼ばなくてもいいのだろうか…? クローズ処理として win32api.CloseHandle(mtx) か mtx.Close() は呼んでおくらしいけど…。巷のサンプルを眺めても、win32event.ReleaseMutex(mtx) を呼んでる事例は見かけなかったので、呼ばなくてもいいのかもしれない。
◎ ソースその2。ctypes版。 :
ctypesを使って呼び出す方法も試してみた。
_02_mutex_ctypes.py
インポートするモジュール数は少なくなってくれた気がする。ただ、ソースが少し分かりづらく…。いや、あまり違いはないか…。
ctypes 経由で呼び出す版は、最後に .ReleaseMutex() と .CloseHandle() を呼び出してもエラーにならなかった。
_02_mutex_ctypes.py
import ctypes import time MUTEXNAME = "python_mutex_sample_02" def main(): knl32 = ctypes.windll.Kernel32 mtx = knl32.CreateMutexA(0, 1, MUTEXNAME) result = knl32.WaitForSingleObject(mtx, 0) if result != 0: print("%s already exists" % MUTEXNAME) else: print("New process.") for i in range(10): print(i) time.sleep(1) knl32.ReleaseMutex(mtx) knl32.CloseHandle(mtx) if __name__ == '__main__': main()
インポートするモジュール数は少なくなってくれた気がする。ただ、ソースが少し分かりづらく…。いや、あまり違いはないか…。
ctypes 経由で呼び出す版は、最後に .ReleaseMutex() と .CloseHandle() を呼び出してもエラーにならなかった。
[ ツッコむ ]
#2 [python][pygame][windows] pygameでWindows用スクリーンセーバを作る
Windows10 x64 21H2 + Python 3.8.10 64bit + pygame 1.9.6 を使って、Windows用のスクリーンセーバが作れそうかどうか試してみた。
動作画面は以下のような感じ。解像度が低過ぎてアレだけど、雰囲気ぐらいは分かるかなと。また、かなり後で話が出てくるけれど、プレビューが表示されるまで妙に待たされてることも分かるかと思う。
動作画面は以下のような感じ。解像度が低過ぎてアレだけど、雰囲気ぐらいは分かるかなと。また、かなり後で話が出てくるけれど、プレビューが表示されるまで妙に待たされてることも分かるかと思う。
◎ 制限事項。 :
pygame でWindows用スクリーンセーバを作成するにあたって、いくつか制限事項(?)がある。
- pygame は 1.9.x であること。pygame 2.x.x は、環境変数 SDL_WINDOWID が反映されないので作れない。(pygame 1.x.x はSDL1.xを、pygame 2.x.x はSDL2を使っていて、SLD2 は件の環境変数が反映されない。)
- pygame 1.9.x の最終バージョン、pygame 1.9.6 は、Python 3.8 までの対応なので、pygame 1.9.6 を使いたいなら、Python 2.7 - 3.8 のどれかで作ることになる。
◎ Windows用スクリーンセーバについてのおさらい。 :
Windows用スクリーンセーバ(.scr)はどんな仕様を要求されるのか、念のために再度列挙してみる。
コマンドラインオプションについて。
「/p xxxx」にも対応させることを考えると、以下の条件を満たすプログラミング言語/ライブラリ/フレームワークなら、Windows用スクリーンセーバを作れることになる。
多重起動禁止処理について。
インストール場所について。
- Windows用スクリーンセーバは、.exe を .scr にリネームしたもの。
コマンドラインオプションについて。
- .scr は、3種類のコマンドラインオプション、「/s」「/c:xxxx」「/p xxxx」のどれかが渡されて実行される。
- .scrファイルを右クリックして出てくるメニューから、「テスト」「構成」等を選んだ場合、コマンドラインオプションがつかない状態で実行される場合もある。
- /s、/c、/p は、大文字だったり小文字だったりするので、プログラム側で大文字化、または小文字化をしてから判別したほうが良い。
- /s はフルスクリーン表示モード。キーボード操作やマウス操作を検出して、操作があったら自分で終了するように作っておく。
- /c:xxxx は、そのスクリーンセーバの設定ダイアログ表示モード。本来は設定内容を .ini やレジストリ等に記録することになるけれど、特に設定項目が無いなら、スクリーンセーバ名を表示するダイアログが出るだけで十分。
- /p xxxx は、スクリーンセーバ設定画面のプレビュー窓の中に表示するモード。xxxx はプレビュー窓のウインドウハンドル。
「/p xxxx」にも対応させることを考えると、以下の条件を満たすプログラミング言語/ライブラリ/フレームワークなら、Windows用スクリーンセーバを作れることになる。
- 与えられたウインドウハンドルからウインドウサイズを取得できて、
- そのウインドウハンドルのウインドウに対して描画ができる。
多重起動禁止処理について。
- スクリーンセーバ設定画面で何かしら操作をすると、その都度 /p xxxx つきでスクリーンセーバが何度も何度も実行されるので、特に何もしないと同じ処理をするプロセスが2つも3つも走ってしまう。多重起動を抑止する処理が必要になる。
- スクリーンセーバが実行されるまでの時間の間隔で、スクリーンセーバが何度も実行される可能性がある。フルスクリーン表示時も、一応、多重起動の禁止処理を入れておいたほうが安心。
インストール場所について。
- Windows が64bit版、スクリーンセーバが32bit版のプログラムなら、C:\Windows\SysWOW64\ 以下に .scr をコピーする。
- Windows が64bit版、スクリーンセーバが64bit版のプログラムなら、C:\Windows\System32\ 以下に .scr をコピーする。
- Windows が32bit版なら、C:\Windows\System32\ 以下に .scr をコピーする。
◎ 開発環境。 :
今回は、Windows10 x64 21H2上で Python 3.9.13 64bit版や 3.8.10 64bit版をインストールしてある状態で、virtualenv を使って Python 3.8 64bit の開発環境を用意してから作業した。
後々、PyInstaller や Nuitka を使って、.py ファイルをexe化するわけだけど。不要なモジュールまで exe に含められるのは困るので、必要最低限のモジュールしか入っていないクリーン(?)な環境を用意できたほうが exe化するには都合がいい。まあ、virtualenv ではなく、venv 等を使っても同じことはできると思うけど…。
virtualenv のインストールと、Python 3.8 の開発環境を作成。
開発環境の有効化。
Pythonのバージョンを確認。
ちなみに、開発環境の無効化は以下。
後々、PyInstaller や Nuitka を使って、.py ファイルをexe化するわけだけど。不要なモジュールまで exe に含められるのは困るので、必要最低限のモジュールしか入っていないクリーン(?)な環境を用意できたほうが exe化するには都合がいい。まあ、virtualenv ではなく、venv 等を使っても同じことはできると思うけど…。
virtualenv のインストールと、Python 3.8 の開発環境を作成。
python -m pip install -U setuptools virtualenv virtualenv -p python3.8 venv38
開発環境の有効化。
.\env38\Scripts\activate
Pythonのバージョンを確認。
python -V
(venv38) ... > python -V Python 3.8.10
ちなみに、開発環境の無効化は以下。
deactivate
◎ 必要なモジュールをインストール。 :
スクリーンセーバを作る際に必要になるモジュールをインストール。pygame、pywin32 と、exe化するための何かしらが入っていれば済む。
手元の開発環境では、以下が入っている。余計なモジュールも入ってるけど…。(Pythonスクリプトの整形ツール、autopep8 とか…。)
python -m pip install pygame==1.9.6 python -m pip install pywin32 python -m pip install pyinstaller python -m pip install nuitka zstandard orderedset
手元の開発環境では、以下が入っている。余計なモジュールも入ってるけど…。(Pythonスクリプトの整形ツール、autopep8 とか…。)
(venv38) ... > python -m pip list Package Version ------------------------- --------- altgraph 0.17.2 autopep8 1.7.0 cachetools 5.2.0 future 0.18.2 Nuitka 1.0.3 orderedset 2.0.3 pefile 2022.5.30 pip 22.2.2 pycodestyle 2.9.1 pygame 1.9.6 pyinstaller 5.3 pyinstaller-hooks-contrib 2022.8 pywin32 304 pywin32-ctypes 0.2.0 setuptools 63.4.1 toml 0.10.2 wheel 0.37.1 zstandard 0.18.0
◎ ソース。 :
Python 3.8 + pygame 1.9.6 + pywin32 で作成した、Windows用スクリーンセーバのソースは以下。画面の中を赤いボールが跳ね回るだけの、いつもの処理内容。
_scrsavpygame.py
使用画像は以下。scrsavpygame.py と同階層に、resources というディレクトリを作成して、その中に入れておく。
_ball_64x64.png
_preview_bg.png
_preview_logo.png
_scrsavpygame.py
import ctypes import datetime import math import os import sys import time import win32api import win32event import win32gui import win32security import winerror DBG = False IMG_NAME = "resources/ball_64x64.png" PREVIEW_BG_IMG = "resources/preview_bg.png" PREVIEW_LOGO_IMG = "resources/preview_logo.png" APPLI_NAME = "screensaver sample by using pygame" VER_NUM = "0.0.1" MUTEX_NAME_FILLSCR = "screensaver_pygame_fullscr" MUTEX_NAME_CONFIG = "screensaver_pygame_config" MUTEX_NAME_PREVIEW = "screensaver_pygame_preview" def resource_path(filename): """Get resource file path.""" if hasattr(sys, "_MEIPASS"): # use PyInstaller base_dir = sys._MEIPASS else: base_dir = os.path.abspath(".") return os.path.join(base_dir, filename) def full_screen(screen): """drawing pygame window.""" # invisivle mouse cursor pygame.mouse.set_visible(False) # load image img = pygame.image.load(resource_path(IMG_NAME)).convert() img.set_colorkey(img.get_at((0, 0)), pygame.RLEACCEL) # initialize work scrw, scrh = screen.get_width(), screen.get_height() x, y = scrw / 2, scrh / 2 dx = float(scrw) / (60 * 2) dy = float(scrh) / (60 * 3) clock = pygame.time.Clock() counter = 0 looping = True # main loop while looping: # check key, mouse for event in pygame.event.get(): if event.type == pygame.QUIT: looping = False if event.type == pygame.KEYUP: # any key up looping = False if event.type == pygame.MOUSEMOTION: # move mouse if counter >= 120: looping = False if not looping: break # update sprite position w, h = img.get_width(), img.get_height() x += dx y += dy if x <= (w / 2) or x >= scrw - (w / 2): dx *= -1 if y <= (h / 2) or y >= scrh - (h / 2): dy *= -1 # clear bg screen.fill((16, 16, 16)) # draw sprite px, py = int(x - (w / 2)), int(y - (h / 2)) screen.blit(img, (px, py)) counter += 1 pygame.display.flip() clock.tick(60) # visible mouse cursor pygame.mouse.set_visible(True) def preview(screen): frames = 360 * 2 bgimg = pygame.image.load(resource_path(PREVIEW_BG_IMG)).convert() logoimg = pygame.image.load(resource_path(PREVIEW_LOGO_IMG)).convert() logoimg.set_colorkey(logoimg.get_at((0, 0)), pygame.RLEACCEL) clock = pygame.time.Clock() ang = 0 looping = True for i in range(frames): for event in pygame.event.get(): if event.type == pygame.QUIT: looping = False if event.type == pygame.KEYDOWN: if event.key == pygame.K_ESCAPE: looping = False if not looping: break scrw, scrh = screen.get_width(), screen.get_height() screen.fill((100, 150, 200)) iw, ih = bgimg.get_width(), bgimg.get_height() px = int((scrw - iw) / 2) py = int((scrh - ih) / 2) screen.blit(bgimg, (px, py)) iw, ih = logoimg.get_width(), logoimg.get_height() px = int((scrw - iw) / 2) py = int((scrh * 0.15) * math.sin(math.radians(ang)) + (scrh / 2) - (ih / 2)) screen.blit(logoimg, (px, py)) ang += 3 pygame.display.flip() clock.tick(60) def get_window_size(hwnd): """Get window size.""" rect = win32gui.GetWindowRect(hwnd) x0, y0, x1, y1 = rect[0], rect[1], rect[2], rect[3] return ((x1 - x0), (y1 - y0)) def msg_box(msg, title): """Display MessageBox by Windows API.""" user32 = ctypes.WinDLL("user32") # user32.MessageBoxW.restype = ctypes.c_int32 # user32.MessageBoxW.argtypes = (ctypes.c_void_p, ctypes.c_wchar_p, # ctypes.c_wchar_p, ctypes.c_uint32) user32.MessageBoxW(0, msg, title, 0x00000040) def write_log(s): global DBG if DBG: desktop_dir = os.path.expanduser('~/Desktop') log_file = os.path.join(desktop_dir, "temp.log.txt") dt_now = datetime.datetime.now().strftime("%Y/%m/%d %H:%M:%S") with open(log_file, 'a') as f: f.write("[%s] %s\n" % (dt_now, s)) def get_mutex(name): sa = win32security.SECURITY_ATTRIBUTES() sa.SECURITY_DESCRIPTOR.SetSecurityDescriptorDacl(True, None, False) mtx = win32event.CreateMutex(sa, False, name) err = win32api.GetLastError() if not mtx or err == winerror.ERROR_ALREADY_EXISTS: return (mtx, False) return (mtx, True) def close_mutex(mtx): global cmdopt if mtx: # win32event.ReleaseMutex(mtx) # win32api.CloseHandle(mtx) mtx.Close() write_log("%s : Close mutex. Exit." % cmdopt) # ---------------------------------------- # parse commandline option kind = "None" hwnd = 0 if len(sys.argv) >= 2: s = sys.argv[1][0:2].lower() if s == "/p": kind = "preview" hwnd = int(sys.argv[2]) elif s == "/c": kind = "config" elif s == "/s": kind = "fullscreen" cmdopt = " ".join(sys.argv) if kind == "config": # ---------------------------------------- # config / setting mtx, success = get_mutex(MUTEX_NAME_CONFIG) if not success: # Process exists write_log("%s : %s already exists. Exit." % (cmdopt, MUTEX_NAME_CONFIG)) sys.exit(0) # new process write_log("%s : New process." % cmdopt) msg_box("%s\nver.%s" % (APPLI_NAME, VER_NUM), "Information") close_mutex(mtx) sys.exit() if kind == "preview": # ---------------------------------------- # preview mtx, success = get_mutex(MUTEX_NAME_PREVIEW) if not success: write_log("%s : %s already exists. Exit." % (cmdopt, MUTEX_NAME_PREVIEW)) sys.exit(0) os.environ['SDL_VIDEODRIVER'] = 'windib' if hwnd != 0: # set SDL_WINDOWID os.environ['SDL_WINDOWID'] = str(hwnd) wdw_size = get_window_size(hwnd) write_log("Windows size: %d x %d" % (wdw_size)) else: wdw_size = (152, 112) write_log("%s : New process. hwnd = %d , Wdw size = %d x %d" % (cmdopt, hwnd, wdw_size[0], wdw_size[1])) import pygame # nopep8 pygame.init() screen = pygame.display.set_mode(wdw_size) preview(screen) pygame.quit() close_mutex(mtx) sys.exit() # ---------------------------------------- # full screen mtx, success = get_mutex(MUTEX_NAME_FILLSCR) if not success: # Process exists write_log("%s : %s already exists. Exit." % (cmdopt, MUTEX_NAME_FILLSCR)) sys.exit(0) # New process write_log("%s : New process." % cmdopt) import pygame # nopep8 pygame.init() screen = pygame.display.set_mode((0, 0), pygame.FULLSCREEN) full_screen(screen) pygame.quit() close_mutex(mtx) sys.exit()
使用画像は以下。scrsavpygame.py と同階層に、resources というディレクトリを作成して、その中に入れておく。
_ball_64x64.png
_preview_bg.png
_preview_logo.png
(venv38) ... > tree /A /f D:. | scrsavpygame.py | +---resources ball_64x64.png preview_bg.png preview_logo.png
◎ ソースについて少し説明。 :
/p xxxx を渡して実行した時(プレビューモード)の動作は、一定時間アニメーションを表示して、その後終了する作りにしてみた。
当初、与えられたウインドウハンドルに対して何かを描画して即座に終了する処理で十分だろうと思っていたのだけど。その方法を実際に試してみたら、描画内容が全く反映されず。どうやら数秒ほど延々と描画を続けてみないと、見た目が反映されないようだなと…。何故かは分からないけれど…。
しかし、そのままいつまでもアニメーションを描画し続けるわけにもいかなくて。スクリーンセーバ設定画面が閉じられた際、スクリーンセーバも終了しなければいけないはずだけど、どうすればスクリーンセーバ設定画面の終了を検出できるのか、そこが分からない。仕方なく、一定時間が経過したら問答無用で終了してしまうことにしてお茶を濁した。このあたり、解決策はあるのだろうか…?
多重起動抑止については、以下を参照のこと。
_Pythonスクリプトの多重起動を抑止したい
環境変数 SDL_WINDOWID を使って、pygame のウインドウを埋め込む処理については、以下を参照のこと。
_tkinterの中にpygameを埋め込む
当初、与えられたウインドウハンドルに対して何かを描画して即座に終了する処理で十分だろうと思っていたのだけど。その方法を実際に試してみたら、描画内容が全く反映されず。どうやら数秒ほど延々と描画を続けてみないと、見た目が反映されないようだなと…。何故かは分からないけれど…。
しかし、そのままいつまでもアニメーションを描画し続けるわけにもいかなくて。スクリーンセーバ設定画面が閉じられた際、スクリーンセーバも終了しなければいけないはずだけど、どうすればスクリーンセーバ設定画面の終了を検出できるのか、そこが分からない。仕方なく、一定時間が経過したら問答無用で終了してしまうことにしてお茶を濁した。このあたり、解決策はあるのだろうか…?
多重起動抑止については、以下を参照のこと。
_Pythonスクリプトの多重起動を抑止したい
環境変数 SDL_WINDOWID を使って、pygame のウインドウを埋め込む処理については、以下を参照のこと。
_tkinterの中にpygameを埋め込む
◎ Pythonスクリプトとして動作確認。 :
動作確認は以下。
「/p 0」を渡したときは、仮で小さなウインドウを生成して、その中にプレビュー窓用の内容をテスト描画するようにしてみた。本番では、例えば「/p 1049354」のように、何かしらの大きな数値(実在するウインドウハンドル)が渡されるはず。
python scrsavpygame.py /s python scrsavpygame.py /c python scrsavpygame.py /p 0 python scrsavpygame.py
「/p 0」を渡したときは、仮で小さなウインドウを生成して、その中にプレビュー窓用の内容をテスト描画するようにしてみた。本番では、例えば「/p 1049354」のように、何かしらの大きな数値(実在するウインドウハンドル)が渡されるはず。
◎ exeに変換。 :
Windows用スクリーンセーバとして利用するためには、ファイルの拡張子が .scr でなければならず、その .scr は .exe をリネームして作られるわけで…。つまり、この Pythonスクリプトをexe化しないと、スクリーンセーバとして利用できない。
今回は PyInstaller 5.3 を使ってexe化してみた。
まずは、以下を打って変換する。
distディレクトリが作られて、その中に scrsavpygame.exe というファイルが出来上がった。また、scrsavpygame.spec というファイルも生成された。
ただ、この状態では、動作に必要な画像ファイル群が ―― resources/*.png が同梱されていない。
scrsavpygame.spec を編集して、画像ファイル群も同梱させる指定をする。
「Tree('resources',prefix='resources'),」という行を挿入することで、「resourcesディレクトリも同梱せよ」と指示してる。
今度は、.specファイルを指定してexe化する。
dist/scrsavpygame.exe を実行して動作確認をしておく。
動作したら、.scr にリネームコピー。
出来上がった .scr を、既定の場所にコピーしてインストールする。
今回は PyInstaller 5.3 を使ってexe化してみた。
python -m pip install pyinstaller -U
まずは、以下を打って変換する。
pyinstaller --onefile --noconsole --noupx scrsavpygame.py
distディレクトリが作られて、その中に scrsavpygame.exe というファイルが出来上がった。また、scrsavpygame.spec というファイルも生成された。
ただ、この状態では、動作に必要な画像ファイル群が ―― resources/*.png が同梱されていない。
scrsavpygame.spec を編集して、画像ファイル群も同梱させる指定をする。
exe = EXE( pyz, a.scripts, a.binaries, ↓ exe = EXE( pyz, Tree('resources',prefix='resources'), a.scripts, a.binaries,
「Tree('resources',prefix='resources'),」という行を挿入することで、「resourcesディレクトリも同梱せよ」と指示してる。
今度は、.specファイルを指定してexe化する。
pyinstaller scrsavpygame.spec
dist/scrsavpygame.exe を実行して動作確認をしておく。
scrsavpygame.exe /s scrsavpygame.exe /c scrsavpygame.exe /p 0 scrsavpygame.exe
動作したら、.scr にリネームコピー。
copy scrsavpygame.exe scrsavpygame.scr
出来上がった .scr を、既定の場所にコピーしてインストールする。
- Windows が64bit版、スクリーンセーバが32bit版のプログラムなら、C:\Windows\SysWOW64\ 以下に .scr をコピーする。
- Windows が64bit版、スクリーンセーバが64bit版のプログラムなら、C:\Windows\System32\ 以下に .scr をコピーする。
- Windows が32bit版なら、C:\Windows\System32\ 以下に .scr をコピーする。
◎ スクリーンセーバとして動作確認。 :
スクリーンセーバの設定画面を呼び出して、「scrsavpygame」が増えているか確認する。Windows10における、スクリーンセーバ設定画面の呼び出し方は以下。
スクリーンセーバ設定画面が表示されるまで、妙に待たされた気もするけれど、それでも一応動作してくれた。
そんなわけで、Python 3.8 + pygame 1.9.6 + pywin32 を使ってWindows用スクリーンセーバを作れそうだ、と分かった。
- デスクトップの何もないところを右クリック → 個人用設定 → ロック画面 → スクリーンセーバー設定。
- あるいは、Windows10の左下の検索欄に「スクリーンセーバー」と打てば、「スクリーンセーバーの変更」という項目が出てくるので、それを選んでもいい。
スクリーンセーバ設定画面が表示されるまで、妙に待たされた気もするけれど、それでも一応動作してくれた。
そんなわけで、Python 3.8 + pygame 1.9.6 + pywin32 を使ってWindows用スクリーンセーバを作れそうだ、と分かった。
◎ 問題点。 :
一応それらしく動いたものの。現状では、ちょっと問題が…。
スクリーンセーバの設定画面で「scrsavpygame」を選ぶと、プレビュー窓の中に何かが表示されるまで随分待たされる…。おそらくだけど、以下が原因なのではなかろうか。
更に、出来上がった .scr に対して、Windows Defender が「これはウイルスだ!」と誤判定してきた。
PyInstaller を使って生成した .exe はウイルスとして誤判定されやすいらしい。巷のウイルスが PyInstaller等を使って作られているせいで、ウイルス対策ソフトが「PyInstaller で作られた .exe はウイルス」と決めつけてる節があるそうで。
もちろん、こうして自分で作ったプログラムだから、ウイルスなわけもなく…。Windows Defender に「このファイルはウイルス判定を除外せよ」と設定して使うしかなさそう。
ただ、出来上がった .scr を配布したいと思った場合はフツーに困る…。ダウンロードした人達が「これ、ウイルスじゃん!」と慌てちゃうだろうな…。
スクリーンセーバの設定画面で「scrsavpygame」を選ぶと、プレビュー窓の中に何かが表示されるまで随分待たされる…。おそらくだけど、以下が原因なのではなかろうか。
- PyInstaller で作成された .exe は、毎回一時フォルダに解凍する処理が走るので、起動がとにかく遅い。
- 毎回、Windows Defender によるウイルスチェックで時間がかかっているのかもしれない。
更に、出来上がった .scr に対して、Windows Defender が「これはウイルスだ!」と誤判定してきた。
PyInstaller を使って生成した .exe はウイルスとして誤判定されやすいらしい。巷のウイルスが PyInstaller等を使って作られているせいで、ウイルス対策ソフトが「PyInstaller で作られた .exe はウイルス」と決めつけてる節があるそうで。
もちろん、こうして自分で作ったプログラムだから、ウイルスなわけもなく…。Windows Defender に「このファイルはウイルス判定を除外せよ」と設定して使うしかなさそう。
ただ、出来上がった .scr を配布したいと思った場合はフツーに困る…。ダウンロードした人達が「これ、ウイルスじゃん!」と慌てちゃうだろうな…。
◎ Nuitkaでexe化してみる。 :
Pythonスクリプトをexe化するツールは、PyInstaller、py2exe、cx_Freeze など色々あるけれど。Pythonスクリプトを一旦C言語のソースに変換して、gccでコンパイルしてexeを作ってくれる Nuitka なるツールがあるそうで。PyInstaller で作ったexeより起動が速く、ファイルサイズも小さくなる傾向があるとのこと。興味が湧いた。試してみる。
インストールは以下。
自分の環境では、orderedset というモジュールもインストールしないと、処理が先に進まなかった。
Nuitka用に修正したソースは以下。
_scrsavpygame.py
変更点は2ヵ所。
リソースファイルパスを求める部分は以下になる。
exeへの変換は以下。
上記を実行したところ、途中で「gccその他が見つからないがダウンロードするか?」と尋ねてきた。「Yes」を入力してダウンロードすることにした。それぞれ、 C:\Users\(USERNAME)\AppData\Local\Nuitka\ 以下にキャッシュとしてダウンロードされる模様。ファイル数を調べてみたら、7923ファイル、全体で1.14GBが入っていた。まあ、Nuitka が要求する gccのバージョン等が固定されている可能性もあるので、このくらいは目を瞑ったほうが面倒臭くないのかもしれない。
数分かかって、.py と同階層に scrsavpygame.exe が作成された。ファイルサイズは 8.59MB。PyInstaller で生成した .exe は 13.06MB だったので、若干ファイルサイズも小さくなっている。
別ディレクトリに .exe をコピーして実行してみたところ、動作してくれた。
なんとなくだけど、PyInstaller で作った .exe より起動がちょっと速いような気もする。
スクリーンセーバとして利用できるか動作確認。.exe を .scr にリネームコピーして、スクリーンセーバの既定の置き場所にコピー。スクリーンセーバ設定画面を出してリストから選んだ。これも一応動いてくれた。
ただ、相変わらず、プレビュー窓内が表示されるまで結構待たされる…。こうなると、一時ディレクトリを作成して、動作に必要なファイルを全て展開してから実行するという、その仕組み自体が厳しいのかもしれない…。
ちなみに、--onefile ではなく --standalone を指定すれば、1つのファイルにまとめずに、*.dist というディレクトリに動作に必要なファイルを全てコピーした状態で .exe を生成してくれる。ファイル数は106ファイル、27.52MB だった。
つまり、1つの .exe にした場合、実行時は毎回106ファイルを一時ディレクトリに展開しているわけで…。それなりに起動が遅くて当然ですよね、という気もしてきた。
インストールは以下。
python -m pip install nuitka zstandard orderedset
自分の環境では、orderedset というモジュールもインストールしないと、処理が先に進まなかった。
Nuitka用に修正したソースは以下。
_scrsavpygame.py
変更点は2ヵ所。
- 一行目のシバン(shebang)を、「#!python」から「#!/usr/bin/python3.8」に変更。この変更をしないと、「Pythonのバージョンが違う」と文句を言ってきて処理が進まなかった。
- 画像ファイル(リソースファイル)を読み込む際の、ファイルパスの求め方を修正。
リソースファイルパスを求める部分は以下になる。
def resource_path(filename): """Get resource file path.""" if False: if hasattr(sys, "_MEIPASS"): # use PyInstaller base_dir = sys._MEIPASS else: base_dir = os.path.abspath(".") else: base_dir = os.path.dirname(__file__) return os.path.join(base_dir, filename)「if False:」で無効化してあるところが、PyInstaller、あるいは通常のPythonスクリプトとして実行する際の処理。Nuitka用は、os.path.dirname(__file__) で基準ディレクトリ(?)を求めることになる模様。
exeへの変換は以下。
python -m nuitka --mingw64 --windows-disable-console --include-data-file=".\\resources\\*.png=.\\resources\\" --onefile scrsavpygame.py
- python -m nuika : nuika の呼び出し。
- --mingw64 : mingw64 (gcc) を使うことを指定。
- --windows-disable-console : exe実行時にコンソールを非表示にする。
- --include-data-file=".\\resources\\*.png=.\\resources\\" : リソースファイル(*.png)を同梱させる。
- --onefile : リソースファイルも含めて1つのexeファイルにする。
- scrsavpygame.py : exe化したいPythonスクリプト名。
上記を実行したところ、途中で「gccその他が見つからないがダウンロードするか?」と尋ねてきた。「Yes」を入力してダウンロードすることにした。それぞれ、 C:\Users\(USERNAME)\AppData\Local\Nuitka\ 以下にキャッシュとしてダウンロードされる模様。ファイル数を調べてみたら、7923ファイル、全体で1.14GBが入っていた。まあ、Nuitka が要求する gccのバージョン等が固定されている可能性もあるので、このくらいは目を瞑ったほうが面倒臭くないのかもしれない。
数分かかって、.py と同階層に scrsavpygame.exe が作成された。ファイルサイズは 8.59MB。PyInstaller で生成した .exe は 13.06MB だったので、若干ファイルサイズも小さくなっている。
別ディレクトリに .exe をコピーして実行してみたところ、動作してくれた。
scrsavpygame.exe /s scrsavpygame.exe /c scrsavpygame.exe /p 0 scrsavpygame.exe
なんとなくだけど、PyInstaller で作った .exe より起動がちょっと速いような気もする。
スクリーンセーバとして利用できるか動作確認。.exe を .scr にリネームコピーして、スクリーンセーバの既定の置き場所にコピー。スクリーンセーバ設定画面を出してリストから選んだ。これも一応動いてくれた。
ただ、相変わらず、プレビュー窓内が表示されるまで結構待たされる…。こうなると、一時ディレクトリを作成して、動作に必要なファイルを全て展開してから実行するという、その仕組み自体が厳しいのかもしれない…。
ちなみに、--onefile ではなく --standalone を指定すれば、1つのファイルにまとめずに、*.dist というディレクトリに動作に必要なファイルを全てコピーした状態で .exe を生成してくれる。ファイル数は106ファイル、27.52MB だった。
つまり、1つの .exe にした場合、実行時は毎回106ファイルを一時ディレクトリに展開しているわけで…。それなりに起動が遅くて当然ですよね、という気もしてきた。
◎ 参考ページ。 :
_Pyinstaller でリソースを含めたexeを作成する - Qiita
_UI に画像を埋め込んだ Python プログラムを exe化する - NakaNote
_【python, pyinstaller】画像や音楽などの外部ファイルも一括でexe化して配布する - MSBOOKS
_Pythonのexe化で画像を組み込みたい
_python - Pyinstaller and --onefile: How to include an image in the exe file - Stack Overflow
_PyInstaller より圧倒的に優れている Nuitka の使い方とハマったポイント | つくみ島だより
_KivyのGUIアプリをNuitkaで簡単に小サイズの実行ファイル(exe)にする(Windows10) - Qiita
_Nuitkaで失敗しやすいポイントを解説 - Qiita
_Nuitkaを使ってスクリプトをバイナリ化してみよう - PythonOsaka
_Kivyで作ったアプリをNuitkaでexeファイル化した時の試行錯誤のメモの巻
_UI に画像を埋め込んだ Python プログラムを exe化する - NakaNote
_【python, pyinstaller】画像や音楽などの外部ファイルも一括でexe化して配布する - MSBOOKS
_Pythonのexe化で画像を組み込みたい
_python - Pyinstaller and --onefile: How to include an image in the exe file - Stack Overflow
_PyInstaller より圧倒的に優れている Nuitka の使い方とハマったポイント | つくみ島だより
_KivyのGUIアプリをNuitkaで簡単に小サイズの実行ファイル(exe)にする(Windows10) - Qiita
_Nuitkaで失敗しやすいポイントを解説 - Qiita
_Nuitkaを使ってスクリプトをバイナリ化してみよう - PythonOsaka
_Kivyで作ったアプリをNuitkaでexeファイル化した時の試行錯誤のメモの巻
[ ツッコむ ]
以上、1 日分です。