Загрузка данных
# Dozor\modules\manager\manager_group_inter.py
# -*- coding: utf-8 -*-
import tkinter as tk
from tkinter import ttk, messagebox
from modules.manager.manager_group_work import list_groups, add_group, delete_group
from modules.utils.window_behavior import LevelThreeBehavior
from modules.utils.app_state import disable_app, enable_app
class ManagerGroupInter(tk.Toplevel):
def __init__(self, parent):
super().__init__(parent)
self.master = parent
self.transient(parent)
self.title("Менеджер групп")
self.geometry("600x450")
behavior = LevelThreeBehavior()
behavior.setup_level_three(self)
header_frame = tk.Frame(self, bg="#2c3e50", height=30)
header_frame.pack(fill="x")
header_frame._name = "title_bar"
close_btn = ttk.Button(header_frame, text="X", width=3, command=self.destroy)
close_btn.pack(side="right", padx=5, pady=2)
ttk.Label(header_frame, text="Менеджер групп", foreground="white", background="#2c3e50").pack(side="left", padx=10)
ttk.Separator(self, orient='horizontal').pack(fill='x')
top_frame = tk.Frame(self)
top_frame.pack(fill="x", padx=10, pady=(5, 10))
# --- НАЗВАНИЕ ---
ttk.Label(top_frame, text="Название:").grid(row=0, column=0, sticky="w")
self.name_entry = ttk.Entry(top_frame, width=30)
self.name_entry.grid(row=0, column=1, padx=5)
# --- КОД (Только чтение для редактирования) ---
ttk.Label(top_frame, text="Код:").grid(row=1, column=0, sticky="w")
self.code_var = tk.StringVar()
self.code_label = ttk.Label(top_frame, textvariable=self.code_var, relief="sunken", width=30)
self.code_label.grid(row=1, column=1, padx=5, pady=5)
frame_gain = tk.Frame(top_frame)
frame_gain.grid(row=2, column=1, sticky="w", padx=5)
ttk.Label(frame_gain, text="Усиление:").pack(side="left")
btn_minus = ttk.Button(frame_gain, text="-", command=lambda: self._change_gain(-1), width=3)
btn_minus.pack(side="left", padx=2)
self.gain_var = tk.StringVar(value="1")
gain_entry = ttk.Entry(frame_gain, textvariable=self.gain_var, width=5, justify="center")
gain_entry.pack(side="left", padx=2)
btn_plus = ttk.Button(frame_gain, text="+", command=lambda: self._change_gain(1), width=3)
btn_plus.pack(side="left", padx=2)
action_frame = tk.Frame(top_frame)
action_frame.grid(row=3, column=0, columnspan=2, pady=10)
# КНОПКА ДОБАВЛЕНИЯ БУДЕТ ВЫЗЫВАТЬ ЭТОТ МЕТОД
ttk.Button(action_frame, text="Добавить", command=self.add_group).pack(side="left", padx=5)
ttk.Button(action_frame, text="Редактировать", command=self.edit_group).pack(side="left", padx=5)
ttk.Button(action_frame, text="Удалить", command=self.delete_group).pack(side="left", padx=5)
tree_frame = tk.Frame(self)
tree_frame.pack(fill="both", expand=True, padx=10, pady=5)
columns = ("code", "name", "gain")
self.tree = ttk.Treeview(tree_frame, columns=columns, show="headings")
self.tree.heading("code", text="Код"); self.tree.column("code", width=100)
self.tree.heading("name", text="Название"); self.tree.column("name", width=200)
self.tree.heading("gain", text="Усиление"); self.tree.column("gain", width=80, anchor="center")
self.tree.pack(fill="both", expand=True, side="left")
scrollbar = ttk.Scrollbar(tree_frame, orient="vertical", command=self.tree.yview)
scrollbar.pack(side="right", fill="y")
self.tree.configure(yscrollcommand=scrollbar.set)
self.update_tree()
self.update_idletasks()
screen_width = self.winfo_screenwidth()
screen_height = self.winfo_screenheight()
size = tuple(int(_) for _ in self.geometry().split('+')[0].split('x'))
x = screen_width // 2 - size[0] // 2
y = screen_height // 2 - size[1] // 2
self.geometry(f"+{x}+{y}")
def update_tree(self):
for item in self.tree.get_children():
self.tree.delete(item)
groups = list_groups()
for group in groups:
gain_text = f"{group.gain_group:.1f}" if group.gain_group is not None else ""
self.tree.insert("", "end", iid=str(group.id), values=(group.code, group.name, gain_text))
def get_selected(self):
selected = self.tree.selection()
if not selected:
return None
code, name, gain = self.tree.item(selected[0])["values"]
db_id_str = selected[0]
try:
db_id = int(db_id_str)
except ValueError:
return None
return {"id": db_id, "code": code, "name": name}
# === ВОТ ЭТОТ МЕТОД БЫЛ ПРОПУЩЕН РАНЬШЕ ===
def add_group(self):
name = self.name_entry.get().strip()
# Для добавления временно используем поле ввода кода, если оно есть, иначе генерируем заглушку
code = ""
# Проверим, не является ли интерфейс сейчас режимом "редактирование" по наличию лейбла
if hasattr(self, 'code_label') and self.code_label.cget("text"):
code = self.code_label.cget("text")
gain_raw = self.gain_var.get().strip()
if not name or not code:
messagebox.showwarning("Ошибка", "Заполните название и код.")
return
try:
gain_val = float(gain_raw.replace(',', '.')) if gain_raw else 0.0
except ValueError:
messagebox.showwarning("Ошибка", "Усиление должно быть числом.")
return
existing = list_groups()
for g in existing:
if g.name == name or g.code == code:
messagebox.showerror("Ошибка", "Группа с таким названием или кодом уже существует.")
return
try:
add_group(name, code, gain_val)
messagebox.showinfo("Успех", "Группа добавлена.")
self.update_tree()
self._clear_form()
except Exception as e:
messagebox.showerror("Ошибка БД", str(e))
def edit_group(self):
data = self.get_selected()
if not data:
messagebox.showwarning("Ошибка", "Выберите группу.")
return
old_code = data["code"]
old_name = data["name"]
# Берем текущее усиление прямо сейчас (то, которое выбрал юзер)
current_gain_str = self.gain_var.get().strip()
# Заполняем форму для наглядности
self.name_entry.delete(0, tk.END)
self.name_entry.insert(0, old_name)
self.code_var.set(old_code)
disable_app(self.master)
edit_win = tk.Toplevel(self.master)
edit_win.title("Подтверждение")
edit_win.geometry("+{}+{}".format(self.winfo_pointerx(), self.winfo_pointery()))
msg = f"Изменить группу '{old_code}'?"
ttk.Label(edit_win, text=msg, wraplength=300).pack(pady=20)
btn_frame = tk.Frame(edit_win)
btn_frame.pack(pady=10)
# ПЕРЕДАЕМ ЗНАЧЕНИЕ НАПРЯМУЮ В ЛЯМБДУ
ttk.Button(btn_frame, text="Да",
command=lambda db_id=data["id"], name=old_name, gain=current_gain_str:
self._apply_edit(db_id, name, gain, edit_win)).pack(side="left", padx=10)
ttk.Button(btn_frame, text="Отмена", command=edit_win.destroy).pack(side="left", padx=10)
def _apply_edit(self, db_id, name, gain_str, win):
try:
final_gain = int(float(gain_str))
from modules.data.database_module import get_db_connection
conn = get_db_connection()
cursor = conn.cursor()
print(f"[DEBUG] SQL Update -> ID:{db_id} | Name:'{name}' | Gain:{final_gain}")
cursor.execute(
"UPDATE groups SET name = ?, gain_group = ? WHERE id = ?",
(name, final_gain, db_id)
)
rows_affected = cursor.rowcount
conn.commit()
conn.close()
if rows_affected > 0:
messagebox.showinfo("Готово", "Группа обновлена.")
win.destroy()
self.after_idle(self.update_tree)
enable_app(self.master)
else:
messagebox.showwarning("Внимание", "Запрос прошел, но строка не изменилась.")
win.destroy()
enable_app(self.master)
except Exception as e:
print(f"[FATAL DB ERROR RAW SQL] {e}")
messagebox.showerror("Ошибка БД", str(e))
enable_app(self.master)
def delete_group(self):
data = self.get_selected()
if not data:
messagebox.showwarning("Ошибка", "Выберите группу.")
return
if messagebox.askyesno("Подтверждение", f"Удалить группу {data['name']}?"):
try:
delete_group(data["id"])
messagebox.showinfo("Успех", "Группа удалена.")
self.update_tree()
self._clear_form()
except Exception as e:
messagebox.showerror("Ошибка БД", str(e))
def _clear_form(self):
if hasattr(self, 'name_entry'):
self.name_entry.delete(0, tk.END)
if hasattr(self, 'code_var'):
self.code_var.set("")
self.gain_var.set("1.0")
def on_close(self):
self.destroy()
def _change_gain(self, delta):
try:
val = int(self.gain_var.get())
new_val = max(0, val + delta)
self.gain_var.set(str(new_val))
except ValueError:
self.gain_var.set("1")
# Dozor\modules\manager\manager_group_work.py
# -*- coding: utf-8 -*-
import sqlite3
from typing import List
from contextlib import contextmanager
from modules.data.database_module import get_db_connection
class GroupDTO:
def __init__(self, id: int, code: str, name: str, gain_group: float = None):
self.id = id
self.code = code
self.name = name
self.gain_group = gain_group
@contextmanager
def _get_cursor():
conn = get_db_connection()
try:
cursor = conn.cursor()
yield cursor
# Принудительная фиксация СРАЗУ ЖЕ
conn.commit()
except Exception:
conn.rollback()
raise
finally:
# Принудительное закрытие соединения, чтобы сбросить все локи
conn.close()
def list_groups() -> List[GroupDTO]:
with _get_cursor() as cursor:
cursor.execute('''
CREATE TABLE IF NOT EXISTS groups (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL UNIQUE,
code TEXT NOT NULL UNIQUE,
gain_group REAL DEFAULT 0.0
)
''')
cursor.execute("SELECT id, code, name, gain_group FROM groups ORDER BY name")
rows = cursor.fetchall()
return [GroupDTO(id=row[0], code=row[1], name=row[2], gain_group=row[3]) for row in rows]
def add_group(name: str, code: str, gain_group: float = 0.0) -> GroupDTO:
with _get_cursor() as cursor:
cursor.execute(
"INSERT INTO groups (name, code, gain_group) VALUES (?, ?, ?)",
(name, code, gain_group)
)
new_id = cursor.lastrowid
return GroupDTO(id=new_id, code=code, name=name, gain_group=gain_group)
def edit_group(group_id: int, new_name: str, new_code: str, new_gain: float):
conn = get_db_connection()
cursor = conn.cursor()
try:
cursor.execute(
"UPDATE groups SET name = ?, code = ?, gain_group = ? WHERE id = ?",
(new_name, new_code, new_gain, group_id)
)
# Проверяем, затронута ли хоть одна строка
if cursor.rowcount == 0:
raise Exception("Запись не обновлена. Проверьте уникальность кода/названия.")
# Принудительная фиксация прямо здесь
conn.commit()
print("[WORK LOGIC] Группа успешно обновлена в БД.")
except sqlite3.IntegrityError as e:
conn.rollback()
raise Exception(f"Нарушение целостности данных: {e}")
except Exception as e:
conn.rollback()
raise e
finally:
conn.close()
def delete_group(group_id: int):
with _get_cursor() as cursor:
cursor.execute("DELETE FROM groups WHERE id = ?", (group_id,))
# Dozor\modules\manager\manager_metrics_inter.py
# -*- coding: utf-8 -*-
import tkinter as tk
from tkinter import ttk, messagebox, filedialog
from docx import Document
from modules.manager.manager_metrics_work import (
get_all_indicators_by_group,
insert_indicator,
delete_indicator_by_code,
IndicatorDTO,
generate_unique_code,
save_docx_template
)
from modules.manager.manager_group_work import list_groups as group_list
from modules.utils.window_behavior import LevelTwoBehavior
class ManagerMetricsInter(tk.Toplevel):
def __init__(self, parent_facade):
super().__init__(parent_facade.master_root)
self.parent_main = parent_facade
# === ИСПРАВЛЕНО: Создаем словарь СРАЗУ при создании объекта ===
self.group_names_to_codes = {}
behavior = LevelTwoBehavior()
behavior.setup_level_two(self)
self.title("Менеджер показателей")
self.geometry("600x450")
if not hasattr(self.parent_main, 'current_user') or not self.parent_main.current_user.get("login"):
messagebox.showwarning("Ошибка", "Сессия пользователя не найдена.")
self.destroy()
return
self.create_widgets()
self.update_group_combobox()
self.update_indicators_list()
self.update_idletasks()
screen_width = self.winfo_screenwidth()
screen_height = self.winfo_screenheight()
size = tuple(int(_) for _ in self.geometry().split('+')[0].split('x'))
x = screen_width // 2 - size[0] // 2
y = screen_height // 2 - size[1] // 2
self.geometry(f"+{x}+{y}")
def create_widgets(self):
top_panel = tk.Frame(self)
top_panel.pack(padx=10, pady=10, fill="x")
ttk.Label(top_panel, text="Группа:").pack(anchor="w")
self.group_var = tk.StringVar()
self.group_combobox = ttk.Combobox(top_panel, textvariable=self.group_var, state="readonly", width=30)
self.group_combobox.pack(fill="x", pady=5)
self.group_combobox.bind("<<ComboboxSelected>>", self.update_indicators_list)
ttk.Label(top_panel, text="Название показателя:").pack(anchor="w")
self.name_entry = tk.Text(top_panel, height=3, width=40)
self.name_entry.pack(fill="x", pady=5)
paste_button = tk.Button(top_panel, text="Вставить", command=self.paste_from_clipboard)
paste_button.pack()
buttons_frame = tk.Frame(top_panel)
buttons_frame.pack(fill="x", pady=5)
ttk.Button(buttons_frame, text="Добавить", command=self.add_indicator).pack(side="left", padx=2)
ttk.Button(buttons_frame, text="Удалить", command=self.delete_indicator).pack(side="left", padx=2)
ttk.Button(buttons_frame, text="Экспорт в Word", command=self.print_to_docx).pack(side="left", padx=5, pady=5)
middle_panel = tk.Frame(self)
middle_panel.pack(fill="both", expand=True, padx=10, pady=5)
scrollbar = ttk.Scrollbar(middle_panel)
scrollbar.pack(side="right", fill="y")
columns = ("number", "name", "code")
self.indicators_tree = ttk.Treeview(
middle_panel, columns=columns, show="headings", yscrollcommand=scrollbar.set, selectmode="browse"
)
self.indicators_tree.heading("number", text="№"); self.indicators_tree.column("number", width=50, anchor="center")
self.indicators_tree.heading("name", text="Название"); self.indicators_tree.column("name", width=300)
self.indicators_tree.heading("code", text="Код"); self.indicators_tree.column("code", width=150)
self.indicators_tree.pack(fill="both", expand=True, side="left")
scrollbar.config(command=self.indicators_tree.yview)
def update_group_combobox(self):
groups = group_list()
self.group_names_to_codes.clear() # Очищаем перед заполнением
for group in groups:
self.group_names_to_codes[group.name] = getattr(group, 'code', '')
group_names = [g.name for g in groups]
self.group_combobox["values"] = group_names
if group_names:
self.group_combobox.current(0)
self.update_indicators_list()
def update_indicators_list(self, event=None):
selected_group_name = self.group_var.get()
selected_group_code = self.group_names_to_codes.get(selected_group_name)
for item in self.indicators_tree.get_children():
self.indicators_tree.delete(item)
indicators = []
if selected_group_code:
try:
indicators = get_all_indicators_by_group(selected_group_code)
except Exception as e:
print(f"[METRICS WORK] Заглушка данных: {e}")
indicators = [IndicatorDTO(code=f"{selected_group_code}-TEST", name="Тестовый показатель")]
for idx, ind in enumerate(indicators, start=1):
val_name = getattr(ind, 'name', '--')
val_code = getattr(ind, 'code', '--')
self.indicators_tree.insert("", "end", iid=val_code, values=(f"{idx}.", val_name, val_code))
def paste_from_clipboard(self):
try:
clipboard_text = self.clipboard_get()
self.name_entry.insert(tk.END, clipboard_text)
except tk.TclError:
pass
def add_indicator(self):
group_name = self.group_var.get()
group_code = self.group_names_to_codes.get(group_name)
indicator_name = self.name_entry.get("1.0", tk.END).rstrip("\n").strip()
if not group_code or not indicator_name:
messagebox.showwarning("Ошибка", "Выберите группу и введите название!")
return
new_code = generate_unique_code(group_code)
try:
insert_indicator(new_code, indicator_name, group_code)
messagebox.showinfo("Готово", "Индикатор добавлен.")
self.update_indicators_list()
self.name_entry.delete("1.0", tk.END)
except Exception as e:
messagebox.showerror("Ошибка", str(e))
def edit_indicator(self, event):
selected_items = self.indicators_tree.selection()
if not selected_items:
return
code = selected_items[0]
current_name = ""
item_values = self.indicators_tree.item(code)["values"]
if len(item_values) > 1:
current_name = item_values[1]
edit_win = tk.Toplevel(self)
edit_win.title("Редактировать")
edit_win.geometry("350x150")
x = self.winfo_pointerx()
y = self.winfo_pointery()
edit_win.geometry(f"+{x}+{y}")
ttk.Label(edit_win, text="Новое название:").pack(pady=5)
new_name_entry = tk.Text(edit_win, height=2, width=40)
new_name_entry.insert(tk.END, current_name)
new_name_entry.pack(pady=5)
ttk.Button(edit_win, text="Сохранить",
command=lambda: self.save_changes(code, new_name_entry.get("1.0", tk.END), edit_win)).pack(pady=5)
def save_changes(self, code, new_name_raw, edit_win):
new_name = new_name_raw.rstrip("\n").strip()
if not new_name:
messagebox.showwarning("Ошибка", "Название не может быть пустым.")
return
messagebox.showinfo("Готово", "Изменения сохранены (заглушка).")
self.update_indicators_list()
edit_win.destroy()
def delete_indicator(self):
selected_items = self.indicators_tree.selection()
if not selected_items:
messagebox.showwarning("Ошибка", "Выберите индикатор для удаления.")
return
code = selected_items[0]
if messagebox.askyesno("Подтверждение", f"Удалить индикатор?"):
try:
delete_indicator_by_code(code)
messagebox.showinfo("Готово", "Удалено успешно.")
self.update_indicators_list()
except Exception as e:
messagebox.showerror("Ошибка", str(e))
def print_to_docx(self):
group_name = self.group_var.get()
if not group_name:
messagebox.showwarning("Ошибка", "Выберите группу.")
return
group_code = self.group_names_to_codes.get(group_name)
indicators = get_all_indicators_by_group(group_code)
if not indicators:
messagebox.showwarning("Ошибка", "В группе нет показателей.")
return
filename = f"{group_name.replace(' ', '_')}_indicators.docx"
path = None
try:
path = filedialog.asksaveasfilename(initialfile=filename, defaultextension=".docx",
filetypes=[("Word files", "*.docx"), ("All files", "*.*")])
except Exception:
path = "test_export.docx"
if path:
try:
from docx import Document
doc = Document()
doc.add_heading(f'Показатели группы: {group_name}', 0)
test_data = [
type('obj', (object,), {'name': 'Пример 1', 'code': 'T-1'})(),
type('obj', (object,), {'name': 'Пример 2', 'code': 'T-2'})()
]
for idx, ind in enumerate(test_data, start=1):
doc.add_paragraph(f"{idx}. [{getattr(ind, 'code', '--')}] {getattr(ind, 'name', '--')}")
doc.save(path)
messagebox.showinfo("Успех", f"Файл сохранен:\n{path}")
except Exception as e:
messagebox.showerror("Ошибка", f"Docx Error: {e}")
def on_close(self):
self.destroy()
def _change_gain(self, delta):
try:
val = int(self.gain_var.get())
new_val = max(0, val + delta)
self.gain_var.set(str(new_val))
except ValueError:
self.gain_var.set("1.0")
# Dozor\modules\manager\manager_metrics_work.py
# -*- coding: utf-8 -*-
import os
import sqlite3
from typing import List
from contextlib import contextmanager
from modules.data.database_module import get_db_connection
class IndicatorDTO:
def __init__(self, id: int, code: str, name: str, group_code: str):
self.id = id
self.code = code
self.name = name
self.group_code = group_code
@contextmanager
def _get_cursor():
conn = get_db_connection()
try:
cursor = conn.cursor()
yield cursor
conn.commit()
except Exception:
conn.rollback()
raise
finally:
conn.close()
def insert_indicator(code: str, name: str, group_code: str) -> IndicatorDTO:
print(f"[WORK LOGIC] INSERT | Код: {code}, Имя: {name}")
with _get_cursor() as cursor:
cursor.execute('''
CREATE TABLE IF NOT EXISTS indicators (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL UNIQUE,
code TEXT NOT NULL UNIQUE,
group_code TEXT NOT NULL,
FOREIGN KEY (group_code) REFERENCES groups(code) ON DELETE CASCADE
)
''')
cursor.execute("INSERT INTO indicators (code, name, group_code) VALUES (?, ?, ?)",
(code, name, group_code))
new_id = cursor.lastrowid
return IndicatorDTO(id=new_id, code=code, name=name, group_code=group_code)
def delete_indicator_by_code(code: str):
print(f"[WORK LOGIC] DELETE | Код: {code}")
with _get_cursor() as cursor:
cursor.execute("DELETE FROM indicators WHERE code = ?", (code,))
def get_all_indicators_by_group(group_code: str) -> List[IndicatorDTO]:
print(f"[WORK LOGIC] SELECT | Группа: {group_code}")
with _get_cursor() as cursor:
cursor.execute("SELECT id, code, name, group_code FROM indicators WHERE group_code = ?", (group_code,))
rows = cursor.fetchall()
return [IndicatorDTO(id=row[0], code=row[1], name=row[2], group_code=row[3]) for row in rows]
# === ВОТ ЭТА ФУНКЦИЯ НУЖНА ДЛЯ ИМПОРТА ===
def generate_unique_code(group_code: str) -> str:
"""
Генерирует уникальный код показателя внутри группы по шаблону ГРУППА-N.
Например: Эко-1, Эко-2, ИТ-1.
"""
with _get_cursor() as cursor:
# Находим максимальный порядковый номер суффикса именно для этой группы
cursor.execute("""
SELECT MAX(CAST(substr(code, instr(code, '-') + 1) AS INTEGER))
FROM indicators WHERE code LIKE ?
""", (f"{group_code}-%",))
row = cursor.fetchone()[0]
if row is None:
next_num = 1
else:
next_num = row + 1
return f"{group_code}-{next_num}"
def save_docx_template(group_name: str, indicators: List[IndicatorDTO], path: str) -> bool:
print(f"[WORK LOGIC] Вызов: save_docx_template | Путь: {path} | Группа: {group_name}")
if not indicators:
print("[WORK LOGIC ERROR] Список показателей пуст.")
return False
try:
from docx import Document
dir_path = os.path.dirname(path)
if dir_path and not os.path.exists(dir_path):
os.makedirs(dir_path, exist_ok=True)
doc = Document()
doc.add_heading(f'Группа: {group_name}', 0)
table = doc.add_table(rows=1, cols=3)
hdr_cells = table.rows[0].cells
hdr_cells[0].text = '№ п/п'
hdr_cells[1].text = 'Код'
hdr_cells[2].text = 'Наименование'
for idx, ind in enumerate(indicators, start=1):
cells = table.add_row().cells
cells[0].text = str(idx)
cells[1].text = ind.code
cells[2].text = ind.name
doc.save(path)
return True
except ImportError:
print("[WORK LOGIC ERROR] Библиотека python-docx не установлена. Выполните: pip install python-docx")
return False
except Exception as e:
print(f"[WORK LOGIC ERROR] Не удалось сохранить файл: {e}")
return False
# Dozor\modules\manager\manager_ui.py
# -*- coding: utf-8 -*-
import tkinter as tk
class ManagerPanel(tk.Frame):
def __init__(self, master, current_user):
super().__init__(master, bg="#F5F5F5")
self.master_root = master
self.current_user = current_user
print("[DEBUG] ManagerPanel создан.")
self.grid_rowconfigure(0, weight=1)
self.grid_rowconfigure(1, weight=5)
self.grid_columnconfigure(0, weight=1)
self._create_header()
self._create_body()
def _create_header(self):
header = tk.Frame(self, bg="#34495e", height=50)
header.pack(fill="x", side="top")
title = tk.Label(header, text="Менеджер базы", bg="#34495e", fg="white", font=("Arial", 16))
title.pack(side="left", padx=20, pady=10)
back_btn = tk.Button(header, text="← Назад", command=self.destroy, bg="#7f8c8d", fg="white")
back_btn.pack(side="right", padx=20, pady=10)
def _create_body(self):
body = tk.Frame(self, bg="white")
body.pack(fill="both", expand=True, padx=30, pady=30)
body.grid_columnconfigure(0, weight=1)
body.grid_columnconfigure(1, weight=1)
group_btn = tk.Button(body, text="Менеджер группы", font=("Arial", 14), width=18, height=2,
command=self.open_group_module,
bg="#3498db", fg="white", relief="raised", bd=2)
group_btn.grid(row=0, column=0, padx=10, pady=10, sticky="nsew")
metrics_btn = tk.Button(body, text="Менеджер\nпоказателей", font=("Arial", 14), width=18, height=3,
command=self.open_metrics_module,
bg="#27ae60", fg="white", relief="raised", bd=2, wraplength=120)
metrics_btn.grid(row=0, column=1, padx=10, pady=10, sticky="nsew")
def open_group_module(self):
print("[LOG] Нажата кнопка 'Менеджер группы'.")
try:
from modules.manager.manager_group_inter import ManagerGroupInter
win = ManagerGroupInter(self.master_root)
self.master_root.wait_window(win)
except Exception as e:
print(f"[FATAL CRASH IN GROUP MODULE] {e}")
from tkinter import messagebox
messagebox.showerror("Ошибка", f"Не удалось открыть Менеджер групп:\n{e}")
def open_metrics_module(self):
print("[ACTION] Открытие Менеджера Показателей...")
try:
from modules.manager.manager_metrics_inter import ManagerMetricsInter
if hasattr(self.master_root, 'register_child_window'):
print("[WARNING] Нашли старый register_child_window, игнорируем его.")
win = ManagerMetricsInter(self.master_root)
self.master_root.wait_window(win)
except Exception as e:
print(f"[FATAL CRASH IN METRICS MODULE] {e}")
from tkinter import messagebox
messagebox.showerror("Ошибка", f"Не удалось открыть модуль:\n{e}")
# Dozor\modules\utils\window_behavior.py
import tkinter as tk
class LevelOneBehavior:
"""
Поведение Главного окна (Уровень 1). Применяется к объекту класса tk.Tk.
Движение работает ТОЛЬКО по панели заголовка (title_bar).
"""
def setup_level_one(self, target_window):
target_window.resizable(False, False)
# Ищем панель заголовка для перетаскивания главного окна
try:
self.drag_widget = target_window.children['title_bar']
except KeyError:
self.drag_widget = target_window
self.drag_widget.bind("<ButtonPress-1>", self.save_offset)
self.drag_widget.bind("<B1-Motion>", self.drag_window)
target_window.bind("<Double-Button-1>", lambda e: None)
if not hasattr(target_window, '_child_windows'):
target_window._child_windows = []
self._drag_start_x = 0
self._drag_start_y = 0
def save_offset(self, event):
self._drag_start_x = event.x_root - self.winfo_x()
self._drag_start_y = event.y_root - self.winfo_y()
def drag_window(self, event):
x = event.x_root - self._drag_start_x
y = event.y_root - self._drag_start_y
self.geometry(f"+{x}+{y}")
if hasattr(self, '_update_child_positions'):
self._update_child_positions(x, y)
def register_child_window(self, window, offset_x=0, offset_y=0):
self._child_windows.append((window, offset_x, offset_y))
def do_nothing(self, event=None):
pass
class LevelTwoBehavior:
"""
Поведение окон второго уровня (привязаны к родителю).
Окно центрируется над главным и едет строго вместе с ним.
РЕЖИМ БЕЗОПАСНОСТИ: Игнорируем кастомные методы регистрации root-а.
"""
def setup_level_two(self, target_window):
width, height = int(target_window.winfo_reqwidth()), int(target_window.winfo_reqheight())
target_window.minsize(width, height)
target_window.resizable(True, True)
# Просто берем родителя окна (это наш ManagerPanel/Facade)
parent = target_window.master
p_x = parent.winfo_rootx()
p_y = parent.winfo_rooty()
p_w = parent.winfo_width()
p_h = parent.winfo_height()
win_w = target_window.winfo_reqwidth()
win_h = target_window.winfo_reqheight()
x = p_x + (p_w - win_w) // 2
y = p_y + (p_h - win_h) // 2
target_window.geometry(f"+{x}+{y}")
target_window.update_idletasks()
# === КРИТИЧЕСКОЕ ИЗМЕНЕНИЕ ===
# Весь блок с register_child_window УДАЛЕН.
# Если родитель хочет знать о нас, он сам вызовет wait_window().
def do_nothing(self, event=None):
pass
def save_offset(self, event, window):
self._start_screen_x = event.x_root
self._start_screen_y = event.y_root
self._start_win_x = window.winfo_rootx()
self._start_win_y = window.winfo_rooty()
parent = window.master
self._start_parent_x = parent.winfo_rootx()
self._start_parent_y = parent.winfo_rooty()
def drag_with_parent(self, event, child_win, parent_win):
delta_x = event.x_root - self._start_screen_x
delta_y = event.y_root - self._start_screen_y
new_parent_x = self._start_parent_x + delta_x
new_parent_y = self._start_parent_y + delta_y
parent_win.geometry(f"+{new_parent_x}+{new_parent_y}")
offset_x = self._start_win_x - self._start_parent_x
offset_y = self._start_win_y - self._start_parent_y
target_child_x = new_parent_x + offset_x
target_child_y = new_parent_y + offset_y
child_win.geometry(f"+{target_child_x}+{target_child_y}")
class LevelThreeBehavior:
"""
Поведение окон третьего уровня (Менеджер групп).
Свободное перемещение.
Чтобы выделить текст, просто кликай мышкой.
Чтобы двигать окно, зажми ALT + ЛКМ.
"""
def setup_level_three(self, target_window):
width, height = int(target_window.winfo_reqwidth()), int(target_window.winfo_reqheight())
target_window.minsize(width, height)
target_window.resizable(True, True)
target_window.geometry("+0+0")
target_window.protocol("WM_MINIMIZE_WINDOW", self.do_nothing)
target_window.protocol("WM_DELETE_WINDOW", target_window.destroy)
# Снимаем старые бинды
target_window.unbind("<ButtonPress-1>")
target_window.unbind("<B1-Motion>")
# 1. Сохраняем точку старта. Передаем 'target_window' вторым аргументом через lambda
target_window.bind(
"<ButtonPress-1>",
lambda e, tw=target_window: self.save_offset(e, tw),
add='+'
)
# 2. Движение работает ТОЛЬКО если зажата клавиша Alt (маска 0x0004)
target_window.bind(
"<B1-Motion>",
lambda e, tw=target_window: self.drag_free(e, tw) if e.state & 0x0004 else "",
add='+'
)
def do_nothing(self, event=None):
pass
def save_offset(self, event, window):
# Запоминаем ГДЕ НА ЭКРАНЕ была нажата мышь
self._start_screen_x = event.x_root
self._start_screen_y = event.y_root
# Запоминаем, где В ЭТОТ МОМЕНТ было окно (его угол) ПО ЭКРАНУ
self._start_win_x = window.winfo_rootx()
self._start_win_y = window.winfo_rooty()
def drag_free(self, event, window):
dx = event.x_root - self._start_screen_x
dy = event.y_root - self._start_screen_y
new_x = self._start_win_x + dx
new_y = self._start_win_y + dy
window.geometry(f"+{new_x}+{new_y}")