Библиотека Pictures в WinRT

60

Приложение может напрямую обратиться к библиотеке Pictures (Мои рисунки) и получить список всех вложенных папок и файлов. При работе с содержимым библиотеки Pictures удобно вывести на экран список миниатюр, а затем обратиться к выбранным изображениям.

Эта возможность продемонстрирована в программе PhotoScatter. Программа создает в левой части страницы экземпляр ListBox со структурой папок библиотеки Pictures. Если выбрать папку, программа отображает ее содержимое в виде миниатюр. Пользователь может перемещать, масштабировать и вращать миниатюры; при этом программа загружает реальный файл, чтобы вывести его в увеличенном виде на полном разрешении.

На следующем рисунке показано, как выглядит приложение. Возможно, вы узнаете некоторые из 200 с лишним изображений, хранящихся в моей папке Screenshots:

Я хотел, чтобы с каждым объектом на экране можно было работать независимо от других и чтобы каждый объект обрабатывал свои операции. Для этого был создан производный от ContentControl класс с именем ManipulableContentControl. Этот элемент управления использует несколько усложненную версию класса ManipulationManager, представленную ранее:

using Windows.Foundation;
using Windows.UI.Input;
using Windows.UI.Xaml.Media;

namespace WinRTTestApp
{
    public class ManipulationManager
    {
        TransformGroup xformGroup;
        MatrixTransform matrixXform;
        CompositeTransform compositeTransform;

        public ManipulationManager() : this(new CompositeTransform())
        { }

        public ManipulationManager(CompositeTransform initialTransform)
        {
            xformGroup = new TransformGroup();
            matrixXform = new MatrixTransform();
            xformGroup.Children.Add(matrixXform);
            compositeTransform = initialTransform;
            xformGroup.Children.Add(compositeTransform);
            this.Matrix = xformGroup.Value;
        }

        public Matrix Matrix { private set; get; }

        public void AccumulateDelta(Point position, ManipulationDelta delta)
        {
            matrixXform.Matrix = xformGroup.Value;
            Point center = matrixXform.TransformPoint(position);
            compositeTransform.CenterX = center.X;
            compositeTransform.CenterY = center.Y;
            compositeTransform.TranslateX = delta.Translation.X;
            compositeTransform.TranslateY = delta.Translation.Y;
            compositeTransform.ScaleX = delta.Scale;
            compositeTransform.ScaleY = delta.Scale;
            compositeTransform.Rotation = delta.Rotation;
            this.Matrix = xformGroup.Value;
        }
    }
}

Единственным нововведением является конструктор, позволяющий инициализировать ориентацию элемента при помощи объекта CompositeTransform, который затем используется внутри класса.

Чтобы создать класс ManipulableContentControl, я создал в Visual Studio новый элемент управления типа User Control. В файле XAML и файле фонового кода я заменил UserControl на ContentControl. Обычно в классах, производных от UserControl, содержимое элемента управления определяется файлом XAML. В этом файле XAML содержимое не определяется, но свойству RenderTransform назначается объект MatrixTransform, который задается в файле фонового кода по экземпляру ManipulationManager:

<ContentControl ...>
    
    <ContentControl.RenderTransform>
        <MatrixTransform x:Name="matrixXform" />
    </ContentControl.RenderTransform>
</ContentControl>

Ниже приведен файл фонового кода. Обратите внимание на конструктор, который получает преобразование CompositeTransform, используемое для создания объекта ManipulationManager:

using Windows.UI.Xaml.Controls;
using Windows.UI.Xaml.Input;
using Windows.UI.Xaml.Media;

namespace WinRTTestApp
{
    public sealed partial class ManipulableContentControl : ContentControl
    {
        static int zIndex;
        ManipulationManager manipulationManager;

        public ManipulableContentControl(CompositeTransform initialTransform)
        {
            this.InitializeComponent();

            // Создание объекта ManipulationManager 
            // и задание MatrixTransform по этому объекту
            manipulationManager = new ManipulationManager(initialTransform);
            matrixXform.Matrix = manipulationManager.Matrix;

            this.ManipulationMode = ManipulationModes.All &
                                   ~ManipulationModes.TranslateRailsX &
                                   ~ManipulationModes.TranslateRailsY;
        }

        protected override void OnManipulationStarting(ManipulationStartingRoutedEventArgs e)
        {
            Canvas.SetZIndex(this, zIndex += 1);
            base.OnManipulationStarting(e);
        }

        protected override void OnManipulationDelta(ManipulationDeltaRoutedEventArgs e)
        {
            manipulationManager.AccumulateDelta(e.Position, e.Delta);
            matrixXform.Matrix = manipulationManager.Matrix;
            base.OnManipulationDelta(e);
        }
    }
}

Статическое свойство zIndex используется для выведения на передний план объекта, к которому прикоснулся пользователь, в начале манипуляции.

Обычно для отображения структуры каталогов используется элемент управления TreeView или его аналог, обеспечивающий расстановку отступов и интерфейс открытия/свертки узлов дерева. В Windows Runtime нет элемента TreeView (пока), поэтому я решил использовать обычный список ListBox. Открытие и свертка узлов не поддерживаются, но отступы есть:

using Windows.Storage;
using Windows.UI.Xaml.Controls;

namespace WinRTTestApp
{
    public class FolderItem
    {
        public int Level { set; get; }
        public StorageFolder StorageFolder { set; get; }
        public Grid DisplayGrid { set; get; }

        public string Indent
        {
            get { return new string('\x00A0', 4 * this.Level); }
        }
    }
}

Каждый объект FolderItem представляет папку. Имя папки берется из объекта StorageFolder, уровень вложенности задается кодом по свойству Level, а свойство Indent использует это значение для построения строки, содержащей четыре пробела для каждого уровня.

Класс FolderItem также определяет свойство DisplayGrid типа Grid. Этот объект Grid задается тогда, когда пользователь впервые выбирает эту конкретную папку, и заполняется набором объектов ManipulableContentControl, соответствующих изображениям в папке. Хранение этого свойства Grid вместе со всем содержимом предотвращает необходимость повторного перебора содержимого каждой папки в процессе перебора. (Однако программа не устанавливает файлового «сторожа», и если позднее в папку будут добавлены новые файлы, программа о них не узнает.)

Шаблон ItemTemplate для ListBox определяется в файле MainPage.xaml и содержит ссылки на свойства из FolderItem:

<Page ...>
    
    <Grid Background="{StaticResource ApplicationPageBackgroundThemeBrush}">
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="Auto" />
            <ColumnDefinition />
        </Grid.ColumnDefinitions>
        
        <ListBox Name="folderListBox"
                 SelectionChanged="OnFolderListBoxSelectionChanged">
            <ListBox.ItemTemplate>
                <DataTemplate>
                    <ContentControl FontSize="22">
                        <StackPanel Orientation="Horizontal">
                            <TextBlock Text="{Binding Indent}" />
                            <TextBlock Text="" 
                                       FontFamily="Segoe UI Symbol" />
                            <TextBlock Text="{Binding StorageFolder.Name}" />
                        </StackPanel>
                    </ContentControl>
                </DataTemplate>
            </ListBox.ItemTemplate>
        </ListBox>

        <Border Grid.Column="1" Name="displayBorder" />
    </Grid>
</Page>

В шрифте Segoe UI Symbol кодовой точке 0xE188 соответствует маленький значок папки. Ему предшествует строка отступа Indent, а после него идет свойство Name объекта StorageFolder в FolderItem.

Программе PhotoScatter необходимо предоставить доступ к библиотеке Pictures в разделе Capabilities файла Package.appxmanifest, потому что в ходе обработки события Loaded программа в процессе создания объектов FolderItem списка ListBox получает полное дерево каталогов рекурсивными вызовами метода GetFoldersAsync объекта StorageFolder.

using System;
using System.Collections.Generic;
using System.IO;
using System.Runtime.InteropServices.WindowsRuntime;
using System.Threading.Tasks;
using Windows.Graphics.Imaging;
using Windows.Storage;
using Windows.Storage.FileProperties;
using Windows.Storage.Streams;
using Windows.UI.Xaml;
using Windows.UI.Xaml.Controls;
using Windows.UI.Xaml.Input;
using Windows.UI.Xaml.Media;
using Windows.UI.Xaml.Media.Imaging;

namespace WinRTTestApp
{
    public sealed partial class MainPage : Page
    {
        public MainPage()
        {
            this.InitializeComponent();
            Loaded += OnMainPageLoaded;
        }

        private void OnMainPageLoaded(object sender, RoutedEventArgs e)
        {
            StorageFolder storageFolder = KnownFolders.PicturesLibrary;
            BuildFolderListBox(storageFolder, 0);
            folderListBox.SelectedIndex = 0;
        }

        private async void BuildFolderListBox(StorageFolder parentStorageFolder, int level)
        {
            FolderItem folderItem = new FolderItem
            {
                StorageFolder = parentStorageFolder,
                Level = level
            };
            folderListBox.Items.Add(folderItem);

            IReadOnlyList<StorageFolder> storageFolders = await parentStorageFolder.GetFoldersAsync();

            foreach (StorageFolder storageFolder in storageFolders)
                BuildFolderListBox(storageFolder, level + 1);
        }

        // ...
    }
}

Обработчик Loaded завершается обнулением свойства SelectedIndex объекта ListBox, что приводит к выделению первого варианта (то есть самой папки Pictures). При этом инициируется вызов обработчика SelectionChanged, который использует метод GetFilesAsync объекта StorageFolder для получения списка всех файлов в этой папке. Для каждого объекта StorageFile метод вызывает GetThumbnailAsync для получения уменьшенного изображения (загрузка миниатюр намного предпочтительнее загрузки полных изображений, которая может потребовать слишком больших затрат времени и памяти). Вызов метода LoadBitmapAsync объекта MainPage (который будет описан далее) создает элемент Image и ManipulableContentControl для отображения миниатюры:

// ...

namespace WinRTTestApp
{
    public sealed partial class MainPage : Page
    {
        // ...
    
        Random rand = new Random();

        private async void OnFolderListBoxSelectionChanged(object sender, SelectionChangedEventArgs e)
        {
            FolderItem folderItem = (sender as ListBox).SelectedItem as FolderItem;

            if (folderItem == null)
            {
                displayBorder.Child = null;
                return;
            }

            if (folderItem.DisplayGrid != null)
            {
                displayBorder.Child = folderItem.DisplayGrid;
                return;
            }

            Grid displayGrid = new Grid();
            folderItem.DisplayGrid = displayGrid;
            displayBorder.Child = displayGrid;

            StorageFolder storageFolder = folderItem.StorageFolder;
            IReadOnlyList<StorageFile> storageFiles = await storageFolder.GetFilesAsync();

            foreach (StorageFile storageFile in storageFiles)
            {
                StorageItemThumbnail thumbnail = 
                            await storageFile.GetThumbnailAsync(ThumbnailMode.SingleItem);
                BitmapSource bitmap = await LoadBitmapAsync(thumbnail);

                if (bitmap == null)
                    continue;

                // Создание нового элемента Image для вывода миниатюры
                Image image = new Image
                    {
                        Source = bitmap,
                        Stretch = Stretch.None,
                        Tag = ImageType.Thumbnail
                    };

                // Создание исходного преобразования CompositeTransform
                CompositeTransform xform = new CompositeTransform();
                xform.TranslateX = (displayBorder.ActualWidth - bitmap.PixelWidth) / 2;
                xform.TranslateY = (displayBorder.ActualHeight - bitmap.PixelHeight) / 2;
                xform.TranslateX += 256 * (0.5 - rand.NextDouble());
                xform.TranslateY += 256 * (0.5 - rand.NextDouble());

                // Создание объекта ManipulableContentControl для Image
                ManipulableContentControl manipulableControl = new ManipulableContentControl(xform)
                {
                    Content = image,
                    Tag = storageFile
                };
                manipulableControl.ManipulationStarted += OnManipulableControlManipulationStarted;

                // Размещение объекта на панели Grid
                displayGrid.Children.Add(manipulableControl);
            }
        }

        // ...
    }
}

Из-за присутствия операторов await в методах GetThumbnailAsync и LoadBitmapAsync объекты BitmapSource, элементы Image и экземпляры ManipulableContentControl создаются последовательно; каждый только что созданный объект выводится на экран. Пользователь видит, как из появляющихся изображений постепенно складывается большая, слегка хаотичная куча. Также можно вывести все изображения одновременно, но в этом случае количество создаваемых программных потоков превысит количество процессоров.

Обработчик SelectionChanged объекта ListBox выполняется только один раз для каждой папки. Свойству Tag объекта ManipulableContentControl задается объект StorageFile, связанный с каждым вариантом списка. Позднее он используется для загрузки фактического изображения (если потребуется). Также обратите внимание на то, что свойству Tag каждого элемента Image задается значение ImageType.Thumbnail. Это значение входит в следующее перечисление:

namespace WinRTTestApp
{
    public enum ImageType
    {
        Full,
        Thumbnail,
        Transitioning
    }
}

Свойство Tag изменяется, когда пользователь начинает операцию с конкретным объектом. Хотя класс ManipulableContentControl обрабатывает события Manipulation, необходимые для перемещения, масштабирования и вращения элементов, обработчик SelectionChanged также назначает обработчик события ManipulationStarted. Этот обработчик отвечает за замену миниатюры полным изображением:

// ...

namespace WinRTTestApp
{
    public sealed partial class MainPage : Page
    {
        // ...

        private async void OnManipulableControlManipulationStarted(object sender, 
                                                           ManipulationStartedRoutedEventArgs e)
        {
            ManipulableContentControl manipulableControl = sender as ManipulableContentControl;
            Image image = manipulableControl.Content as Image;

            if ((ImageType)image.Tag == ImageType.Thumbnail)
            {
                // Свойству Tag задается переходное значение Transitioning
                image.Tag = ImageType.Transitioning;

                // Загрузка полного растрового файла
                StorageFile storageFile = manipulableControl.Tag as StorageFile;
                BitmapSource newBitmap = await LoadBitmapAsync(storageFile);

                // Для файлов, которые не может обработать BitmapDecoder
                if (newBitmap != null)
                {
                    // Получение миниатюры из элемента Image
                    BitmapSource oldBitmap = image.Source as BitmapSource;

                    // Определение преобразования ScaleTransform 
                    // между старым и новым изображением
                    double scale = 1;

                    if (oldBitmap.PixelWidth > oldBitmap.PixelHeight)
                        scale = (double)oldBitmap.PixelWidth / newBitmap.PixelWidth;
                    else
                        scale = (double)oldBitmap.PixelHeight / newBitmap.PixelHeight;

                    // Задание свойств элемента Image
                    image.Source = newBitmap;
                    image.RenderTransform = new ScaleTransform
                    {
                        ScaleX = scale,
                        ScaleY = scale,
                    };
                }
                image.Tag = ImageType.Full;
            }
        }

        // ...
    }
}

Пожалуй, замена миниатюры полным изображением является самой нетривиальной частью программы. Так как обработчик ManipulationStarted содержит асинхронные вызовы, он может обрабатывать перекрывающиеся события от нескольких объектов, если пользователь выполняет операции сразу с несколькими объектами одновременно. Основная логика выполняется только в том случае, если свойство Tag элемента Image равно ImageType.Thumbnail. Свойству Tag задается значение ImageType.Transitioning (не является строго необходимым, но полезно для отладки), после чего вызов LoadBitmapAsync загружает это изображение. После замены свойство Tag элемента Image становится равным ImageType.Full.

Я хотел, чтобы процесс был по возможности визуально плавным, поэтому в коде вычисляются множители для преобразования размера полного изображения к размеру миниатюры. Размер и ориентация изображения при этом не изменяются, но разрешение улучшается.

Осталось сказать о трех перегруженных версиях LoadBitmapAsync, каждая из которых возвращает BitmapSource. Сначала программа несколько различающимися способами получает объекты IRandomAccessStream для изображения и миниатюры, после чего выполняется уже приводившийся ранее код загрузки файла.

// ...

namespace WinRTTestApp
{
    public sealed partial class MainPage : Page
    {
        // ...

        private async Task<BitmapSource> LoadBitmapAsync(StorageFile storageFile)
        {
            BitmapSource bitmapSource = null;

            // Открытие объекта StorageFile для чтения
            using (IRandomAccessStreamWithContentType stream = await storageFile.OpenReadAsync())
            {
                bitmapSource = await LoadBitmapAsync(stream);
            }

            return bitmapSource;
        }

        private async Task<BitmapSource> LoadBitmapAsync(StorageItemThumbnail thumbnail)
        {
            return await LoadBitmapAsync(thumbnail as IRandomAccessStream);
        }

        private async Task<BitmapSource> LoadBitmapAsync(IRandomAccessStream stream)
        {
            WriteableBitmap bitmap = null;

            // Создание объекта BitmapDecoder для потока
            BitmapDecoder decoder = null;

            try
            {
                decoder = await BitmapDecoder.CreateAsync(stream);
            }
            catch
            {
                // Неудачные попытки просто игнорируются
                return null;
            }

            // Получение первого кадра
            BitmapFrame bitmapFrame = await decoder.GetFrameAsync(0);

            // Получение пикселов
            PixelDataProvider dataProvider =
                    await bitmapFrame.GetPixelDataAsync(BitmapPixelFormat.Bgra8,
                                                        BitmapAlphaMode.Premultiplied,
                                                        new BitmapTransform(),
                                                        ExifOrientationMode.RespectExifOrientation,
                                                        ColorManagementMode.ColorManageToSRgb);

            byte[] pixels = dataProvider.DetachPixelData();

            // Создание объекта WriteableBitmap и заполнение пикселов
            bitmap = new WriteableBitmap((int)bitmapFrame.PixelWidth,
                                         (int)bitmapFrame.PixelHeight);

            using (Stream pixelStream = bitmap.PixelBuffer.AsStream())
            {
                pixelStream.Write(pixels, 0, pixels.Length);
            }

            bitmap.Invalidate();
            return bitmap;
        }
    }
}
Пройди тесты
Лучший чат для C# программистов