Pythonでwindows通知(トースト・バルーン)を出す方法

Python Advent Calendar 2019の14日目の記事です。

速攻作れるだろ~と思っていたら、意外とめんどくさかった。
なにより日本語の標準モジュールだけで実現する解説記事が多分ない。(多分)

色々調べたので、忘れないうちに未来の自分と同じことをしたい人へ共有

作り方なんていいんだよ!ソースくれって人は下記のソースをぶっこんで使ってどうぞ。
GitHub-easyToast
ちなみにLicenceはMITLicenceです。
あ、スターください。

また、今回勉強しつつ調べてまとめているので、
もし間違っているところがあればツイッターなどで教えて下さい

今回は利用する標準モジュールのみで実装を行う。

■トーストとは?
古すぎないWindowsでなにかApplicationからのお知らせがあるときに
画面右下に出てくる通知のようなもの。
バルーンと呼ばれることもあるが、厳密にはこれはトーストである。
※バルーンは小アイコンからの吹き出し

■トーストを出すまでの流れ
1. Pythonでctypeを利用する
2. APIにあった構造体を用意する
3. WindowsAPIを利用できるように読み込む
4. LoadImageWで画像アイコンを利用できるようにする
5. Shell_NotifyIconWで通知申請を行う

なにがなんのことかわからないと思うので、順番に説明する。
サンプルコードが何度か登場するが、基本的には以下のimportを行っている前提で話をすすめる。

import os
import ctypes
from ctypes import Structure, windll, sizeof, POINTER, WINFUNCTYPE
from ctypes.wintypes import ( 
    DWORD, HICON, HWND, UINT, WCHAR, WORD, 
    BYTE, LPCWSTR, INT, HINSTANCE, BOOL, HANDLE
    ) 

■Pythonでctypeを利用する
Windowsでトーストを出すにはWindowsAPIからバルーンを出すリクエストを出す必要がある。
WindowsAPIを叩く際にbathなどを経由してもいいが、PythonからWindowsAPIを叩く良いやり方はctypeを使う

ctypeとはPythonでCの関数ライブラリを使うことが出来るものだ。
簡単なコードでとりあえずctypeを試したいのであれば、以下の記事がとても参考になった。
Python から Windows API を呼び出す – MessageBox の例
ctypeの詳細を知りたい方は以下をどうぞ。
ctypes — Pythonのための外部関数ライブラリ

■APIにあった構造体を用意する
ctypeを使って、まずはAPIを叩くための構造体を準備する。
ctypesのStructureを継承したクラスで構造体を定義します。

最初聞いたときなんのこっちゃと思いましたが、
ざっくりいうと、WindowsAPIになんらかの動作を指示する際に渡すデータの集まり(タイトルとか表示方法とか)には
事前に決められた構造体(何番目になんの型のなんて名前のデータが入るなどの定義)を用意する必要があり、
後述するWindowsAPIの仕様書(英語)を片手に準備する必要があります。

今回、トーストを出すために作成する必要がある構造体は以下

class GUID(Structure):
    _fields_ = [
        ("Data1", DWORD),
        ("Data2", WORD),
        ("Data3", WORD),
        ("Data4", BYTE * 8)
    ]

class NOTIFYICONDATAW(Structure):
    _fields_ = [
        ("cbSize", DWORD),
        ("hWnd", HWND),
        ("uID", UINT),
        ("uFlags", UINT),
        ("uCallbackMessage", UINT),
        ("hIcon", HICON),
        ("szTip", WCHAR * 128),
        ("dwState", DWORD),
        ("dwStateMask", DWORD),
        ("szInfo", WCHAR * 256),
        ("uVersion", UINT),
        ("szInfoTitle", WCHAR * 64),
        ("dwInfoFlags", DWORD),
        ("guidItem", GUID),
        ("hBalloonIcon", HICON),
    ]

具体的なやり方などは下記が参考になった。
Python: ctypesパターン集

GUID と NOTIFYICONDATAW ってのを使うんだなーくらいで今は大丈夫。

■WindowsAPIを利用できるように読み込む
WindowsAPIを読み込んで、引数の定義と戻り値の定義を行う
今回利用するAPIはLoadImageとNotifyIconだ。
▼サンプルコード

LoadImageW = windll.User32.LoadImageW
LoadImageW.argtypes = [HINSTANCE, LPCWSTR, UINT, INT, INT, UINT]
LoadImageW.restype = HANDLE
    
Shell_NotifyIconW = windll.Shell32.Shell_NotifyIconW
Shell_NotifyIconW.argtypes = [DWORD, POINTER(NOTIFYICONDATAW)]
Shell_NotifyIconW.restype = BOOL

API名の最後にWがついているが、WindowsAPIは最後にWがつくのと、Aがつくものがあり、
利用している文字コードなどの違いがある。今回はWを利用している。

必要な引数等はMicrosoftのドキュメントが参考になった。
Shell_NotifyIconW function
LoadImageW function
NOTIFYICONDATAW structure

ひとまず、これでWindowsAPIが利用可能になった。

■LoadImageWで画像アイコンを利用できるようにする
バルーンで利用しているアイコンは.icoといった拡張子の画像ファイルだ。
変換サービスやフリー素材などがあるので、適当な画像を用意しよう。
また、アイコンなしでトーストを出す方法も記載するため、この項目は飛ばしても構わない。

hicon = LoadImageW( None, icon, 1, 0, 0, 0x00000050)

これはLoadImageWを呼び出すコード。
引数の詳細は上記Microsoftのドキュメントに記載があるが、ざっくりと解説。

・第一引数
画像を使う場合はNoneを指定する(nullを指定したことになる)
・第二引数
今回は画像のファイルパスを指定する。
・第三引数
読み込む画像の形式を指定する。
アイコンは1を指定。試していないけど0だとビットマップが読み込める?
・第四引数
アイコンの幅を指定する。0なら自動
・第五引数
アイコンの高さを指定する。0なら自動
・第六引数
オプションフラグを設定する。

16進数で記載されていて最初はよくわからないかもしれないが、
WindowsAPIでは有効にする設定の数値を合計したものを指定する。
例えば、0x00000001の設定と0x00000002の設定を有効にするのなら、0x00000003を指定する。(16進数なので繰り上がりに注意)
足し算でなく、ビット演算で合算してもよい。 0x00000001 | 0x00000002
どのような設定値があるかは上記マニュアルを確認する。

今回はLR_DEFAULTSIZE(0x00000040)とLR_LOADFROMFILE(0x00000010)を指定している。
成功すればイメージのハンドルが返却される。

5. Shell_NotifyIconWで通知申請を行う
まずはWindowsAPIに渡すデータを作成する。
▼サンプルコード(画像表示あり)

flags = 0x00000016
infoflag = 0x00000034
mem_size = sizeof(NOTIFYICONDATAW)
notify_data = NOTIFYICONDATAW(
        mem_size, 0, os.getpid(), flags, 0, hicon, "tips", 
        0, 0, "message", 4, "title", infoflag, GUID(), hicon
)

▼サンプルコード(画像表示なし)

hicon = None
flags = 0x00000014
infoflag = 0x00000000
mem_size = sizeof(NOTIFYICONDATAW)
notify_data = NOTIFYICONDATAW(
        mem_size, 0, os.getpid(), flags, 0, hicon, "tips", 
        0, 0, "message", 4, "title", infoflag, GUID(), hicon
)

さて、とうとう通知を出す。
それぞれ何をしているかざっくり解説する。
※詳細は上記Microsoftのドキュメントを確認されたし

第一引数
作成した構造体のメモリサイズを指定する。
ctypeのsizeofで取得が可能
第二引数
今回は13引数を指定するため、読み飛ばされる
第三引数
実行しているPIDを取得する。上記サンプルでは自身のPIDを取得している
第四引数
どんな情報があるかやどんな表示をするかなどを指定する。
今回は NIF_ICON,NIF_TIP,NIF_INFOを有効にした。
画像を利用しない場合はNIF_TIP,NIF_INFOを有効にする。
※詳細は上記Microsoftのドキュメントを確認されたし
第五引数
LoadImageWで生成したハンドルを指定する
画像表示を行わない場合、NONEを指定
第六引数
アプリ名や詳細の記載を行う。
トーストのアプリ名が以前はここの指定内容になっているような記載をみたが、特に反映されなかった。
第七引数
画像の情報の取り扱い?
利用しないので0を指定する
第八引数
0を指定する
第九引数
通知に表示するメッセージを指定
第十引数
シェルのversionを指定する。
第十一引数
通知のタイトルを指定する。
第十二引数
利用するアイコンの設定を行う。
0x00000001や0x00000002を加算するとシステムに用意されたアイコンを利用できる
他にもいくつかあるので、上記のマニュアルを参照
今回はNIIF_USER,NIIF_NOSOUND,NIIF_LARGE_ICONが有効になっている。
通知時に音を出したいなら0x00000010を引く。
第十三引数
GUIDを与える。なぜ設定するかはドキュメント読むだけだとわからなかった。
第十四引数
LoadImageWで生成したハンドルを指定する
画像表示を行わない場合、NONEを指定
第五との違いはタスクバーのアイコンか、トーストのアイコンかの違い。
今回はどちらも同じものを設定する。

データを渡す。
▼サンプルコード

Shell_NotifyIconW(0x00000000, notify_data)
Shell_NotifyIconW(0x00000002, notify_data)

第一引数の0は表示、2は通知を汚さないように削除している。

以上でトーストをPythonから利用できる。
きっと他のWindowsAPIにも応用できるはずだ。

最後にトーストを出す便利ソースを紹介する。
GitHub-easyToast
ぜひスターを(ry

コメントを残す

メールアドレスが公開されることはありません。