import os
import sqlite3
import threading
import time
import tkinter as tk
from tkinter import ttk
import keyboard
import pywinauto
from pywinauto.keyboard import send_keys
DB_FILE = "caret_data.db"
class CaretReaderApp:
def __init__(self, root):
self.root = root
self.root.title("Fast Caret DB v1.3")
self.root.geometry("360x160")
self.root.attributes("-topmost", True)
self.root.resizable(False, False)
# 1. 성능 최적화: 메모리 캐시 딕셔너리 초기화
self.memory_cache = {}
# 2. 성능 최적화: 고성능 DB 초기화 및 커넥션 풀링 유지
self.db_conn = None
self.init_database()
# UI 요소 배치
style = ttk.Style()
style.theme_use("clam")
self.label_info = ttk.Label(root, text="[Ctrl + Shift + F12]를 누르면\n커서 위치 문자를 고속 조회합니다.", justify="center", font=("Malgun Gothic", 10))
self.label_info.pack(pady=10)
self.label_result = ttk.Label(root, text="읽은 문자: 없음", font=("Malgun Gothic", 11, "bold"), foreground="#555555")
self.label_result.pack(pady=2)
self.label_db_content = ttk.Label(root, text="DB 내용: 매칭 데이터 없음", font=("Malgun Gothic", 12, "bold"), foreground="#0078D7", wraplength=340, justify="center")
self.label_db_content.pack(pady=8)
# 단축키 리스너 등록 (데몬 스레드)
self.hotkey_thread = threading.Thread(target=self.setup_hotkey, daemon=True)
self.hotkey_thread.start()
def init_database(self):
"""SQLite 고속 읽기 튜닝 및 인덱스 설정"""
is_new = not os.path.exists(DB_FILE)
# 상시 연결 유지 (매 요청마다 open/close를 하지 않음)
self.db_conn = sqlite3.connect(DB_FILE, check_same_thread=False)
cursor = self.db_conn.cursor()
# 성능 향상을 위한 SQLite PRAGMA 튜닝 설정
cursor.execute("PRAGMA journal_mode=WAL;") # Write-Ahead Logging 모드로 읽기 성능 향상
cursor.execute("PRAGMA synchronous=NORMAL;") # 디스크 동기화 오버헤드 감소
cursor.execute("PRAGMA cache_size=-2000;") # 2MB 캐시 지정
# 테이블 생성
cursor.execute("""
CREATE TABLE IF NOT EXISTS dict_data (
character TEXT PRIMARY KEY,
content TEXT
)
""")
# 고속 조회를 위한 인덱스 생성
cursor.execute("CREATE INDEX IF NOT EXISTS idx_char ON dict_data(character);")
if is_new:
samples = [
('A', '알파벳의 첫 번째 글자 (Apple, Ace)'),
('B', '알파벳의 두 번째 글자 (Banana, Best)'),
('C', '알파벳의 세 번째 글자 (Cherry, Cool)'),
('한', '대한민국의 국호이자 한글의 첫 글자'),
('1', '숫자의 시작, 최초 혹은 최고를 의미'),
('?', '질문이나 의문을 나타내는 문장 부호')
]
cursor.executemany("INSERT OR IGNORE INTO dict_data (character, content) VALUES (?, ?)", samples)
self.db_conn.commit()
print("[성공] SQLite 고속 데이터베이스 인프라 튜닝 및 캐시 활성화 완료.")
def setup_hotkey(self):
keyboard.add_hotkey("ctrl+shift+f12", self.trigger_caret_reading)
keyboard.wait()
def trigger_caret_reading(self):
# 무거운 작업 분리
threading.Thread(target=self.read_caret_pipeline, daemon=True).start()
def read_caret_pipeline(self):
# 1단계: 고성능 UI Automation 백엔드 시도
character = self.try_uia_backend()
# 2단계: 실패 시 고속 폴백 매크로 시도
if not character:
character = self.try_cursor_right_fast_fallback()
if character and character.strip() != "":
cleaned_char = character.strip()
# 3단계: 고속 조회 패턴 (메모리 캐시 확인 -> DB 파일 확인)
db_content = self.query_database_fast(cleaned_char)
display_char = cleaned_char.replace("\n", "\\n").replace("\r", "\\r")
self.root.after(0, lambda: self.update_ui(f"읽은 문자: '{display_char}'", db_content))
else:
self.root.after(0, lambda: self.update_ui("읽은 문자: 추출 실패", "매칭 데이터 없음"))
def query_database_fast(self, char):
"""메모리 캐시 ➔ 상시 개방된 DB 인덱스 스캔을 통한 Ultra-fast 조회"""
# 1단계: 가장 빠른 메모리 딕셔너리 캐시 탐색
if char in self.memory_cache:
return self.memory_cache[char]
# 2단계: 캐시 미스 시, 상시 열려있는 고성능 DB 인덱스 조회
try:
cursor = self.db_conn.cursor()
cursor.execute("SELECT content FROM dict_data WHERE character = ?", (char,))
row = cursor.fetchone()
if row:
res = f"DB 내용: {row[0]}"
else:
res = "DB 내용: 등록되지 않은 문자입니다."
# 결과를 다음 조회를 위해 메모리 캐시에 적재
self.memory_cache[char] = res
return res
except Exception as e:
return f"DB 조회 에러: {e}"
def update_ui(self, result_text, db_text):
self.label_result.config(text=result_text)
self.label_db_content.config(text=db_text)
if "등록되지 않은" in db_text or "없음" in db_text:
self.label_db_content.config(foreground="#E81123")
else:
self.label_db_content.config(foreground="#0078D7")
def try_uia_backend(self):
try:
app = pywinauto.Application(backend="uia").connect(active_only=True)
focused_control = app.top_window().set_focus()
text_pattern = focused_control.get_text_block()
selection = text_pattern.get_selection()
if selection:
caret_range = selection
caret_range.expand_to_enclosing_unit("Character")
return caret_range.get_text()
except Exception:
pass
return None
def try_cursor_right_fast_fallback(self):
"""딜레이 오버헤드를 극한으로 줄인 고속 매크로"""
try:
import win32clipboard
original_text = ""
try:
win32clipboard.OpenClipboard()
if win32clipboard.IsClipboardFormatAvailable(win32clipboard.CF_UNICODETEXT):
original_text = win32clipboard.GetClipboardData(win32clipboard.CF_UNICODETEXT)
win32clipboard.CloseClipboard()
except Exception:
pass
win32clipboard.OpenClipboard()
win32clipboard.EmptyClipboard()
win32clipboard.CloseClipboard()
# 성능 최적화: OS 타겟 윈도우 부하 타이밍 최소화 (기존 0.06초 -> 0.02초)
send_keys("+{RIGHT}")
time.sleep(0.02)
send_keys("^c")
time.sleep(0.02)
target_char = None
win32clipboard.OpenClipboard()
try:
if win32clipboard.IsClipboardFormatAvailable(win32clipboard.CF_UNICODETEXT):
target_char = win32clipboard.GetClipboardData(win32clipboard.CF_UNICODETEXT)
except Exception:
pass
finally:
win32clipboard.CloseClipboard()
send_keys("{LEFT}")
if original_text:
try:
win32clipboard.OpenClipboard()
win32clipboard.EmptyClipboard()
win32clipboard.SetClipboardText(original_text, win32clipboard.CF_UNICODETEXT)
win32clipboard.CloseClipboard()
except Exception:
pass
return target_char
except Exception:
pass
return None
def __del__(self):
# 프로그램 종료 시 안전하게 DB 세션 닫기
if self.db_conn:
self.db_conn.close()
if __name__ == "__main__":
root = tk.Tk()
app = CaretReaderApp(root)
root.mainloop()