327 lines
16 KiB
Python
327 lines
16 KiB
Python
"""
|
||
Программа 17: Заполнение формы по выявленным-1
|
||
Графический интерфейс (tkinter) + запуск обработки.
|
||
"""
|
||
|
||
import sys
|
||
import threading
|
||
import traceback
|
||
import tkinter as tk
|
||
from tkinter import filedialog, messagebox, ttk
|
||
from pathlib import Path
|
||
|
||
from services.data_loader import load_ki, load_table2, build_mif_index, load_table3
|
||
from services.form_filler import fill_form
|
||
from services.output_generator import make_filename, combine_forms, generate_table4
|
||
|
||
# ─────────────────────────────── Цвета / стиль ──────────────────────────── #
|
||
|
||
BG = "#1C3557" # тёмно-синий фон
|
||
BG_FRAME = "#243F6B" # фон панелей
|
||
FG = "#FFFFFF" # белый текст
|
||
FG_DIM = "#A8C4E0" # приглушённый текст
|
||
BTN_RUN = "#2E7D32" # зелёная кнопка «Запустить»
|
||
BTN_HELP = "#1565C0" # синяя кнопка «Справка»
|
||
FONT = ("Arial", 11)
|
||
FONT_H = ("Arial", 13, "bold")
|
||
FONT_LOG = ("Courier New", 9)
|
||
|
||
# ─────────────────────────────── Основная логика ────────────────────────── #
|
||
|
||
def run_processing(table1_path: str, table2_path: str, mif_dir: str,
|
||
template_path: str, output_dir: str,
|
||
log_callback, progress_callback, done_callback,
|
||
table3_path: str = ""):
|
||
"""Выполняется в отдельном потоке."""
|
||
try:
|
||
out = Path(output_dir)
|
||
out.mkdir(parents=True, exist_ok=True)
|
||
|
||
log_callback("[1/4] Загрузка исходных данных...")
|
||
progress_callback(10)
|
||
ki = load_ki(table1_path)
|
||
t2_rows = load_table2(table2_path)
|
||
mif_idx = build_mif_index(mif_dir)
|
||
log_callback(f" КИ: {next(iter(ki.values()), '?')}")
|
||
log_callback(f" Таблица 2: {len(t2_rows)} строк")
|
||
log_callback(f" MIF-файлов: {len(mif_idx)}")
|
||
|
||
t3_images = {}
|
||
t3_scales = {}
|
||
if table3_path and Path(table3_path).is_file():
|
||
try:
|
||
t3_images, t3_scales = load_table3(table3_path)
|
||
log_callback(f" Таблица 3 (схемы): {len(t3_images)} записей")
|
||
except Exception as exc:
|
||
log_callback(f" Предупреждение: Не удалось загрузить Таблицу 3: {exc}")
|
||
|
||
log_callback("\n[2/4] Генерация форм...")
|
||
generated, errors = [], []
|
||
total = sum(1 for r in t2_rows if r[0])
|
||
|
||
for i, t2_row in enumerate(t2_rows, 1):
|
||
zu_file = t2_row[0] if t2_row[0] else ""
|
||
kv = t2_row[1] if t2_row[1] else ""
|
||
if not zu_file:
|
||
continue
|
||
filename = make_filename(str(kv), str(zu_file))
|
||
out_path = out / filename
|
||
log_callback(f" [{len(generated)+len(errors)+1}/{total}] {filename} ...", end="")
|
||
try:
|
||
scheme_png = t3_images.get(str(zu_file).strip())
|
||
# fallback: если в таблице 3 нет изображения, попробуем найти файл в папке с MIF (mif_dir)
|
||
if scheme_png is None:
|
||
found = None
|
||
mif_root = Path(mif_dir)
|
||
if mif_root.exists():
|
||
for ext in ('.png', '.jpg', '.jpeg'):
|
||
for p in mif_root.rglob(f'*{str(zu_file).strip()}*'):
|
||
if p.suffix.lower() == ext:
|
||
found = p
|
||
break
|
||
if found:
|
||
break
|
||
if found:
|
||
try:
|
||
scheme_png = found.read_bytes()
|
||
log_callback(f" image loaded from {found}")
|
||
except Exception:
|
||
scheme_png = None
|
||
oks_file_name = str(t2_row[10]).strip() if len(t2_row) > 10 and t2_row[10] else ""
|
||
oks_scheme_png = t3_images.get(oks_file_name) if oks_file_name else None
|
||
zu_scale = t3_scales.get(str(zu_file).strip())
|
||
oks_scale = t3_scales.get(oks_file_name) if oks_file_name else None
|
||
loc_zu = str(t2_row[4]).strip() if len(t2_row) > 4 and t2_row[4] else ""
|
||
fill_form(template_path, str(out_path), ki, t2_row, mif_idx,
|
||
zu_scheme=scheme_png, oks_scheme=oks_scheme_png,
|
||
zu_scale=zu_scale, oks_scale=oks_scale)
|
||
generated.append({"name": filename, "path": str(out_path),
|
||
"kv": str(kv), "zu_file": str(zu_file),
|
||
"loc_zu": loc_zu})
|
||
log_callback(" OK")
|
||
except Exception as exc:
|
||
log_callback(f" ОШИБКА: {exc}")
|
||
errors.append((filename, str(exc)))
|
||
progress_callback(10 + int(70 * (len(generated) + len(errors)) / max(total, 1)))
|
||
|
||
log_callback(f"\n Создано: {len(generated)}, ошибок: {len(errors)}")
|
||
|
||
if generated:
|
||
log_callback("\n[3/4] Создание объединённого файла...")
|
||
progress_callback(82)
|
||
combined = out / "ВСЕ_ФОРМЫ.xlsx"
|
||
try:
|
||
combine_forms([r["path"] for r in generated], str(combined))
|
||
log_callback(f" → {combined}")
|
||
except Exception as exc:
|
||
log_callback(f" ОШИБКА объединения: {exc}")
|
||
|
||
log_callback("\n[4/4] Создание Таблицы 4...")
|
||
progress_callback(95)
|
||
t4 = out / "Таблица4_Перечень_форм.xlsx"
|
||
generate_table4(generated, str(t4))
|
||
log_callback(f" → {t4}")
|
||
|
||
progress_callback(100)
|
||
done_callback(generated, errors, str(out))
|
||
|
||
except Exception as exc:
|
||
log_callback(f"\nКРИТИЧЕСКАЯ ОШИБКА:\n{traceback.format_exc()}")
|
||
done_callback([], [(str(exc), str(exc))], output_dir)
|
||
|
||
# ─────────────────────────────── GUI ────────────────────────────────────── #
|
||
|
||
class App(tk.Tk):
|
||
def __init__(self):
|
||
super().__init__()
|
||
self.title("Программа 17 — Выявленные-1")
|
||
self.geometry("700x600")
|
||
self.minsize(600, 550)
|
||
self.configure(bg=BG)
|
||
self._build_ui()
|
||
|
||
# ── Построение интерфейса ────────────────────────────────────────────── #
|
||
|
||
def _build_ui(self):
|
||
# Заголовок
|
||
tk.Label(self, text="Программа 17: Заполнение формы по Выявленным-1",
|
||
font=FONT_H, bg=BG, fg=FG).pack(pady=(14, 8))
|
||
|
||
# Карточка с полями ввода
|
||
card = tk.Frame(self, bg=BG_FRAME, bd=0)
|
||
card.pack(fill=tk.X, padx=18, pady=4)
|
||
|
||
self.var_t1 = self._add_field(card, "Таблица 1 (КИ):", "xlsx", 0)
|
||
self.var_t2 = self._add_field(card, "Таблица 2 (данные):", "xlsx", 1)
|
||
self.var_t3 = self._add_field(card, "Таблица 3 (схемы):", "xlsx", 2)
|
||
self.var_mif = self._add_field(card, "Папка MIF:", "dir", 3)
|
||
self.var_template = self._add_field(card, "Шаблон XLSX:", "xlsx", 4)
|
||
self.var_output = self._add_field(card, "Папка вывода:", "dir", 5)
|
||
|
||
# Дефолты
|
||
base = Path(__file__).parent
|
||
data = base / "data"
|
||
self.var_t1.set(str(data / "Таблица1 КИ-Неустроева.xlsx"))
|
||
self.var_t2.set(str(data / "Таблица 2.xlsx"))
|
||
self.var_t3.set("")
|
||
self.var_mif.set(str(data))
|
||
self.var_template.set(str(data / "ШАБЛОН ВЫЯВЛЕННЫЕ-2.xlsx"))
|
||
self.var_output.set(str(base / "output"))
|
||
|
||
# Прогресс-бар
|
||
pf = tk.Frame(self, bg=BG)
|
||
pf.pack(fill=tk.X, padx=18, pady=(10, 2))
|
||
self.progress = ttk.Progressbar(pf, length=600, mode="determinate")
|
||
self.progress.pack(fill=tk.X)
|
||
self.progress_label = tk.Label(pf, text="Готов к работе",
|
||
font=("Arial", 9), bg=BG, fg=FG_DIM)
|
||
self.progress_label.pack(anchor="w")
|
||
|
||
# Лог
|
||
lf = tk.Frame(self, bg=BG)
|
||
lf.pack(fill=tk.BOTH, expand=True, padx=18, pady=(4, 4))
|
||
tk.Label(lf, text="Журнал:", font=("Arial", 9), bg=BG, fg=FG_DIM).pack(anchor="w")
|
||
self.log_text = tk.Text(lf, font=FONT_LOG, bg="#0D1F33", fg="#C8E6FF",
|
||
height=10, state=tk.DISABLED, wrap=tk.WORD)
|
||
sb = tk.Scrollbar(lf, command=self.log_text.yview)
|
||
self.log_text.configure(yscrollcommand=sb.set)
|
||
sb.pack(side=tk.RIGHT, fill=tk.Y)
|
||
self.log_text.pack(fill=tk.BOTH, expand=True)
|
||
|
||
# Кнопки
|
||
bf = tk.Frame(self, bg=BG)
|
||
bf.pack(fill=tk.X, padx=18, pady=(6, 12))
|
||
tk.Button(bf, text="Справка", command=self._show_help,
|
||
font=FONT, bg=BTN_HELP, fg=FG,
|
||
padx=14, pady=8, relief=tk.FLAT).pack(side=tk.LEFT)
|
||
self.btn_run = tk.Button(bf, text="▶ Запустить",
|
||
command=self._start, font=FONT_H,
|
||
bg=BTN_RUN, fg=FG, padx=20, pady=8, relief=tk.FLAT)
|
||
self.btn_run.pack(side=tk.RIGHT)
|
||
|
||
def _add_field(self, parent, label: str, kind: str, row_idx: int):
|
||
"""Добавить строку: метка + поле + кнопка «...»."""
|
||
var = tk.StringVar()
|
||
tk.Label(parent, text=label, font=FONT, bg=BG_FRAME, fg=FG,
|
||
anchor="w").grid(row=row_idx * 2, column=0, columnspan=3,
|
||
sticky="w", padx=12, pady=(8, 0))
|
||
entry = tk.Entry(parent, textvariable=var, font=FONT, width=55)
|
||
entry.grid(row=row_idx * 2 + 1, column=0, padx=(12, 4), pady=(0, 2), sticky="ew")
|
||
btn = tk.Button(parent, text="…", font=FONT, width=3,
|
||
command=lambda k=kind, v=var: self._browse(k, v))
|
||
btn.grid(row=row_idx * 2 + 1, column=1, padx=(0, 12), pady=(0, 2))
|
||
parent.columnconfigure(0, weight=1)
|
||
return var
|
||
|
||
def _browse(self, kind: str, var: tk.StringVar):
|
||
if kind == "dir":
|
||
path = filedialog.askdirectory(title="Выберите папку")
|
||
else:
|
||
path = filedialog.askopenfilename(
|
||
title="Выберите файл",
|
||
filetypes=[("Excel файлы", "*.xlsx *.xls"), ("Все файлы", "*.*")]
|
||
)
|
||
if path:
|
||
var.set(path)
|
||
|
||
# ── Запуск ──────────────────────────────────────────────────────────── #
|
||
|
||
def _start(self):
|
||
t1 = self.var_t1.get().strip()
|
||
t2 = self.var_t2.get().strip()
|
||
t3 = self.var_t3.get().strip()
|
||
mif = self.var_mif.get().strip()
|
||
tmpl = self.var_template.get().strip()
|
||
out = self.var_output.get().strip()
|
||
|
||
missing = [name for name, v in [("Таблица 1", t1), ("Таблица 2", t2),
|
||
("Папка MIF", mif), ("Шаблон", tmpl),
|
||
("Папка вывода", out)] if not v]
|
||
if missing:
|
||
messagebox.showerror("Ошибка", "Не заполнены поля:\n• " + "\n• ".join(missing))
|
||
return
|
||
|
||
self._log_clear()
|
||
self._set_progress(0, "Запуск...")
|
||
self.btn_run.config(state=tk.DISABLED)
|
||
|
||
thread = threading.Thread(
|
||
target=run_processing,
|
||
args=(t1, t2, mif, tmpl, out,
|
||
self._log_append,
|
||
lambda v: self.after(0, self._set_progress, v, None),
|
||
self._on_done),
|
||
kwargs={"table3_path": t3},
|
||
daemon=True,
|
||
)
|
||
thread.start()
|
||
|
||
# ── Обратные вызовы из потока ────────────────────────────────────────── #
|
||
|
||
def _log_append(self, text: str, end: str = "\n"):
|
||
def _do():
|
||
self.log_text.config(state=tk.NORMAL)
|
||
self.log_text.insert(tk.END, text + end)
|
||
self.log_text.see(tk.END)
|
||
self.log_text.config(state=tk.DISABLED)
|
||
self.after(0, _do)
|
||
|
||
def _log_clear(self):
|
||
self.log_text.config(state=tk.NORMAL)
|
||
self.log_text.delete("1.0", tk.END)
|
||
self.log_text.config(state=tk.DISABLED)
|
||
|
||
def _set_progress(self, value: int, label_text=None):
|
||
self.progress["value"] = value
|
||
if label_text is not None:
|
||
self.progress_label.config(text=label_text)
|
||
elif value == 100:
|
||
self.progress_label.config(text="Готово!")
|
||
|
||
def _on_done(self, generated: list, errors: list, out_dir: str):
|
||
def _do():
|
||
self.btn_run.config(state=tk.NORMAL)
|
||
if errors and not generated:
|
||
messagebox.showerror("Ошибка", f"Обработка завершилась с ошибками:\n{errors[0][1]}")
|
||
return
|
||
msg = (
|
||
f"Обработка завершена!\n\n"
|
||
f"Создано форм: {len(generated)}\n"
|
||
f"Ошибок: {len(errors)}\n\n"
|
||
f"Результаты в папке:\n{out_dir}"
|
||
)
|
||
if errors:
|
||
msg += f"\n\nФайлы с ошибками ({len(errors)}):\n" + \
|
||
"\n".join(f"• {n}" for n, _ in errors[:10])
|
||
messagebox.showinfo("Готово", msg)
|
||
self.after(0, _do)
|
||
|
||
# ── Справка ─────────────────────────────────────────────────────────── #
|
||
|
||
def _show_help(self):
|
||
messagebox.showinfo("Справка", """\
|
||
Программа 17: Заполнение формы по Выявленным-1
|
||
|
||
Поля ввода:
|
||
• Таблица 1 — файл xlsx с данными кадастрового инженера
|
||
• Таблица 2 — файл xlsx со списком выявленных объектов
|
||
• Таблица 3 — файл xlsx со схемами (необязательно)
|
||
• Папка MIF — папка с MIF-файлами контуров (*.mif)
|
||
• Шаблон — файл шаблона XLSX
|
||
• Папка вывода — куда сохранять результаты
|
||
|
||
Результаты:
|
||
• Отдельный xlsx для каждой строки Таблицы 2
|
||
• ВСЕ_ФОРМЫ.xlsx — все формы на одном листе
|
||
• Таблица4_Перечень_форм.xlsx — перечень файлов
|
||
|
||
Шрифт всех ячеек: Times New Roman 11 pt.
|
||
""")
|
||
|
||
|
||
# ─────────────────────────────── Точка входа ────────────────────────────── #
|
||
|
||
if __name__ == "__main__":
|
||
app = App()
|
||
app.mainloop()
|