Compartilhar via


Interações de foco e acompanhamento ocular em aplicativos do Windows

Herói de rastreamento ocular

Forneça suporte para rastrear o olhar, a atenção e a presença de um usuário com base na localização e no movimento de seus olhos.

Observação

Para entrada de foco em Windows Mixed Reality, consulte [Gaze]/windows/mixed-reality/mrtk-unity/features/input/gaze).

APIs importantes: Windows.Devices.Input.Preview, GazeDevicePreview, GazePointPreview, GazeInputSourcePreview

Visão geral

A entrada do olhar é uma maneira poderosa de interagir e usar aplicativos do Windows que é especialmente útil como uma tecnologia assistiva para usuários com doenças neuromusculares (como ELA) e outras deficiências que envolvem funções musculares ou nervosas prejudicadas.

Além disso, a entrada do olhar oferece oportunidades igualmente atraentes para jogos (incluindo aquisição e rastreamento de alvos) e aplicativos de produtividade tradicionais, quiosques e outros cenários interativos em que os dispositivos de entrada tradicionais (teclado, mouse, toque) não estão disponíveis ou onde pode ser útil/útil liberar as mãos do usuário para outras tarefas (como segurar sacolas de compras).

Observação

O suporte para hardware de rastreamento ocular foi introduzido no Windows 10 Fall Creators Update junto com o controle ocular, um recurso interno que permite usar seus olhos para controlar o ponteiro na tela, digitar com o teclado na tela e se comunicar com pessoas usando conversão de texto em fala. Um conjunto de APIs do Tempo de Execução do Windows (Windows.Devices.Input.Preview) para criar aplicativos que podem interagir com o hardware de acompanhamento ocular está disponível com a Atualização de abril de 2018 do Windows 10 (versão 1803, build 17134) e mais recente.

Privacidade

Devido aos dados pessoais potencialmente confidenciais coletados por dispositivos de rastreamento ocular, você deve declarar a gazeInput funcionalidade no manifesto do aplicativo do seu aplicativo (consulte a seção Configuração a seguir). Quando declarado, o Windows solicita automaticamente aos usuários uma caixa de diálogo de consentimento (quando o aplicativo é executado pela primeira vez), em que o usuário deve conceder permissão para que o aplicativo se comunique com o dispositivo de rastreamento ocular e acesse esses dados.

Além disso, se seu aplicativo coletar, armazenar ou transferir dados de acompanhamento ocular, você deverá descrever isso na política de privacidade do aplicativo e seguir todos os outros requisitos para Informações Pessoais no Contrato de Desenvolvedor de Aplicativos e nas Políticas da Microsoft Store.

Instalação

Para usar as APIs de entrada de foco em seu aplicativo do Windows, você precisará:

  • Especifique a gazeInput funcionalidade no manifesto do aplicativo.

    Abra o arquivo Package.appxmanifest com o designer de manifesto do Visual Studio ou adicione a funcionalidade manualmente selecionando Exibir código e inserindo o Capabilities seguinte DeviceCapability no nó:

    <Capabilities>
       <DeviceCapability Name="gazeInput" />
    </Capabilities>
    
  • Um dispositivo de rastreamento ocular compatível com Windows conectado ao seu sistema (integrado ou periférico) e ligado.

    Consulte Introdução ao controle ocular no Windows 10 para obter uma lista de dispositivos de rastreamento ocular com suporte.

Rastreamento ocular básico

Neste exemplo, demonstramos como acompanhar o olhar do usuário em um aplicativo do Windows e usar uma função de tempo com teste de clique básico para indicar o quão bem eles podem manter o foco do olhar em um elemento específico.

Uma pequena elipse é usada para mostrar onde o ponto de foco está dentro do visor do aplicativo, enquanto um RadialProgressBar do Windows Community Toolkit é colocado aleatoriamente na tela. Quando o foco do foco é detectado na barra de progresso, um cronômetro é iniciado e a barra de progresso é realocada aleatoriamente na tela quando a barra de progresso atinge 100%.

Rastreamento de olhar com amostra de temporizador

Rastreamento de olhar com amostra de temporizador

Baixe este exemplo do Exemplo de entrada do Foco (básico)

  1. Primeiro, configuramos a interface do usuário (MainPage.xaml).

    <Page
        x:Class="gazeinput.MainPage"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:local="using:gazeinput"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:controls="using:Microsoft.Toolkit.Uwp.UI.Controls"    
        mc:Ignorable="d">
    
        <Grid Background="{ThemeResource ApplicationPageBackgroundThemeBrush}">
            <Grid x:Name="containerGrid">
                <Grid.RowDefinitions>
                    <RowDefinition Height="Auto"/>
                    <RowDefinition Height="*"/>
                </Grid.RowDefinitions>
                <StackPanel x:Name="HeaderPanel" 
                        Orientation="Horizontal" 
                        Grid.Row="0">
                    <StackPanel.Transitions>
                        <TransitionCollection>
                            <AddDeleteThemeTransition/>
                        </TransitionCollection>
                    </StackPanel.Transitions>
                    <TextBlock x:Name="Header" 
                           Text="Gaze tracking sample" 
                           Style="{ThemeResource HeaderTextBlockStyle}" 
                           Margin="10,0,0,0" />
                    <TextBlock x:Name="TrackerCounterLabel"
                           VerticalAlignment="Center"                 
                           Style="{ThemeResource BodyTextBlockStyle}"
                           Text="Number of trackers: " 
                           Margin="50,0,0,0"/>
                    <TextBlock x:Name="TrackerCounter"
                           VerticalAlignment="Center"                 
                           Style="{ThemeResource BodyTextBlockStyle}"
                           Text="0" 
                           Margin="10,0,0,0"/>
                    <TextBlock x:Name="TrackerStateLabel"
                           VerticalAlignment="Center"                 
                           Style="{ThemeResource BodyTextBlockStyle}"
                           Text="State: " 
                           Margin="50,0,0,0"/>
                    <TextBlock x:Name="TrackerState"
                           VerticalAlignment="Center"                 
                           Style="{ThemeResource BodyTextBlockStyle}"
                           Text="n/a" 
                           Margin="10,0,0,0"/>
                </StackPanel>
                <Canvas x:Name="gazePositionCanvas" Grid.Row="1">
                    <controls:RadialProgressBar
                        x:Name="GazeRadialProgressBar" 
                        Value="0"
                        Foreground="Blue" 
                        Background="White"
                        Thickness="4"
                        Minimum="0"
                        Maximum="100"
                        Width="100"
                        Height="100"
                        Outline="Gray"
                        Visibility="Collapsed"/>
                    <Ellipse 
                        x:Name="eyeGazePositionEllipse"
                        Width="20" Height="20"
                        Fill="Blue" 
                        Opacity="0.5" 
                        Visibility="Collapsed">
                    </Ellipse>
                </Canvas>
            </Grid>
        </Grid>
    </Page>
    
  2. Em seguida, inicializamos nosso aplicativo.

    Neste snippet, declaramos nossos objetos globais e substituímos o evento de página OnNavigatedTo para iniciar nosso observador de dispositivo de foco e o evento de página OnNavigatedFrom para interromper nosso observador de dispositivo de foco.

    using System;
    using Windows.Devices.Input.Preview;
    using Windows.UI.Xaml.Controls;
    using Windows.UI.Xaml;
    using Windows.Foundation;
    using System.Collections.Generic;
    using Windows.UI.Xaml.Media;
    using Windows.UI.Xaml.Navigation;
    
    namespace gazeinput
    {
        public sealed partial class MainPage : Page
        {
            /// <summary>
            /// Reference to the user's eyes and head as detected
            /// by the eye-tracking device.
            /// </summary>
            private GazeInputSourcePreview gazeInputSource;
    
            /// <summary>
            /// Dynamic store of eye-tracking devices.
            /// </summary>
            /// <remarks>
            /// Receives event notifications when a device is added, removed, 
            /// or updated after the initial enumeration.
            /// </remarks>
            private GazeDeviceWatcherPreview gazeDeviceWatcher;
    
            /// <summary>
            /// Eye-tracking device counter.
            /// </summary>
            private int deviceCounter = 0;
    
            /// <summary>
            /// Timer for gaze focus on RadialProgressBar.
            /// </summary>
            DispatcherTimer timerGaze = new DispatcherTimer();
    
            /// <summary>
            /// Tracker used to prevent gaze timer restarts.
            /// </summary>
            bool timerStarted = false;
    
            /// <summary>
            /// Initialize the app.
            /// </summary>
            public MainPage()
            {
                InitializeComponent();
            }
    
            /// <summary>
            /// Override of OnNavigatedTo page event starts GazeDeviceWatcher.
            /// </summary>
            /// <param name="e">Event args for the NavigatedTo event</param>
            protected override void OnNavigatedTo(NavigationEventArgs e)
            {
                // Start listening for device events on navigation to eye-tracking page.
                StartGazeDeviceWatcher();
            }
    
            /// <summary>
            /// Override of OnNavigatedFrom page event stops GazeDeviceWatcher.
            /// </summary>
            /// <param name="e">Event args for the NavigatedFrom event</param>
            protected override void OnNavigatedFrom(NavigationEventArgs e)
            {
                // Stop listening for device events on navigation from eye-tracking page.
                StopGazeDeviceWatcher();
            }
        }
    }
    
  3. Em seguida, adicionamos nossos métodos de observador de dispositivo de foco.

    No StartGazeDeviceWatcher, chamamos CreateWatcher e declaramos os ouvintes de eventos do dispositivo de rastreamento ocular (DeviceAdded, DeviceUpdated e DeviceRemoved).

    Em DeviceAdded, verificamos o estado do dispositivo de rastreamento ocular. Se for um dispositivo viável, aumentamos nossa contagem de dispositivos e habilitamos o rastreamento do olhar. Consulte a próxima etapa para obter detalhes.

    No DeviceUpdated, também ativamos o rastreamento do olhar, pois esse evento é acionado se um dispositivo for recalibrado.

    No DeviceRemoved, diminuímos nosso contador de dispositivos e removemos os manipuladores de eventos do dispositivo.

    No StopGazeDeviceWatcher, desligamos o observador do dispositivo de foco.

    /// <summary>
    /// Start gaze watcher and declare watcher event handlers.
    /// </summary>
    private void StartGazeDeviceWatcher()
    {
        if (gazeDeviceWatcher == null)
        {
            gazeDeviceWatcher = GazeInputSourcePreview.CreateWatcher();
            gazeDeviceWatcher.Added += this.DeviceAdded;
            gazeDeviceWatcher.Updated += this.DeviceUpdated;
            gazeDeviceWatcher.Removed += this.DeviceRemoved;
            gazeDeviceWatcher.Start();
        }
    }

    /// <summary>
    /// Shut down gaze watcher and stop listening for events.
    /// </summary>
    private void StopGazeDeviceWatcher()
    {
        if (gazeDeviceWatcher != null)
        {
            gazeDeviceWatcher.Stop();
            gazeDeviceWatcher.Added -= this.DeviceAdded;
            gazeDeviceWatcher.Updated -= this.DeviceUpdated;
            gazeDeviceWatcher.Removed -= this.DeviceRemoved;
            gazeDeviceWatcher = null;
        }
    }

    /// <summary>
    /// Eye-tracking device connected (added, or available when watcher is initialized).
    /// </summary>
    /// <param name="sender">Source of the device added event</param>
    /// <param name="e">Event args for the device added event</param>
    private void DeviceAdded(GazeDeviceWatcherPreview source, 
        GazeDeviceWatcherAddedPreviewEventArgs args)
    {
        if (IsSupportedDevice(args.Device))
        {
            deviceCounter++;
            TrackerCounter.Text = deviceCounter.ToString();
        }
        // Set up gaze tracking.
        TryEnableGazeTrackingAsync(args.Device);
    }

    /// <summary>
    /// Initial device state might be uncalibrated, 
    /// but device was subsequently calibrated.
    /// </summary>
    /// <param name="sender">Source of the device updated event</param>
    /// <param name="e">Event args for the device updated event</param>
    private void DeviceUpdated(GazeDeviceWatcherPreview source,
        GazeDeviceWatcherUpdatedPreviewEventArgs args)
    {
        // Set up gaze tracking.
        TryEnableGazeTrackingAsync(args.Device);
    }

    /// <summary>
    /// Handles disconnection of eye-tracking devices.
    /// </summary>
    /// <param name="sender">Source of the device removed event</param>
    /// <param name="e">Event args for the device removed event</param>
    private void DeviceRemoved(GazeDeviceWatcherPreview source,
        GazeDeviceWatcherRemovedPreviewEventArgs args)
    {
        // Decrement gaze device counter and remove event handlers.
        if (IsSupportedDevice(args.Device))
        {
            deviceCounter--;
            TrackerCounter.Text = deviceCounter.ToString();

            if (deviceCounter == 0)
            {
                gazeInputSource.GazeEntered -= this.GazeEntered;
                gazeInputSource.GazeMoved -= this.GazeMoved;
                gazeInputSource.GazeExited -= this.GazeExited;
            }
        }
    }
  1. Aqui, verificamos se o dispositivo é viável e IsSupportedDevice , em caso afirmativo, tentamos ativar o rastreamento do olhar em TryEnableGazeTrackingAsync.

    No TryEnableGazeTrackingAsync, declaramos os manipuladores de eventos de foco e chamamos GazeInputSourcePreview.GetForCurrentView() para obter uma referência à fonte de entrada (isso deve ser chamado no thread da interface do usuário, consulte Manter o thread da interface do usuário responsivo).

    Observação

    Você deve chamar GazeInputSourcePreview.GetForCurrentView() somente quando um dispositivo de acompanhamento ocular compatível estiver conectado e exigido pelo seu aplicativo. Caso contrário, a caixa de diálogo de consentimento será desnecessária.

    /// <summary>
    /// Initialize gaze tracking.
    /// </summary>
    /// <param name="gazeDevice"></param>
    private async void TryEnableGazeTrackingAsync(GazeDevicePreview gazeDevice)
    {
        // If eye-tracking device is ready, declare event handlers and start tracking.
        if (IsSupportedDevice(gazeDevice))
        {
            timerGaze.Interval = new TimeSpan(0, 0, 0, 0, 20);
            timerGaze.Tick += TimerGaze_Tick;

            SetGazeTargetLocation();

            // This must be called from the UI thread.
            gazeInputSource = GazeInputSourcePreview.GetForCurrentView();

            gazeInputSource.GazeEntered += GazeEntered;
            gazeInputSource.GazeMoved += GazeMoved;
            gazeInputSource.GazeExited += GazeExited;
        }
        // Notify if device calibration required.
        else if (gazeDevice.ConfigurationState ==
                    GazeDeviceConfigurationStatePreview.UserCalibrationNeeded ||
                    gazeDevice.ConfigurationState ==
                    GazeDeviceConfigurationStatePreview.ScreenSetupNeeded)
        {
            // Device isn't calibrated, so invoke the calibration handler.
            System.Diagnostics.Debug.WriteLine(
                "Your device needs to calibrate. Please wait for it to finish.");
            await gazeDevice.RequestCalibrationAsync();
        }
        // Notify if device calibration underway.
        else if (gazeDevice.ConfigurationState == 
            GazeDeviceConfigurationStatePreview.Configuring)
        {
            // Device is currently undergoing calibration.  
            // A device update is sent when calibration complete.
            System.Diagnostics.Debug.WriteLine(
                "Your device is being configured. Please wait for it to finish"); 
        }
        // Device is not viable.
        else if (gazeDevice.ConfigurationState == GazeDeviceConfigurationStatePreview.Unknown)
        {
            // Notify if device is in unknown state.  
            // Reconfigure/recalbirate the device.  
            System.Diagnostics.Debug.WriteLine(
                "Your device is not ready. Please set up your device or reconfigure it."); 
        }
    }

    /// <summary>
    /// Check if eye-tracking device is viable.
    /// </summary>
    /// <param name="gazeDevice">Reference to eye-tracking device.</param>
    /// <returns>True, if device is viable; otherwise, false.</returns>
    private bool IsSupportedDevice(GazeDevicePreview gazeDevice)
    {
        TrackerState.Text = gazeDevice.ConfigurationState.ToString();
        return (gazeDevice.CanTrackEyes &&
                    gazeDevice.ConfigurationState == 
                    GazeDeviceConfigurationStatePreview.Ready);
    }
  1. Em seguida, configuramos nossos manipuladores de eventos de foco.

    Exibimos e ocultamos a elipse de rastreamento do olhar em GazeEntered e GazeExited, respectivamente.

    No GazeMoved, movemos nossa elipse de rastreamento de foco com base no EyeGazePosition fornecido pelo CurrentPoint do GazeEnteredPreviewEventArgs. Também gerenciamos o temporizador de foco do foco no RadialProgressBar, que dispara o reposicionamento da barra de progresso. Consulte a próxima etapa para obter detalhes.

    /// <summary>
    /// GazeEntered handler.
    /// </summary>
    /// <param name="sender">Source of the gaze entered event</param>
    /// <param name="e">Event args for the gaze entered event</param>
    private void GazeEntered(
        GazeInputSourcePreview sender, 
        GazeEnteredPreviewEventArgs args)
    {
        // Show ellipse representing gaze point.
        eyeGazePositionEllipse.Visibility = Visibility.Visible;
    
        // Mark the event handled.
        args.Handled = true;
    }
    
    /// <summary>
    /// GazeExited handler.
    /// Call DisplayRequest.RequestRelease to conclude the 
    /// RequestActive called in GazeEntered.
    /// </summary>
    /// <param name="sender">Source of the gaze exited event</param>
    /// <param name="e">Event args for the gaze exited event</param>
    private void GazeExited(
        GazeInputSourcePreview sender, 
        GazeExitedPreviewEventArgs args)
    {
        // Hide gaze tracking ellipse.
        eyeGazePositionEllipse.Visibility = Visibility.Collapsed;
    
        // Mark the event handled.
        args.Handled = true;
    }
    
    /// <summary>
    /// GazeMoved handler translates the ellipse on the canvas to reflect gaze point.
    /// </summary>
    /// <param name="sender">Source of the gaze moved event</param>
    /// <param name="e">Event args for the gaze moved event</param>
    private void GazeMoved(GazeInputSourcePreview sender, GazeMovedPreviewEventArgs args)
    {
        // Update the position of the ellipse corresponding to gaze point.
        if (args.CurrentPoint.EyeGazePosition != null)
        {
            double gazePointX = args.CurrentPoint.EyeGazePosition.Value.X;
            double gazePointY = args.CurrentPoint.EyeGazePosition.Value.Y;
    
            double ellipseLeft = 
                gazePointX - 
                (eyeGazePositionEllipse.Width / 2.0f);
            double ellipseTop = 
                gazePointY - 
                (eyeGazePositionEllipse.Height / 2.0f) - 
                (int)Header.ActualHeight;
    
            // Translate transform for moving gaze ellipse.
            TranslateTransform translateEllipse = new TranslateTransform
            {
                X = ellipseLeft,
                Y = ellipseTop
            };
    
            eyeGazePositionEllipse.RenderTransform = translateEllipse;
    
            // The gaze point screen location.
            Point gazePoint = new Point(gazePointX, gazePointY);
    
            // Basic hit test to determine if gaze point is on progress bar.
            bool hitRadialProgressBar = 
                DoesElementContainPoint(
                    gazePoint, 
                    GazeRadialProgressBar.Name, 
                    GazeRadialProgressBar); 
    
            // Use progress bar thickness for visual feedback.
            if (hitRadialProgressBar)
            {
                GazeRadialProgressBar.Thickness = 10;
            }
            else
            {
                GazeRadialProgressBar.Thickness = 4;
            }
    
            // Mark the event handled.
            args.Handled = true;
        }
    }
    
  2. Por fim, aqui estão os métodos usados para gerenciar o temporizador de foco do olhar para este aplicativo.

    DoesElementContainPoint Verifica se o ponteiro do foco está sobre a barra de progresso. Nesse caso, ele inicia o temporizador de foco e incrementa a barra de progresso em cada tique do temporizador de foco.

    SetGazeTargetLocation Define a localização inicial da barra de progresso e, se a barra de progresso for concluída (dependendo do temporizador de foco do foco), move a barra de progresso para um local aleatório.

    /// <summary>
    /// Return whether the gaze point is over the progress bar.
    /// </summary>
    /// <param name="gazePoint">The gaze point screen location</param>
    /// <param name="elementName">The progress bar name</param>
    /// <param name="uiElement">The progress bar UI element</param>
    /// <returns></returns>
    private bool DoesElementContainPoint(
        Point gazePoint, string elementName, UIElement uiElement)
    {
        // Use entire visual tree of progress bar.
        IEnumerable<UIElement> elementStack = 
            VisualTreeHelper.FindElementsInHostCoordinates(gazePoint, uiElement, true);
        foreach (UIElement item in elementStack)
        {
            //Cast to FrameworkElement and get element name.
            if (item is FrameworkElement feItem)
            {
                if (feItem.Name.Equals(elementName))
                {
                    if (!timerStarted)
                    {
                        // Start gaze timer if gaze over element.
                        timerGaze.Start();
                        timerStarted = true;
                    }
                    return true;
                }
            }
        }
    
        // Stop gaze timer and reset progress bar if gaze leaves element.
        timerGaze.Stop();
        GazeRadialProgressBar.Value = 0;
        timerStarted = false;
        return false;
    }
    
    /// <summary>
    /// Tick handler for gaze focus timer.
    /// </summary>
    /// <param name="sender">Source of the gaze entered event</param>
    /// <param name="e">Event args for the gaze entered event</param>
    private void TimerGaze_Tick(object sender, object e)
    {
        // Increment progress bar.
        GazeRadialProgressBar.Value += 1;
    
        // If progress bar reaches maximum value, reset and relocate.
        if (GazeRadialProgressBar.Value == 100)
        {
            SetGazeTargetLocation();
        }
    }
    
    /// <summary>
    /// Set/reset the screen location of the progress bar.
    /// </summary>
    private void SetGazeTargetLocation()
    {
        // Ensure the gaze timer restarts on new progress bar location.
        timerGaze.Stop();
        timerStarted = false;
    
        // Get the bounding rectangle of the app window.
        Rect appBounds = Windows.UI.ViewManagement.ApplicationView.GetForCurrentView().VisibleBounds;
    
        // Translate transform for moving progress bar.
        TranslateTransform translateTarget = new TranslateTransform();
    
        // Calculate random location within gaze canvas.
            Random random = new Random();
            int randomX = 
                random.Next(
                    0, 
                    (int)appBounds.Width - (int)GazeRadialProgressBar.Width);
            int randomY = 
                random.Next(
                    0, 
                    (int)appBounds.Height - (int)GazeRadialProgressBar.Height - (int)Header.ActualHeight);
    
        translateTarget.X = randomX;
        translateTarget.Y = randomY;
    
        GazeRadialProgressBar.RenderTransform = translateTarget;
    
        // Show progress bar.
        GazeRadialProgressBar.Visibility = Visibility.Visible;
        GazeRadialProgressBar.Value = 0;
    }
    

Confira também

Recursos

Amostras de tópico