import tkinter as tk from tkinter import messagebox, filedialog class CrosswordApp: def __init__(self): self.root = tk.Tk() self.root.title("Генератор кроссворда — центральное слово вертикально + легенда") self.root.geometry("1250x820") # Главный контейнер с Canvas и Scrollbar self.main_canvas = tk.Canvas(self.root) self.scrollbar = tk.Scrollbar(self.root, orient="vertical", command=self.main_canvas.yview) self.scrollable_frame = tk.Frame(self.main_canvas) self.scrollable_frame.bind( "", lambda e: self.main_canvas.configure(scrollregion=self.main_canvas.bbox("all")) ) self.main_canvas.create_window((0, 0), window=self.scrollable_frame, anchor="nw") self.main_canvas.configure(yscrollcommand=self.scrollbar.set) # Размещаем элементы self.main_canvas.pack(side="left", fill="both", expand=True) self.scrollbar.pack(side="right", fill="y") # ====================== ВВОД ДАННЫХ ====================== input_frame = tk.Frame(self.scrollable_frame, padx=20, pady=15) input_frame.pack(fill="x") tk.Label(input_frame, text="Генератор кроссворда\n(центральное слово — вертикально по середине)", font=("Arial", 18, "bold")).pack(pady=10) # Центральное слово tk.Label(input_frame, text="Центральное слово (будет ВЕРТИКАЛЬНЫМ):", font=("Arial", 12, "bold")).pack(anchor="w", pady=(10, 0)) self.central_entry = tk.Entry(input_frame, width=85, font=("Arial", 14), justify="center") self.central_entry.pack(pady=8) tk.Button(input_frame, text="✅ Задать центральное слово", command=self.set_central_word, font=("Arial", 12), bg="#4CAF50", fg="white", height=1).pack(pady=5) self.central_word = None # Горизонтальные слова tk.Label(input_frame, text="Горизонтальные слова (по одному на строку):", font=("Arial", 12)).pack(anchor="w", pady=(15, 0)) self.other_words_text = tk.Text(input_frame, height=7, width=90, font=("Arial", 11)) self.other_words_text.pack(pady=5) # Подсказки tk.Label(input_frame, text="Подсказки (по одной на строку, в порядке горизонтальных слов):", font=("Arial", 12)).pack(anchor="w", pady=(10, 0)) tk.Label(input_frame, text="Пример: Столица Франции или 1. По горизонтали: Столица Франции", font=("Arial", 10), fg="gray").pack(anchor="w") self.clues_text = tk.Text(input_frame, height=10, width=90, font=("Arial", 11)) self.clues_text.pack(pady=5) # Кнопка генерации tk.Button(input_frame, text="🚀 СГЕНЕРИРОВАТЬ КРОССВОРД", command=self.generate_crossword, font=("Arial", 15, "bold"), bg="#2196F3", fg="white", height=2).pack(pady=20) # ====================== КНОПКИ ЭКСПОРТА ====================== export_frame = tk.Frame(self.scrollable_frame, pady=10) export_frame.pack() self.export_filled_btn = tk.Button( export_frame, text="💾 Экспорт ЗАПОЛНЕННОГО SVG (с буквами + легенда)", command=lambda: self.export_svg(filled=True), font=("Arial", 12), bg="#FF9800", fg="white", state="disabled", height=2 ) self.export_filled_btn.pack(side="left", padx=15) self.export_puzzle_btn = tk.Button( export_frame, text="💾 Экспорт ПУСТОГО SVG (подсказки, стрелки + легенда)", command=lambda: self.export_svg(filled=False), font=("Arial", 12), bg="#9C27B0", fg="white", state="disabled", height=2 ) self.export_puzzle_btn.pack(side="left", padx=15) # ====================== ОБЛАСТЬ ПРЕДПРОСМОТРА ====================== self.preview_frame = tk.Frame(self.scrollable_frame, bg="#f5f5f5", pady=15) self.preview_frame.pack(fill="both", expand=True, padx=20, pady=10) self.canvas = None self.shifted_grid = None self.grid_height = 0 self.grid_width = 0 self.word_placements = None self.clues_list = None # Привязка колесика мыши к скроллу self.main_canvas.bind_all("", self._on_mousewheel) self.root.mainloop() def _on_mousewheel(self, event): self.main_canvas.yview_scroll(int(-1 * (event.delta / 120)), "units") def set_central_word(self): word = self.central_entry.get().strip().upper().replace(" ", "") if not word or not word.isalpha(): messagebox.showerror("Ошибка", "Введите корректное центральное слово (только буквы)!") return self.central_word = word messagebox.showinfo("Готово", f"Центральное слово сохранено:\n{word}\nОно будет размещено вертикально по середине.") def generate_crossword(self): if not self.central_word: messagebox.showerror("Ошибка", "Сначала задайте центральное слово!") return # Получаем горизонтальные слова other_text = self.other_words_text.get("1.0", tk.END).strip() other_words = [line.strip().upper().replace(" ", "") for line in other_text.splitlines() if line.strip() and line.strip().isalpha()] if not other_words: messagebox.showerror("Ошибка", "Введите хотя бы одно горизонтальное слово!") return # Получаем подсказки clues_raw = self.clues_text.get("1.0", tk.END).strip().splitlines() self.clues_list = [] for line in clues_raw: clue = line.strip() if not clue: continue # Убираем номера в начале if clue[0].isdigit(): for i, ch in enumerate(clue): if ch.isalpha() or ch == '«' or ch == '"': self.clues_list.append(clue[i:].strip()) break else: self.clues_list.append(clue) # ====================== ПОСТРОЕНИЕ КРОССВОРДА ====================== central_list = list(self.central_word) n_rows_central = len(central_list) placements = {} used_rows = set() placed = [] not_placed = [] for word in other_words: found = False for row_idx in range(n_rows_central): if row_idx in used_rows: continue for idx in range(len(word)): if word[idx] == central_list[row_idx]: placements[row_idx] = (word, idx) used_rows.add(row_idx) placed.append(word) found = True break if found: break if not found: not_placed.append(word) # Построение сетки grid = {} word_placements = [] mid_col = 12 central_start_row = 8 # Центральное слово (вертикально) for i, letter in enumerate(central_list): r = central_start_row + i c = mid_col grid[(r, c)] = letter word_placements.append({ 'dir': 'down', 'start_r': central_start_row, 'start_c': mid_col, 'word': self.central_word, 'clue': "Центральное слово (вертикально)" }) # Горизонтальные слова clue_idx = 0 for central_r_idx, (word, match_idx) in placements.items(): r = central_start_row + central_r_idx start_c = mid_col - match_idx for j, letter in enumerate(word): c = start_c + j grid[(r, c)] = letter clue = self.clues_list[clue_idx] if clue_idx < len(self.clues_list) else f"Слово: {word}" word_placements.append({ 'dir': 'across', 'start_r': r, 'start_c': start_c, 'word': word, 'clue': clue }) clue_idx += 1 # Сдвиг координат rows_list = [r for r, c in grid.keys()] cols_list = [c for r, c in grid.keys()] min_r = min(rows_list) min_c = min(cols_list) shift_r = -min_r + 4 shift_c = -min_c + 4 self.shifted_grid = {(r + shift_r, c + shift_c): letter for (r, c), letter in grid.items()} self.grid_height = (max(rows_list) - min_r + 9) self.grid_width = (max(cols_list) - min_c + 9) for p in word_placements: p['start_r'] += shift_r p['start_c'] += shift_c # Нумерация word_placements.sort(key=lambda x: (x['start_r'], x['start_c'])) for num, p in enumerate(word_placements, 1): p['number'] = num self.word_placements = word_placements # ====================== ОТРИСОВКА ====================== if self.canvas: self.canvas.destroy() cell_size = 48 canvas_w = self.grid_width * cell_size + 40 canvas_h = self.grid_height * cell_size + 40 self.canvas = tk.Canvas(self.preview_frame, width=canvas_w, height=canvas_h, bg="#eeeeee", highlightthickness=0) self.canvas.pack(pady=10) for row in range(self.grid_height): for col in range(self.grid_width): x1 = col * cell_size + 20 y1 = row * cell_size + 20 x2 = x1 + cell_size y2 = y1 + cell_size key = (row, col) if key in self.shifted_grid: self.canvas.create_rectangle(x1, y1, x2, y2, fill="#ffffff", outline="#333333", width=3) self.canvas.create_text((x1 + x2)//2, (y1 + y2)//2, text=self.shifted_grid[key], font=("Arial", 24, "bold"), fill="#1a1a1a") else: self.canvas.create_rectangle(x1, y1, x2, y2, fill="#1a1a1a", outline="#1a1a1a") # Активация кнопок экспорта self.export_filled_btn.config(state="normal") self.export_puzzle_btn.config(state="normal") messagebox.showinfo("Готово!", f"Кроссворд сгенерирован!\n" f"Центральное слово: {self.central_word}\n" f"Размещено горизонтальных слов: {len(placed)}") def export_svg(self, filled=True): if not self.shifted_grid: messagebox.showerror("Ошибка", "Сначала сгенерируйте кроссворд!") return default_name = f"кроссворд_{self.central_word}_{'filled' if filled else 'puzzle'}.svg" filename = filedialog.asksaveasfilename( defaultextension=".svg", filetypes=[("SVG файлы", "*.svg")], initialfile=default_name ) if not filename: return cell_size = 65 padding = 60 grid_w = self.grid_width * cell_size grid_h = self.grid_height * cell_size legend_x = grid_w + padding + 100 total_w = legend_x + 520 total_h = max(grid_h + 120, 900) svg = f''' ''' # Сетка for row in range(self.grid_height): for col in range(self.grid_width): x = padding + col * cell_size y = padding + row * cell_size key = (row, col) if key in self.shifted_grid: svg += f' \n' if filled: letter = self.shifted_grid[key] svg += f' {letter}\n' start_info = next((p for p in self.word_placements if p.get('start_r') == row and p.get('start_c') == col), None) if start_info: num = start_info['number'] svg += f' {num}\n' if not filled: arrow = "→" if start_info['dir'] == "across" else "↓" svg += f' {arrow}\n' else: svg += f' \n' # Легенда y = padding + 40 svg += f' ПО ГОРИЗОНТАЛИ (Across)\n' y += 60 for p in self.word_placements: if p['dir'] == 'across': svg += f' {p["number"]}. {p["clue"]}\n' y += 38 y += 50 svg += f' ПО ВЕРТИКАЛИ (Down)\n' y += 55 for p in self.word_placements: if p['dir'] == 'down': svg += f' {p["number"]}. {p["clue"]}\n' y += 38 svg += '' try: with open(filename, "w", encoding="utf-8") as f: f.write(svg) mode_text = "ЗАПОЛНЕННЫЙ (с буквами)" if filled else "ПУСТОЙ (с подсказками, стрелками и легендой)" messagebox.showinfo("Успешно!", f"SVG-файл сохранён:\n{filename}\n\nРежим: {mode_text}") except Exception as e: messagebox.showerror("Ошибка сохранения", str(e)) if __name__ == "__main__": app = CrosswordApp()