у меня так выполняется программа. если я хочу в будущем добавить gui tkinter, как сделать чтобы программа имела возможность настроек из трея, показ предупреждений, и при этом выполнялось отслеживание текста. можно ли пользоваться в программе чем нибудь, пока работает отслеживание, хотя бы вызвать остановку процесса? нужна какая то оболочка

Код программы:
import json
import traceback
import win32gui
import win32process
import psutil
import ctypes
import ctypes.wintypes
import time
import comtypes
import comtypes.client
import pythoncom
import queue  # Для передачи данных из колбэка в основной поток
from datetime import datetime

from Win.redis_conn import redis_sync_conn

redis_sync_conn.set('st_1d::', json.dumps(list(range(560, 980))))
redis_sync_conn.set('st_1e::', json.dumps(list(range(999, 2500))))
redis_sync_conn.set('st_1f::', json.dumps(list(range(2600, 3100))))

# Инициализация COM
pythoncom.CoInitializeEx(pythoncom.COINIT_APARTMENTTHREADED)

# UIA для получения HWND и начального значения
comtypes.client.GetModule('UIAutomationCore.dll')
from comtypes.gen.UIAutomationClient import (
    CUIAutomation, IUIAutomationValuePattern,
    UIA_NativeWindowHandlePropertyId
)

try:
    from comtypes.gen.UIAutomationClient import TreeScope_Descendants as UIA_STANDARD_TREESCOPE_DESCENDANTS
except ImportError:
    UIA_STANDARD_TREESCOPE_DESCENDANTS = 4

UIA_ValuePatternId = 10002
UIA_ControlTypePropertyId = 30003
UIA_EditControlTypeId = 50004

# Константы для SetWinEventHook
WINEVENT_OUTOFCONTEXT = 0x0000
EVENT_OBJECT_VALUECHANGE = 0x800C
EVENT_OBJECT_NAMECHANGE = 0x800B  # На всякий случай
OBJID_CLIENT = 0xFFFFFFFC  # Клиентская область окна, содержащего объект

# Для WM_GETTEXT
WM_GETTEXT = 0x000D
WM_GETTEXTLENGTH = 0x000E

user32 = ctypes.windll.user32
kernel32 = ctypes.windll.kernel32

# Определения для SetWinEventHook и UnhookWinEvent
WINEVENTPROC = ctypes.WINFUNCTYPE(
    None,  # void
    ctypes.wintypes.HANDLE,  # hWinEventHook
    ctypes.wintypes.DWORD,  # event
    ctypes.wintypes.HWND,  # hwnd
    ctypes.wintypes.LONG,  # idObject
    ctypes.wintypes.LONG,  # idChild
    ctypes.wintypes.DWORD,  # dwEventThread
    ctypes.wintypes.DWORD  # dwmsEventTime
)
user32.SetWinEventHook.argtypes = [
    ctypes.wintypes.DWORD, ctypes.wintypes.DWORD, ctypes.wintypes.HMODULE,
    WINEVENTPROC, ctypes.wintypes.DWORD, ctypes.wintypes.DWORD, ctypes.wintypes.DWORD
]
user32.SetWinEventHook.restype = ctypes.wintypes.HANDLE
user32.UnhookWinEvent.argtypes = [ctypes.wintypes.HANDLE]
user32.UnhookWinEvent.restype = ctypes.wintypes.BOOL

# Для клавиатурного хука (остается без изменений)
LRESULT = ctypes.c_longlong if ctypes.sizeof(ctypes.c_void_p) == 8 else ctypes.c_long
HOOKPROC = ctypes.WINFUNCTYPE(LRESULT, ctypes.c_int, ctypes.wintypes.WPARAM, ctypes.wintypes.LPARAM)

LowLevelKeyboardProc = ctypes.WINFUNCTYPE(
    ctypes.c_long,
    ctypes.c_int,
    ctypes.wintypes.WPARAM,
    ctypes.wintypes.LPARAM
)
# ... (остальные определения для хука клавиатуры) ...

# Глобальные переменные
hook_id_kb = None  # Для клавиатурного хука
block_enter = False
find_window_hwnd = None  # HWND диалога "Найти"
target_edit_hwnd = None  # HWND самого поля Edit
target_pid = 0
target_thread_id = 0  # Можно оставить 0 для SetWinEventHook
win_event_hook = None
CMPFUNC_KB = None  # Для клавиатурного хука
CMPFUNC_EVENT = None  # Для WinEvent хука

text_validation_queue = queue.Queue()

target_process_name = "notepad.exe"
target_window_substr = "Найти"


# --- Клавиатурный хук (без изменений) ---
class KBDLLHOOKSTRUCT(ctypes.Structure):
    _fields_ = [
        ("vkCode", ctypes.wintypes.DWORD), ("scanCode", ctypes.wintypes.DWORD),
        ("flags", ctypes.wintypes.DWORD), ("time", ctypes.wintypes.DWORD),
        ("dwExtraInfo", ctypes.POINTER(ctypes.c_ulong))
    ]


def keyboard_hook_proc(nCode, wParam, lParam):
    global block_enter, find_window_hwnd, hook_id_kb
    if nCode == 0:
        kb_struct = ctypes.cast(lParam, ctypes.POINTER(KBDLLHOOKSTRUCT)).contents
        if wParam == 0x0100 and kb_struct.vkCode == 0x0D:  # WM_KEYDOWN, VK_RETURN
            fg_hwnd = user32.GetForegroundWindow()
            if find_window_hwnd and (fg_hwnd == find_window_hwnd or win32gui.IsChild(find_window_hwnd, fg_hwnd)):
                if block_enter:
                    print("[KB HOOK] Enter заблокирован!")
                    return 1
    return user32.CallNextHookEx(
        ctypes.c_void_p(hook_id_kb),
        ctypes.c_int(nCode),
        ctypes.wintypes.WPARAM(wParam),
        ctypes.wintypes.LPARAM(lParam)
    )


def install_keyboard_hook():
    global hook_id_kb, CMPFUNC_KB
    CMPFUNC_KB = LowLevelKeyboardProc(keyboard_hook_proc)
    # ... (остальное без изменений)

    hook_id_kb = user32.SetWindowsHookExW(13, CMPFUNC_KB, 0, 0)  # WH_KEYBOARD_LL = 13
    if not hook_id_kb:
        err_code = ctypes.GetLastError()
        print(f"Ошибка установки хука: {err_code} - {ctypes.WinError(err_code)}")
        hook_id = None
        raise ctypes.WinError(err_code)
    else:
        hook_id = hook_id_kb
        print(f"[HOOK] Хук клавиатуры установлен. ID: {hook_id if hook_id else 'N/A'}")

def remove_keyboard_hook():
    global hook_id_kb
    if hook_id_kb: user32.UnhookWindowsHookEx(hook_id_kb); hook_id_kb = None; print(
        "[KB HOOK] Клавиатурный хук удален.")


# --- Конец клавиатурного хука ---


def find_target_process_and_window():
    global find_window_hwnd, target_edit_hwnd, target_pid, target_thread_id

    temp_find_window_hwnd = None

    # Найти окно "Найти"
    def enum_windows_callback(hwnd, windows_list):
        if win32gui.IsWindowVisible(hwnd):
            title = win32gui.GetWindowText(hwnd)
            if target_window_substr in title:
                try:
                    win_thread_id, proc_id = win32process.GetWindowThreadProcessId(hwnd)
                    p = psutil.Process(proc_id)
                    if p.name().lower() == target_process_name.lower():
                        windows_list.append((hwnd, proc_id, win_thread_id))  # Сохраняем HWND, PID, ThreadID
                except (psutil.NoSuchProcess, psutil.AccessDenied, win32process.error):
                    pass
        return True

    windows = []
    win32gui.EnumWindows(enum_windows_callback, windows)
    if not windows: return False

    find_window_hwnd, target_pid, target_thread_id = windows[0]  # Берем первое найденное

    # Используем UIA, чтобы найти HWND поля Edit внутри диалога "Найти"
    try:
        uia = comtypes.client.CreateObject(CUIAutomation)
        element_find_dialog = uia.ElementFromHandle(find_window_hwnd)
        if not element_find_dialog: return False

        condition = uia.CreatePropertyCondition(UIA_ControlTypePropertyId, UIA_EditControlTypeId)
        edit_element = element_find_dialog.FindFirst(UIA_STANDARD_TREESCOPE_DESCENDANTS, condition)
        if not edit_element: return False

        target_edit_hwnd = edit_element.CurrentNativeWindowHandle
        if not target_edit_hwnd:
            print("Не удалось получить NativeWindowHandle для Edit элемента.")
            return False
        print(f"Найден Edit элемент с HWND: {target_edit_hwnd}")
        return True
    except Exception as e:
        print(f"Ошибка при поиске Edit HWND через UIA: {e}")
        return False


def get_text_from_hwnd(hwnd):
    """Получает текст из окна с заданным HWND."""
    length = user32.SendMessageW(hwnd, WM_GETTEXTLENGTH, 0, 0)
    if length == 0:
        return ""
    buffer = ctypes.create_unicode_buffer(length + 1)
    user32.SendMessageW(hwnd, WM_GETTEXT, length + 1, ctypes.byref(buffer))
    return buffer.value


# --- WinEventHook Callback ---
def win_event_proc(hWinEventHook, event, hwnd, idObject, idChild, dwEventThread, dwmsEventTime):
    global target_edit_hwnd
    # print(f"Event: {event:#X}, HWND: {hwnd}, idObject: {idObject:#X}, idChild: {idChild:#X}")

    # Фильтруем: нас интересует только наше поле Edit и определенные события
    # and idObject == OBJID_CLIENT
    if hwnd == target_edit_hwnd:
        if event == EVENT_OBJECT_VALUECHANGE or event == EVENT_OBJECT_NAMECHANGE:  # Проверяем оба
            # Получаем текущее время с микросекундами
            now = datetime.now()

            # Форматируем как часы:минуты:секунды:миллисекунды (три цифры миллисекунд, т.к. microsec / 1000)
            current_time_evt = now.strftime("%H:%M:%S:%f")[:-3]  # Убираем последние три цифры микросекунд, т.е. оставляем миллисекунды
            print(f"[{current_time_evt}] >>> WinEventHook: Получено событие {event:#X} для HWND {hwnd}")

            # Получаем текст из поля ввода (используя WM_GETTEXT, т.к. мы знаем HWND)
            try:
                text = get_text_from_hwnd(target_edit_hwnd)
                print(f"[{current_time_evt}] >>> WinEventHook: Текст из поля '{text}'")
                cache = redis_sync_conn.get(f'st_1{text[0]}::')
                if cache:
                    print(f'[{current_time_evt}]' + cache.decode())

                text_validation_queue.put(text)
            except Exception as e:
                print(f"[{current_time_evt}] >>> WinEventHook: Ошибка получения текста: {e}")


# --- Функции установки/удаления WinEventHook ---
def install_win_event_hook():
    global win_event_hook, CMPFUNC_EVENT, target_pid, target_thread_id
    if not target_edit_hwnd or not target_pid:  # target_thread_id может быть 0
        print("Не установлены target_edit_hwnd или target_pid. Невозможно установить WinEventHook.")
        return False

    CMPFUNC_EVENT = WINEVENTPROC(win_event_proc)

    # Пробуем сначала EVENT_OBJECT_VALUECHANGE
    # win_event_hook = user32.SetWinEventHook(
    #     EVENT_OBJECT_VALUECHANGE, EVENT_OBJECT_VALUECHANGE,  # minEvent, maxEvent
    #     0,  # hmodWinEventProc (0 для out-of-context)
    #     CMPFUNC_EVENT,  # lpfnWinEventProc
    #     target_pid,  # idProcess
    #     0,  # idThread (0 для всех потоков процесса)
    #     WINEVENT_OUTOFCONTEXT
    # )

    # Если VALUECHANGE не сработал, попробуем NAMECHANGE
    print(
        f"Не удалось установить WinEventHook для EVENT_OBJECT_VALUECHANGE (ошибка: {ctypes.GetLastError()}). Пробуем EVENT_OBJECT_NAMECHANGE...")
    win_event_hook = user32.SetWinEventHook(
        EVENT_OBJECT_NAMECHANGE, EVENT_OBJECT_NAMECHANGE, 0, CMPFUNC_EVENT,
        target_pid, 0, WINEVENT_OUTOFCONTEXT
    )

    if not win_event_hook:
        print(f"Не удалось установить WinEventHook (ошибка: {ctypes.GetLastError()})")
        return False

    print(f"WinEventHook установлен (ID: {win_event_hook}). События: VALUECHANGE и/или NAMECHANGE.")
    return True


def remove_win_event_hook():
    global win_event_hook
    if win_event_hook:
        if user32.UnhookWinEvent(win_event_hook):
            print("WinEventHook удален.")
        else:
            print(f"Ошибка при удалении WinEventHook: {ctypes.GetLastError()}")
        win_event_hook = None


# --- Вспомогательные функции для UI (get_button, enable_window) ---
# ... (без изменений, как в предыдущих версиях) ...
MB_OK = 0x00000000
MB_ICONWARNING = 0x00000030


def get_hwnd_of_button(parent_hwnd, button_text):
    def normalize_text(text):
        return text.replace('&', '').strip()

    target = normalize_text(button_text)
    found_hwnds = []

    def enum_child(hwnd, lParam):
        if win32gui.GetClassName(hwnd).lower() == "button":
            if normalize_text(win32gui.GetWindowText(hwnd)) == target:
                found_hwnds.append(hwnd);
                return False
        return True

    win32gui.EnumChildWindows(parent_hwnd, enum_child, None)
    return found_hwnds[0] if found_hwnds else None


def enable_window_and_refresh(hwnd_button, enable=True):
    win32gui.EnableWindow(hwnd_button, enable)
    parent_dialog_hwnd = win32gui.GetParent(hwnd_button)
    if parent_dialog_hwnd:
        user32.InvalidateRect(parent_dialog_hwnd, None, True)
        user32.UpdateWindow(parent_dialog_hwnd)
    else:  # маловероятно для кнопки в диалоге
        user32.InvalidateRect(hwnd_button, None, True)
        user32.UpdateWindow(hwnd_button)

def main():
    global block_enter  # find_window_hwnd, target_edit_hwnd, target_pid, target_thread_id - устанавливаются в find_target_process_and_window

    find_next_button_hwnd_local = None  # Локальная переменная для HWND кнопки

    try:
        print("Поиск целевого окна и поля ввода...")
        if not find_target_process_and_window():
            print(
                f"Окно '{target_window_substr}' или поле Edit не найдено. Убедитесь, что Блокнот запущен и окно поиска открыто.")
            return
        print(f"Целевой диалог: {find_window_hwnd}, поле Edit: {target_edit_hwnd}, PID: {target_pid}")

        find_next_button_hwnd_local = get_hwnd_of_button(find_window_hwnd, "Найти далее")
        if not find_next_button_hwnd_local:
            print("Не найдена кнопка 'Найти далее'.");
            return

        def on_text_changed_validation(text_value):
            nonlocal find_next_button_hwnd_local
            global block_enter
            is_valid = len(text_value.strip()) > 0 and not any(char.isdigit() for char in text_value)
            # Получаем текущее время с микросекундами
            now = datetime.now()

            # Форматируем как часы:минуты:секунды:миллисекунды (три цифры миллисекунд, т.к. microsec / 1000)
            current_time = now.strftime("%H:%M:%S:%f")[:-3]  # Убираем последние три цифры микросекунд, т.е. оставляем миллисекунды
            if is_valid:
                if not win32gui.IsWindowEnabled(find_next_button_hwnd_local):
                    enable_window_and_refresh(find_next_button_hwnd_local, True)
                block_enter = False
                print(f"[{current_time}] Текст валиден: '{text_value}'. Кнопка и Enter разблокированы.")
            else:
                if win32gui.IsWindowEnabled(find_next_button_hwnd_local):
                    enable_window_and_refresh(find_next_button_hwnd_local, False)
                block_enter = True
                print(f"[{current_time}] Текст НЕ валиден: '{text_value}'. Кнопка и Enter заблокированы.")

        # Начальная инициализация UI
        # initial_text = get_text_from_hwnd(target_edit_hwnd)
        # on_text_changed_validation(initial_text if initial_text is not None else "")

        install_keyboard_hook()
        if not install_win_event_hook():
            print("Не удалось установить WinEventHook. Отслеживание изменений текста не будет работать этим методом.")
            # Можно решить, продолжать ли без этого или выйти
            # return

        print("Начинаем цикл обработки. Введите текст. Ctrl+C для выхода.")
        while True:
            pythoncom.PumpWaitingMessages()  # Для COM и других оконных сообщений нашего потока

            try:
                new_text = text_validation_queue.get_nowait()
                # Получаем текущее время с микросекундами
                now = datetime.now()
                # Форматируем как часы:минуты:сет.к. microsec / 1000)
                log_time = now.strftime("%H:%M:%S:%f")[:-3]  # Убираем последние три цифры микросекунд, т.е. оставляем миллисекунды

                print(f"[{log_time}] Получено из очереди (WinEvent): '{new_text}'. Вызов валидации...")
                on_text_changed_validation(new_text)
            except queue.Empty:
                pass
            time.sleep(0.05)

    except KeyboardInterrupt:
        print("Выход из программы по Ctrl+C.")
    except Exception as e:
        print(f"Критическая ошибка в main: {e}"); traceback.print_exc()
    finally:
        print("Начинаем процедуру очистки...")
        remove_win_event_hook()
        remove_keyboard_hook()
        print("Очистка завершена.")


if __name__ == "__main__":
    try:
        main()
    finally:
        print("Вызов CoUninitialize из __main__ finally.")
        pythoncom.CoUninitialize()