easypyui 2.2.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.
- easypyui/__init__.py +105 -0
- easypyui/animation.py +122 -0
- easypyui/core.py +1192 -0
- easypyui/modern.py +358 -0
- easypyui-2.2.0.dist-info/METADATA +7 -0
- easypyui-2.2.0.dist-info/RECORD +8 -0
- easypyui-2.2.0.dist-info/WHEEL +5 -0
- easypyui-2.2.0.dist-info/top_level.txt +1 -0
easypyui/core.py
ADDED
|
@@ -0,0 +1,1192 @@
|
|
|
1
|
+
import tkinter as tk
|
|
2
|
+
from tkinter import ttk, messagebox, filedialog, colorchooser, simpledialog
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class Value:
|
|
6
|
+
def __init__(self, variable, widget=None):
|
|
7
|
+
self.var = variable
|
|
8
|
+
self.widget = widget
|
|
9
|
+
self.tk = widget
|
|
10
|
+
|
|
11
|
+
def get(self):
|
|
12
|
+
return self.var.get()
|
|
13
|
+
|
|
14
|
+
def set(self, value):
|
|
15
|
+
self.var.set(value)
|
|
16
|
+
return self
|
|
17
|
+
|
|
18
|
+
def __str__(self):
|
|
19
|
+
return str(self.get())
|
|
20
|
+
|
|
21
|
+
def __bool__(self):
|
|
22
|
+
return bool(self.get())
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class Widget:
|
|
26
|
+
def __init__(self, widget):
|
|
27
|
+
self.widget = widget
|
|
28
|
+
self.tk = widget
|
|
29
|
+
|
|
30
|
+
def get(self):
|
|
31
|
+
w = self.widget
|
|
32
|
+
if isinstance(w, tk.Text):
|
|
33
|
+
return w.get("1.0", "end-1c")
|
|
34
|
+
if isinstance(w, tk.Listbox):
|
|
35
|
+
sel = w.curselection()
|
|
36
|
+
return w.get(sel[0]) if sel else None
|
|
37
|
+
try:
|
|
38
|
+
return w.get()
|
|
39
|
+
except Exception:
|
|
40
|
+
return None
|
|
41
|
+
|
|
42
|
+
def set(self, value):
|
|
43
|
+
w = self.widget
|
|
44
|
+
if isinstance(w, tk.Text):
|
|
45
|
+
w.delete("1.0", "end")
|
|
46
|
+
w.insert("1.0", value)
|
|
47
|
+
elif isinstance(w, tk.Listbox):
|
|
48
|
+
w.delete(0, "end")
|
|
49
|
+
if isinstance(value, (list, tuple)):
|
|
50
|
+
for item in value:
|
|
51
|
+
w.insert("end", item)
|
|
52
|
+
else:
|
|
53
|
+
w.insert("end", value)
|
|
54
|
+
else:
|
|
55
|
+
try:
|
|
56
|
+
w.delete(0, "end")
|
|
57
|
+
w.insert(0, value)
|
|
58
|
+
except Exception:
|
|
59
|
+
try:
|
|
60
|
+
w.configure(text=value)
|
|
61
|
+
except Exception:
|
|
62
|
+
pass
|
|
63
|
+
return self
|
|
64
|
+
|
|
65
|
+
def text(self, value=None):
|
|
66
|
+
if value is None:
|
|
67
|
+
try:
|
|
68
|
+
return self.widget.cget("text")
|
|
69
|
+
except Exception:
|
|
70
|
+
return None
|
|
71
|
+
self.widget.configure(text=value)
|
|
72
|
+
return self
|
|
73
|
+
|
|
74
|
+
def config(self, **kwargs):
|
|
75
|
+
self.widget.configure(**kwargs)
|
|
76
|
+
return self
|
|
77
|
+
|
|
78
|
+
configure = config
|
|
79
|
+
|
|
80
|
+
def enable(self):
|
|
81
|
+
try:
|
|
82
|
+
self.widget.configure(state="normal")
|
|
83
|
+
except Exception:
|
|
84
|
+
pass
|
|
85
|
+
return self
|
|
86
|
+
|
|
87
|
+
def disable(self):
|
|
88
|
+
try:
|
|
89
|
+
self.widget.configure(state="disabled")
|
|
90
|
+
except Exception:
|
|
91
|
+
pass
|
|
92
|
+
return self
|
|
93
|
+
|
|
94
|
+
def focus(self):
|
|
95
|
+
self.widget.focus_set()
|
|
96
|
+
return self
|
|
97
|
+
|
|
98
|
+
def destroy(self):
|
|
99
|
+
self.widget.destroy()
|
|
100
|
+
|
|
101
|
+
def hide(self):
|
|
102
|
+
manager = self.widget.winfo_manager()
|
|
103
|
+
if manager == "grid":
|
|
104
|
+
self.widget.grid_remove()
|
|
105
|
+
elif manager == "place":
|
|
106
|
+
self.widget.place_forget()
|
|
107
|
+
else:
|
|
108
|
+
self.widget.pack_forget()
|
|
109
|
+
return self
|
|
110
|
+
|
|
111
|
+
def show(self, **kwargs):
|
|
112
|
+
self.widget.pack(**kwargs)
|
|
113
|
+
return self
|
|
114
|
+
|
|
115
|
+
def pack(self, **kwargs):
|
|
116
|
+
self.widget.pack(**kwargs)
|
|
117
|
+
return self
|
|
118
|
+
|
|
119
|
+
def grid(self, row=0, column=0, **kwargs):
|
|
120
|
+
self.widget.grid(row=row, column=column, **kwargs)
|
|
121
|
+
return self
|
|
122
|
+
|
|
123
|
+
def place(self, x=None, y=None, **kwargs):
|
|
124
|
+
if x is not None:
|
|
125
|
+
kwargs["x"] = x
|
|
126
|
+
if y is not None:
|
|
127
|
+
kwargs["y"] = y
|
|
128
|
+
self.widget.place(**kwargs)
|
|
129
|
+
return self
|
|
130
|
+
|
|
131
|
+
def on(self, event, func, add=None):
|
|
132
|
+
self.widget.bind(event, func, add)
|
|
133
|
+
return self
|
|
134
|
+
|
|
135
|
+
def on_click(self, func, button=1):
|
|
136
|
+
self.widget.bind(f"<Button-{button}>", func)
|
|
137
|
+
return self
|
|
138
|
+
|
|
139
|
+
def tooltip(self, text, delay=400):
|
|
140
|
+
Tooltip(self.widget, text, delay)
|
|
141
|
+
return self
|
|
142
|
+
|
|
143
|
+
def context_menu(self, items):
|
|
144
|
+
attach_context_menu(self.widget, items)
|
|
145
|
+
return self
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
class Table(Widget):
|
|
149
|
+
def add(self, values, parent="", index="end", iid=None):
|
|
150
|
+
return self.widget.insert(parent, index, iid=iid, values=values)
|
|
151
|
+
|
|
152
|
+
insert = add
|
|
153
|
+
|
|
154
|
+
def selected_id(self):
|
|
155
|
+
sel = self.widget.selection()
|
|
156
|
+
return sel[0] if sel else None
|
|
157
|
+
|
|
158
|
+
def selected(self):
|
|
159
|
+
iid = self.selected_id()
|
|
160
|
+
return self.widget.item(iid, "values") if iid else None
|
|
161
|
+
|
|
162
|
+
def update(self, iid=None, values=None, **kwargs):
|
|
163
|
+
iid = iid or self.selected_id()
|
|
164
|
+
if iid:
|
|
165
|
+
if values is not None:
|
|
166
|
+
kwargs["values"] = values
|
|
167
|
+
self.widget.item(iid, **kwargs)
|
|
168
|
+
return self
|
|
169
|
+
|
|
170
|
+
def delete(self, iid=None):
|
|
171
|
+
iid = iid or self.selected_id()
|
|
172
|
+
if iid:
|
|
173
|
+
self.widget.delete(iid)
|
|
174
|
+
return self
|
|
175
|
+
|
|
176
|
+
def clear(self):
|
|
177
|
+
for iid in self.widget.get_children():
|
|
178
|
+
self.widget.delete(iid)
|
|
179
|
+
return self
|
|
180
|
+
|
|
181
|
+
def rows(self):
|
|
182
|
+
return [self.widget.item(iid, "values") for iid in self.widget.get_children()]
|
|
183
|
+
|
|
184
|
+
def heading(self, column, text=None, command=None):
|
|
185
|
+
opts = {}
|
|
186
|
+
if text is not None:
|
|
187
|
+
opts["text"] = text
|
|
188
|
+
if command is not None:
|
|
189
|
+
opts["command"] = command
|
|
190
|
+
self.widget.heading(column, **opts)
|
|
191
|
+
return self
|
|
192
|
+
|
|
193
|
+
def column(self, column, **kwargs):
|
|
194
|
+
self.widget.column(column, **kwargs)
|
|
195
|
+
return self
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
class Canvas(Widget):
|
|
199
|
+
def line(self, x1, y1, x2, y2, **kwargs):
|
|
200
|
+
return self.widget.create_line(x1, y1, x2, y2, **kwargs)
|
|
201
|
+
|
|
202
|
+
def rect(self, x1, y1, x2, y2, **kwargs):
|
|
203
|
+
return self.widget.create_rectangle(x1, y1, x2, y2, **kwargs)
|
|
204
|
+
|
|
205
|
+
rectangle = rect
|
|
206
|
+
|
|
207
|
+
def oval(self, x1, y1, x2, y2, **kwargs):
|
|
208
|
+
return self.widget.create_oval(x1, y1, x2, y2, **kwargs)
|
|
209
|
+
|
|
210
|
+
def text_at(self, x, y, text, **kwargs):
|
|
211
|
+
return self.widget.create_text(x, y, text=text, **kwargs)
|
|
212
|
+
|
|
213
|
+
def polygon(self, *points, **kwargs):
|
|
214
|
+
return self.widget.create_polygon(*points, **kwargs)
|
|
215
|
+
|
|
216
|
+
def delete(self, item="all"):
|
|
217
|
+
self.widget.delete(item)
|
|
218
|
+
return self
|
|
219
|
+
|
|
220
|
+
clear = delete
|
|
221
|
+
|
|
222
|
+
def move(self, item, dx, dy):
|
|
223
|
+
self.widget.move(item, dx, dy)
|
|
224
|
+
return self
|
|
225
|
+
|
|
226
|
+
def coords(self, item, *coords):
|
|
227
|
+
if coords:
|
|
228
|
+
self.widget.coords(item, *coords)
|
|
229
|
+
return self
|
|
230
|
+
return self.widget.coords(item)
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
class Tooltip:
|
|
234
|
+
def __init__(self, widget, text, delay=400):
|
|
235
|
+
self.widget = widget
|
|
236
|
+
self.text = text
|
|
237
|
+
self.delay = delay
|
|
238
|
+
self._after = None
|
|
239
|
+
self._tip = None
|
|
240
|
+
widget.bind("<Enter>", self._schedule, add="+")
|
|
241
|
+
widget.bind("<Leave>", self._hide, add="+")
|
|
242
|
+
widget.bind("<ButtonPress>", self._hide, add="+")
|
|
243
|
+
|
|
244
|
+
def _schedule(self, _event=None):
|
|
245
|
+
self._cancel()
|
|
246
|
+
self._after = self.widget.after(self.delay, self._show)
|
|
247
|
+
|
|
248
|
+
def _cancel(self):
|
|
249
|
+
if self._after:
|
|
250
|
+
self.widget.after_cancel(self._after)
|
|
251
|
+
self._after = None
|
|
252
|
+
|
|
253
|
+
def _show(self):
|
|
254
|
+
if self._tip or not self.widget.winfo_exists():
|
|
255
|
+
return
|
|
256
|
+
x = self.widget.winfo_rootx() + 18
|
|
257
|
+
y = self.widget.winfo_rooty() + self.widget.winfo_height() + 4
|
|
258
|
+
self._tip = win = tk.Toplevel(self.widget)
|
|
259
|
+
win.wm_overrideredirect(True)
|
|
260
|
+
win.wm_geometry(f"+{x}+{y}")
|
|
261
|
+
label = tk.Label(
|
|
262
|
+
win, text=self.text, justify="left",
|
|
263
|
+
relief="solid", borderwidth=1, padx=6, pady=3
|
|
264
|
+
)
|
|
265
|
+
label.pack()
|
|
266
|
+
|
|
267
|
+
def _hide(self, _event=None):
|
|
268
|
+
self._cancel()
|
|
269
|
+
if self._tip:
|
|
270
|
+
self._tip.destroy()
|
|
271
|
+
self._tip = None
|
|
272
|
+
|
|
273
|
+
|
|
274
|
+
def attach_context_menu(widget, items):
|
|
275
|
+
menu = tk.Menu(widget, tearoff=0)
|
|
276
|
+
for item in items:
|
|
277
|
+
if item is None:
|
|
278
|
+
menu.add_separator()
|
|
279
|
+
elif len(item) == 2:
|
|
280
|
+
menu.add_command(label=item[0], command=item[1])
|
|
281
|
+
else:
|
|
282
|
+
menu.add_command(label=item[0], command=item[1], state=item[2])
|
|
283
|
+
|
|
284
|
+
def popup(event):
|
|
285
|
+
menu.tk_popup(event.x_root, event.y_root)
|
|
286
|
+
menu.grab_release()
|
|
287
|
+
|
|
288
|
+
widget.bind("<Button-3>", popup, add="+")
|
|
289
|
+
return menu
|
|
290
|
+
|
|
291
|
+
|
|
292
|
+
class _Builder:
|
|
293
|
+
def __init__(self, app, parent):
|
|
294
|
+
self.app = app
|
|
295
|
+
self._parent = parent
|
|
296
|
+
self.tk = parent
|
|
297
|
+
|
|
298
|
+
def _wrap_and_layout(self, widget, layout="pack", **layout_opts):
|
|
299
|
+
wrapped = Widget(widget)
|
|
300
|
+
if layout == "none":
|
|
301
|
+
return wrapped
|
|
302
|
+
if layout == "grid":
|
|
303
|
+
widget.grid(**layout_opts)
|
|
304
|
+
elif layout == "place":
|
|
305
|
+
widget.place(**layout_opts)
|
|
306
|
+
else:
|
|
307
|
+
defaults = {"fill": "x", "pady": 4}
|
|
308
|
+
defaults.update(layout_opts)
|
|
309
|
+
widget.pack(**defaults)
|
|
310
|
+
return wrapped
|
|
311
|
+
|
|
312
|
+
def text(self, text="", size=12, bold=False, layout="pack", **kwargs):
|
|
313
|
+
font_family = kwargs.pop("font_family", "Arial")
|
|
314
|
+
font = kwargs.pop("font", (font_family, size, "bold" if bold else "normal"))
|
|
315
|
+
layout_opts = kwargs.pop("layout_opts", {})
|
|
316
|
+
return self._wrap_and_layout(
|
|
317
|
+
ttk.Label(self._parent, text=text, font=font, **kwargs),
|
|
318
|
+
layout, **layout_opts
|
|
319
|
+
)
|
|
320
|
+
|
|
321
|
+
def button(self, text, action=None, layout="pack", **kwargs):
|
|
322
|
+
layout_opts = kwargs.pop("layout_opts", {})
|
|
323
|
+
return self._wrap_and_layout(
|
|
324
|
+
ttk.Button(self._parent, text=text, command=action, **kwargs),
|
|
325
|
+
layout, **layout_opts
|
|
326
|
+
)
|
|
327
|
+
|
|
328
|
+
def input(self, label=None, password=False, default="", layout="pack", **kwargs):
|
|
329
|
+
if label:
|
|
330
|
+
self.text(label, size=10)
|
|
331
|
+
layout_opts = kwargs.pop("layout_opts", {})
|
|
332
|
+
v = tk.StringVar(value=default)
|
|
333
|
+
w = ttk.Entry(self._parent, textvariable=v, show="*" if password else "", **kwargs)
|
|
334
|
+
wrapped = self._wrap_and_layout(w, layout, **layout_opts)
|
|
335
|
+
return Value(v, wrapped.tk)
|
|
336
|
+
|
|
337
|
+
def password(self, label="密碼", **kwargs):
|
|
338
|
+
return self.input(label=label, password=True, **kwargs)
|
|
339
|
+
|
|
340
|
+
def textbox(self, height=6, width=None, scroll=True, layout="pack", **kwargs):
|
|
341
|
+
layout_opts = kwargs.pop("layout_opts", {})
|
|
342
|
+
if scroll:
|
|
343
|
+
frame = ttk.Frame(self._parent)
|
|
344
|
+
text = tk.Text(frame, height=height, width=width, **kwargs)
|
|
345
|
+
sy = ttk.Scrollbar(frame, orient="vertical", command=text.yview)
|
|
346
|
+
text.configure(yscrollcommand=sy.set)
|
|
347
|
+
text.pack(side="left", fill="both", expand=True)
|
|
348
|
+
sy.pack(side="right", fill="y")
|
|
349
|
+
if layout == "grid":
|
|
350
|
+
frame.grid(**layout_opts)
|
|
351
|
+
elif layout == "place":
|
|
352
|
+
frame.place(**layout_opts)
|
|
353
|
+
else:
|
|
354
|
+
defaults = {"fill": "both", "expand": True, "pady": 4}
|
|
355
|
+
defaults.update(layout_opts)
|
|
356
|
+
frame.pack(**defaults)
|
|
357
|
+
return Widget(text)
|
|
358
|
+
w = tk.Text(self._parent, height=height, width=width, **kwargs)
|
|
359
|
+
return self._wrap_and_layout(w, layout, **layout_opts)
|
|
360
|
+
|
|
361
|
+
def check(self, text, checked=False, action=None, layout="pack", **kwargs):
|
|
362
|
+
layout_opts = kwargs.pop("layout_opts", {})
|
|
363
|
+
v = tk.BooleanVar(value=checked)
|
|
364
|
+
w = ttk.Checkbutton(self._parent, text=text, variable=v, command=action, **kwargs)
|
|
365
|
+
self._wrap_and_layout(w, layout, **layout_opts)
|
|
366
|
+
return Value(v, w)
|
|
367
|
+
|
|
368
|
+
checkbox = check
|
|
369
|
+
|
|
370
|
+
def radio(self, options, default=None, horizontal=False):
|
|
371
|
+
options = list(options)
|
|
372
|
+
v = tk.StringVar(value=default if default is not None else (str(options[0]) if options else ""))
|
|
373
|
+
holder = ttk.Frame(self._parent)
|
|
374
|
+
holder.pack(fill="x", pady=4)
|
|
375
|
+
for item in options:
|
|
376
|
+
w = ttk.Radiobutton(holder, text=str(item), value=str(item), variable=v)
|
|
377
|
+
w.pack(side="left" if horizontal else "top", anchor="w", padx=3, pady=2)
|
|
378
|
+
return Value(v, holder)
|
|
379
|
+
|
|
380
|
+
def select(self, items, default=None, readonly=True, layout="pack", **kwargs):
|
|
381
|
+
items = list(items)
|
|
382
|
+
v = tk.StringVar(value=default if default is not None else (items[0] if items else ""))
|
|
383
|
+
layout_opts = kwargs.pop("layout_opts", {})
|
|
384
|
+
w = ttk.Combobox(
|
|
385
|
+
self._parent, values=items, textvariable=v,
|
|
386
|
+
state="readonly" if readonly else "normal", **kwargs
|
|
387
|
+
)
|
|
388
|
+
self._wrap_and_layout(w, layout, **layout_opts)
|
|
389
|
+
return Value(v, w)
|
|
390
|
+
|
|
391
|
+
def slider(self, start=0, end=100, default=0, action=None, layout="pack", **kwargs):
|
|
392
|
+
v = tk.DoubleVar(value=default)
|
|
393
|
+
layout_opts = kwargs.pop("layout_opts", {})
|
|
394
|
+
w = ttk.Scale(self._parent, from_=start, to=end, variable=v, command=action, **kwargs)
|
|
395
|
+
self._wrap_and_layout(w, layout, **layout_opts)
|
|
396
|
+
return Value(v, w)
|
|
397
|
+
|
|
398
|
+
def spin(self, start=0, end=100, default=0, layout="pack", **kwargs):
|
|
399
|
+
v = tk.IntVar(value=default)
|
|
400
|
+
layout_opts = kwargs.pop("layout_opts", {})
|
|
401
|
+
w = ttk.Spinbox(self._parent, from_=start, to=end, textvariable=v, **kwargs)
|
|
402
|
+
self._wrap_and_layout(w, layout, **layout_opts)
|
|
403
|
+
return Value(v, w)
|
|
404
|
+
|
|
405
|
+
def progress(self, value=0, maximum=100, mode="determinate", layout="pack", **kwargs):
|
|
406
|
+
v = tk.DoubleVar(value=value)
|
|
407
|
+
layout_opts = kwargs.pop("layout_opts", {})
|
|
408
|
+
w = ttk.Progressbar(self._parent, maximum=maximum, variable=v, mode=mode, **kwargs)
|
|
409
|
+
self._wrap_and_layout(w, layout, **layout_opts)
|
|
410
|
+
return Value(v, w)
|
|
411
|
+
|
|
412
|
+
def listbox(self, items=(), height=6, scroll=True, layout="pack", **kwargs):
|
|
413
|
+
layout_opts = kwargs.pop("layout_opts", {})
|
|
414
|
+
if scroll:
|
|
415
|
+
frame = ttk.Frame(self._parent)
|
|
416
|
+
w = tk.Listbox(frame, height=height, **kwargs)
|
|
417
|
+
sy = ttk.Scrollbar(frame, orient="vertical", command=w.yview)
|
|
418
|
+
w.configure(yscrollcommand=sy.set)
|
|
419
|
+
w.pack(side="left", fill="both", expand=True)
|
|
420
|
+
sy.pack(side="right", fill="y")
|
|
421
|
+
if layout == "grid":
|
|
422
|
+
frame.grid(**layout_opts)
|
|
423
|
+
elif layout == "place":
|
|
424
|
+
frame.place(**layout_opts)
|
|
425
|
+
else:
|
|
426
|
+
defaults = {"fill": "both", "expand": True, "pady": 4}
|
|
427
|
+
defaults.update(layout_opts)
|
|
428
|
+
frame.pack(**defaults)
|
|
429
|
+
else:
|
|
430
|
+
w = tk.Listbox(self._parent, height=height, **kwargs)
|
|
431
|
+
self._wrap_and_layout(w, layout, **layout_opts)
|
|
432
|
+
for x in items:
|
|
433
|
+
w.insert("end", x)
|
|
434
|
+
return Widget(w)
|
|
435
|
+
|
|
436
|
+
def table(self, columns, rows=(), headings=None, height=8, scroll=True, layout="pack", **kwargs):
|
|
437
|
+
cols = list(columns)
|
|
438
|
+
headings = headings or cols
|
|
439
|
+
layout_opts = kwargs.pop("layout_opts", {})
|
|
440
|
+
|
|
441
|
+
holder = ttk.Frame(self._parent) if scroll else self._parent
|
|
442
|
+
w = ttk.Treeview(holder, columns=cols, show="headings", height=height, **kwargs)
|
|
443
|
+
for c, h in zip(cols, headings):
|
|
444
|
+
w.heading(c, text=h)
|
|
445
|
+
w.column(c, anchor="w")
|
|
446
|
+
for row in rows:
|
|
447
|
+
w.insert("", "end", values=row)
|
|
448
|
+
|
|
449
|
+
if scroll:
|
|
450
|
+
sy = ttk.Scrollbar(holder, orient="vertical", command=w.yview)
|
|
451
|
+
sx = ttk.Scrollbar(holder, orient="horizontal", command=w.xview)
|
|
452
|
+
w.configure(yscrollcommand=sy.set, xscrollcommand=sx.set)
|
|
453
|
+
w.grid(row=0, column=0, sticky="nsew")
|
|
454
|
+
sy.grid(row=0, column=1, sticky="ns")
|
|
455
|
+
sx.grid(row=1, column=0, sticky="ew")
|
|
456
|
+
holder.rowconfigure(0, weight=1)
|
|
457
|
+
holder.columnconfigure(0, weight=1)
|
|
458
|
+
if layout == "grid":
|
|
459
|
+
holder.grid(**layout_opts)
|
|
460
|
+
elif layout == "place":
|
|
461
|
+
holder.place(**layout_opts)
|
|
462
|
+
else:
|
|
463
|
+
defaults = {"fill": "both", "expand": True, "pady": 4}
|
|
464
|
+
defaults.update(layout_opts)
|
|
465
|
+
holder.pack(**defaults)
|
|
466
|
+
else:
|
|
467
|
+
self._wrap_and_layout(w, layout, **layout_opts)
|
|
468
|
+
|
|
469
|
+
return Table(w)
|
|
470
|
+
|
|
471
|
+
def canvas(self, width=400, height=250, scroll=False, layout="pack", **kwargs):
|
|
472
|
+
layout_opts = kwargs.pop("layout_opts", {})
|
|
473
|
+
if scroll:
|
|
474
|
+
holder = ttk.Frame(self._parent)
|
|
475
|
+
w = tk.Canvas(holder, width=width, height=height, **kwargs)
|
|
476
|
+
sy = ttk.Scrollbar(holder, orient="vertical", command=w.yview)
|
|
477
|
+
sx = ttk.Scrollbar(holder, orient="horizontal", command=w.xview)
|
|
478
|
+
w.configure(yscrollcommand=sy.set, xscrollcommand=sx.set)
|
|
479
|
+
w.grid(row=0, column=0, sticky="nsew")
|
|
480
|
+
sy.grid(row=0, column=1, sticky="ns")
|
|
481
|
+
sx.grid(row=1, column=0, sticky="ew")
|
|
482
|
+
holder.rowconfigure(0, weight=1)
|
|
483
|
+
holder.columnconfigure(0, weight=1)
|
|
484
|
+
if layout == "grid":
|
|
485
|
+
holder.grid(**layout_opts)
|
|
486
|
+
elif layout == "place":
|
|
487
|
+
holder.place(**layout_opts)
|
|
488
|
+
else:
|
|
489
|
+
defaults = {"fill": "both", "expand": True, "pady": 4}
|
|
490
|
+
defaults.update(layout_opts)
|
|
491
|
+
holder.pack(**defaults)
|
|
492
|
+
else:
|
|
493
|
+
w = tk.Canvas(self._parent, width=width, height=height, **kwargs)
|
|
494
|
+
self._wrap_and_layout(w, layout, **layout_opts)
|
|
495
|
+
return Canvas(w)
|
|
496
|
+
|
|
497
|
+
def image(self, path, layout="pack", **layout_opts):
|
|
498
|
+
img = tk.PhotoImage(file=path)
|
|
499
|
+
w = ttk.Label(self._parent, image=img)
|
|
500
|
+
w.image = img
|
|
501
|
+
return self._wrap_and_layout(w, layout, **layout_opts)
|
|
502
|
+
|
|
503
|
+
def separator(self, orient="horizontal"):
|
|
504
|
+
return self._wrap_and_layout(ttk.Separator(self._parent, orient=orient))
|
|
505
|
+
|
|
506
|
+
def group(self, title="", **kwargs):
|
|
507
|
+
frame = ttk.LabelFrame(self._parent, text=title, padding=kwargs.pop("padding", 8))
|
|
508
|
+
frame.pack(fill=kwargs.pop("fill", "both"), expand=kwargs.pop("expand", False),
|
|
509
|
+
padx=kwargs.pop("padx", 0), pady=kwargs.pop("pady", 4))
|
|
510
|
+
return Container(self.app, frame)
|
|
511
|
+
|
|
512
|
+
def row(self, **kwargs):
|
|
513
|
+
frame = ttk.Frame(self._parent)
|
|
514
|
+
frame.pack(fill=kwargs.pop("fill", "x"), expand=kwargs.pop("expand", False),
|
|
515
|
+
padx=kwargs.pop("padx", 0), pady=kwargs.pop("pady", 3))
|
|
516
|
+
return Row(self.app, frame)
|
|
517
|
+
|
|
518
|
+
def frame(self, layout="pack", **kwargs):
|
|
519
|
+
layout_opts = kwargs.pop("layout_opts", {})
|
|
520
|
+
f = ttk.Frame(self._parent, **kwargs)
|
|
521
|
+
self._wrap_and_layout(f, layout, **layout_opts)
|
|
522
|
+
return Container(self.app, f)
|
|
523
|
+
|
|
524
|
+
def scroll_area(self, height=300):
|
|
525
|
+
outer = ttk.Frame(self._parent)
|
|
526
|
+
outer.pack(fill="both", expand=True)
|
|
527
|
+
canvas = tk.Canvas(outer, height=height, highlightthickness=0)
|
|
528
|
+
scrollbar = ttk.Scrollbar(outer, orient="vertical", command=canvas.yview)
|
|
529
|
+
content = ttk.Frame(canvas)
|
|
530
|
+
content.bind("<Configure>", lambda e: canvas.configure(scrollregion=canvas.bbox("all")))
|
|
531
|
+
window_id = canvas.create_window((0, 0), window=content, anchor="nw")
|
|
532
|
+
canvas.bind("<Configure>", lambda e: canvas.itemconfigure(window_id, width=e.width))
|
|
533
|
+
canvas.configure(yscrollcommand=scrollbar.set)
|
|
534
|
+
canvas.pack(side="left", fill="both", expand=True)
|
|
535
|
+
scrollbar.pack(side="right", fill="y")
|
|
536
|
+
return Container(self.app, content)
|
|
537
|
+
|
|
538
|
+
def context_menu(self, items):
|
|
539
|
+
return attach_context_menu(self._parent, items)
|
|
540
|
+
|
|
541
|
+
|
|
542
|
+
class Row(_Builder):
|
|
543
|
+
def _wrap_and_layout(self, widget, layout="pack", **layout_opts):
|
|
544
|
+
if layout == "none":
|
|
545
|
+
return Widget(widget)
|
|
546
|
+
if layout == "grid":
|
|
547
|
+
widget.grid(**layout_opts)
|
|
548
|
+
elif layout == "place":
|
|
549
|
+
widget.place(**layout_opts)
|
|
550
|
+
else:
|
|
551
|
+
defaults = {"side": "left", "padx": 3, "pady": 3}
|
|
552
|
+
defaults.update(layout_opts)
|
|
553
|
+
widget.pack(**defaults)
|
|
554
|
+
return Widget(widget)
|
|
555
|
+
|
|
556
|
+
|
|
557
|
+
class Container(_Builder):
|
|
558
|
+
def __init__(self, app, frame, window=None):
|
|
559
|
+
super().__init__(app, frame)
|
|
560
|
+
self.frame = frame
|
|
561
|
+
self.window = window
|
|
562
|
+
|
|
563
|
+
def close(self):
|
|
564
|
+
if self.window:
|
|
565
|
+
self.window.destroy()
|
|
566
|
+
else:
|
|
567
|
+
self.frame.destroy()
|
|
568
|
+
|
|
569
|
+
|
|
570
|
+
class App(_Builder):
|
|
571
|
+
def __init__(self, title="EasyPyUI", width=500, height=400, resizable=True, theme=None):
|
|
572
|
+
self.root = tk.Tk()
|
|
573
|
+
self.root.title(title)
|
|
574
|
+
self.root.geometry(f"{width}x{height}")
|
|
575
|
+
self.root.resizable(resizable, resizable)
|
|
576
|
+
self.style = ttk.Style(self.root)
|
|
577
|
+
if theme:
|
|
578
|
+
self.theme(theme)
|
|
579
|
+
self.body = ttk.Frame(self.root, padding=12)
|
|
580
|
+
self.body.pack(fill="both", expand=True)
|
|
581
|
+
super().__init__(self, self.body)
|
|
582
|
+
self.tk = self.root
|
|
583
|
+
|
|
584
|
+
def title(self, text):
|
|
585
|
+
self.root.title(text)
|
|
586
|
+
return self
|
|
587
|
+
|
|
588
|
+
def size(self, width, height):
|
|
589
|
+
self.root.geometry(f"{width}x{height}")
|
|
590
|
+
return self
|
|
591
|
+
|
|
592
|
+
def min_size(self, width, height):
|
|
593
|
+
self.root.minsize(width, height)
|
|
594
|
+
return self
|
|
595
|
+
|
|
596
|
+
def max_size(self, width, height):
|
|
597
|
+
self.root.maxsize(width, height)
|
|
598
|
+
return self
|
|
599
|
+
|
|
600
|
+
def position(self, x, y):
|
|
601
|
+
self.root.geometry(f"+{x}+{y}")
|
|
602
|
+
return self
|
|
603
|
+
|
|
604
|
+
def center(self):
|
|
605
|
+
self.root.update_idletasks()
|
|
606
|
+
w = self.root.winfo_width()
|
|
607
|
+
h = self.root.winfo_height()
|
|
608
|
+
sw = self.root.winfo_screenwidth()
|
|
609
|
+
sh = self.root.winfo_screenheight()
|
|
610
|
+
self.root.geometry(f"+{(sw-w)//2}+{(sh-h)//2}")
|
|
611
|
+
return self
|
|
612
|
+
|
|
613
|
+
def icon(self, path):
|
|
614
|
+
self.root.iconbitmap(path)
|
|
615
|
+
return self
|
|
616
|
+
|
|
617
|
+
def topmost(self, yes=True):
|
|
618
|
+
self.root.attributes("-topmost", yes)
|
|
619
|
+
return self
|
|
620
|
+
|
|
621
|
+
def fullscreen(self, yes=True):
|
|
622
|
+
self.root.attributes("-fullscreen", yes)
|
|
623
|
+
return self
|
|
624
|
+
|
|
625
|
+
def opacity(self, value=1.0):
|
|
626
|
+
self.root.attributes("-alpha", value)
|
|
627
|
+
return self
|
|
628
|
+
|
|
629
|
+
def resizable(self, width=True, height=True):
|
|
630
|
+
self.root.resizable(width, height)
|
|
631
|
+
return self
|
|
632
|
+
|
|
633
|
+
def theme(self, name=None):
|
|
634
|
+
if name is None:
|
|
635
|
+
return self.style.theme_use()
|
|
636
|
+
self.style.theme_use(name)
|
|
637
|
+
return self
|
|
638
|
+
|
|
639
|
+
def themes(self):
|
|
640
|
+
return self.style.theme_names()
|
|
641
|
+
|
|
642
|
+
def style_widget(self, name, **kwargs):
|
|
643
|
+
self.style.configure(name, **kwargs)
|
|
644
|
+
return self
|
|
645
|
+
|
|
646
|
+
def map_style(self, name, **kwargs):
|
|
647
|
+
self.style.map(name, **kwargs)
|
|
648
|
+
return self
|
|
649
|
+
|
|
650
|
+
def grid_config(self, rows=None, columns=None, weight=1):
|
|
651
|
+
if rows:
|
|
652
|
+
for r in rows:
|
|
653
|
+
self.body.rowconfigure(r, weight=weight)
|
|
654
|
+
if columns:
|
|
655
|
+
for c in columns:
|
|
656
|
+
self.body.columnconfigure(c, weight=weight)
|
|
657
|
+
return self
|
|
658
|
+
|
|
659
|
+
def tabs(self, names):
|
|
660
|
+
notebook = ttk.Notebook(self._parent)
|
|
661
|
+
notebook.pack(fill="both", expand=True)
|
|
662
|
+
result = {}
|
|
663
|
+
for name in names:
|
|
664
|
+
f = ttk.Frame(notebook, padding=8)
|
|
665
|
+
notebook.add(f, text=name)
|
|
666
|
+
result[name] = Container(self, f)
|
|
667
|
+
result["_notebook"] = Widget(notebook)
|
|
668
|
+
return result
|
|
669
|
+
|
|
670
|
+
def menu(self, structure):
|
|
671
|
+
bar = tk.Menu(self.root)
|
|
672
|
+
for title, items in structure.items():
|
|
673
|
+
m = tk.Menu(bar, tearoff=0)
|
|
674
|
+
for item in items:
|
|
675
|
+
if item is None:
|
|
676
|
+
m.add_separator()
|
|
677
|
+
elif isinstance(item, dict):
|
|
678
|
+
sub = tk.Menu(m, tearoff=0)
|
|
679
|
+
for label, command in item.items():
|
|
680
|
+
sub.add_command(label=label, command=command)
|
|
681
|
+
m.add_cascade(label=title, menu=sub)
|
|
682
|
+
else:
|
|
683
|
+
m.add_command(label=item[0], command=item[1])
|
|
684
|
+
bar.add_cascade(label=title, menu=m)
|
|
685
|
+
self.root.config(menu=bar)
|
|
686
|
+
return bar
|
|
687
|
+
|
|
688
|
+
def context_menu(self, items):
|
|
689
|
+
return attach_context_menu(self.root, items)
|
|
690
|
+
|
|
691
|
+
def alert(self, text, title="提示"):
|
|
692
|
+
return messagebox.showinfo(title, text, parent=self.root)
|
|
693
|
+
|
|
694
|
+
def error(self, text, title="錯誤"):
|
|
695
|
+
return messagebox.showerror(title, text, parent=self.root)
|
|
696
|
+
|
|
697
|
+
def warning(self, text, title="警告"):
|
|
698
|
+
return messagebox.showwarning(title, text, parent=self.root)
|
|
699
|
+
|
|
700
|
+
def ask(self, text, title="確認"):
|
|
701
|
+
return messagebox.askyesno(title, text, parent=self.root)
|
|
702
|
+
|
|
703
|
+
def ask_ok(self, text, title="確認"):
|
|
704
|
+
return messagebox.askokcancel(title, text, parent=self.root)
|
|
705
|
+
|
|
706
|
+
def ask_retry(self, text, title="重試"):
|
|
707
|
+
return messagebox.askretrycancel(title, text, parent=self.root)
|
|
708
|
+
|
|
709
|
+
def prompt(self, text, title="輸入", default=None):
|
|
710
|
+
return simpledialog.askstring(title, text, initialvalue=default, parent=self.root)
|
|
711
|
+
|
|
712
|
+
def prompt_int(self, text, title="輸入", default=None, min=None, max=None):
|
|
713
|
+
return simpledialog.askinteger(
|
|
714
|
+
title, text, initialvalue=default, minvalue=min, maxvalue=max, parent=self.root
|
|
715
|
+
)
|
|
716
|
+
|
|
717
|
+
def prompt_float(self, text, title="輸入", default=None, min=None, max=None):
|
|
718
|
+
return simpledialog.askfloat(
|
|
719
|
+
title, text, initialvalue=default, minvalue=min, maxvalue=max, parent=self.root
|
|
720
|
+
)
|
|
721
|
+
|
|
722
|
+
def open_file(self, **kwargs):
|
|
723
|
+
return filedialog.askopenfilename(parent=self.root, **kwargs)
|
|
724
|
+
|
|
725
|
+
def open_files(self, **kwargs):
|
|
726
|
+
return filedialog.askopenfilenames(parent=self.root, **kwargs)
|
|
727
|
+
|
|
728
|
+
def save_file(self, **kwargs):
|
|
729
|
+
return filedialog.asksaveasfilename(parent=self.root, **kwargs)
|
|
730
|
+
|
|
731
|
+
def folder(self, **kwargs):
|
|
732
|
+
return filedialog.askdirectory(parent=self.root, **kwargs)
|
|
733
|
+
|
|
734
|
+
def pick_color(self):
|
|
735
|
+
return colorchooser.askcolor(parent=self.root)[1]
|
|
736
|
+
|
|
737
|
+
def clipboard(self, text=None):
|
|
738
|
+
if text is None:
|
|
739
|
+
try:
|
|
740
|
+
return self.root.clipboard_get()
|
|
741
|
+
except tk.TclError:
|
|
742
|
+
return ""
|
|
743
|
+
self.root.clipboard_clear()
|
|
744
|
+
self.root.clipboard_append(str(text))
|
|
745
|
+
return self
|
|
746
|
+
|
|
747
|
+
def later(self, ms, func):
|
|
748
|
+
return self.root.after(ms, func)
|
|
749
|
+
|
|
750
|
+
def every(self, ms, func):
|
|
751
|
+
state = {"active": True, "id": None}
|
|
752
|
+
|
|
753
|
+
def tick():
|
|
754
|
+
if not state["active"]:
|
|
755
|
+
return
|
|
756
|
+
func()
|
|
757
|
+
state["id"] = self.root.after(ms, tick)
|
|
758
|
+
|
|
759
|
+
state["id"] = self.root.after(ms, tick)
|
|
760
|
+
return state
|
|
761
|
+
|
|
762
|
+
def cancel(self, timer):
|
|
763
|
+
if isinstance(timer, dict):
|
|
764
|
+
timer["active"] = False
|
|
765
|
+
if timer.get("id"):
|
|
766
|
+
try:
|
|
767
|
+
self.root.after_cancel(timer["id"])
|
|
768
|
+
except Exception:
|
|
769
|
+
pass
|
|
770
|
+
else:
|
|
771
|
+
self.root.after_cancel(timer)
|
|
772
|
+
return self
|
|
773
|
+
|
|
774
|
+
def on(self, event, func, add=None):
|
|
775
|
+
self.root.bind(event, func, add)
|
|
776
|
+
return self
|
|
777
|
+
|
|
778
|
+
def on_key(self, key, func):
|
|
779
|
+
self.root.bind(f"<{key}>", func)
|
|
780
|
+
return self
|
|
781
|
+
|
|
782
|
+
def on_close(self, func):
|
|
783
|
+
self.root.protocol("WM_DELETE_WINDOW", func)
|
|
784
|
+
return self
|
|
785
|
+
|
|
786
|
+
def clear(self):
|
|
787
|
+
for widget in self.body.winfo_children():
|
|
788
|
+
widget.destroy()
|
|
789
|
+
return self
|
|
790
|
+
|
|
791
|
+
def new_window(self, title="視窗", width=400, height=300, modal=False):
|
|
792
|
+
win = tk.Toplevel(self.root)
|
|
793
|
+
win.title(title)
|
|
794
|
+
win.geometry(f"{width}x{height}")
|
|
795
|
+
frame = ttk.Frame(win, padding=12)
|
|
796
|
+
frame.pack(fill="both", expand=True)
|
|
797
|
+
if modal:
|
|
798
|
+
win.transient(self.root)
|
|
799
|
+
win.grab_set()
|
|
800
|
+
return Container(self, frame, window=win)
|
|
801
|
+
|
|
802
|
+
def close(self):
|
|
803
|
+
self.root.destroy()
|
|
804
|
+
|
|
805
|
+
def run(self):
|
|
806
|
+
self.root.mainloop()
|
|
807
|
+
|
|
808
|
+
|
|
809
|
+
|
|
810
|
+
class ModernCard(Container):
|
|
811
|
+
def __init__(self, app, parent, title=None, padding=12):
|
|
812
|
+
outer = ttk.Frame(parent, padding=padding, relief="solid", borderwidth=1)
|
|
813
|
+
outer.pack(fill="x", pady=6)
|
|
814
|
+
if title:
|
|
815
|
+
ttk.Label(outer, text=title, font=("Arial", 12, "bold")).pack(anchor="w", pady=(0, 6))
|
|
816
|
+
body = ttk.Frame(outer)
|
|
817
|
+
body.pack(fill="both", expand=True)
|
|
818
|
+
super().__init__(app, body)
|
|
819
|
+
self.outer = outer
|
|
820
|
+
|
|
821
|
+
|
|
822
|
+
class StatusBar:
|
|
823
|
+
def __init__(self, parent, text="Ready"):
|
|
824
|
+
self.var = tk.StringVar(value=text)
|
|
825
|
+
self.widget = ttk.Label(parent, textvariable=self.var, relief="sunken", anchor="w")
|
|
826
|
+
self.widget.pack(side="bottom", fill="x")
|
|
827
|
+
|
|
828
|
+
def set(self, text):
|
|
829
|
+
self.var.set(text)
|
|
830
|
+
return self
|
|
831
|
+
|
|
832
|
+
def get(self):
|
|
833
|
+
return self.var.get()
|
|
834
|
+
|
|
835
|
+
def clear(self):
|
|
836
|
+
self.var.set("")
|
|
837
|
+
return self
|
|
838
|
+
|
|
839
|
+
|
|
840
|
+
class LoadingOverlay:
|
|
841
|
+
def __init__(self, app, text="Loading..."):
|
|
842
|
+
self.app = app
|
|
843
|
+
self.top = tk.Toplevel(app.root)
|
|
844
|
+
self.top.transient(app.root)
|
|
845
|
+
self.top.overrideredirect(True)
|
|
846
|
+
self.top.attributes("-topmost", True)
|
|
847
|
+
|
|
848
|
+
frame = ttk.Frame(self.top, padding=16, relief="solid", borderwidth=1)
|
|
849
|
+
frame.pack(fill="both", expand=True)
|
|
850
|
+
|
|
851
|
+
ttk.Label(frame, text=text, font=("Arial", 11, "bold")).pack(pady=(0, 8))
|
|
852
|
+
self.bar = ttk.Progressbar(frame, mode="indeterminate", length=180)
|
|
853
|
+
self.bar.pack()
|
|
854
|
+
self.bar.start(12)
|
|
855
|
+
|
|
856
|
+
self.top.update_idletasks()
|
|
857
|
+
w = self.top.winfo_reqwidth()
|
|
858
|
+
h = self.top.winfo_reqheight()
|
|
859
|
+
x = app.root.winfo_rootx() + max(0, (app.root.winfo_width() - w)//2)
|
|
860
|
+
y = app.root.winfo_rooty() + max(0, (app.root.winfo_height() - h)//2)
|
|
861
|
+
self.top.geometry(f"+{x}+{y}")
|
|
862
|
+
|
|
863
|
+
def close(self):
|
|
864
|
+
try:
|
|
865
|
+
self.bar.stop()
|
|
866
|
+
self.top.destroy()
|
|
867
|
+
except Exception:
|
|
868
|
+
pass
|
|
869
|
+
|
|
870
|
+
|
|
871
|
+
def _modern_methods():
|
|
872
|
+
def switch(self, text="", checked=False, action=None, layout="pack", **kwargs):
|
|
873
|
+
return self.check(text, checked=checked, action=action, layout=layout, **kwargs)
|
|
874
|
+
|
|
875
|
+
def card(self, title=None, padding=12):
|
|
876
|
+
return ModernCard(self.app if hasattr(self, "app") else self, self._parent, title, padding)
|
|
877
|
+
|
|
878
|
+
def badge(self, text, layout="pack"):
|
|
879
|
+
lbl = tk.Label(
|
|
880
|
+
self._parent,
|
|
881
|
+
text=text,
|
|
882
|
+
padx=8,
|
|
883
|
+
pady=2,
|
|
884
|
+
relief="groove",
|
|
885
|
+
borderwidth=1,
|
|
886
|
+
font=("Arial", 9, "bold")
|
|
887
|
+
)
|
|
888
|
+
return self._wrap_and_layout(lbl, layout)
|
|
889
|
+
|
|
890
|
+
def navbar(self, items):
|
|
891
|
+
frame = ttk.Frame(self._parent)
|
|
892
|
+
frame.pack(fill="x", pady=(0, 8))
|
|
893
|
+
for label, command in items:
|
|
894
|
+
ttk.Button(frame, text=label, command=command).pack(side="left", padx=3)
|
|
895
|
+
return Container(self.app if hasattr(self, "app") else self, frame)
|
|
896
|
+
|
|
897
|
+
def sidebar(self, items, width=160):
|
|
898
|
+
frame = ttk.Frame(self._parent, width=width)
|
|
899
|
+
frame.pack(side="left", fill="y", padx=(0, 8))
|
|
900
|
+
frame.pack_propagate(False)
|
|
901
|
+
for label, command in items:
|
|
902
|
+
ttk.Button(frame, text=label, command=command).pack(fill="x", pady=3)
|
|
903
|
+
return Container(self.app if hasattr(self, "app") else self, frame)
|
|
904
|
+
|
|
905
|
+
_Builder.switch = switch
|
|
906
|
+
_Builder.card = card
|
|
907
|
+
_Builder.badge = badge
|
|
908
|
+
_Builder.navbar = navbar
|
|
909
|
+
_Builder.sidebar = sidebar
|
|
910
|
+
|
|
911
|
+
_modern_methods()
|
|
912
|
+
|
|
913
|
+
|
|
914
|
+
def _app_modern_methods():
|
|
915
|
+
def toast(self, text, duration=2000, title=None):
|
|
916
|
+
win = tk.Toplevel(self.root)
|
|
917
|
+
win.overrideredirect(True)
|
|
918
|
+
win.attributes("-topmost", True)
|
|
919
|
+
|
|
920
|
+
frame = ttk.Frame(win, padding=10, relief="solid", borderwidth=1)
|
|
921
|
+
frame.pack(fill="both", expand=True)
|
|
922
|
+
|
|
923
|
+
if title:
|
|
924
|
+
ttk.Label(frame, text=title, font=("Arial", 10, "bold")).pack(anchor="w")
|
|
925
|
+
ttk.Label(frame, text=text).pack(anchor="w")
|
|
926
|
+
|
|
927
|
+
win.update_idletasks()
|
|
928
|
+
w = win.winfo_reqwidth()
|
|
929
|
+
h = win.winfo_reqheight()
|
|
930
|
+
x = self.root.winfo_rootx() + self.root.winfo_width() - w - 20
|
|
931
|
+
y = self.root.winfo_rooty() + self.root.winfo_height() - h - 40
|
|
932
|
+
win.geometry(f"+{max(0,x)}+{max(0,y)}")
|
|
933
|
+
self.root.after(duration, lambda: win.winfo_exists() and win.destroy())
|
|
934
|
+
return win
|
|
935
|
+
|
|
936
|
+
def loading(self, text="Loading..."):
|
|
937
|
+
return LoadingOverlay(self, text)
|
|
938
|
+
|
|
939
|
+
def statusbar(self, text="Ready"):
|
|
940
|
+
return StatusBar(self.root, text)
|
|
941
|
+
|
|
942
|
+
def page(self):
|
|
943
|
+
frame = ttk.Frame(self.body)
|
|
944
|
+
frame.pack(fill="both", expand=True)
|
|
945
|
+
return Container(self, frame)
|
|
946
|
+
|
|
947
|
+
def clear_body(self):
|
|
948
|
+
for widget in self.body.winfo_children():
|
|
949
|
+
widget.destroy()
|
|
950
|
+
return self
|
|
951
|
+
|
|
952
|
+
App.toast = toast
|
|
953
|
+
App.loading = loading
|
|
954
|
+
App.statusbar = statusbar
|
|
955
|
+
App.page = page
|
|
956
|
+
App.clear_body = clear_body
|
|
957
|
+
|
|
958
|
+
_app_modern_methods()
|
|
959
|
+
|
|
960
|
+
|
|
961
|
+
|
|
962
|
+
# =========================
|
|
963
|
+
# EasyPyUI 2.0 extensions
|
|
964
|
+
# =========================
|
|
965
|
+
|
|
966
|
+
import threading
|
|
967
|
+
import traceback
|
|
968
|
+
|
|
969
|
+
|
|
970
|
+
THEMES = {
|
|
971
|
+
"light": {
|
|
972
|
+
"bg": "#f5f5f5",
|
|
973
|
+
"surface": "#ffffff",
|
|
974
|
+
"text": "#111111",
|
|
975
|
+
"muted": "#666666",
|
|
976
|
+
"accent": "#2563eb",
|
|
977
|
+
},
|
|
978
|
+
"dark": {
|
|
979
|
+
"bg": "#181818",
|
|
980
|
+
"surface": "#242424",
|
|
981
|
+
"text": "#f5f5f5",
|
|
982
|
+
"muted": "#b0b0b0",
|
|
983
|
+
"accent": "#60a5fa",
|
|
984
|
+
},
|
|
985
|
+
}
|
|
986
|
+
|
|
987
|
+
|
|
988
|
+
class Router:
|
|
989
|
+
def __init__(self, app):
|
|
990
|
+
self.app = app
|
|
991
|
+
self.pages = {}
|
|
992
|
+
self.current = None
|
|
993
|
+
|
|
994
|
+
def add(self, name, builder):
|
|
995
|
+
self.pages[name] = builder
|
|
996
|
+
return self
|
|
997
|
+
|
|
998
|
+
def go(self, name):
|
|
999
|
+
if name not in self.pages:
|
|
1000
|
+
raise KeyError(f"找不到頁面: {name}")
|
|
1001
|
+
self.app.clear_body()
|
|
1002
|
+
self.current = name
|
|
1003
|
+
self.pages[name](self.app)
|
|
1004
|
+
return self
|
|
1005
|
+
|
|
1006
|
+
def back_to(self, name):
|
|
1007
|
+
return self.go(name)
|
|
1008
|
+
|
|
1009
|
+
|
|
1010
|
+
class Switch(Value):
|
|
1011
|
+
def __init__(self, parent, text="", checked=False, action=None):
|
|
1012
|
+
self.var = tk.BooleanVar(value=checked)
|
|
1013
|
+
self.frame = ttk.Frame(parent)
|
|
1014
|
+
self.label = ttk.Label(self.frame, text=text)
|
|
1015
|
+
self.label.pack(side="left", padx=(0, 8))
|
|
1016
|
+
|
|
1017
|
+
self.button = ttk.Checkbutton(
|
|
1018
|
+
self.frame,
|
|
1019
|
+
variable=self.var,
|
|
1020
|
+
command=action
|
|
1021
|
+
)
|
|
1022
|
+
self.button.pack(side="left")
|
|
1023
|
+
self.widget = self.button
|
|
1024
|
+
self.tk = self.button
|
|
1025
|
+
|
|
1026
|
+
def pack(self, **kwargs):
|
|
1027
|
+
self.frame.pack(**kwargs)
|
|
1028
|
+
return self
|
|
1029
|
+
|
|
1030
|
+
def grid(self, **kwargs):
|
|
1031
|
+
self.frame.grid(**kwargs)
|
|
1032
|
+
return self
|
|
1033
|
+
|
|
1034
|
+
def place(self, **kwargs):
|
|
1035
|
+
self.frame.place(**kwargs)
|
|
1036
|
+
return self
|
|
1037
|
+
|
|
1038
|
+
|
|
1039
|
+
def _builder_switch_v2(self, text="", checked=False, action=None, layout="pack", **kwargs):
|
|
1040
|
+
sw = Switch(self._parent, text=text, checked=checked, action=action)
|
|
1041
|
+
layout_opts = kwargs.pop("layout_opts", {})
|
|
1042
|
+
if layout == "grid":
|
|
1043
|
+
sw.grid(**layout_opts)
|
|
1044
|
+
elif layout == "place":
|
|
1045
|
+
sw.place(**layout_opts)
|
|
1046
|
+
elif layout != "none":
|
|
1047
|
+
defaults = {"fill": "x", "pady": 4}
|
|
1048
|
+
defaults.update(layout_opts)
|
|
1049
|
+
sw.pack(**defaults)
|
|
1050
|
+
return sw
|
|
1051
|
+
|
|
1052
|
+
|
|
1053
|
+
_Builder.switch = _builder_switch_v2
|
|
1054
|
+
|
|
1055
|
+
|
|
1056
|
+
def _app_theme2(self, name="light", accent=None):
|
|
1057
|
+
if name not in THEMES:
|
|
1058
|
+
raise ValueError(f"未知主題: {name}. 可用: {', '.join(THEMES)}")
|
|
1059
|
+
|
|
1060
|
+
palette = dict(THEMES[name])
|
|
1061
|
+
if accent:
|
|
1062
|
+
palette["accent"] = accent
|
|
1063
|
+
|
|
1064
|
+
self._theme_name = name
|
|
1065
|
+
self._palette = palette
|
|
1066
|
+
|
|
1067
|
+
self.root.configure(bg=palette["bg"])
|
|
1068
|
+
try:
|
|
1069
|
+
self.body.configure(style="Easy.TFrame")
|
|
1070
|
+
except Exception:
|
|
1071
|
+
pass
|
|
1072
|
+
|
|
1073
|
+
self.style.configure("Easy.TFrame", background=palette["bg"])
|
|
1074
|
+
self.style.configure(
|
|
1075
|
+
"EasyCard.TFrame",
|
|
1076
|
+
background=palette["surface"],
|
|
1077
|
+
relief="flat"
|
|
1078
|
+
)
|
|
1079
|
+
self.style.configure(
|
|
1080
|
+
"Easy.TLabel",
|
|
1081
|
+
background=palette["bg"],
|
|
1082
|
+
foreground=palette["text"]
|
|
1083
|
+
)
|
|
1084
|
+
self.style.configure(
|
|
1085
|
+
"EasyMuted.TLabel",
|
|
1086
|
+
background=palette["bg"],
|
|
1087
|
+
foreground=palette["muted"]
|
|
1088
|
+
)
|
|
1089
|
+
self.style.configure(
|
|
1090
|
+
"Accent.TButton",
|
|
1091
|
+
padding=8
|
|
1092
|
+
)
|
|
1093
|
+
return self
|
|
1094
|
+
|
|
1095
|
+
|
|
1096
|
+
def _app_palette(self, key=None):
|
|
1097
|
+
palette = getattr(self, "_palette", THEMES["light"])
|
|
1098
|
+
return palette.get(key) if key else dict(palette)
|
|
1099
|
+
|
|
1100
|
+
|
|
1101
|
+
def _app_router(self):
|
|
1102
|
+
if not hasattr(self, "_router"):
|
|
1103
|
+
self._router = Router(self)
|
|
1104
|
+
return self._router
|
|
1105
|
+
|
|
1106
|
+
|
|
1107
|
+
def _app_route(self, name, builder=None):
|
|
1108
|
+
router = self.router()
|
|
1109
|
+
if builder is not None:
|
|
1110
|
+
router.add(name, builder)
|
|
1111
|
+
return self
|
|
1112
|
+
return router.go(name)
|
|
1113
|
+
|
|
1114
|
+
|
|
1115
|
+
def _app_run_task(self, func, done=None, error=None, loading_text=None):
|
|
1116
|
+
loader = self.loading(loading_text) if loading_text else None
|
|
1117
|
+
|
|
1118
|
+
def worker():
|
|
1119
|
+
try:
|
|
1120
|
+
result = func()
|
|
1121
|
+
except Exception as exc:
|
|
1122
|
+
tb = traceback.format_exc()
|
|
1123
|
+
def fail():
|
|
1124
|
+
if loader:
|
|
1125
|
+
loader.close()
|
|
1126
|
+
if error:
|
|
1127
|
+
error(exc)
|
|
1128
|
+
else:
|
|
1129
|
+
self.error(tb, title="背景任務錯誤")
|
|
1130
|
+
self.root.after(0, fail)
|
|
1131
|
+
return
|
|
1132
|
+
|
|
1133
|
+
def finish():
|
|
1134
|
+
if loader:
|
|
1135
|
+
loader.close()
|
|
1136
|
+
if done:
|
|
1137
|
+
done(result)
|
|
1138
|
+
|
|
1139
|
+
self.root.after(0, finish)
|
|
1140
|
+
|
|
1141
|
+
threading.Thread(target=worker, daemon=True).start()
|
|
1142
|
+
return self
|
|
1143
|
+
|
|
1144
|
+
|
|
1145
|
+
def _app_confirm(self, text, yes=None, no=None, title="確認"):
|
|
1146
|
+
result = self.ask(text, title=title)
|
|
1147
|
+
if result and yes:
|
|
1148
|
+
yes()
|
|
1149
|
+
elif not result and no:
|
|
1150
|
+
no()
|
|
1151
|
+
return result
|
|
1152
|
+
|
|
1153
|
+
|
|
1154
|
+
def _app_notify(self, text, title=None, duration=2500):
|
|
1155
|
+
return self.toast(text, title=title, duration=duration)
|
|
1156
|
+
|
|
1157
|
+
|
|
1158
|
+
def _app_set_status(self, text):
|
|
1159
|
+
if not hasattr(self, "_statusbar2"):
|
|
1160
|
+
self._statusbar2 = self.statusbar(text)
|
|
1161
|
+
else:
|
|
1162
|
+
self._statusbar2.set(text)
|
|
1163
|
+
return self._statusbar2
|
|
1164
|
+
|
|
1165
|
+
|
|
1166
|
+
App.theme2 = _app_theme2
|
|
1167
|
+
App.palette = _app_palette
|
|
1168
|
+
App.router = _app_router
|
|
1169
|
+
App.route = _app_route
|
|
1170
|
+
App.run_task = _app_run_task
|
|
1171
|
+
App.confirm = _app_confirm
|
|
1172
|
+
App.notify = _app_notify
|
|
1173
|
+
App.set_status = _app_set_status
|
|
1174
|
+
|
|
1175
|
+
|
|
1176
|
+
# Patch App.__init__ so a modern theme can be selected with appearance=
|
|
1177
|
+
_old_app_init = App.__init__
|
|
1178
|
+
|
|
1179
|
+
def _new_app_init(self, title="EasyPyUI", width=500, height=400,
|
|
1180
|
+
resizable=True, theme=None, appearance="light", accent=None):
|
|
1181
|
+
_old_app_init(self, title, width, height, resizable, theme)
|
|
1182
|
+
self.theme2(appearance, accent=accent)
|
|
1183
|
+
|
|
1184
|
+
App.__init__ = _new_app_init
|
|
1185
|
+
|
|
1186
|
+
|
|
1187
|
+
# EasyPyUI 2.1 modern drawing widgets
|
|
1188
|
+
from .modern import install_modern_api
|
|
1189
|
+
install_modern_api(App, _Builder)
|
|
1190
|
+
|
|
1191
|
+
from .animation import install_animation_api
|
|
1192
|
+
install_animation_api(App, _Builder)
|