/
Paff
/
declarant
Обзор
Документация
Войти
/
Paff
/
declarant
Код
Запросы
0
Задачи
Вики
Пакеты
0
Релизы
0
CI/CD
Аналитика
Безопасность
main
excel_widget.py
356 строк
15 KB
1pash1985-gif
Декларант - веб-версия для Timeweb Cloud
15 июл 2026, 09:36
15 июл 2026, 09:36
a6b08b8
Код
Авторство
О чём код?
""" Работа с Microsoft Excel через COM автоматизацию (без встраивания окна). Требует: pywin32, установленный Microsoft Excel """ import os import threading import tkinter as tk from tkinter import ttk, messagebox, filedialog try: import win32com.client as win32 import pythoncom WIN32_AVAILABLE = True except ImportError: WIN32_AVAILABLE = False class ExcelWidget(ttk.Frame): """ Виджет для работы с Microsoft Excel через COM. Открывает Excel как отдельное окно и предоставляет кнопки для управления и синхронизации данных. """ # Глобальный Excel Application (один для всех виджетов) _global_excel_app = None _global_excel_lock = threading.Lock() def __init__(self, master, file_path=None, **kwargs): """ Создать Excel виджет. Args: master: Родительский Tkinter виджет file_path: Путь к Excel файлу для открытия (опционально) """ super().__init__(master, **kwargs) if not WIN32_AVAILABLE: self._show_error("pywin32 не установлен. Установите: pip install pywin32") return self._excel_app = None self._workbook = None self._file_path = file_path self._init_thread = None self._is_initialized = False self._init_error = None # Настроить Frame self.columnconfigure(0, weight=1) self.rowconfigure(1, weight=1) # Информационная панель info_frame = ttk.Frame(self) info_frame.grid(row=0, column=0, sticky="ew", padx=5, pady=5) self._status_label = ttk.Label(info_frame, text="Загрузка Excel...") self._status_label.pack(side="left", padx=5) # Кнопки управления btn_frame = ttk.Frame(info_frame) btn_frame.pack(side="right", padx=5) self._btn_save = ttk.Button(btn_frame, text="Сохранить", command=self.save, state="disabled") self._btn_save.pack(side="left", padx=2) self._btn_save_as = ttk.Button(btn_frame, text="Сохранить как...", command=self.save_as, state="disabled") self._btn_save_as.pack(side="left", padx=2) self._btn_bring_to_front = ttk.Button(btn_frame, text="Показать Excel", command=self._bring_excel_to_front, state="disabled") self._btn_bring_to_front.pack(side="left", padx=2) # Placeholder self._placeholder = ttk.Label(self, text="Инициализация Excel...", foreground="gray") self._placeholder.grid(row=1, column=0, sticky="nsew", padx=20, pady=20) # Запустить инициализацию в отдельном потоке self._init_thread = threading.Thread(target=self._init_excel_thread, daemon=True) self._init_thread.start() # Периодически проверять готовность self._check_init_complete() def _show_error(self, message): """Показать сообщение об ошибке с кнопкой 'Повторить'.""" try: # Очистить текущие виджеты (кроме toolbar) for widget in self.winfo_children(): if widget != self._placeholder: try: widget.destroy() except: pass # Создать Frame для сообщения об ошибке error_frame = ttk.Frame(self) error_frame.grid(row=1, column=0, sticky="nsew", padx=20, pady=20) # Иконка ошибки ttk.Label(error_frame, text="⚠", font=("Arial", 48), foreground="orange").pack(pady=10) # Текст ошибки error_label = ttk.Label(error_frame, text=message, foreground="red", wraplength=400, justify="center") error_label.pack(pady=10) # Кнопка повторить ttk.Button(error_frame, text="Повторить", command=self._retry_init).pack(pady=10) # Подсказка hint_label = ttk.Label(error_frame, text="Убедитесь, что Microsoft Excel установлен на компьютере", foreground="gray", font=("Arial", 9)) hint_label.pack(pady=5) except Exception as e: print(f"[ERROR] _show_error: {e}") def _retry_init(self): """Повторить инициализацию Excel.""" # Сбросить состояние self._is_initialized = False self._init_error = None self._workbook = None self._excel_app = None # Очистить UI for widget in self.winfo_children(): try: widget.destroy() except: pass # Пересоздать UI self.columnconfigure(0, weight=1) self.rowconfigure(1, weight=1) # Информационная панель info_frame = ttk.Frame(self) info_frame.grid(row=0, column=0, sticky="ew", padx=5, pady=5) self._status_label = ttk.Label(info_frame, text="Загрузка Excel...") self._status_label.pack(side="left", padx=5) # Кнопки управления btn_frame = ttk.Frame(info_frame) btn_frame.pack(side="right", padx=5) self._btn_save = ttk.Button(btn_frame, text="Сохранить", command=self.save, state="disabled") self._btn_save.pack(side="left", padx=2) self._btn_save_as = ttk.Button(btn_frame, text="Сохранить как...", command=self.save_as, state="disabled") self._btn_save_as.pack(side="left", padx=2) self._btn_bring_to_front = ttk.Button(btn_frame, text="Показать Excel", command=self._bring_excel_to_front, state="disabled") self._btn_bring_to_front.pack(side="left", padx=2) # Placeholder self._placeholder = ttk.Label(self, text="Инициализация Excel...", foreground="gray") self._placeholder.grid(row=1, column=0, sticky="nsew", padx=20, pady=20) # Запустить инициализацию в отдельном потоке self._init_thread = threading.Thread(target=self._init_excel_thread, daemon=True) self._init_thread.start() # Периодически проверять готовность self._check_init_complete() def _init_excel_thread(self): """Инициализировать Excel в отдельном потоке (не блокирует GUI).""" try: # Инициализировать COM для этого потока pythoncom.CoInitialize() # Получить или создать глобальный Excel Application with ExcelWidget._global_excel_lock: if ExcelWidget._global_excel_app is None: print("[DEBUG] Creating new Excel Application...") try: # Попробовать подключиться к существующему Excel ExcelWidget._global_excel_app = win32.Dispatch("Excel.Application") except Exception as e1: print(f"[DEBUG] Failed to connect to existing Excel: {e1}") # Если не получилось, создать новый ExcelWidget._global_excel_app = win32.DispatchEx("Excel.Application") ExcelWidget._global_excel_app.Visible = True ExcelWidget._global_excel_app.DisplayAlerts = False print("[DEBUG] Excel Application created successfully") self._excel_app = ExcelWidget._global_excel_app # Открыть файл или создать новую книгу if self._file_path and os.path.exists(self._file_path): print(f"[DEBUG] Opening file: {self._file_path}") abs_path = os.path.abspath(self._file_path) self._workbook = self._excel_app.Workbooks.Open(abs_path) print(f"[DEBUG] File opened successfully") else: print("[DEBUG] Creating new workbook") self._workbook = self._excel_app.Workbooks.Add() print(f"[DEBUG] New workbook created") # Активировать workbook (сделать его активным окном) self._workbook.Activate() # Пометить как инициализированный self._is_initialized = True print(f"[DEBUG] Excel initialized: {self._file_path}") except Exception as e: print(f"[ERROR] Excel init failed: {type(e).__name__}: {e}") import traceback traceback.print_exc() self._init_error = f"{type(e).__name__}: {e}" # Сбросить глобальный Excel Application если он был создан with ExcelWidget._global_excel_lock: if ExcelWidget._global_excel_app is not None: try: ExcelWidget._global_excel_app.Quit() except: pass ExcelWidget._global_excel_app = None def _check_init_complete(self): """Периодически проверять, завершена ли инициализация.""" try: if self._init_error: self._show_error(f"Ошибка инициализации Excel: {self._init_error}") return if self._is_initialized: # Инициализация завершена — обновить UI self._on_excel_ready() else: # Проверить снова через 100мс self.after(100, self._check_init_complete) except Exception as e: print(f"[ERROR] Error in _check_init_complete: {e}") def _on_excel_ready(self): """Вызывается когда Excel готов к работе.""" try: # Обновить placeholder file_name = os.path.basename(self._file_path) if self._file_path else "Новая книга" if self._placeholder: self._placeholder.config( text=f"Excel открыт: {file_name}\n\n" f"Используйте кнопку 'Показать Excel' для работы с файлом.\n" f"Или редактируйте напрямую в окне Excel.", foreground="green" ) # Активировать кнопки self._btn_save.config(state="normal") self._btn_save_as.config(state="normal") self._btn_bring_to_front.config(state="normal") self._status_label.config(text=f"Excel: {file_name}") except Exception as e: print(f"[ERROR] _on_excel_ready: {e}") def _bring_excel_to_front(self): """Показать окно Excel поверх всех окон.""" try: if self._excel_app and self._workbook: self._excel_app.Visible = True self._workbook.Activate() # Попытка поднять окно try: import win32gui import win32con hwnd = self._excel_app.Hwnd win32gui.ShowWindow(hwnd, win32con.SW_RESTORE) win32gui.SetForegroundWindow(hwnd) except: pass except Exception as e: print(f"[ERROR] Не удалось показать Excel: {e}") def save(self): """Сохранить workbook.""" try: if self._workbook: self._workbook.Save() messagebox.showinfo("Успех", "Файл сохранён!") return True except Exception as e: messagebox.showerror("Ошибка", f"Не удалось сохранить: {e}") return False def save_as(self): """Сохранить workbook с диалогом 'Сохранить как'.""" try: if not self._workbook: return False file_path = filedialog.asksaveasfilename( title="Сохранить Excel файл", defaultextension=".xlsx", filetypes=[ ("Excel файлы", "*.xlsx *.xlsm *.xls"), ("XLSX файлы", "*.xlsx"), ("XLSM файлы", "*.xlsm"), ("XLS файлы", "*.xls"), ("Все файлы", "*.*") ] ) if file_path: self._workbook.SaveAs(file_path) self._file_path = file_path self._status_label.config(text=f"Excel: {os.path.basename(file_path)}") messagebox.showinfo("Успех", f"Файл сохранён как: {file_path}") return True except Exception as e: messagebox.showerror("Ошибка", f"Не удалось сохранить: {e}") return False def close(self): """Закрыть Excel workbook.""" try: if self._workbook: try: self._workbook.Close(SaveChanges=False) except: pass self._workbook = None except Exception as e: print(f"[ERROR] Ошибка закрытия Excel: {e}") # Тест if __name__ == "__main__": if not WIN32_AVAILABLE: print("pywin32 не установлен!") print("Установите: pip install pywin32") exit(1) root = tk.Tk() root.title("Excel Widget Test") root.geometry("800x600") # Создать Excel виджет excel = ExcelWidget(root, file_path=None) # Новая книга excel.pack(fill="both", expand=True, padx=10, pady=10) # Кнопка закрытия ttk.Button(root, text="Закрыть", command=lambda: (excel.close(), root.destroy())).pack(pady=10) root.protocol("WM_DELETE_WINDOW", lambda: (excel.close(), root.destroy())) root.mainloop()