Giải pháp xử lý sự cố mất tiêu điểm chuột và bàn phím khi nhúng Unity3D vào ứng dụng WPF

Một cách phổ biến để tích hợp nội dung 3D vào ứng dụng desktop hiện nay là chạy một tiến trình độc lập của Unity3D từ ứng dụng WPF bằng cách sử dụng lớp Process. Khi WPF khởi động file thực thi (.exe) Unity, nó sẽ nhúng cửa sổ 3D vào một vùng giao diện nhất định. Tuy nhiên, sau thao tác tương tác trên giao diện WPF (ví dụ: nhấn nút), tiêu điểm đầu vào (input focus) thường bị giữ lại bởi WPF, dẫn đến việc Unity không nhận được sự kiện chuột hoặc bàn phím.

Vấn đề xảy ra do hệ điều hành chỉ gửi các sự kiện đầu vào đến cửa sổ đang hoạt động (active window). Nếu cửa sổ Unity không được kích hoạt đúng lúc, dù vẫn hiển thị, nó sẽ không phản hồi các thao tác người dùng. Giải pháp là theo dõi vị trí con trỏ chuột và tự động kích hoạt hoặc vô hiệu hóa cửa sổ Unity dựa trên vị trí hiện tại của chuột.

Khởi tạo tiến trình Unity3D trong WPF

Đầu tiên, tiến trình Unity được khởi động với tham số truyền handle của panel chứa, để Unity có thể nhúng vào đúng vị trí:

private Process _unityProcess;
private IntPtr _unityWindowHandle = IntPtr.Zero;

internal void LaunchUnityApplication(string exePath)
{
    var containerHandle = ContainerPanel.Handle;
    
    this.Dispatcher.InvokeAsync(() =>
    {
        try
        {
            // Kiểm tra và đóng tiến trình cũ nếu còn tồn tại
            TerminateExistingProcess(exePath);

            _unityProcess = new Process();
            _unityProcess.StartInfo.FileName = exePath;
            _unityProcess.StartInfo.WorkingDirectory = Path.GetDirectoryName(exePath);
            _unityProcess.StartInfo.Arguments = $"-parentHWND {containerHandle.ToInt64()} -width={ContainerPanel.Width} -height={ContainerPanel.Height}";
            _unityProcess.StartInfo.UseShellExecute = true;
            _unityProcess.StartInfo.CreateNoWindow = true;

            _unityProcess.Start();
            _unityProcess.WaitForInputIdle();

            // Liệt kê các cửa sổ con để lấy handle chính của Unity
            EnumChildWindows(containerHandle, (hWnd, lParam) =>
            {
                _unityWindowHandle = hWnd;
                return false; // Dừng sau khi tìm thấy cửa sổ con đầu tiên
            }, IntPtr.Zero);
        }
        catch (Exception ex)
        {
            System.Diagnostics.Debug.WriteLine($"Lỗi khởi động Unity: {ex.Message}");
        }
    });
}

Theo dõi vị trí chuột để quản lý tiêu điểm

Để đảm bảo Unity nhận được sự kiện đầu vào khi người dùng di chuyển chuột vào vùng hiển thị, ta cần thiết lập một bộ định thời (DispatcherTimer) kiểm tra vị trí con trỏ liên tục và gửi tin nhắn kích hoạt tới cửa sổ Unity khi cần.

[DllImport("user32.dll")]
static extern bool GetCursorPos(out POINT point);

[DllImport("user32.dll")]
static extern IntPtr WindowFromPoint(POINT point);

[DllImport("user32.dll")]
static extern IntPtr SendMessage(IntPtr hWnd, uint msg, IntPtr wParam, IntPtr lParam);

private DispatcherTimer _focusTimer;
private IntPtr _lastWindowHandle = IntPtr.Zero;

private const uint WM_ACTIVATE = 0x0006;
private const IntPtr WA_ACTIVE = (IntPtr)1;
private const IntPtr WA_INACTIVE = (IntPtr)0;

struct POINT
{
    public int X;
    public int Y;
    public POINT(int x, int y) { X = x; Y = y; }
}

private void StartFocusMonitoring()
{
    _focusTimer = new DispatcherTimer();
    _focusTimer.Interval = TimeSpan.FromMilliseconds(250);
    _focusTimer.Tick += OnMousePositionChanged;
    _focusTimer.Start();
}

private void OnMousePositionChanged(object sender, EventArgs e)
{
    try
    {
        if (_unityWindowHandle == IntPtr.Zero) return;

        if (GetCursorPos(out POINT currentPoint))
        {
            var windowAtPoint = WindowFromPoint(currentPoint);

            // Tránh gửi tin nhắn lặp lại nếu vẫn ở cùng cửa sổ
            if (windowAtPoint != _lastWindowHandle)
            {
                if (windowAtPoint == _unityWindowHandle)
                {
                    SendMessage(_unityWindowHandle, WM_ACTIVATE, WA_ACTIVE, IntPtr.Zero);
                }
                else if (_lastWindowHandle == _unityWindowHandle)
                {
                    SendMessage(_unityWindowHandle, WM_ACTIVATE, WA_INACTIVE, IntPtr.Zero);
                }

                _lastWindowHandle = windowAtPoint;
            }
        }
    }
    catch (Exception ex)
    {
        System.Diagnostics.Debug.WriteLine($"Lỗi theo dõi tiêu điểm: {ex.Message}");
    }
}

Sau khi khởi động tiến trình Unity, gọi StartFocusMonitoring() để bắt đầu theo dõi. Hàm này sẽ kiểm tra mỗi 250ms vị trí con trỏ chuột, xác định cửa sổ nào nằm dưới con trỏ, và gửi thông điệp WM_ACTIVATE phù hợp để kích hoạt hoặc vô hiệu hóa cửa sổ Unity.

Nhờ cơ chế này, người dùng có thể tương tác liền mạch giữa giao diện WPF và nội dung 3D mà không gặp hiện tượng "mất phản hồi" từ Unity sau khi thao tác trên các điều khiển WPF.

Thẻ: WPF unity3d inter-process communication user32.dll window activation

Đăng vào ngày 18 tháng 6 lúc 17:34