Буфер обмена в WinRT

137

В обмене данными через чудо-кнопку Общий доступ (Share) задействованы классы из пространств имен Windows.ApplicationModel.DataTransfer и Windows.ApplicationModel.DataTransfer.ShareTarget. Первое пространство имен также включает поддержку традиционного механизма передачи данных между приложениями Windows: буфера обмена (clipboard).

Я хочу добавить поддержку буфера обмена в программу Finger Paint перед тем, как браться за чудо-кнопку "Общий доступ". Добавление этой поддержки в программу, работающую с растровыми изображениями, связано с потенциальными сложностями. Возможно, вы захотите реализовать функцию выделения прямоугольного участка изображения и копирования его в буфер. Также возможна реализация функции вставки, которая позволяет разместить входное изображение в любой точке текущего изображения.

Но мы пойдем простым путем: команда Copy будет копировать в буфер все изображение, а команда Paste будет интерпретировать вставляемую графику как новое изображение - как если бы оно было загружено из файла, но без указания имени. Начнем с добавления кнопок Copy и Paste в строку приложения:

<Page.BottomAppBar>
        <AppBar>
            <Grid>
                <StackPanel Orientation="Horizontal" HorizontalAlignment="Left">
                
                    ...
                
                    <AppBarButton Icon="Copy"
                            Click="OnCopyAppBarButtonClick" />

                    <AppBarButton Name="pasteAppBarButton"
                            Icon="Paste"
                            Click="OnPasteAppBarButtonClick" />
                </StackPanel>
            </Grid>
        </AppBar>
</Page.BottomAppBar>

Кнопке Paste необходимо присвоить имя, потому что ее блокировка должна устанавливаться и сниматься в коде в зависимости от того, содержатся ли в буфере данные растрового изображения.

Я решил реализовать весь код обмена данными в другой частичной реализации класса MainPage с именем файла MainPage.Share.cs. Конструктор MainPage вызывает метод из этого файла:

public MainPage()
{
    // ...

    // Вызов метода из MainPage.Share.cs
    InitializeSharing();

    // ...
}

В частности, этот метод проверяет, должна ли кнопка Paste быть доступна изначально, после чего назначает обработчик события, который должен вызываться при изменении содержимого буфера:

public sealed partial class MainPage : Page
{
     private void InitializeSharing()
     {
        // Инициализация кнопки Paste и обеспечение обновлений
        CheckForPasteEnable();
        Clipboard.ContentChanged += OnClipboardContentChanged;

        // ...
     }

     private void OnClipboardContentChanged(object sender, object args)
     {
        CheckForPasteEnable();
     }

     private void CheckForPasteEnable()
     {
        pasteAppBarButton.IsEnabled = CheckClipboardForBitmap();
     }

     private bool CheckClipboardForBitmap()
     {
        DataPackageView dataView = Clipboard.GetContent();
        return dataView.Contains(StandardDataFormats.Bitmap);
     }
        
    // ...
}

Маленький статический класс Clipboard содержит всего четыре метода и одно событие. Два самых важных метода - GetContent и SetContent. Метод GetContent возвращает объект DataPackageView, предоставляющий удобный способ проверки текущего содержимого буфера (является ли оно растровым изображением или нет).

Методу SetContent передается объект DataPackage, который содержит набор методов для помещения в буфер различных видов данных. В этот набор входит и метод с именем SetBitmap. Обработчик кнопки Сору создает объект DataPackage и выбирает тип операции Move, означающий, что программа не собирается дальше работать с растровым изображением, помещенным в буфер:

private async void OnCopyAppBarButtonClick(object sender, RoutedEventArgs e)
{
    DataPackage dataPackage = new DataPackage
    {
         RequestedOperation = DataPackageOperation.Move,
    };
    dataPackage.SetBitmap(await GetBitmapStream(bitmap));

    Clipboard.SetContent(dataPackage);
    this.BottomAppBar.IsOpen = false;
}

Тем не менее метод SetBitmap не использует такие привычные данные, как BitmapSource. Ему нужен объект RandomAccessStreamReference, который ссылается на закодированное растровое изображение. Объект RandomAccessStreamReference можно создать на базе InMemoryRandomAccessStream. Эта задача решается методом GetBitmapStream, используемым при вызове SetBitmap.

Обратите внимание: аргумент вызова GetBitmapStream представляет собой объект WriteableBitmap, сохраняемый в поле внутри MainPage. Я постарался немного обобщить GetBitmapStream в том смысле, что он создает по этому аргументу собственный массив пикселов, но с таким же успехом он мог бы обращаться к существующему массиву пикселов, также хранимому в поле MainPage:

private async Task<RandomAccessStreamReference> GetBitmapStream(WriteableBitmap bitmap)
{
    // Получение массива пикселов для растрового изображения
    byte[] pixels = new byte[4 * bitmap.PixelWidth * bitmap.PixelHeight];
    Stream stream = bitmap.PixelBuffer.AsStream();
    await stream.ReadAsync(pixels, 0, pixels.Length);

    // Создание объекта BitmapEncoder, связанного с потоком в памяти
    InMemoryRandomAccessStream memoryStream = new InMemoryRandomAccessStream();
    BitmapEncoder encoder = await BitmapEncoder.CreateAsync(BitmapEncoder.PngEncoderId, 
    memoryStream);

    // Передача пикселов кодеру
    encoder.SetPixelData(BitmapPixelFormat.Bgra8, BitmapAlphaMode.Premultiplied,
         (uint)bitmap.PixelWidth, (uint)bitmap.PixelHeight, 96, 96, pixels);
    await encoder.FlushAsync();

    // Возвращение RandomAccessStreamReference
    return RandomAccessStreamReference.CreateFromStream(memoryStream);
}

Логика Paste чуть сложнее, и не только потому, что доступность кнопки должна определяться существованием растрового содержимого в буфере обмена. Если текущий рисунок еще не был сохранен, то при нажатии кнопки Paste программа должна предложить сохранить или уничтожить рисунок (как при загрузке нового файла).

Это означает, что обработчик кнопки Paste должен вызвать метод CheckIfOkToTrashFile из MainPage.File.cs, передавая ему метод, который должен быть выполнен в случае продолжения операции Paste. Мне было неясно, какую четь обработки входящего изображения следует выполнить до вызова CheckIfOkToTrashFile. Меня беспокоило, что пользователь может выбрать сохранение текущего изображения, в ходе которого содержимое буфера может как-то измениться. Чтобы избежать проблем, я немедленно сохраняю массив пикселов, однако объект WriteableBitmap при этом еще не создается. Для отложенного выполнения этой операции необходимо сохранить в полях несколько значений, связанных с новым изображением:

public sealed partial class MainPage : Page
{
    int pastedPixelWidth, pastedPixelHeight;
    byte[] pastedPixels;
    
    // ...

    private async void OnPasteAppBarButtonClick(object sender, RoutedEventArgs args)
    {
        // Временная блокировка для кнопки Paste
        Button button = sender as Button;
        button.IsEnabled = false;

        // Получение содержимого буфера и проверка изображения
        DataPackageView dataView = Clipboard.GetContent();

        if (dataView.Contains(StandardDataFormats.Bitmap))
        {
            // Получение ссылки на поток и самого потока
            RandomAccessStreamReference streamRef = await dataView.GetBitmapAsync();
            IRandomAccessStreamWithContentType stream = await streamRef.OpenReadAsync();

            // Создание объекта BitmapDecoder для чтения изображения
            BitmapDecoder decoder = await BitmapDecoder.CreateAsync(stream);
            BitmapFrame bitmapFrame = await decoder.GetFrameAsync(0);
            PixelDataProvider pixelProvider = 
                await bitmapFrame.GetPixelDataAsync(BitmapPixelFormat.Bgra8, 
                                        BitmapAlphaMode.Premultiplied, 
                                        new BitmapTransform(),
                                        ExifOrientationMode.RespectExifOrientation, 
                                        ColorManagementMode.ColorManageToSRgb);

            // Сохранение информации, достаточной для создания WriteableBitmap
            pastedPixelWidth = (int)bitmapFrame.PixelWidth;
            pastedPixelHeight = (int)bitmapFrame.PixelHeight;
            pastedPixels = pixelProvider.DetachPixelData();

            // Поверка замены текущего рисунка
            await CheckIfOkToTrashFile(FinishPasteBitmapAndPixelArray);
        }

        // Снятие блокировки с кнопки и закрытие строки приложения
        button.IsEnabled = true;
        this.BottomAppBar.IsOpen = false;
    }

    private async Task FinishPasteBitmapAndPixelArray()
    {
        bitmap = new WriteableBitmap(pastedPixelWidth, pastedPixelHeight);
        pixels = pastedPixels;

        // Передача пикселов в изображение (и не только)
        await InitializeBitmap();

        //Задание свойств AppSettings для нового изображения
        appSettings.LoadedFilePath = null;
        appSettings.LoadedFilename = null;
        appSettings.IsImageModified = false;
    }

    // ...
}

Для поддержки операций с буфером необходимо сделать еще одно: многие пользователи привыкли использовать комбинации Ctrl+C и Ctrl+V для выполнения копирования и вставки. Я добавил их поддержку в MalnPage.Share.cs, воспользовавшись существующими обработчиками кнопок:

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

    private void InitializeSharing()
    {
        // ...

        // Отслеживание комбинаций клавиш для копирования и вставки
        Window.Current.CoreWindow.Dispatcher.AcceleratorKeyActivated += 
                                                OnAcceleratorKeyActivated;

        // ...
    }
    
    // ...

    private void OnAcceleratorKeyActivated(CoreDispatcher sender, AcceleratorKeyEventArgs args)
    {
        if ((args.EventType == CoreAcceleratorKeyEventType.SystemKeyDown ||
             args.EventType == CoreAcceleratorKeyEventType.KeyDown) &&
            (args.VirtualKey == VirtualKey.C || args.VirtualKey == VirtualKey.V))
        {
            CoreWindow window = Window.Current.CoreWindow;
            CoreVirtualKeyStates down = CoreVirtualKeyStates.Down;

            // Only want case where Ctrl is down
            if ((window.GetKeyState(VirtualKey.Shift) & down) == down ||
                (window.GetKeyState(VirtualKey.Control) & down) != down ||
                (window.GetKeyState(VirtualKey.Menu) & down) == down)
            {
                return;
            }

            if (args.VirtualKey == VirtualKey.C)
            {
                OnCopyAppBarButtonClick(null, null);   
            }
            else if (args.VirtualKey == VirtualKey.V)
            {
                OnPasteAppBarButtonClick(pasteAppBarButton, null);
            }
        }
    }
}
Пройди тесты
Лучший чат для C# программистов