From eb9b9b31d5a9a0545de48a05ce61f0bf0d2a2241 Mon Sep 17 00:00:00 2001 From: "Jim Walker (WINDOWS)" Date: Tue, 25 Aug 2020 21:31:54 +0000 Subject: [PATCH] Merged PR 168: update tutorials to use WinUI update tutorials to use WinUI --- PhotoLab/App.xaml | 11 +- PhotoLab/DetailPage.xaml | 50 +- PhotoLab/DetailPage.xaml.cs | 16 - PhotoLab/MainPage.xaml | 48 +- PhotoLab/MainPage.xaml.cs | 19 +- PhotoLab/MobileScreenTrigger.cs | 71 --- PhotoLab/PhotoLab.csproj | 1 - .../adaptive-layout/PhotoLab/App.xaml | 13 +- .../adaptive-layout/PhotoLab/DetailPage.xaml | 512 ++++++++-------- .../PhotoLab/DetailPage.xaml.cs | 231 ++++---- .../adaptive-layout/PhotoLab/ImageFileInfo.cs | 30 +- .../PhotoLab/LoadedImageBrush.cs | 276 +++++++++ .../adaptive-layout/PhotoLab/MainPage.xaml | 28 +- .../adaptive-layout/PhotoLab/MainPage.xaml.cs | 70 ++- .../PhotoLab/MobileScreenTrigger.cs | 71 --- .../adaptive-layout/PhotoLab/PhotoLab.csproj | 8 +- .../data-binding/PhotoLab/App.xaml | 17 +- .../data-binding/PhotoLab/DetailPage.xaml | 558 ++++++++--------- .../data-binding/PhotoLab/DetailPage.xaml.cs | 231 ++++---- .../data-binding/PhotoLab/ImageFileInfo.cs | 30 +- .../data-binding/PhotoLab/LoadedImageBrush.cs | 276 +++++++++ .../data-binding/PhotoLab/MainPage.xaml | 32 +- .../data-binding/PhotoLab/MainPage.xaml.cs | 67 ++- .../PhotoLab/MobileScreenTrigger.cs | 71 --- .../data-binding/PhotoLab/PhotoLab.csproj | 8 +- .../style/PhotoLab/App.xaml | 15 +- .../style/PhotoLab/DetailPage.xaml | 104 +--- .../style/PhotoLab/DetailPage.xaml.cs | 223 +++---- .../style/PhotoLab/ImageFileInfo.cs | 30 +- .../style/PhotoLab/LoadedImageBrush.cs | 276 +++++++++ .../style/PhotoLab/MainPage.xaml | 74 +-- .../style/PhotoLab/MainPage.xaml.cs | 70 ++- .../style/PhotoLab/MobileScreenTrigger.cs | 71 --- .../style/PhotoLab/PhotoLab.csproj | 8 +- .../user-interface/PhotoLab/App.xaml | 16 +- .../user-interface/PhotoLab/DetailPage.xaml | 559 ++++++++---------- .../PhotoLab/DetailPage.xaml.cs | 231 ++++---- .../user-interface/PhotoLab/ImageFileInfo.cs | 32 +- .../PhotoLab/LoadedImageBrush.cs | 276 +++++++++ .../user-interface/PhotoLab/MainPage.xaml | 7 +- .../user-interface/PhotoLab/MainPage.xaml.cs | 93 +-- .../PhotoLab/MobileScreenTrigger.cs | 71 --- .../user-interface/PhotoLab/PhotoLab.csproj | 8 +- 43 files changed, 2680 insertions(+), 2229 deletions(-) delete mode 100644 PhotoLab/MobileScreenTrigger.cs create mode 100644 xaml-basics-starting-points/adaptive-layout/PhotoLab/LoadedImageBrush.cs delete mode 100644 xaml-basics-starting-points/adaptive-layout/PhotoLab/MobileScreenTrigger.cs create mode 100644 xaml-basics-starting-points/data-binding/PhotoLab/LoadedImageBrush.cs delete mode 100644 xaml-basics-starting-points/data-binding/PhotoLab/MobileScreenTrigger.cs create mode 100644 xaml-basics-starting-points/style/PhotoLab/LoadedImageBrush.cs delete mode 100644 xaml-basics-starting-points/style/PhotoLab/MobileScreenTrigger.cs create mode 100644 xaml-basics-starting-points/user-interface/PhotoLab/LoadedImageBrush.cs delete mode 100644 xaml-basics-starting-points/user-interface/PhotoLab/MobileScreenTrigger.cs diff --git a/PhotoLab/App.xaml b/PhotoLab/App.xaml index 48151ab..2bb27e6 100644 --- a/PhotoLab/App.xaml +++ b/PhotoLab/App.xaml @@ -28,16 +28,17 @@ - - 0 - 641 - 1008 - + + + + 0 + 641 + 1008 diff --git a/PhotoLab/DetailPage.xaml b/PhotoLab/DetailPage.xaml index b14c54e..9c91f2a 100644 --- a/PhotoLab/DetailPage.xaml +++ b/PhotoLab/DetailPage.xaml @@ -515,7 +515,7 @@ Style="{StaticResource ValueTextBlock}" Text="{x:Bind item.Exposure.ToString('N', culture), Mode=OneWay}" /> - + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/PhotoLab/DetailPage.xaml.cs b/PhotoLab/DetailPage.xaml.cs index 4a91b80..df90066 100644 --- a/PhotoLab/DetailPage.xaml.cs +++ b/PhotoLab/DetailPage.xaml.cs @@ -196,22 +196,6 @@ private void ToggleEditState() private async void ExportImage() { - // We haven't enabled image export for phone. - var qualifiers = Windows.ApplicationModel.Resources.Core.ResourceContext.GetForCurrentView().QualifierValues; - if (qualifiers.ContainsKey("DeviceFamily") && qualifiers["DeviceFamily"] == "Mobile") - { - ContentDialog notSupportedDialog = new ContentDialog - { - Title = "Export is not enabled", - Content = "Image export is not enabled on phones.", - CloseButtonText = "OK", - }; - - ContentDialogResult result = await notSupportedDialog.ShowAsync(); - return; - } - - // Not on phone, so export image. CanvasDevice device = CanvasDevice.GetSharedDevice(); using (CanvasRenderTarget offscreen = new CanvasRenderTarget( device, item.ImageProperties.Width, item.ImageProperties.Height, 96)) diff --git a/PhotoLab/MainPage.xaml b/PhotoLab/MainPage.xaml index 4e05225..3d546a0 100644 --- a/PhotoLab/MainPage.xaml +++ b/PhotoLab/MainPage.xaml @@ -41,9 +41,8 @@ 8 0 0 - 16 - 32 - + 16 + - @@ -140,7 +139,7 @@ - - - - - + + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + + + - - + + - - + + + - - + + - - + + + + + + + + + + + Grid.Row="0" + Content="{TemplateBinding Header}" + ContentTemplate="{TemplateBinding HeaderTemplate}" + FontWeight="{ThemeResource SliderHeaderThemeFontWeight}" + Foreground="{ThemeResource SliderHeaderForeground}" + Margin="{ThemeResource SliderTopHeaderMargin}" + TextWrapping="Wrap" + Visibility="Collapsed" + x:DeferLoadStrategy="Lazy"/> - + Grid.Row="1" + Background="{ThemeResource SliderContainerBackground}" + Control.IsTemplateFocusTarget="True"> + + + - + - + + + - + + Height="{ThemeResource SliderTrackThemeHeight}" + Grid.Row="1" + Grid.ColumnSpan="3" + contract7Present:RadiusX="{Binding CornerRadius, RelativeSource={RelativeSource TemplatedParent}, Converter={StaticResource TopLeftCornerRadiusDoubleValueConverter}}" + contract7Present:RadiusY="{Binding CornerRadius, RelativeSource={RelativeSource TemplatedParent}, Converter={StaticResource BottomRightCornerRadiusDoubleValueConverter}}" + contract7NotPresent:RadiusX="{Binding Source={ThemeResource ControlCornerRadius}, Converter={StaticResource TopLeftCornerRadiusDoubleValueConverter}}" + contract7NotPresent:RadiusY="{Binding Source={ThemeResource ControlCornerRadius}, Converter={StaticResource BottomRightCornerRadiusDoubleValueConverter}}" /> + + Visibility="Collapsed" + Fill="{ThemeResource SliderTickBarFill}" + Height="{ThemeResource SliderOutsideTickBarThemeHeight}" + VerticalAlignment="Bottom" + Margin="0,0,0,4" + Grid.ColumnSpan="3" /> + Visibility="Collapsed" + Fill="{ThemeResource SliderInlineTickBarFill}" + Height="{ThemeResource SliderTrackThemeHeight}" + Grid.Row="1" + Grid.ColumnSpan="3" /> + Visibility="Collapsed" + Fill="{ThemeResource SliderTickBarFill}" + Height="{ThemeResource SliderOutsideTickBarThemeHeight}" + VerticalAlignment="Top" + Margin="0,4,0,0" + Grid.Row="2" + Grid.ColumnSpan="3" /> + Style="{StaticResource SliderThumbStyle}" + DataContext="{TemplateBinding Value}" + Height="{ThemeResource SliderHorizontalThumbHeight}" + Width="{ThemeResource SliderHorizontalThumbWidth}" + Grid.Row="0" + Grid.RowSpan="3" + Grid.Column="1" + FocusVisualMargin="-14,-6,-14,-6" + AutomationProperties.AccessibilityView="Raw" /> - + + + - + - + + + Fill="{TemplateBinding Background}" + Width="{ThemeResource SliderTrackThemeHeight}" + Grid.Column="1" + Grid.RowSpan="3" + contract7Present:RadiusX="{Binding CornerRadius, RelativeSource={RelativeSource TemplatedParent}, Converter={StaticResource TopLeftCornerRadiusDoubleValueConverter}}" + contract7Present:RadiusY="{Binding CornerRadius, RelativeSource={RelativeSource TemplatedParent}, Converter={StaticResource BottomRightCornerRadiusDoubleValueConverter}}" + contract7NotPresent:RadiusX="{Binding Source={ThemeResource ControlCornerRadius}, Converter={StaticResource TopLeftCornerRadiusDoubleValueConverter}}" + contract7NotPresent:RadiusY="{Binding Source={ThemeResource ControlCornerRadius}, Converter={StaticResource BottomRightCornerRadiusDoubleValueConverter}}" /> + Fill="{TemplateBinding Foreground}" + Grid.Column="1" + Grid.Row="2" + contract7Present:RadiusX="{Binding CornerRadius, RelativeSource={RelativeSource TemplatedParent}, Converter={StaticResource TopLeftCornerRadiusDoubleValueConverter}}" + contract7Present:RadiusY="{Binding CornerRadius, RelativeSource={RelativeSource TemplatedParent}, Converter={StaticResource BottomRightCornerRadiusDoubleValueConverter}}" + contract7NotPresent:RadiusX="{Binding Source={ThemeResource ControlCornerRadius}, Converter={StaticResource TopLeftCornerRadiusDoubleValueConverter}}" + contract7NotPresent:RadiusY="{Binding Source={ThemeResource ControlCornerRadius}, Converter={StaticResource BottomRightCornerRadiusDoubleValueConverter}}" /> + Visibility="Collapsed" + Fill="{ThemeResource SliderTickBarFill}" + Width="{ThemeResource SliderOutsideTickBarThemeHeight}" + HorizontalAlignment="Right" + Margin="0,0,4,0" + Grid.RowSpan="3" /> + Visibility="Collapsed" + Fill="{ThemeResource SliderInlineTickBarFill}" + Width="{ThemeResource SliderTrackThemeHeight}" + Grid.Column="1" + Grid.RowSpan="3" /> + Visibility="Collapsed" + Fill="{ThemeResource SliderTickBarFill}" + Width="{ThemeResource SliderOutsideTickBarThemeHeight}" + HorizontalAlignment="Left" + Margin="4,0,0,0" + Grid.Column="2" + Grid.RowSpan="3" /> + Style="{StaticResource SliderThumbStyle}" + DataContext="{TemplateBinding Value}" + Width="{ThemeResource SliderVerticalThumbWidth}" + Height="{ThemeResource SliderVerticalThumbHeight}" + Grid.Row="1" + Grid.Column="0" + Grid.ColumnSpan="3" + FocusVisualMargin="-6,-14,-6,-14" + AutomationProperties.AccessibilityView="Raw" /> @@ -397,9 +372,9 @@ - + - + + + + + - - @@ -489,6 +476,7 @@ + - - - - - - - - - - - - + + + + + + Maximum="5"/> - + + + + @@ -641,6 +620,5 @@ - diff --git a/xaml-basics-starting-points/adaptive-layout/PhotoLab/DetailPage.xaml.cs b/xaml-basics-starting-points/adaptive-layout/PhotoLab/DetailPage.xaml.cs index e91a9ce..df90066 100644 --- a/xaml-basics-starting-points/adaptive-layout/PhotoLab/DetailPage.xaml.cs +++ b/xaml-basics-starting-points/adaptive-layout/PhotoLab/DetailPage.xaml.cs @@ -23,20 +23,18 @@ // --------------------------------------------------------------------------------- using Microsoft.Graphics.Canvas; -using Microsoft.Graphics.Canvas.Effects; using System; using System.Collections.Generic; using System.Globalization; -using System.Numerics; +using System.Threading.Tasks; using Windows.Storage; using Windows.Storage.Pickers; using Windows.Storage.Streams; -using Windows.UI.Composition; using Windows.UI.Core; using Windows.UI.Xaml.Controls; using Windows.UI.Xaml.Controls.Primitives; -using Windows.UI.Xaml.Hosting; using Windows.UI.Xaml.Media.Animation; +using Windows.UI.Xaml.Media.Imaging; using Windows.UI.Xaml.Navigation; namespace PhotoLab @@ -44,42 +42,47 @@ namespace PhotoLab public sealed partial class DetailPage : Page { ImageFileInfo item; - Compositor compositor; - CompositionEffectBrush combinedBrush; CultureInfo culture = CultureInfo.CurrentCulture; - ContrastEffect contrastEffect; - ExposureEffect exposureEffect; - TemperatureAndTintEffect temperatureAndTintEffect; - GaussianBlurEffect graphicsEffect; - SaturationEffect saturationEffect; - bool editingInitialized = false; bool canNavigateWithUnsavedChanges = false; + BitmapImage ImageSource = null; public DetailPage() { this.InitializeComponent(); } - protected override void OnNavigatedTo(NavigationEventArgs e) + protected override async void OnNavigatedTo(NavigationEventArgs e) { item = e.Parameter as ImageFileInfo; canNavigateWithUnsavedChanges = false; - ResetEffects(); + ImageSource = await item.GetImageSourceAsync(); if (item != null) { - item.PropertyChanged += (s, e2) => UpdateEffectBrush(e2.PropertyName); - targetImage.Source = item.ImageSource; ConnectedAnimation imageAnimation = ConnectedAnimationService.GetForCurrentView().GetAnimation("itemAnimation"); if (imageAnimation != null) { - imageAnimation.Completed += (s, e_) => + targetImage.Source = ImageSource; + + imageAnimation.Completed += async (s, e_) => { - MainImage.Source = item.ImageSource; + await LoadBrushAsync(); + // This delay prevents a flicker that can happen when + // a large image is being loaded to the brush. It gives + // the image time to finish loading. (200 is ok, 400 to be safe.) + await Task.Delay(400); targetImage.Source = null; }; imageAnimation.TryStart(targetImage); } + + if (ImageSource.PixelHeight == 0 && ImageSource.PixelWidth == 0) + { + // There is no editable image loaded. Disable zoom and edit + // to prevent other errors. + EditButton.IsEnabled = false; + ZoomButton.IsEnabled = false; + }; } else { @@ -116,8 +119,11 @@ protected override void OnNavigatingFrom(NavigatingCancelEventArgs e) } else { + if (e.NavigationMode == NavigationMode.Back) + { + ConnectedAnimationService.GetForCurrentView().PrepareToAnimate("backAnimation", imgRect); + } canNavigateWithUnsavedChanges = false; - ConnectedAnimationService.GetForCurrentView().PrepareToAnimate("backAnimation", MainImage); base.OnNavigatingFrom(e); } } @@ -140,7 +146,8 @@ private async void ShowSaveDialog(NavigatingCancelEventArgs e) { // The user decided to leave the page. Restart // the navigation attempt. - canNavigateWithUnsavedChanges = true; + canNavigateWithUnsavedChanges = true; + ResetEffects(); Frame.Navigate(e.SourcePageType, e.Parameter); } } @@ -160,8 +167,8 @@ private void MainImageScroller_ViewChanged(object sender, ScrollViewerViewChange private void FitToScreen() { - var zoomFactor = (float)Math.Min(MainImageScroller.ActualWidth / item.ImageSource.PixelWidth, - MainImageScroller.ActualHeight / item.ImageSource.PixelHeight); + var zoomFactor = (float)Math.Min(MainImageScroller.ActualWidth / item.ImageProperties.Width, + MainImageScroller.ActualHeight / item.ImageProperties.Height); MainImageScroller.ChangeView(null, null, zoomFactor); } @@ -182,125 +189,33 @@ private void UpdateZoomState() } } - private void InitializeEffects() - { - saturationEffect = new SaturationEffect() - { - Name = "SaturationEffect", - Saturation = item.Saturation, - Source = new CompositionEffectSourceParameter("Backdrop") - }; - contrastEffect = new ContrastEffect() - { - Name = "ContrastEffect", - Contrast = item.Contrast, - Source = saturationEffect - }; - exposureEffect = new ExposureEffect() - { - Name = "ExposureEffect", - Source = contrastEffect, - Exposure = item.Exposure, - }; - temperatureAndTintEffect = new TemperatureAndTintEffect() - { - Name = "TemperatureAndTintEffect", - Source = exposureEffect, - Temperature = item.Temperature, - Tint = item.Tint - }; - graphicsEffect = new GaussianBlurEffect() - { - Name = "Blur", - Source = temperatureAndTintEffect, - BlurAmount = item.Blur, - BorderMode = EffectBorderMode.Hard, - }; - } - - private void InitializeCompositor() - { - compositor = ElementCompositionPreview.GetElementVisual(this).Compositor; - InitializeEffects(); - MainImage.Source = item.ImageSource; - MainImage.InvalidateArrange(); - - var destinationBrush = compositor.CreateBackdropBrush(); - - var graphicsEffectFactory = compositor.CreateEffectFactory(graphicsEffect, new[] { - "SaturationEffect.Saturation", "ExposureEffect.Exposure", "Blur.BlurAmount", - "TemperatureAndTintEffect.Temperature", "TemperatureAndTintEffect.Tint", - "ContrastEffect.Contrast" }); - combinedBrush = graphicsEffectFactory.CreateBrush(); - combinedBrush.SetSourceParameter("Backdrop", destinationBrush); - - var effectSprite = compositor.CreateSpriteVisual(); - effectSprite.Size = new Vector2((float)item.ImageSource.PixelWidth, (float)item.ImageSource.PixelHeight); - effectSprite.Brush = combinedBrush; - ElementCompositionPreview.SetElementChildVisual(MainImage, effectSprite); - - editingInitialized = true; - } - private void ToggleEditState() { - if (MainSplitView.IsPaneOpen) - { - MainSplitView.IsPaneOpen = false; - } - else - { - if (!editingInitialized) - { - InitializeCompositor(); - } - MainSplitView.IsPaneOpen = true; - } - } - - private void UpdateEffectBrush(string propertyName) - { - void update(string effectName, float effectValue) => - combinedBrush?.Properties.InsertScalar(effectName, effectValue); - - switch (propertyName) - { - case nameof(item.Exposure): update("ExposureEffect.Exposure", item.Exposure); break; - case nameof(item.Temperature): update("TemperatureAndTintEffect.Temperature", item.Temperature); break; - case nameof(item.Tint): update("TemperatureAndTintEffect.Tint", item.Tint); break; - case nameof(item.Contrast): update("ContrastEffect.Contrast", item.Contrast); break; - case nameof(item.Saturation): update("SaturationEffect.Saturation", item.Saturation); break; - case nameof(item.Blur): update("Blur.BlurAmount", item.Blur); break; - default: break; - } + MainSplitView.IsPaneOpen = !MainSplitView.IsPaneOpen; } private async void ExportImage() { CanvasDevice device = CanvasDevice.GetSharedDevice(); using (CanvasRenderTarget offscreen = new CanvasRenderTarget( - device, item.ImageSource.PixelWidth, item.ImageSource.PixelHeight, 96)) + device, item.ImageProperties.Width, item.ImageProperties.Height, 96)) { using (IRandomAccessStream stream = await item.ImageFile.OpenReadAsync()) using (CanvasBitmap image = await CanvasBitmap.LoadAsync(offscreen, stream, 96)) { - saturationEffect.Source = image; + ImageEffectsBrush.SetSource(image); + using (CanvasDrawingSession ds = offscreen.CreateDrawingSession()) { ds.Clear(Windows.UI.Colors.Black); - // Need to copy the value of each effect setting. - contrastEffect.Contrast = item.Contrast; - exposureEffect.Exposure = item.Exposure; - temperatureAndTintEffect.Temperature = item.Temperature; - temperatureAndTintEffect.Tint = item.Tint; - saturationEffect.Saturation = item.Saturation; - graphicsEffect.BlurAmount = item.Blur; - ds.DrawImage(graphicsEffect); + var img = ImageEffectsBrush.Image; + ds.DrawImage(img); } var fileSavePicker = new FileSavePicker() { + SuggestedStartLocation = PickerLocationId.PicturesLibrary, SuggestedSaveFile = item.ImageFile }; @@ -315,24 +230,78 @@ private async void ExportImage() await offscreen.SaveAsync(outStream, CanvasBitmapFileFormat.Jpeg); } - ResetEffects(); - var newItem = await MainPage.LoadImageInfo(outputFile); + // Check whether this save is overwriting the original image. + // If it is, replace it in the list. Otherwise, insert it as a copy. + bool replace = false; + if (outputFile.IsEqual(item.ImageFile)) + { + replace = true; + } - if (outputFile.Path == item.ImageFile.Path) + try { - item.ImageSource = newItem.ImageSource; + await LoadSavedImageAsync(outputFile, replace); } - else + catch (Exception ex) { - item = newItem; + if (ex.Message.Contains("0x80070323")) + { + // The handle with which this oplock was associated has been closed. + // The oplock is now broken. (Exception from HRESULT: 0x80070323) + // This is a temporary condition, so just try again. + await LoadSavedImageAsync(outputFile, replace); + } } - - MainImage.Source = item.ImageSource; } } } } + private async Task LoadSavedImageAsync(StorageFile imageFile, bool replaceImage) + { + item.NeedsSaved = false; + var newItem = await MainPage.LoadImageInfo(imageFile); + ResetEffects(); + + // Get the index of the original image so we can + // insert the saved image in the same place. + var index = MainPage.Current.Images.IndexOf(item); + + item = newItem; + this.Bindings.Update(); + + UnloadObject(imgRect); + FindName("imgRect"); + await LoadBrushAsync(); + + // Insert the saved image into the collection. + if (replaceImage == true) + { + MainPage.Current.Images.RemoveAt(index); + MainPage.Current.Images.Insert(index, item); + } + else + { + MainPage.Current.Images.Insert(index+1, item); + } + + + // Replace the persisted image used for connected animation. + MainPage.Current.UpdatePersistedItem(item); + + // Update BitMapImage used for small picture. + ImageSource = await item.GetImageSourceAsync(); + SmallImage.Source = ImageSource; + } + + private async Task LoadBrushAsync() + { + using (IRandomAccessStream fileStream = await item.ImageFile.OpenReadAsync()) + { + ImageEffectsBrush.LoadImageFromStream(fileStream); + } + } + private void ResetEffects() { item.Exposure = @@ -343,5 +312,9 @@ private void ResetEffects() item.Saturation = 1; } + private void SmallImage_Loaded(object sender, Windows.UI.Xaml.RoutedEventArgs e) + { + SmallImage.Source = ImageSource; + } } } diff --git a/xaml-basics-starting-points/adaptive-layout/PhotoLab/ImageFileInfo.cs b/xaml-basics-starting-points/adaptive-layout/PhotoLab/ImageFileInfo.cs index b3fc945..6e5220c 100644 --- a/xaml-basics-starting-points/adaptive-layout/PhotoLab/ImageFileInfo.cs +++ b/xaml-basics-starting-points/adaptive-layout/PhotoLab/ImageFileInfo.cs @@ -25,16 +25,17 @@ using System; using System.ComponentModel; using System.Runtime.CompilerServices; +using System.Threading.Tasks; using Windows.Storage; using Windows.Storage.FileProperties; +using Windows.Storage.Streams; using Windows.UI.Xaml.Media.Imaging; namespace PhotoLab { public class ImageFileInfo : INotifyPropertyChanged { - public ImageFileInfo(ImageProperties properties, StorageFile imageFile, - BitmapImage src, string name, string type) + public ImageFileInfo(ImageProperties properties, StorageFile imageFile, BitmapImage src, string name, string type) { ImageProperties = properties; ImageSource = src; @@ -50,6 +51,29 @@ public ImageFileInfo(ImageProperties properties, StorageFile imageFile, public ImageProperties ImageProperties { get; } + public async Task GetImageSourceAsync() + { + using (IRandomAccessStream fileStream = await ImageFile.OpenReadAsync()) + { + // Create a bitmap to be the image source. + BitmapImage bitmapImage = new BitmapImage(); + bitmapImage.SetSource(fileStream); + + return bitmapImage; + } + } + + public async Task GetImageThumbnailAsync() + { + var thumbnail = await ImageFile.GetThumbnailAsync(ThumbnailMode.PicturesView); + // Create a bitmap to be the image source. + BitmapImage bitmapImage = new BitmapImage(); + bitmapImage.SetSource(thumbnail); + thumbnail.Dispose(); + + return bitmapImage; + } + private BitmapImage _imageSource = null; public BitmapImage ImageSource { @@ -61,7 +85,7 @@ public BitmapImage ImageSource public string ImageFileType { get; } - public string ImageDimensions => $"{ImageSource.PixelWidth} x {ImageSource.PixelHeight}"; + public string ImageDimensions => $"{ImageProperties.Width} x {ImageProperties.Height}"; public string ImageTitle { diff --git a/xaml-basics-starting-points/adaptive-layout/PhotoLab/LoadedImageBrush.cs b/xaml-basics-starting-points/adaptive-layout/PhotoLab/LoadedImageBrush.cs new file mode 100644 index 0000000..2712497 --- /dev/null +++ b/xaml-basics-starting-points/adaptive-layout/PhotoLab/LoadedImageBrush.cs @@ -0,0 +1,276 @@ +using Microsoft.Graphics.Canvas; +using Microsoft.Graphics.Canvas.Brushes; +using Microsoft.Graphics.Canvas.Effects; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Windows.Foundation; +using Windows.Graphics.Effects; +using Windows.UI.Composition; +using Windows.UI.Xaml; +using Windows.UI.Xaml.Media; +using Windows.Storage.Streams; + +namespace PhotoLab +{ + class LoadedImageBrush : XamlCompositionBrushBase + { + private bool IsImageLoading = false; + private LoadedImageSurface _surface; + CompositionEffectBrush combinedBrush; + ContrastEffect contrastEffect; + ExposureEffect exposureEffect; + TemperatureAndTintEffect temperatureAndTintEffect; + GaussianBlurEffect graphicsEffect; + SaturationEffect saturationEffect; + + public ICanvasImage Image + { + get + { + contrastEffect.Contrast = (float)ContrastAmount; + exposureEffect.Exposure = (float)ExposureAmount; + temperatureAndTintEffect.Tint = (float)TintAmount; + temperatureAndTintEffect.Temperature = (float)TemperatureAmount; + saturationEffect.Saturation = (float)SaturationAmount; + graphicsEffect.BlurAmount = (float)BlurAmount; + return graphicsEffect; + } + } + + public void SetSource(ICanvasImage source) + { + saturationEffect.Source = source; + } + + public static readonly DependencyProperty BlurAmountProperty = DependencyProperty.Register( + "BlurAmount", + typeof(double), + typeof(LoadedImageBrush), + new PropertyMetadata(0.0, new PropertyChangedCallback(OnBlurAmountChanged) + ) +); + + public double BlurAmount + { + get { return (double)GetValue(BlurAmountProperty); } + set { SetValue(BlurAmountProperty, value); } + } + + private static void OnBlurAmountChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) + { + var brush = (LoadedImageBrush)d; + // Unbox and set a new blur amount if the CompositionBrush exists. + brush.CompositionBrush?.Properties.InsertScalar("Blur.BlurAmount", (float)(double)e.NewValue); + } + + + + public static readonly DependencyProperty ContrastAmountProperty = DependencyProperty.Register( +"ContrastAmount", +typeof(double), +typeof(LoadedImageBrush), +new PropertyMetadata(0.0, new PropertyChangedCallback(OnContrastAmountChanged) +) +); + + public double ContrastAmount + { + get { return (double)GetValue(ContrastAmountProperty); } + set { SetValue(ContrastAmountProperty, value); } + } + + private static void OnContrastAmountChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) + { + var brush = (LoadedImageBrush)d; + // Unbox and set a new blur amount if the CompositionBrush exists. + brush.CompositionBrush?.Properties.InsertScalar("ContrastEffect.Contrast", (float)(double)e.NewValue); + } + + public static readonly DependencyProperty SaturationAmountProperty = DependencyProperty.Register( +"SaturationAmount", +typeof(double), +typeof(LoadedImageBrush), +new PropertyMetadata(1.0, new PropertyChangedCallback(OnSaturationAmountChanged) +) +); + + public double SaturationAmount + { + get { return (double)GetValue(SaturationAmountProperty); } + set { SetValue(SaturationAmountProperty, value); } + } + + private static void OnSaturationAmountChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) + { + var brush = (LoadedImageBrush)d; + // Unbox and set a new blur amount if the CompositionBrush exists. + brush.CompositionBrush?.Properties.InsertScalar("SaturationEffect.Saturation", (float)(double)e.NewValue); + } + + public static readonly DependencyProperty ExposureAmountProperty = DependencyProperty.Register( +"ExposureAmount", +typeof(double), +typeof(LoadedImageBrush), +new PropertyMetadata(0.0, new PropertyChangedCallback(OnExposureAmountChanged) +) +); + + public double ExposureAmount + { + get { return (double)GetValue(ExposureAmountProperty); } + set { SetValue(ExposureAmountProperty, value); } + } + + private static void OnExposureAmountChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) + { + var brush = (LoadedImageBrush)d; + // Unbox and set a new blur amount if the CompositionBrush exists. + brush.CompositionBrush?.Properties.InsertScalar("ExposureEffect.Exposure", (float)(double)e.NewValue); + } + + + public static readonly DependencyProperty TintAmountProperty = DependencyProperty.Register( +"TintAmount", +typeof(double), +typeof(LoadedImageBrush), +new PropertyMetadata(0.0, new PropertyChangedCallback(OnTintAmountChanged) +) +); + + public double TintAmount + { + get { return (double)GetValue(TintAmountProperty); } + set { SetValue(TintAmountProperty, value); } + } + + private static void OnTintAmountChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) + { + var brush = (LoadedImageBrush)d; + // Unbox and set a new blur amount if the CompositionBrush exists. + brush.CompositionBrush?.Properties.InsertScalar("TemperatureAndTintEffect.Tint", (float)(double)e.NewValue); + } + + public static readonly DependencyProperty TemperatureAmountProperty = DependencyProperty.Register( +"TemperatureAmount", +typeof(double), +typeof(LoadedImageBrush), +new PropertyMetadata(0.0, new PropertyChangedCallback(OnTemperatureAmountChanged) +) +); + + public double TemperatureAmount + { + get { return (double)GetValue(TemperatureAmountProperty); } + set { SetValue(TemperatureAmountProperty, value); } + } + + private static void OnTemperatureAmountChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) + { + var brush = (LoadedImageBrush)d; + // Unbox and set a new blur amount if the CompositionBrush exists. + brush.CompositionBrush?.Properties.InsertScalar("TemperatureAndTintEffect.Temperature", (float)(double)e.NewValue); + } + + public LoadedImageBrush() + { + } + + public void LoadImageFromPath(string path) + { + var compositor = Window.Current.Compositor; + // Load image + _surface = LoadedImageSurface.StartLoadFromUri(new Uri(path)); + _surface.LoadCompleted += Load_Completed; + } + + public void LoadImageFromStream(IRandomAccessStream stream) + { + if (stream != null && IsImageLoading == false) + { + var compositor = Window.Current.Compositor; + // Load image + IsImageLoading = true; + _surface = LoadedImageSurface.StartLoadFromStream(stream); + _surface.LoadCompleted += Load_Completed; + } + } + + private void Load_Completed(LoadedImageSurface sender, LoadedImageSourceLoadCompletedEventArgs e) + { + IsImageLoading = false; + + if (e.Status == LoadedImageSourceLoadStatus.Success) + { + var compositor = Window.Current.Compositor; + var brush = compositor.CreateSurfaceBrush(_surface); + brush.Stretch = CompositionStretch.UniformToFill; + + // Create effects chain. + saturationEffect = new SaturationEffect() + { + Name = "SaturationEffect", + Saturation = (float)SaturationAmount, + Source = new CompositionEffectSourceParameter("image") + }; + contrastEffect = new ContrastEffect() + { + Name = "ContrastEffect", + Contrast = (float)ContrastAmount, + Source = saturationEffect + }; + exposureEffect = new ExposureEffect() + { + Name = "ExposureEffect", + Source = contrastEffect, + Exposure = (float)ExposureAmount, + }; + temperatureAndTintEffect = new TemperatureAndTintEffect() + { + Name = "TemperatureAndTintEffect", + Source = exposureEffect, + Temperature = (float)TemperatureAmount, + Tint = (float)TintAmount + }; + graphicsEffect = new GaussianBlurEffect() + { + Name = "Blur", + Source = temperatureAndTintEffect, + BlurAmount = (float)BlurAmount, + BorderMode = EffectBorderMode.Hard, + }; + + var graphicsEffectFactory = compositor.CreateEffectFactory(graphicsEffect, new[] { + "SaturationEffect.Saturation", "ExposureEffect.Exposure", "Blur.BlurAmount", + "TemperatureAndTintEffect.Temperature", "TemperatureAndTintEffect.Tint", + "ContrastEffect.Contrast" }); + combinedBrush = graphicsEffectFactory.CreateBrush(); + combinedBrush.SetSourceParameter("image", brush); + + // Composition Brush is what is being applied to the UI Element. + CompositionBrush = combinedBrush; + } + else + { + LoadImageFromPath("ms-appx:///Assets/StoreLogo.png"); + } + } + + protected override void OnDisconnected() + { + if (_surface != null) + { + _surface.Dispose(); + _surface = null; + } + + if (CompositionBrush != null) + { + CompositionBrush.Dispose(); + CompositionBrush = null; + } + } + } +} diff --git a/xaml-basics-starting-points/adaptive-layout/PhotoLab/MainPage.xaml b/xaml-basics-starting-points/adaptive-layout/PhotoLab/MainPage.xaml index f630095..3f24105 100644 --- a/xaml-basics-starting-points/adaptive-layout/PhotoLab/MainPage.xaml +++ b/xaml-basics-starting-points/adaptive-layout/PhotoLab/MainPage.xaml @@ -29,7 +29,7 @@ xmlns:local="using:PhotoLab" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" - xmlns:telerikInput="using:Telerik.UI.Xaml.Controls.Input" + xmlns:muxc="using:Microsoft.UI.Xaml.Controls" xmlns:animations="using:Microsoft.Toolkit.Uwp.UI.Animations" mc:Ignorable="d" NavigationCacheMode="Enabled" @@ -41,8 +41,7 @@ 8 0 0 - 16 - 32 + 16 @@ -96,21 +95,8 @@ Style="{StaticResource CaptionTextBlockStyle}" Margin="8,0,0,0" /> - - - - - - - - - - - - + @@ -123,11 +109,11 @@ Value="{StaticResource LargeItemMargin}" /> - - - - - + + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + + + - - + + - - + + + - - + + - - + + + + + + + + + + + Grid.Row="0" + Content="{TemplateBinding Header}" + ContentTemplate="{TemplateBinding HeaderTemplate}" + FontWeight="{ThemeResource SliderHeaderThemeFontWeight}" + Foreground="{ThemeResource SliderHeaderForeground}" + Margin="{ThemeResource SliderTopHeaderMargin}" + TextWrapping="Wrap" + Visibility="Collapsed" + x:DeferLoadStrategy="Lazy"/> - + Grid.Row="1" + Background="{ThemeResource SliderContainerBackground}" + Control.IsTemplateFocusTarget="True"> + + + - + - + + + - + + Height="{ThemeResource SliderTrackThemeHeight}" + Grid.Row="1" + Grid.ColumnSpan="3" + contract7Present:RadiusX="{Binding CornerRadius, RelativeSource={RelativeSource TemplatedParent}, Converter={StaticResource TopLeftCornerRadiusDoubleValueConverter}}" + contract7Present:RadiusY="{Binding CornerRadius, RelativeSource={RelativeSource TemplatedParent}, Converter={StaticResource BottomRightCornerRadiusDoubleValueConverter}}" + contract7NotPresent:RadiusX="{Binding Source={ThemeResource ControlCornerRadius}, Converter={StaticResource TopLeftCornerRadiusDoubleValueConverter}}" + contract7NotPresent:RadiusY="{Binding Source={ThemeResource ControlCornerRadius}, Converter={StaticResource BottomRightCornerRadiusDoubleValueConverter}}" /> + + Visibility="Collapsed" + Fill="{ThemeResource SliderTickBarFill}" + Height="{ThemeResource SliderOutsideTickBarThemeHeight}" + VerticalAlignment="Bottom" + Margin="0,0,0,4" + Grid.ColumnSpan="3" /> + Visibility="Collapsed" + Fill="{ThemeResource SliderInlineTickBarFill}" + Height="{ThemeResource SliderTrackThemeHeight}" + Grid.Row="1" + Grid.ColumnSpan="3" /> + Visibility="Collapsed" + Fill="{ThemeResource SliderTickBarFill}" + Height="{ThemeResource SliderOutsideTickBarThemeHeight}" + VerticalAlignment="Top" + Margin="0,4,0,0" + Grid.Row="2" + Grid.ColumnSpan="3" /> + Style="{StaticResource SliderThumbStyle}" + DataContext="{TemplateBinding Value}" + Height="{ThemeResource SliderHorizontalThumbHeight}" + Width="{ThemeResource SliderHorizontalThumbWidth}" + Grid.Row="0" + Grid.RowSpan="3" + Grid.Column="1" + FocusVisualMargin="-14,-6,-14,-6" + AutomationProperties.AccessibilityView="Raw" /> - + + + - + - + + + Fill="{TemplateBinding Background}" + Width="{ThemeResource SliderTrackThemeHeight}" + Grid.Column="1" + Grid.RowSpan="3" + contract7Present:RadiusX="{Binding CornerRadius, RelativeSource={RelativeSource TemplatedParent}, Converter={StaticResource TopLeftCornerRadiusDoubleValueConverter}}" + contract7Present:RadiusY="{Binding CornerRadius, RelativeSource={RelativeSource TemplatedParent}, Converter={StaticResource BottomRightCornerRadiusDoubleValueConverter}}" + contract7NotPresent:RadiusX="{Binding Source={ThemeResource ControlCornerRadius}, Converter={StaticResource TopLeftCornerRadiusDoubleValueConverter}}" + contract7NotPresent:RadiusY="{Binding Source={ThemeResource ControlCornerRadius}, Converter={StaticResource BottomRightCornerRadiusDoubleValueConverter}}" /> + Fill="{TemplateBinding Foreground}" + Grid.Column="1" + Grid.Row="2" + contract7Present:RadiusX="{Binding CornerRadius, RelativeSource={RelativeSource TemplatedParent}, Converter={StaticResource TopLeftCornerRadiusDoubleValueConverter}}" + contract7Present:RadiusY="{Binding CornerRadius, RelativeSource={RelativeSource TemplatedParent}, Converter={StaticResource BottomRightCornerRadiusDoubleValueConverter}}" + contract7NotPresent:RadiusX="{Binding Source={ThemeResource ControlCornerRadius}, Converter={StaticResource TopLeftCornerRadiusDoubleValueConverter}}" + contract7NotPresent:RadiusY="{Binding Source={ThemeResource ControlCornerRadius}, Converter={StaticResource BottomRightCornerRadiusDoubleValueConverter}}" /> + Visibility="Collapsed" + Fill="{ThemeResource SliderTickBarFill}" + Width="{ThemeResource SliderOutsideTickBarThemeHeight}" + HorizontalAlignment="Right" + Margin="0,0,4,0" + Grid.RowSpan="3" /> + Visibility="Collapsed" + Fill="{ThemeResource SliderInlineTickBarFill}" + Width="{ThemeResource SliderTrackThemeHeight}" + Grid.Column="1" + Grid.RowSpan="3" /> + Visibility="Collapsed" + Fill="{ThemeResource SliderTickBarFill}" + Width="{ThemeResource SliderOutsideTickBarThemeHeight}" + HorizontalAlignment="Left" + Margin="4,0,0,0" + Grid.Column="2" + Grid.RowSpan="3" /> + Style="{StaticResource SliderThumbStyle}" + DataContext="{TemplateBinding Value}" + Width="{ThemeResource SliderVerticalThumbWidth}" + Height="{ThemeResource SliderVerticalThumbHeight}" + Grid.Row="1" + Grid.Column="0" + Grid.ColumnSpan="3" + FocusVisualMargin="-6,-14,-6,-14" + AutomationProperties.AccessibilityView="Raw" /> @@ -397,7 +372,7 @@ - + - + + + + + - - @@ -489,6 +476,7 @@ + - - - - - - - - - - - - + + + Style="{StaticResource ValueTextBlock}" + Text="0.00" /> + - @@ -641,53 +613,5 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/xaml-basics-starting-points/data-binding/PhotoLab/DetailPage.xaml.cs b/xaml-basics-starting-points/data-binding/PhotoLab/DetailPage.xaml.cs index e91a9ce..df90066 100644 --- a/xaml-basics-starting-points/data-binding/PhotoLab/DetailPage.xaml.cs +++ b/xaml-basics-starting-points/data-binding/PhotoLab/DetailPage.xaml.cs @@ -23,20 +23,18 @@ // --------------------------------------------------------------------------------- using Microsoft.Graphics.Canvas; -using Microsoft.Graphics.Canvas.Effects; using System; using System.Collections.Generic; using System.Globalization; -using System.Numerics; +using System.Threading.Tasks; using Windows.Storage; using Windows.Storage.Pickers; using Windows.Storage.Streams; -using Windows.UI.Composition; using Windows.UI.Core; using Windows.UI.Xaml.Controls; using Windows.UI.Xaml.Controls.Primitives; -using Windows.UI.Xaml.Hosting; using Windows.UI.Xaml.Media.Animation; +using Windows.UI.Xaml.Media.Imaging; using Windows.UI.Xaml.Navigation; namespace PhotoLab @@ -44,42 +42,47 @@ namespace PhotoLab public sealed partial class DetailPage : Page { ImageFileInfo item; - Compositor compositor; - CompositionEffectBrush combinedBrush; CultureInfo culture = CultureInfo.CurrentCulture; - ContrastEffect contrastEffect; - ExposureEffect exposureEffect; - TemperatureAndTintEffect temperatureAndTintEffect; - GaussianBlurEffect graphicsEffect; - SaturationEffect saturationEffect; - bool editingInitialized = false; bool canNavigateWithUnsavedChanges = false; + BitmapImage ImageSource = null; public DetailPage() { this.InitializeComponent(); } - protected override void OnNavigatedTo(NavigationEventArgs e) + protected override async void OnNavigatedTo(NavigationEventArgs e) { item = e.Parameter as ImageFileInfo; canNavigateWithUnsavedChanges = false; - ResetEffects(); + ImageSource = await item.GetImageSourceAsync(); if (item != null) { - item.PropertyChanged += (s, e2) => UpdateEffectBrush(e2.PropertyName); - targetImage.Source = item.ImageSource; ConnectedAnimation imageAnimation = ConnectedAnimationService.GetForCurrentView().GetAnimation("itemAnimation"); if (imageAnimation != null) { - imageAnimation.Completed += (s, e_) => + targetImage.Source = ImageSource; + + imageAnimation.Completed += async (s, e_) => { - MainImage.Source = item.ImageSource; + await LoadBrushAsync(); + // This delay prevents a flicker that can happen when + // a large image is being loaded to the brush. It gives + // the image time to finish loading. (200 is ok, 400 to be safe.) + await Task.Delay(400); targetImage.Source = null; }; imageAnimation.TryStart(targetImage); } + + if (ImageSource.PixelHeight == 0 && ImageSource.PixelWidth == 0) + { + // There is no editable image loaded. Disable zoom and edit + // to prevent other errors. + EditButton.IsEnabled = false; + ZoomButton.IsEnabled = false; + }; } else { @@ -116,8 +119,11 @@ protected override void OnNavigatingFrom(NavigatingCancelEventArgs e) } else { + if (e.NavigationMode == NavigationMode.Back) + { + ConnectedAnimationService.GetForCurrentView().PrepareToAnimate("backAnimation", imgRect); + } canNavigateWithUnsavedChanges = false; - ConnectedAnimationService.GetForCurrentView().PrepareToAnimate("backAnimation", MainImage); base.OnNavigatingFrom(e); } } @@ -140,7 +146,8 @@ private async void ShowSaveDialog(NavigatingCancelEventArgs e) { // The user decided to leave the page. Restart // the navigation attempt. - canNavigateWithUnsavedChanges = true; + canNavigateWithUnsavedChanges = true; + ResetEffects(); Frame.Navigate(e.SourcePageType, e.Parameter); } } @@ -160,8 +167,8 @@ private void MainImageScroller_ViewChanged(object sender, ScrollViewerViewChange private void FitToScreen() { - var zoomFactor = (float)Math.Min(MainImageScroller.ActualWidth / item.ImageSource.PixelWidth, - MainImageScroller.ActualHeight / item.ImageSource.PixelHeight); + var zoomFactor = (float)Math.Min(MainImageScroller.ActualWidth / item.ImageProperties.Width, + MainImageScroller.ActualHeight / item.ImageProperties.Height); MainImageScroller.ChangeView(null, null, zoomFactor); } @@ -182,125 +189,33 @@ private void UpdateZoomState() } } - private void InitializeEffects() - { - saturationEffect = new SaturationEffect() - { - Name = "SaturationEffect", - Saturation = item.Saturation, - Source = new CompositionEffectSourceParameter("Backdrop") - }; - contrastEffect = new ContrastEffect() - { - Name = "ContrastEffect", - Contrast = item.Contrast, - Source = saturationEffect - }; - exposureEffect = new ExposureEffect() - { - Name = "ExposureEffect", - Source = contrastEffect, - Exposure = item.Exposure, - }; - temperatureAndTintEffect = new TemperatureAndTintEffect() - { - Name = "TemperatureAndTintEffect", - Source = exposureEffect, - Temperature = item.Temperature, - Tint = item.Tint - }; - graphicsEffect = new GaussianBlurEffect() - { - Name = "Blur", - Source = temperatureAndTintEffect, - BlurAmount = item.Blur, - BorderMode = EffectBorderMode.Hard, - }; - } - - private void InitializeCompositor() - { - compositor = ElementCompositionPreview.GetElementVisual(this).Compositor; - InitializeEffects(); - MainImage.Source = item.ImageSource; - MainImage.InvalidateArrange(); - - var destinationBrush = compositor.CreateBackdropBrush(); - - var graphicsEffectFactory = compositor.CreateEffectFactory(graphicsEffect, new[] { - "SaturationEffect.Saturation", "ExposureEffect.Exposure", "Blur.BlurAmount", - "TemperatureAndTintEffect.Temperature", "TemperatureAndTintEffect.Tint", - "ContrastEffect.Contrast" }); - combinedBrush = graphicsEffectFactory.CreateBrush(); - combinedBrush.SetSourceParameter("Backdrop", destinationBrush); - - var effectSprite = compositor.CreateSpriteVisual(); - effectSprite.Size = new Vector2((float)item.ImageSource.PixelWidth, (float)item.ImageSource.PixelHeight); - effectSprite.Brush = combinedBrush; - ElementCompositionPreview.SetElementChildVisual(MainImage, effectSprite); - - editingInitialized = true; - } - private void ToggleEditState() { - if (MainSplitView.IsPaneOpen) - { - MainSplitView.IsPaneOpen = false; - } - else - { - if (!editingInitialized) - { - InitializeCompositor(); - } - MainSplitView.IsPaneOpen = true; - } - } - - private void UpdateEffectBrush(string propertyName) - { - void update(string effectName, float effectValue) => - combinedBrush?.Properties.InsertScalar(effectName, effectValue); - - switch (propertyName) - { - case nameof(item.Exposure): update("ExposureEffect.Exposure", item.Exposure); break; - case nameof(item.Temperature): update("TemperatureAndTintEffect.Temperature", item.Temperature); break; - case nameof(item.Tint): update("TemperatureAndTintEffect.Tint", item.Tint); break; - case nameof(item.Contrast): update("ContrastEffect.Contrast", item.Contrast); break; - case nameof(item.Saturation): update("SaturationEffect.Saturation", item.Saturation); break; - case nameof(item.Blur): update("Blur.BlurAmount", item.Blur); break; - default: break; - } + MainSplitView.IsPaneOpen = !MainSplitView.IsPaneOpen; } private async void ExportImage() { CanvasDevice device = CanvasDevice.GetSharedDevice(); using (CanvasRenderTarget offscreen = new CanvasRenderTarget( - device, item.ImageSource.PixelWidth, item.ImageSource.PixelHeight, 96)) + device, item.ImageProperties.Width, item.ImageProperties.Height, 96)) { using (IRandomAccessStream stream = await item.ImageFile.OpenReadAsync()) using (CanvasBitmap image = await CanvasBitmap.LoadAsync(offscreen, stream, 96)) { - saturationEffect.Source = image; + ImageEffectsBrush.SetSource(image); + using (CanvasDrawingSession ds = offscreen.CreateDrawingSession()) { ds.Clear(Windows.UI.Colors.Black); - // Need to copy the value of each effect setting. - contrastEffect.Contrast = item.Contrast; - exposureEffect.Exposure = item.Exposure; - temperatureAndTintEffect.Temperature = item.Temperature; - temperatureAndTintEffect.Tint = item.Tint; - saturationEffect.Saturation = item.Saturation; - graphicsEffect.BlurAmount = item.Blur; - ds.DrawImage(graphicsEffect); + var img = ImageEffectsBrush.Image; + ds.DrawImage(img); } var fileSavePicker = new FileSavePicker() { + SuggestedStartLocation = PickerLocationId.PicturesLibrary, SuggestedSaveFile = item.ImageFile }; @@ -315,24 +230,78 @@ private async void ExportImage() await offscreen.SaveAsync(outStream, CanvasBitmapFileFormat.Jpeg); } - ResetEffects(); - var newItem = await MainPage.LoadImageInfo(outputFile); + // Check whether this save is overwriting the original image. + // If it is, replace it in the list. Otherwise, insert it as a copy. + bool replace = false; + if (outputFile.IsEqual(item.ImageFile)) + { + replace = true; + } - if (outputFile.Path == item.ImageFile.Path) + try { - item.ImageSource = newItem.ImageSource; + await LoadSavedImageAsync(outputFile, replace); } - else + catch (Exception ex) { - item = newItem; + if (ex.Message.Contains("0x80070323")) + { + // The handle with which this oplock was associated has been closed. + // The oplock is now broken. (Exception from HRESULT: 0x80070323) + // This is a temporary condition, so just try again. + await LoadSavedImageAsync(outputFile, replace); + } } - - MainImage.Source = item.ImageSource; } } } } + private async Task LoadSavedImageAsync(StorageFile imageFile, bool replaceImage) + { + item.NeedsSaved = false; + var newItem = await MainPage.LoadImageInfo(imageFile); + ResetEffects(); + + // Get the index of the original image so we can + // insert the saved image in the same place. + var index = MainPage.Current.Images.IndexOf(item); + + item = newItem; + this.Bindings.Update(); + + UnloadObject(imgRect); + FindName("imgRect"); + await LoadBrushAsync(); + + // Insert the saved image into the collection. + if (replaceImage == true) + { + MainPage.Current.Images.RemoveAt(index); + MainPage.Current.Images.Insert(index, item); + } + else + { + MainPage.Current.Images.Insert(index+1, item); + } + + + // Replace the persisted image used for connected animation. + MainPage.Current.UpdatePersistedItem(item); + + // Update BitMapImage used for small picture. + ImageSource = await item.GetImageSourceAsync(); + SmallImage.Source = ImageSource; + } + + private async Task LoadBrushAsync() + { + using (IRandomAccessStream fileStream = await item.ImageFile.OpenReadAsync()) + { + ImageEffectsBrush.LoadImageFromStream(fileStream); + } + } + private void ResetEffects() { item.Exposure = @@ -343,5 +312,9 @@ private void ResetEffects() item.Saturation = 1; } + private void SmallImage_Loaded(object sender, Windows.UI.Xaml.RoutedEventArgs e) + { + SmallImage.Source = ImageSource; + } } } diff --git a/xaml-basics-starting-points/data-binding/PhotoLab/ImageFileInfo.cs b/xaml-basics-starting-points/data-binding/PhotoLab/ImageFileInfo.cs index b3fc945..6e5220c 100644 --- a/xaml-basics-starting-points/data-binding/PhotoLab/ImageFileInfo.cs +++ b/xaml-basics-starting-points/data-binding/PhotoLab/ImageFileInfo.cs @@ -25,16 +25,17 @@ using System; using System.ComponentModel; using System.Runtime.CompilerServices; +using System.Threading.Tasks; using Windows.Storage; using Windows.Storage.FileProperties; +using Windows.Storage.Streams; using Windows.UI.Xaml.Media.Imaging; namespace PhotoLab { public class ImageFileInfo : INotifyPropertyChanged { - public ImageFileInfo(ImageProperties properties, StorageFile imageFile, - BitmapImage src, string name, string type) + public ImageFileInfo(ImageProperties properties, StorageFile imageFile, BitmapImage src, string name, string type) { ImageProperties = properties; ImageSource = src; @@ -50,6 +51,29 @@ public ImageFileInfo(ImageProperties properties, StorageFile imageFile, public ImageProperties ImageProperties { get; } + public async Task GetImageSourceAsync() + { + using (IRandomAccessStream fileStream = await ImageFile.OpenReadAsync()) + { + // Create a bitmap to be the image source. + BitmapImage bitmapImage = new BitmapImage(); + bitmapImage.SetSource(fileStream); + + return bitmapImage; + } + } + + public async Task GetImageThumbnailAsync() + { + var thumbnail = await ImageFile.GetThumbnailAsync(ThumbnailMode.PicturesView); + // Create a bitmap to be the image source. + BitmapImage bitmapImage = new BitmapImage(); + bitmapImage.SetSource(thumbnail); + thumbnail.Dispose(); + + return bitmapImage; + } + private BitmapImage _imageSource = null; public BitmapImage ImageSource { @@ -61,7 +85,7 @@ public BitmapImage ImageSource public string ImageFileType { get; } - public string ImageDimensions => $"{ImageSource.PixelWidth} x {ImageSource.PixelHeight}"; + public string ImageDimensions => $"{ImageProperties.Width} x {ImageProperties.Height}"; public string ImageTitle { diff --git a/xaml-basics-starting-points/data-binding/PhotoLab/LoadedImageBrush.cs b/xaml-basics-starting-points/data-binding/PhotoLab/LoadedImageBrush.cs new file mode 100644 index 0000000..2712497 --- /dev/null +++ b/xaml-basics-starting-points/data-binding/PhotoLab/LoadedImageBrush.cs @@ -0,0 +1,276 @@ +using Microsoft.Graphics.Canvas; +using Microsoft.Graphics.Canvas.Brushes; +using Microsoft.Graphics.Canvas.Effects; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Windows.Foundation; +using Windows.Graphics.Effects; +using Windows.UI.Composition; +using Windows.UI.Xaml; +using Windows.UI.Xaml.Media; +using Windows.Storage.Streams; + +namespace PhotoLab +{ + class LoadedImageBrush : XamlCompositionBrushBase + { + private bool IsImageLoading = false; + private LoadedImageSurface _surface; + CompositionEffectBrush combinedBrush; + ContrastEffect contrastEffect; + ExposureEffect exposureEffect; + TemperatureAndTintEffect temperatureAndTintEffect; + GaussianBlurEffect graphicsEffect; + SaturationEffect saturationEffect; + + public ICanvasImage Image + { + get + { + contrastEffect.Contrast = (float)ContrastAmount; + exposureEffect.Exposure = (float)ExposureAmount; + temperatureAndTintEffect.Tint = (float)TintAmount; + temperatureAndTintEffect.Temperature = (float)TemperatureAmount; + saturationEffect.Saturation = (float)SaturationAmount; + graphicsEffect.BlurAmount = (float)BlurAmount; + return graphicsEffect; + } + } + + public void SetSource(ICanvasImage source) + { + saturationEffect.Source = source; + } + + public static readonly DependencyProperty BlurAmountProperty = DependencyProperty.Register( + "BlurAmount", + typeof(double), + typeof(LoadedImageBrush), + new PropertyMetadata(0.0, new PropertyChangedCallback(OnBlurAmountChanged) + ) +); + + public double BlurAmount + { + get { return (double)GetValue(BlurAmountProperty); } + set { SetValue(BlurAmountProperty, value); } + } + + private static void OnBlurAmountChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) + { + var brush = (LoadedImageBrush)d; + // Unbox and set a new blur amount if the CompositionBrush exists. + brush.CompositionBrush?.Properties.InsertScalar("Blur.BlurAmount", (float)(double)e.NewValue); + } + + + + public static readonly DependencyProperty ContrastAmountProperty = DependencyProperty.Register( +"ContrastAmount", +typeof(double), +typeof(LoadedImageBrush), +new PropertyMetadata(0.0, new PropertyChangedCallback(OnContrastAmountChanged) +) +); + + public double ContrastAmount + { + get { return (double)GetValue(ContrastAmountProperty); } + set { SetValue(ContrastAmountProperty, value); } + } + + private static void OnContrastAmountChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) + { + var brush = (LoadedImageBrush)d; + // Unbox and set a new blur amount if the CompositionBrush exists. + brush.CompositionBrush?.Properties.InsertScalar("ContrastEffect.Contrast", (float)(double)e.NewValue); + } + + public static readonly DependencyProperty SaturationAmountProperty = DependencyProperty.Register( +"SaturationAmount", +typeof(double), +typeof(LoadedImageBrush), +new PropertyMetadata(1.0, new PropertyChangedCallback(OnSaturationAmountChanged) +) +); + + public double SaturationAmount + { + get { return (double)GetValue(SaturationAmountProperty); } + set { SetValue(SaturationAmountProperty, value); } + } + + private static void OnSaturationAmountChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) + { + var brush = (LoadedImageBrush)d; + // Unbox and set a new blur amount if the CompositionBrush exists. + brush.CompositionBrush?.Properties.InsertScalar("SaturationEffect.Saturation", (float)(double)e.NewValue); + } + + public static readonly DependencyProperty ExposureAmountProperty = DependencyProperty.Register( +"ExposureAmount", +typeof(double), +typeof(LoadedImageBrush), +new PropertyMetadata(0.0, new PropertyChangedCallback(OnExposureAmountChanged) +) +); + + public double ExposureAmount + { + get { return (double)GetValue(ExposureAmountProperty); } + set { SetValue(ExposureAmountProperty, value); } + } + + private static void OnExposureAmountChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) + { + var brush = (LoadedImageBrush)d; + // Unbox and set a new blur amount if the CompositionBrush exists. + brush.CompositionBrush?.Properties.InsertScalar("ExposureEffect.Exposure", (float)(double)e.NewValue); + } + + + public static readonly DependencyProperty TintAmountProperty = DependencyProperty.Register( +"TintAmount", +typeof(double), +typeof(LoadedImageBrush), +new PropertyMetadata(0.0, new PropertyChangedCallback(OnTintAmountChanged) +) +); + + public double TintAmount + { + get { return (double)GetValue(TintAmountProperty); } + set { SetValue(TintAmountProperty, value); } + } + + private static void OnTintAmountChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) + { + var brush = (LoadedImageBrush)d; + // Unbox and set a new blur amount if the CompositionBrush exists. + brush.CompositionBrush?.Properties.InsertScalar("TemperatureAndTintEffect.Tint", (float)(double)e.NewValue); + } + + public static readonly DependencyProperty TemperatureAmountProperty = DependencyProperty.Register( +"TemperatureAmount", +typeof(double), +typeof(LoadedImageBrush), +new PropertyMetadata(0.0, new PropertyChangedCallback(OnTemperatureAmountChanged) +) +); + + public double TemperatureAmount + { + get { return (double)GetValue(TemperatureAmountProperty); } + set { SetValue(TemperatureAmountProperty, value); } + } + + private static void OnTemperatureAmountChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) + { + var brush = (LoadedImageBrush)d; + // Unbox and set a new blur amount if the CompositionBrush exists. + brush.CompositionBrush?.Properties.InsertScalar("TemperatureAndTintEffect.Temperature", (float)(double)e.NewValue); + } + + public LoadedImageBrush() + { + } + + public void LoadImageFromPath(string path) + { + var compositor = Window.Current.Compositor; + // Load image + _surface = LoadedImageSurface.StartLoadFromUri(new Uri(path)); + _surface.LoadCompleted += Load_Completed; + } + + public void LoadImageFromStream(IRandomAccessStream stream) + { + if (stream != null && IsImageLoading == false) + { + var compositor = Window.Current.Compositor; + // Load image + IsImageLoading = true; + _surface = LoadedImageSurface.StartLoadFromStream(stream); + _surface.LoadCompleted += Load_Completed; + } + } + + private void Load_Completed(LoadedImageSurface sender, LoadedImageSourceLoadCompletedEventArgs e) + { + IsImageLoading = false; + + if (e.Status == LoadedImageSourceLoadStatus.Success) + { + var compositor = Window.Current.Compositor; + var brush = compositor.CreateSurfaceBrush(_surface); + brush.Stretch = CompositionStretch.UniformToFill; + + // Create effects chain. + saturationEffect = new SaturationEffect() + { + Name = "SaturationEffect", + Saturation = (float)SaturationAmount, + Source = new CompositionEffectSourceParameter("image") + }; + contrastEffect = new ContrastEffect() + { + Name = "ContrastEffect", + Contrast = (float)ContrastAmount, + Source = saturationEffect + }; + exposureEffect = new ExposureEffect() + { + Name = "ExposureEffect", + Source = contrastEffect, + Exposure = (float)ExposureAmount, + }; + temperatureAndTintEffect = new TemperatureAndTintEffect() + { + Name = "TemperatureAndTintEffect", + Source = exposureEffect, + Temperature = (float)TemperatureAmount, + Tint = (float)TintAmount + }; + graphicsEffect = new GaussianBlurEffect() + { + Name = "Blur", + Source = temperatureAndTintEffect, + BlurAmount = (float)BlurAmount, + BorderMode = EffectBorderMode.Hard, + }; + + var graphicsEffectFactory = compositor.CreateEffectFactory(graphicsEffect, new[] { + "SaturationEffect.Saturation", "ExposureEffect.Exposure", "Blur.BlurAmount", + "TemperatureAndTintEffect.Temperature", "TemperatureAndTintEffect.Tint", + "ContrastEffect.Contrast" }); + combinedBrush = graphicsEffectFactory.CreateBrush(); + combinedBrush.SetSourceParameter("image", brush); + + // Composition Brush is what is being applied to the UI Element. + CompositionBrush = combinedBrush; + } + else + { + LoadImageFromPath("ms-appx:///Assets/StoreLogo.png"); + } + } + + protected override void OnDisconnected() + { + if (_surface != null) + { + _surface.Dispose(); + _surface = null; + } + + if (CompositionBrush != null) + { + CompositionBrush.Dispose(); + CompositionBrush = null; + } + } + } +} diff --git a/xaml-basics-starting-points/data-binding/PhotoLab/MainPage.xaml b/xaml-basics-starting-points/data-binding/PhotoLab/MainPage.xaml index 2f89360..7353d30 100644 --- a/xaml-basics-starting-points/data-binding/PhotoLab/MainPage.xaml +++ b/xaml-basics-starting-points/data-binding/PhotoLab/MainPage.xaml @@ -29,7 +29,7 @@ xmlns:local="using:PhotoLab" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" - xmlns:telerikInput="using:Telerik.UI.Xaml.Controls.Input" + xmlns:muxc="using:Microsoft.UI.Xaml.Controls" xmlns:animations="using:Microsoft.Toolkit.Uwp.UI.Animations" mc:Ignorable="d" NavigationCacheMode="Enabled"> @@ -41,13 +41,11 @@ 8 0 0 - 16 - 32 + 16 @@ -75,9 +72,9 @@ - + - - - - - - - - - - - - + @@ -138,7 +122,7 @@ Flyout="{StaticResource zoomFlyout}" /> - Images { get; } = new ObservableCollection(); @@ -49,6 +49,14 @@ public sealed partial class MainPage : Page public MainPage() { this.InitializeComponent(); + Current = this; + } + + // If the image is edited and saved in the details page, this method gets called + // so that the back navigation connected animation uses the correct image. + public void UpdatePersistedItem(ImageFileInfo item) + { + persistedItem = item; } protected async override void OnNavigatedTo(NavigationEventArgs e) @@ -56,9 +64,6 @@ protected async override void OnNavigatedTo(NavigationEventArgs e) SystemNavigationManager.GetForCurrentView().AppViewBackButtonVisibility = AppViewBackButtonVisibility.Collapsed; - // Remove this when replaced with XAML bindings - ImageGridView.ItemsSource = Images; - if (Images.Count == 0) { await GetItemsAsync(); @@ -84,27 +89,54 @@ private async void StartConnectedAnimationForBackNavigation() private async Task GetItemsAsync() { - // https://docs.microsoft.com/uwp/api/windows.ui.xaml.controls.image#Windows_UI_Xaml_Controls_Image_Source - // See "Using a stream source to show images from the Pictures library". - // This code is modified to get images from the app folder. + QueryOptions options = new QueryOptions(); + options.FolderDepth = FolderDepth.Deep; + options.FileTypeFilter.Add(".jpg"); + options.FileTypeFilter.Add(".png"); + options.FileTypeFilter.Add(".gif"); - // Get the app folder where the images are stored. + // Get the Pictures library. (Requires 'Pictures Library' capability.) + //Windows.Storage.StorageFolder picturesFolder = Windows.Storage.KnownFolders.PicturesLibrary; + // OR + // Get the Sample pictures. StorageFolder appInstalledFolder = Package.Current.InstalledLocation; - StorageFolder assets = await appInstalledFolder.GetFolderAsync("Assets\\Samples"); + StorageFolder picturesFolder = await appInstalledFolder.GetFolderAsync("Assets\\Samples"); + + var result = picturesFolder.CreateFileQueryWithOptions(options); - // Get and process files in folder - IReadOnlyList fileList = await assets.GetFilesAsync(); - foreach (StorageFile file in fileList) + IReadOnlyList imageFiles = await result.GetFilesAsync(); + bool unsupportedFilesFound = false; + foreach (StorageFile file in imageFiles) { - // Limit to only png or jpg files. - if (file.ContentType == "image/png" || file.ContentType == "image/jpeg") + // Only files on the local computer are supported. + // Files on OneDrive or a network location are excluded. + if (file.Provider.Id == "computer") { Images.Add(await LoadImageInfo(file)); } + else + { + unsupportedFilesFound = true; + } } + + if (unsupportedFilesFound == true) + { + ContentDialog unsupportedFilesDialog = new ContentDialog + { + Title = "Unsupported images found", + Content = "This sample app only supports images stored locally on the computer. We found files in your library that are stored in OneDrive or another network location. We didn't load those images.", + CloseButtonText = "Ok" + }; + + ContentDialogResult resultNotUsed = await unsupportedFilesDialog.ShowAsync(); + } + + // Remove this when replaced with XAML bindings + ImageGridView.ItemsSource = Images; } - public async static Task LoadImageInfo(StorageFile file) + public async static Task LoadImageInfo(StorageFile file) { // Open a stream for the selected file. // The 'using' block ensures the stream is disposed @@ -120,9 +152,8 @@ public async static Task LoadImageInfo(StorageFile file) properties, file, bitmapImage, file.DisplayName, file.DisplayType); - return info; + return info; } } - } } diff --git a/xaml-basics-starting-points/data-binding/PhotoLab/MobileScreenTrigger.cs b/xaml-basics-starting-points/data-binding/PhotoLab/MobileScreenTrigger.cs deleted file mode 100644 index 80caf29..0000000 --- a/xaml-basics-starting-points/data-binding/PhotoLab/MobileScreenTrigger.cs +++ /dev/null @@ -1,71 +0,0 @@ -// --------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// -// The MIT License (MIT) -// -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in -// all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -// THE SOFTWARE. -// --------------------------------------------------------------------------------- - -using Windows.System.Profile; -using Windows.UI.ViewManagement; -using Windows.UI.Xaml; - -namespace PhotoLab -{ - /// - /// Custom trigger for Mobile device UI states. - /// This trigger is active when the app runs on a mobile device and the - /// UserInteractionMode is touch, which indicates that the app is showing - /// on the device screen. When UserInteractionMode is mouse, the app is - /// using Continuum to show on a larger screen. - /// https://blogs.windows.com/buildingapps/2015/12/07/optimizing-apps-for-continuum-for-phone/#Yubo3bUdIM4H6Vle.97 - /// - public class MobileScreenTrigger : StateTriggerBase - { - public MobileScreenTrigger() - { - Window.Current.SizeChanged += (s, e) => UpdateTrigger(); - } - - /// - /// The target device family. - /// - public UserInteractionMode InteractionMode - { - get => _interactionMode; - set - { - _interactionMode = value; - UpdateTrigger(); - } - } - private UserInteractionMode _interactionMode; - - private void UpdateTrigger() - { - // Get the current device family and interaction mode. - var currentDeviceFamily = AnalyticsInfo.VersionInfo.DeviceFamily; - var currentInteractionMode = UIViewSettings.GetForCurrentView().UserInteractionMode; - - // The trigger will be activated if the current device family is Windows.Mobile - // and the UserInteractionMode matches the interaction mode value in XAML. - SetActive(InteractionMode == currentInteractionMode && currentDeviceFamily == "Windows.Mobile"); - } - } -} diff --git a/xaml-basics-starting-points/data-binding/PhotoLab/PhotoLab.csproj b/xaml-basics-starting-points/data-binding/PhotoLab/PhotoLab.csproj index c3b3f5c..097822e 100644 --- a/xaml-basics-starting-points/data-binding/PhotoLab/PhotoLab.csproj +++ b/xaml-basics-starting-points/data-binding/PhotoLab/PhotoLab.csproj @@ -11,7 +11,7 @@ PhotoLab en-US UAP - 10.0.17763.0 + 10.0.19041.0 10.0.17763.0 14 512 @@ -96,10 +96,10 @@ DetailPage.xaml + MainPage.xaml - @@ -158,8 +158,8 @@ 1.5.1 - - 1.0.0.4 + + 2.4.2 diff --git a/xaml-basics-starting-points/style/PhotoLab/App.xaml b/xaml-basics-starting-points/style/PhotoLab/App.xaml index 3b45103..83f9e6b 100644 --- a/xaml-basics-starting-points/style/PhotoLab/App.xaml +++ b/xaml-basics-starting-points/style/PhotoLab/App.xaml @@ -27,9 +27,16 @@ xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"> - - 0 - 641 - 1008 + + + + + + + + 0 + 641 + 1008 + diff --git a/xaml-basics-starting-points/style/PhotoLab/DetailPage.xaml b/xaml-basics-starting-points/style/PhotoLab/DetailPage.xaml index 91beabb..c00982e 100644 --- a/xaml-basics-starting-points/style/PhotoLab/DetailPage.xaml +++ b/xaml-basics-starting-points/style/PhotoLab/DetailPage.xaml @@ -26,7 +26,7 @@ xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:local="using:PhotoLab" - xmlns:telerikInput="using:Telerik.UI.Xaml.Controls.Input" + xmlns:muxc="using:Microsoft.UI.Xaml.Controls" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" mc:Ignorable="d" @@ -61,9 +61,9 @@ - + - + + + + + - - @@ -144,6 +155,7 @@ + - - - - - - - - - - - - + + - @@ -255,53 +255,5 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/xaml-basics-starting-points/style/PhotoLab/DetailPage.xaml.cs b/xaml-basics-starting-points/style/PhotoLab/DetailPage.xaml.cs index e91a9ce..d94f1a8 100644 --- a/xaml-basics-starting-points/style/PhotoLab/DetailPage.xaml.cs +++ b/xaml-basics-starting-points/style/PhotoLab/DetailPage.xaml.cs @@ -23,20 +23,18 @@ // --------------------------------------------------------------------------------- using Microsoft.Graphics.Canvas; -using Microsoft.Graphics.Canvas.Effects; using System; using System.Collections.Generic; using System.Globalization; -using System.Numerics; +using System.Threading.Tasks; using Windows.Storage; using Windows.Storage.Pickers; using Windows.Storage.Streams; -using Windows.UI.Composition; using Windows.UI.Core; using Windows.UI.Xaml.Controls; using Windows.UI.Xaml.Controls.Primitives; -using Windows.UI.Xaml.Hosting; using Windows.UI.Xaml.Media.Animation; +using Windows.UI.Xaml.Media.Imaging; using Windows.UI.Xaml.Navigation; namespace PhotoLab @@ -44,42 +42,47 @@ namespace PhotoLab public sealed partial class DetailPage : Page { ImageFileInfo item; - Compositor compositor; - CompositionEffectBrush combinedBrush; CultureInfo culture = CultureInfo.CurrentCulture; - ContrastEffect contrastEffect; - ExposureEffect exposureEffect; - TemperatureAndTintEffect temperatureAndTintEffect; - GaussianBlurEffect graphicsEffect; - SaturationEffect saturationEffect; - bool editingInitialized = false; bool canNavigateWithUnsavedChanges = false; + BitmapImage ImageSource = null; public DetailPage() { this.InitializeComponent(); } - protected override void OnNavigatedTo(NavigationEventArgs e) + protected override async void OnNavigatedTo(NavigationEventArgs e) { item = e.Parameter as ImageFileInfo; canNavigateWithUnsavedChanges = false; - ResetEffects(); + ImageSource = await item.GetImageSourceAsync(); if (item != null) { - item.PropertyChanged += (s, e2) => UpdateEffectBrush(e2.PropertyName); - targetImage.Source = item.ImageSource; ConnectedAnimation imageAnimation = ConnectedAnimationService.GetForCurrentView().GetAnimation("itemAnimation"); if (imageAnimation != null) { - imageAnimation.Completed += (s, e_) => + targetImage.Source = ImageSource; + + imageAnimation.Completed += async (s, e_) => { - MainImage.Source = item.ImageSource; + await LoadBrushAsync(); + // This delay prevents a flicker that can happen when + // a large image is being loaded to the brush. It gives + // the image time to finish loading. (200 is ok, 400 to be safe.) + await Task.Delay(400); targetImage.Source = null; }; imageAnimation.TryStart(targetImage); } + + if (ImageSource.PixelHeight == 0 && ImageSource.PixelWidth == 0) + { + // There is no editable image loaded. Disable zoom and edit + // to prevent other errors. + EditButton.IsEnabled = false; + ZoomButton.IsEnabled = false; + }; } else { @@ -116,8 +119,11 @@ protected override void OnNavigatingFrom(NavigatingCancelEventArgs e) } else { + if (e.NavigationMode == NavigationMode.Back) + { + ConnectedAnimationService.GetForCurrentView().PrepareToAnimate("backAnimation", imgRect); + } canNavigateWithUnsavedChanges = false; - ConnectedAnimationService.GetForCurrentView().PrepareToAnimate("backAnimation", MainImage); base.OnNavigatingFrom(e); } } @@ -140,7 +146,8 @@ private async void ShowSaveDialog(NavigatingCancelEventArgs e) { // The user decided to leave the page. Restart // the navigation attempt. - canNavigateWithUnsavedChanges = true; + canNavigateWithUnsavedChanges = true; + ResetEffects(); Frame.Navigate(e.SourcePageType, e.Parameter); } } @@ -160,8 +167,8 @@ private void MainImageScroller_ViewChanged(object sender, ScrollViewerViewChange private void FitToScreen() { - var zoomFactor = (float)Math.Min(MainImageScroller.ActualWidth / item.ImageSource.PixelWidth, - MainImageScroller.ActualHeight / item.ImageSource.PixelHeight); + var zoomFactor = (float)Math.Min(MainImageScroller.ActualWidth / item.ImageProperties.Width, + MainImageScroller.ActualHeight / item.ImageProperties.Height); MainImageScroller.ChangeView(null, null, zoomFactor); } @@ -182,125 +189,33 @@ private void UpdateZoomState() } } - private void InitializeEffects() - { - saturationEffect = new SaturationEffect() - { - Name = "SaturationEffect", - Saturation = item.Saturation, - Source = new CompositionEffectSourceParameter("Backdrop") - }; - contrastEffect = new ContrastEffect() - { - Name = "ContrastEffect", - Contrast = item.Contrast, - Source = saturationEffect - }; - exposureEffect = new ExposureEffect() - { - Name = "ExposureEffect", - Source = contrastEffect, - Exposure = item.Exposure, - }; - temperatureAndTintEffect = new TemperatureAndTintEffect() - { - Name = "TemperatureAndTintEffect", - Source = exposureEffect, - Temperature = item.Temperature, - Tint = item.Tint - }; - graphicsEffect = new GaussianBlurEffect() - { - Name = "Blur", - Source = temperatureAndTintEffect, - BlurAmount = item.Blur, - BorderMode = EffectBorderMode.Hard, - }; - } - - private void InitializeCompositor() - { - compositor = ElementCompositionPreview.GetElementVisual(this).Compositor; - InitializeEffects(); - MainImage.Source = item.ImageSource; - MainImage.InvalidateArrange(); - - var destinationBrush = compositor.CreateBackdropBrush(); - - var graphicsEffectFactory = compositor.CreateEffectFactory(graphicsEffect, new[] { - "SaturationEffect.Saturation", "ExposureEffect.Exposure", "Blur.BlurAmount", - "TemperatureAndTintEffect.Temperature", "TemperatureAndTintEffect.Tint", - "ContrastEffect.Contrast" }); - combinedBrush = graphicsEffectFactory.CreateBrush(); - combinedBrush.SetSourceParameter("Backdrop", destinationBrush); - - var effectSprite = compositor.CreateSpriteVisual(); - effectSprite.Size = new Vector2((float)item.ImageSource.PixelWidth, (float)item.ImageSource.PixelHeight); - effectSprite.Brush = combinedBrush; - ElementCompositionPreview.SetElementChildVisual(MainImage, effectSprite); - - editingInitialized = true; - } - private void ToggleEditState() { - if (MainSplitView.IsPaneOpen) - { - MainSplitView.IsPaneOpen = false; - } - else - { - if (!editingInitialized) - { - InitializeCompositor(); - } - MainSplitView.IsPaneOpen = true; - } - } - - private void UpdateEffectBrush(string propertyName) - { - void update(string effectName, float effectValue) => - combinedBrush?.Properties.InsertScalar(effectName, effectValue); - - switch (propertyName) - { - case nameof(item.Exposure): update("ExposureEffect.Exposure", item.Exposure); break; - case nameof(item.Temperature): update("TemperatureAndTintEffect.Temperature", item.Temperature); break; - case nameof(item.Tint): update("TemperatureAndTintEffect.Tint", item.Tint); break; - case nameof(item.Contrast): update("ContrastEffect.Contrast", item.Contrast); break; - case nameof(item.Saturation): update("SaturationEffect.Saturation", item.Saturation); break; - case nameof(item.Blur): update("Blur.BlurAmount", item.Blur); break; - default: break; - } + MainSplitView.IsPaneOpen = !MainSplitView.IsPaneOpen; } private async void ExportImage() { CanvasDevice device = CanvasDevice.GetSharedDevice(); using (CanvasRenderTarget offscreen = new CanvasRenderTarget( - device, item.ImageSource.PixelWidth, item.ImageSource.PixelHeight, 96)) + device, item.ImageProperties.Width, item.ImageProperties.Height, 96)) { using (IRandomAccessStream stream = await item.ImageFile.OpenReadAsync()) using (CanvasBitmap image = await CanvasBitmap.LoadAsync(offscreen, stream, 96)) { - saturationEffect.Source = image; + ImageEffectsBrush.SetSource(image); + using (CanvasDrawingSession ds = offscreen.CreateDrawingSession()) { ds.Clear(Windows.UI.Colors.Black); - // Need to copy the value of each effect setting. - contrastEffect.Contrast = item.Contrast; - exposureEffect.Exposure = item.Exposure; - temperatureAndTintEffect.Temperature = item.Temperature; - temperatureAndTintEffect.Tint = item.Tint; - saturationEffect.Saturation = item.Saturation; - graphicsEffect.BlurAmount = item.Blur; - ds.DrawImage(graphicsEffect); + var img = ImageEffectsBrush.Image; + ds.DrawImage(img); } var fileSavePicker = new FileSavePicker() { + SuggestedStartLocation = PickerLocationId.PicturesLibrary, SuggestedSaveFile = item.ImageFile }; @@ -315,24 +230,73 @@ private async void ExportImage() await offscreen.SaveAsync(outStream, CanvasBitmapFileFormat.Jpeg); } - ResetEffects(); - var newItem = await MainPage.LoadImageInfo(outputFile); + // Check whether this save is overwriting the original image. + // If it is, replace it in the list. Otherwise, insert it as a copy. + bool replace = false; + if (outputFile.IsEqual(item.ImageFile)) + { + replace = true; + } - if (outputFile.Path == item.ImageFile.Path) + try { - item.ImageSource = newItem.ImageSource; + await LoadSavedImageAsync(outputFile, replace); } - else + catch (Exception ex) { - item = newItem; + if (ex.Message.Contains("0x80070323")) + { + // The handle with which this oplock was associated has been closed. + // The oplock is now broken. (Exception from HRESULT: 0x80070323) + // This is a temporary condition, so just try again. + await LoadSavedImageAsync(outputFile, replace); + } } - - MainImage.Source = item.ImageSource; } } } } + private async Task LoadSavedImageAsync(StorageFile imageFile, bool replaceImage) + { + item.NeedsSaved = false; + var newItem = await MainPage.LoadImageInfo(imageFile); + ResetEffects(); + + // Get the index of the original image so we can + // insert the saved image in the same place. + var index = MainPage.Current.Images.IndexOf(item); + + item = newItem; + this.Bindings.Update(); + + UnloadObject(imgRect); + FindName("imgRect"); + await LoadBrushAsync(); + + // Insert the saved image into the collection. + if (replaceImage == true) + { + MainPage.Current.Images.RemoveAt(index); + MainPage.Current.Images.Insert(index, item); + } + else + { + MainPage.Current.Images.Insert(index+1, item); + } + + // Replace the persisted image used for connected animation. + MainPage.Current.UpdatePersistedItem(item); + } + + private async Task LoadBrushAsync() + { + using (IRandomAccessStream fileStream = await item.ImageFile.OpenReadAsync()) + { + ImageEffectsBrush.LoadImageFromStream(fileStream); + } + } + private void ResetEffects() { item.Exposure = @@ -342,6 +306,5 @@ private void ResetEffects() item.Contrast = 0; item.Saturation = 1; } - } } diff --git a/xaml-basics-starting-points/style/PhotoLab/ImageFileInfo.cs b/xaml-basics-starting-points/style/PhotoLab/ImageFileInfo.cs index b3fc945..6e5220c 100644 --- a/xaml-basics-starting-points/style/PhotoLab/ImageFileInfo.cs +++ b/xaml-basics-starting-points/style/PhotoLab/ImageFileInfo.cs @@ -25,16 +25,17 @@ using System; using System.ComponentModel; using System.Runtime.CompilerServices; +using System.Threading.Tasks; using Windows.Storage; using Windows.Storage.FileProperties; +using Windows.Storage.Streams; using Windows.UI.Xaml.Media.Imaging; namespace PhotoLab { public class ImageFileInfo : INotifyPropertyChanged { - public ImageFileInfo(ImageProperties properties, StorageFile imageFile, - BitmapImage src, string name, string type) + public ImageFileInfo(ImageProperties properties, StorageFile imageFile, BitmapImage src, string name, string type) { ImageProperties = properties; ImageSource = src; @@ -50,6 +51,29 @@ public ImageFileInfo(ImageProperties properties, StorageFile imageFile, public ImageProperties ImageProperties { get; } + public async Task GetImageSourceAsync() + { + using (IRandomAccessStream fileStream = await ImageFile.OpenReadAsync()) + { + // Create a bitmap to be the image source. + BitmapImage bitmapImage = new BitmapImage(); + bitmapImage.SetSource(fileStream); + + return bitmapImage; + } + } + + public async Task GetImageThumbnailAsync() + { + var thumbnail = await ImageFile.GetThumbnailAsync(ThumbnailMode.PicturesView); + // Create a bitmap to be the image source. + BitmapImage bitmapImage = new BitmapImage(); + bitmapImage.SetSource(thumbnail); + thumbnail.Dispose(); + + return bitmapImage; + } + private BitmapImage _imageSource = null; public BitmapImage ImageSource { @@ -61,7 +85,7 @@ public BitmapImage ImageSource public string ImageFileType { get; } - public string ImageDimensions => $"{ImageSource.PixelWidth} x {ImageSource.PixelHeight}"; + public string ImageDimensions => $"{ImageProperties.Width} x {ImageProperties.Height}"; public string ImageTitle { diff --git a/xaml-basics-starting-points/style/PhotoLab/LoadedImageBrush.cs b/xaml-basics-starting-points/style/PhotoLab/LoadedImageBrush.cs new file mode 100644 index 0000000..2712497 --- /dev/null +++ b/xaml-basics-starting-points/style/PhotoLab/LoadedImageBrush.cs @@ -0,0 +1,276 @@ +using Microsoft.Graphics.Canvas; +using Microsoft.Graphics.Canvas.Brushes; +using Microsoft.Graphics.Canvas.Effects; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Windows.Foundation; +using Windows.Graphics.Effects; +using Windows.UI.Composition; +using Windows.UI.Xaml; +using Windows.UI.Xaml.Media; +using Windows.Storage.Streams; + +namespace PhotoLab +{ + class LoadedImageBrush : XamlCompositionBrushBase + { + private bool IsImageLoading = false; + private LoadedImageSurface _surface; + CompositionEffectBrush combinedBrush; + ContrastEffect contrastEffect; + ExposureEffect exposureEffect; + TemperatureAndTintEffect temperatureAndTintEffect; + GaussianBlurEffect graphicsEffect; + SaturationEffect saturationEffect; + + public ICanvasImage Image + { + get + { + contrastEffect.Contrast = (float)ContrastAmount; + exposureEffect.Exposure = (float)ExposureAmount; + temperatureAndTintEffect.Tint = (float)TintAmount; + temperatureAndTintEffect.Temperature = (float)TemperatureAmount; + saturationEffect.Saturation = (float)SaturationAmount; + graphicsEffect.BlurAmount = (float)BlurAmount; + return graphicsEffect; + } + } + + public void SetSource(ICanvasImage source) + { + saturationEffect.Source = source; + } + + public static readonly DependencyProperty BlurAmountProperty = DependencyProperty.Register( + "BlurAmount", + typeof(double), + typeof(LoadedImageBrush), + new PropertyMetadata(0.0, new PropertyChangedCallback(OnBlurAmountChanged) + ) +); + + public double BlurAmount + { + get { return (double)GetValue(BlurAmountProperty); } + set { SetValue(BlurAmountProperty, value); } + } + + private static void OnBlurAmountChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) + { + var brush = (LoadedImageBrush)d; + // Unbox and set a new blur amount if the CompositionBrush exists. + brush.CompositionBrush?.Properties.InsertScalar("Blur.BlurAmount", (float)(double)e.NewValue); + } + + + + public static readonly DependencyProperty ContrastAmountProperty = DependencyProperty.Register( +"ContrastAmount", +typeof(double), +typeof(LoadedImageBrush), +new PropertyMetadata(0.0, new PropertyChangedCallback(OnContrastAmountChanged) +) +); + + public double ContrastAmount + { + get { return (double)GetValue(ContrastAmountProperty); } + set { SetValue(ContrastAmountProperty, value); } + } + + private static void OnContrastAmountChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) + { + var brush = (LoadedImageBrush)d; + // Unbox and set a new blur amount if the CompositionBrush exists. + brush.CompositionBrush?.Properties.InsertScalar("ContrastEffect.Contrast", (float)(double)e.NewValue); + } + + public static readonly DependencyProperty SaturationAmountProperty = DependencyProperty.Register( +"SaturationAmount", +typeof(double), +typeof(LoadedImageBrush), +new PropertyMetadata(1.0, new PropertyChangedCallback(OnSaturationAmountChanged) +) +); + + public double SaturationAmount + { + get { return (double)GetValue(SaturationAmountProperty); } + set { SetValue(SaturationAmountProperty, value); } + } + + private static void OnSaturationAmountChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) + { + var brush = (LoadedImageBrush)d; + // Unbox and set a new blur amount if the CompositionBrush exists. + brush.CompositionBrush?.Properties.InsertScalar("SaturationEffect.Saturation", (float)(double)e.NewValue); + } + + public static readonly DependencyProperty ExposureAmountProperty = DependencyProperty.Register( +"ExposureAmount", +typeof(double), +typeof(LoadedImageBrush), +new PropertyMetadata(0.0, new PropertyChangedCallback(OnExposureAmountChanged) +) +); + + public double ExposureAmount + { + get { return (double)GetValue(ExposureAmountProperty); } + set { SetValue(ExposureAmountProperty, value); } + } + + private static void OnExposureAmountChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) + { + var brush = (LoadedImageBrush)d; + // Unbox and set a new blur amount if the CompositionBrush exists. + brush.CompositionBrush?.Properties.InsertScalar("ExposureEffect.Exposure", (float)(double)e.NewValue); + } + + + public static readonly DependencyProperty TintAmountProperty = DependencyProperty.Register( +"TintAmount", +typeof(double), +typeof(LoadedImageBrush), +new PropertyMetadata(0.0, new PropertyChangedCallback(OnTintAmountChanged) +) +); + + public double TintAmount + { + get { return (double)GetValue(TintAmountProperty); } + set { SetValue(TintAmountProperty, value); } + } + + private static void OnTintAmountChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) + { + var brush = (LoadedImageBrush)d; + // Unbox and set a new blur amount if the CompositionBrush exists. + brush.CompositionBrush?.Properties.InsertScalar("TemperatureAndTintEffect.Tint", (float)(double)e.NewValue); + } + + public static readonly DependencyProperty TemperatureAmountProperty = DependencyProperty.Register( +"TemperatureAmount", +typeof(double), +typeof(LoadedImageBrush), +new PropertyMetadata(0.0, new PropertyChangedCallback(OnTemperatureAmountChanged) +) +); + + public double TemperatureAmount + { + get { return (double)GetValue(TemperatureAmountProperty); } + set { SetValue(TemperatureAmountProperty, value); } + } + + private static void OnTemperatureAmountChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) + { + var brush = (LoadedImageBrush)d; + // Unbox and set a new blur amount if the CompositionBrush exists. + brush.CompositionBrush?.Properties.InsertScalar("TemperatureAndTintEffect.Temperature", (float)(double)e.NewValue); + } + + public LoadedImageBrush() + { + } + + public void LoadImageFromPath(string path) + { + var compositor = Window.Current.Compositor; + // Load image + _surface = LoadedImageSurface.StartLoadFromUri(new Uri(path)); + _surface.LoadCompleted += Load_Completed; + } + + public void LoadImageFromStream(IRandomAccessStream stream) + { + if (stream != null && IsImageLoading == false) + { + var compositor = Window.Current.Compositor; + // Load image + IsImageLoading = true; + _surface = LoadedImageSurface.StartLoadFromStream(stream); + _surface.LoadCompleted += Load_Completed; + } + } + + private void Load_Completed(LoadedImageSurface sender, LoadedImageSourceLoadCompletedEventArgs e) + { + IsImageLoading = false; + + if (e.Status == LoadedImageSourceLoadStatus.Success) + { + var compositor = Window.Current.Compositor; + var brush = compositor.CreateSurfaceBrush(_surface); + brush.Stretch = CompositionStretch.UniformToFill; + + // Create effects chain. + saturationEffect = new SaturationEffect() + { + Name = "SaturationEffect", + Saturation = (float)SaturationAmount, + Source = new CompositionEffectSourceParameter("image") + }; + contrastEffect = new ContrastEffect() + { + Name = "ContrastEffect", + Contrast = (float)ContrastAmount, + Source = saturationEffect + }; + exposureEffect = new ExposureEffect() + { + Name = "ExposureEffect", + Source = contrastEffect, + Exposure = (float)ExposureAmount, + }; + temperatureAndTintEffect = new TemperatureAndTintEffect() + { + Name = "TemperatureAndTintEffect", + Source = exposureEffect, + Temperature = (float)TemperatureAmount, + Tint = (float)TintAmount + }; + graphicsEffect = new GaussianBlurEffect() + { + Name = "Blur", + Source = temperatureAndTintEffect, + BlurAmount = (float)BlurAmount, + BorderMode = EffectBorderMode.Hard, + }; + + var graphicsEffectFactory = compositor.CreateEffectFactory(graphicsEffect, new[] { + "SaturationEffect.Saturation", "ExposureEffect.Exposure", "Blur.BlurAmount", + "TemperatureAndTintEffect.Temperature", "TemperatureAndTintEffect.Tint", + "ContrastEffect.Contrast" }); + combinedBrush = graphicsEffectFactory.CreateBrush(); + combinedBrush.SetSourceParameter("image", brush); + + // Composition Brush is what is being applied to the UI Element. + CompositionBrush = combinedBrush; + } + else + { + LoadImageFromPath("ms-appx:///Assets/StoreLogo.png"); + } + } + + protected override void OnDisconnected() + { + if (_surface != null) + { + _surface.Dispose(); + _surface = null; + } + + if (CompositionBrush != null) + { + CompositionBrush.Dispose(); + CompositionBrush = null; + } + } + } +} diff --git a/xaml-basics-starting-points/style/PhotoLab/MainPage.xaml b/xaml-basics-starting-points/style/PhotoLab/MainPage.xaml index 817586f..c37097a 100644 --- a/xaml-basics-starting-points/style/PhotoLab/MainPage.xaml +++ b/xaml-basics-starting-points/style/PhotoLab/MainPage.xaml @@ -29,7 +29,7 @@ xmlns:local="using:PhotoLab" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" - xmlns:telerikInput="using:Telerik.UI.Xaml.Controls.Input" + xmlns:muxc="using:Microsoft.UI.Xaml.Controls" xmlns:animations="using:Microsoft.Toolkit.Uwp.UI.Animations" mc:Ignorable="d" NavigationCacheMode="Enabled" @@ -41,13 +41,11 @@ 8 0 0 - 16 - 32 - + 16 + - @@ -96,21 +93,9 @@ Style="{StaticResource CaptionTextBlockStyle}" Margin="8,0,0,0" /> - - - - - - - - - - - - + + @@ -123,12 +108,12 @@ Value="{StaticResource LargeItemMargin}" /> - @@ -154,7 +139,7 @@ - - - - - + + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + + + - - + + - - + + + - - + + - - + + + + + + + + + + + Grid.Row="0" + Content="{TemplateBinding Header}" + ContentTemplate="{TemplateBinding HeaderTemplate}" + FontWeight="{ThemeResource SliderHeaderThemeFontWeight}" + Foreground="{ThemeResource SliderHeaderForeground}" + Margin="{ThemeResource SliderTopHeaderMargin}" + TextWrapping="Wrap" + Visibility="Collapsed" + x:DeferLoadStrategy="Lazy"/> - + Grid.Row="1" + Background="{ThemeResource SliderContainerBackground}" + Control.IsTemplateFocusTarget="True"> + + + - + - + + + - + + Height="{ThemeResource SliderTrackThemeHeight}" + Grid.Row="1" + Grid.ColumnSpan="3" + contract7Present:RadiusX="{Binding CornerRadius, RelativeSource={RelativeSource TemplatedParent}, Converter={StaticResource TopLeftCornerRadiusDoubleValueConverter}}" + contract7Present:RadiusY="{Binding CornerRadius, RelativeSource={RelativeSource TemplatedParent}, Converter={StaticResource BottomRightCornerRadiusDoubleValueConverter}}" + contract7NotPresent:RadiusX="{Binding Source={ThemeResource ControlCornerRadius}, Converter={StaticResource TopLeftCornerRadiusDoubleValueConverter}}" + contract7NotPresent:RadiusY="{Binding Source={ThemeResource ControlCornerRadius}, Converter={StaticResource BottomRightCornerRadiusDoubleValueConverter}}" /> + + Visibility="Collapsed" + Fill="{ThemeResource SliderTickBarFill}" + Height="{ThemeResource SliderOutsideTickBarThemeHeight}" + VerticalAlignment="Bottom" + Margin="0,0,0,4" + Grid.ColumnSpan="3" /> + Visibility="Collapsed" + Fill="{ThemeResource SliderInlineTickBarFill}" + Height="{ThemeResource SliderTrackThemeHeight}" + Grid.Row="1" + Grid.ColumnSpan="3" /> + Visibility="Collapsed" + Fill="{ThemeResource SliderTickBarFill}" + Height="{ThemeResource SliderOutsideTickBarThemeHeight}" + VerticalAlignment="Top" + Margin="0,4,0,0" + Grid.Row="2" + Grid.ColumnSpan="3" /> + Style="{StaticResource SliderThumbStyle}" + DataContext="{TemplateBinding Value}" + Height="{ThemeResource SliderHorizontalThumbHeight}" + Width="{ThemeResource SliderHorizontalThumbWidth}" + Grid.Row="0" + Grid.RowSpan="3" + Grid.Column="1" + FocusVisualMargin="-14,-6,-14,-6" + AutomationProperties.AccessibilityView="Raw" /> - + + + - + - + + + Fill="{TemplateBinding Background}" + Width="{ThemeResource SliderTrackThemeHeight}" + Grid.Column="1" + Grid.RowSpan="3" + contract7Present:RadiusX="{Binding CornerRadius, RelativeSource={RelativeSource TemplatedParent}, Converter={StaticResource TopLeftCornerRadiusDoubleValueConverter}}" + contract7Present:RadiusY="{Binding CornerRadius, RelativeSource={RelativeSource TemplatedParent}, Converter={StaticResource BottomRightCornerRadiusDoubleValueConverter}}" + contract7NotPresent:RadiusX="{Binding Source={ThemeResource ControlCornerRadius}, Converter={StaticResource TopLeftCornerRadiusDoubleValueConverter}}" + contract7NotPresent:RadiusY="{Binding Source={ThemeResource ControlCornerRadius}, Converter={StaticResource BottomRightCornerRadiusDoubleValueConverter}}" /> + Fill="{TemplateBinding Foreground}" + Grid.Column="1" + Grid.Row="2" + contract7Present:RadiusX="{Binding CornerRadius, RelativeSource={RelativeSource TemplatedParent}, Converter={StaticResource TopLeftCornerRadiusDoubleValueConverter}}" + contract7Present:RadiusY="{Binding CornerRadius, RelativeSource={RelativeSource TemplatedParent}, Converter={StaticResource BottomRightCornerRadiusDoubleValueConverter}}" + contract7NotPresent:RadiusX="{Binding Source={ThemeResource ControlCornerRadius}, Converter={StaticResource TopLeftCornerRadiusDoubleValueConverter}}" + contract7NotPresent:RadiusY="{Binding Source={ThemeResource ControlCornerRadius}, Converter={StaticResource BottomRightCornerRadiusDoubleValueConverter}}" /> + Visibility="Collapsed" + Fill="{ThemeResource SliderTickBarFill}" + Width="{ThemeResource SliderOutsideTickBarThemeHeight}" + HorizontalAlignment="Right" + Margin="0,0,4,0" + Grid.RowSpan="3" /> + Visibility="Collapsed" + Fill="{ThemeResource SliderInlineTickBarFill}" + Width="{ThemeResource SliderTrackThemeHeight}" + Grid.Column="1" + Grid.RowSpan="3" /> + Visibility="Collapsed" + Fill="{ThemeResource SliderTickBarFill}" + Width="{ThemeResource SliderOutsideTickBarThemeHeight}" + HorizontalAlignment="Left" + Margin="4,0,0,0" + Grid.Column="2" + Grid.RowSpan="3" /> + Style="{StaticResource SliderThumbStyle}" + DataContext="{TemplateBinding Value}" + Width="{ThemeResource SliderVerticalThumbWidth}" + Height="{ThemeResource SliderVerticalThumbHeight}" + Grid.Row="1" + Grid.Column="0" + Grid.ColumnSpan="3" + FocusVisualMargin="-6,-14,-6,-14" + AutomationProperties.AccessibilityView="Raw" /> @@ -397,9 +372,9 @@ - + - + + + + + - - @@ -489,6 +476,7 @@ + - - - - - - - - - - - - + + + + + + Maximum="5"/> - + + + + @@ -641,53 +620,5 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/xaml-basics-starting-points/user-interface/PhotoLab/DetailPage.xaml.cs b/xaml-basics-starting-points/user-interface/PhotoLab/DetailPage.xaml.cs index e91a9ce..df90066 100644 --- a/xaml-basics-starting-points/user-interface/PhotoLab/DetailPage.xaml.cs +++ b/xaml-basics-starting-points/user-interface/PhotoLab/DetailPage.xaml.cs @@ -23,20 +23,18 @@ // --------------------------------------------------------------------------------- using Microsoft.Graphics.Canvas; -using Microsoft.Graphics.Canvas.Effects; using System; using System.Collections.Generic; using System.Globalization; -using System.Numerics; +using System.Threading.Tasks; using Windows.Storage; using Windows.Storage.Pickers; using Windows.Storage.Streams; -using Windows.UI.Composition; using Windows.UI.Core; using Windows.UI.Xaml.Controls; using Windows.UI.Xaml.Controls.Primitives; -using Windows.UI.Xaml.Hosting; using Windows.UI.Xaml.Media.Animation; +using Windows.UI.Xaml.Media.Imaging; using Windows.UI.Xaml.Navigation; namespace PhotoLab @@ -44,42 +42,47 @@ namespace PhotoLab public sealed partial class DetailPage : Page { ImageFileInfo item; - Compositor compositor; - CompositionEffectBrush combinedBrush; CultureInfo culture = CultureInfo.CurrentCulture; - ContrastEffect contrastEffect; - ExposureEffect exposureEffect; - TemperatureAndTintEffect temperatureAndTintEffect; - GaussianBlurEffect graphicsEffect; - SaturationEffect saturationEffect; - bool editingInitialized = false; bool canNavigateWithUnsavedChanges = false; + BitmapImage ImageSource = null; public DetailPage() { this.InitializeComponent(); } - protected override void OnNavigatedTo(NavigationEventArgs e) + protected override async void OnNavigatedTo(NavigationEventArgs e) { item = e.Parameter as ImageFileInfo; canNavigateWithUnsavedChanges = false; - ResetEffects(); + ImageSource = await item.GetImageSourceAsync(); if (item != null) { - item.PropertyChanged += (s, e2) => UpdateEffectBrush(e2.PropertyName); - targetImage.Source = item.ImageSource; ConnectedAnimation imageAnimation = ConnectedAnimationService.GetForCurrentView().GetAnimation("itemAnimation"); if (imageAnimation != null) { - imageAnimation.Completed += (s, e_) => + targetImage.Source = ImageSource; + + imageAnimation.Completed += async (s, e_) => { - MainImage.Source = item.ImageSource; + await LoadBrushAsync(); + // This delay prevents a flicker that can happen when + // a large image is being loaded to the brush. It gives + // the image time to finish loading. (200 is ok, 400 to be safe.) + await Task.Delay(400); targetImage.Source = null; }; imageAnimation.TryStart(targetImage); } + + if (ImageSource.PixelHeight == 0 && ImageSource.PixelWidth == 0) + { + // There is no editable image loaded. Disable zoom and edit + // to prevent other errors. + EditButton.IsEnabled = false; + ZoomButton.IsEnabled = false; + }; } else { @@ -116,8 +119,11 @@ protected override void OnNavigatingFrom(NavigatingCancelEventArgs e) } else { + if (e.NavigationMode == NavigationMode.Back) + { + ConnectedAnimationService.GetForCurrentView().PrepareToAnimate("backAnimation", imgRect); + } canNavigateWithUnsavedChanges = false; - ConnectedAnimationService.GetForCurrentView().PrepareToAnimate("backAnimation", MainImage); base.OnNavigatingFrom(e); } } @@ -140,7 +146,8 @@ private async void ShowSaveDialog(NavigatingCancelEventArgs e) { // The user decided to leave the page. Restart // the navigation attempt. - canNavigateWithUnsavedChanges = true; + canNavigateWithUnsavedChanges = true; + ResetEffects(); Frame.Navigate(e.SourcePageType, e.Parameter); } } @@ -160,8 +167,8 @@ private void MainImageScroller_ViewChanged(object sender, ScrollViewerViewChange private void FitToScreen() { - var zoomFactor = (float)Math.Min(MainImageScroller.ActualWidth / item.ImageSource.PixelWidth, - MainImageScroller.ActualHeight / item.ImageSource.PixelHeight); + var zoomFactor = (float)Math.Min(MainImageScroller.ActualWidth / item.ImageProperties.Width, + MainImageScroller.ActualHeight / item.ImageProperties.Height); MainImageScroller.ChangeView(null, null, zoomFactor); } @@ -182,125 +189,33 @@ private void UpdateZoomState() } } - private void InitializeEffects() - { - saturationEffect = new SaturationEffect() - { - Name = "SaturationEffect", - Saturation = item.Saturation, - Source = new CompositionEffectSourceParameter("Backdrop") - }; - contrastEffect = new ContrastEffect() - { - Name = "ContrastEffect", - Contrast = item.Contrast, - Source = saturationEffect - }; - exposureEffect = new ExposureEffect() - { - Name = "ExposureEffect", - Source = contrastEffect, - Exposure = item.Exposure, - }; - temperatureAndTintEffect = new TemperatureAndTintEffect() - { - Name = "TemperatureAndTintEffect", - Source = exposureEffect, - Temperature = item.Temperature, - Tint = item.Tint - }; - graphicsEffect = new GaussianBlurEffect() - { - Name = "Blur", - Source = temperatureAndTintEffect, - BlurAmount = item.Blur, - BorderMode = EffectBorderMode.Hard, - }; - } - - private void InitializeCompositor() - { - compositor = ElementCompositionPreview.GetElementVisual(this).Compositor; - InitializeEffects(); - MainImage.Source = item.ImageSource; - MainImage.InvalidateArrange(); - - var destinationBrush = compositor.CreateBackdropBrush(); - - var graphicsEffectFactory = compositor.CreateEffectFactory(graphicsEffect, new[] { - "SaturationEffect.Saturation", "ExposureEffect.Exposure", "Blur.BlurAmount", - "TemperatureAndTintEffect.Temperature", "TemperatureAndTintEffect.Tint", - "ContrastEffect.Contrast" }); - combinedBrush = graphicsEffectFactory.CreateBrush(); - combinedBrush.SetSourceParameter("Backdrop", destinationBrush); - - var effectSprite = compositor.CreateSpriteVisual(); - effectSprite.Size = new Vector2((float)item.ImageSource.PixelWidth, (float)item.ImageSource.PixelHeight); - effectSprite.Brush = combinedBrush; - ElementCompositionPreview.SetElementChildVisual(MainImage, effectSprite); - - editingInitialized = true; - } - private void ToggleEditState() { - if (MainSplitView.IsPaneOpen) - { - MainSplitView.IsPaneOpen = false; - } - else - { - if (!editingInitialized) - { - InitializeCompositor(); - } - MainSplitView.IsPaneOpen = true; - } - } - - private void UpdateEffectBrush(string propertyName) - { - void update(string effectName, float effectValue) => - combinedBrush?.Properties.InsertScalar(effectName, effectValue); - - switch (propertyName) - { - case nameof(item.Exposure): update("ExposureEffect.Exposure", item.Exposure); break; - case nameof(item.Temperature): update("TemperatureAndTintEffect.Temperature", item.Temperature); break; - case nameof(item.Tint): update("TemperatureAndTintEffect.Tint", item.Tint); break; - case nameof(item.Contrast): update("ContrastEffect.Contrast", item.Contrast); break; - case nameof(item.Saturation): update("SaturationEffect.Saturation", item.Saturation); break; - case nameof(item.Blur): update("Blur.BlurAmount", item.Blur); break; - default: break; - } + MainSplitView.IsPaneOpen = !MainSplitView.IsPaneOpen; } private async void ExportImage() { CanvasDevice device = CanvasDevice.GetSharedDevice(); using (CanvasRenderTarget offscreen = new CanvasRenderTarget( - device, item.ImageSource.PixelWidth, item.ImageSource.PixelHeight, 96)) + device, item.ImageProperties.Width, item.ImageProperties.Height, 96)) { using (IRandomAccessStream stream = await item.ImageFile.OpenReadAsync()) using (CanvasBitmap image = await CanvasBitmap.LoadAsync(offscreen, stream, 96)) { - saturationEffect.Source = image; + ImageEffectsBrush.SetSource(image); + using (CanvasDrawingSession ds = offscreen.CreateDrawingSession()) { ds.Clear(Windows.UI.Colors.Black); - // Need to copy the value of each effect setting. - contrastEffect.Contrast = item.Contrast; - exposureEffect.Exposure = item.Exposure; - temperatureAndTintEffect.Temperature = item.Temperature; - temperatureAndTintEffect.Tint = item.Tint; - saturationEffect.Saturation = item.Saturation; - graphicsEffect.BlurAmount = item.Blur; - ds.DrawImage(graphicsEffect); + var img = ImageEffectsBrush.Image; + ds.DrawImage(img); } var fileSavePicker = new FileSavePicker() { + SuggestedStartLocation = PickerLocationId.PicturesLibrary, SuggestedSaveFile = item.ImageFile }; @@ -315,24 +230,78 @@ private async void ExportImage() await offscreen.SaveAsync(outStream, CanvasBitmapFileFormat.Jpeg); } - ResetEffects(); - var newItem = await MainPage.LoadImageInfo(outputFile); + // Check whether this save is overwriting the original image. + // If it is, replace it in the list. Otherwise, insert it as a copy. + bool replace = false; + if (outputFile.IsEqual(item.ImageFile)) + { + replace = true; + } - if (outputFile.Path == item.ImageFile.Path) + try { - item.ImageSource = newItem.ImageSource; + await LoadSavedImageAsync(outputFile, replace); } - else + catch (Exception ex) { - item = newItem; + if (ex.Message.Contains("0x80070323")) + { + // The handle with which this oplock was associated has been closed. + // The oplock is now broken. (Exception from HRESULT: 0x80070323) + // This is a temporary condition, so just try again. + await LoadSavedImageAsync(outputFile, replace); + } } - - MainImage.Source = item.ImageSource; } } } } + private async Task LoadSavedImageAsync(StorageFile imageFile, bool replaceImage) + { + item.NeedsSaved = false; + var newItem = await MainPage.LoadImageInfo(imageFile); + ResetEffects(); + + // Get the index of the original image so we can + // insert the saved image in the same place. + var index = MainPage.Current.Images.IndexOf(item); + + item = newItem; + this.Bindings.Update(); + + UnloadObject(imgRect); + FindName("imgRect"); + await LoadBrushAsync(); + + // Insert the saved image into the collection. + if (replaceImage == true) + { + MainPage.Current.Images.RemoveAt(index); + MainPage.Current.Images.Insert(index, item); + } + else + { + MainPage.Current.Images.Insert(index+1, item); + } + + + // Replace the persisted image used for connected animation. + MainPage.Current.UpdatePersistedItem(item); + + // Update BitMapImage used for small picture. + ImageSource = await item.GetImageSourceAsync(); + SmallImage.Source = ImageSource; + } + + private async Task LoadBrushAsync() + { + using (IRandomAccessStream fileStream = await item.ImageFile.OpenReadAsync()) + { + ImageEffectsBrush.LoadImageFromStream(fileStream); + } + } + private void ResetEffects() { item.Exposure = @@ -343,5 +312,9 @@ private void ResetEffects() item.Saturation = 1; } + private void SmallImage_Loaded(object sender, Windows.UI.Xaml.RoutedEventArgs e) + { + SmallImage.Source = ImageSource; + } } } diff --git a/xaml-basics-starting-points/user-interface/PhotoLab/ImageFileInfo.cs b/xaml-basics-starting-points/user-interface/PhotoLab/ImageFileInfo.cs index b3fc945..a503ebd 100644 --- a/xaml-basics-starting-points/user-interface/PhotoLab/ImageFileInfo.cs +++ b/xaml-basics-starting-points/user-interface/PhotoLab/ImageFileInfo.cs @@ -25,19 +25,19 @@ using System; using System.ComponentModel; using System.Runtime.CompilerServices; +using System.Threading.Tasks; using Windows.Storage; using Windows.Storage.FileProperties; +using Windows.Storage.Streams; using Windows.UI.Xaml.Media.Imaging; namespace PhotoLab { public class ImageFileInfo : INotifyPropertyChanged { - public ImageFileInfo(ImageProperties properties, StorageFile imageFile, - BitmapImage src, string name, string type) + public ImageFileInfo(ImageProperties properties, StorageFile imageFile, string name, string type) { ImageProperties = properties; - ImageSource = src; ImageName = name; ImageFileType = type; ImageFile = imageFile; @@ -50,18 +50,34 @@ public ImageFileInfo(ImageProperties properties, StorageFile imageFile, public ImageProperties ImageProperties { get; } - private BitmapImage _imageSource = null; - public BitmapImage ImageSource + public async Task GetImageSourceAsync() { - get => _imageSource; - set => SetProperty(ref _imageSource, value); + using (IRandomAccessStream fileStream = await ImageFile.OpenReadAsync()) + { + // Create a bitmap to be the image source. + BitmapImage bitmapImage = new BitmapImage(); + bitmapImage.SetSource(fileStream); + + return bitmapImage; + } + } + + public async Task GetImageThumbnailAsync() + { + var thumbnail = await ImageFile.GetThumbnailAsync(ThumbnailMode.PicturesView); + // Create a bitmap to be the image source. + BitmapImage bitmapImage = new BitmapImage(); + bitmapImage.SetSource(thumbnail); + thumbnail.Dispose(); + + return bitmapImage; } public string ImageName { get; } public string ImageFileType { get; } - public string ImageDimensions => $"{ImageSource.PixelWidth} x {ImageSource.PixelHeight}"; + public string ImageDimensions => $"{ImageProperties.Width} x {ImageProperties.Height}"; public string ImageTitle { diff --git a/xaml-basics-starting-points/user-interface/PhotoLab/LoadedImageBrush.cs b/xaml-basics-starting-points/user-interface/PhotoLab/LoadedImageBrush.cs new file mode 100644 index 0000000..2712497 --- /dev/null +++ b/xaml-basics-starting-points/user-interface/PhotoLab/LoadedImageBrush.cs @@ -0,0 +1,276 @@ +using Microsoft.Graphics.Canvas; +using Microsoft.Graphics.Canvas.Brushes; +using Microsoft.Graphics.Canvas.Effects; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Windows.Foundation; +using Windows.Graphics.Effects; +using Windows.UI.Composition; +using Windows.UI.Xaml; +using Windows.UI.Xaml.Media; +using Windows.Storage.Streams; + +namespace PhotoLab +{ + class LoadedImageBrush : XamlCompositionBrushBase + { + private bool IsImageLoading = false; + private LoadedImageSurface _surface; + CompositionEffectBrush combinedBrush; + ContrastEffect contrastEffect; + ExposureEffect exposureEffect; + TemperatureAndTintEffect temperatureAndTintEffect; + GaussianBlurEffect graphicsEffect; + SaturationEffect saturationEffect; + + public ICanvasImage Image + { + get + { + contrastEffect.Contrast = (float)ContrastAmount; + exposureEffect.Exposure = (float)ExposureAmount; + temperatureAndTintEffect.Tint = (float)TintAmount; + temperatureAndTintEffect.Temperature = (float)TemperatureAmount; + saturationEffect.Saturation = (float)SaturationAmount; + graphicsEffect.BlurAmount = (float)BlurAmount; + return graphicsEffect; + } + } + + public void SetSource(ICanvasImage source) + { + saturationEffect.Source = source; + } + + public static readonly DependencyProperty BlurAmountProperty = DependencyProperty.Register( + "BlurAmount", + typeof(double), + typeof(LoadedImageBrush), + new PropertyMetadata(0.0, new PropertyChangedCallback(OnBlurAmountChanged) + ) +); + + public double BlurAmount + { + get { return (double)GetValue(BlurAmountProperty); } + set { SetValue(BlurAmountProperty, value); } + } + + private static void OnBlurAmountChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) + { + var brush = (LoadedImageBrush)d; + // Unbox and set a new blur amount if the CompositionBrush exists. + brush.CompositionBrush?.Properties.InsertScalar("Blur.BlurAmount", (float)(double)e.NewValue); + } + + + + public static readonly DependencyProperty ContrastAmountProperty = DependencyProperty.Register( +"ContrastAmount", +typeof(double), +typeof(LoadedImageBrush), +new PropertyMetadata(0.0, new PropertyChangedCallback(OnContrastAmountChanged) +) +); + + public double ContrastAmount + { + get { return (double)GetValue(ContrastAmountProperty); } + set { SetValue(ContrastAmountProperty, value); } + } + + private static void OnContrastAmountChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) + { + var brush = (LoadedImageBrush)d; + // Unbox and set a new blur amount if the CompositionBrush exists. + brush.CompositionBrush?.Properties.InsertScalar("ContrastEffect.Contrast", (float)(double)e.NewValue); + } + + public static readonly DependencyProperty SaturationAmountProperty = DependencyProperty.Register( +"SaturationAmount", +typeof(double), +typeof(LoadedImageBrush), +new PropertyMetadata(1.0, new PropertyChangedCallback(OnSaturationAmountChanged) +) +); + + public double SaturationAmount + { + get { return (double)GetValue(SaturationAmountProperty); } + set { SetValue(SaturationAmountProperty, value); } + } + + private static void OnSaturationAmountChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) + { + var brush = (LoadedImageBrush)d; + // Unbox and set a new blur amount if the CompositionBrush exists. + brush.CompositionBrush?.Properties.InsertScalar("SaturationEffect.Saturation", (float)(double)e.NewValue); + } + + public static readonly DependencyProperty ExposureAmountProperty = DependencyProperty.Register( +"ExposureAmount", +typeof(double), +typeof(LoadedImageBrush), +new PropertyMetadata(0.0, new PropertyChangedCallback(OnExposureAmountChanged) +) +); + + public double ExposureAmount + { + get { return (double)GetValue(ExposureAmountProperty); } + set { SetValue(ExposureAmountProperty, value); } + } + + private static void OnExposureAmountChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) + { + var brush = (LoadedImageBrush)d; + // Unbox and set a new blur amount if the CompositionBrush exists. + brush.CompositionBrush?.Properties.InsertScalar("ExposureEffect.Exposure", (float)(double)e.NewValue); + } + + + public static readonly DependencyProperty TintAmountProperty = DependencyProperty.Register( +"TintAmount", +typeof(double), +typeof(LoadedImageBrush), +new PropertyMetadata(0.0, new PropertyChangedCallback(OnTintAmountChanged) +) +); + + public double TintAmount + { + get { return (double)GetValue(TintAmountProperty); } + set { SetValue(TintAmountProperty, value); } + } + + private static void OnTintAmountChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) + { + var brush = (LoadedImageBrush)d; + // Unbox and set a new blur amount if the CompositionBrush exists. + brush.CompositionBrush?.Properties.InsertScalar("TemperatureAndTintEffect.Tint", (float)(double)e.NewValue); + } + + public static readonly DependencyProperty TemperatureAmountProperty = DependencyProperty.Register( +"TemperatureAmount", +typeof(double), +typeof(LoadedImageBrush), +new PropertyMetadata(0.0, new PropertyChangedCallback(OnTemperatureAmountChanged) +) +); + + public double TemperatureAmount + { + get { return (double)GetValue(TemperatureAmountProperty); } + set { SetValue(TemperatureAmountProperty, value); } + } + + private static void OnTemperatureAmountChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) + { + var brush = (LoadedImageBrush)d; + // Unbox and set a new blur amount if the CompositionBrush exists. + brush.CompositionBrush?.Properties.InsertScalar("TemperatureAndTintEffect.Temperature", (float)(double)e.NewValue); + } + + public LoadedImageBrush() + { + } + + public void LoadImageFromPath(string path) + { + var compositor = Window.Current.Compositor; + // Load image + _surface = LoadedImageSurface.StartLoadFromUri(new Uri(path)); + _surface.LoadCompleted += Load_Completed; + } + + public void LoadImageFromStream(IRandomAccessStream stream) + { + if (stream != null && IsImageLoading == false) + { + var compositor = Window.Current.Compositor; + // Load image + IsImageLoading = true; + _surface = LoadedImageSurface.StartLoadFromStream(stream); + _surface.LoadCompleted += Load_Completed; + } + } + + private void Load_Completed(LoadedImageSurface sender, LoadedImageSourceLoadCompletedEventArgs e) + { + IsImageLoading = false; + + if (e.Status == LoadedImageSourceLoadStatus.Success) + { + var compositor = Window.Current.Compositor; + var brush = compositor.CreateSurfaceBrush(_surface); + brush.Stretch = CompositionStretch.UniformToFill; + + // Create effects chain. + saturationEffect = new SaturationEffect() + { + Name = "SaturationEffect", + Saturation = (float)SaturationAmount, + Source = new CompositionEffectSourceParameter("image") + }; + contrastEffect = new ContrastEffect() + { + Name = "ContrastEffect", + Contrast = (float)ContrastAmount, + Source = saturationEffect + }; + exposureEffect = new ExposureEffect() + { + Name = "ExposureEffect", + Source = contrastEffect, + Exposure = (float)ExposureAmount, + }; + temperatureAndTintEffect = new TemperatureAndTintEffect() + { + Name = "TemperatureAndTintEffect", + Source = exposureEffect, + Temperature = (float)TemperatureAmount, + Tint = (float)TintAmount + }; + graphicsEffect = new GaussianBlurEffect() + { + Name = "Blur", + Source = temperatureAndTintEffect, + BlurAmount = (float)BlurAmount, + BorderMode = EffectBorderMode.Hard, + }; + + var graphicsEffectFactory = compositor.CreateEffectFactory(graphicsEffect, new[] { + "SaturationEffect.Saturation", "ExposureEffect.Exposure", "Blur.BlurAmount", + "TemperatureAndTintEffect.Temperature", "TemperatureAndTintEffect.Tint", + "ContrastEffect.Contrast" }); + combinedBrush = graphicsEffectFactory.CreateBrush(); + combinedBrush.SetSourceParameter("image", brush); + + // Composition Brush is what is being applied to the UI Element. + CompositionBrush = combinedBrush; + } + else + { + LoadImageFromPath("ms-appx:///Assets/StoreLogo.png"); + } + } + + protected override void OnDisconnected() + { + if (_surface != null) + { + _surface.Dispose(); + _surface = null; + } + + if (CompositionBrush != null) + { + CompositionBrush.Dispose(); + CompositionBrush = null; + } + } + } +} diff --git a/xaml-basics-starting-points/user-interface/PhotoLab/MainPage.xaml b/xaml-basics-starting-points/user-interface/PhotoLab/MainPage.xaml index 3332518..1d2023a 100644 --- a/xaml-basics-starting-points/user-interface/PhotoLab/MainPage.xaml +++ b/xaml-basics-starting-points/user-interface/PhotoLab/MainPage.xaml @@ -33,13 +33,12 @@ NavigationCacheMode="Enabled"> - - + + - - + diff --git a/xaml-basics-starting-points/user-interface/PhotoLab/MainPage.xaml.cs b/xaml-basics-starting-points/user-interface/PhotoLab/MainPage.xaml.cs index 15205f7..b5da320 100644 --- a/xaml-basics-starting-points/user-interface/PhotoLab/MainPage.xaml.cs +++ b/xaml-basics-starting-points/user-interface/PhotoLab/MainPage.xaml.cs @@ -25,31 +25,34 @@ using System; using System.Collections.Generic; using System.Collections.ObjectModel; -using System.ComponentModel; using System.Threading.Tasks; using Windows.ApplicationModel; using Windows.Storage; -using Windows.Storage.Streams; -using Windows.System.Profile; +using Windows.Storage.Search; using Windows.UI.Core; -using Windows.UI.ViewManagement; using Windows.UI.Xaml.Controls; -using Windows.UI.Xaml.Media.Animation; -using Windows.UI.Xaml.Media.Imaging; using Windows.UI.Xaml.Navigation; namespace PhotoLab { - public sealed partial class MainPage : Page, INotifyPropertyChanged + public sealed partial class MainPage : Page { + public static MainPage Current; private ImageFileInfo persistedItem; public ObservableCollection Images { get; } = new ObservableCollection(); - public event PropertyChangedEventHandler PropertyChanged; public MainPage() { this.InitializeComponent(); + Current = this; + } + + // If the image is edited and saved in the details page, this method gets called + // so that the back navigation connected animation uses the correct image. + public void UpdatePersistedItem(ImageFileInfo item) + { + persistedItem = item; } protected async override void OnNavigatedTo(NavigationEventArgs e) @@ -67,58 +70,58 @@ protected async override void OnNavigatedTo(NavigationEventArgs e) private async Task GetItemsAsync() { - // https://docs.microsoft.com/uwp/api/windows.ui.xaml.controls.image#Windows_UI_Xaml_Controls_Image_Source - // See "Using a stream source to show images from the Pictures library". - // This code is modified to get images from the app folder. + QueryOptions options = new QueryOptions(); + options.FolderDepth = FolderDepth.Deep; + options.FileTypeFilter.Add(".jpg"); + options.FileTypeFilter.Add(".png"); + options.FileTypeFilter.Add(".gif"); - // Get the app folder where the images are stored. + // Get the Pictures library. (Requires 'Pictures Library' capability.) + //Windows.Storage.StorageFolder picturesFolder = Windows.Storage.KnownFolders.PicturesLibrary; + // OR + // Get the Sample pictures. StorageFolder appInstalledFolder = Package.Current.InstalledLocation; - StorageFolder assets = await appInstalledFolder.GetFolderAsync("Assets\\Samples"); + StorageFolder picturesFolder = await appInstalledFolder.GetFolderAsync("Assets\\Samples"); + + var result = picturesFolder.CreateFileQueryWithOptions(options); - // Get and process files in folder - IReadOnlyList fileList = await assets.GetFilesAsync(); - foreach (StorageFile file in fileList) + IReadOnlyList imageFiles = await result.GetFilesAsync(); + bool unsupportedFilesFound = false; + foreach (StorageFile file in imageFiles) { - // Limit to only png or jpg files. - if (file.ContentType == "image/png" || file.ContentType == "image/jpeg") + // Only files on the local computer are supported. + // Files on OneDrive or a network location are excluded. + if (file.Provider.Id == "computer") { Images.Add(await LoadImageInfo(file)); } + else + { + unsupportedFilesFound = true; + } } - } - public async static Task LoadImageInfo(StorageFile file) - { - // Open a stream for the selected file. - // The 'using' block ensures the stream is disposed - // after the image is loaded. - using (IRandomAccessStream fileStream = await file.OpenReadAsync()) + if (unsupportedFilesFound == true) { - // Create a bitmap to be the image source. - BitmapImage bitmapImage = new BitmapImage(); - bitmapImage.SetSource(fileStream); - - var properties = await file.Properties.GetImagePropertiesAsync(); - ImageFileInfo info = new ImageFileInfo( - properties, file, bitmapImage, - file.DisplayName, file.DisplayType); + ContentDialog unsupportedFilesDialog = new ContentDialog + { + Title = "Unsupported images found", + Content = "This sample app only supports images stored locally on the computer. We found files in your library that are stored in OneDrive or another network location. We didn't load those images.", + CloseButtonText = "Ok" + }; - return info; + ContentDialogResult resultNotUsed = await unsupportedFilesDialog.ShowAsync(); } } - public double ItemSize + public async static Task LoadImageInfo(StorageFile file) { - get => _itemSize; - set - { - if (_itemSize != value) - { - _itemSize = value; - PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(ItemSize))); - } - } + var properties = await file.Properties.GetImagePropertiesAsync(); + ImageFileInfo info = new ImageFileInfo( + properties, file, + file.DisplayName, file.DisplayType); + + return info; } - private double _itemSize; } } diff --git a/xaml-basics-starting-points/user-interface/PhotoLab/MobileScreenTrigger.cs b/xaml-basics-starting-points/user-interface/PhotoLab/MobileScreenTrigger.cs deleted file mode 100644 index 80caf29..0000000 --- a/xaml-basics-starting-points/user-interface/PhotoLab/MobileScreenTrigger.cs +++ /dev/null @@ -1,71 +0,0 @@ -// --------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// -// The MIT License (MIT) -// -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in -// all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -// THE SOFTWARE. -// --------------------------------------------------------------------------------- - -using Windows.System.Profile; -using Windows.UI.ViewManagement; -using Windows.UI.Xaml; - -namespace PhotoLab -{ - /// - /// Custom trigger for Mobile device UI states. - /// This trigger is active when the app runs on a mobile device and the - /// UserInteractionMode is touch, which indicates that the app is showing - /// on the device screen. When UserInteractionMode is mouse, the app is - /// using Continuum to show on a larger screen. - /// https://blogs.windows.com/buildingapps/2015/12/07/optimizing-apps-for-continuum-for-phone/#Yubo3bUdIM4H6Vle.97 - /// - public class MobileScreenTrigger : StateTriggerBase - { - public MobileScreenTrigger() - { - Window.Current.SizeChanged += (s, e) => UpdateTrigger(); - } - - /// - /// The target device family. - /// - public UserInteractionMode InteractionMode - { - get => _interactionMode; - set - { - _interactionMode = value; - UpdateTrigger(); - } - } - private UserInteractionMode _interactionMode; - - private void UpdateTrigger() - { - // Get the current device family and interaction mode. - var currentDeviceFamily = AnalyticsInfo.VersionInfo.DeviceFamily; - var currentInteractionMode = UIViewSettings.GetForCurrentView().UserInteractionMode; - - // The trigger will be activated if the current device family is Windows.Mobile - // and the UserInteractionMode matches the interaction mode value in XAML. - SetActive(InteractionMode == currentInteractionMode && currentDeviceFamily == "Windows.Mobile"); - } - } -} diff --git a/xaml-basics-starting-points/user-interface/PhotoLab/PhotoLab.csproj b/xaml-basics-starting-points/user-interface/PhotoLab/PhotoLab.csproj index c3b3f5c..097822e 100644 --- a/xaml-basics-starting-points/user-interface/PhotoLab/PhotoLab.csproj +++ b/xaml-basics-starting-points/user-interface/PhotoLab/PhotoLab.csproj @@ -11,7 +11,7 @@ PhotoLab en-US UAP - 10.0.17763.0 + 10.0.19041.0 10.0.17763.0 14 512 @@ -96,10 +96,10 @@ DetailPage.xaml + MainPage.xaml - @@ -158,8 +158,8 @@ 1.5.1 - - 1.0.0.4 + + 2.4.2