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/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)