[Windows 10] Experiencias multipantalla utilizando ProjectionManager

Introducción

Entre el conjunto de posibilidades nuevas
disponibles con Windows 10, sin duda alguna, hay una brilla fuertemente
sobre las demas, Continuum. Esta característica permite
conectar un teléfono a un monitor externo permitiendo interactuar con
la App en modo escritorio mientras podemos continuar utilizando el
teléfono.

Continuum
Continuum

Es
vital utilizar los nuevos AdaptiveTriggers, RelativePanel además de
controlar el modo de interacción y otros detalles para adaptar la
interfaz y usabilidad a cada posible situación. De esta forma
conseguimos aplicaciones adaptativas pudiendo ofrecer la experiencia
idónea en cada familia de dispositivo soportado.

En Continuum
podemos tener una única App en la pantalla secundaria de forma
simultánea. Sin embargo, podemos crear experiencias con múltiples
pantallas. ¿Os imagináis ver listado de restaurantes cercanos en el
teléfono mientras que en pantalla grande vemos mapa mostrando cercanía a
nuestra posisión y críticas?, ¿ tener detalles de una película y ver el
trailer de la misma en pantalla completa?. Escenarios donde sacar
partido de la proyección de información a una pantalla
secundaria hay muchos tanto en aplicaciones como en juegos. En este
artículo vamos a sacarle todo el partido a la clase ProjectionManager y el trabajo multipantalla.

ProjectionManager
ProjectionManager

Proyección de vistas

Crearemos un nuevo proyecto UAP:

Nueva App UAP
Nueva App UAP

Añadimos las carpetas Views, ViewModels y Services además de las clases base necesarias para implementar el patrón MVVM de la misma forma que vimos en este artículo.

El objetivo del artículo será proyectar una pantalla secundaria para aprender a:

  • Proyectar pantalla secundaria.
  • Detener la proyección.
  • Hacer un intercambio de la pantalla donde se proyecta.

Detectar pantalla secundaria

ProjectionManager
nos permite proyectar una ventana de nuestra App en una pantalla
secundaria. A nivel de desarrollo, el proceso es similar a trabajar con múltiples ventanas en la misma App. Para proyectar en otra pantalla lo primero que debemos verificar es si disponemos de esa pantalla.

En nuestro ejemplo, mostraremos en la interfaz si contamos o no con la pantalla donde proyectar:

<TextBlock 
     Text="{Binding IsProjectionDisplayAvailable}"/>

Usaremos una sencilla propiedad bool en la viewmodel:

private bool _isProjectionDisplayAvailable;

public bool IsProjectionDisplayAvailable
{
     get { return _isProjectionDisplayAvailable; }
     set
     {
          _isProjectionDisplayAvailable = value;
          RaisePropertyChanged();
     }
}

En la clase ProjectionManager contamos con el evento ProjectionDisplayAvailableChanged que se lanza cada vez que la pantalla secundaria sobre la que proyecta pasa a estar disponible o no disponible:

ProjectionManager.ProjectionDisplayAvailableChanged += ProjectionManager_ProjectionDisplayAvailableChanged;

También podemos realizar la verificación de si tenemos disponible la pantalla secundaris utilizando la propiedad ProjectionDisplayAvailable:

IsProjectionDisplayAvailable = ProjectionManager.ProjectionDisplayAvailable;

NOTA: Si no contamos con pantalla secundaria, la
vista proyectada se mostrará en la misma pantalla donde se encuentra la
vista principal.

Proyectar

Conocemos como verificar si contamos con pantalla secundaria sobre la que proyectar, veamos como realizar la proyección.

En nuestra interfaz tendremos un botón que nos permitirá proyectar una vista específica:

<Button
     Content="Project"
     Command="{Binding ProjectCommand}"/>

Nuestra interfaz principal:

Vista principal

Vista principal

El comando a ejecutar:

private ICommand _projectCommand;
 
public ICommand ProjectCommand
{
     get { return _projectCommand = _projectCommand ?? new DelegateCommandAsync(ProjectCommandExecute); }
}
 
public async Task ProjectCommandExecute()
{
     App.MainViewId = await _projectionService.ProjectAsync(typeof(ProjectionView));
}

Hemos creado un servicio ProjectionService en el que tenemos agrupada toda la lógica de proyección. Para proyectar utilizamos el siguiente método:

public async Task<int> ProjectAsync(Type viewType, DeviceInformation device = null)
{
     int mainViewId = ApplicationView.GetForCurrentView().Id;
     int? secondViewId = null;
 
     var view = CoreApplication.CreateNewView();
     await view.Dispatcher.RunAsync(CoreDispatcherPriority.Normal, () =>
     {
          secondViewId = ApplicationView.GetForCurrentView().Id;
          var rootFrame = new Frame();
          rootFrame.Navigate(viewType, null);
          Window.Current.Content = rootFrame;
          Window.Current.Activate();
     });
 
     if (secondViewId.HasValue)
     {
          if(device == null)
              await ProjectionManager.StartProjectingAsync(secondViewId.Value, mainViewId);
          else
              await ProjectionManager.StartProjectingAsync(secondViewId.Value, mainViewId, device);
     }
 
     return mainViewId;
}

Para realizar la proyección utilizamos el método StartProjectingAsync(Int32,Int32) al que le pasamos como parámetros:

  • ProjectionViewId: El  identificador de la ventana que se va a mostrar en la pantalla secundaria.
  • AnchorViewId: El identificador de la ventana original.

Comenzamos creando una nueva vista vacía en blanca. En esta vista
navegamos a la vista que deseamos proyectar y la asignamos como
contenido. Podemos pasar los parámetros necesarios en este punto.

NOTA: Es totalmente necesario realizar la llamada a Window.Current.Activate para que la vista pueda visualizarse.

La vista no aparecerá hasta lanzar el método StartProjectingAsync.
Tras lanzarlo, colocamos una vista existente en una pantalla
secundaria, en caso de detectar una. De lo contrario, la vista se sitúa
en el monitor principal.

Proyectar seleccionando la pantalla

Lo visto hasta este punto es sencillo y efectivo. Sin embargo,
podemos tener situaciones más complejas con múltiples pantallas
sencundarias.

¿Podemos elegir sobre que pantalla proyectar?

Si, podemos. Vamos a ver como realizar este proceso. Creamos en la
interfaz otro botón de modo que al ser pulsado nos muestre todas las
pantallas disponibles. Una vez seleccionada una pantalla específica
proyectaríamos sobre la misma:

<Button
     Content="Select Target and Project"
     Command="{Binding SelectTargetCommand}"/>
<ListView
     ItemsSource="{Binding Devices}"
     SelectedItem="{Binding SelectedDevice, Mode=TwoWay}"
     Height="300"
     Width="300"
     HorizontalAlignment="Left">
     <ListView.ItemTemplate>
          <DataTemplate>
               <TextBlock Text="{Binding Name}" />
          </DataTemplate>
     </ListView.ItemTemplate>
</ListView>

En la viewmodel:

private ICommand _selectTargetCommand;
 
public ICommand SelectTargetCommand
{
     get { return _selectTargetCommand = _selectTargetCommand ?? new DelegateCommandAsync(SelectTargetCommandExecute); }
}
 
public async Task SelectTargetCommandExecute()
{
     try
     {
          Devices = new ObservableCollection<DeviceInformation>(await _projectionService.GetProjectionDevices());
     }
     catch (Exception ex)
     {
          Debug.WriteLine(ex.Message);
     }
}

Utilizamos el siguiente método:

public async Task<IEnumerable<DeviceInformation>> GetProjectionDevices()
{
     // List wired/wireless displays
     String projectorSelectorQuery = ProjectionManager.GetDeviceSelector();
 
     // Use device API to find devices based on the query
     var projectionDevices = await DeviceInformation.FindAllAsync(projectorSelectorQuery);
 
     var devices = new ObservableCollection<DeviceInformation>();
     foreach (var device in projectionDevices)
          devices.Add(device);
 
     return devices;
}

Utilizamos el método GetDeviceSelector disponible en ProjectionManager
que nos devuelve una cadena con la enumeración de dispositivos
disponibles. Utilizamos la cadena para obtener una colección de
dispositivos (DeviceInformation) en los cuales tenemos toda la información necesaria.

La colección obtenida es la que bindeamos a nuestra interfaz. Una vez seleccionado un dispositivo concreto:

private async Task Project(DeviceInformation device)
{
     try
     {
          // Show the view on a second display (if available)
          App.MainViewId = await _projectionService.ProjectAsync(typeof(ProjectionView), device);
 
          Debug.WriteLine("Projection started in {0} successfully!", device.Name);
     }
     catch (Exception ex)
     {
          Debug.WriteLine(ex.Message);
     }
}

Utilizamos un método al que le pasamos el dispositivo y se encarga de
realizar la proyección. En este caso, en nuestro servicio utilizamos el
método StartProjectingAsync(Int32,Int32,DeviceInformation)
donde además de los identificadores de la nueva vista y de la original,
indicamos el dispositivo, es decir, la pantalla específica sobre la que
proyectar.

También podemos de forma muy sencilla permitir elegir el dispositivo sobre el que proyectar utilizando el método RequestStartProjectingAsync(Int32,Int32,Rect,Placement).
Utilizando este método se mostrará un flyout con el listado de
dispositivos, de modo que, una vez seleccionado uno, comenzamos la
proyección. Para indicar la posición del flyout podemos utilizar un
parámetro de tipo Rect.

public async Task<int> RequestProjectAsync(Type viewType, Rect? position = null)
{
     int mainViewId = ApplicationView.GetForCurrentView().Id;
     int? secondViewId = null;
 
     var view = CoreApplication.CreateNewView();
     await view.Dispatcher.RunAsync(CoreDispatcherPriority.Normal, () =>
     {
          secondViewId = ApplicationView.GetForCurrentView().Id;
          var rootFrame = new Frame();
          rootFrame.Navigate(viewType, mainViewId);
          Window.Current.Content = rootFrame;
          Window.Current.Activate();
     });
 
     if (secondViewId.HasValue)
     {
          var defaultPosition = new Rect(0.0, 0.0, 200.0, 200.0);
          await ProjectionManager.RequestStartProjectingAsync(secondViewId.Value, mainViewId, position.HasValue ? position.Value : defaultPosition);
     }
 
     return mainViewId;
}

La vista proyectada

¿Y que ocurre con la vista proyecta?. Nada en especial, puede ser
cualquier vista de la aplicación. Sin embargo, puede llegar a
interesarnos realizar algunas interacciones con la API de proyección
como:

  • Detener la proyección.
  • Modificar el dispositivo donde proyectamos.

La interfaz de usuario contará con dos botones, uno para detener la proyección y otro para modificar el dispositivo utilizado.

<Grid>
     <StackPanel>
          <TextBlock
              Text="Projection View"
              FontWeight="SemiBold"/>
          <Button
              Content="Swap"
              Command="{Binding SwitchViewCommand}"/>
          <Button
              Content="Stop"
              Command="{Binding StopCommand}"/>
     </StackPanel>
</Grid>

El resultado:

Vista proyectada

Vista proyectada

En la viewmodel:

private ICommand _switchViewCommand;
private ICommand _stopCommand;
 
public ICommand SwitchViewCommand
{
     get { return _switchViewCommand = _switchViewCommand ?? new DelegateCommandAsync(SwitchViewCommandExecute); }
}
 
public ICommand StopCommand
{
     get { return _stopCommand = _stopCommand ?? new DelegateCommandAsync(StopCommandExecute); }
}
 
public async Task SwitchViewCommandExecute()
{
     try
     {
          await _projectionService.SwitchProjection(App.MainViewId);
     }
     catch (Exception ex)
     {
          Debug.WriteLine(ex.Message);
     }
}
 
public async Task StopCommandExecute()
{
     try
     {
          await _projectionService.StopProjection(App.MainViewId);
     }
     catch (Exception ex)
     {
          Debug.WriteLine(ex.Message);
     }
}

Detener proyección

Para detener la proyección tenemos a nuestra disposición el método StopProjectingAsync que oculta la vista mostrada en proyector o pantalla secundaria.

public async Task StopProjection(int mainViewId)
{
     await ProjectionManager.StopProjectingAsync(       
                ApplicationView.GetForCurrentView().Id,
                mainViewId);
}

Cambiar pantalla

Podemos cambiar al vuelo la pantalla donde se realiza la proyección utilizando el método SwapDisplaysForViewsAsync de la clase ProjectionManager:

public async Task SwapProjection(int mainViewId)
{
     await ProjectionManager.SwapDisplaysForViewsAsync(
                ApplicationView.GetForCurrentView().Id,
                mainViewId);
}

Tenéis el código fuente disponible e GitHub:

Ver GitHub

Recordar que podéis dejar en los comentarios cualquier tipo de sugerencia o pregunta.

Más información

Deja un comentario

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *