using System.Runtime.InteropServices; using System.Text; using System.Windows.Threading; namespace AxCopilot.Services; /// /// Windows 열기/저장 대화상자(#32770)를 감지하여 이벤트를 발생시킵니다. /// SetWinEventHook으로 HWND 생성/소멸을 모니터링합니다. /// public class FileDialogWatcher : IDisposable { // ─── P/Invoke ──────────────────────────────────────────────────────────── private delegate void WinEventDelegate(IntPtr hWinEventHook, uint eventType, IntPtr hwnd, int idObject, int idChild, uint idEventThread, uint dwmsEventTime); [DllImport("user32.dll")] private static extern IntPtr SetWinEventHook(uint eventMin, uint eventMax, IntPtr hmodWinEventProc, WinEventDelegate lpfnWinEventProc, uint idProcess, uint idThread, uint dwFlags); [DllImport("user32.dll")] private static extern bool UnhookWinEvent(IntPtr hWinEventHook); [DllImport("user32.dll", CharSet = CharSet.Unicode)] private static extern int GetClassName(IntPtr hWnd, StringBuilder lpClassName, int nMaxCount); [DllImport("user32.dll", CharSet = CharSet.Unicode)] private static extern int GetWindowText(IntPtr hWnd, StringBuilder lpString, int nMaxCount); [DllImport("user32.dll")] private static extern bool IsWindowVisible(IntPtr hWnd); private const uint EVENT_OBJECT_SHOW = 0x8002; private const uint EVENT_OBJECT_CREATE = 0x8000; private const uint WINEVENT_OUTOFCONTEXT = 0x0000; private const uint WINEVENT_SKIPOWNPROCESS = 0x0002; // ─── 상태 ──────────────────────────────────────────────────────────────── private IntPtr _hook; private WinEventDelegate? _delegate; // prevent GC private bool _disposed; private readonly Dispatcher _dispatcher; /// 열기/저장 대화상자가 감지되면 발생합니다. IntPtr = 대화상자 HWND. public event EventHandler? FileDialogOpened; public FileDialogWatcher() { _dispatcher = Dispatcher.CurrentDispatcher; } public void Start() { if (_hook != IntPtr.Zero) return; _delegate = OnWinEvent; _hook = SetWinEventHook( EVENT_OBJECT_SHOW, EVENT_OBJECT_SHOW, IntPtr.Zero, _delegate, 0, 0, WINEVENT_OUTOFCONTEXT | WINEVENT_SKIPOWNPROCESS); } public void Stop() { if (_hook != IntPtr.Zero) { UnhookWinEvent(_hook); _hook = IntPtr.Zero; } } private void OnWinEvent(IntPtr hWinEventHook, uint eventType, IntPtr hwnd, int idObject, int idChild, uint idEventThread, uint dwmsEventTime) { if (hwnd == IntPtr.Zero || idObject != 0) return; if (!IsWindowVisible(hwnd)) return; // 클래스명 #32770 = 공통 대화상자 var sb = new StringBuilder(256); GetClassName(hwnd, sb, 256); var className = sb.ToString(); if (className != "#32770") return; // 창 제목으로 열기/저장 대화상자인지 확인 var titleSb = new StringBuilder(256); GetWindowText(hwnd, titleSb, 256); var title = titleSb.ToString(); if (title.Contains("열기") || title.Contains("Open") || title.Contains("저장") || title.Contains("Save") || title.Contains("다른 이름") || title.Contains("Browse") || title.Contains("폴더") || title.Contains("Folder")) { _dispatcher.BeginInvoke(() => FileDialogOpened?.Invoke(this, hwnd)); } } public void Dispose() { if (_disposed) return; _disposed = true; Stop(); } }