mieki256's diary



2025/12/26(金) [n年前の日記]

#1 [python] Pythonスクリプト内に画像を含めたい

Pythonスクリプト内にpng画像を含めておいて、Pillowで読み込んで画像処理をしたい。そんなことできるのかな…?

png画像をbase64に変換してしまえば可能。と、Microsoft Copilot が言っている。Windows11 x64 25H2 + Python 3.10.10 で試してみた。

_01_embed_image_file.py
"""
画像ファイルをPythonスクリプト内にbase64の形で含めておいて、
Pillow で読み込んで処理をするスクリプト。
"""

import base64
from io import BytesIO
from PIL import Image

# --- PNG画像(64x64, RGB)を base64 で埋め込む ---
EMBEDDED_PNG_BASE64 = (
    "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"
    "RU5ErkJggg=="
)


def main():
    binary = base64.b64decode(EMBEDDED_PNG_BASE64)
    im = Image.open(BytesIO(binary))
    print(f"Loaded image: size={im.size} mode={im.mode}")

    newim = im.convert("RGBA").point(lambda p: p * 2.0)

    newim.save("output.png")
    print("Saved processed image as output.png")


if __name__ == "__main__":
    main()

im.convert("RGBA").point(lambda p: p * 2.0) で、画像の明度を上げている。

元画像は以下。

input.png

スクリプト実行後の出力画像は以下。

output.png

たしかに、base64文字列を Pillow で画像として読み込んで、画像処理ができている。

base64への変換 :

png画像をbase64に変換する方法で少し悩んだ。最初は Python スクリプトで書いたけど…。

_png2base64.py
"""
Convert png to base64

Usage : python png2base64.py

Windows11 x64 25H2 + Python 3.10.10 64bit
"""

import base64

FILENAME = "input.png"


def image_to_base64(file_path):
    try:
        with open(file_path, "rb") as image_file:
            encoded_string = base64.b64encode(image_file.read()).decode("utf-8")
            return encoded_string
    except FileNotFoundError:
        return f"Error: Not found {file_path}"


if __name__ == "__main__":
    result = image_to_base64(FILENAME)
    print(result)


ググってみたら、Windows環境の場合、certutil というプログラムで base64 に変換できるらしい。
certutil -f -encode input.png base64.txt

_WindowsでBase64エンコード/デコードする方法 コマンドプロンプト - Qiita


DevToys でも base64 に変換できることに気づいた…。DevToys もインストール済みだった…。

_DevToys - A Swiss Army knife for developers
_エンジニアのための十徳ナイフ「DevToys」がバージョン2になってクロスプラットフォームやCLI対応しさらに便利すぎる ツール - Qiita

CLI版の devtoys.cli.exe なら、以下で base64 にできた。
devtoys.cli.exe base64img -i input.png

ヘルプは以下で表示できる。
devtoys.cli --help
devtoys.cli base64img --help

#2 [xyzzy] xyzzy上で1行を任意の文字数で分割したい

base64の文字列はずらずらと長いので、一定の文字数で複数行に分割したい。テキストエディタ xyzzy でそういうことはできないのかな? できたような気がする。たぶん。

_Tips - xyzzy - 編集機能 - 詰め込み機能
_xyzzy の音 - 編集

fillコマンドとやらを使えばできるらしい。fill-paragraph に M-q が割り当てられてるから、分割したい行にカーソルを置いて M-q を押すだけで分割できた。

桁数を変更 :

1行が72文字になるな…。もっと1行の文字数を減らしたい…。

折り返したい位置までカーソルを持っていって、set-fill-column (C-x f) を実行すれば、文字数、というか桁数を指定できるらしい。

あるいは ESC 桁数 C-x f と打つらしい。

余談。Obsidian上で検索したら、昔、このあたりをメモしてあった…。すっかり忘れてた…。

#3 [python] Pythonスクリプトからウインドウハンドルを使ってアレコレしたい

Windows11 x64 25H2 + Python 3.10.10 で、Pythonスクリプトに与えられたウインドウハンドル(HWND)に対して描画処理ができるのかどうかが気になった。

ウインドウハンドル(HWND)というのは、Windowsのデスクトップに表示されているウインドウを識別する管理ID、みたいなものと思っておけばいいのだろうか。ウインドウハンドルが分かれば、そのウインドウに対してアレコレできる。らしい。たぶん。

pywin32 というモジュールを使えば、そのあたり比較的楽に実現できるらしい。おそらく ctypes を使ってもできるのだろうけど、どうせなら簡単なほうがいいよな…。

_pywin32 - PyPI

pywin32 のインストールは以下。
python -m pip install pywin32

pywin32 311 がインストールされた。

一般的には以下を import して使うらしい。
import win32gui  # Windowsのウィンドウ操作(作成、描画開始など)に使用
import win32con  # Windows APIの定数(WM_PAINT, WS_CHILDなど)を定義
import win32api  # Windowsの基本機能(インスタンス取得、サイズ取得など)に使用

ウインドウを新規作成する :

本来は何かしらのプログラムからウインドウハンドルをPythonスクリプトに渡して処理したいけれど…。まずはPythonでウインドウを新規作成して、そのウインドウのウインドウハンドルを取得できるのか確認してみたい。

Google Geminiにお願いしてスクリプトを作ってもらった。一発では動かなくて何度かやり取りしたけれど…。せっかくだからコメントもマシマシにしてもらった。

import のあたりに pywin32 _01_createwindow.py
"""
pywin32でウインドウを新規作成してウインドウハンドル(HWND)を取得。
描画は何もしない。ウインドウの生成のみを行う。

Windows11 x64 25H2 + Python 3.10.10 64bit
"""

import win32gui
import win32con
import win32api


def wnd_proc(hwnd, msg, wparam, lparam):
    """
    ウィンドウプロシージャ。ウィンドウに送られてくるイベント(メッセージ)を処理する関数
    """
    # ウィンドウが破棄される時の処理。閉じるボタンが押された等。
    if msg == win32con.WM_DESTROY:
        print("WM_DESTROY 受信。メッセージループ(PumpMessages)終了信号を送る。")
        win32gui.PostQuitMessage(0)
        return 0

    # 自分で処理しないメッセージは、OS提供のデフォルト処理に任せる
    return win32gui.DefWindowProc(hwnd, msg, wparam, lparam)


def create_window():
    # --- 1. ウィンドウクラスの定義 ---
    window_class_name = "MyPythonWindowClass"  # ウィンドウの「型」となるクラス名

    wc = win32gui.WNDCLASS()  # ウィンドウの属性を設定する構造体
    wc.lpfnWndProc = wnd_proc  # メッセージ処理関数(コールバック)を登録
    wc.lpszClassName = window_class_name  # クラス名

    # このウィンドウを実行しているアプリケーションのインスタンスハンドルを取得
    wc.hInstance = win32api.GetModuleHandle(None)

    # 背景色を設定
    wc.hbrBackground = win32gui.GetStockObject(win32con.WHITE_BRUSH)

    # マウスカーソルを標準矢印に設定
    wc.hCursor = win32gui.LoadCursor(0, win32con.IDC_ARROW)

    # --- 2. ウィンドウクラスをOSに登録 ---
    try:
        # OSに「この設定のウィンドウを使います」と宣言
        win32gui.RegisterClass(wc)
    except win32gui.error as e:
        # すでに登録済みの場合はエラー(1410)が出るため、それ以外の場合のみ例外を出す
        # http://ir9.jp/prog/ayu/win32err.htm
        if e.winerror != 1410:
            raise

    # --- 3. ウィンドウの生成 (HWNDの取得) ---
    # 成功すると、ウィンドウを操作するための「ハンドル(HWND)」が返却される
    hwnd = win32gui.CreateWindow(
        window_class_name,  # 使用するクラス名
        "Python Win32 Window Sample",  # ウインドウタイトル
        # ウィンドウのスタイル(最小化・最大化ボタン等あり)
        win32con.WS_OVERLAPPEDWINDOW | win32con.WS_VISIBLE,
        win32con.CW_USEDEFAULT,  # 出現位置 X座標 (OSにお任せ)
        win32con.CW_USEDEFAULT,  # 出現位置 Y座標 (OSにお任せ)
        512,  # ウィンドウの幅 (ピクセル)
        288,  # ウィンドウの高さ (ピクセル)
        0,  # 親ウィンドウのHWND (なし)
        0,  # メニューのハンドル (なし)
        wc.hInstance,  # インスタンスハンドル
        None,  # 予備のデータ
    )

    if not hwnd:
        print("Error : CreateWindow() は失敗")
        return None

    # --- 4. ウィンドウの可視化 ---
    # 作成直後のウィンドウは非表示なので明示的に表示を命令
    print(f"CreateWindow() は成功。HWND : {hwnd}")
    win32gui.ShowWindow(hwnd, win32con.SW_SHOW)

    # ウィンドウの描画を即座に更新する
    win32gui.UpdateWindow(hwnd)

    return hwnd


def main():
    # ウィンドウの作成とHWNDの取得
    main_hwnd = create_window()

    if main_hwnd:
        # --- 5. メッセージループ ---
        # Windowsから送られてくる「マウスが動いた」「キーが押された」などの
        # メッセージを絶えず受け取り、wnd_procに振り分ける無限ループ
        print("メッセージループ開始。")
        win32gui.PumpMessages()

        print("Pythonプロセスを終了。")


if __name__ == "__main__":
    main()

実行すると以下のようなウインドウが表示された。

01_createwindow_ss01.png

単にウインドウを作っただけで、まだ何も描画してないけれど、Python + pywin32 を使ってウインドウを新規作成できること、ウインドウハンドルを取得できることは分かった。

ウインドウに文字を描画する :

ウインドウ上に何かを描画してみる。とりあえず文字列というかテキストを描画してみよう…。

_02_drawtext2window.py
"""
pywin32でウインドウを新規作成してウインドウハンドル(HWND)を取得。
そのHWNDに対して、背景色は黒、文字色は白で文字列を描画。

Windows11 x64 25H2 + Python 3.10.10 64bit
"""

import win32gui
import win32con
import win32api

FONT_NAME = "Meiryo"
FONT_SIZE = 32


def on_paint(hwnd):
    """文字列をウインドウに描画"""

    # 描画開始
    hdc, ps = win32gui.BeginPaint(hwnd)

    # クライアント領域のサイズを取得。(left, top, right, bottom)
    rect = win32gui.GetClientRect(hwnd)

    # 背景を黒で塗りつぶし
    hbrush = win32gui.CreateSolidBrush(win32api.RGB(0, 0, 0))
    win32gui.FillRect(hdc, rect, hbrush)

    # 作成したGDIオブジェクトは削除していかないとメモリを圧迫(リソースリーク)する。
    win32gui.DeleteObject(hbrush)

    # フォントの設定。LOGFONT構造体(辞書形式)で詳細を指定
    lf = win32gui.LOGFONT()

    # フォント名、サイズ(高さ)、ウェイト、アンチエイリアスを設定
    lf.lfFaceName = FONT_NAME
    lf.lfHeight = FONT_SIZE
    lf.lfWeight = win32con.FW_BOLD
    lf.lfQuality = win32con.ANTIALIASED_QUALITY

    # フォントオブジェクトの作成
    hfont = win32gui.CreateFontIndirect(lf)

    # 作成したフォントオブジェクトをHDCに選択(戻り値は以前のフォント)
    hfont_old = win32gui.SelectObject(hdc, hfont)

    # 文字色指定
    win32gui.SetTextColor(hdc, win32api.RGB(255, 255, 255))
    win32gui.SetBkMode(hdc, win32con.TRANSPARENT)

    # 文字描画時の処理を指定、DT_WORDBREAK を指定すると改行も有効になる
    draw_style = win32con.DT_CENTER | win32con.DT_VCENTER | win32con.DT_WORDBREAK

    # 文字描画
    lines = [
        "Hello !",
        "Windows 11のフォント描画です。",
        f"Font : {FONT_NAME}",
        f"Size : {FONT_SIZE}",
    ]
    txt = "\n".join(lines)
    win32gui.DrawText(hdc, txt, -1, rect, draw_style)

    # 後片付け。元のフォントに戻して、作成したフォントを削除(メモリリーク防止)
    win32gui.SelectObject(hdc, hfont_old)
    win32gui.DeleteObject(hfont)

    # 描画終了
    win32gui.EndPaint(hwnd, ps)


def wnd_proc(hwnd, msg, wparam, lparam):
    """ウインドウプロシージャ。イベントメッセージ処理関数"""

    if msg == win32con.WM_PAINT:
        # 描画開始
        on_paint(hwnd)
        return 0

    if msg == win32con.WM_DESTROY:
        # ウィンドウ破棄時の処理(閉じるボタンが押された等)
        print("WM_DESTROY を受け取ったので Quit メッセージを送る。")
        win32gui.PostQuitMessage(0)
        return 0

    # 自分で処理しないメッセージは、OS提供のデフォルト処理に任せる
    return win32gui.DefWindowProc(hwnd, msg, wparam, lparam)


def create_window_with_custom_font():
    """ウインドウを新規作成"""

    window_class_name = "PythonCustomFontWindow"

    # ウインドウクラスの登録
    wc = win32gui.WNDCLASS()
    wc.lpszClassName = window_class_name
    wc.lpfnWndProc = wnd_proc
    wc.hInstance = win32api.GetModuleHandle(None)
    wc.hCursor = win32gui.LoadCursor(0, win32con.IDC_ARROW)

    # 背景色を指定
    wc.hbrBackground = win32gui.GetStockObject(win32con.BLACK_BRUSH)
    # wc.hbrBackground = win32gui.CreateSolidBrush(win32api.RGB(0, 0, 0))

    # ウインドウリサイズ時に再描画するように指定
    wc.style = win32con.CS_HREDRAW | win32con.CS_VREDRAW

    # ウインドウクラスをOSに登録
    try:
        win32gui.RegisterClass(wc)
    except win32gui.error as e:
        print(f"Error: {e}")
        if e.winerror != 1410:
            # 登録済みの場合は 1410 が返ってくるのでそれ以外はエラーとする
            # http://ir9.jp/prog/ayu/win32err.htm
            raise

    # ウインドウ新規作成。ウインドウハンドル(HWND)が返ってくる
    # CreateWindowEx() と CreateWindow() の違いは拡張スタイルを使えるかどうか
    hwnd = win32gui.CreateWindowEx(
        0,  # 拡張スタイル
        window_class_name,  # 使用クラス名
        "Font Selection Sample",  # ウインドウタイトル
        win32con.WS_OVERLAPPEDWINDOW | win32con.WS_VISIBLE,  # ウインドウスタイル
        win32con.CW_USEDEFAULT,  # 表示位置 x (OSにまかせる)
        win32con.CW_USEDEFAULT,  # 表示位置 y (OSにまかせる)
        512,  # ウインドウ幅
        288,  # ウインドウ高さ
        0,  # 親ウインドウ HWND
        0,  # メニューハンドル
        wc.hInstance,  # インスタンス
        None,  # 追加パラメータ
    )

    if not hwnd:
        print("Error: ウインドウの作成に失敗")
        return None

    print(f"ウインドウを作成。 HWND: {hwnd}")
    return hwnd


def main():
    hwnd = create_window_with_custom_font()

    if hwnd:
        # ウインドウを強制的に最前面に表示し、更新を促す
        # 作成直後のウィンドウは非表示なので、明示的に表示を命令する
        win32gui.ShowWindow(hwnd, win32con.SW_SHOW)

        # クライアント領域全体を無効に設定し、WM_PAINTを誘発させる。
        # これを呼ばないとウインドウ生成直後の描画処理が行われない時がある。
        # 第2引数をNoneにすると全体、第3引数をTrueにすると背景消去も行う
        win32gui.InvalidateRect(hwnd, None, True)

        # メッセージキューを待たずにウインドウの描画を即座に更新
        win32gui.UpdateWindow(hwnd)

        # メッセージループ
        # Windowsから送られてくる「マウスが動いた」「キーが押された」などの
        # メッセージを絶えず受け取って wnd_proc に振り分ける無限ループ
        print("メッセージループ開始")
        win32gui.PumpMessages()

        print("プロセス終了。")


if __name__ == "__main__":
    main()

02_drawtext2window_ss01.png

一応テキスト描画もできた。

ちょっとハマったのは、文字が描画できる時とできない時があって…。win32gui.InvalidateRect(hwnd, None, True) と win32gui.UpdateWindow(hwnd) を呼びことで、必ず描画される状態になった。

子スクリプトを呼び出して文字を描画する :

Python + pywin32 からウインドウを生成して、ウインドウハンドルを取得することが出来ると分かった。

次は、得られたウインドウハンドルを別のPythonスクリプトに渡して、その別スクリプト側で描画処理をしてみたい。

ググっていたら、こういった処理をしたい場合、親ウインドウに対して子ウインドウを新規作成して、その子ウインドウの中に何かしらを描画していく処理が一般的、という話を見かけたのでその方向で試してみる。

まずは親ウインドウを担当するスクリプトを作成。

ウインドウを作成してウインドウハンドルが得られたら、subprocess.Popen() で、コマンドラインオプションをつけながら別スクリプトを呼び出す。subprocess.Popen() を使うと、親スクリプトとは別プロセスで何かしらを起動できるらしい。つまり、親スクリプトと子スクリプトが並列で動作している状態になる。

_04_parent_window.py
"""
pywin32でウインドウを作成。
ウインドウハンドル(HWND)を子スクリプトに渡す。

Windows11 x64 25H2 + Python 3.10.10 64bit
"""

import win32gui
import win32con
import win32api
import subprocess
import sys

# 起動対象となる子プロセスのスクリプト名
# CHILD_PY = "child_drawtext.py"
CHILD_PY = "child_drawimage.py"


def wnd_proc(hwnd, msg, wparam, lparam):
    """ウインドウプロシージャ。イベントメッセージ処理関数"""

    if msg == win32con.WM_DESTROY:
        # ウィンドウ破棄時のメッセージ(WM_DESTROY)
        # メッセージループ(PumpMessages)を終了させる信号を送る
        print("[Parent] WM_DESTROY 受信。Quit を送信。")
        win32gui.PostQuitMessage(0)
        return 0

    # 自分で処理しないメッセージは、Windowsのデフォルト処理に任せる
    return win32gui.DefWindowProc(hwnd, msg, wparam, lparam)


def create_window_and_launch_child():
    # --- 1. ウィンドウクラスの定義と登録 ---
    window_class_name = "MyPythonWindowClass"

    # ウィンドウの「型」となるクラス構造体を作成
    wc = win32gui.WNDCLASS()
    wc.lpfnWndProc = wnd_proc  # 上で定義したメッセージ処理関数を紐付け
    wc.lpszClassName = window_class_name  # クラス名(ウィンドウを識別する名前)
    # 現在実行中のインスタンスハンドルを取得
    wc.hInstance = win32api.GetModuleHandle(None)
    wc.hbrBackground = win32gui.GetStockObject(win32con.WHITE_BRUSH)  # 背景色を白に設定

    try:
        # 定義したウィンドウクラスをOSに登録
        win32gui.RegisterClass(wc)
    except win32gui.error as e:
        # 同じクラス名が登録済み(Error code = 1410)は無視
        if e.winerror != 1410:
            raise

    # --- 2. ウィンドウの実体を作成 ---
    hwnd = win32gui.CreateWindow(
        window_class_name,  # 使用する登録済みクラス名
        "Parent Window",  # ウィンドウのタイトルバーに表示される文字列
        win32con.WS_OVERLAPPEDWINDOW,  # 一般的なウィンドウ形式(最小化・最大化・枠あり)
        win32con.CW_USEDEFAULT,  # 表示位置 X(OSにお任せ)
        win32con.CW_USEDEFAULT,  # 表示位置 Y(OSにお任せ)
        512,  # ウィンドウの幅
        288,  # ウィンドウの高さ
        0,  # 親ウィンドウのハンドル(今回は自身が親なので0)
        0,  # メニューハンドル
        wc.hInstance,  # インスタンスハンドル
        None,  # 追加の作成パラメータ
    )

    if not hwnd:
        print("[Parent] Error: ウィンドウ作成に失敗。")
        return

    # 作成したウィンドウを表示状態にする
    win32gui.ShowWindow(hwnd, win32con.SW_SHOW)

    # HWND (Window Handle) は、Windows内でのこのウィンドウの固有の背番号のようなもの
    print(f"[Parent] ウインドウ生成。HWND: {hwnd}")

    # --- 3. 子プロセス起動 ---
    try:
        # 起動中のPython実行環境 (sys.executable) を使って子スクリプトを実行
        # 引数として「/p HWND」を渡す
        print(f"[Parent] {CHILD_PY} を起動...")
        subprocess.Popen([sys.executable, CHILD_PY, "/p", str(hwnd)])
    except Exception as e:
        print(f"子プロセス起動に失敗: {e}")

    # --- 4. メッセージループの開始 ---
    # これを実行しないとウィンドウが「応答なし」になり、すぐにプログラムが終了してしまう。
    # Windowsから送られてくる「再描画」「移動」等のメッセージを常に待ち受け wnd_procに振り分ける。
    print("[Parent] メッセージループ開始。")
    win32gui.PumpMessages()

    print("[Parent] プロセス終了。")


if __name__ == "__main__":
    create_window_and_launch_child()


続いて、子ウインドウを担当するスクリプト。ひとまず文字描画をしてみる。

_child_drawtext.py
"""
与えられたウインドウハンドル(HWND)を親として、
子ウインドウを作成して文字を描画

Windows11 x64 25H2 + Python 3.10.10 64bit
"""

import sys
import time
import win32gui  # WindowsのGUI操作用
import win32con  # Windowsの定数(WM_xxxなど)用
import win32api  # Windowsの基本API(RGB, GetModuleHandleなど)用


class ChildOverlay:
    def __init__(self, parent_hwnd):
        """初期化"""
        self.parent_hwnd = parent_hwnd  # 描画対象となる親のウィンドウハンドル
        self.hwnd = None  # 自身(子)のウィンドウハンドル

        self.class_name = "PersistentOverlayClass"  # ウィンドウクラス名
        self.timer_id = 1  # 親ウィンドウ監視用タイマーの識別ID

        self.font_name = "Verdana"  # フォント名
        self.font_height = 32  # フォントサイズ
        self.font_weight = win32con.FW_NORMAL

        # 表示文字列
        lines = [
            "Overlay Activate",
            f"Font name: {self.font_name}",
            f"Font size: {self.font_height}",
        ]
        self.text_content = "\n".join(lines)

    def create_window(self):
        """ウインドウ生成"""

        # 親ウィンドウが現在も存在するか確認
        if not win32gui.IsWindow(self.parent_hwnd):
            print(f"Error: Parent HWND {self.parent_hwnd} is not valid.")
            return False

        # 親ウィンドウのクライアント領域(枠の内側)のサイズを取得
        left, top, right, bottom = win32gui.GetClientRect(self.parent_hwnd)
        width = right - left
        height = bottom - top

        # ウインドウクラスを設定
        wc = win32gui.WNDCLASS()  # ウィンドウクラス構造体
        wc.lpfnWndProc = self.wnd_proc  # イベント(メッセージ)処理関数を指定
        wc.lpszClassName = self.class_name  # クラス名
        # 実行ファイルモジュールハンドル取得
        wc.hInstance = win32api.GetModuleHandle(None)
        wc.hbrBackground = win32gui.GetStockObject(win32con.BLACK_BRUSH)  # 背景色指定
        wc.hCursor = win32gui.LoadCursor(0, win32con.IDC_ARROW)  # マウスカーソル指定

        try:
            win32gui.RegisterClass(wc)  # システムに登録
        except win32gui.error as e:
            # 既に同じ名前のクラスが登録されている場合はエラー(1410)が出るが、その場合は続行
            if e.winerror != 1410:
                raise

        # ウィンドウの作成
        self.hwnd = win32gui.CreateWindow(
            self.class_name,  # 登録したクラス名
            "Child Window",  # ウインドウタイトル
            win32con.WS_CHILD | win32con.WS_VISIBLE,  # 子ウィンドウとして作成、即表示
            0,  # x
            0,  # y
            width,  # width
            height,  # height
            self.parent_hwnd,  # 親ウィンドウのハンドルを指定
            0,  # メニューハンドル(なし)
            win32api.GetModuleHandle(None),  # インスタンスハンドル
            None,  # 追加パラメータ(なし)
        )

        if self.hwnd:
            print(f"[Child] 子ウインドウ生成。 HWND: {self.hwnd}")
            win32gui.InvalidateRect(self.hwnd, None, True)  # 初回描画を強制
            win32gui.UpdateWindow(self.hwnd)  # 強制的に再描画を発生
            return True

        print("[Child] 子ウインドウ生成に失敗。")
        return False

    def wnd_proc(self, hwnd, msg, wparam, lparam):
        """イベントメッセージ処理関数"""
        if msg == win32con.WM_CREATE:
            # ウィンドウが作られた直後にタイマー(500ミリ秒間隔)をセット
            win32gui.SetTimer(hwnd, self.timer_id, 500, None)
            return 0

        if msg == win32con.WM_TIMER:
            # 指定したタイマーIDの通知が来た場合
            if wparam == self.timer_id:
                # 親ウィンドウが閉じられていないかチェック
                if not win32gui.IsWindow(self.parent_hwnd):
                    print("[Child] 親ウインドウ消失。終了します。")
                    win32gui.KillTimer(hwnd, self.timer_id)  # タイマー停止
                    win32gui.DestroyWindow(hwnd)  # 自分を破棄
            return 0

        if msg == win32con.WM_PAINT:
            # 描画が必要なタイミング(ウィンドウ表示時など)で呼ばれる
            self._on_paint(hwnd)
            return 0

        if msg == win32con.WM_DESTROY:
            # ウィンドウが破棄されたら、メッセージループを抜けるよう指示
            print("[Child] WM_DESTROY 受信。Quit 送信。")
            win32gui.PostQuitMessage(0)
            return 0

        if msg == win32con.WM_NCDESTROY:
            # ウィンドウが破棄されたら、メッセージループを抜けるよう指示
            print("[Child] WM_NCDESTROY 受信。Quit 送信。")
            win32gui.PostQuitMessage(0)
            return 0

        # 自分が処理しなかったメッセージはWindowsの標準処理に任せる
        return win32gui.DefWindowProc(hwnd, msg, wparam, lparam)

    def _on_paint(self, hwnd):
        """描画。GDIを使用"""
        hdc, ps = win32gui.BeginPaint(hwnd)  # 描画開始。デバイスコンテキスト(HDC)を取得
        rect = win32gui.GetClientRect(hwnd)  # 描画範囲(自分自身のサイズ)を取得

        # 背景塗りつぶし
        brush = win32gui.CreateSolidBrush(win32api.RGB(0, 0, 0))  # ブラシ作成
        win32gui.FillRect(hdc, rect, brush)  # 範囲内を塗りつぶす
        win32gui.DeleteObject(brush)  # ブラシを捨ててメモリ解放

        # フォントオブジェクトの作成
        lf = win32gui.LOGFONT()
        lf.lfFaceName = self.font_name  # 書体を指定
        lf.lfHeight = self.font_height  # 高さを指定
        lf.lfWeight = self.font_weight  # ウェイトを指定
        lf.lfQuality = win32con.ANTIALIASED_QUALITY  # アンチエイリアス

        hfont = win32gui.CreateFontIndirect(lf)  # フォントを作成

        # HDCにフォントを適用(古いのは保管)
        old_font = win32gui.SelectObject(hdc, hfont)

        # 文字色の設定と背景モード(文字の隙間を塗りつぶさない設定)
        win32gui.SetTextColor(hdc, win32api.RGB(255, 255, 255))  # 白色
        win32gui.SetBkMode(hdc, win32con.TRANSPARENT)  # 背景透過

        # テキストの描画実行 (中央揃え設定)
        draw_style = win32con.DT_CENTER | win32con.DT_VCENTER | win32con.DT_WORDBREAK
        win32gui.DrawText(
            hdc,
            self.text_content,
            -1,
            rect,
            draw_style,
        )

        # 後片付け (作成したリソースの解放)
        win32gui.SelectObject(hdc, old_font)  # フォントを元に戻す
        win32gui.DeleteObject(hfont)  # 作成したフォントを削除
        win32gui.EndPaint(hwnd, ps)  # 描画終了を報告

    def run(self):
        """メインループを実行"""
        if self.create_window():
            print("[Child] メッセージループ開始...")
            # Windowsからのメッセージを待ち受け、wnd_procに振り分ける無限ループ
            win32gui.PumpMessages()
            print("[Child] プロセス終了。")


def main():
    # 引数チェック: python script.py /p <HWND> が前提
    if len(sys.argv) == 3 and sys.argv[1].lower() == "/p":
        try:
            parent_hwnd = int(sys.argv[2])  # HWNDを数値に変換
        except ValueError:
            print("Error: Invalid HWND. 10進数を指定してください。")
            return
    else:
        print("Usage: python child_drawtext2.py /p <HWND>")
        return

    # 親ウィンドウが準備できるまで少し待機
    time.sleep(1.0)

    # オーバーレイオブジェクトを作成して実行
    overlay = ChildOverlay(parent_hwnd)
    overlay.run()


if __name__ == "__main__":
    main()


親スクリプトの 04_parent_window.py を実行すると、親ウインドウ生成後に child_drawtext.py が呼び出されて、子ウインドウを生成してその中に文字描画がされる。以下のような見た目になった。

child_drawtext_ss01.png

子スクリプトを呼び出して画像を描画する :

文字ではなくて、画像を描画してみたい。

親スクリプトは前述のものを再利用。呼び出す子スクリプト名だけ変更する。子スクリプトは以下。

_child_drawimage.py
"""
与えられたウインドウハンドル(HWND)を親として、
子ウインドウを作成して画像を描画。

Windows11 x64 25H2 + Python 3.10.10 64bit
"""

import sys
import win32gui  # Windowsのウィンドウ操作(作成、描画開始など)に使用
import win32con  # Windows APIの定数(WM_PAINT, WS_CHILDなど)を定義
import win32api  # Windowsの基本機能(インスタンス取得、サイズ取得など)に使用
import ctypes  # WindowsのDLL(システム設定)を直接呼び出すために使用
from PIL import Image, ImageWin  # 画像処理とWin32描画の橋渡しに使用

IMAGEFILE = "preview.png"

# --- Windows 11等の高解像度ディスプレイ(DPI)への対応設定 ---
try:
    # プロセスに対して「DPIを認識する」よう命令し、文字や画像のボケを防ぐ
    ctypes.windll.shcore.SetProcessDpiAwareness(1)
except Exception:
    # 古いOS環境(Windows 7等)でエラーにならないための回避策
    ctypes.windll.user32.SetProcessDPIAware()


class PreviewWindow:
    """親ウィンドウの中に画像を高品質に描画する子ウィンドウを管理するクラス"""

    def __init__(self, parent_hwnd, image_path):
        """初期化"""
        self.parent_hwnd = parent_hwnd  # 外部から渡された親ウィンドウのハンドルを保存
        self.resized_dib = None  # リサイズ後の描画データを保持

        # 描画範囲(left, top, right, bottom)を保持
        self.draw_rect = (0, 0, 0, 0)

        # 起動時の親ウィンドウのサイズに合わせて画像を高品質にリサイズする
        self._prepare_high_quality_image(image_path)

        # ウィンドウの見た目や振る舞いをOSに登録
        # 実際に子ウィンドウを作成し、ハンドルを取得
        self.class_name = "PythonStaticHighQualityChild"
        self.hwnd = self._create_window()

    def _prepare_high_quality_image(self, path):
        """Pillowで親ウィンドウのクライアント領域サイズに合わせた画像を作成"""
        try:
            # クライアント領域サイズを取得
            rect = win32gui.GetClientRect(self.parent_hwnd)
            target_w = rect[2] - rect[0]
            target_h = rect[3] - rect[1]

            if target_w <= 0 or target_h <= 0:
                print("Error: 親ウインドウサイズが取得できません。")
                sys.exit(1)

            # 画像をPillowで開いてRGB形式に変換
            img = Image.open(path).convert("RGB")

            # LANCZOSアルゴリズムでリサイズ
            resized_img = img.resize((target_w, target_h), Image.Resampling.LANCZOS)

            # リサイズ画像を Win32 API で直接描画できるDibオブジェクトに変換して保持
            self.resized_dib = ImageWin.Dib(resized_img)

            # 描画時に使用する矩形情報を保存
            self.draw_rect = (0, 0, target_w, target_h)

        except Exception as e:
            print(f"Error: {e}")
            sys.exit(1)

    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

        w = self.draw_rect[2]  # 準備した画像の幅
        h = self.draw_rect[3]  # 準備した画像の高さ

        # ウィンドウを作成してハンドルを返す
        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("[Child] Error: ウインドウ生成に失敗")
            return None

        print(f"[Child] ウインドウ生成。HWND: {hwnd}")
        return hwnd

    def wnd_proc(self, hwnd, msg, wparam, lparam):
        """メッセージ処理関数"""
        if msg == win32con.WM_PAINT:
            # ウィンドウの再描画が必要になったとき(初回表示時や、隠れていた窓が出た時)

            # 描画の準備。hdc(デバイスコンテキスト)を取得
            hdc, ps = win32gui.BeginPaint(hwnd)

            if self.resized_dib:
                # 事前にPillowで作成した高品質画像を画面(HDC)に転送
                self.resized_dib.draw(hdc, self.draw_rect)

            # 描画終了をOSに告げる
            win32gui.EndPaint(hwnd, ps)

            print("[Child] WM_PAINT 受信。")
            return 0  # メッセージを処理した場合は0を返すのが基本

        if msg == win32con.WM_DESTROY:
            # ウィンドウが破棄されるとき(親が閉じる時など)
            print("[Child] WM_DESTROY 受信。Quit を送信。")
            win32gui.PostQuitMessage(0)
            return 0

        # 自分で行わないメッセージ処理はOS既定の処理に渡す
        return win32gui.DefWindowProc(hwnd, msg, wparam, lparam)


def main():
    """プログラムのメイン実行部分"""
    # 引数が正しく渡されているか確認 (/p HWND)
    if len(sys.argv) == 3 and sys.argv[1].lower() == "/p":
        parent_hwnd = int(sys.argv[2])
    else:
        print("Usage: python script.py /p <HWND_DECIMAL>")
        return

    # 指定されたハンドルが、実際に動作しているウィンドウか確認
    if not win32gui.IsWindow(parent_hwnd):
        print("Error: 指定された親ウィンドウ(HWND)は存在しません。")
        return

    # プレビュー管理クラスを作成
    preview = PreviewWindow(parent_hwnd, IMAGEFILE)

    # 初回描画を強制的に促す(メッセージキューにWM_PAINTを入れる)
    win32gui.InvalidateRect(preview.hwnd, None, True)

    # キューを待たずに、今すぐ描画を反映
    win32gui.UpdateWindow(preview.hwnd)

    # メッセージループ: OSからの信号(WM_PAINT等)を待ち受け、wnd_procへ流し続ける
    # これがないと、スクリプトは一瞬で終了してしまいます。
    win32gui.PumpMessages()

    print("[Child] プロセス終了。")


if __name__ == "__main__":
    main()

表示に使う画像ファイルは以下。

preview.png
_preview.png

親スクリプトを実行すると、以下のような表示になった。画像の描画もできると分かった。

child_drawimage_ss01.png

ハマった点 :

動作確認をしていて、いくつかハマった。

最初、ウインドウ内への描画処理は1回だけ行って、それで表示できたと思い込んでいたのだけど…。ウインドウを最小化してからまた元に戻したらウインドウの中身が消えてしまった。

最小化してから元に戻したり、ウインドウをリサイズしたりすると、ウインドウの再描画が必要になるので、ウインドウプロシージャ(イベントメッセージを処理する関数)を用意して WM_PAINT が送られてきたら再描画するように作っておかないといけない模様。

また、親ウインドウに子ウインドウを作るあたりもハマった。親ウインドウを閉じると、子ウインドウも見た目では消えてくれるけれど、子スクリプトを動かしているPythonのプロセス自体は残ってしまう…。子スクリプト側でも WM_DESTROY が送られてきたら自身のプロセスを終了するように処理を書かないといけない。

ただ、WM_DESTROY なのか、WM_NCDESTROY なのか、どちらが送られてくれるのかよく分かってない…。とりあえず両方並べてみたけれど…。

また、場合によってはそれらのメッセージが送られてこない時もありそうで…。タイマーを使って一定時間毎に、親ウインドウが消えているかどうかをチェックして、親ウインドウが消えていたら自身も消滅させる処理も追加してみた。ただ、動作確認した感じでは、そのチェック処理で終了する場面は確認できてない。WM_DESTROY / WM_NCDESTROY で終了させるだけでも十分かもしれない。

以上、1 日分です。

過去ログ表示

Prev - 2025/12 -
1 2 3 4 5 6
7 8 9 10 11 12 13
14 15 16 17 18 19 20
21 22 23 24 25 26 27
28 29 30 31

カテゴリで表示

検索機能は Namazu for hns で提供されています。(詳細指定/ヘルプ


注意: 現在使用の日記自動生成システムは Version 2.19.6 です。
公開されている日記自動生成システムは Version 2.19.5 です。

Powered by hns-2.19.6, HyperNikkiSystem Project