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
|
@@ -0,0 +1,555 @@
|
|
|
1
|
+
import customtkinter as ctk
|
|
2
|
+
import os
|
|
3
|
+
import tkinter as tk
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import Optional, Callable, Any
|
|
6
|
+
from importlib.resources import files
|
|
7
|
+
from io import BytesIO
|
|
8
|
+
|
|
9
|
+
from PIL import Image
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class CTkButtonWithTooltip(ctk.CTkButton):
|
|
13
|
+
"""ツールチップ対応のカスタムCTkButtonクラス"""
|
|
14
|
+
def __init__(self, *args, **kwargs):
|
|
15
|
+
super().__init__(*args, **kwargs)
|
|
16
|
+
self._ui_base: Optional['CTkUiBase'] = None
|
|
17
|
+
self._command: Optional[Callable[[], Any]] = None
|
|
18
|
+
|
|
19
|
+
@property
|
|
20
|
+
def tooltip_text(self) -> Optional[str]:
|
|
21
|
+
"""ツールチップテキストを取得"""
|
|
22
|
+
if self._ui_base:
|
|
23
|
+
return self._ui_base._button_tooltips.get(self)
|
|
24
|
+
return None
|
|
25
|
+
|
|
26
|
+
@tooltip_text.setter
|
|
27
|
+
def tooltip_text(self, value: Optional[str]):
|
|
28
|
+
"""ツールチップテキストを設定"""
|
|
29
|
+
if self._ui_base:
|
|
30
|
+
if value:
|
|
31
|
+
self._ui_base._button_tooltips[self] = value
|
|
32
|
+
else:
|
|
33
|
+
self._ui_base._button_tooltips.pop(self, None)
|
|
34
|
+
|
|
35
|
+
@property
|
|
36
|
+
def command(self) -> Optional[Callable[[], Any]]:
|
|
37
|
+
"""クリック時のコマンドを取得"""
|
|
38
|
+
return self._command
|
|
39
|
+
|
|
40
|
+
@command.setter
|
|
41
|
+
def command(self, func: Optional[Callable[[], Any]]):
|
|
42
|
+
"""クリック時のコマンドを設定"""
|
|
43
|
+
self._command = func
|
|
44
|
+
if func is not None:
|
|
45
|
+
self.configure(command=func, state=tk.NORMAL)
|
|
46
|
+
else:
|
|
47
|
+
self.configure(state=tk.DISABLED)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
class TextInputPair:
|
|
51
|
+
"""ラベルとテキストボックスのペア"""
|
|
52
|
+
def __init__(self, label: ctk.CTkLabel, entry: ctk.CTkEntry):
|
|
53
|
+
self.label = label
|
|
54
|
+
self.entry = entry
|
|
55
|
+
self._ui_base: Optional['CTkUiBase'] = None
|
|
56
|
+
|
|
57
|
+
@property
|
|
58
|
+
def tooltip_text(self) -> Optional[str]:
|
|
59
|
+
"""ツールチップテキストを取得"""
|
|
60
|
+
if self._ui_base:
|
|
61
|
+
return self._ui_base._entry_tooltips.get(self.entry)
|
|
62
|
+
return None
|
|
63
|
+
|
|
64
|
+
@tooltip_text.setter
|
|
65
|
+
def tooltip_text(self, value: Optional[str]):
|
|
66
|
+
"""ツールチップテキストを設定"""
|
|
67
|
+
if self._ui_base:
|
|
68
|
+
if value:
|
|
69
|
+
self._ui_base._entry_tooltips[self.entry] = value
|
|
70
|
+
else:
|
|
71
|
+
self._ui_base._entry_tooltips.pop(self.entry, None)
|
|
72
|
+
|
|
73
|
+
@property
|
|
74
|
+
def display_text(self) -> str:
|
|
75
|
+
"""表示テキストを取得"""
|
|
76
|
+
return self.entry.get()
|
|
77
|
+
|
|
78
|
+
@display_text.setter
|
|
79
|
+
def display_text(self, text: str):
|
|
80
|
+
"""表示テキストを設定(読み取り専用テキストボックスに値を設定)"""
|
|
81
|
+
# 読み取り専用を一時的に解除して値を設定
|
|
82
|
+
self.entry.configure(state="normal")
|
|
83
|
+
self.entry.delete(0, "end")
|
|
84
|
+
self.entry.insert(0, text)
|
|
85
|
+
self.entry.configure(state="disabled")
|
|
86
|
+
|
|
87
|
+
# 背景色を黄色く2回点滅
|
|
88
|
+
self._blink_entry()
|
|
89
|
+
|
|
90
|
+
def _blink_entry(self):
|
|
91
|
+
"""エントリーの背景色を黄色く2回点滅"""
|
|
92
|
+
if not self._ui_base or not self._ui_base._tkRoot:
|
|
93
|
+
return
|
|
94
|
+
|
|
95
|
+
blink_state = [0] # 点滅状態を保持(0=黄→1=元→2=黄→3=元→終了)
|
|
96
|
+
original_bg = self.entry.cget("fg_color") # 元の背景色を保存
|
|
97
|
+
|
|
98
|
+
def toggle_color():
|
|
99
|
+
if blink_state[0] % 2 == 0:
|
|
100
|
+
self.entry.configure(fg_color="#FFFF00") # 黄色
|
|
101
|
+
else:
|
|
102
|
+
self.entry.configure(fg_color=original_bg) # 元の色
|
|
103
|
+
|
|
104
|
+
blink_state[0] += 1
|
|
105
|
+
if blink_state[0] < 4: # 4回の遷移 = 2回点滅
|
|
106
|
+
self._ui_base._tkRoot.after(300, toggle_color)
|
|
107
|
+
|
|
108
|
+
toggle_color()
|
|
109
|
+
|
|
110
|
+
# 後方互換性のための value プロパティ
|
|
111
|
+
@property
|
|
112
|
+
def value(self) -> str:
|
|
113
|
+
"""テキストボックスの値を取得(deprecated: display_text を使用してください)"""
|
|
114
|
+
return self.display_text
|
|
115
|
+
|
|
116
|
+
@value.setter
|
|
117
|
+
def value(self, text: str):
|
|
118
|
+
"""テキストボックスの値を設定(deprecated: display_text を使用してください)"""
|
|
119
|
+
self.display_text = text
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
class LogViewBox:
|
|
123
|
+
"""ログ表示用テキストボックス"""
|
|
124
|
+
def __init__(self, text_widget: ctk.CTkTextbox, x: int, y: int, width: int, initial_lines: int = 2):
|
|
125
|
+
self.text_widget = text_widget
|
|
126
|
+
self.x = x
|
|
127
|
+
self.y = y
|
|
128
|
+
self.width = width
|
|
129
|
+
self.lines = initial_lines
|
|
130
|
+
self.parent = None
|
|
131
|
+
|
|
132
|
+
# ログレベルごとの色定義
|
|
133
|
+
self._log_level_colors = {
|
|
134
|
+
"ERROR": "#ff6b6b", # 赤
|
|
135
|
+
"WARN": "#ffa500", # オレンジ
|
|
136
|
+
"INFO": "#4dabf7", # 青
|
|
137
|
+
"DEBUG": "#909090" # グレー
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
# タグ設定
|
|
141
|
+
for level, color in self._log_level_colors.items():
|
|
142
|
+
text_widget.tag_config(level, foreground=color)
|
|
143
|
+
|
|
144
|
+
def add_log(self, message: str, level: str = "INFO"):
|
|
145
|
+
"""ログを追加(最下行に追加、自動スクロール)"""
|
|
146
|
+
if level not in self._log_level_colors:
|
|
147
|
+
level = "INFO"
|
|
148
|
+
|
|
149
|
+
# テキストボックスを編集可能に
|
|
150
|
+
self.text_widget.configure(state="normal")
|
|
151
|
+
|
|
152
|
+
# ログエントリを作成
|
|
153
|
+
log_entry = f"[{level}] {message}\n"
|
|
154
|
+
|
|
155
|
+
# 末尾に追加
|
|
156
|
+
start_index = self.text_widget.index("end-1c")
|
|
157
|
+
self.text_widget.insert("end", log_entry)
|
|
158
|
+
|
|
159
|
+
# ログレベルに応じて色付け
|
|
160
|
+
end_index = self.text_widget.index("end-1c")
|
|
161
|
+
self.text_widget.tag_add(level, start_index, end_index)
|
|
162
|
+
|
|
163
|
+
# 最下行にスクロール
|
|
164
|
+
self.text_widget.see("end")
|
|
165
|
+
|
|
166
|
+
# 読み取り専用に戻す
|
|
167
|
+
self.text_widget.configure(state="disabled")
|
|
168
|
+
|
|
169
|
+
def expand(self, lines: int):
|
|
170
|
+
"""表示行数を拡張"""
|
|
171
|
+
self.lines = lines
|
|
172
|
+
line_height = 20 # 1行あたりのピクセル高さ(概算)
|
|
173
|
+
new_height = lines * line_height + 10
|
|
174
|
+
self.text_widget.configure(height=new_height)
|
|
175
|
+
|
|
176
|
+
def collapse(self):
|
|
177
|
+
"""デフォルト表示に戻す"""
|
|
178
|
+
self.expand(self.lines)
|
|
179
|
+
|
|
180
|
+
def clear(self):
|
|
181
|
+
"""ログをクリア"""
|
|
182
|
+
self.text_widget.configure(state="normal")
|
|
183
|
+
self.text_widget.delete("1.0", "end")
|
|
184
|
+
self.text_widget.configure(state="disabled")
|
|
185
|
+
|
|
186
|
+
@property
|
|
187
|
+
def tooltip_text(self) -> Optional[str]:
|
|
188
|
+
"""ツールチップ(互換性のため)"""
|
|
189
|
+
return None
|
|
190
|
+
|
|
191
|
+
@tooltip_text.setter
|
|
192
|
+
def tooltip_text(self, value: Optional[str]):
|
|
193
|
+
"""ツールチップ設定"""
|
|
194
|
+
if self.parent and value:
|
|
195
|
+
self.parent._entry_tooltips[self.text_widget] = value
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
class CTkUiBase:
|
|
199
|
+
"""
|
|
200
|
+
Tkは必ずメインスレッドで動かす。
|
|
201
|
+
サブクラスは _build(self, root) をオーバーライドする。
|
|
202
|
+
"""
|
|
203
|
+
# パッケージリソースのイメージディレクトリ
|
|
204
|
+
_image_dir = files('ikkVisualKit').joinpath('image')
|
|
205
|
+
|
|
206
|
+
# ==========================================
|
|
207
|
+
def __init__(self
|
|
208
|
+
,title: str = "Title"
|
|
209
|
+
,size: tuple[int, int] = (600, 300)
|
|
210
|
+
,icon_path: Optional[str] = None
|
|
211
|
+
):
|
|
212
|
+
'''コンストラクタ
|
|
213
|
+
title: ウィンドウタイトル
|
|
214
|
+
size: ウィンドウサイズ (幅, 高さ)
|
|
215
|
+
icon_path: アイコン画像のパス(必要に応じて指定、Noneの場合はデフォルトアイコン)
|
|
216
|
+
'''
|
|
217
|
+
# テーマ設定
|
|
218
|
+
ctk.set_appearance_mode("Light")
|
|
219
|
+
ctk.set_default_color_theme("green")
|
|
220
|
+
self._title = title
|
|
221
|
+
self._size = size
|
|
222
|
+
self._icon_path = icon_path
|
|
223
|
+
self._tkRoot: Optional[ctk.CTk] = ctk.CTk()
|
|
224
|
+
self._tooltip: Optional[tk.Toplevel] = None
|
|
225
|
+
self._button_tooltips: dict[ctk.CTkButton, str] = {}
|
|
226
|
+
self._entry_tooltips: dict[ctk.CTkEntry, str] = {}
|
|
227
|
+
#画像をキーで管理する辞書
|
|
228
|
+
self._images: dict[str, ctk.CTkImage] = {}
|
|
229
|
+
|
|
230
|
+
# 汎用画像を起動時に自動登録
|
|
231
|
+
self._register_common_images()
|
|
232
|
+
self._tkRoot.title(self._title)
|
|
233
|
+
self._tkRoot.geometry(f"{self._size[0]}x{self._size[1]}")
|
|
234
|
+
self._tkRoot.protocol("WM_DELETE_WINDOW", self._handle_close)
|
|
235
|
+
|
|
236
|
+
# UI構築
|
|
237
|
+
self._build(self._tkRoot)
|
|
238
|
+
# ==========================================
|
|
239
|
+
# サブクラス用
|
|
240
|
+
# ==========================================
|
|
241
|
+
def _build(self, root: ctk.CTk):
|
|
242
|
+
"""UI構築(サブクラスでオーバーライド)"""
|
|
243
|
+
# ウィンドウアイコン(.ico)
|
|
244
|
+
iconPath = self._icon_path
|
|
245
|
+
if iconPath and os.path.exists(iconPath):
|
|
246
|
+
root.iconbitmap(iconPath)
|
|
247
|
+
# ==========================================
|
|
248
|
+
# 他スレッドから安全にUI処理を投げる
|
|
249
|
+
# ==========================================
|
|
250
|
+
def invoke_ui(self, func: Callable, *args, **kwargs):
|
|
251
|
+
if not self._tkRoot:
|
|
252
|
+
return
|
|
253
|
+
self._tkRoot.after(0, lambda: func(*args, **kwargs))
|
|
254
|
+
# ==========================================
|
|
255
|
+
# 表示制御
|
|
256
|
+
# ==========================================
|
|
257
|
+
def show_window(self):
|
|
258
|
+
if not self._tkRoot:
|
|
259
|
+
return
|
|
260
|
+
self._tkRoot.deiconify()
|
|
261
|
+
self._tkRoot.lift()
|
|
262
|
+
|
|
263
|
+
def hide_window(self):
|
|
264
|
+
if not self._tkRoot:
|
|
265
|
+
return
|
|
266
|
+
self._tkRoot.withdraw()
|
|
267
|
+
# ==========================================
|
|
268
|
+
# 終了制御
|
|
269
|
+
# ==========================================
|
|
270
|
+
def close(self):
|
|
271
|
+
if self._tkRoot:
|
|
272
|
+
self._tkRoot.destroy()
|
|
273
|
+
self._tkRoot = None
|
|
274
|
+
#--------------------------------------------
|
|
275
|
+
def _handle_close(self):
|
|
276
|
+
self._on_close()
|
|
277
|
+
#--------------------------------------------
|
|
278
|
+
def _on_close(self):
|
|
279
|
+
"""×ボタン動作(必要なら継承先で上書き)"""
|
|
280
|
+
self.close()
|
|
281
|
+
# ==========================================
|
|
282
|
+
# mainloop開始(必ずメインスレッドで呼ぶ)
|
|
283
|
+
# ==========================================
|
|
284
|
+
def run(self):
|
|
285
|
+
if self._tkRoot:
|
|
286
|
+
self._tkRoot.mainloop()
|
|
287
|
+
#==========================================
|
|
288
|
+
# ボタン作成
|
|
289
|
+
def make_ActionButton(self,
|
|
290
|
+
x: int,y: int, #位置
|
|
291
|
+
text: Optional[str]="", #表示する文字
|
|
292
|
+
command: Optional[Callable[[], Any]] = None, #割り当てるメソッド
|
|
293
|
+
image_key: Optional[str] = None,
|
|
294
|
+
compound: str = "left",
|
|
295
|
+
size_rank: int = 0, # 0=48x32, 1=96x64(ライトブルー), 2=128x64(Lime)
|
|
296
|
+
) -> Optional[ctk.CTkButton]:
|
|
297
|
+
"""共通ボタン生成
|
|
298
|
+
size_rank=0: 通常サイズ(幅48、高さ32)グレー
|
|
299
|
+
size_rank=1: 中サイズ(幅96、高さ64)ライトブルー
|
|
300
|
+
size_rank=2: 大サイズ(幅128、高さ64)Lime
|
|
301
|
+
"""
|
|
302
|
+
if not self._tkRoot:
|
|
303
|
+
return None
|
|
304
|
+
|
|
305
|
+
# size_rankに応じてパラメータを変更
|
|
306
|
+
if size_rank == 1:
|
|
307
|
+
btn_width = 96
|
|
308
|
+
btn_height = 64
|
|
309
|
+
btn_font_size = 12
|
|
310
|
+
btn_compound = "bottom" # アイコンの下に文字
|
|
311
|
+
fg_color = "#ADD8E6" # ライトブルー
|
|
312
|
+
hover_color = "#87CEEB" # スカイブルー
|
|
313
|
+
elif size_rank == 2:
|
|
314
|
+
btn_width = 128
|
|
315
|
+
btn_height = 64
|
|
316
|
+
btn_font_size = 12
|
|
317
|
+
btn_compound = "bottom" # アイコンの下に文字
|
|
318
|
+
fg_color = "#00FF00" # Lime
|
|
319
|
+
hover_color = "#32CD32" # ライムグリーン
|
|
320
|
+
else: # size_rank == 0
|
|
321
|
+
btn_width = 48
|
|
322
|
+
btn_height = 32
|
|
323
|
+
btn_font_size = 12
|
|
324
|
+
btn_compound = compound
|
|
325
|
+
fg_color = "#c3c3c3" # グレー
|
|
326
|
+
hover_color = "#DB8D0F" # オレンジ
|
|
327
|
+
|
|
328
|
+
#ボタンの基本スタイル
|
|
329
|
+
button_kwargs: dict[str, Any] = {
|
|
330
|
+
"text": text,
|
|
331
|
+
"width": btn_width,
|
|
332
|
+
"height": btn_height,
|
|
333
|
+
"state": tk.NORMAL if command is not None else tk.DISABLED,
|
|
334
|
+
"fg_color": fg_color,
|
|
335
|
+
"hover_color": hover_color,
|
|
336
|
+
"text_color": "black",
|
|
337
|
+
"font": ctk.CTkFont(size=btn_font_size, weight="normal"),
|
|
338
|
+
"border_width": 2,
|
|
339
|
+
"border_color": "#004382",
|
|
340
|
+
"corner_radius": 4,
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
if command is not None:
|
|
344
|
+
button_kwargs["command"] = command
|
|
345
|
+
#画像があれば追加
|
|
346
|
+
image = self.get_image(image_key) if image_key else None
|
|
347
|
+
if image is not None:
|
|
348
|
+
button_kwargs["image"] = image
|
|
349
|
+
button_kwargs["compound"] = btn_compound
|
|
350
|
+
#ボタン生成
|
|
351
|
+
btn = CTkButtonWithTooltip(self._tkRoot, **button_kwargs)
|
|
352
|
+
btn._ui_base = self # UIBase参照を設定
|
|
353
|
+
|
|
354
|
+
# デフォルトの色を保存
|
|
355
|
+
default_fg_color = button_kwargs["fg_color"]
|
|
356
|
+
hover_fg_color = button_kwargs["hover_color"]
|
|
357
|
+
|
|
358
|
+
#マウスホバー処理
|
|
359
|
+
def on_enter(event):
|
|
360
|
+
btn.configure(fg_color=hover_fg_color, text_color="black")
|
|
361
|
+
tooltip_text = self._button_tooltips.get(btn)
|
|
362
|
+
if tooltip_text:
|
|
363
|
+
self._show_tooltip(btn, tooltip_text)
|
|
364
|
+
#マウスホバー解除処理
|
|
365
|
+
def on_leave(event):
|
|
366
|
+
btn.configure(fg_color=default_fg_color, text_color="black")
|
|
367
|
+
self._hide_tooltip()
|
|
368
|
+
#ホバーイベントのバインド
|
|
369
|
+
btn.bind("<Enter>", on_enter)
|
|
370
|
+
btn.bind("<Leave>", on_leave)
|
|
371
|
+
btn.place(x=x, y=y)
|
|
372
|
+
return btn
|
|
373
|
+
#--------------------------------------------------------
|
|
374
|
+
def set_button_tooltip(self, button: ctk.CTkButton, text: Optional[str]):
|
|
375
|
+
"""生成済みボタンへツールチップを後付け設定する。"""
|
|
376
|
+
if text:
|
|
377
|
+
self._button_tooltips[button] = text
|
|
378
|
+
return
|
|
379
|
+
|
|
380
|
+
self._button_tooltips.pop(button, None)
|
|
381
|
+
self._hide_tooltip()
|
|
382
|
+
#========================================================
|
|
383
|
+
# テキスト入力フィールド作成
|
|
384
|
+
def make_labelTextBox(self,
|
|
385
|
+
x: int, y: int, # ラベルの左上座標
|
|
386
|
+
label_text: str, # ラベルのテキスト
|
|
387
|
+
width: int = 200, # 幅
|
|
388
|
+
entry_text: str = "", # テキストボックスの初期テキスト
|
|
389
|
+
) -> Optional[TextInputPair]:
|
|
390
|
+
"""ラベルとテキストボックスのペアを生成"""
|
|
391
|
+
if not self._tkRoot:
|
|
392
|
+
return None
|
|
393
|
+
# ラベル生成
|
|
394
|
+
_labnel_height = 32
|
|
395
|
+
label = ctk.CTkLabel(
|
|
396
|
+
self._tkRoot,
|
|
397
|
+
text=label_text,
|
|
398
|
+
font=ctk.CTkFont("Meiryo",size=16,weight="normal"),
|
|
399
|
+
width=width,
|
|
400
|
+
height=_labnel_height,
|
|
401
|
+
anchor="e", # ウィジェット内での位置を右側に(east)
|
|
402
|
+
justify="right", # 複数行のテキストを右揃えに
|
|
403
|
+
)
|
|
404
|
+
label.place(x=x, y=y)
|
|
405
|
+
# テキストボックス生成
|
|
406
|
+
entry = ctk.CTkEntry(
|
|
407
|
+
self._tkRoot,
|
|
408
|
+
font=ctk.CTkFont(size=30),
|
|
409
|
+
width=width,
|
|
410
|
+
height=48,
|
|
411
|
+
justify="center",
|
|
412
|
+
border_width=1,
|
|
413
|
+
border_color="#124463",
|
|
414
|
+
corner_radius=1, #角丸
|
|
415
|
+
)
|
|
416
|
+
entry.place(x=x, y=y + _labnel_height)
|
|
417
|
+
entry.insert(0, entry_text)
|
|
418
|
+
# 読み取り専用に設定
|
|
419
|
+
entry.configure(state="disabled")
|
|
420
|
+
# ホバーイベント設定
|
|
421
|
+
def on_enter(event):
|
|
422
|
+
tooltip_text = self._entry_tooltips.get(entry)
|
|
423
|
+
if tooltip_text:
|
|
424
|
+
self._show_tooltip(entry, tooltip_text)
|
|
425
|
+
#マウスホバー解除処理
|
|
426
|
+
def on_leave(event):
|
|
427
|
+
self._hide_tooltip()
|
|
428
|
+
#ホバーイベントのバインド
|
|
429
|
+
entry.bind("<Enter>", on_enter)
|
|
430
|
+
entry.bind("<Leave>", on_leave)
|
|
431
|
+
# ペアオブジェクト生成
|
|
432
|
+
pair = TextInputPair(label, entry)
|
|
433
|
+
pair._ui_base = self
|
|
434
|
+
|
|
435
|
+
return pair
|
|
436
|
+
#========================================================
|
|
437
|
+
# ログ表示ボックス作成
|
|
438
|
+
def make_LogTerminal(self,
|
|
439
|
+
x: int, y: int, # 左上座標
|
|
440
|
+
width: int = 300, # 幅
|
|
441
|
+
initial_lines: int = 2, # 初期表示行数
|
|
442
|
+
) -> Optional[LogViewBox]:
|
|
443
|
+
"""ログ表示用テキストボックスを生成"""
|
|
444
|
+
if not self._tkRoot:
|
|
445
|
+
return None
|
|
446
|
+
# CTkTextbox生成
|
|
447
|
+
textbox = ctk.CTkTextbox(
|
|
448
|
+
self._tkRoot,
|
|
449
|
+
width=width,
|
|
450
|
+
height=initial_lines * 20 + 10,
|
|
451
|
+
font=ctk.CTkFont("Courier", size=10),
|
|
452
|
+
border_width=1,
|
|
453
|
+
border_color="#000000",
|
|
454
|
+
corner_radius=4,
|
|
455
|
+
)
|
|
456
|
+
textbox.place(x=x, y=y)
|
|
457
|
+
textbox.configure(state="disabled") # 読み取り専用
|
|
458
|
+
|
|
459
|
+
# ログビューボックス生成
|
|
460
|
+
log_view = LogViewBox(textbox, x, y, width, initial_lines)
|
|
461
|
+
log_view.parent = self
|
|
462
|
+
|
|
463
|
+
return log_view
|
|
464
|
+
#========================================================
|
|
465
|
+
#UIに使う画像の登録
|
|
466
|
+
def register_image(self, key: str, filename: str, size: tuple[int, int] = (32, 24)) -> bool:
|
|
467
|
+
"""imageフォルダ配下の画像をキーで登録する(主用途: png)
|
|
468
|
+
size: 画像サイズ(幅, 高さ)デフォルト(32, 24)でボタン内にフィッティング
|
|
469
|
+
"""
|
|
470
|
+
try:
|
|
471
|
+
image_file = self._image_dir.joinpath(filename)
|
|
472
|
+
if not image_file.is_file():
|
|
473
|
+
return False
|
|
474
|
+
pil_image = Image.open(BytesIO(image_file.read_bytes()))
|
|
475
|
+
self._images[key] = ctk.CTkImage(light_image=pil_image, dark_image=pil_image, size=size)
|
|
476
|
+
return True
|
|
477
|
+
except Exception:
|
|
478
|
+
return False
|
|
479
|
+
|
|
480
|
+
def get_image(self, key: str) -> Optional[ctk.CTkImage]:
|
|
481
|
+
return self._images.get(key)
|
|
482
|
+
|
|
483
|
+
def _register_common_images(self):
|
|
484
|
+
"""baseクラスで汎用利用するpngを事前登録する。"""
|
|
485
|
+
try:
|
|
486
|
+
for image_path in self._image_dir.iterdir():
|
|
487
|
+
if image_path.name.endswith('.png'):
|
|
488
|
+
# 拡張子なしファイル名をキーとして登録(例: setting.dio.png -> setting.dio)
|
|
489
|
+
self.register_image(image_path.name[:-4], image_path.name)
|
|
490
|
+
except Exception:
|
|
491
|
+
# イメージディレクトリが存在しない場合は何もしない
|
|
492
|
+
pass
|
|
493
|
+
|
|
494
|
+
# アイコンリスト(icoファイルのアイコンはTkinterで直接扱えないため、ここではpngを想定)
|
|
495
|
+
@staticmethod
|
|
496
|
+
def icon_list() -> list[str]:
|
|
497
|
+
"""imageフォルダ内のpngファイル名のリストを返す"""
|
|
498
|
+
try:
|
|
499
|
+
image_dir = files('ikkVisualKit').joinpath('image')
|
|
500
|
+
return [p.name[:-4] for p in image_dir.iterdir() if p.name.endswith('.png')]
|
|
501
|
+
except Exception:
|
|
502
|
+
return []
|
|
503
|
+
#-------------------------------------------------------------------
|
|
504
|
+
#ツールチップ表示
|
|
505
|
+
#-------------------------------------------------------------------
|
|
506
|
+
def _show_tooltip(self, widget, text):
|
|
507
|
+
self._hide_tooltip()
|
|
508
|
+
if not self._tkRoot:
|
|
509
|
+
return
|
|
510
|
+
#ウィジェットの位置を取得してツールチップを表示
|
|
511
|
+
x = widget.winfo_rootx() + (widget.winfo_width() /2)+4
|
|
512
|
+
y = widget.winfo_rooty() - (widget.winfo_height() /2)-4
|
|
513
|
+
#
|
|
514
|
+
tooltip = tk.Toplevel(self._tkRoot)
|
|
515
|
+
tooltip.wm_overrideredirect(True)
|
|
516
|
+
tooltip.wm_geometry(f"+{int(x)}+{int(y)}")
|
|
517
|
+
tooltip.configure(bg="#CCCCCC")
|
|
518
|
+
tooltip.attributes("-alpha", 0.7)#半透明
|
|
519
|
+
#ツールチップとして表示させるラベルの設定
|
|
520
|
+
label = tk.Label(
|
|
521
|
+
tooltip,
|
|
522
|
+
text=text,
|
|
523
|
+
justify="left",
|
|
524
|
+
bg="#CCCCCC",
|
|
525
|
+
fg="black",
|
|
526
|
+
relief="solid",
|
|
527
|
+
borderwidth=1,
|
|
528
|
+
padx=8,
|
|
529
|
+
pady=4,
|
|
530
|
+
font=("Meiryo", 10),
|
|
531
|
+
)
|
|
532
|
+
label.pack()
|
|
533
|
+
self._tooltip = tooltip
|
|
534
|
+
#ツールチップ非表示
|
|
535
|
+
def _hide_tooltip(self):
|
|
536
|
+
if self._tooltip is not None:
|
|
537
|
+
self._tooltip.destroy()
|
|
538
|
+
self._tooltip = None
|
|
539
|
+
|
|
540
|
+
#####################################################################
|
|
541
|
+
#
|
|
542
|
+
#####################################################################
|
|
543
|
+
if __name__ == "__main__":
|
|
544
|
+
window = CTkUiBase(title="テスト", size=(400, 200,), icon_path="../../icon.ico")
|
|
545
|
+
window.show_window()
|
|
546
|
+
|
|
547
|
+
# 2秒後に閉じる
|
|
548
|
+
def _close_later():
|
|
549
|
+
if window._tkRoot:
|
|
550
|
+
window._tkRoot.after(2000, window.close)
|
|
551
|
+
|
|
552
|
+
window.invoke_ui(_close_later)
|
|
553
|
+
|
|
554
|
+
window.run()
|
|
555
|
+
print("ウィンドウを閉じました")
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
# DialogBase.py
|
|
2
|
+
from enum import Enum
|
|
3
|
+
import customtkinter as tk
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class CdialogResult(Enum):
|
|
7
|
+
NONE = 0
|
|
8
|
+
OK = 1
|
|
9
|
+
CANCEL = 2
|
|
10
|
+
CLOSED = 3
|
|
11
|
+
YES = 4
|
|
12
|
+
NO = 5
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class CdialogIcon(Enum):
|
|
16
|
+
INFO = "info"
|
|
17
|
+
WARNING = "warning"
|
|
18
|
+
ERROR = "error"
|
|
19
|
+
|
|
20
|
+
class CdialogBase:
|
|
21
|
+
"""
|
|
22
|
+
Toplevel ベースのモーダルダイアログ基底クラス
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
def __init__(self, parent: tk.CTk, title="Dialog", size=(400, 200)):
|
|
26
|
+
self._parent = parent
|
|
27
|
+
self._result = CdialogResult.NONE
|
|
28
|
+
self._size = size
|
|
29
|
+
|
|
30
|
+
self._tkRoot = tk.CTkToplevel(parent)
|
|
31
|
+
self._tkRoot.title(title)
|
|
32
|
+
self._tkRoot.geometry(f"{size[0]}x{size[1]}")
|
|
33
|
+
self._tkRoot.protocol("WM_DELETE_WINDOW", self._on_window_close)
|
|
34
|
+
|
|
35
|
+
self._tkRoot.transient(parent)
|
|
36
|
+
self._tkRoot.resizable(False, False)
|
|
37
|
+
self._tkRoot.withdraw()
|
|
38
|
+
|
|
39
|
+
self._build(self._tkRoot)
|
|
40
|
+
|
|
41
|
+
# ---------------------------------
|
|
42
|
+
def _build(self, root: tk.CTkToplevel):
|
|
43
|
+
pass
|
|
44
|
+
|
|
45
|
+
# ---------------------------------
|
|
46
|
+
def _on_window_close(self):
|
|
47
|
+
self._result = CdialogResult.CLOSED
|
|
48
|
+
self._tkRoot.destroy()
|
|
49
|
+
|
|
50
|
+
# ---------------------------------
|
|
51
|
+
def _action_handle(self, result: CdialogResult):
|
|
52
|
+
self._result = result
|
|
53
|
+
self._tkRoot.destroy()
|
|
54
|
+
|
|
55
|
+
# ---------------------------------
|
|
56
|
+
def show(self) -> CdialogResult:
|
|
57
|
+
self._result = CdialogResult.NONE
|
|
58
|
+
|
|
59
|
+
self._center_to_parent()
|
|
60
|
+
|
|
61
|
+
self._tkRoot.deiconify()
|
|
62
|
+
self._tkRoot.lift()
|
|
63
|
+
self._tkRoot.grab_set()
|
|
64
|
+
self._tkRoot.focus_set()
|
|
65
|
+
self._parent.wait_window(self._tkRoot)
|
|
66
|
+
|
|
67
|
+
return self._result
|
|
68
|
+
|
|
69
|
+
# ---------------------------------
|
|
70
|
+
def _center_to_parent(self):
|
|
71
|
+
self._tkRoot.update_idletasks()
|
|
72
|
+
#画面サイズを取得
|
|
73
|
+
screen_width = self._tkRoot.winfo_screenwidth()
|
|
74
|
+
screen_height = self._tkRoot.winfo_screenheight()
|
|
75
|
+
#ダイアログのサイズ
|
|
76
|
+
w = self._size[0]
|
|
77
|
+
h = self._size[1]
|
|
78
|
+
#画面の中央にダイアログを配置
|
|
79
|
+
x = (screen_width - w) // 2
|
|
80
|
+
y = (screen_height - h) // 2
|
|
81
|
+
#ダイアログのサイズと位置を設定
|
|
82
|
+
self._tkRoot.geometry(f"{w}x{h}+{x}+{y}")
|
|
83
|
+
|
|
84
|
+
#####################################################################
|
|
85
|
+
# 使用例(単体テスト)
|
|
86
|
+
#####################################################################
|
|
87
|
+
if __name__ == "__main__":
|
|
88
|
+
root = tk.CTk()
|
|
89
|
+
root.withdraw()
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
dlg = CdialogBase(root, title="Dialog Example", size=(400, 200))
|
|
93
|
+
result = dlg.show()
|
|
94
|
+
print(f"Dialog result: {result}")
|
|
95
|
+
|
|
96
|
+
root.destroy()
|