Загрузка данных
# -*- coding: utf-8 -*-
# main.py
import os
import sys
import tkinter as tk
from tkinter import messagebox
from tkinter import ttk
from PIL import Image, ImageTk
from gui.group_manager import GroupManager
from gui.indicator_manager import IndicatorManager
# --- Импорты из папки gui ---
from gui.interface_expert import InterfaceExpert
from gui.project_manager import ProjectManager
from modules.adminmanager import DatabaseWindow, UsersWindow
from modules.Aggregate_Excel import AggregateFilesWindow
# --- Импорты из папки modules ---
from modules.Export_Excel import ExportMatrixWindow
from modules.login import ChangeRoleWindow, LoginWindow
from modules.project_archive import ArchiveWindow
from modules.project_create import CreateProjectWindow
from modules.project_event import ProjectEventWindow
from modules.database_module import initialize_database
from modules.project_list import ProjectListWindow
from modules.base_app_window import show_modal_dialog
# --- Константы цветов ---
HEADER_FOOTER_BG_COLOR = "#007BFF"
BUTTON_TEXT_COLOR = "black"
ORANGE_TEXT_COLOR = "#FFA500"
COMMON_FRAME_BG_COLOR = "#EEEEEE"
class MainWindow(tk.Tk):
def __init__(self):
super().__init__()
self.title("ДОЗОР -- Heri-Hodie-Cras")
self.geometry("450x360")
self.resizable(False, False)
self.configure(bg=COMMON_FRAME_BG_COLOR)
self.current_user = {}
self.project_manager = ProjectManager()
self.is_master_db_active = tk.BooleanVar(value=False)
# --- ОСНОВНОЙ КОНТЕЙНЕР (как в тестовом файле) ---
# Убираем сложный main_container и используем один main_frame
main_frame = tk.Frame(self, bg=COMMON_FRAME_BG_COLOR)
main_frame.pack(padx=2, pady=2, fill='both', expand=True)
# --- КОНФИГУРАЦИЯ СЕТКИ ---
for i in range(6):
main_frame.grid_rowconfigure(i, weight=0)
for j in range(2):
main_frame.grid_columnconfigure(j, weight=0)
# --- НАСТРОЙКА ШРИФТА И РАЗМЕРА КНОПОК ---
button_width = 20
button_height = 2
button_font = ("Arial", 12, "bold")
# --- ЛЕВАЯ ПАНЕЛЬ: КНОПКИ 1-5 ---
# Кнопка 1: Администратор
btn1 = tk.Button(main_frame,
text="Администратор",
width=button_width,
height=button_height,
font=button_font,
relief="raised",
borderwidth=2,
command=lambda: self.open_admin_menu())
btn1.grid(row=0, column=0, padx=5, pady=5)
# Кнопка 2: Менеджер базы
btn2 = tk.Button(main_frame,
text="Менеджер базы",
width=button_width,
height=button_height,
font=button_font,
relief="raised",
borderwidth=2,
command=lambda: self.open_base_manager())
btn2.grid(row=1, column=0, padx=5, pady=5)
# Кнопка 3: Эксперты
btn3 = tk.Button(main_frame,
text="Эксперты",
width=button_width,
height=button_height,
font=button_font,
relief="raised",
borderwidth=2,
command=lambda: self.open_experts_menu())
btn3.grid(row=2, column=0, padx=5, pady=5)
# Кнопка 4: Проект
btn4 = tk.Button(main_frame,
text="Проект",
width=button_width,
height=button_height,
font=button_font,
relief="raised",
borderwidth=2,
command=lambda: self.create_file())
btn4.grid(row=3, column=0, padx=5, pady=5)
# Кнопка 5: Прогнозирование
btn5 = tk.Button(main_frame,
text="Прогнозирование",
width=button_width,
height=button_height,
font=button_font,
relief="raised",
borderwidth=2,
command=lambda: self.open_export_matrix_window())
btn5.grid(row=4, column=0, padx=5, pady=(5, 15)) # Добавили pady внизу для красоты
# --- ПРАВАЯ ПАНЕЛЬ: КНОПКИ 6-7 И КАРТИНКА ---
# Картинка (Кнопка 6 и 7 будут под ней)
picture_frame = tk.Frame(main_frame) # Убрали фон bg="#d9ead3"
picture_frame.grid(row=0, column=1, rowspan=3, sticky="nsew", ipadx=5, ipady=5)
current_dir = os.path.dirname(os.path.abspath(__file__))
image_path = os.path.join(current_dir, "images", "Сова1.png")
if hasattr(self, "load_and_resize_image"):
self.tk_image = self.load_and_resize_image(image_path, 300, 170)
if self.tk_image:
label = tk.Label(picture_frame, image=self.tk_image)
label.image = self.tk_image # Сохраняем ссылку
label.pack()
# Кнопка 6: Руководство
btn6 = tk.Button(main_frame,
text="Руководство",
width=button_width,
height=button_height,
font=button_font,
relief="raised",
borderwidth=2,
command=self.open_user_manual) # <-- ОШИБКА: ЗДЕСЬ БЫЛ ОТСТУП
btn6.grid(row=3, column=1, padx=5, pady=(5, 0))
# Кнопка 7: О программе
btn7 = tk.Button(main_frame,
text="О программе",
width=button_width,
height=button_height,
font=button_font,
relief="raised",
borderwidth=2)
btn7.grid(row=4, column=1, padx=5, pady=(5, 15)) # pady для симметрии с левой панелью
# --- НИЖНЯЯ ЧАСТЬ: ПОДВАЛ ---
footer_frame = tk.Frame(main_frame, bg="#0078D7")
footer_frame.grid(row=5, column=0, columnspan=2, sticky="ew", pady=(5,0))
footer_label = tk.Label(
footer_frame,
text="Copyright © Минск, НИИ, 2027 год",
fg="#FF8C00",
bg="#0078D7",
font=( "Arial", 12, "bold")
)
footer_label.pack(expand=True)
# --- МЕТОДЫ ДЛЯ ОТКРЫТИЯ ОКОН ---
def open_user_manual(self):
"""
Открывает файл 'Руководство.chm' из корня проекта.
"""
# 1. Определяем путь к файлу.
# os.path.abspath(__file__) - это путь к текущему файлу (main.py)
# os.path.dirname(...) - это папка, где лежит main.py (корень проекта)
project_root = os.path.dirname(os.path.abspath(__file__))
manual_path = os.path.join(project_root, "Руководство.chm")
# 2. Проверяем, существует ли файл
if os.path.exists(manual_path):
try:
# 3. Используем os.startfile, чтобы попросить Windows открыть файл
# Это самый надежный способ для .chm файлов.
os.startfile(manual_path)
except Exception as e:
messagebox.showerror("Ошибка", f"Не удалось открыть файл справки: {e}")
else:
messagebox.showerror("Файл не найден", "Файл 'Руководство.chm' не найден в корне программы.")
def load_and_resize_image(self, path, max_width, max_height):
"""Загружает и изменяет размер изображения."""
try:
original_img = Image.open(path)
ratio = min(
max_width / original_img.width, max_height / original_img.height
)
resized_img = original_img.resize(
(int(original_img.width * ratio), int(original_img.height * ratio)),
resample=Image.LANCZOS,
)
return ImageTk.PhotoImage(resized_img)
except FileNotFoundError:
print(f"[WARNING] Изображение не найдено по пути: {path}")
# Возвращаем None или заглушку, чтобы программа не упала
return None
# --- НОВЫЙ КОД, КОТОРЫЙ НУЖНО ВСТАВИТЬ В MAIN.PY ---
# 1. Добавьте этот метод в ваш главный класс (обычно он называется как-то вроде App или MainWindow)
def _on_child_close(self, event=None):
# Эта строчка "разблокирует" главное окно
self.wm_attributes('-disabled', 0)
# 2. Замените ваши старые функции на эти новые:
# ВСТАВЬТЕ ЭТОТ НОВЫЙ МЕТОД В ВАШ main.py
def open_create_project_window(self):
# Создаем экземпляр окна. Передаем self (главное окно) как родителя.
# Если в конструкторе CreateProjectWindow не нужен parent, просто оставьте как есть.
# Но лучше, если он есть.
create_window = CreateProjectWindow(self.project_manager)
# Делаем новое окно модальным (блокируем главное)
create_window.window.grab_set()
# Ждем, пока окно будет уничтожено.
# Теперь это безопасно, потому что мы исправили класс окна.
self.wait_window(create_window.window)
# После того как wait_window завершится (окно закрыто), снимаем блокировку.
self.wm_attributes('-disabled', 0)
def open_archive_window(self):
window = ArchiveWindow()
tw = window.window
tw.protocol("WM_DELETE_WINDOW", lambda: self._on_child_close())
self.wm_attributes('-disabled', 1)
def open_group_manager(self):
# Проверка прав доступа остается без изменений
if self.current_user.get("role") != "Администратор":
messagebox.showwarning(
"Доступ запрещён", "Только администратор может управлять группами."
)
return
# --- НАЧАЛО ИСПРАВЛЕННОГО КОДА ---
group_manager = GroupManager(self)
# 1. Отключаем главное окно
self.wm_attributes('-disabled', 1)
# 2. Привязываем событие закрытия окна к методу, который его "отпустит"
group_manager.bind("<Destroy>", lambda e: self._on_child_close())
# --- КОНЕЦ ИСПРАВЛЕННОГО КОДА ---
def open_indicator_manager(self):
indicator_manager = IndicatorManager(self)
# --- Блок для позиционирования окна (оставляем как есть) ---
self.update_idletasks()
parent_x = self.winfo_x()
parent_y = self.winfo_y()
parent_w = self.winfo_width()
parent_h = self.winfo_height()
w = indicator_manager.winfo_width()
h = indicator_manager.winfo_height()
x = parent_x + (parent_w - w) // 2
y = parent_y + (parent_h - h) // 2
indicator_manager.geometry(f"+{x}+{y}")
# --- Конец блока позиционирования ---
# --- НОВОЕ: Отключаем главное окно ---
# Это делает главное окно неактивным (серым), пока открыто окно индикаторов.
# Это заменяет grab_set() и wait_window().
self.wm_attributes('-disabled', 1)
# --- НОВОЕ: Привязываем событие закрытия ---
# Когда окно индикаторов будет закрыто, сработает эта функция.
indicator_manager.bind("<Destroy>", lambda e: self._on_child_destroy())
# --- НОВЫЙ МЕТОД в вашем классе main.py ---
def _on_child_destroy(self):
"""Вызывается, когда дочернее окно (индикаторов) закрывается."""
# Снова включаем главное окно
self.wm_attributes('-disabled', 0)
# Возвращаем ему фокус, чтобы можно было сразу нажимать на кнопки
self.focus_set()
def on_indicator_manager_close(self):
"""Этот метод вызывается, когда окно индикаторов закрывается."""
# Проверяем, существует ли еще окно и активен ли grab
if hasattr(self, 'indicator_manager') and self.indicator_manager.winfo_exists():
try:
# Снимаем блокировку с главного окна
self.indicator_manager.grab_release()
except tk.TclError:
# Ошибка может возникнуть, если grab уже был снят
pass
def open_admin_menu(self):
admin_menu = tk.Menu(self, tearoff=0)
admin_menu.add_command(label="Пользователи", command=self.open_users_window)
admin_menu.add_command(label="Базы данных", command=self.open_database_window)
root_x = self.winfo_rootx()
root_y = self.winfo_rooty()
menu_x = root_x + 100
menu_y = root_y + 100
admin_menu.post(menu_x, menu_y)
def open_users_window(self):
# Проверка роли пользователя остается без изменений
if not self.current_user.get("role"):
messagebox.showwarning(
"Ошибка", "Вы не авторизованы. Пожалуйста, выполните вход."
)
return
success = self.propose_admin_access()
if success or self.current_user.get("role") == "Администратор":
# --- НОВЫЙ КОД ---
# Создаем окно UsersWindow БЕЗ передачи функций или ссылок на logout.
# Окно будет работать само по себе.
users_window = UsersWindow(parent=self)
# 1. Отключаем главное окно приложения
self.wm_attributes('-disabled', 1)
# 2. Привязываем событие закрытия окна к методу, который его "отпустит"
users_window.bind("<Destroy>", lambda e: self._on_child_close())
def open_database_window(self):
if self.current_user.get("role"):
success = self.propose_admin_access()
if success or self.current_user.get("role") == "Администратор":
db_window = DatabaseWindow(self)
# 1. Отключаем главное окно приложения
self.wm_attributes('-disabled', 1)
# 2. Привязываем событие закрытия окна к методу, который его "отпустит"
db_window.bind("<Destroy>", lambda e: self._on_child_close())
else:
messagebox.showwarning(
"Ошибка", "Вы не авторизованы. Пожалуйста, выполните вход."
)
def propose_admin_access(self):
if self.current_user.get("role") != "Администратор":
confirm_dialog = ChangeRoleWindow(self)
confirm_dialog.grab_set()
confirm_dialog.wait_window()
def create_file(self):
"""Открывает меню управления проектами при нажатии кнопки 'Проект'."""
project_menu = tk.Menu(self, tearoff=0)
project_menu.add_command(
label="Создать проект", command=self.open_create_project_window
)
project_menu.add_command(
label="Редактировать проект", command=self.open_edit_project_window
)
project_menu.add_command(
label="Архив проектов", command=self.open_archive_window
)
root_x = self.winfo_rootx()
root_y = self.winfo_rooty()
menu_x = root_x + 100 # Смещение по X от левого верхнего угла главного окна
menu_y = root_y + 100 # Смещение по Y
project_menu.post(menu_x, menu_y)
def open_base_manager(self):
base_menu = tk.Menu(self, tearoff=0)
base_menu.add_command(label="Менеджер групп", command=self.open_group_manager)
base_menu.add_command(
label="Менеджер показателей", command=self.open_indicator_manager
)
root_x = self.winfo_rootx()
root_y = self.winfo_rooty()
menu_x = root_x + 100
menu_y = root_y + 100
base_menu.post(menu_x, menu_y)
def open_experts_menu(self):
experts_menu = tk.Menu(self, tearoff=0)
experts_menu.add_command(
label="Создание файла для эксперта", command=self.open_create_file_window
)
experts_menu.add_command(
label="Агрегирование файлов", command=self.open_aggregation_window
)
root_x = self.winfo_rootx()
root_y = self.winfo_rooty()
menu_x = root_x + 100
menu_y = root_y + 100
experts_menu.post(menu_x, menu_y)
def open_create_file_window(self):
create_file_window = ExportMatrixWindow(self)
# Код для позиционирования окна оставляем как есть
self.update_idletasks()
parent_x = self.winfo_x()
parent_y = self.winfo_y()
parent_w = self.winfo_width()
parent_h = self.winfo_height()
w = create_file_window.winfo_width()
h = create_file_window.winfo_height()
x = parent_x + (parent_w - w) // 2
y = parent_y + (parent_h - h) // 2
create_file_window.geometry(f"+{x}+{y}")
# --- НАЧАЛО ИСПРАВЛЕННОГО КОДА ---
# Убираем grab_set() и wait_window()
# 1. Отключаем главное окно
self.wm_attributes('-disabled', 1)
# 2. Привязываем событие закрытия окна к методу, который его "отпустит"
create_file_window.bind("<Destroy>", lambda e: self._on_child_close())
# --- КОНЕЦ ИСПРАВЛЕННОГО КОДА ---
def open_aggregation_window(self):
aggregation_window = AggregateFilesWindow(self)
# Код для позиционирования окна оставляем как есть (в вашей версии была опечатка в parent_y)
self.update_idletasks()
parent_x = self.winfo_x()
parent_y = self.winfo_rooty() # Оставляем как у вас было, хотя обычно используют winfo_y()
parent_w = self.winfo_width()
parent_h = self.winfo_height()
w = aggregation_window.winfo_width()
h = aggregation_window.winfo_height()
x = parent_x + (parent_w - w) // 2
y = parent_y + (parent_h - h) // 2
aggregation_window.geometry(f"+{x}+{y}")
# --- НАЧАЛО ИСПРАВЛЕННОГО КОДА ---
# Убираем grab_set() и wait_window()
# 1. Отключаем главное окно
self.wm_attributes('-disabled', 1)
# 2. Привязываем событие закрытия окна к методу, который его "отпустит"
aggregation_window.bind("<Destroy>", lambda e: self._on_child_close())
# --- КОНЕЦ ИСПРАВЛЕННОГО КОДА ---
def on_exit(self):
"""Корректно закрывает приложение."""
self.destroy()
def start_login_process(self):
login_window = LoginWindow(self)
login_window.grab_set()
login_window.wait_window()
# --- ДОБАВЛЕНИЕ ТЕСТОВЫХ ПРОЕКТОВ ---
try:
from gui.project_manager import Project
test_project_1_name = "Тестовый проект №1"
test_project_2_name = "Тестовый проект №2"
existing_names = [p.name for p in self.project_manager.list_projects()]
# Добавляем проект 1, если его нет
if test_project_1_name not in existing_names:
new_project_1_obj = Project(name=test_project_1_name)
new_project_1_obj.id = "TEST-001"
self.project_manager.add_project(new_project_1_obj)
# Добавляем проект 2, если его нет
if test_project_2_name not in existing_names:
new_project_2_obj = Project(name=test_project_2_name)
new_project_2_obj.id = "TEST-002"
self.project_manager.add_project(new_project_2_obj)
except Exception as e:
pass # Лучше ничего не делать, если не удалось добавить тесты
# --- РЕДАКТИРОВАНИЕ ПРОЕКТА ---
def open_edit_project_window(self):
"""Открывает окно для выбора проекта."""
# print("DEBUG: Открываем окно выбора проекта...")
# Просто создаем окно выбора.
# Нам не нужно его хранить или ждать.
list_window = ProjectListWindow(self.project_manager, root_window=self)
# Добавляем кнопку
open_event_btn = tk.Button(
list_window.window,
text="Открыть ввод событий",
width=25,
bg="#4CAF50",
fg="white",
command=lambda: self.open_event_window_from_list(list_window),
)
open_event_btn.pack(pady=10)
def return_from_event_window(self):
"""Этот метод вызывается из окна ProjectEventWindow при закрытии.
Он показывает обратно скрытое окно со списками."""
print("DEBUG: Пользователь возвращается в окно выбора параметров.")
try:
# Проверяем, что ссылка на окно со списками существует
if hasattr(self, "list_window_instance"):
# Снимаем блокировку с главного окна
self.list_window_instance.window.grab_release()
# Показываем окно со списками обратно
self.list_window_instance.window.deiconify()
# Принудительно выводим его на передний план
self.list_window_instance.window.attributes("-topmost", 1)
self.list_window_instance.window.after(
100,
lambda: self.list_window_instance.window.attributes("-topmost", 0),
)
except Exception as e:
print(f"ОШИБКА при возврате в окно выбора: {e}")
def open_event_window_from_list(self, list_window_instance):
"""Получает данные и открывает окно событий."""
# --- НОВОЕ: Блокируем кнопку, чтобы избежать двойного клика ---
try:
# Получаем кнопку из переданного окна и блокируем ее
# Предполагается, что кнопка имеет имя или мы ее находим
# Если кнопка это переменная в list_window_instance, используй ее.
# Если нет, найди ее по тексту или положи в переменную при создании.
# Здесь я покажу универсальный способ через поиск по тексту.
for widget in list_window_instance.window.winfo_children():
if isinstance(
widget, tk.Button
) and "Открыть ввод событий" in widget.cget("text"):
widget.config(state="disabled")
break
# Теперь основная логика
selected_data = list_window_instance.get_selected_data()
if not selected_data.get("project_name"):
# Если кнопка заблокирована, вернем ей состояние NORMAL
for widget in list_window_instance.window.winfo_children():
if isinstance(widget, tk.Button):
widget.config(state="normal")
messagebox.showwarning("Ошибка", "Сначала выберите проект.")
return
# Уничтожаем окно выбора.
list_window_instance.window.destroy()
# Создаем окно событий.
event_window = ProjectEventWindow(
manager=list_window_instance.manager,
selected_data=selected_data,
root_window=self,
)
event_window.window.after(100, event_window.load_events)
except Exception as e:
pass # Логирование убрано для чистоты
def return_from_event_window(self):
"""Этот метод вызывается из окна ProjectEventWindow при закрытии."""
try:
if hasattr(self, "list_window_instance"):
self.list_window_instance.window.grab_release()
# --- НОВОЕ: Разблокируем кнопки в главном окне ---
self.enable_all_buttons()
except Exception as e:
pass
def show_selection_window(self):
"""Показывает окно выбора проектов обратно."""
try:
if (
hasattr(self, "list_window_instance")
and self.list_window_instance.window.winfo_exists()
):
self.list_window_instance.window.deiconify()
else:
self.open_edit_project_window()
except Exception as e:
pass
def enable_all_buttons(self):
"""Проходит по всем виджетам и включает все кнопки (снимает блокировку)."""
for frame in self.winfo_children():
for widget in frame.winfo_children():
if isinstance(widget, tk.Button):
widget.config(state="normal")
def open_export_matrix_window(self):
"""
Открывает окно прогнозирования.
Создает новый экземпляр ForecastWindow каждый раз.
"""
# Проверяем, не было ли окно уже открыто, и закрываем его, если нужно
if hasattr(self, 'forecast_window'):
try:
self.forecast_window.window.destroy()
except Exception as e:
print(f"Окно уже закрыто: {e}")
# --- ИЗМЕНЕНИЕ ---
# Перед созданием нового окна, проверим наличие виджета для вывода текста
# Если его нет (при первом запуске), создадим его в главном окне
if not hasattr(self, 'result_frame'):
# Фрейм для блока с результатами
self.result_frame = ttk.LabelFrame(self, text="Результаты прогнозирования", padding="5")
self.result_frame.pack(side="bottom", fill="x") # Размещаем внизу главного окна
# Текстовое поле с прокруткой для вывода логов и результатов
self.result_text = tk.Text(self.result_frame, height=8, width=70, wrap='word', state='disabled')
scrollbar = ttk.Scrollbar(self.result_frame, command=self.result_text.yview)
scrollbar.pack(side="right", fill="y")
self.result_text.configure(yscrollcommand=scrollbar.set)
self.result_text.pack(padx=10, pady=5, fill="both", expand=True)
# Стало в main.py:
from gui.forecast_window import ForecastWindow
# Передаем и главное окно (self), и менеджер проектов (self.project_manager)
self.forecast_window = ForecastWindow(parent=self, project_manager=self.project_manager)
if __name__ == "__main__":
# 1. Инициализируем базу данных в самом начале
initialize_database()
try:
import sys
import os
import logging # Импортируем логгинг здесь, чтобы использовать в блоке
# --- 1. ВРЕМЕННО ОТКЛЮЧАЕМ ВЫВОД В КОНСОЛЬ ---
old_stdout = sys.stdout
old_stderr = sys.stderr
devnull = open(os.devnull, "w")
sys.stdout = devnull
sys.stderr = devnull
# --- 2. ЗАПУСКАЕМ ПРИЛОЖЕНИЕ ---
app = MainWindow()
# --- 3. ВОЗВРАЩАЕМ ВЫВОД ОБРАТНО ---
sys.stdout = old_stdout
sys.stderr = old_stderr
devnull.close()
# --- 4. ЗАПУСК ОКНА ЛОГИНА ---
app.withdraw()
login_window = LoginWindow(app)
# Привязываем обработчик закрытия к окну логина
login_window.protocol("WM_DELETE_WINDOW", login_window.on_close)
login_window.grab_set()
login_window.wait_window()
# --- 5. ПРОВЕРКА ПОСЛЕ ЗАКРЫТИЯ ЛОГИНА ---
# Если пользователь не вошел в систему, закрываем приложение
if not app.current_user:
app.destroy()
sys.exit(0)
# --- 6. ОКОНЧАТЕЛЬНО ОТКЛЮЧАЕМ ЛОГИ ---
logging.disable(logging.INFO)
# --- 7. ЗАПУСК ОСНОВНОГО ЦИКЛА ПРИЛОЖЕНИЯ ---
app.mainloop()
except Exception as e:
print(f"Возникла критическая ошибка при запуске приложения: {e}")