Files
AX-Copilot-Codex/src/AxCopilot/Services/FileDialogWatcher.cs

109 lines
3.9 KiB
C#

using System.Runtime.InteropServices;
using System.Text;
using System.Windows.Threading;
namespace AxCopilot.Services;
/// <summary>
/// Windows 열기/저장 대화상자(#32770)를 감지하여 이벤트를 발생시킵니다.
/// SetWinEventHook으로 HWND 생성/소멸을 모니터링합니다.
/// </summary>
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;
/// <summary>열기/저장 대화상자가 감지되면 발생합니다. IntPtr = 대화상자 HWND.</summary>
public event EventHandler<IntPtr>? 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();
}
}