2026/01/05(月) [n年前の日記]
#1 [python][windows] Pythonで作ったスクリーンセーバのプレビュー画面がなかなか出てこない
ここ数日、Python + pywin32 + Nuitka を使って Windows用のスクリーンセーバを作れないかと実験しているけれど。
_PythonでWindows用スクリーンセーバを作成してみた - mieki256's diary
_Pythonで作ったスクリーンセーバを修正中 - mieki256's diary
しかし、プレビュー画面モード時の動作が ―― 「/p HWND」(HWNDはウインドウハンドル)が渡されて実行される際の動作が期待した状態にならない。単にbmp画像を表示するだけの処理なのに、数秒経過してからようやく画像が表示されてしまう。
色々実験してみたけれど、結論を先に書く。PythonでWindows用のスクリーンセーバを作成する場合、以下のどちらかで実装するしかなさそう。
_PythonでWindows用スクリーンセーバを作成してみた - mieki256's diary
_Pythonで作ったスクリーンセーバを修正中 - mieki256's diary
しかし、プレビュー画面モード時の動作が ―― 「/p HWND」(HWNDはウインドウハンドル)が渡されて実行される際の動作が期待した状態にならない。単にbmp画像を表示するだけの処理なのに、数秒経過してからようやく画像が表示されてしまう。
色々実験してみたけれど、結論を先に書く。PythonでWindows用のスクリーンセーバを作成する場合、以下のどちらかで実装するしかなさそう。
- プレビュー画面モードは何もしないですぐに終了させる。真っ黒画面が表示されてしまうけど、その代わりすぐに操作できるようになるので…。まあ、勘弁してほしい。
- 一応プレビュー画面モードで何か描画する。数秒待たされてしまうけれど、その代わりそれっぽいプレビュー画面が表示されるので…。まあ、勘弁してほしい。
◎ 検証 :
以下、検証内容。
プレビュー画面モードしか動かない、最低限の処理しかしないスクリプトを作成して試してみた。処理内容は、与えられたウインドウハンドルを親とした子ウインドウを生成して、子ウインドウの全面を青一色で塗り潰すだけ。やってることがめちゃくちゃ少ないので、同梱されるファイル群も最小限になって、Nuitka で1ファイルにexe化した場合のファイル展開も軽くなるはず…。
環境は Windows10 x64 22H2 + Python 3.10.10 64bit。
動作には pywin32 が必要。以下でインストールできる。今回は pywin32 311 がインストールされた。
_pywinsc0.py
このスクリプトを、Nuitka を使ってexe化する。インストールは以下。今回は Nuitka 2.8.9 がインストールされた。
Nutika を使って .py を .exe に変換。pywinsc0.exe が出来上がる。--onefile をつけているので、1ファイルだけになる。
exeファイルをリネームコピーして、.scr を作成。
以下の場所に .scr をコピー。
「スクリーンセーバーの変更」を起動して、リストの中から「pywinsc0」を選べば動作確認できる。
プレビュー画面モードしか動かない、最低限の処理しかしないスクリプトを作成して試してみた。処理内容は、与えられたウインドウハンドルを親とした子ウインドウを生成して、子ウインドウの全面を青一色で塗り潰すだけ。やってることがめちゃくちゃ少ないので、同梱されるファイル群も最小限になって、Nuitka で1ファイルにexe化した場合のファイル展開も軽くなるはず…。
環境は Windows10 x64 22H2 + Python 3.10.10 64bit。
動作には pywin32 が必要。以下でインストールできる。今回は pywin32 311 がインストールされた。
python -m pip install pywin32
_pywinsc0.py
"""
与えられたウインドウハンドル(HWND)を親として、
子ウインドウを作成して1色で塗り潰す。
スクリーンセーバのプレビューモードの反応を確認するために作成。
以下のオプションを受け付ける。
* /p <HWND> : プレビュー画面モード
* /c : 設定画面モード。今回はメッセージボックスのみを表示
Windows11 x64 25H2 + Python 3.10.10 64bit
"""
import sys
import ctypes
import win32gui
import win32api
import win32con
dbg = False
# dbg = True
# 描画処理をするかしないか
draw_enable = True
# draw_enable = False
WINDOW_CLASSNAME = "PythonFillOnlySimpleWindow"
# Windows11等の高解像度ディスプレイへの対応
try:
ctypes.windll.shcore.SetProcessDpiAwareness(1)
except Exception:
ctypes.windll.user32.SetProcessDPIAware()
def print_log(s: str):
"""コンソールにメッセージを出力"""
if dbg:
print(s)
class MyChildWindow:
"""子ウインドウ担当クラス"""
def __init__(self, parent_hwnd):
"""初期化処理"""
self.parent_hwnd = parent_hwnd # 親のウインドウハンドル
self.class_name = WINDOW_CLASSNAME
self.paint_count = 0
self.hwnd = self._create_window()
def _create_window(self):
"""子ウインドウを作成"""
wc = win32gui.WNDCLASS()
wc.lpfnWndProc = self.wnd_proc
wc.lpszClassName = self.class_name
wc.hInstance = win32api.GetModuleHandle(None)
wc.hCursor = win32gui.LoadCursor(0, win32con.IDC_ARROW)
wc.hbrBackground = win32gui.CreateSolidBrush(win32api.RGB(0, 0, 0))
# リサイズ時に再描画を要求
# wc.style = win32con.CS_HREDRAW | win32con.CS_VREDRAW
try:
# 定義したクラスをOSに登録
win32gui.RegisterClass(wc)
except win32gui.error as e:
# 登録済みなら Error code = 1410 が出るので、それ以外なら例外を出す
if e.winerror != 1410:
raise
# 親のクライアント領域を取得
rect = win32gui.GetClientRect(self.parent_hwnd)
w = rect[2] - rect[0]
h = rect[3] - rect[1]
# ウインドウを作成。ウインドウハンドルを返す
hwnd = win32gui.CreateWindowEx(
0, # 拡張スタイル(今回は無し)
self.class_name, # 登録クラス名
"Child Window", # ウインドウタイトル
win32con.WS_CHILD | win32con.WS_VISIBLE, # 子ウィンドウとして作成、即表示
0,
0,
w,
h, # 親の左上(0,0)を起点に配置
self.parent_hwnd, # 親ウィンドウハンドル
0, # メニューなし
win32api.GetModuleHandle(None), # インスタンス
None, # 追加パラメータなし
)
if not hwnd:
print_log("[Child] Error: ウインドウ生成に失敗")
return None
print_log(f"[Child] ウインドウ生成。HWND: {hwnd}")
return hwnd
def wnd_proc(self, hwnd, msg, wparam, lparam):
"""ウインドウプロシージャ"""
if msg == win32con.WM_PAINT:
# 描画を要求された
if win32gui.IsWindow(self.hwnd):
self.paint_count += 1
print_log(f"[Child] WM_PAINT 受信。count={self.paint_count}")
hdc = win32gui.GetDC(hwnd) # 描画を準備。hdc(デバイスコンテキスト)取得
rect = win32gui.GetClientRect(hwnd) # クライアント領域のサイズを取得
col = win32api.RGB(0, 0, 255) # 色を設定
brush = win32gui.CreateSolidBrush(col) # ブラシを作成
win32gui.FillRect(hdc, rect, brush) # 塗り潰し
# クリーンアップ。ブラシやhdcを削除や開放しないとメモリリークを起こす
win32gui.DeleteObject(brush)
win32gui.ReleaseDC(hwnd, hdc)
return 0 # メッセージを処理した場合は0を返してやる
if msg == win32con.WM_DESTROY:
# ウインドウの破棄を要求された
print_log("[Child] WM_DESTROY 受信。Quit を送信。")
win32gui.PostQuitMessage(0)
return 0
# 自分で処理しないメッセージはOS既定の処理に渡す
return win32gui.DefWindowProc(hwnd, msg, wparam, lparam)
def run(self):
if win32gui.IsWindow(self.hwnd):
# 初回描画を強制的に促す(メッセージキューにWM_PAINTを入れる)
win32gui.InvalidateRect(self.hwnd, None, True)
# キューを待たずに、今すぐ描画を反映
win32gui.UpdateWindow(self.hwnd)
# メッセージループ開始。
# OSからの信号(WM_PAINT等)を待ち受けて wnd_procへ流し続ける
# これがないとスクリプトは終了してしまう。
win32gui.PumpMessages()
return
def config_mode():
"""設定画面モード"""
# メッセージボックスのみを表示
message = "Config mode"
title = "Information"
ctypes.windll.user32.MessageBoxW(None, message, title, 0)
def main():
# コマンドラインオプションを解析して処理を分ける
args = sys.argv[1:]
if len(args) >= 1:
opt = args[0].lower()
if opt == "/p" and len(args) == 2:
hwnd = int(sys.argv[2])
if draw_enable:
# 子ウインドウを生成して描画する状態
if win32gui.IsWindow(hwnd):
preview = MyChildWindow(hwnd)
preview.run()
else:
# 何もしないで抜ける状態
return
elif opt == "/c":
config_mode()
elif opt == "/s":
print_log("Fullscreen mode")
else:
config_mode()
else:
config_mode()
print_log("[Child] プロセス終了")
if __name__ == "__main__":
main()
このスクリプトを、Nuitka を使ってexe化する。インストールは以下。今回は Nuitka 2.8.9 がインストールされた。
python -m pip install nuitka
Nutika を使って .py を .exe に変換。pywinsc0.exe が出来上がる。--onefile をつけているので、1ファイルだけになる。
python -m nuitka --remove-output --windows-console-mode=disable --follow-imports --onefile pywinsc0.py
exeファイルをリネームコピーして、.scr を作成。
copy pywinsc0.exe pywinsc0.scr
以下の場所に .scr をコピー。
C:\Windows\System32\
「スクリーンセーバーの変更」を起動して、リストの中から「pywinsc0」を選べば動作確認できる。
◎ 動作結果 :
結果だけど、相変わらず5秒ぐらい待たされてしまう…。ダメだこりゃ。
◎ 考察 :
「スクリーンセーバーの変更」上で「設定」ボタンを押した時、つまりは「/p HWND」ではなく、「/c:HWND」あるいは「/c」を与えて実行された時は、ほぼ瞬時にメッセージボックスが表示されている。
つまり、Nuitka で作った .exe (.scr) がテンポラリフォルダにファイル群を展開している時間はそこそこ短くて済んでいるはず。もし、展開時間が長ければ、メッセージボックスが表示される際も、もっと時間がかかるはずなので…。
Nuitka に、1ファイル化を指定する --onefile ではなく、ファイル群を展開したままの状態でexe化させる --standalone を指定して exe を作成してみた。
必要なファイル数はたかだか12ファイルだった。1ファイル化する場合、この12ファイルが内包されて、実行時には展開されるのだろう。
tkinter を使ったスクリプトを Nuitka でexe化した際は 1000ファイル以上のファイル群になっていたので、それと比べたら圧倒的に展開時間は短くなってくれたはず。
しかしそれでも、プレビュー画面モードは表示が遅い…。設定画面モードなら瞬時に表示されているのだから、プレビュー画面モードの動作だけがおかしい…。
つまり、Nuitka で作った .exe (.scr) がテンポラリフォルダにファイル群を展開している時間はそこそこ短くて済んでいるはず。もし、展開時間が長ければ、メッセージボックスが表示される際も、もっと時間がかかるはずなので…。
Nuitka に、1ファイル化を指定する --onefile ではなく、ファイル群を展開したままの状態でexe化させる --standalone を指定して exe を作成してみた。
必要なファイル数はたかだか12ファイルだった。1ファイル化する場合、この12ファイルが内包されて、実行時には展開されるのだろう。
> dir
...
2026/01/05 19:38 <DIR> .
2026/01/05 19:38 <DIR> ..
2026/01/05 19:38 32,792 libffi-7.dll
2026/01/05 19:38 4,462,360 python310.dll
2026/01/05 19:38 679,936 pythoncom310.dll
2026/01/05 19:38 3,787,776 pywinsc0.exe
2026/01/05 19:38 135,168 pywintypes310.dll
2026/01/05 19:38 29,976 select.pyd
2026/01/05 19:38 1,123,608 unicodedata.pyd
2026/01/05 19:38 98,224 vcruntime140.dll
2026/01/05 19:38 37,256 vcruntime140_1.dll
2026/01/05 19:38 134,144 win32api.pyd
2026/01/05 19:38 219,648 win32gui.pyd
2026/01/05 19:38 123,672 _ctypes.pyd
12 個のファイル 10,864,560 バイト
tkinter を使ったスクリプトを Nuitka でexe化した際は 1000ファイル以上のファイル群になっていたので、それと比べたら圧倒的に展開時間は短くなってくれたはず。
しかしそれでも、プレビュー画面モードは表示が遅い…。設定画面モードなら瞬時に表示されているのだから、プレビュー画面モードの動作だけがおかしい…。
◎ ありえる可能性 :
Google Gemini や Microsoft Copilot にこのあたりを尋ねてみたけれど…。
AI君曰く。Windowsの「スクリーンセーバーの変更」ウインドウは、ウインドウハンドルを渡したら即座に描画が始まることを前提としていて、もしちょっとでも描画の開始が遅くなったら「ははあ。このプラグラム、さては固まってやがるな?」という扱いになって、タイムアウトするまで描画されない状態になる、という可能性を提示してきた。
いやまあ、AI君の言うことだから嘘八百かもしれないのだけど…。情報ソースを出してくれと言ってもさっぱり出してくれなかったし…。
でも、たしかにその可能性もあるかもなと…。Nuitka で1ファイル化したexeファイルはどうしてもファイル群の展開時間がかかってしまう。その間に、「固まってやがるな? じゃあ、お前の描画は後回しな」という扱いになっていてもおかしくはない…。
AI君は「--onefile を使わずに --standalone で試せ。起動が速くなるから状況が変わるかもよ?」と言ってきたけど、その場合 .exe (.scr) と同階層に大量(?)の .dll や .pyd も配置しないといけない。C:\Windows\System32\ の中にそんなもん入れられんわい。
Nuitka に、.exe以外のファイル群を別フォルダにまとめるオプションがあればいいのだけど、そんなものは無さそうで…。
AI君曰く。Windowsの「スクリーンセーバーの変更」ウインドウは、ウインドウハンドルを渡したら即座に描画が始まることを前提としていて、もしちょっとでも描画の開始が遅くなったら「ははあ。このプラグラム、さては固まってやがるな?」という扱いになって、タイムアウトするまで描画されない状態になる、という可能性を提示してきた。
いやまあ、AI君の言うことだから嘘八百かもしれないのだけど…。情報ソースを出してくれと言ってもさっぱり出してくれなかったし…。
でも、たしかにその可能性もあるかもなと…。Nuitka で1ファイル化したexeファイルはどうしてもファイル群の展開時間がかかってしまう。その間に、「固まってやがるな? じゃあ、お前の描画は後回しな」という扱いになっていてもおかしくはない…。
AI君は「--onefile を使わずに --standalone で試せ。起動が速くなるから状況が変わるかもよ?」と言ってきたけど、その場合 .exe (.scr) と同階層に大量(?)の .dll や .pyd も配置しないといけない。C:\Windows\System32\ の中にそんなもん入れられんわい。
Nuitka に、.exe以外のファイル群を別フォルダにまとめるオプションがあればいいのだけど、そんなものは無さそうで…。
◎ 改善策 :
試しに、「/p HWND」が渡された時は何もせずに即座にプロセスを終了する処理にしてみた。以下のような結果になった。
「スクリーンセーバーの変更」にすぐさま主導権?が返ってきて、プレビュー画面は真っ黒になっている。この真っ黒は、おそらくプレビュー画面のウインドウの背景色だと思われる。呼ばれたスクリーンセーバがプレビュー画面モードで何もしない時は、プレビュー画面ウインドウを背景色で一応クリアしてくれるのだろう…。
これで、何の処理もしなければ、少なくとも数秒待たされる状態は回避できると分かった。つまり、PythonでWindows用スクリーンセーバを作る際は、以下のどちらかを選ぶことになるのかなと…。
まあ、そんな感じっぽいということで…。勘弁してほしい。
「スクリーンセーバーの変更」にすぐさま主導権?が返ってきて、プレビュー画面は真っ黒になっている。この真っ黒は、おそらくプレビュー画面のウインドウの背景色だと思われる。呼ばれたスクリーンセーバがプレビュー画面モードで何もしない時は、プレビュー画面ウインドウを背景色で一応クリアしてくれるのだろう…。
これで、何の処理もしなければ、少なくとも数秒待たされる状態は回避できると分かった。つまり、PythonでWindows用スクリーンセーバを作る際は、以下のどちらかを選ぶことになるのかなと…。
- プレビュー画面モードは何もしないですぐに終了させる。真っ黒画面が表示されてしまうけど、その代わりすぐに操作できるようになるので…。まあ、勘弁してほしい。
- 一応プレビュー画面モードで何か描画する。数秒待たされてしまうけれど、その代わりそれっぽいプレビュー画面が表示されるので…。まあ、勘弁してほしい。
まあ、そんな感じっぽいということで…。勘弁してほしい。
◎ 改善策その2 :
プレビュー画面モードの動作だけが問題なのだから、そこだけラッパーを作って任せてしまう手もあるかもしれない。自分も以前そういうのを書いた。
_mieki256/scrsavwr: Screensaver wrapper on Windows
このラッパー、「フルスクリーン表示をするプログラムさえ作ればなんでもスクリーンセーバにできる」と書いてあるけど、ちょっと記述が抜けてるな…。正確には…。
そういうプログラムさえ作ればなんでもスクリーンセーバにできるはず。
それはそうと、このラッパーはHSP(というプログラミング言語)で作ったので、ウイルスとして誤判定されがちで…。誰かもっとちゃんとしたヤツを C/C++/C# あたりで書いてくれないものかなあ、と…。せっかくだから、一々 .iniファイルを編集したりせず、設定ダイアログ上で呼び出すプログラムや画像等を指定できたら楽になるかもしれない…。
_mieki256/scrsavwr: Screensaver wrapper on Windows
このラッパー、「フルスクリーン表示をするプログラムさえ作ればなんでもスクリーンセーバにできる」と書いてあるけど、ちょっと記述が抜けてるな…。正確には…。
- 多重起動を禁止する処理が入っていて、
- フルスクリーン表示をして、
- キーボードやマウスの操作で終了する、
そういうプログラムさえ作ればなんでもスクリーンセーバにできるはず。
それはそうと、このラッパーはHSP(というプログラミング言語)で作ったので、ウイルスとして誤判定されがちで…。誰かもっとちゃんとしたヤツを C/C++/C# あたりで書いてくれないものかなあ、と…。せっかくだから、一々 .iniファイルを編集したりせず、設定ダイアログ上で呼び出すプログラムや画像等を指定できたら楽になるかもしれない…。
◎ 余談。テスト用のスクリプト :
前述のスクリプト、pywinsc0.py は、「python pywinsc0.py /p HWND」という形で呼ばないと動作確認できない。でも、ウインドウハンドル(HWND)なんてどうやって入手したらええんや…。
そこで、ウインドウを新規に作成して、そのウインドウハンドルを渡すテストスクリプトも書いて実験していた。一応載せておく。
_test_preview.py
実行の仕方は以下。ウインドウを作成したら、そのウインドウハンドルを pywinsc0.py に渡して呼び出してくれる。
このスクリプトを使って動作確認する分には、期待通りに瞬時に青い塗り潰し画面が出てくるのだよなあ…。しかし、「スクリーンセーバーの変更」から起動させると期待した結果にならない…。どういう制限があるんだろう…。
そこで、ウインドウを新規に作成して、そのウインドウハンドルを渡すテストスクリプトも書いて実験していた。一応載せておく。
_test_preview.py
"""
pywin32でウインドウを作成して、
ウインドウハンドル(HWND)を子スクリプトに渡す。
Windows11 x64 25H2 + Python 3.10.10 64bit
"""
import win32gui
import win32con
import win32api
import subprocess
import sys
import os
# 起動対象となる子プロセスのスクリプト名
CHILD_PY = "pywinsc0.py"
# CHILD_PY = "pywinsc0.exe"
WINDOW_CLASSNAME = "MyPythonWindowClassSample"
def wnd_proc(hwnd, msg, wparam, lparam):
if msg == win32con.WM_DESTROY:
print("[Parent] WM_DESTROY 受信。Quit を送信。")
win32gui.PostQuitMessage(0)
return 0
return win32gui.DefWindowProc(hwnd, msg, wparam, lparam)
def create_window_and_launch_child():
window_class_name = WINDOW_CLASSNAME
wc = win32gui.WNDCLASS()
wc.lpfnWndProc = wnd_proc
wc.lpszClassName = window_class_name
wc.hInstance = win32api.GetModuleHandle(None)
# 背景色を設定
bgcolor = win32gui.CreateSolidBrush(win32api.RGB(0, 128, 0))
wc.hbrBackground = bgcolor
try:
win32gui.RegisterClass(wc)
except win32gui.error as e:
# 同じクラス名が登録済み(Error code = 1410)は無視
if e.winerror != 1410:
raise
hwnd = win32gui.CreateWindowEx(
0,
window_class_name,
"Parent Window",
win32con.WS_OVERLAPPEDWINDOW, # 一般的なウィンドウ形式(最小化・最大化・枠あり)
win32con.CW_USEDEFAULT, # 表示位置 X(OSにお任せ)
win32con.CW_USEDEFAULT, # 表示位置 Y(OSにお任せ)
640, # ウィンドウ幅
480, # ウィンドウ高さ
0, # 親ウィンドウのハンドル(今回は自身が親なので0)
0, # メニューハンドル
wc.hInstance, # インスタンスハンドル
None, # 追加の作成パラメータ
)
if not hwnd:
print("[Parent] Error: ウィンドウ作成に失敗。")
return
# 作成したウィンドウを表示状態にする
win32gui.ShowWindow(hwnd, win32con.SW_SHOW)
print(f"[Parent] ウインドウ生成。HWND: {hwnd}")
# 子プロセス起動
try:
# 引数として「/p HWND」を渡す
print(f"[Parent] {CHILD_PY} を起動...")
_, ext = os.path.splitext(CHILD_PY)
if ext == ".py" or ext == ".pyw":
# 起動中のPython実行環境 (sys.executable) を使って子スクリプトを実行
subprocess.Popen([sys.executable, CHILD_PY, "/p", str(hwnd)])
else:
# .exe等を実行
subprocess.Popen([CHILD_PY, "/p", str(hwnd)])
except Exception as e:
print(f"[Parent] 子プロセス起動に失敗: {e}")
# 初回描画を強制的に促す(メッセージキューにWM_PAINTを入れる)
win32gui.InvalidateRect(hwnd, None, True)
# キューを待たずに、今すぐ描画を反映
win32gui.UpdateWindow(hwnd)
# メッセージループ開始
# これを実行しないとウィンドウが「応答なし」になり、すぐにプログラムが終了してしまう。
# Windowsから送られてくる「再描画」「移動」等のメッセージを常に待ち受け wnd_procに振り分ける。
print("[Parent] メッセージループ開始。")
win32gui.PumpMessages()
print("[Parent] プロセス終了。")
def main():
create_window_and_launch_child()
if __name__ == "__main__":
main()
実行の仕方は以下。ウインドウを作成したら、そのウインドウハンドルを pywinsc0.py に渡して呼び出してくれる。
python test_preview.py
このスクリプトを使って動作確認する分には、期待通りに瞬時に青い塗り潰し画面が出てくるのだよなあ…。しかし、「スクリーンセーバーの変更」から起動させると期待した結果にならない…。どういう制限があるんだろう…。
[ ツッコむ ]
以上、1 日分です。