디바이스에서 미디어 가져오기

이 문서에서는 사용 가능한 미디어 원본 검색, 비디오, 사진 및 사이드카 파일과 같은 파일 가져오기, 원본 장치에서 가져온 파일 삭제 등 장치에서 미디어를 가져오는 방법에 대해 설명합니다.

참고 항목

이 문서의 코드는 MediaImport UWP 앱 샘플에서 조정되었습니다. 유니버설 Windows 앱 샘플 Git 리포지토리에서 이 샘플을 복제하거나 다운로드하여 컨텍스트에서 코드를 보거나 사용자 고유의 앱의 시작점으로 사용할 수 있습니다.

간단한 미디어 가져오기 UI 만들기

이 문서의 예제에서는 최소 UI를 사용하여 핵심 미디어 가져오기 시나리오를 사용하도록 설정합니다. 미디어 가져오기 앱에 대한 보다 강력한 UI를 만드는 방법을 보려면 MediaImport 샘플을 참조하세요. 다음 XAML은 다음 컨트롤을 사용하여 스택 패널을 만듭니다.

  • 미디어를 가져올 수 있는 원본 검색을 시작하는 단추입니다.
  • 찾은 미디어 가져오기 원본을 나열하고 선택할 ComboBox입니다.
  • 선택한 가져오기 원본의 미디어 항목을 표시하고 선택할 ListView 컨트롤입니다.
  • 선택한 원본에서 미디어 항목 가져오기를 시작하는 단추입니다.
  • 선택한 원본에서 가져온 항목 삭제를 시작하는 단추입니다.
  • 비동기 미디어 가져오기 작업을 취소하는 단추입니다.
<StackPanel Orientation="Vertical">
    <Button x:Name="findSourcesButton" Click="findSourcesButton_Click" Content="Find sources"/>
    <ComboBox x:Name="sourcesComboBox" SelectionChanged="sourcesComboBox_SelectionChanged"/>
    <ListView x:Name="fileListView" 
                    HorizontalAlignment="Left" Margin="182,260,0,171" 
                    Width="715" 
                    SelectionMode="None" 
                    BorderBrush="#FF858585"   
                    BorderThickness="1" 
                    ScrollViewer.VerticalScrollBarVisibility="Visible">
        <ListView.ItemTemplate>
            <DataTemplate>
                <Grid>
                    <Grid.ColumnDefinitions>
                        <ColumnDefinition Width="0.05*"/>
                        <ColumnDefinition Width="0.20*"/>
                        <ColumnDefinition Width="0.75*"/>
                    </Grid.ColumnDefinitions>
                    <CheckBox Grid.Column="0" IsChecked="{Binding ImportableItem.IsSelected, Mode=TwoWay}" />
                    <!-- Click="CheckBox_Click"/>-->
                    <Image Grid.Column="1" Source="{Binding Thumbnail}" Width="120" Height="120" Stretch="Uniform"/>
                    <TextBlock Grid.Column="2" Text="{Binding ImportableItem.Name}" VerticalAlignment="Center" Margin="10,0"/>
                </Grid>
            </DataTemplate>
        </ListView.ItemTemplate>
    </ListView>
    
    <Button x:Name="importButton" Click="importButton_Click" Content="Import"/>
    <Button x:Name="deleteButton" Click="deleteButton_Click" Content="Delete"/>
    <Button x:Name="cancelButton" Click="cancelButton_Click" Content="Cancel"/>
    <ProgressBar x:Name="progressBar" SmallChange="0.01" LargeChange="0.1" Maximum="1"/>
    
</StackPanel>

코드 비하인드 파일 설정

기본 프로젝트 템플릿에 아직 포함되지 않은 이 예제에서 사용하는 네임스페이스를 포함하도록 using 지시문을 추가합니다.

using Windows.Media.Import;
using System.Threading;
using Windows.UI.Core;
using System.Text;

미디어 가져오기 작업에 대한 작업 취소 설정

미디어 가져오기 작업은 시간이 오래 걸릴 수 있으므로 IAsyncOperationWithProgress를 사용하여 비동기적으로 수행됩니다. 사용자가 취소 단추를 클릭하면 진행 중인 작업을 취소하는 데 사용할 CancellationTokenSource형식의 클래스 멤버 변수를 선언합니다.

CancellationTokenSource cts;

취소 단추에 대한 처리기를 구현합니다. 이 문서의 뒷부분에 나와 있는 예제에서는 작업이 시작될 때 CancellationTokenSource를 초기화하고 작업이 완료되면 null로 설정합니다. 취소 단추 처리기에서 검사 토큰이 null인지 확인하고, 그렇지 않은 경우 Cancel을 호출하여 작업을 취소합니다.

private void cancelButton_Click(object sender, RoutedEventArgs e)
{
    if (cts != null)
    {
        cts.Cancel();
        System.Diagnostics.Debug.WriteLine("Operation canceled by the Cancel button.");
    }
}

데이터 바인딩 도우미 클래스

일반적인 미디어 가져오기 시나리오에서는 사용자에게 가져올 수 있는 미디어 항목 목록을 표시하고, 선택할 미디어 파일이 많을 수 있으며, 일반적으로 각 미디어 항목에 대한 썸네일을 표시하려고 합니다. 이러한 이유로 이 예제에서는 사용자가 목록을 스크롤할 때 3개의 도우미 클래스를 사용하여 ListView 컨트롤에 항목을 점진적으로 로드합니다.

  • IncrementalLoadingBase 클래스 - IList, ISupportIncrementalLoading, 및 INotifyCollectionChanged를 구현하여 기본 증분 로드 동작을 제공합니다.
  • GeneratorIncrementalLoadingClass 클래스 - 증분 로드 기본 클래스의 구현을 제공합니다.
  • ImportableItemWrapper 클래스 - 가져온 각 항목에 대해 축소판 그림 이미지에 바인딩 가능한 BitmapImage 속성을 추가하는 PhotoImportItem 클래스 주위의 씬 래퍼입니다.

이러한 클래스는 MediaImport 샘플에서 제공되며 수정 없이 프로젝트에 추가할 수 있습니다. 프로젝트에 도우미 클래스를 추가한 후 이 예제의 뒷부분에서 사용할 GeneratorIncrementalLoadingClass 형식의 클래스 멤버 변수를 선언합니다.

GeneratorIncrementalLoadingClass<ImportableItemWrapper> itemsToImport = null;

미디어를 가져올 수 있는 사용 가능한 소스 찾기

원본 찾기 단추에 대한 클릭 처리기에서 정적 메서드 PhotoImportManager.FindAllSourcesAsync를 호출하여 미디어를 가져올 수 있는 디바이스에 대한 시스템 검색을 시작합니다. 작업이 완료되기를 기다리면 반환된 목록의 각 PhotoImportSource 개체를 반복하고 ComboBox에 항목을 추가하여 사용자가 선택할 때 쉽게 검색할 수 있도록 Tag 속성을 원본 개체 자체로 설정합니다.

private async void findSourcesButton_Click(object sender, RoutedEventArgs e)
{
    var sources = await PhotoImportManager.FindAllSourcesAsync();
    foreach (PhotoImportSource source in sources)
    {
        ComboBoxItem item = new ComboBoxItem();
        item.Content = source.DisplayName;
        item.Tag = source;
        sourcesComboBox.Items.Add(item);
    }
}

사용자가 선택한 가져오기 소스를 저장할 클래스 멤버 변수를 선언합니다.

PhotoImportSource importSource;

가져오기 원본 ComboBox에 대한 SelectionChanged 처리기에서 클래스 멤버 변수를 선택한 원본으로 설정한 다음, 이 문서의 뒷부분에 나와 있는 FindItems 도우미 메서드를 호출합니다.

private void sourcesComboBox_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
    this.importSource = (PhotoImportSource)((ComboBoxItem)sourcesComboBox.SelectedItem).Tag;
    FindItems();
}

가져올 항목 찾기

다음 단계에서 사용할 PhotoImportSessionPhotoImportFindItemsResult 형식의 클래스 멤버 변수를 추가합니다.

PhotoImportSession importSession;
PhotoImportFindItemsResult itemsResult;

필요한 경우 찾기 작업을 취소하는 데 사용할 수 있도록 FindItems 메서드에서 CancellationTokenSource 변수를 초기화합니다. try 블록 내에서 사용자가 선택한 PhotoImportSource 개체에서 CreateImportSession을 호출하여 새 가져오기 세션을 만듭니다. 찾기 작업의 진행률을 표시하는 콜백을 제공하는 새 Progress 개체를 만듭니다. 그런 다음, FindItemsAsync를 호출하여 찾기 작업을 시작합니다. PhotoImportContentTypeFilter 값을 제공하여 사진, 비디오 또는 둘 다를 반환할지 여부를 지정합니다. PhotoImportItemSelectionMode 값을 제공하여 IsSelected 속성이 true로 설정된 상태에서 모든 미디어 항목, 없음 또는 새 미디어 항목만 반환되는지 여부를 지정합니다. 이 속성은 ListBox 항목 템플릿의 각 미디어 항목에 대한 확인 상자에 바인딩됩니다.

FindItemsAsyncIAsyncOperationWithProgress를 반환합니다. 확장 메서드 AsTask는 대기할 수 있고 취소 토큰으로 취소할 수 있으며 제공된 Progress 개체를 사용하여 진행 상황을 보고하는 작업을 만드는 데 사용됩니다.

다음으로 데이터 바인딩 도우미 클래스인 GeneratorIncrementalLoadingClass가 초기화됩니다. FindItemsAsync가 대기 중에서 반환되면 PhotoImportFindItemsResult 개체를 반환합니다. 이 개체에는 작업의 성공 및 발견된 다양한 유형의 미디어 항목 수를 포함하여 찾기 작업에 대한 상태 정보가 포함됩니다. FoundItems 속성에는 찾은 미디어 항목을 나타내는 PhotoImportItem 개체 목록이 포함됩니다. GeneratorIncrementalLoadingClass 생성자는 증분 방식으로 로드될 항목의 총 개수와 필요에 따라 로드할 새 항목을 생성하는 함수를 인수로 사용합니다. 이 경우 제공된 람다 식은 PhotoImportItem을 래핑하고 각 항목에 대한 썸네일을 포함하는 ImportableItemWrapper의 새 인스턴스를 만듭니다. 증분 로드 클래스가 초기화되면 UI에서 ListView 컨트롤의 ItemsSource 속성으로 설정합니다. 이제 찾은 미디어 항목이 증분 방식으로 로드되고 목록에 표시됩니다.

다음으로 찾기 작업의 상태 정보가 출력됩니다. 일반적인 앱은 UI에서 사용자에게 이 정보를 표시하지만, 이 예제에서는 단순히 디버그 콘솔에 정보를 출력합니다. 마지막으로 작업이 완료되었기 때문에 취소 토큰을 null로 설정합니다.

private async void FindItems()
{
    this.cts = new CancellationTokenSource();

    try
    {
        this.importSession = this.importSource.CreateImportSession();

        // Progress handler for FindItemsAsync
        var progress = new Progress<uint>((result) =>
        {
            System.Diagnostics.Debug.WriteLine(String.Format("Found {0} Files", result.ToString()));
        });

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

        // GeneratorIncrementalLoadingClass is used to incrementally load items in the Listview view including thumbnails
        this.itemsToImport = new GeneratorIncrementalLoadingClass<ImportableItemWrapper>(this.itemsResult.TotalCount,
        (int index) =>
        {
            return new ImportableItemWrapper(this.itemsResult.FoundItems[index]);
        });

        // Set the items source for the ListView control
        this.fileListView.ItemsSource = this.itemsToImport;

        // Log the find results
        if (this.itemsResult != null)
        {
            var findResultProperties = new System.Text.StringBuilder();
            findResultProperties.AppendLine(String.Format("Photos\t\t\t :  {0} \t\t Selected Photos\t\t:  {1}", itemsResult.PhotosCount, itemsResult.SelectedPhotosCount));
            findResultProperties.AppendLine(String.Format("Videos\t\t\t :  {0} \t\t Selected Videos\t\t:  {1}", itemsResult.VideosCount, itemsResult.SelectedVideosCount));
            findResultProperties.AppendLine(String.Format("SideCars\t\t :  {0} \t\t Selected Sidecars\t:  {1}", itemsResult.SidecarsCount, itemsResult.SelectedSidecarsCount));
            findResultProperties.AppendLine(String.Format("Siblings\t\t\t :  {0} \t\t Selected Sibilings\t:  {1} ", itemsResult.SiblingsCount, itemsResult.SelectedSiblingsCount));
            findResultProperties.AppendLine(String.Format("Total Items Items\t :  {0} \t\t Selected TotalCount \t:  {1}", itemsResult.TotalCount, itemsResult.SelectedTotalCount));
            System.Diagnostics.Debug.WriteLine(findResultProperties.ToString());
        }

        if (this.itemsResult.HasSucceeded)
        {
            // Update UI to indicate success
            System.Diagnostics.Debug.WriteLine("FindItemsAsync succeeded.");
        }
        else
        {
            // Update UI to indicate that the operation did not complete
            System.Diagnostics.Debug.WriteLine("FindItemsAsync did not succeed or was not completed.");
        }
    }
    catch (Exception ex)
    {
        System.Diagnostics.Debug.WriteLine("Photo import find items operation failed. " + ex.Message);
    }


    this.cts = null;
}

미디어 항목 가져오기

가져오기 작업을 구현하기 전에 PhotoImportImportItemsResult 개체를 선언하여 가져오기 작업의 결과를 저장합니다. 나중에 원본에서 성공적으로 가져온 미디어 항목을 삭제하는 데 사용됩니다.

private PhotoImportImportItemsResult importedResult;

미디어 가져오기 작업을 시작하기 전에 CancellationTokenSource 변수를 초기화하고 ProgressBar 컨트롤의 값을 0으로 설정합니다.

ListView 컨트롤에 선택한 항목이 없으면 가져올 항목이 없습니다. 그렇지 않으면 Progress 개체를 초기화하여 진행률 표시줄 컨트롤의 값을 업데이트하는 진행률 콜백을 제공합니다. 찾기 작업에서 반환된 PhotoImportFindItemsResultItemImported 이벤트에 대한 처리기를 등록합니다. 이 이벤트는 항목을 가져올 때마다 발생하며, 이 예제에서는 가져온 각 파일의 이름을 디버그 콘솔에 출력합니다.

ImportItemsAsync를 호출하여 가져오기 작업을 시작합니다. 찾기 작업 과 마찬가지로 AsTask 확장 메서드는 반환된 작업을 대기할 수 있고 진행 상황을 보고하며 취소할 수 있는 작업으로 변환하는 데 사용됩니다.

가져오기 작업이 완료되면 ImportItemsAsync에서 반환된 PhotoImportImportItemsResult개체에서 상태 작업을 가져올 수 있습니다. 다음 예제에서는 상태 정보를 디버그 콘솔에 출력한 다음 마지막으로 취소 토큰을 null로 설정합니다.

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

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

            this.itemsResult.ItemImported += async (s, a) =>
            {
                await Dispatcher.RunAsync(CoreDispatcherPriority.Normal, () =>
                {
                    System.Diagnostics.Debug.WriteLine(String.Format("Imported: {0}", a.ImportedItem.Name));
                });
            };

            // import items from the our list of selected items
            this.importedResult = await this.itemsResult.ImportItemsAsync().AsTask(cts.Token, progress);

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

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

            if (!this.importedResult.HasSucceeded)
            {
                System.Diagnostics.Debug.WriteLine("ImportItemsAsync did not succeed or was not completed");
            }
        }
    }
    catch (Exception ex)
    {
        System.Diagnostics.Debug.WriteLine("Files could not be imported. " + "Exception: " + ex.ToString());
    }

    cts = null;
}

가져온 항목 삭제

성공적으로 가져온 항목을 가져온 소스에서 삭제하려면 먼저 삭제 작업을 취소할 수 있도록 취소 토큰을 초기화하고 진행률 표시줄 값을 0으로 설정합니다. ImportItemsAsync에서 반환된 PhotoImportImportItemsResult가 null이 아닌지 확인합니다. 그렇지 않은 경우 다시 한 번 Progress 개체를 만들어 삭제 작업에 대한 진행률 콜백을 제공합니다. DeleteImportedItemsFromSourceAsync를 호출하여 가져온 항목 삭제를 시작합니다. 진행률 및 취소 기능을 사용하여 결과를 대기 가능한 작업으로 변환하기 위한 AsTask입니다. 대기한 후 반환된 PhotoImportDeleteImportedItemsFromSourceResult 개체를 사용하여 삭제 작업에 대한 상태 정보를 가져와 표시할 수 있습니다.


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

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

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

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

            if (!deleteResult.HasSucceeded)
            {
                System.Diagnostics.Debug.WriteLine("Delete operation did not succeed or was not completed");
            }
        }

    }
    catch (Exception ex)
    {
        System.Diagnostics.Debug.WriteLine("Files could not be Deleted." + "Exception: " + ex.ToString());
    }

    // set the CancellationTokenSource to null when the work is complete.
    cts = null;


}