Загрузка данных
Работаем с модулем modules/project/project_window.py - необходимо сделать косметические правки.
Показываю для образца окно Панель Администратора (окно Администрирование)
К этотму образцу нужно привести окно Наполнение проекта.
Обращаю внимание что я вайб кодер,поэтому замены не кусками,а целыми методами.
В Панели администратора структура:
Заголовок,
3 заголовка аккордеона,
Кнопка Закрыть.
В Наполнении проекта необходимо:
Заголовок - Все что сейчас в окне есть опустить ниже и тем же шрифтом как в Панели Администратора написать заголовок Наполнение проекта (окно соответственно изменить на Журнал событий) над блоком Выбрать проект,
3 заголовка изменить на 4, сразу после блока Выбрать проект добавить новый первый заголовок Создать проект,
Нижние кнопки - сейчас внизу 2 кнопки Отмена и Сохранить изменения - сделать как в Администрировании одну Закрыть.
Необходимо поправить схлопываение заголовков - сейчас их 3 и когда их пользователь закрывает нижние кнопки наполовину прячутся. Учитывая что добавляется 4й заголовок необходимо высоту наверное скорректировать.
Ниже даюдля образца код Администратора и для исправления Наполнение проекта.
#================================================
# modules\admin\admin_accordion_window.py
#================================================
import customtkinter as ctk
from tkinter import ttk, messagebox
# modules/admin/admin_accordion_window.py
from .users_tab import AdminUsersPanel
from .db_manager_panel import DBManagerPanel
from .migration_panel import MigrationPanel
class AdminAccordionWindow(ctk.CTkToplevel):
def __init__(self, master):
super().__init__(master)
self.parent_main = master
self.title("Администрирование")
# --- 1. БАЗОВЫЕ ГАБАРИТЫ ОКНА ---
window_width = 650
initial_height = 380
screen_w = self.winfo_screenwidth()
screen_h = self.winfo_screenheight()
x_cord = int((screen_w - window_width) / 2)
y_cord = int((screen_h - initial_height) / 2)
self.geometry(f"{window_width}x{initial_height}+{x_cord}+{y_cord}")
# === 2. ЖЕСТКИЕ ОГРАНИЧЕНИЯ РАЗМЕРА ===
self.minsize(width=window_width, height=250)
self.maxsize(width=900, height=screen_h - 50)
self.resizable(True, False)
self.grab_set()
self.transient(master)
# --- КОНТЕЙНЕРЫ ---
main_container = ctk.CTkFrame(self, corner_radius=10)
main_container.pack(padx=10, pady=10, fill="both", expand=True)
title_lbl = ctk.CTkLabel(main_container, text="Панель администратора", font=ctk.CTkFont(size=20, weight="bold"))
title_lbl.pack(pady=(0, 15))
accordion_frame = ctk.CTkFrame(main_container)
accordion_frame.pack(fill="both", expand=True, pady=(10, 0))
self.accordion_data = {}
self.currently_open_button = None
# === ЗАГОЛОВКИ АККОРДЕОНА ===
users_item = self._create_accordion_header(accordion_frame, "Управление пользователями")
security_item = self._create_accordion_header(accordion_frame, "Безопасность")
migration_item = self._create_accordion_header(accordion_frame, "Миграция")
# Наполняем секции контентом
self._fill_users_section(users_item["content"])
self._fill_security_section(security_item["content"])
self._fill_migration_section(migration_item["content"])
# Привязка команд переключения
for item in [users_item, security_item, migration_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))
# Открываем первую секцию
self.toggle_section(users_item["button"], users_item["content"])
# Кнопки внизу
button_frame = ctk.CTkFrame(main_container)
button_frame.pack(fill="x", pady=(10, 0))
close_btn = ctk.CTkButton(button_frame, text="Закрыть", width=120, command=self.destroy)
close_btn.pack(side="right", padx=10, pady=10)
self.protocol("WM_DELETE_WINDOW", self.on_closing)
# === 3. ЦЕНТРИРОВАНИЕ ПОСЛЕ ПОЛНОЙ ОТРИСТОВКИ ===
self.after(100, self._center_over_parent)
def _center_over_parent(self):
if not self.parent_main or not self.parent_main.winfo_exists():
return
# Обновляем данные о геометрии окна после того, как аккордеон раскрылся
self.update_idletasks()
w = self.winfo_width() # ширина вашего аккордеона
# === НОВАЯ ЛОГИКА ПОЗИЦИОНИРОВАНИЯ ===
# 1. Считаем горизонтальный центр относительно всего экрана
screen_w = self.winfo_screenwidth()
final_x = int((screen_w - w) / 2)
# 2. Фиксированная позиция по вертикали (отступ от верхнего края экрана)
taskbar_offset_top = 40
final_y = taskbar_offset_top
# Применяем только смещение, размер трогать нельзя (иначе сломается maxsize/minsize)
self.geometry(f"+{final_x}+{final_y}")
# === МЕТОД toggle_section (твой оригинальный) ===
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)
self.geometry(new_geo)
# === РАСЧЕТ ГЕОМЕТРИИ (Исправлено!) ===
def _calculate_window_geometry(self, collapsed=False):
current_width = self.winfo_width()
# Если ширина еще не посчиталась (например, при первом запуске)
if current_width == 1:
current_width = 650
header_height = 50 * 3 + 30
# === ФИКС ДЛЯ КНОПОК: ДОБАВИЛИ ОТСТУП ПОД НИЖНЮЮ ПАНЕЛЬ ===
button_panel_height = 70 # Высота рамки с кнопкой "Закрыть" + её отступы
if collapsed or not self.currently_open_button:
target_height = header_height + 80 + button_panel_height
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 + button_panel_height
max_h = self.winfo_screenheight() - 50
min_h = 250
final_height = min(target_height, max_h) # Не выше экрана
final_height = max(final_height, min_h) # Не ниже порога для кнопок
return f"{current_width}x{final_height}"
# === СОЗДАНИЕ ХЕДЕРА ===
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}
# === НАПОЛНЕНИЕ СЕКЦИЙ ===
def _fill_users_section(self, frame):
panel = AdminUsersPanel(frame)
panel.pack(fill="both", expand=True, padx=10, pady=10)
def _fill_security_section(self, frame):
panel = DBManagerPanel(frame)
panel.pack(fill="both", expand=True, padx=10, pady=10)
def _fill_migration_section(self, frame):
panel = MigrationPanel(frame)
panel.pack(fill="both", expand=True, padx=10, pady=10)
def on_closing(self):
try:
self.after_cancel("all")
except Exception:
pass
try:
self.destroy()
except Exception:
pass
# ======================================
# ======================================
# modules/project/project_window.py
# ======================================
import customtkinter as ctk
from tkinter import ttk, messagebox
import datetime
import os
# Попытка импорта внешних библиотек (оставляем ваши try/except)
try:
from tkcalendar import DateEntry
except ImportError:
DateEntry = None
try:
import docx
except ImportError:
docx = None
class ProjectWindow(ctk.CTkToplevel):
def __init__(self, master):
super().__init__(master)
self.title("Наполнение проекта")
self.current_user = getattr(master, 'current_user', None) # На случай передачи пользователя
# --- Настройка размеров ---
screen_height = self.winfo_screenheight()
window_width = 650
initial_height = 380
# Горизонтальный центр экрана
screen_w = self.winfo_screenwidth()
x_cord = int((screen_w - window_width) / 2)
# Фиксированный отступ от верхнего края (под панель задач Windows)
taskbar_offset_top = 40
y_cord = taskbar_offset_top
self.geometry(f"{window_width}x{initial_height}+{x_cord}+{y_cord}")
self.geometry(f"{window_width}x{initial_height}+{x_cord}+{y_cord}")
self.resizable(True, False)
# Делаем окно МОДАЛЬНЫМ (блокируем родителя)
self.grab_set()
self.transient(master)
# --- КОНТЕЙНЕРЫ ---
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", command=self.destroy)
cancel_btn.pack(side="right")
# Обработчик закрытия крестиком
self.protocol("WM_DELETE_WINDOW", self.on_closing)
# === ВАШ МЕТОД toggle_section БЕЗ ИЗМЕНЕНИЙ ===
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)
self.after_idle(lambda g=new_geo: self.geometry(g))
# === ВАШИ МЕТОДЫ _calculate... и _build... БЕЗ ИЗМЕНЕНИЙ ===
def _calculate_window_geometry(self, collapsed=False):
current_width = self.winfo_width()
if current_width == 1:
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)
return f"{current_width}x{final_height}"
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}
# === ВАШИ МЕТОДЫ НАПОЛНЕНИЯ (_fill_...) ОСТАЮТСЯ ПРЕЖНИМИ ===
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)
# ... (Весь ваш огромный код формы со Treeview остается здесь без изменений) ...
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))
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
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
def _generate_word_report(self, group, report_date, orientation):
if docx is None:
messagebox.showerror("Библиотека не найдена", "Для экспорта нужна библиотека 'python-docx'.\nВыполните:\npip install python-docx")
return
def on_closing(self):
# Ваша логика очистки потоков...
try:
self.after_cancel("all")
except Exception:
pass
for item in list(self.accordion_data.values()):
try:
content = item.get("content")
if content:
content.destroy()
except Exception:
pass
try:
self.destroy()
except Exception:
pass
# Уходим мягко, если это было отдельное приложение, или просто закрываемся
# os._exit(0) ЗДЕСЬ НЕ НУЖНО, ТАК КАК МЫ ВНУТРИ ОСНОВНОГО ОКНА