вот мой код по отслеживанию текста в программе. проанналзируй его и выдай структуру проекта, чтобы раскидать файлы. а так же распиши полный код для каждого модуля

import tkinter as tk import win32gui import win32process import psutil import time import traceback import pythoncom import queue

import redis # Раскомментируйте, если используется
import threading import ctypes.wintypes import logging

from pywinauto.application import Application from pywinauto.findwindows import ElementNotFoundError

from logger_setup import setup_logger from hooks import HookManager from tkinter import messagebox

from config import TARGET_ELEMENT_CONFIG, load_settings, save_settings # Импорт из config.py from win_utils import ( # Импорт из win_utils.py get_window_text, enable_window_and_refresh, is_window_valid, is_process_alive, get_class_name, show_modal_message_box ) from gui_settings_dialog import SettingsDialog # Импорт из gui_settings_dialog.py

logger = setup_logger(name)

loggers = [logging.getLogger(name) for name in logging.root.manager.loggerDict] for x in loggers: setup_logger(x.name)

user32 = ctypes.windll.user32

def find_target_process_and_window(): logger.debug("main_app.find_target_process_and_window Вызов")

# 1. Получаем настройки из загруженного конфига
proc_name = TARGET_ELEMENT_CONFIG["process_name"]
main_title = TARGET_ELEMENT_CONFIG["main_window_title"]

if not proc_name or not main_title:
    logger.error("Целевой элемент не настроен. Пожалуйста, используйте Настройки.")
    return None, None, None, None

# 2. Подключаемся к приложению
try:
    app = Application(backend="uia").connect(path=proc_name, timeout=5)
    main_win = app.window(title_re=f".*{main_title}.*")
    main_win.wait('ready', timeout=10)
    pid = main_win.process_id()
    logger.info(f"Найдено главное окно: '{main_win.window_text()}', PID={pid}")
except (ElementNotFoundError, RuntimeError) as e:
    logger.warning(f"Главное окно '{main_title}' не найдено или не отвечает: {e}. Попытка позже.")
    return None, None, None, None

# 3. Ищем целевое ПОЛЕ ВВОДА
field_criteria_config = TARGET_ELEMENT_CONFIG.get("search_criteria", {})
active_field_criteria = {k: v for k, v in field_criteria_config.items() if v}
target_element = None # Инициализируем

if not active_field_criteria:
    logger.error("В конфиге не заданы критерии поиска для целевого поля ввода.")
else:
    try:
        search_criteria = {
            "auto_id": active_field_criteria.get("automation_id"),
            "title": active_field_criteria.get("title"),
            "class_name": active_field_criteria.get("class_name")
        }

        target_element = main_win.child_window(**search_criteria)
        target_element.wait('exists', timeout=5)
        logger.info(f"Найдено целевое поле с критериями {search_criteria}: HWND={target_element.handle}")
    except ElementNotFoundError:
        logger.warning(f"Не найдено целевое поле с критериями: {search_criteria}. Попытка позже.")
        # Не выходим, возможно, кнопка найдется, а поле появится позже

# 4. Ищем целевую КНОПКУ
button_criteria_config = TARGET_ELEMENT_CONFIG.get("button_search_criteria", {})
active_button_criteria = {k: v for k, v in button_criteria_config.items() if v}
target_button = None # Инициализируем

if not active_button_criteria:
    logger.error("В конфиге не заданы критерии поиска для целевой кнопки.")
else:
    try:
        button_criteria = {
            "auto_id": active_button_criteria.get("automation_id"),
            "title": active_button_criteria.get("title"),
            "class_name": active_button_criteria.get("class_name")
        }

        target_button = main_win.child_window(**button_criteria)
        target_button.wait('exists', timeout=5)
        logger.info(f"Найдена целевая кнопка с критериями {button_criteria}: HWND={target_button.handle}")
    except ElementNotFoundError:
        logger.warning(f"Не найдена целевая кнопка с критериями: {button_criteria}. Попытка позже.")

# Возвращаем все, что удалось найти
return main_win, target_element, target_button, pid
command_queue = queue.Queue() event_queue = queue.Queue()

def hook_worker_main(stop_event): pythoncom.CoInitialize() hooks = None main_ezcad_hwnd, target_edit_hwnd, button_hwnd, pid = (None, None, None, None)

# --- Переменные состояния ---
duplicate_confirmed = False
duplicate_warning_sent = False
last_processed_text = ""

def validate_text(text):
    nonlocal duplicate_confirmed, duplicate_warning_sent
    is_duplicate = any(c.isdigit() for c in text)
    is_valid = len(text.strip()) > 0 and (not is_duplicate or duplicate_confirmed)

    if hooks is None or not button_hwnd or not is_window_valid(button_hwnd):
        return

    try:
        if is_valid:
            if not win32gui.IsWindowEnabled(button_hwnd):
                enable_window_and_refresh(button_hwnd, True)
            hooks.block_enter = False
            logger.debug(f"Текст '{text}' валиден. Кнопка и Enter разблокированы.")
        else:
            if win32gui.IsWindowEnabled(button_hwnd):
                enable_window_and_refresh(button_hwnd, False)
            hooks.block_enter = True
            if not duplicate_warning_sent:
                event_queue.put({'warning': 'duplicate'})
                duplicate_warning_sent = True
                logger.info(f"Обнаружен дубль в тексте: '{text}'. Отправлено предупреждение в GUI.")
            else:
                logger.debug(f"Текст '{text}' НЕ валиден, но предупреждение уже было отправлено.")
    except Exception as ex:
        logger.error(f"Ошибка в validate_text: {ex}")

try:
    while not stop_event.is_set():
        actual_target_edit_hwnd = target_edit_hwnd
        if hooks:
            actual_target_edit_hwnd = hooks.target_edit_hwnd_getter()

        if not is_window_valid(main_ezcad_hwnd) or \
                not is_window_valid(actual_target_edit_hwnd) or \
                not button_hwnd or not is_window_valid(button_hwnd):

            if hooks:
                logger.info("Целевое окно/элементы недоступны. Удаляем хуки.")
                hooks.remove_win_event_hook();
                hooks.remove_keyboard_hook()
                hooks = None

            duplicate_confirmed = False
            duplicate_warning_sent = False
            last_processed_text = ""
            main_ezcad_hwnd, target_edit_hwnd, button_hwnd, pid = None, None, None, None

            logger.debug("Поиск целевого процесса и элементов...")
            main_win_w, target_element_w, button_w, pid = find_target_process_and_window()

            if main_win_w and target_element_w and button_w and pid:
                main_ezcad_hwnd, target_edit_hwnd, button_hwnd = main_win_w.handle, target_element_w.handle, button_w.handle
                logger.info(
                    f"Найдены HWND: главное окно={main_ezcad_hwnd}, поле={target_edit_hwnd}, кнопка={button_hwnd}")

                hooks = HookManager(lambda: target_edit_hwnd, lambda: main_ezcad_hwnd)
                if not hooks.install_keyboard_hook() or not hooks.install_win_event_hook(pid):
                    logger.error("Не удалось установить хуки. Повторная попытка...")
                    if hooks: hooks.remove_keyboard_hook(); hooks.remove_win_event_hook()
                    hooks = None
                    time.sleep(2)
                    continue

                logger.info("Хуки успешно установлены. Начало отслеживания.")
                event_queue.put(
                    {'target_edit_hwnd': target_edit_hwnd, 'find_window_hwnd': main_ezcad_hwnd, 'target_pid': pid})

                try:
                    current_text = get_window_text(target_edit_hwnd)
                    last_processed_text = current_text
                    # todo вернуть, когда мы будем сбрасывать потверждения дубля после маркировки
                    # validate_text(current_text)
                    event_queue.put(current_text)
                except Exception as e:
                    logger.error(f"Ошибка при получении текста для начальной валидации: {e}")
            else:
                time.sleep(2)
                continue

        if hooks:
            pythoncom.PumpWaitingMessages()

            try:
                new_text = None
                while not hooks.text_queue.empty():
                    new_text = hooks.text_queue.get_nowait()

                if new_text is not None and new_text != last_processed_text:
                    logger.debug(f"Текст изменился с '{last_processed_text}' на '{new_text}'.")

                    # 1. Проверяем, является ли НОВЫЙ текст дублем.
                    is_new_text_duplicate = any(c.isdigit() for c in new_text)

                    # 2. Сбрасываем флаг подтверждения, ТОЛЬКО ЕСЛИ новый текст
                    #    перестал быть дублем (т.е. пользователь удалил все цифры).
                    if not is_new_text_duplicate:
                        if duplicate_confirmed:  # Логируем для отладки, что сброс произошел
                            logger.info(
                                f"Текст '{new_text}' больше не является дублем. Сброс 'липкого' подтверждения.")
                        duplicate_confirmed = False

                    # 3. Флаг отправки предупреждения сбрасываем всегда при изменении текста,
                    #    чтобы система была готова показать его для нового невалидного
                    #    значения, если потребуется (если `duplicate_confirmed` будет False).
                    duplicate_warning_sent = False

                    # 4. Обновляем последнюю обработанную строку.
                    last_processed_text = new_text

                    # 5. Выполняем валидацию с обновленным состоянием.
                    validate_text(new_text)
                    event_queue.put(new_text)

            except queue.Empty:
                pass
            except Exception as e:
                logger.warning(f"Ошибка при обработке очереди текста: {e}")

        try:
            cmd = command_queue.get_nowait()
            if cmd == "STOP":
                stop_event.set()
                break
            elif cmd == "CONFIRM_DUPLICATE":
                logger.info("Получена команда подтверждения дубликата. Включен режим 'липкого' подтверждения.")
                duplicate_confirmed = True
                validate_text(last_processed_text)
        except queue.Empty:
            pass

        time.sleep(0.05)

except Exception as e:
    logger.critical(f"Критическая ошибка в hook_worker_main: {e}\n{traceback.format_exc()}")
finally:
    if hooks:
        hooks.remove_win_event_hook()
        hooks.remove_keyboard_hook()
    pythoncom.CoUninitialize()
    logger.info("Поток hook_worker_main завершил свою работу.")
def find_ezcad_process(): for proc in psutil.process_iter(['pid', 'name']): if proc.info['name'] and proc.info['name'].lower() == TARGET_ELEMENT_CONFIG['process_name'].lower(): return True return False

class AppGUI: def init(self, root_tk): self.root = root_tk self.root.title("Монитор окна 'Найти'") self.root.geometry("400x200")

    self.status_label = tk.Label(self.root, text="Статус хуков: Выключены")
    self.status_label.pack(pady=5)

    self.text_label = tk.Label(self.root, text="Последний текст: —")
    self.text_label.pack(pady=5)

    button_frame = tk.Frame(self.root)
    button_frame.pack(pady=10)

    self.start_button = tk.Button(button_frame, text="Запустить хуки", command=self.start_hooks)
    self.start_button.pack(side=tk.LEFT, padx=5)

    self.stop_button = tk.Button(button_frame, text="Остановить хуки", command=self.stop_hooks,
                                 state='disabled')
    self.stop_button.pack(side=tk.LEFT, padx=5)

    self.settings_button = tk.Button(button_frame, text="Настройки", command=self.open_settings)
    self.settings_button.pack(side=tk.LEFT, padx=5)

    self.stop_event = None
    self.hook_thread = None
    self.target_edit_hwnd = None
    self.main_ezcad_hwnd = None
    self.target_pid = None
    self.showing_duplicate_warning = False

    self.check_events()
    self.root.protocol("WM_DELETE_WINDOW", self.on_closing)

def open_settings(self):
    if not find_ezcad_process():
        messagebox.showwarning("EzCad2 не найден",
                               f"Процесс '{TARGET_ELEMENT_CONFIG['process_name']}' не найден. "
                               "Убедитесь, что он запущен перед открытием настроек.", parent=self.root)
    settings_dialog = SettingsDialog(self.root)
    self.root.wait_window(settings_dialog)
    logger.info("Диалог настроек закрыт.")  # Это лог для разработчика

def start_hooks(self):
    if self.hook_thread and self.hook_thread.is_alive():
        messagebox.showinfo("Информация", "Хуки уже запущены.", parent=self.root)
        return

    # not TARGET_ELEMENT_CONFIG["target_hwnd"] or
    # todo возможно что-то вернуть
    # if  not TARGET_ELEMENT_CONFIG["target_class"]:
    #     messagebox.showerror("Отсутствует конфигурация",
    #                          "Целевой элемент не настроен. Пожалуйста, используйте 'Настройки' для выбора элемента в EzCad2.",
    #                          parent=self.root)
    #     return

    def clear_q(q):
        try:
            while True: q.get_nowait()
        except queue.Empty:
            pass

    clear_q(command_queue)
    clear_q(event_queue)

    self.stop_event = threading.Event()
    self.hook_thread = threading.Thread(target=hook_worker_main, args=(self.stop_event,), daemon=True)
    self.hook_thread.start()

    self.status_label.config(text="Статус хуков: Запущены")
    self.start_button.config(state='disabled')
    self.stop_button.config(state='normal')
    self.settings_button.config(state='disabled')
    logger.info("Хуки запущены из GUI.")

def stop_hooks(self):
    if not self.hook_thread or not self.hook_thread.is_alive(): return

    if messagebox.askyesno("Подтверждение", "Остановить хуки?", parent=self.root):
        command_queue.put("STOP")
        self.status_label.config(text="Статус хуков: Остановка...")
        logger.info("Хуки остановлены из GUI.")

def check_events(self):
    try:
        while True:
            evt = event_queue.get_nowait()
            if isinstance(evt, dict):
                if 'target_edit_hwnd' in evt: self.target_edit_hwnd = evt['target_edit_hwnd']
                if 'find_window_hwnd' in evt: self.main_ezcad_hwnd = evt['find_window_hwnd']
                if 'target_pid' in evt: self.target_pid = evt['target_pid']
                if evt.get("warning") == "duplicate" and not self.showing_duplicate_warning:
                    self.showing_duplicate_warning = True
                    self.show_duplicate_warning()
            else:
                self.text_label.config(text=f"Последний текст: {evt}")
    except queue.Empty:
        pass

    if self.hook_thread and not self.hook_thread.is_alive():
        if self.status_label.cget("text") != "Статус хуков: Остановлен":  # Избегаем лишних логов
            logger.info("Поток хуков завершился.")  # Лог для разработчика
        self.status_label.config(text="Статус хуков: Остановлен")
        self.start_button.config(state='normal')
        self.stop_button.config(state='disabled')
        self.settings_button.config(state='normal')
        self.hook_thread = None

    self.root.after(100, self.check_events)

def show_duplicate_warning(self):
    # Запоминаем текущее активное окно, чтобы потом вернуть ему фокус
    foreground_window_before = win32gui.GetForegroundWindow()

    # Временно делаем наше GUI окно верхним, чтобы оно могло породить диалог, который тоже будет верхним
    # Это небольшой трюк, чтобы диалог гарантированно появился поверх всего
    self.root.attributes('-topmost', True)

    # Вызываем нашу новую функцию, передавая HWND главного окна EzCad2 как родителя
    res = show_modal_message_box(
        self.main_ezcad_hwnd,
        "Обнаружен дубль",
        "Обнаружен дубль.\nПодтвердить ввод?"
    )

    # Возвращаем атрибут topmost в исходное состояние
    self.root.attributes('-topmost', False)

    # Пытаемся вернуть фокус окну, которое было активным до диалога
    try:
        if is_window_valid(foreground_window_before):
            win32gui.SetForegroundWindow(foreground_window_before)
    except Exception as e:
        logger.debug(f"Не удалось вернуть фокус предыдущему окну: {e}")

    # Проверяем, что целевые элементы все еще существуют, прежде чем отправлять команду
    can_confirm = (self.target_edit_hwnd and is_window_valid(self.target_edit_hwnd) and
                   self.main_ezcad_hwnd and is_window_valid(self.main_ezcad_hwnd) and
                   self.target_pid and is_process_alive(self.target_pid))

    if res and can_confirm:
        logger.info("Пользователь подтвердил дубль. Отправка команды.")
        command_queue.put("CONFIRM_DUPLICATE")
    elif res and not can_confirm:
        logger.warning("Пользователь подтвердил дубль, но целевой процесс/окно уже не действительны.")
    else:
        logger.info("Пользователь отклонил ввод дубля.")

    # Сбрасываем флаг в любом случае, чтобы можно было показать предупреждение снова
    self.showing_duplicate_warning = False

def on_closing(self):
    if messagebox.askokcancel("Выход", "Закрыть программу?", parent=self.root):
        if self.hook_thread and self.hook_thread.is_alive():
            logger.info("Закрытие: Остановка потока хуков.")
            command_queue.put("STOP")
            if self.stop_event: self.stop_event.set()
            self.hook_thread.join(timeout=2)
        self.root.destroy()
def main(): try: pythoncom.CoInitializeEx(pythoncom.COINIT_APARTMENTTHREADED) except pythoncom.com_error as e: logger.warning(f"Ошибка CoInitializeEx: {e}. COM может быть уже инициализирован.")

load_settings(logger) # Загружаем настройки

# init_redis_keys() # Если используете Redis
# load_uia_module() # Если используете UIAutomation

root_tk = tk.Tk()
app = AppGUI(root_tk)
root_tk.mainloop()

save_settings(logger) # Сохранение настроек

pythoncom.CoUninitialize()
logger.info("Программа завершена.")  
if name == "main": try: main() except Exception as e_main: logger.critical(f"Необработанное исключение в main: {e_main}\n{traceback.format_exc()}") finally: logger.info("Основной скрипт завершил выполнение.")