Загрузка данных
import customtkinter as ctk
from tkinter import ttk, messagebox
import datetime
import os
# Для календаря
try:
from tkcalendar import DateEntry
except ImportError:
DateEntry = None
# Для экспорта в Word
try:
import docx
except ImportError:
docx = None
ctk.set_appearance_mode("System")
ctk.set_default_color_theme("blue")
class ProjectFillerApp(ctk.CTk):
def __init__(self):
super().__init__()
self.title("Наполнение проекта")
# --- Настройка динамического окна ---
screen_height = self.winfo_screenheight()
window_width = 650
# Начальное состояние: только заголовки + отступы
initial_height = 220
x_cord = int((self.winfo_screenwidth() / 2) - (window_width / 2))
y_cord = int((screen_height / 2) - (initial_height / 2))
self.geometry(f"{window_width}x{initial_height}+{x_cord}+{y_cord}")
self.resizable(True, False) # Менять можно только высоту для чистоты анимации
main_container = ctk.CTkFrame(self, corner_radius=10)
main_container.pack(padx=10, pady=10, fill="both", expand=True)
self._build_project_selector(main_container)
accordion_frame = ctk.CTkFrame(main_container)
accordion_frame.pack(fill="both", expand=True, pady=(10, 0))
self.accordion_data = {}
self.currently_open_button = None
# Создание секций
events_item = self._create_accordion_header(accordion_frame, "Открыть для ввода событий")
self._fill_events_section(events_item["content"])
charts_item = self._create_accordion_header(accordion_frame, "Показать графики")
self._fill_charts_section(charts_item["content"])
print_item = self._create_accordion_header(accordion_frame, "Настройка печати")
self._fill_print_section(print_item["content"])
# Привязка команд переключения
for item in [events_item, charts_item, print_item]:
btn = item["button"]
content = item["content"]
self.accordion_data[btn] = {"content": content}
btn.configure(command=lambda b=btn, c=content: self.toggle_section(b, c))
button_frame = ctk.CTkFrame(main_container)
button_frame.pack(fill="x", pady=(10, 0))
save_btn = ctk.CTkButton(button_frame, text="Сохранить изменения", width=150)
save_btn.pack(side="right", padx=10)
cancel_btn = ctk.CTkButton(button_frame, text="Отмена", width=100, fg_color="transparent", border_color="#666666", text_color="#666666")
cancel_btn.pack(side="right")
# ПРИВЯЗКА ОБРАБОТЧИКА ЗАКРЫТИЯ ОКНА:
self.protocol("WM_DELETE_WINDOW", self.on_closing)
# --- УПРАВЛЕНИЕ ОКНОМ И АККОРДЕОНОМ ---
def toggle_section(self, button, content):
is_currently_open = (self.currently_open_button == button)
if self.currently_open_button and not is_currently_open:
prev_content = self.accordion_data[self.currently_open_button]["content"]
prev_content.pack_forget()
self.currently_open_button.configure(fg_color="#34495e")
if is_currently_open:
content.pack_forget()
button.configure(fg_color="#34495e")
self.currently_open_button = None
new_geo = self._calculate_window_geometry(collapsed=True)
else:
content.pack(fill="x", padx=5, pady=(0, 5), anchor="center")
button.configure(fg_color="#2c3e50")
self.currently_open_button = button
new_geo = self._calculate_window_geometry(collapsed=False)
# Используем after_idle для корректного расчета размеров после отрисовки
self.after_idle(lambda g=new_geo: self.geometry(g))
def _calculate_window_geometry(self, collapsed=False):
"""Вычисляет ПОЛНУЮ геометрию окна (Ширина x Высота)"""
# Получаем текущую ширину окна, чтобы она не прыгала
current_width = self.winfo_width()
if current_width == 1: # Если окно еще не нарисовалось, берем заданную из __init__
current_width = 650
header_height = 50 * 3 + 30
if collapsed or not self.currently_open_button:
target_height = header_height + 80
else:
data = self.accordion_data[self.currently_open_button]
content_widget = data["content"]
# Обновляем дерево виджетов перед расчетом высоты
self.update_idletasks()
open_content_height = content_widget.winfo_reqheight()
target_height = header_height + open_content_height + 120
max_h = self.winfo_screenheight() - 50
final_height = min(target_height, max_h)
# ВОЗВРАЩАЕМ СТРОКУ "ШИРИНА x ВЫСОТА"
return f"{current_width}x{final_height}"
# --- БЛОК 1: СЕЛЕКТОР ПРОЕКТА СВЕРХУ ---
def _build_project_selector(self, container):
frame = ctk.CTkFrame(container)
frame.pack(fill="x", pady=(0, 10))
label_project = ctk.CTkLabel(frame, text="Выбрать проект:", anchor="w")
label_project.pack(side="left", padx=(0, 10))
projects_list = ["Проект Альфа", "Бета-версия 2.0", "Гамма-отчет", "Тестовый стенд"]
self.project_selector = ctk.CTkComboBox(
frame,
values=projects_list,
state="readonly",
width=250
)
self.project_selector.set(projects_list[0])
self.project_selector.pack(side="left")
# --- УТИЛИТА ЗАГОЛОВКА ---
def _create_accordion_header(self, parent, title):
item_container = ctk.CTkFrame(parent, fg_color="#f0f2f5", border_color="#d1d8e0", border_width=1, corner_radius=8)
item_container.pack(fill="x", pady=5)
header_btn = ctk.CTkButton(
item_container,
text=title,
height=45,
font=ctk.CTkFont(weight="bold", size=16),
anchor="w",
fg_color="#34495e",
hover_color="#3c5a72",
)
header_btn.pack(fill="x")
content_frame = ctk.CTkFrame(item_container, fg_color="transparent", corner_radius=0)
return {"button": header_btn, "content": content_frame}
# --- БЛОК 2: ОТКРЫТЬ ДЛЯ ВВОДА СОБЫТИЙ ---
def _fill_events_section(self, frame):
form = ctk.CTkFrame(frame, fg_color="transparent")
form.pack(fill="x", expand=True, padx=10, pady=10)
form.grid_columnconfigure(1, weight=1)
# Группа
lbl_group = ctk.CTkLabel(form, text="Выбрать группу:", anchor="w")
lbl_group.grid(row=0, column=0, sticky="w", pady=(0, 10), padx=(0, 10))
cmb_group = ctk.CTkComboBox(form, values=["Группа А", "Группа Б", "Складской учет"], width=200)
cmb_group.set("Группа А")
cmb_group.grid(row=0, column=1, sticky="ew", pady=(0, 10))
# Показатель
lbl_metric = ctk.CTkLabel(form, text="Выбрать показатель:", anchor="w")
lbl_metric.grid(row=1, column=0, sticky="w", pady=(0, 10), padx=(0, 10))
cmb_metric = ctk.CTkComboBox(form, values=["Тип ошибки", "Время простоя", "Кол-во операций"])
cmb_metric.set("Тип ошибки")
cmb_metric.grid(row=1, column=1, sticky="ew", pady=(0, 15))
# Дата и Направленность
meta_frame = ctk.CTkFrame(form, fg_color="transparent")
meta_frame.grid(row=2, column=0, columnspan=2, sticky="ew", pady=(0, 15))
meta_frame.grid_columnconfigure(1, weight=1)
date_label = ctk.CTkLabel(meta_frame, text="Дата события:", anchor="w")
date_label.grid(row=0, column=0, sticky="w", padx=(0, 10))
date_entry = ctk.CTkEntry(meta_frame, placeholder_text="ДД.ММ.ГГГГ", width=120)
date_entry.grid(row=0, column=1, sticky="w")
dir_label = ctk.CTkLabel(meta_frame, text="Направленность:", anchor="w")
dir_label.grid(row=0, column=2, sticky="w", padx=(15, 0))
dir_entry = ctk.CTkEntry(meta_frame, placeholder_text="Входящее", width=120)
dir_entry.grid(row=0, column=3, sticky="w")
# Название
name_label = ctk.CTkLabel(form, text="Название события:", anchor="w")
name_label.grid(row=3, column=0, sticky="w", padx=(0, 10))
event_name_entry = ctk.CTkEntry(form, height=30)
event_name_entry.grid(row=3, column=1, sticky="ew", pady=(0, 15))
# Кнопки действий
btns_frame = ctk.CTkFrame(form, fg_color="transparent")
btns_frame.grid(row=4, column=0, columnspan=2, sticky="ew")
insert_btn = ctk.CTkButton(btns_frame, text="Вставить", width=80)
insert_btn.pack(side="left", padx=5)
add_btn = ctk.CTkButton(btns_frame, text="Добавить", width=80)
add_btn.pack(side="left", padx=5)
edit_btn = ctk.CTkButton(btns_frame, text="Редактировать", width=100)
edit_btn.pack(side="left", padx=5)
del_btn = ctk.CTkButton(btns_frame, text="Удалить", width=80, fg_color="#D32F2F", hover_color="#C62828")
del_btn.pack(side="left", padx=5)
# Таблица
table_frame = ctk.CTkFrame(form)
table_frame.grid(row=5, column=0, columnspan=2, sticky="nsew", pady=(10, 0))
table_frame.grid_rowconfigure(0, weight=1)
table_frame.grid_columnconfigure(0, weight=1)
columns = ("#", "code", "date", "name")
tree = ttk.Treeview(table_frame, columns=columns, show="headings", height=5)
tree.heading("#", text="№ п/п"); tree.column("#", width=60, stretch=False, anchor="center")
tree.heading("code", text="Код"); tree.column("code", width=80, stretch=False, anchor="center")
tree.heading("date", text="Дата"); tree.column("date", width=100, stretch=False, anchor="center")
tree.heading("name", text="Название"); tree.column("name", minwidth=200, anchor="w")
scrollbar = ctk.CTkScrollbar(table_frame, orientation="vertical", command=tree.yview)
tree.configure(yscrollcommand=scrollbar.set)
tree.grid(row=0, column=0, sticky="nsew")
scrollbar.grid(row=0, column=1, sticky="ns")
for i in range(1, 11):
tree.insert("", "end", values=(i, f"C{i:03}", f"01.07.2026", f"Эталонное название строки {i}"))
# --- Поиск ---
search_frame = ctk.CTkFrame(form, fg_color="transparent")
search_frame.grid(row=6, column=0, columnspan=2, sticky="ew", pady=(10, 0))
# Настраиваем колонки ВНУТРИ search_frame
search_frame.grid_columnconfigure(0, weight=0) # Лейбл
search_frame.grid_columnconfigure(1, weight=1) # Поле ввода (растягивается)
search_frame.grid_columnconfigure(2, weight=0) # Кнопка
code_search_label = ctk.CTkLabel(search_frame, text="Код события:", anchor="w")
code_search_label.grid(row=0, column=0, sticky="w", padx=(0, 5), pady=5)
code_search_entry = ctk.CTkEntry(search_frame, placeholder_text="Например: INV-2026-001")
code_search_entry.grid(row=0, column=1, sticky="ew", padx=(0, 5), pady=5)
find_btn = ctk.CTkButton(search_frame, text="Найти", width=80)
find_btn.grid(row=0, column=2, sticky="e", pady=5)
# --- БЛОК 3: ПОКАЗАТЬ ГРАФИКИ ---
def _fill_charts_section(self, frame):
if DateEntry is None:
error_lbl = ctk.CTkLabel(frame, text="Ошибка: установите библиотеку\npip install tkcalendar", text_color="red")
error_lbl.pack(expand=True)
return
form = ctk.CTkFrame(frame, fg_color="transparent")
form.pack(fill="x", expand=True, padx=10, pady=10)
form.grid_columnconfigure(1, weight=1)
lbl_group = ctk.CTkLabel(form, text="Выбрать группу:", anchor="w")
lbl_group.grid(row=0, column=0, sticky="w", pady=(0, 10), padx=(0, 10))
cmb_group = ctk.CTkComboBox(form, values=["Группа А", "Группа Б", "Все данные"])
cmb_group.set("Все данные")
cmb_group.grid(row=0, column=1, sticky="ew", pady=(0, 10))
lbl_date = ctk.CTkLabel(form, text="Дата начала:", anchor="w")
lbl_date.grid(row=1, column=0, sticky="w", pady=(0, 10), padx=(0, 10))
date_entry = DateEntry(
master=form,
width=18,
background='darkblue',
foreground='white',
locale='ru_RU',
date_pattern='dd.MM.yyyy'
)
date_entry.delete(0, "end")
date_entry.insert(0, datetime.date.today().strftime("%d.%m.%Y"))
date_entry.grid(row=1, column=1, sticky="w", pady=(0, 10))
lbl_step_event = ctk.CTkLabel(form, text="Шаг событий:", anchor="w")
lbl_step_event.grid(row=2, column=0, sticky="w", pady=(0, 10), padx=(0, 10))
cmb_step_event = ctk.CTkComboBox(form, values=["Авто", "Час", "Смена", "День"])
cmb_step_event.set("Авто")
cmb_step_event.grid(row=2, column=1, sticky="ew", pady=(0, 10))
lbl_step_days = ctk.CTkLabel(form, text="Шаг по дням:", anchor="w")
lbl_step_days.grid(row=3, column=0, sticky="w", pady=(0, 20), padx=(0, 10))
cmb_step_days = ctk.CTkComboBox(form, values=["1 день", "3 дня", "Неделя", "Месяц"])
cmb_step_days.set("1 день")
cmb_step_days.grid(row=3, column=1, sticky="ew", pady=(0, 20))
lbl_chart = ctk.CTkLabel(frame, text="Образец графика:", anchor="w")
lbl_chart.pack(anchor="w", padx=10, pady=(10, 5))
chart_placeholder = ctk.CTkFrame(frame, height=250, fg_color="transparent")
chart_placeholder.pack(fill="x", padx=10, pady=(0, 15))
try:
import matplotlib.pyplot as plt
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg
fig, ax = plt.subplots(figsize=(6, 3), dpi=100)
ax.plot([0, 1, 2, 3], [10, 40, 20, 50], marker='o', color='#3b82f6')
ax.fill_between([0, 1, 2, 3], [10, 40, 20, 50], alpha=0.2, color='#3b82f6')
ax.set_title('Динамика показателя', fontsize=10)
ax.tick_params(axis='x', labelrotation=45)
ax.grid(True, linestyle='--', alpha=0.6)
plt.tight_layout()
canvas = FigureCanvasTkAgg(fig, master=chart_placeholder)
canvas.draw()
canvas.get_tk_widget().pack(fill="both", expand=True)
except ImportError:
fallback_label = ctk.CTkLabel(chart_placeholder, text="[График]\nУстановите matplotlib", justify="center")
fallback_label.pack(expand=True)
btns_frame = ctk.CTkFrame(frame, fg_color="transparent")
btns_frame.pack(fill="x", padx=10, pady=(0, 15))
show_btn = ctk.CTkButton(btns_frame, text="Показать", width=120)
show_btn.pack(side="left", expand=True, padx=(0, 5))
copy_btn = ctk.CTkButton(btns_frame, text="Копировать", width=120, fg_color="#1F8A5B", hover_color="#165C3E")
copy_btn.pack(side="left", expand=True, padx=(5, 0))
# --- БЛОК 4: НАСТРОЙКА ПЕЧАТИ ---
def _fill_print_section(self, frame):
if DateEntry is None:
error_lbl = ctk.CTkLabel(frame, text="Ошибка: установите библиотеку\npip install tkcalendar", text_color="red")
error_lbl.pack(expand=True)
return
form = ctk.CTkFrame(frame, fg_color="transparent")
form.pack(fill="x", expand=True, padx=10, pady=10)
form.grid_columnconfigure(1, weight=1)
lbl_group = ctk.CTkLabel(form, text="Выбрать группу:", anchor="w")
lbl_group.grid(row=0, column=0, sticky="w", pady=(0, 10), padx=(0, 10))
cmb_group = ctk.CTkComboBox(form, values=["Группа А", "Группа Б", "Складской учет"])
cmb_group.set("Группа А")
cmb_group.grid(row=0, column=1, sticky="ew", pady=(0, 10))
lbl_date = ctk.CTkLabel(form, text="Выбрать дату:", anchor="w")
lbl_date.grid(row=1, column=0, sticky="w", pady=(0, 10), padx=(0, 10))
date_entry = DateEntry(
master=form,
width=18,
background='darkblue',
foreground='white',
locale='ru_RU',
date_pattern='dd.MM.yyyy'
)
date_entry.delete(0, "end")
date_entry.insert(0, datetime.date.today().strftime("%d.%m.%Y"))
date_entry.grid(row=1, column=1, sticky="w", pady=(0, 10))
lbl_orientation = ctk.CTkLabel(form, text="Ориентация страницы:", anchor="w")
lbl_orientation.grid(row=2, column=0, sticky="w", pady=(0, 10), padx=(0, 10))
cmb_orientation = ctk.CTkComboBox(form, values=["Книжная", "Альбомная"])
cmb_orientation.set("Книжная")
cmb_orientation.grid(row=2, column=1, sticky="ew", pady=(0, 20))
btn_frame = ctk.CTkFrame(frame, fg_color="transparent")
btn_frame.pack(fill="x", padx=10, pady=(0, 15))
word_btn = ctk.CTkButton(
btn_frame,
text="Печать в Word",
width=160,
fg_color="#1E87F0",
hover_color="#1869C0",
command=lambda: self._generate_word_report(
cmb_group.get(),
date_entry.get(),
cmb_orientation.get()
)
)
word_btn.pack(side="left", expand=True)
# --- ЛОГИКА ЭКСПОРТА В WORD ---
def _generate_word_report(self, group, report_date, orientation):
if docx is None:
messagebox.showerror("Библиотека не найдена", "Для экспорта нужна библиотека 'python-docx'.\nВыполните:\npip install python-docx")
return
safe_group = "".join(x for x in group if x.isalnum() or x in " _-")
filename = f"Отчет_{safe_group}_{report_date}.docx"
document = docx.Document()
document.add_heading(f'Отчет по группе: {group}', level=1)
p = document.add_paragraph('Дата создания: ')
p.add_run(datetime.datetime.now().strftime('%d.%m.%Y %H:%M')).italic = True
document.add_heading('Параметры отчета:', level=2)
document.add_paragraph(f"• Выбранная группа: {group}")
document.add_paragraph(f"• Дата данных: {report_date}")
document.add_paragraph(f"• Ориентация: {orientation}")
try:
document.save(filename)
messagebox.showinfo("Успех", f"Файл успешно создан в папке скрипта:\n{filename}")
except Exception as e:
messagebox.showerror("Ошибка записи", f"Не удалось сохранить файл:\n{e}")
def on_closing(self):
"""Жесткое завершение процесса"""
# 1. Пытаемся мягко очистить очередь задач
try:
self.after_cancel("all")
except Exception:
pass
# 2. Уничтожаем виджеты вручную (помогает сборщику мусора)
for item in list(self.accordion_data.values()):
try:
content = item.get("content")
if content:
content.destroy()
except Exception:
pass
# 3. Разрушаем само окно
try:
self.destroy()
except Exception:
pass
# === ГЛАВНОЕ ИСПРАВЛЕНИЕ ===
# 4. Принудительно выходим из Python с кодом 0 (успех)
# Это игнорирует все висящие потоки и мгновенно возвращает управление консоли.
import sys
os._exit(0)
import sys
import os
class HiddenStderr:
"""Перенаправляет stderr в null-устройство (пустоту)"""
def __enter__(self):
# Сохраняем старый дескриптор потока ошибок (обычно 2)
self.old_stderr_fd = os.dup(2)
# Создаем ссылку на "черную дыру" системы
devnull = os.open(os.devnull, os.O_WRONLY)
# Заменяем стандартный поток ошибок (дескриптор 2) на /dev/null
os.dup2(devnull, 2)
# Закрываем временный файловый дескриптор
os.close(devnull)
# Также подменяем объект sys.stderr для чистого вывода питоновских print()
self.old_stderr = sys.stderr
sys.stderr = open(os.devnull, 'w')
def __exit__(self, exc_type, exc_val, exc_tb):
# Возвращаем оригинальный дескриптор обратно
os.dup2(self.old_stderr_fd, 2)
os.close(self.old_stderr_fd)
# Возвращаем объект sys.stderr
if self.old_stderr:
sys.stderr.close() # закрываем заглушку
sys.stderr = self.old_stderr
if __name__ == "__main__":
ctk.set_widget_scaling(1.0)
ctk.set_window_scaling(1.0)
app = ProjectFillerApp()
app.protocol("WM_DELETE_WINDOW", app.on_closing)
# Запускаем приложение внутри "тихого" режима
with HiddenStderr():
app.mainloop()