flexgui 1.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.
- flexgui/__init__.py +71 -0
- flexgui/app.py +221 -0
- flexgui/builder.py +211 -0
- flexgui/designer.py +1974 -0
- flexgui/editor.py +277 -0
- flexgui/mode.py +95 -0
- flexgui/registry.py +188 -0
- flexgui/runtime.py +190 -0
- flexgui/theme.py +139 -0
- flexgui/triggers.py +467 -0
- flexgui/widgets.py +738 -0
- flexgui-1.0.0.dist-info/METADATA +235 -0
- flexgui-1.0.0.dist-info/RECORD +15 -0
- flexgui-1.0.0.dist-info/WHEEL +4 -0
- flexgui-1.0.0.dist-info/licenses/LICENSE +21 -0
flexgui/__init__.py
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
"""
|
|
3
|
+
FlexGUI — thư viện GUI Python kéo-thả-thiết-kế trên nền tkinter.
|
|
4
|
+
|
|
5
|
+
pip install flexgui
|
|
6
|
+
|
|
7
|
+
ĐỒNG GÓI 2 PHẦN TRONG 1 THƯ VIỆN:
|
|
8
|
+
|
|
9
|
+
(A) THƯ VIỆN RUNTIME — cú pháp gọn, định vị tuyệt đối bằng place(x, y):
|
|
10
|
+
|
|
11
|
+
import flexgui
|
|
12
|
+
from flexgui import App, Label, Button, Input, Checkbox, Text
|
|
13
|
+
|
|
14
|
+
app = App("Ứng dụng của tôi", 860, 560)
|
|
15
|
+
Label("Xin chào!", 40, 30)
|
|
16
|
+
Button("Bấm tôi", 40, 80)
|
|
17
|
+
Input(40, 130, width=26)
|
|
18
|
+
Checkbox("Đồng ý", 40, 180)
|
|
19
|
+
Text(40, 230, width=42, height=7)
|
|
20
|
+
app.run()
|
|
21
|
+
|
|
22
|
+
(B) CHẾ ĐỘ THIẾT KẾ (FlexGUI Studio) — mở bằng MỘT trong các cách:
|
|
23
|
+
|
|
24
|
+
import flexgui
|
|
25
|
+
flexgui.setup() # cách 1 — mở Studio ngay
|
|
26
|
+
|
|
27
|
+
from flexgui import design # cách 2 — tương đương
|
|
28
|
+
design.mode = True
|
|
29
|
+
|
|
30
|
+
python myapp.py --design # cách 3 — cờ dòng lệnh
|
|
31
|
+
|
|
32
|
+
→ Chạy file ở design mode: Studio tự tạo GIAO DIỆN TRỐNG (hoặc nạp
|
|
33
|
+
myapp.flexgui.json đã lưu). Kéo thả thành phần, chỉnh thuộc tính,
|
|
34
|
+
viết trigger bằng mini VS Code ngay trong Studio HOẶC mở file
|
|
35
|
+
myapp.triggers.py bằng VS Code thật — cả hai sửa CÙNG một file.
|
|
36
|
+
|
|
37
|
+
→ Chạy thật: đổi setup() thành flexgui.run(__file__) rồi chạy lại.
|
|
38
|
+
|
|
39
|
+
Truy cập widget theo ID (tự động, tái sử dụng ID khi xóa):
|
|
40
|
+
from flexgui import label, button, input, checkbox, text
|
|
41
|
+
label[1].set_text("Xin chào!") # đọc/ghi thuộc tính trực tiếp
|
|
42
|
+
|
|
43
|
+
Chi tiết: README.md đi kèm package.
|
|
44
|
+
"""
|
|
45
|
+
|
|
46
|
+
from .mode import design
|
|
47
|
+
from .registry import label, button, input, checkbox, text
|
|
48
|
+
from .triggers import (
|
|
49
|
+
on_click, on_change, on_enter, when, every, on_init,
|
|
50
|
+
refresh, set_title, set_size, set_bg, alert, confirm, prompt, close_app,
|
|
51
|
+
)
|
|
52
|
+
from .widgets import Label, Button, Input, Checkbox, Text
|
|
53
|
+
from .app import App
|
|
54
|
+
from .runtime import run, setup, run_embedded
|
|
55
|
+
|
|
56
|
+
__version__ = "1.0.0"
|
|
57
|
+
__author__ = "FlexGUI Team"
|
|
58
|
+
|
|
59
|
+
__all__ = [
|
|
60
|
+
# cửa sổ & widget
|
|
61
|
+
"App", "Label", "Button", "Input", "Checkbox", "Text",
|
|
62
|
+
# vùng tên theo ID
|
|
63
|
+
"label", "button", "input", "checkbox", "text",
|
|
64
|
+
# chế độ thiết kế & chạy
|
|
65
|
+
"design", "setup", "run", "run_embedded",
|
|
66
|
+
# decorator trigger
|
|
67
|
+
"on_click", "on_change", "on_enter", "when", "every", "on_init",
|
|
68
|
+
# lệnh app
|
|
69
|
+
"refresh", "set_title", "set_size", "set_bg",
|
|
70
|
+
"alert", "confirm", "prompt", "close_app",
|
|
71
|
+
]
|
flexgui/app.py
ADDED
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
"""
|
|
3
|
+
App — cửa sổ ứng dụng + đăng ký widget (ID tái sử dụng) + vòng đời chạy.
|
|
4
|
+
|
|
5
|
+
from flexgui import App, Label, Button
|
|
6
|
+
|
|
7
|
+
app = App("Ứng dụng của tôi", 860, 560)
|
|
8
|
+
Label("Xin chào!", 40, 30)
|
|
9
|
+
app.run() # chạy thật
|
|
10
|
+
app.run(design_mode=True) # chuyển sang FlexGUI Studio
|
|
11
|
+
|
|
12
|
+
ID tái sử dụng (từ phiên bản trước, giữ nguyên): widget bị xóa trả ID
|
|
13
|
+
về pool; widget tạo kế tiếp nhận ID nhỏ nhất vừa rảnh — ví dụ xóa
|
|
14
|
+
input[3] rồi thêm widget mới thì widget mới đúng là [3].
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
import heapq
|
|
18
|
+
import sys
|
|
19
|
+
from pathlib import Path
|
|
20
|
+
|
|
21
|
+
import tkinter as tk
|
|
22
|
+
|
|
23
|
+
from . import registry as _reg
|
|
24
|
+
from .mode import design
|
|
25
|
+
from .theme import MAU_APP
|
|
26
|
+
from .triggers import TriggerEngine
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _doc_co_design(argv=None):
|
|
30
|
+
"""Đọc cờ --design từ dòng lệnh (giữ tương thích thói quen cũ)."""
|
|
31
|
+
argv = sys.argv if argv is None else argv
|
|
32
|
+
return any(str(a).lower() in ("--design", "-design") for a in argv)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class App:
|
|
36
|
+
"""Cửa sổ ứng dụng.
|
|
37
|
+
|
|
38
|
+
Tham số:
|
|
39
|
+
title, width, height : tiêu đề + kích thước cửa sổ
|
|
40
|
+
bg : màu nền (mặc định #f5f6fa)
|
|
41
|
+
goc : None = cửa sổ chính (tk.Tk)
|
|
42
|
+
tk widget = cửa sổ con (Toplevel) — dùng khi
|
|
43
|
+
Studio mở bản XEM TRƯỚC, không chặn Studio.
|
|
44
|
+
"""
|
|
45
|
+
|
|
46
|
+
def __init__(self, title="Ứng dụng FlexGUI", width=860, height=560,
|
|
47
|
+
bg=None, goc=None):
|
|
48
|
+
self.title = title
|
|
49
|
+
self.width = int(width)
|
|
50
|
+
self.height = int(height)
|
|
51
|
+
self._nen = bg or MAU_APP["nen_mac_dinh"]
|
|
52
|
+
self.xem_truoc = goc is not None # True = bản xem trước từ Studio
|
|
53
|
+
self.dang_thiet_ke = False
|
|
54
|
+
|
|
55
|
+
if goc is None:
|
|
56
|
+
self.root = tk.Tk()
|
|
57
|
+
else:
|
|
58
|
+
self.root = tk.Toplevel(goc)
|
|
59
|
+
self.root.title(title)
|
|
60
|
+
self.root.geometry(f"{self.width}x{self.height}")
|
|
61
|
+
self.root.minsize(320, 200)
|
|
62
|
+
self.root.configure(bg=self._nen)
|
|
63
|
+
self.khung = tk.Frame(self.root, bg=self._nen)
|
|
64
|
+
self.khung.pack(fill="both", expand=True)
|
|
65
|
+
|
|
66
|
+
# Sổ đăng ký widget
|
|
67
|
+
self.widgets = {} # id -> widget (mọi loại)
|
|
68
|
+
self.theo_loai = { # loại -> {id -> widget}
|
|
69
|
+
"label": {}, "button": {}, "input": {}, "checkbox": {}, "text": {},
|
|
70
|
+
}
|
|
71
|
+
self._id_tiep_theo = 1 # ID mới kế tiếp (pool rỗng)
|
|
72
|
+
self._id_tu_do = [] # pool ID đã xóa — TÁI SỬ DỤNG
|
|
73
|
+
|
|
74
|
+
# Hệ thống trigger + cổng chặn khi ở chế độ thiết kế
|
|
75
|
+
self.dong_co = TriggerEngine(self)
|
|
76
|
+
self.trigger_nguon = {}
|
|
77
|
+
self._cho_phep_thu_kich = False
|
|
78
|
+
self.designer = None
|
|
79
|
+
|
|
80
|
+
_reg._dat_app(self)
|
|
81
|
+
|
|
82
|
+
# ------------------------------------------------------------ vòng đời
|
|
83
|
+
def con_song(self):
|
|
84
|
+
"""Cửa sổ còn tồn tại không (để registry tự dọn app chết)."""
|
|
85
|
+
try:
|
|
86
|
+
return bool(self.root.winfo_exists())
|
|
87
|
+
except Exception:
|
|
88
|
+
return False
|
|
89
|
+
|
|
90
|
+
# ------------------------------------------------------------ đăng ký ID
|
|
91
|
+
def _dang_ky(self, wgt, id=None):
|
|
92
|
+
"""Cấp ID duy nhất — ưu tiên ID nhỏ nhất trong pool TÁI SỬ DỤNG."""
|
|
93
|
+
if id is None:
|
|
94
|
+
if self._id_tu_do:
|
|
95
|
+
id = heapq.heappop(self._id_tu_do)
|
|
96
|
+
else:
|
|
97
|
+
id = self._id_tiep_theo
|
|
98
|
+
id = int(id)
|
|
99
|
+
if id in self.widgets:
|
|
100
|
+
raise ValueError(f"ID {id} đã được widget khác sử dụng!")
|
|
101
|
+
if id in self._id_tu_do: # id tường minh trùng id đang rảnh
|
|
102
|
+
self._id_tu_do.remove(id)
|
|
103
|
+
heapq.heapify(self._id_tu_do)
|
|
104
|
+
if id >= self._id_tiep_theo:
|
|
105
|
+
self._id_tiep_theo = id + 1
|
|
106
|
+
self.widgets[id] = wgt
|
|
107
|
+
self.theo_loai[wgt.loai][id] = wgt
|
|
108
|
+
return id
|
|
109
|
+
|
|
110
|
+
def _huy_dang_ky(self, wgt):
|
|
111
|
+
"""Gỡ widget khỏi sổ đăng ký — ID trả về pool TÁI SỬ DỤNG."""
|
|
112
|
+
self.widgets.pop(wgt.id, None)
|
|
113
|
+
self.theo_loai.get(wgt.loai, {}).pop(wgt.id, None)
|
|
114
|
+
self.trigger_nguon.pop(wgt.id, None)
|
|
115
|
+
self.dong_co._click.pop(wgt.id, None)
|
|
116
|
+
self.dong_co._change.pop(wgt.id, None)
|
|
117
|
+
self.dong_co._enter.pop(wgt.id, None)
|
|
118
|
+
if wgt.id is not None and wgt.id not in self._id_tu_do:
|
|
119
|
+
heapq.heappush(self._id_tu_do, wgt.id)
|
|
120
|
+
|
|
121
|
+
# ------------------------------------------------------------ tiện ích
|
|
122
|
+
def bg(self, mau):
|
|
123
|
+
"""Đổi màu nền cửa sổ."""
|
|
124
|
+
self._nen = mau
|
|
125
|
+
self.root.config(bg=mau)
|
|
126
|
+
self.khung.config(bg=mau)
|
|
127
|
+
for w in self.widgets.values():
|
|
128
|
+
try:
|
|
129
|
+
if w._mau_nen is None and w.loai in ("label", "checkbox"):
|
|
130
|
+
w.tkw.config(bg=mau)
|
|
131
|
+
if w.loai == "checkbox":
|
|
132
|
+
w.tkw.config(activebackground=mau)
|
|
133
|
+
except Exception:
|
|
134
|
+
pass
|
|
135
|
+
|
|
136
|
+
def dat_trigger_chung(self, code):
|
|
137
|
+
"""Gán đoạn code trigger CHUNG (giữ tương thích bản cũ)."""
|
|
138
|
+
self.trigger_nguon["global"] = code or ""
|
|
139
|
+
|
|
140
|
+
def lay_thiet_ke(self):
|
|
141
|
+
"""Chụp toàn bộ app thành dict thiết kế (dùng khi --design trên
|
|
142
|
+
app viết bằng code thuần: chuyển code thành thiết kế chỉnh được)."""
|
|
143
|
+
return {
|
|
144
|
+
"window": {
|
|
145
|
+
"title": self.title,
|
|
146
|
+
"width": self.width,
|
|
147
|
+
"height": self.height,
|
|
148
|
+
"bg": self._nen,
|
|
149
|
+
},
|
|
150
|
+
"components": [w.lay_du_lieu()
|
|
151
|
+
for w in self.widgets.values()],
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
# ------------------------------------------------------------ chạy
|
|
155
|
+
def run(self, design_mode=None, chay_mainloop=True):
|
|
156
|
+
"""Khởi động ứng dụng.
|
|
157
|
+
|
|
158
|
+
design_mode=None : tự đọc design.mode (flexgui.design) + cờ --design
|
|
159
|
+
design_mode=True : chuyển sang FlexGUI Studio (mang theo toàn bộ
|
|
160
|
+
widget đang có — thiết kế tiếp bằng kéo thả)
|
|
161
|
+
chay_mainloop=False : chỉ kích hoạt engine, không vào mainloop
|
|
162
|
+
(dùng cho bản XEM TRƯỚC của Studio).
|
|
163
|
+
"""
|
|
164
|
+
if design_mode is None:
|
|
165
|
+
design_mode = design.mode or _doc_co_design()
|
|
166
|
+
if design_mode:
|
|
167
|
+
from .designer import Studio
|
|
168
|
+
thiet_ke = self.lay_thiet_ke()
|
|
169
|
+
design._danh_dau_da_mo()
|
|
170
|
+
# file .py của người dùng (nếu chạy python myapp.py --design)
|
|
171
|
+
file_py = None
|
|
172
|
+
try:
|
|
173
|
+
if sys.argv and sys.argv[0].endswith(".py") \
|
|
174
|
+
and Path(sys.argv[0]).exists():
|
|
175
|
+
file_py = Path(sys.argv[0]).resolve()
|
|
176
|
+
except Exception:
|
|
177
|
+
pass
|
|
178
|
+
# Đóng cửa sổ code thuần (chưa vào mainloop nên chỉ cần destroy)
|
|
179
|
+
self.dong_co.dung()
|
|
180
|
+
try:
|
|
181
|
+
self.root.destroy()
|
|
182
|
+
except Exception:
|
|
183
|
+
pass
|
|
184
|
+
_reg._dat_app(None)
|
|
185
|
+
st = Studio(duong_dan=file_py, thiet_ke_ban_dau=thiet_ke)
|
|
186
|
+
st.chay()
|
|
187
|
+
return st
|
|
188
|
+
|
|
189
|
+
self._cho_phep_thu_kich = True
|
|
190
|
+
self.dong_co.bat_dau()
|
|
191
|
+
|
|
192
|
+
if self.xem_truoc:
|
|
193
|
+
# Bản xem trước từ Studio: đóng = dọn dẹp + destroy Toplevel
|
|
194
|
+
self.root.protocol("WM_DELETE_WINDOW", self._dong_xem_truoc)
|
|
195
|
+
else:
|
|
196
|
+
self.root.protocol("WM_DELETE_WINDOW", self._dong_app)
|
|
197
|
+
|
|
198
|
+
if chay_mainloop:
|
|
199
|
+
self.root.mainloop()
|
|
200
|
+
|
|
201
|
+
def _dong_app(self):
|
|
202
|
+
"""Đóng ứng dụng thật: dừng engine, thoát mainloop rồi hủy."""
|
|
203
|
+
self.dong_co.dung()
|
|
204
|
+
try:
|
|
205
|
+
self.root.quit()
|
|
206
|
+
except Exception:
|
|
207
|
+
pass
|
|
208
|
+
try:
|
|
209
|
+
self.root.destroy()
|
|
210
|
+
except Exception:
|
|
211
|
+
pass
|
|
212
|
+
|
|
213
|
+
def _dong_xem_truoc(self):
|
|
214
|
+
"""Đóng bản XEM TRƯỚC (Studio vẫn sống)."""
|
|
215
|
+
self.dong_co.dung()
|
|
216
|
+
try:
|
|
217
|
+
self.root.destroy()
|
|
218
|
+
except Exception:
|
|
219
|
+
pass
|
|
220
|
+
if _reg._app_hien_tai() is self:
|
|
221
|
+
_reg._dat_app(None)
|
flexgui/builder.py
ADDED
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
"""
|
|
3
|
+
Builder — xuất file chạy đơn & đóng gói .exe bằng PyInstaller.
|
|
4
|
+
|
|
5
|
+
• xuat_file_dung() : gộp thiết kế + trigger thành MỘT file .py duy nhất
|
|
6
|
+
(chạy được ngay: cần đã cài pip install flexgui)
|
|
7
|
+
• TrinhDongGoi : chạy pyinstaller trong thread nền, log realtime,
|
|
8
|
+
tự cài pyinstaller nếu thiếu
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
import os
|
|
12
|
+
import queue
|
|
13
|
+
import subprocess
|
|
14
|
+
import sys
|
|
15
|
+
import threading
|
|
16
|
+
|
|
17
|
+
import tkinter as tk
|
|
18
|
+
from tkinter import ttk, messagebox
|
|
19
|
+
|
|
20
|
+
from .theme import MAU, font_ui, font_mono
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def xuat_file_dung(duong_dan, thiet_ke, nguon_trigger):
|
|
24
|
+
"""Ghi file .py tự chạy (nhúng toàn bộ thiết kế + trigger).
|
|
25
|
+
|
|
26
|
+
duong_dan : file .py đích (ví dụ myapp_app.py)
|
|
27
|
+
thiet_ke : dict thiết kế (window + components)
|
|
28
|
+
nguon_trigger : chuỗi code trigger (file .triggers.py)
|
|
29
|
+
Trả về đường dẫn đã ghi.
|
|
30
|
+
"""
|
|
31
|
+
import pprint
|
|
32
|
+
|
|
33
|
+
ten = os.path.splitext(os.path.basename(duong_dan))[0]
|
|
34
|
+
body = (
|
|
35
|
+
f"# {'=' * 74}\n"
|
|
36
|
+
f"# {ten} — sinh bởi FlexGUI Studio\n"
|
|
37
|
+
f"# Chạy: python {os.path.basename(duong_dan)}\n"
|
|
38
|
+
f"# Cần: pip install flexgui\n"
|
|
39
|
+
f"# {'=' * 74}\n"
|
|
40
|
+
f"import flexgui\n"
|
|
41
|
+
f"\n"
|
|
42
|
+
f"_THIET_KE = "
|
|
43
|
+
)
|
|
44
|
+
# pprint.pformat sinh literal PYTHON đúng (None/True/False, chuỗi '...')
|
|
45
|
+
body += pprint.pformat(thiet_ke, width=68)
|
|
46
|
+
body += "\n\n_TRIGGERS = "
|
|
47
|
+
body += pprint.pformat(nguon_trigger or "", width=68)
|
|
48
|
+
body += "\n\nflexgui.run_embedded(_THIET_KE, _TRIGGERS)\n"
|
|
49
|
+
|
|
50
|
+
with open(duong_dan, "w", encoding="utf-8") as f:
|
|
51
|
+
f.write(body)
|
|
52
|
+
return duong_dan
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
class TrinhDongGoi:
|
|
56
|
+
"""Đóng gói file .py thành file thực thi duy nhất (.exe trên Windows).
|
|
57
|
+
|
|
58
|
+
Quy trình tự động trong thread nền (không đơ giao diện):
|
|
59
|
+
1. Kiểm tra PyInstaller → thiếu thì pip install tự động
|
|
60
|
+
2. pyinstaller --onefile --windowed --name <tên> <file.py>
|
|
61
|
+
3. Kết quả: dist/<tên>.exe — log hiển thị trực tiếp trong cửa sổ
|
|
62
|
+
"""
|
|
63
|
+
|
|
64
|
+
def __init__(self, goc, ten="ung_dung"):
|
|
65
|
+
self.goc = goc # cửa sổ cha (root Studio)
|
|
66
|
+
self.ten = ten
|
|
67
|
+
self.hang_doi = queue.Queue()
|
|
68
|
+
self._xong = False
|
|
69
|
+
self._thanh_cong = False
|
|
70
|
+
self._duong_dan_dist = None
|
|
71
|
+
|
|
72
|
+
# ------------------------------------------------------------ giao diện
|
|
73
|
+
def chay(self, duong_dan):
|
|
74
|
+
"""Mở cửa sổ build rồi chạy build trong thread nền."""
|
|
75
|
+
cs = tk.Toplevel(self.goc)
|
|
76
|
+
cs.title(f"Đóng gói .exe — {self.ten}")
|
|
77
|
+
cs.geometry("780x460")
|
|
78
|
+
cs.configure(bg=MAU["nen_panel"])
|
|
79
|
+
cs.transient(self.goc)
|
|
80
|
+
self.cua_so = cs
|
|
81
|
+
|
|
82
|
+
tk.Label(cs, text="⏳ PyInstaller đang làm việc — có thể mất 1–3 phút "
|
|
83
|
+
"lần đầu (nếu phải cài đặt).",
|
|
84
|
+
bg=MAU["nen_panel"], fg=MAU["chu"], anchor="w",
|
|
85
|
+
font=font_ui(10)).pack(fill="x", padx=14, pady=(12, 6))
|
|
86
|
+
|
|
87
|
+
self.thanh_tien = ttk.Progressbar(cs, mode="indeterminate")
|
|
88
|
+
self.thanh_tien.pack(fill="x", padx=14)
|
|
89
|
+
self.thanh_tien.start(14)
|
|
90
|
+
|
|
91
|
+
self.nhat_ky = tk.Text(cs, bg="#141414", fg=MAU["chu"], bd=0,
|
|
92
|
+
font=font_mono(10), wrap="none",
|
|
93
|
+
state="disabled")
|
|
94
|
+
self.nhat_ky.pack(fill="both", expand=True, padx=14, pady=(6, 6))
|
|
95
|
+
|
|
96
|
+
duoi = tk.Frame(cs, bg=MAU["nen_panel"])
|
|
97
|
+
duoi.pack(fill="x", padx=14, pady=(0, 12))
|
|
98
|
+
tk.Button(duoi, text="📂 Mở thư mục dist", state="disabled",
|
|
99
|
+
bg=MAU["nen_nut"], fg=MAU["chu_mo"], bd=0,
|
|
100
|
+
padx=12, pady=4, font=font_ui(9), cursor="hand2",
|
|
101
|
+
command=self._mo_thu_muc).pack(side="right")
|
|
102
|
+
|
|
103
|
+
threading.Thread(target=self._thuc_thi, args=(duong_dan,),
|
|
104
|
+
daemon=True).start()
|
|
105
|
+
self._do_hang()
|
|
106
|
+
|
|
107
|
+
def _ghi(self, s):
|
|
108
|
+
"""Thread nền ghi log vào queue (thread-safe)."""
|
|
109
|
+
self.hang_doi.put(s)
|
|
110
|
+
|
|
111
|
+
def _do_hang(self):
|
|
112
|
+
"""Vòng poll 150ms: rút log từ queue, phát hiện trạng thái kết thúc."""
|
|
113
|
+
try:
|
|
114
|
+
while True:
|
|
115
|
+
dong = self.hang_doi.get_nowait()
|
|
116
|
+
self.nhat_ky.config(state="normal")
|
|
117
|
+
self.nhat_ky.insert("end", dong)
|
|
118
|
+
self.nhat_ky.see("end")
|
|
119
|
+
self.nhat_ky.config(state="disabled")
|
|
120
|
+
except queue.Empty:
|
|
121
|
+
pass
|
|
122
|
+
if self._xong:
|
|
123
|
+
self.thanh_tien.stop()
|
|
124
|
+
if self._thanh_cong:
|
|
125
|
+
messagebox.showinfo(
|
|
126
|
+
"Build xong", "Đã tạo file thực thi thành công!\n\n"
|
|
127
|
+
f"{self._duong_dan_dist}", parent=self.cua_so)
|
|
128
|
+
else:
|
|
129
|
+
self.cua_so.after(150, self._do_hang)
|
|
130
|
+
|
|
131
|
+
def _mo_thu_muc(self):
|
|
132
|
+
"""Mở thư mục chứa kết quả (đa nền tảng)."""
|
|
133
|
+
thu_muc = os.path.dirname(self._duong_dan_dist or "")
|
|
134
|
+
try:
|
|
135
|
+
if sys.platform.startswith("win"):
|
|
136
|
+
os.startfile(thu_muc) # type: ignore[attr-defined]
|
|
137
|
+
elif sys.platform == "darwin":
|
|
138
|
+
subprocess.Popen(["open", thu_muc])
|
|
139
|
+
else:
|
|
140
|
+
subprocess.Popen(["xdg-open", thu_muc])
|
|
141
|
+
except Exception:
|
|
142
|
+
messagebox.showinfo("Thư mục", thu_muc, parent=self.cua_so)
|
|
143
|
+
|
|
144
|
+
# ------------------------------------------------------------ quy trình
|
|
145
|
+
def _moi_truong_build(self):
|
|
146
|
+
"""Môi trường cho lệnh build.
|
|
147
|
+
|
|
148
|
+
Trên Linux với Python "di động" (uv, pyenv...), libtcl/libtk nằm
|
|
149
|
+
ngoài LD_LIBRARY_PATH mặc định nên PyInstaller không nhúng được —
|
|
150
|
+
thêm thư mục đó vào để file chạy được trên mọi máy.
|
|
151
|
+
"""
|
|
152
|
+
import glob
|
|
153
|
+
mt = os.environ.copy()
|
|
154
|
+
if os.name == "posix":
|
|
155
|
+
thu_muc = os.path.join(sys.base_prefix, "lib")
|
|
156
|
+
if os.path.isdir(thu_muc) and \
|
|
157
|
+
glob.glob(os.path.join(thu_muc, "libtcl*")):
|
|
158
|
+
mt["LD_LIBRARY_PATH"] = (thu_muc + os.pathsep +
|
|
159
|
+
mt.get("LD_LIBRARY_PATH", ""))
|
|
160
|
+
return mt
|
|
161
|
+
|
|
162
|
+
def _thuc_thi(self, duong_dan):
|
|
163
|
+
"""Toàn bộ pipeline build — chạy trong thread nền."""
|
|
164
|
+
try:
|
|
165
|
+
self._ghi("► Bước 1/3: Kiểm tra PyInstaller...\n")
|
|
166
|
+
r = subprocess.run([sys.executable, "-m", "PyInstaller",
|
|
167
|
+
"--version"], capture_output=True, text=True)
|
|
168
|
+
if r.returncode != 0:
|
|
169
|
+
self._ghi(" Chưa có PyInstaller → tự cài bằng pip...\n")
|
|
170
|
+
p = subprocess.run(
|
|
171
|
+
[sys.executable, "-m", "pip", "install", "--quiet",
|
|
172
|
+
"pyinstaller"], capture_output=True, text=True)
|
|
173
|
+
self._ghi((p.stdout or "") + (p.stderr or "") + "\n")
|
|
174
|
+
if p.returncode != 0:
|
|
175
|
+
self._ghi("✘ Cài PyInstaller thất bại — kiểm tra mạng "
|
|
176
|
+
"hoặc cài thủ công: pip install pyinstaller\n")
|
|
177
|
+
return
|
|
178
|
+
else:
|
|
179
|
+
self._ghi(f" Đã có PyInstaller {r.stdout.strip()}\n")
|
|
180
|
+
|
|
181
|
+
self._ghi("► Bước 2/3: Đang build (thường 1–3 phút)...\n")
|
|
182
|
+
thu_muc = os.path.dirname(os.path.abspath(duong_dan))
|
|
183
|
+
# --paths: bảo đảm PyInstaller luôn tìm thấy package flexgui
|
|
184
|
+
# (cần khi cài kiểu editable / pip install -e)
|
|
185
|
+
duong_goi = os.path.dirname(
|
|
186
|
+
os.path.dirname(os.path.abspath(__file__)))
|
|
187
|
+
lenh = [sys.executable, "-m", "PyInstaller", "--onefile",
|
|
188
|
+
"--windowed", "--name", self.ten,
|
|
189
|
+
"--paths", duong_goi,
|
|
190
|
+
os.path.basename(duong_dan)]
|
|
191
|
+
tien = subprocess.Popen(lenh, cwd=thu_muc,
|
|
192
|
+
stdout=subprocess.PIPE,
|
|
193
|
+
stderr=subprocess.STDOUT, text=True,
|
|
194
|
+
bufsize=1, env=self._moi_truong_build())
|
|
195
|
+
for dong in tien.stdout:
|
|
196
|
+
self._ghi(dong)
|
|
197
|
+
ma = tien.wait()
|
|
198
|
+
|
|
199
|
+
ten_duoi = self.ten + (".exe" if os.name == "nt" else "")
|
|
200
|
+
self._duong_dan_dist = os.path.join(thu_muc, "dist", ten_duoi)
|
|
201
|
+
if ma == 0:
|
|
202
|
+
self._thanh_cong = True
|
|
203
|
+
self._ghi(f"\n✔✔✔ ĐÃ XONG: {self._duong_dan_dist}\n")
|
|
204
|
+
else:
|
|
205
|
+
self._ghi(f"\n✘ BUILD THẤT BẠI (mã thoát {ma}) — xem log "
|
|
206
|
+
"phía trên.\n")
|
|
207
|
+
except Exception:
|
|
208
|
+
import traceback
|
|
209
|
+
self._ghi("\n✘ LỖI BẤT NGỜ:\n" + traceback.format_exc() + "\n")
|
|
210
|
+
finally:
|
|
211
|
+
self._xong = True
|