ikkVisualKit 0.0.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- ikkVisualKit/CTkUiBase.py +555 -0
- ikkVisualKit/Dialog/CdialogBase.py +96 -0
- ikkVisualKit/Dialog/CdialogOk.py +102 -0
- ikkVisualKit/Dialog/CdialogYesNo.py +91 -0
- ikkVisualKit/Dialog/DialogBase.py +97 -0
- ikkVisualKit/Dialog/DialogOk.py +59 -0
- ikkVisualKit/Dialog/DialogSelectReaderPort.py +110 -0
- ikkVisualKit/Dialog/DialogYesNo.py +62 -0
- ikkVisualKit/Dialog/__init__.py +21 -0
- ikkVisualKit/TkUiBase.py +167 -0
- ikkVisualKit/Tray/ResidentApp.py +264 -0
- ikkVisualKit/Tray/TrayMenu.py +140 -0
- ikkVisualKit/Tray/__init__.py +4 -0
- ikkVisualKit/Window/WindowBase.py +149 -0
- ikkVisualKit/Window/WindowBaseModan.py +233 -0
- ikkVisualKit/Window/__init__.py +3 -0
- ikkVisualKit/__init__.py +22 -0
- ikkVisualKit/image/clear.dio.png +0 -0
- ikkVisualKit/image/goto.dio.png +0 -0
- ikkVisualKit/image/help.dio.png +0 -0
- ikkVisualKit/image/link.dio.png +0 -0
- ikkVisualKit/image/lock.dio.png +0 -0
- ikkVisualKit/image/log.dio.png +0 -0
- ikkVisualKit/image/open.dio.png +0 -0
- ikkVisualKit/image/recipe.dio.png +0 -0
- ikkVisualKit/image/save.dio.png +0 -0
- ikkVisualKit/image/search.dio.png +0 -0
- ikkVisualKit/image/setting.dio.png +0 -0
- ikkVisualKit/image/start.dio.png +0 -0
- ikkVisualKit/image/unlock.dio.png +0 -0
- ikkvisualkit-0.0.0.dist-info/METADATA +176 -0
- ikkvisualkit-0.0.0.dist-info/RECORD +35 -0
- ikkvisualkit-0.0.0.dist-info/WHEEL +5 -0
- ikkvisualkit-0.0.0.dist-info/licenses/LICENSE +21 -0
- ikkvisualkit-0.0.0.dist-info/top_level.txt +1 -0
ikkVisualKit/TkUiBase.py
ADDED
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
# TkUiBase.py
|
|
2
|
+
import tkinter as tk
|
|
3
|
+
import os
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import Optional, Callable, Any
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class TkUiBase:
|
|
9
|
+
"""
|
|
10
|
+
Tkは必ずメインスレッドで動かす。
|
|
11
|
+
サブクラスは _build(self, root) をオーバーライドする。
|
|
12
|
+
"""
|
|
13
|
+
# ==========================================
|
|
14
|
+
def __init__(self
|
|
15
|
+
,title: str = "Title"
|
|
16
|
+
,size: tuple[int, int] = (600, 300)
|
|
17
|
+
,icon_path: Optional[str] = None
|
|
18
|
+
):
|
|
19
|
+
'''コンストラクタ
|
|
20
|
+
title: ウィンドウタイトル
|
|
21
|
+
size: ウィンドウサイズ (幅, 高さ)
|
|
22
|
+
icon_path: アイコン画像のパス(必要に応じて指定、Noneの場合はデフォルトアイコン)
|
|
23
|
+
'''
|
|
24
|
+
self._title = title
|
|
25
|
+
self._size = size
|
|
26
|
+
self._icon_path = icon_path
|
|
27
|
+
self._tkRoot: Optional[tk.Tk] = tk.Tk()
|
|
28
|
+
|
|
29
|
+
self._tkRoot.title(self._title)
|
|
30
|
+
self._tkRoot.geometry(f"{self._size[0]}x{self._size[1]}")
|
|
31
|
+
self._tkRoot.protocol("WM_DELETE_WINDOW", self._handle_close)
|
|
32
|
+
#画像をキーで管理する辞書
|
|
33
|
+
self._images: dict[str, tk.PhotoImage] = {}
|
|
34
|
+
base_dir = Path(__file__).resolve().parent
|
|
35
|
+
self._image_dir = base_dir / "image"
|
|
36
|
+
|
|
37
|
+
# 汎用画像を起動時に自動登録
|
|
38
|
+
self._register_common_images()
|
|
39
|
+
|
|
40
|
+
# UI構築
|
|
41
|
+
self._build(self._tkRoot)
|
|
42
|
+
# ==========================================
|
|
43
|
+
# サブクラス用
|
|
44
|
+
# ==========================================
|
|
45
|
+
def _build(self, root: tk.Tk):
|
|
46
|
+
"""UI構築(サブクラスでオーバーライド)"""
|
|
47
|
+
# ウィンドウアイコン(.ico)
|
|
48
|
+
iconPath = self._icon_path
|
|
49
|
+
if iconPath and os.path.exists(iconPath):
|
|
50
|
+
root.iconbitmap(iconPath)
|
|
51
|
+
# ==========================================
|
|
52
|
+
# 他スレッドから安全にUI処理を投げる
|
|
53
|
+
# ==========================================
|
|
54
|
+
def invoke_ui(self, func: Callable, *args, **kwargs):
|
|
55
|
+
if not self._tkRoot:
|
|
56
|
+
return
|
|
57
|
+
self._tkRoot.after(0, lambda: func(*args, **kwargs))
|
|
58
|
+
# ==========================================
|
|
59
|
+
# 表示制御
|
|
60
|
+
# ==========================================
|
|
61
|
+
def show_window(self):
|
|
62
|
+
if not self._tkRoot:
|
|
63
|
+
return
|
|
64
|
+
self._tkRoot.deiconify()
|
|
65
|
+
self._tkRoot.lift()
|
|
66
|
+
|
|
67
|
+
def hide_window(self):
|
|
68
|
+
if not self._tkRoot:
|
|
69
|
+
return
|
|
70
|
+
self._tkRoot.withdraw()
|
|
71
|
+
# ==========================================
|
|
72
|
+
# 終了制御
|
|
73
|
+
# ==========================================
|
|
74
|
+
def close(self):
|
|
75
|
+
if self._tkRoot:
|
|
76
|
+
self._tkRoot.destroy()
|
|
77
|
+
self._tkRoot = None
|
|
78
|
+
#--------------------------------------------
|
|
79
|
+
def _handle_close(self):
|
|
80
|
+
self._on_close()
|
|
81
|
+
#--------------------------------------------
|
|
82
|
+
def _on_close(self):
|
|
83
|
+
"""×ボタン動作(必要なら継承先で上書き)"""
|
|
84
|
+
self.close()
|
|
85
|
+
# ==========================================
|
|
86
|
+
# mainloop開始(必ずメインスレッドで呼ぶ)
|
|
87
|
+
# ==========================================
|
|
88
|
+
def run(self):
|
|
89
|
+
if self._tkRoot:
|
|
90
|
+
self._tkRoot.mainloop()
|
|
91
|
+
|
|
92
|
+
#==========================================
|
|
93
|
+
# ボタン作成
|
|
94
|
+
def create_button(self,
|
|
95
|
+
text: str, #表示する文字
|
|
96
|
+
x: int,y: int, #位置
|
|
97
|
+
command: Optional[Callable[[], Any]] = None, #割り当てるメソッド
|
|
98
|
+
image_key: Optional[str] = None,
|
|
99
|
+
compound: str = "left",
|
|
100
|
+
) -> Optional[tk.Button]:
|
|
101
|
+
"""共通ボタン生成"""
|
|
102
|
+
if not self._tkRoot:
|
|
103
|
+
return None
|
|
104
|
+
#
|
|
105
|
+
button_kwargs: dict[str, Any] = {
|
|
106
|
+
"text": text,
|
|
107
|
+
"width": 32,
|
|
108
|
+
"height": 24,
|
|
109
|
+
"state": tk.NORMAL if command is not None else tk.DISABLED,
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
if command is not None:
|
|
113
|
+
button_kwargs["command"] = command
|
|
114
|
+
|
|
115
|
+
image = self.get_image(image_key) if image_key else None
|
|
116
|
+
if image is not None:
|
|
117
|
+
button_kwargs["image"] = image
|
|
118
|
+
button_kwargs["compound"] = compound
|
|
119
|
+
|
|
120
|
+
btn = tk.Button(self._tkRoot, **button_kwargs)
|
|
121
|
+
btn.place(x=x, y=y)
|
|
122
|
+
return btn
|
|
123
|
+
#UIに使う画像の登録
|
|
124
|
+
def register_image(self, key: str, filename: str) -> bool:
|
|
125
|
+
"""imageフォルダ配下の画像をキーで登録する(主用途: png)"""
|
|
126
|
+
path = self._image_dir / filename
|
|
127
|
+
if not path.exists():
|
|
128
|
+
return False
|
|
129
|
+
try:
|
|
130
|
+
self._images[key] = tk.PhotoImage(file=str(path))
|
|
131
|
+
return True
|
|
132
|
+
except tk.TclError:
|
|
133
|
+
return False
|
|
134
|
+
#登録した画像を取得
|
|
135
|
+
def get_image(self, key: str) -> Optional[tk.PhotoImage]:
|
|
136
|
+
return self._images.get(key)
|
|
137
|
+
#
|
|
138
|
+
def _register_common_images(self):
|
|
139
|
+
"""baseクラスで汎用利用するpngを事前登録する。"""
|
|
140
|
+
if not self._image_dir.exists():
|
|
141
|
+
return
|
|
142
|
+
|
|
143
|
+
for image_path in self._image_dir.glob("*.png"):
|
|
144
|
+
# 拡張子なしファイル名をキーとして登録(例: setting.dio.png -> setting.dio)
|
|
145
|
+
self.register_image(image_path.stem, image_path.name)
|
|
146
|
+
# アイコンリスト(icoファイルのアイコンはTkinterで直接扱えないため、ここではpngを想定)
|
|
147
|
+
@property
|
|
148
|
+
def icon_list(self) -> list[str]:
|
|
149
|
+
"""imageフォルダ内のpngファイル名のリストを返す"""
|
|
150
|
+
if not self._image_dir.exists():
|
|
151
|
+
return []
|
|
152
|
+
return [p.stem for p in self._image_dir.glob("*.png")]
|
|
153
|
+
#####################################################################
|
|
154
|
+
#
|
|
155
|
+
#####################################################################
|
|
156
|
+
if __name__ == "__main__":
|
|
157
|
+
window = TkUiBase(title="テスト", size=(400, 200,), icon_path="../../icon.ico")
|
|
158
|
+
window.show_window()
|
|
159
|
+
|
|
160
|
+
# 2秒後に閉じる
|
|
161
|
+
def _close_later():
|
|
162
|
+
if window._tkRoot:
|
|
163
|
+
window._tkRoot.after(2000, window.close)
|
|
164
|
+
window.invoke_ui(_close_later)
|
|
165
|
+
|
|
166
|
+
window.run()
|
|
167
|
+
print("ウィンドウを閉じました")
|
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
import sys
|
|
2
|
+
import os
|
|
3
|
+
import re
|
|
4
|
+
import tempfile
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
import logging
|
|
7
|
+
import threading
|
|
8
|
+
from typing import Protocol, Callable, Any, Optional, List, Tuple
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class SingleInstanceGuard:
|
|
12
|
+
"""Keeps a single-instance lock alive for the process lifetime."""
|
|
13
|
+
|
|
14
|
+
def __init__(self, lock_handle: Any, releaser: Optional[Callable[[Any], None]] = None):
|
|
15
|
+
self._lock_handle = lock_handle
|
|
16
|
+
self._releaser = releaser
|
|
17
|
+
|
|
18
|
+
def release(self) -> None:
|
|
19
|
+
if self._lock_handle is None:
|
|
20
|
+
return
|
|
21
|
+
|
|
22
|
+
try:
|
|
23
|
+
if self._releaser:
|
|
24
|
+
self._releaser(self._lock_handle)
|
|
25
|
+
finally:
|
|
26
|
+
self._lock_handle = None
|
|
27
|
+
self._releaser = None
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def _build_lock_name(app_name: str) -> str:
|
|
31
|
+
normalized_name = re.sub(r"[^A-Za-z0-9_.-]+", "_", app_name).strip("._")
|
|
32
|
+
return normalized_name or "resident_app"
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def _release_windows_mutex(mutex_handle: Any) -> None:
|
|
36
|
+
import ctypes
|
|
37
|
+
|
|
38
|
+
ctypes.windll.kernel32.CloseHandle(mutex_handle)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def _acquire_windows_guard(app_name: str) -> Optional[SingleInstanceGuard]:
|
|
42
|
+
import ctypes
|
|
43
|
+
|
|
44
|
+
mutex_name = f"Global\\{app_name}_Mutex"
|
|
45
|
+
mutex_handle = ctypes.windll.kernel32.CreateMutexW(None, False, mutex_name)
|
|
46
|
+
if not mutex_handle:
|
|
47
|
+
raise OSError("CreateMutexW failed")
|
|
48
|
+
|
|
49
|
+
last_error = ctypes.windll.kernel32.GetLastError()
|
|
50
|
+
ERROR_ALREADY_EXISTS = 183
|
|
51
|
+
if last_error == ERROR_ALREADY_EXISTS:
|
|
52
|
+
ctypes.windll.kernel32.CloseHandle(mutex_handle)
|
|
53
|
+
return None
|
|
54
|
+
|
|
55
|
+
return SingleInstanceGuard(mutex_handle, _release_windows_mutex)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def _release_posix_lock(lock_data: tuple[Any, str]) -> None:
|
|
59
|
+
import fcntl
|
|
60
|
+
|
|
61
|
+
lock_file, lock_path = lock_data
|
|
62
|
+
try:
|
|
63
|
+
fcntl.flock(lock_file.fileno(), fcntl.LOCK_UN)
|
|
64
|
+
finally:
|
|
65
|
+
lock_file.close()
|
|
66
|
+
try:
|
|
67
|
+
os.unlink(lock_path)
|
|
68
|
+
except FileNotFoundError:
|
|
69
|
+
pass
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def _acquire_posix_guard(app_name: str) -> Optional[SingleInstanceGuard]:
|
|
73
|
+
import fcntl
|
|
74
|
+
|
|
75
|
+
lock_name = _build_lock_name(app_name)
|
|
76
|
+
lock_path = os.path.join(tempfile.gettempdir(), f"{lock_name}.lock")
|
|
77
|
+
lock_file = open(lock_path, "a+")
|
|
78
|
+
|
|
79
|
+
try:
|
|
80
|
+
fcntl.flock(lock_file.fileno(), fcntl.LOCK_EX | fcntl.LOCK_NB)
|
|
81
|
+
except BlockingIOError:
|
|
82
|
+
lock_file.close()
|
|
83
|
+
return None
|
|
84
|
+
|
|
85
|
+
lock_file.seek(0)
|
|
86
|
+
lock_file.truncate()
|
|
87
|
+
lock_file.write(str(os.getpid()))
|
|
88
|
+
lock_file.flush()
|
|
89
|
+
|
|
90
|
+
return SingleInstanceGuard((lock_file, lock_path), _release_posix_lock)
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def acquire_single_instance_guard(app_name: str) -> Optional[SingleInstanceGuard]:
|
|
94
|
+
"""Create a process-wide single-instance guard.
|
|
95
|
+
|
|
96
|
+
Returns a guard object when this is the first instance, otherwise None.
|
|
97
|
+
"""
|
|
98
|
+
if sys.platform == "win32":
|
|
99
|
+
return _acquire_windows_guard(app_name)
|
|
100
|
+
return _acquire_posix_guard(app_name)
|
|
101
|
+
# ============================================================
|
|
102
|
+
# アプリケーションのインターフェース定義
|
|
103
|
+
# ============================================================
|
|
104
|
+
class IAppLogic(Protocol):
|
|
105
|
+
def start(self) -> None: ... #アプリのロジック開始
|
|
106
|
+
# def show(self, icon=None, item=None) -> None: ... #表示
|
|
107
|
+
# def hide(self, icon=None, item=None) -> None: ... #非表示
|
|
108
|
+
# def settings(self, icon=None, item=None) -> None: ... #設定の表示
|
|
109
|
+
def shutdown(self) -> None: ... #アプリのシャットダウン処理
|
|
110
|
+
# ============================================================
|
|
111
|
+
# タスクトレイの インターフェース定義
|
|
112
|
+
# ============================================================
|
|
113
|
+
class ITray(Protocol):
|
|
114
|
+
def run(self) -> None: ... #タスクトレイを開始(ブロッキング)
|
|
115
|
+
def run_detached(self) -> None: ... #タスクトレイを開始(非ブロッキング)
|
|
116
|
+
def stop(self) -> None: ... #タスクトレイを停止
|
|
117
|
+
# ============================================================
|
|
118
|
+
# 常駐
|
|
119
|
+
# ============================================================
|
|
120
|
+
class ResidentApp:
|
|
121
|
+
"""
|
|
122
|
+
常駐アプリのフレームワーク
|
|
123
|
+
"""
|
|
124
|
+
# --------------------------------------------------------
|
|
125
|
+
def __init__(
|
|
126
|
+
self,
|
|
127
|
+
app_logic: IAppLogic,
|
|
128
|
+
tray_factory: Callable[..., ITray],
|
|
129
|
+
app_name: str = "常駐アプリ",
|
|
130
|
+
single_instance: bool = True,
|
|
131
|
+
icon_path: Optional[str] = None,
|
|
132
|
+
menu_items: Optional[List[Tuple[str, Callable]]] = None,
|
|
133
|
+
):
|
|
134
|
+
'''コンストラクタ
|
|
135
|
+
:param app_logic: アプリのロジックを定義したクラスのインスタンス(IAppLogicを実装している必要がある)
|
|
136
|
+
:param tray_factory: タスクトレイ管理クラスのファクトリー関数(例:TrayApp)
|
|
137
|
+
:param app_name: アプリ名(トレイアイコンの右クリックメニューなどで使用)
|
|
138
|
+
:param single_instance: 多重起動を防止するかどうか
|
|
139
|
+
:param menu_items: 「表示名」と「コールバック関数」のタプルのリスト
|
|
140
|
+
'''
|
|
141
|
+
self._app_logic = app_logic
|
|
142
|
+
self._tray_factory = tray_factory
|
|
143
|
+
self._app_name = app_name
|
|
144
|
+
self._single_instance = single_instance
|
|
145
|
+
self._icon_path = icon_path
|
|
146
|
+
self._tray: Optional[ITray] = None
|
|
147
|
+
self._single_instance_guard: Optional[SingleInstanceGuard] = None
|
|
148
|
+
self._menu_items: Optional[List[Tuple[str, Callable]]] = menu_items
|
|
149
|
+
self._setup_logging()
|
|
150
|
+
# --------------------------------------------------------
|
|
151
|
+
def _setup_logging(self) -> None:
|
|
152
|
+
logging.basicConfig(
|
|
153
|
+
level=logging.INFO,
|
|
154
|
+
format="%(asctime)s [%(levelname)s] %(message)s",
|
|
155
|
+
)
|
|
156
|
+
self._logger = logging.getLogger(self._app_name)
|
|
157
|
+
# --------------------------------------------------------
|
|
158
|
+
def run(self) -> None:
|
|
159
|
+
'''アプリを起動'''
|
|
160
|
+
try:
|
|
161
|
+
self._logger.info("Application starting...")
|
|
162
|
+
# 多重起動防止
|
|
163
|
+
if self._single_instance:
|
|
164
|
+
self._single_instance_guard = acquire_single_instance_guard(self._app_name)
|
|
165
|
+
if self._single_instance_guard is None:
|
|
166
|
+
self._logger.warning("Another instance is already running.")
|
|
167
|
+
return
|
|
168
|
+
# アプリのロジックを開始
|
|
169
|
+
self._app_logic.start()
|
|
170
|
+
# 呼び出し元から渡されたメニュー + 終了メニューを構成
|
|
171
|
+
menu_items: List[Tuple[str, Callable]] = []
|
|
172
|
+
if self._menu_items:
|
|
173
|
+
menu_items.extend(self._menu_items)
|
|
174
|
+
menu_items.append(("終了", self._exit)) # 終了は固定でメニューに追加
|
|
175
|
+
# タスクトレイを開始
|
|
176
|
+
self._tray = self._tray_factory(
|
|
177
|
+
icon_name=self._app_name,
|
|
178
|
+
on_exit=self._exit,
|
|
179
|
+
menu_items=menu_items,
|
|
180
|
+
icon_path=self._icon_path, # アイコン画像のパス(必要に応じて指定)
|
|
181
|
+
)
|
|
182
|
+
self._tray.run()
|
|
183
|
+
except Exception as e:
|
|
184
|
+
self._logger.exception("Fatal error occurred: %s", e)
|
|
185
|
+
self._safe_shutdown()
|
|
186
|
+
# --------------------------------------------------------
|
|
187
|
+
def _exit(self, icon=None, item=None) -> None:
|
|
188
|
+
'''アプリを終了
|
|
189
|
+
:param icon: タスクトレイアイコン(pystray.Icon)※トレイの終了アクションから呼び出された場合に渡される
|
|
190
|
+
:param item: タスクトレイメニューアイテム(pystray.MenuItem)※トレイの終了アクションから呼び出された場合に渡される
|
|
191
|
+
'''
|
|
192
|
+
self._logger.info("Exit requested.")
|
|
193
|
+
self._safe_shutdown()
|
|
194
|
+
if icon:
|
|
195
|
+
icon.stop()
|
|
196
|
+
|
|
197
|
+
# --------------------------------------------------------
|
|
198
|
+
def _safe_shutdown(self) -> None:
|
|
199
|
+
'''安全にシャットダウン'''
|
|
200
|
+
try:
|
|
201
|
+
self._app_logic.shutdown()
|
|
202
|
+
self._logger.info("Application shutdown complete.")
|
|
203
|
+
except Exception as e:
|
|
204
|
+
self._logger.exception("Error during shutdown: %s", e)
|
|
205
|
+
finally:
|
|
206
|
+
self._release_mutex()
|
|
207
|
+
# --------------------------------------------------------
|
|
208
|
+
# 多重起動防止
|
|
209
|
+
# --------------------------------------------------------
|
|
210
|
+
def _acquire_mutex(self) -> bool:
|
|
211
|
+
'''多重起動防止のためのミューテックスを取得'''
|
|
212
|
+
self._single_instance_guard = acquire_single_instance_guard(self._app_name)
|
|
213
|
+
return self._single_instance_guard is not None
|
|
214
|
+
# --------------------------------------------------------
|
|
215
|
+
def _release_mutex(self) -> None:
|
|
216
|
+
'''多重起動防止のためのミューテックスを解放'''
|
|
217
|
+
if self._single_instance_guard:
|
|
218
|
+
self._single_instance_guard.release()
|
|
219
|
+
self._single_instance_guard = None
|
|
220
|
+
# ============================================================
|
|
221
|
+
# 動作例
|
|
222
|
+
# ============================================================
|
|
223
|
+
try:
|
|
224
|
+
from .TrayMenu import TrayMenu # パッケージとして import される通常ケース
|
|
225
|
+
except ImportError:
|
|
226
|
+
from TrayMenu import TrayMenu # ResidentApp.py を直接実行した場 合のフォールバック
|
|
227
|
+
import time
|
|
228
|
+
# アプリロジッククラスのダミー:インタフェースで基底されている関数は必須実装
|
|
229
|
+
class DummyLogic(IAppLogic):
|
|
230
|
+
def start(self) -> None:
|
|
231
|
+
print("🚀アプリのロジックを開始します")
|
|
232
|
+
def show(self, icon=None, item=None):
|
|
233
|
+
print("🦊表示します")
|
|
234
|
+
def hide(self, icon=None, item=None):
|
|
235
|
+
print("🦉非表示にします")
|
|
236
|
+
def settings(self, icon=None, item=None):
|
|
237
|
+
print("設定ウインドウを表示します")
|
|
238
|
+
def shutdown(self):
|
|
239
|
+
print("🚦停止します")
|
|
240
|
+
|
|
241
|
+
if __name__ == "__main__":
|
|
242
|
+
# 表示名とメソッドのペアをここで定義
|
|
243
|
+
tray_menu_items = [
|
|
244
|
+
("表示", lambda: print("表示")), # 例: 任意のメソッド名
|
|
245
|
+
("一時停止", lambda: print("一時停止")), # 例
|
|
246
|
+
]
|
|
247
|
+
|
|
248
|
+
#常駐アプリクラスのインスタンスを作成
|
|
249
|
+
app = ResidentApp(
|
|
250
|
+
app_logic=DummyLogic(),
|
|
251
|
+
tray_factory=TrayMenu, #タスクトレイモジュールを渡す(スレッドの中でインスタンスして起動される)
|
|
252
|
+
app_name="常駐アプリテスト",
|
|
253
|
+
single_instance=False, #多重起動を許可
|
|
254
|
+
menu_items=tray_menu_items,
|
|
255
|
+
icon_path="../../icon.ico", #アイコン画像のパス(必要に応じて指定、Noneの場合はデフォルトアイコン)
|
|
256
|
+
)
|
|
257
|
+
#スレッドで常駐アプリを起動
|
|
258
|
+
t = threading.Thread(target=app.run, daemon=True)
|
|
259
|
+
t.start()
|
|
260
|
+
|
|
261
|
+
print("トレイ表示確認してください(15秒)")
|
|
262
|
+
time.sleep(15)
|
|
263
|
+
print("終了します")
|
|
264
|
+
app._exit()
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import threading
|
|
2
|
+
from typing import Callable, List, Tuple, Optional
|
|
3
|
+
|
|
4
|
+
import pystray #タスクトレイアイコン用ライブラリ
|
|
5
|
+
from pystray import MenuItem as Item #トレイアイコンの右クリックメニューのアイテム用クラス
|
|
6
|
+
from PIL import Image, ImageDraw
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import Callable, Optional
|
|
9
|
+
|
|
10
|
+
class TrayMenu:
|
|
11
|
+
"""
|
|
12
|
+
汎用システムトレイ管理クラス
|
|
13
|
+
他アプリにそのまま流用可能
|
|
14
|
+
"""
|
|
15
|
+
# ----------------------------------------------------------
|
|
16
|
+
def __init__(self,
|
|
17
|
+
icon_name: str,
|
|
18
|
+
on_exit: Callable,
|
|
19
|
+
menu_items: List[Tuple[str, Callable]],
|
|
20
|
+
icon_path: Optional[str] = None,
|
|
21
|
+
):
|
|
22
|
+
'''コンストラクタ
|
|
23
|
+
:param icon_name: アイコンの名前(トレイアイコンの右クリックメニューなどで使用)
|
|
24
|
+
:param on_exit: 終了アクション関数(例:アプリの終了処理を呼び出す関数)
|
|
25
|
+
:param menu_items: 右クリックメニューのアイテム(表示名とアクション関数のタプルのリスト)
|
|
26
|
+
:param icon_path: アイコン画像のパス(省略可、見つからない場合はデフォルトアイコンを使用)
|
|
27
|
+
'''
|
|
28
|
+
self._icon_name = icon_name
|
|
29
|
+
self._on_exit: Callable = on_exit
|
|
30
|
+
self._menu_items: List[Tuple[str, Callable]] = menu_items
|
|
31
|
+
self._icon_path: Optional[str] = icon_path
|
|
32
|
+
self._tray_icon: Optional[pystray.Icon] = None
|
|
33
|
+
self._thread: Optional[threading.Thread] = None
|
|
34
|
+
# ----------------------------------------------------------
|
|
35
|
+
def run(self) -> None:
|
|
36
|
+
"""タスクトレイを開始(ブロッキング)"""
|
|
37
|
+
self._create_icon()
|
|
38
|
+
self._tray_icon.run()
|
|
39
|
+
# ----------------------------------------------------------
|
|
40
|
+
def run_detached(self) -> None:
|
|
41
|
+
"""タスクトレイ開始(別スレッド)"""
|
|
42
|
+
self._create_icon()
|
|
43
|
+
self._thread = threading.Thread(target=self._tray_icon.run, daemon=True)
|
|
44
|
+
self._thread.start() #タスクスタート
|
|
45
|
+
# ----------------------------------------------------------
|
|
46
|
+
def stop(self) -> None:
|
|
47
|
+
"""トレイを停止"""
|
|
48
|
+
if self._tray_icon:
|
|
49
|
+
self._tray_icon.stop()
|
|
50
|
+
# ----------------------------------------------------------
|
|
51
|
+
def _create_icon(self) -> None:
|
|
52
|
+
#タスクトレイ用アイコン作成
|
|
53
|
+
image = self._load_icon_image()
|
|
54
|
+
#右クリックメニュー作成
|
|
55
|
+
menu = pystray.Menu(
|
|
56
|
+
*[ Item(name, action)
|
|
57
|
+
for name, action in self._menu_items
|
|
58
|
+
]
|
|
59
|
+
)
|
|
60
|
+
#タスクトレイアイコン作成
|
|
61
|
+
self._tray_icon = pystray.Icon(
|
|
62
|
+
self._icon_name, # アイコンの名前
|
|
63
|
+
image, # アイコン画像
|
|
64
|
+
self._icon_name, # ツールチップ
|
|
65
|
+
menu # 右クリックメニュー
|
|
66
|
+
)
|
|
67
|
+
# ----------------------------------------------------------
|
|
68
|
+
def _load_icon_image(self) -> Image.Image:
|
|
69
|
+
default_size = (64, 64)
|
|
70
|
+
|
|
71
|
+
# 指定パスがあれば候補を順に探す(絶対/カレント/このモジュールと同じフォルダ)
|
|
72
|
+
if self._icon_path:
|
|
73
|
+
p = Path(self._icon_path)
|
|
74
|
+
candidates = [p] if p.is_absolute() else [
|
|
75
|
+
Path.cwd() / p,
|
|
76
|
+
Path(__file__).parent / p,
|
|
77
|
+
p, # try raw as last resort (Pillow が扱える場合)
|
|
78
|
+
]
|
|
79
|
+
for c in candidates:
|
|
80
|
+
try:
|
|
81
|
+
if c.exists():
|
|
82
|
+
img = Image.open(c)
|
|
83
|
+
return self._prepare_image(img, default_size)
|
|
84
|
+
except Exception:
|
|
85
|
+
continue
|
|
86
|
+
# 見つからなければデフォルトアイコンへフォールバック
|
|
87
|
+
|
|
88
|
+
# デフォルトアイコン(RGBA で返す)
|
|
89
|
+
image = Image.new("RGBA", default_size, (0, 0, 255, 255))
|
|
90
|
+
draw = ImageDraw.Draw(image)
|
|
91
|
+
draw.rectangle((16, 16, 48, 48), fill="white")
|
|
92
|
+
#draw.chord((32, 32, 48), 0, 180, fill="red")
|
|
93
|
+
return image
|
|
94
|
+
|
|
95
|
+
def _prepare_image(self, img: Image.Image, size: tuple) -> Image.Image:
|
|
96
|
+
"""
|
|
97
|
+
Image を pystray 用サイズに変換して返す(透過パディングで中央配置)
|
|
98
|
+
"""
|
|
99
|
+
img = img.convert("RGBA")
|
|
100
|
+
# サムネイル(アスペクト比維持)
|
|
101
|
+
img.thumbnail(size, Image.LANCZOS)
|
|
102
|
+
background = Image.new("RGBA", size, (0, 0, 0, 0))
|
|
103
|
+
x = (size[0] - img.width) // 2
|
|
104
|
+
y = (size[1] - img.height) // 2
|
|
105
|
+
background.paste(img, (x, y), img)
|
|
106
|
+
return background
|
|
107
|
+
#####################################################################
|
|
108
|
+
#テスト
|
|
109
|
+
#####################################################################
|
|
110
|
+
if __name__ == "__main__":
|
|
111
|
+
import time
|
|
112
|
+
loop = True
|
|
113
|
+
# 終処理の例
|
|
114
|
+
def on_exit_sample():
|
|
115
|
+
print("Exiting...")
|
|
116
|
+
global loop
|
|
117
|
+
loop = False
|
|
118
|
+
tray.stop()
|
|
119
|
+
# メニューアクションの例
|
|
120
|
+
def on_action_sample():
|
|
121
|
+
print("Hello from tray!")
|
|
122
|
+
# 右クリックメニューに登録する例(表示文字と関数のタプルのリスト)
|
|
123
|
+
menu_items = [
|
|
124
|
+
("Say Hello", on_action_sample),
|
|
125
|
+
("Exit", on_exit_sample)
|
|
126
|
+
]
|
|
127
|
+
# トレイメニューの作成と起動
|
|
128
|
+
tray = TrayMenu(
|
|
129
|
+
icon_name="MyTrayApp",
|
|
130
|
+
on_exit=on_exit_sample,
|
|
131
|
+
menu_items=menu_items,
|
|
132
|
+
icon_path=None # アイコンパスを指定(例: "icon.png")
|
|
133
|
+
)
|
|
134
|
+
tray.run_detached()
|
|
135
|
+
#止めるまでループ
|
|
136
|
+
try:
|
|
137
|
+
while loop:
|
|
138
|
+
time.sleep(1)
|
|
139
|
+
except KeyboardInterrupt:
|
|
140
|
+
tray.stop()
|