Importar mídia de um dispositivo

Este artigo descreve como importar mídia de um dispositivo, incluindo pesquisar fontes de mídia disponíveis, importar arquivos como vídeos, fotos e arquivos sidecar e excluir os arquivos importados do dispositivo de origem.

Note

O código neste artigo foi adaptado do exemplo de aplicativo UWP MediaImport UWP. Você pode clonar ou baixar este exemplo do repositório Git de exemplos de aplicativos Universal Windows para ver o código em contexto ou usá-lo como ponto de partida para o seu próprio aplicativo.

Criar uma interface do usuário de importação de mídia simples

O exemplo neste artigo usa uma interface do usuário mínima para habilitar os principais cenários de importação de mídia. Para ver como criar uma interface do usuário mais robusta para um aplicativo de importação de mídia, consulte o exemplo MediaImport. O XAML a seguir cria um StackPanel com os seguintes controles:

  • Um botão para iniciar a pesquisa de fontes das quais a mídia pode ser importada.
  • Um ComboBox para listar e selecionar entre as fontes de importação de mídia encontradas.
  • Um controle ListView para exibir e selecionar os itens de mídia da origem de importação selecionada.
  • Um botão para iniciar a importação de itens de mídia da origem selecionada.
  • Um botão para iniciar a exclusão dos itens que foram importados da origem selecionada.
  • Um botão para cancelar uma operação de importação de mídia assíncrona.
<ScrollViewer VerticalScrollBarVisibility="Auto">
    <StackPanel Padding="24" Spacing="16">
        <TextBlock
            Text="Import media from device"
            Style="{StaticResource SubtitleTextBlockStyle}" />

        <TextBlock
            Text="Choose a connected device source, review the files that can be imported, and then import or delete imported files."
            TextWrapping="WrapWholeWords" />

        <StackPanel Spacing="8">
            <TextBlock
                Text="Source"
                Style="{StaticResource BodyStrongTextBlockStyle}" />
            <Button
                x:Name="findSourcesButton"
                Content="Find sources"
                Click="findSourcesButton_Click" />
            <ComboBox
                x:Name="sourcesComboBox"
                PlaceholderText="Select a device source"
                SelectionChanged="sourcesComboBox_SelectionChanged" />
        </StackPanel>

        <StackPanel Spacing="8">
            <TextBlock
                Text="Importable items"
                Style="{StaticResource BodyStrongTextBlockStyle}" />
            <ListView
                x:Name="fileListView"
                MinHeight="320"
                MaxHeight="480"
                SelectionMode="None"
                BorderBrush="{ThemeResource CardStrokeColorDefaultBrush}"
                BorderThickness="1"
                ScrollViewer.VerticalScrollBarVisibility="Visible">
                <ListView.ItemTemplate>
                    <DataTemplate>
                        <Grid ColumnSpacing="12" Padding="0,8">
                            <Grid.ColumnDefinitions>
                                <ColumnDefinition Width="Auto" />
                                <ColumnDefinition Width="Auto" />
                                <ColumnDefinition Width="*" />
                            </Grid.ColumnDefinitions>

                            <CheckBox
                                Grid.Column="0"
                                VerticalAlignment="Center"
                                IsChecked="{Binding ImportableItem.IsSelected, Mode=TwoWay}" />

                            <Image
                                Grid.Column="1"
                                Width="120"
                                Height="120"
                                Source="{Binding Thumbnail}"
                                Stretch="UniformToFill" />

                            <TextBlock
                                Grid.Column="2"
                                VerticalAlignment="Center"
                                Text="{Binding ImportableItem.Name}"
                                TextWrapping="WrapWholeWords" />
                        </Grid>
                    </DataTemplate>
                </ListView.ItemTemplate>
            </ListView>
        </StackPanel>

        <StackPanel Spacing="8">
            <TextBlock
                Text="Import actions"
                Style="{StaticResource BodyStrongTextBlockStyle}" />
            <StackPanel Orientation="Horizontal" Spacing="12">
                <Button
                    x:Name="importButton"
                    Content="Import"
                    Click="importButton_Click" />
                <Button
                    x:Name="deleteButton"
                    Content="Delete imported"
                    Click="deleteButton_Click" />
                <Button
                    x:Name="cancelButton"
                    Content="Cancel"
                    Click="cancelButton_Click" />
            </StackPanel>
            <ProgressBar
                x:Name="progressBar"
                Minimum="0"
                Maximum="1"
                ShowError="False"
                ShowPaused="False" />
        </StackPanel>

        <TextBlock
            x:Name="statusTextBlock"
            Style="{StaticResource CaptionTextBlockStyle}"
            Foreground="{ThemeResource TextFillColorSecondaryBrush}"
            TextWrapping="WrapWholeWords" />
    </StackPanel>
</ScrollViewer>

Configurar o cancelamento de tarefas para operações de importação de mídia

Como as operações de importação de mídia podem levar muito tempo, elas são executadas de forma assíncrona usando IAsyncOperationWithProgress. Declare uma variável de membro de classe do tipo CancellationTokenSource que será usada para cancelar uma operação em andamento se o usuário clicar no botão cancelar.

CancellationTokenSource? cts;

Implemente um manipulador para o botão cancelar. Os exemplos mostrados posteriormente neste artigo inicializarão o CancellationTokenSource quando uma operação começar e defini-la como nula quando a operação for concluída. No manipulador de botão cancelar, verifique se o token é nulo e, caso contrário, chame Cancelar para cancelar a operação.

private void cancelButton_Click(object sender, RoutedEventArgs e)
{
    if (cts is not null)
    {
        cts.Cancel();
        SetStatus("Operation canceled by the Cancel button.");
    }
}

Classes auxiliares de associação de dados

Em um cenário típico de importação de mídia, você mostra ao usuário uma lista de itens de mídia disponíveis a serem importados. Pode haver um grande número de arquivos de mídia para escolher e, normalmente, você deseja mostrar uma miniatura para cada item de mídia. Por esse motivo, este exemplo usa três classes auxiliares para carregar entradas incrementalmente no controle ListView à medida que o usuário rola para baixo pela lista.

  • Classe IncrementalLoadingBase – Implementa iList, ISupportIncrementalLoading e INotifyCollectionChanged para fornecer o comportamento de carregamento incremental base.
  • Classe GeneratorIncrementalLoadingClass – Fornece uma implementação da classe base de carregamento incremental.
  • ImportableItemWrapper classe - Um wrapper leve para a classe PhotoImportItem para adicionar uma propriedade BitmapImage associável para a imagem em miniatura de cada item importado.

Essas classes são fornecidas no exemplo MediaImport e podem ser adicionadas ao projeto sem modificações. Depois de adicionar as classes auxiliares ao projeto, declare uma variável de membro de classe do tipo GeneratorIncrementalLoadingClass que será usada posteriormente neste exemplo.

GeneratorIncrementalLoadingClass<ImportableItemWrapper>? itemsToImport = null;

Localizar fontes disponíveis das quais a mídia pode ser importada

No manipulador de clique para o botão localizar fontes, chame o método estático PhotoImportManager.FindAllSourcesAsync para iniciar a pesquisa do sistema em busca de dispositivos dos quais a mídia pode ser importada. Depois de aguardar a conclusão da operação, faça loop por cada objeto PhotoImportSource na lista retornada e adicione uma entrada ao ComboBox, definindo a propriedade Tag para o próprio objeto de origem para que ele possa ser facilmente recuperado quando o usuário fizer uma seleção.

private async void findSourcesButton_Click(object sender, RoutedEventArgs e)
{
    var sources = await PhotoImportManager.FindAllSourcesAsync();
    sourcesComboBox.Items.Clear();

    foreach (PhotoImportSource source in sources)
    {
        ComboBoxItem item = new();
        item.Content = source.DisplayName;
        item.Tag = source;
        sourcesComboBox.Items.Add(item);
    }

    SetStatus($"Found {sources.Count} import source(s).");
}

Declare uma variável membro da classe para armazenar a origem de importação selecionada pelo usuário.

PhotoImportSource? importSource;

No manipulador SelectionChanged para o ComboBox de origem de importação, defina a variável de membro de classe como a origem selecionada e chame o método auxiliar FindItems , que será mostrado posteriormente neste artigo.

private void sourcesComboBox_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
    if (sourcesComboBox.SelectedItem is ComboBoxItem item && item.Tag is PhotoImportSource selectedSource)
    {
        importSource = selectedSource;
        FindItems();
    }
}

Localizar itens a serem importados

Adicione variáveis de membro de classe do tipo PhotoImportSession e PhotoImportFindItemsResult que serão usadas nas etapas a seguir.

PhotoImportSession? importSession;
PhotoImportFindItemsResult? itemsResult;

No método FindItems , inicialize a variável CancellationTokenSource para que ela possa ser usada para cancelar a operação de localização, se necessário. Em um bloco try, crie uma nova sessão de importação chamando CreateImportSession no objeto PhotoImportSource selecionado pelo usuário. Crie um novo objeto Progress para fornecer um retorno de chamada para exibir o progresso da operação de localização. Em seguida, chame FindItemsAsync para iniciar a operação de localização. Forneça um valor PhotoImportContentTypeFilter para especificar se fotos, vídeos ou ambos devem ser retornados. Forneça um valor PhotoImportItemSelectionMode para especificar se todos, nenhum ou apenas os novos itens de mídia são retornados com sua propriedade IsSelected definida como true. Essa propriedade está associada a uma caixa de seleção para cada item de mídia em nosso modelo de item ListView.

FindItemsAsync retorna um IAsyncOperationWithProgress. O método de extensão AsTask é usado para criar uma tarefa que pode ser aguardada, pode ser cancelado com o token de cancelamento e que relata o progresso usando o objeto Progress fornecido.

Em seguida, a classe auxiliar de associação de dados, GeneratorIncrementalLoadingClass é inicializada. FindItemsAsync, quando retorna de ser aguardado, retorna um objeto PhotoImportFindItemsResult. Este objeto contém informações de status sobre a operação de localização, incluindo o êxito da operação e a contagem de diferentes tipos de itens de mídia que foram encontrados. A propriedade FoundItems contém uma lista de objetos PhotoImportItem que representam os itens de mídia encontrados. O construtor GeneratorIncrementalLoadingClass usa como argumentos a contagem total de itens que serão carregados incrementalmente e uma função que gera novos itens a serem carregados conforme necessário. Nesse caso, a expressão lambda fornecida cria uma nova instância do ImportableItemWrapper que encapsula PhotoImportItem e inclui uma miniatura para cada item. Depois que a classe de carregamento incremental tiver sido inicializada, defina-a como a propriedade ItemsSource do controle ListView na interface do usuário. Agora, os itens de mídia encontrados serão carregados incrementalmente e exibidos na lista.

Por fim, defina o token de cancelamento como nulo porque a operação foi concluída.

private async void FindItems()
{
    if (importSource is null)
    {
        SetStatus("Select an import source first.");
        return;
    }

    cts = new CancellationTokenSource();

    try
    {
        importSession = importSource.CreateImportSession();

        var progress = new Progress<uint>((result) =>
        {
            SetStatus($"Found {result} file(s)...");
        });

        var currentItemsResult =
            await importSession.FindItemsAsync(PhotoImportContentTypeFilter.ImagesAndVideos, PhotoImportItemSelectionMode.SelectAll)
            .AsTask(cts.Token, progress);

        itemsResult = currentItemsResult;

        itemsToImport = new GeneratorIncrementalLoadingClass<ImportableItemWrapper>(currentItemsResult.TotalCount,
        (int index) =>
        {
            return new ImportableItemWrapper(currentItemsResult.FoundItems[index]);
        });

        fileListView.ItemsSource = itemsToImport;

        var findResultProperties = new StringBuilder();
        findResultProperties.AppendLine($"Photos\t\t\t :  {currentItemsResult.PhotosCount} \t\t Selected Photos\t\t:  {currentItemsResult.SelectedPhotosCount}");
        findResultProperties.AppendLine($"Videos\t\t\t :  {currentItemsResult.VideosCount} \t\t Selected Videos\t\t:  {currentItemsResult.SelectedVideosCount}");
        findResultProperties.AppendLine($"SideCars\t\t :  {currentItemsResult.SidecarsCount} \t\t Selected Sidecars\t:  {currentItemsResult.SelectedSidecarsCount}");
        findResultProperties.AppendLine($"Siblings\t\t\t :  {currentItemsResult.SiblingsCount} \t\t Selected Sibilings\t:  {currentItemsResult.SelectedSiblingsCount}");
        findResultProperties.AppendLine($"Total Items Items\t :  {currentItemsResult.TotalCount} \t\t Selected TotalCount \t:  {currentItemsResult.SelectedTotalCount}");
        System.Diagnostics.Debug.WriteLine(findResultProperties.ToString());

        if (currentItemsResult.HasSucceeded)
        {
            SetStatus("FindItemsAsync succeeded.");
        }
        else
        {
            SetStatus("FindItemsAsync did not succeed or was not completed.");
        }
    }
    catch (Exception ex)
    {
        SetStatus("Photo import find items operation failed. " + ex.Message);
    }

    cts = null;
}

Importar itens de mídia

Antes de implementar a operação de importação, declare um objeto PhotoImportImportItemsResult para armazenar os resultados da operação de importação. Isso será usado posteriormente para excluir itens de mídia que foram importados com êxito da origem.

private PhotoImportImportItemsResult? importedResult;

Antes de iniciar a operação de importação de mídia, inicialize a variável CancellationTokenSource e defina o valor do controle ProgressBar como 0.

Se não houver itens selecionados no controle ListView , não haverá nada a importar. Caso contrário, inicialize um objeto Progress para definir uma função de callback de progresso que atualiza o valor do controle de barra de progresso. Registre um manipulador para o evento ItemImported do PhotoImportFindItemsResult retornado pela operação de busca. Esse evento será gerado sempre que um item for importado e, neste exemplo, gerará o nome de cada arquivo importado para o console de depuração.

Chame ImportItemsAsync para iniciar a operação de importação. Assim como acontece com a operação de localização, o método de extensão AsTask é usado para converter a operação retornada em uma tarefa que pode ser aguardada, relata o progresso e pode ser cancelada.

Após a conclusão da operação de importação, o status da operação poderá ser obtido do objeto PhotoImportImportItemsResult retornado por ImportItemsAsync. Este exemplo gera as informações de status para o console de depuração e, por fim, define o token de cancelamento como nulo.

private async void importButton_Click(object sender, RoutedEventArgs e)
{
    if (itemsResult is null)
    {
        SetStatus("Find importable items before importing.");
        return;
    }

    cts = new CancellationTokenSource();
    progressBar.Value = 0;

    try
    {
        if (itemsResult.SelectedTotalCount <= 0)
        {
            SetStatus("Nothing Selected for Import.");
        }
        else
        {
            var progress = new Progress<PhotoImportProgress>((result) =>
            {
                progressBar.Value = result.ImportProgress;
            });

            itemsResult.ItemImported += (s, a) =>
            {
                DispatcherQueue.TryEnqueue(() =>
                {
                    System.Diagnostics.Debug.WriteLine($"Imported: {a.ImportedItem.Name}");
                });
            };

            importedResult = await itemsResult.ImportItemsAsync().AsTask(cts.Token, progress);

            if (importedResult is not null)
            {
                StringBuilder importedSummary = new();
                importedSummary.AppendLine($"Photos Imported   \t:  {importedResult.PhotosCount} ");
                importedSummary.AppendLine($"Videos Imported    \t:  {importedResult.VideosCount} ");
                importedSummary.AppendLine($"SideCars Imported   \t:  {importedResult.SidecarsCount} ");
                importedSummary.AppendLine($"Siblings Imported   \t:  {importedResult.SiblingsCount} ");
                importedSummary.AppendLine($"Total Items Imported \t:  {importedResult.TotalCount} ");
                importedSummary.AppendLine($"Total Bytes Imported \t:  {importedResult.TotalSizeInBytes} ");

                System.Diagnostics.Debug.WriteLine(importedSummary.ToString());
            }

            if (importedResult is null || !importedResult.HasSucceeded)
            {
                SetStatus("ImportItemsAsync did not succeed or was not completed");
            }
            else
            {
                SetStatus("Import completed.");
            }
        }
    }
    catch (Exception ex)
    {
        SetStatus("Files could not be imported. Exception: " + ex);
    }

    cts = null;
}

Excluir itens importados

Para excluir os itens importados com êxito da origem da qual foram importados, primeiro inicialize o token de cancelamento para que a operação de exclusão possa ser cancelada e defina o valor da barra de progresso como 0. Verifique se o PhotoImportImportItemsResult retornado de ImportItemsAsync não é nulo. Caso contrário, crie novamente um objeto Progress para fornecer um callback de progresso para a operação de exclusão. Chame DeleteImportedItemsFromSourceAsync para começar a excluir os itens importados. Use o AsTask para converter o resultado em uma tarefa aguardada com recursos de progresso e cancelamento. Após a espera, o objeto PhotoImportDeleteImportedItemsFromSourceResult retornado pode ser usado para obter e exibir informações de status sobre a operação de exclusão.

private async void deleteButton_Click(object sender, RoutedEventArgs e)
{
    cts = new CancellationTokenSource();
    progressBar.Value = 0;

    try
    {
        if (importedResult is null)
        {
            SetStatus("Nothing was imported for deletion.");
        }
        else
        {
            var progress = new Progress<double>((result) =>
            {
                progressBar.Value = result;
            });

            PhotoImportDeleteImportedItemsFromSourceResult? deleteResult = await importedResult.DeleteImportedItemsFromSourceAsync().AsTask(cts.Token, progress);

            if (deleteResult is not null)
            {
                StringBuilder deletedResults = new();
                deletedResults.AppendLine($"Total Photos Deleted:\t{deleteResult.PhotosCount} ");
                deletedResults.AppendLine($"Total Videos Deleted:\t{deleteResult.VideosCount} ");
                deletedResults.AppendLine($"Total Sidecars Deleted:\t{deleteResult.SidecarsCount} ");
                deletedResults.AppendLine($"Total Sibilings Deleted:\t{deleteResult.SiblingsCount} ");
                deletedResults.AppendLine($"Total Files Deleted:\t{deleteResult.TotalCount} ");
                deletedResults.AppendLine($"Total Bytes Deleted:\t{deleteResult.TotalSizeInBytes} ");
                System.Diagnostics.Debug.WriteLine(deletedResults.ToString());
            }

            if (deleteResult is null || !deleteResult.HasSucceeded)
            {
                SetStatus("Delete operation did not succeed or was not completed");
            }
            else
            {
                SetStatus("Delete operation completed.");
            }
        }
    }
    catch (Exception ex)
    {
        SetStatus("Files could not be deleted. Exception: " + ex);
    }

    cts = null;
}