첨부 실행 코드는 나눔고딕코딩 폰트를 사용합니다.
유용한 소스 코드가 있으면 icodebroker@naver.com으로 보내주시면 감사합니다.
블로그 자료는 자유롭게 사용하세요.

728x90
반응형

TestProject.zip
5.66MB
TestProject.z01
10.00MB
TestProject.z02
10.00MB

▶ ImageFileInfo.cs

using System;
using System.ComponentModel;
using System.Runtime.CompilerServices;
using System.Threading.Tasks;
using Windows.Foundation;
using Windows.Storage;
using Windows.Storage.FileProperties;
using Windows.Storage.Streams;
using Windows.UI.Xaml.Media.Imaging;

namespace TestProject
{
    /// <summary>
    /// 이미지 파일 정보
    /// </summary>
    public class ImageFileInfo : INotifyPropertyChanged
    {
        //////////////////////////////////////////////////////////////////////////////////////////////////// Event
        ////////////////////////////////////////////////////////////////////////////////////////// Public

        #region 속성 변경시 이벤트 - PropertyChanged

        /// <summary>
        /// 속성 변경시 이벤트
        /// </summary>
        public event PropertyChangedEventHandler PropertyChanged;

        #endregion

        //////////////////////////////////////////////////////////////////////////////////////////////////// Field
        ////////////////////////////////////////////////////////////////////////////////////////// Private

        #region Field

        /// <summary>
        /// 노출
        /// </summary>
        private float exposure = 0;

        /// <summary>
        /// 온도
        /// </summary>
        private float temperature = 0;

        /// <summary>
        /// 색조
        /// </summary>
        private float tint = 0;

        /// <summary>
        /// 대비
        /// </summary>
        private float contrast = 0;

        /// <summary>
        /// 채도
        /// </summary>
        private float saturation = 1;

        /// <summary>
        /// 불선명도
        /// </summary>
        private float blur = 0;

        /// <summary>
        /// 저장 필요 여부
        /// </summary>
        private bool needsSaved = false;

        #endregion

        //////////////////////////////////////////////////////////////////////////////////////////////////// Property
        ////////////////////////////////////////////////////////////////////////////////////////// Public

        #region 이미지 속성 - ImageProperties

        /// <summary>
        /// 이미지 속성
        /// </summary>
        public ImageProperties ImageProperties { get; }

        #endregion
        #region 이미지 파일 - ImageFile

        /// <summary>
        /// 이미지 파일
        /// </summary>
        public StorageFile ImageFile { get; }

        #endregion
        #region 이미지명 - ImageName

        /// <summary>
        /// 이미지명
        /// </summary>
        public string ImageName { get; }

        #endregion
        #region 이미지 파일 타입 - ImageFileType

        /// <summary>
        /// 이미지 파일 타입
        /// </summary>
        public string ImageFileType { get; }

        #endregion

        #region 이미지 제목 - ImageTitle

        /// <summary>
        /// 이미지 제목
        /// </summary>
        public string ImageTitle
        {
            get => string.IsNullOrEmpty(ImageProperties.Title) ? ImageName : ImageProperties.Title;
            set
            {
                if(ImageProperties.Title != value)
                {
                    ImageProperties.Title = value;

                    IAsyncAction action = ImageProperties.SavePropertiesAsync();

                    FirePropertyChangedEvent();
                }
            }
        }

        #endregion
        #region 이미지 등급 - ImageRating

        /// <summary>
        /// 이미지 등급
        /// </summary>
        public int ImageRating
        {
            get => (int)ImageProperties.Rating;
            set
            {
                if(ImageProperties.Rating != value)
                {
                    ImageProperties.Rating = (uint)value;

                    IAsyncAction action = ImageProperties.SavePropertiesAsync();

                    FirePropertyChangedEvent();
                }
            }
        }

        #endregion
        #region 이미지 크기 - ImageDimensions

        /// <summary>
        /// 이미지 크기
        /// </summary>
        public string ImageDimensions => $"{ImageProperties.Width} x {ImageProperties.Height}";

        #endregion

        #region 노출 - Exposure

        /// <summary>
        /// 노출
        /// </summary>
        public float Exposure
        {
            get => this.exposure;
            set => SetEditingProperty(ref this.exposure, value);
        }

        #endregion
        #region 온도 - Temperature

        /// <summary>
        /// 온도
        /// </summary>
        public float Temperature
        {
            get => this.temperature;
            set => SetEditingProperty(ref this.temperature, value);
        }

        #endregion
        #region 색조 - Tint

        /// <summary>
        /// 색조
        /// </summary>
        public float Tint
        {
            get => this.tint;
            set => SetEditingProperty(ref this.tint, value);
        }

        #endregion
        #region 대비 - Contrast

        /// <summary>
        /// 대비
        /// </summary>
        public float Contrast
        {
            get => this.contrast;
            set => SetEditingProperty(ref this.contrast, value);
        }

        #endregion
        #region 채도 - Saturation

        /// <summary>
        /// 채도
        /// </summary>
        public float Saturation
        {
            get => this.saturation;
            set => SetEditingProperty(ref this.saturation, value);
        }

        #endregion
        #region 불선명도 - Blur

        /// <summary>
        /// 불선명도
        /// </summary>
        public float Blur
        {
            get => this.blur;
            set => SetEditingProperty(ref this.blur, value);
        }

        #endregion

        #region 저장 필요 여부 - NeedsSaved

        /// <summary>
        /// 저장 필요 여부
        /// </summary>
        public bool NeedsSaved
        {
            get => this.needsSaved;
            set => SetProperty(ref this.needsSaved, value);
        }

        #endregion

        //////////////////////////////////////////////////////////////////////////////////////////////////// Constructor
        ////////////////////////////////////////////////////////////////////////////////////////// Public

        #region 생성자 - ImageFileInfo(imageProperties, imageFile, imageName, imageFileType)

        /// <summary>
        /// 생성자
        /// </summary>
        /// <param name="imageProperties">이미지 속성</param>
        /// <param name="imageFile">이미지 파일</param>
        /// <param name="imageName">이미지명</param>
        /// <param name="imageFileType">이미지 파일 타입</param>
        public ImageFileInfo(ImageProperties imageProperties, StorageFile imageFile, string imageName, string imageFileType)
        {
            ImageProperties = imageProperties;
            ImageFile       = imageFile;
            ImageName       = imageName;
            ImageFileType   = imageFileType;

            Random random = new Random();

            int rating = (int)imageProperties.Rating;

            ImageRating = rating == 0 ? random.Next(1, 5) : rating;
        }

        #endregion

        //////////////////////////////////////////////////////////////////////////////////////////////////// Method
        ////////////////////////////////////////////////////////////////////////////////////////// Public

        #region 비트맵 이미지 구하기 (비동기) - GetBitmapImageAsyc()

        /// <summary>
        /// 비트맵 이미지 구하기 (비동기)
        /// </summary>
        /// <returns>비트맵 이미지 태스크</returns>
        public async Task<BitmapImage> GetBitmapImageAsyc()
        {
            using(IRandomAccessStream stream = await ImageFile.OpenReadAsync())
            {
                BitmapImage bitmapImage = new BitmapImage();

                bitmapImage.SetSource(stream);

                return bitmapImage;
            }
        }

        #endregion
        #region 썸네일 비트맵 이미지 구하기 (비동기) - GetThumbnailBitmapImageAsync()

        /// <summary>
        /// 썸네일 비트맵 이미지 구하기 (비동기)
        /// </summary>
        /// <returns>썸네일 비트맵 이미지</returns>
        public async Task<BitmapImage> GetThumbnailBitmapImageAsync()
        {
            StorageItemThumbnail thumbnail = await ImageFile.GetThumbnailAsync(ThumbnailMode.PicturesView);

            BitmapImage bitmapImage = new BitmapImage();

            bitmapImage.SetSource(thumbnail);

            thumbnail.Dispose();

            return bitmapImage;
        }

        #endregion

        ////////////////////////////////////////////////////////////////////////////////////////// Protected

        #region 속성 변경시 이벤트 발생시키기 - FirePropertyChangedEvent(propertyName)

        /// <summary>
        /// 속성 변경시 이벤트 발생시키기
        /// </summary>
        /// <param name="propertyName">속성명</param>
        protected void FirePropertyChangedEvent([CallerMemberName] string propertyName = null) => PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));

        #endregion
        #region 편집 속성 설정하기 - SetEditingProperty<T>(storage, value, propertyName)

        /// <summary>
        /// 편집 속성 설정하기
        /// </summary>
        /// <typeparam name="T">타입</typeparam>
        /// <param name="storage">저장소</param>
        /// <param name="value">값</param>
        /// <param name="propertyName">속성명</param>
        /// <returns>처리 결과</returns>
        protected bool SetEditingProperty<T>(ref T storage, T value, [CallerMemberName] string propertyName = null)
        {
            if(SetProperty(ref storage, value, propertyName))
            {
                if(Exposure != 0 || Temperature != 0 || Tint != 0 || Contrast != 0 || Saturation != 1 || Blur != 0)
                {
                    NeedsSaved = true;
                }
                else
                {
                    NeedsSaved = false;
                }

                return true;
            }
            else
            {
                return false;
            }
        }

        #endregion
        #region 속성 설정하기 - SetProperty<T>(storage, value, propertyName)

        /// <summary>
        /// 속성 설정하기
        /// </summary>
        /// <typeparam name="T">타입</typeparam>
        /// <param name="storage">저장소</param>
        /// <param name="value">값</param>
        /// <param name="propertyName">속성명</param>
        /// <returns>처리 결과</returns>
        protected bool SetProperty<T>(ref T storage, T value, [CallerMemberName] string propertyName = null)
        {
            if(object.Equals(storage, value))
            {
                return false;
            }
            else
            {
                storage = value;

                FirePropertyChangedEvent(propertyName);

                return true;
            }
        }

        #endregion
    }
}

 

▶ CustomCompositionBrush.cs

using Microsoft.Graphics.Canvas;
using Microsoft.Graphics.Canvas.Effects;
using System;
using Windows.UI.Composition;
using Windows.UI.Xaml;
using Windows.UI.Xaml.Media;
using Windows.Storage.Streams;

namespace TestProject
{
    /// <summary>
    /// 커스텀 혼합 브러시
    /// </summary>
    public class CustomCompositionBrush : XamlCompositionBrushBase
    {
        //////////////////////////////////////////////////////////////////////////////////////////////////// Dependency Property
        ////////////////////////////////////////////////////////////////////////////////////////// Static
        //////////////////////////////////////////////////////////////////////////////// Public

        #region 불선명도 양 속성 - BlurAmountProperty

        /// <summary>
        /// 불선명도 양 속성
        /// </summary>
        public static readonly DependencyProperty BlurAmountProperty = DependencyProperty.Register
        (
            "BlurAmount",
            typeof(double),
            typeof(CustomCompositionBrush),
            new PropertyMetadata
            (
                0.0,
                BlurAmountPropertyChangedCallback
            )
        );

        #endregion
        #region 대비 양 속성 - ContrastAmountProperty

        /// <summary>
        /// 대비 양 속성
        /// </summary>
        public static readonly DependencyProperty ContrastAmountProperty = DependencyProperty.Register
        (
            "ContrastAmount",
            typeof(double),
            typeof(CustomCompositionBrush),
            new PropertyMetadata
            (
                0.0,
                new PropertyChangedCallback(ContrastAmountPropertyChangedCallback)
            )
        );

        #endregion
        #region 채도 양 속성 - SaturationAmountProperty

        /// <summary>
        /// 채도 양 속성
        /// </summary>
        public static readonly DependencyProperty SaturationAmountProperty = DependencyProperty.Register
        (
            "SaturationAmount",
            typeof(double),
            typeof(CustomCompositionBrush),
            new PropertyMetadata
            (
                1.0,
                new PropertyChangedCallback(SaturationAmountPropertyChangedCallback)
            )
        );

        #endregion
        #region 노출 양 속성 - ExposureAmountProperty

        /// <summary>
        /// 노출 양 속성
        /// </summary>
        public static readonly DependencyProperty ExposureAmountProperty = DependencyProperty.Register
        (
            "ExposureAmount",
            typeof(double),
            typeof(CustomCompositionBrush),
            new PropertyMetadata
            (
                0.0,
                new PropertyChangedCallback(ExposureAmountPropertyChangedCallback)
            )
        );

        #endregion
        #region 색조 양 속성 - TintAmountProperty

        /// <summary>
        /// 색조 양 속성
        /// </summary>
        public static readonly DependencyProperty TintAmountProperty = DependencyProperty.Register
        (
            "TintAmount",
            typeof(double),
            typeof(CustomCompositionBrush),
            new PropertyMetadata
            (
                0.0,
                new PropertyChangedCallback(TintAmountPropertyChangedCallback)
            )
        );

        #endregion
        #region 온도 양 속성 - TemperatureAmountProperty

        /// <summary>
        /// 온도 양 속성
        /// </summary>
        public static readonly DependencyProperty TemperatureAmountProperty = DependencyProperty.Register
        (
            "TemperatureAmount",
            typeof(double),
            typeof(CustomCompositionBrush),
            new PropertyMetadata
            (
                0.0,
                new PropertyChangedCallback(TemperatureAmountPropertyChangedCallback)
            )
        );

        #endregion

        //////////////////////////////////////////////////////////////////////////////////////////////////// Field
        ////////////////////////////////////////////////////////////////////////////////////////// Private

        #region Field

        /// <summary>
        /// 이미지 로딩 여부
        /// </summary>
        private bool isImageLoading = false;

        /// <summary>
        /// 로드된 이미지 표면
        /// </summary>
        private LoadedImageSurface loadedImageSurface;

        /// <summary>
        /// 혼합 효과 브러시
        /// </summary>
        private CompositionEffectBrush compositionEffectBrush;

        /// <summary>
        /// 대비 효과
        /// </summary>
        private ContrastEffect contrastEffect;

        /// <summary>
        /// 노출 효과
        /// </summary>
        private ExposureEffect exposureEffect;

        /// <summary>
        /// 온도/색조 효과
        /// </summary>
        private TemperatureAndTintEffect temperatureAndTintEffect;

        /// <summary>
        /// 가우스안 불선명 효과
        /// </summary>
        private GaussianBlurEffect gaussianBlurEffect;

        /// <summary>
        /// 채도 효과
        /// </summary>
        private SaturationEffect saturationEffect;

        #endregion

        //////////////////////////////////////////////////////////////////////////////////////////////////// Property
        ////////////////////////////////////////////////////////////////////////////////////////// Public

        #region 이미지 - Image

        /// <summary>
        /// 이미지
        /// </summary>
        public ICanvasImage Image
        {
            get
            {
                this.contrastEffect.Contrast              = (float)ContrastAmount;
                this.exposureEffect.Exposure              = (float)ExposureAmount;
                this.temperatureAndTintEffect.Tint        = (float)TintAmount;
                this.temperatureAndTintEffect.Temperature = (float)TemperatureAmount;
                this.saturationEffect.Saturation          = (float)SaturationAmount;
                this.gaussianBlurEffect.BlurAmount        = (float)BlurAmount;

                return this.gaussianBlurEffect;
            }
        }

        #endregion

        #region 불선명도 양 - BlurAmount

        /// <summary>
        /// 불선명도 양
        /// </summary>
        public double BlurAmount
        {
            get
            {
                return (double)GetValue(BlurAmountProperty);
            }
            set
            {
                SetValue(BlurAmountProperty, value);
            }
        }

        #endregion
        #region 대비 양 - ContrastAmount

        /// <summary>
        /// 대비 양
        /// </summary>
        public double ContrastAmount
        {
            get
            {
                return (double)GetValue(ContrastAmountProperty);
            }
            set
            {
                SetValue(ContrastAmountProperty, value);
            }
        }

        #endregion
        #region 채도 양 - SaturationAmount

        /// <summary>
        /// 채도 양
        /// </summary>
        public double SaturationAmount
        {
            get
            {
                return (double)GetValue(SaturationAmountProperty);
            }
            set
            {
                SetValue(SaturationAmountProperty, value);
            }
        }

        #endregion
        #region 노출 양 - ExposureAmount

        /// <summary>
        /// 노출 양
        /// </summary>
        public double ExposureAmount
        {
            get
            {
                return (double)GetValue(ExposureAmountProperty);
            }
            set
            {
                SetValue(ExposureAmountProperty, value);
            }
        }

        #endregion
        #region 색조 양 - TintAmount

        /// <summary>
        /// 색조 양
        /// </summary>
        public double TintAmount
        {
            get
            {
                return (double)GetValue(TintAmountProperty);
            }
            set
            {
                SetValue(TintAmountProperty, value);
            }
        }

        #endregion
        #region 온도 양 - TemperatureAmount

        /// <summary>
        /// 온도 양
        /// </summary>
        public double TemperatureAmount
        {
            get
            {
                return (double)GetValue(TemperatureAmountProperty);
            }
            set
            {
                SetValue(TemperatureAmountProperty, value);
            }
        }

        #endregion

        //////////////////////////////////////////////////////////////////////////////////////////////////// Constructor
        ////////////////////////////////////////////////////////////////////////////////////////// Public

        #region 생성자 - CustomCompositionBrush()

        /// <summary>
        /// 생성자
        /// </summary>
        public CustomCompositionBrush()
        {
        }

        #endregion

        //////////////////////////////////////////////////////////////////////////////////////////////////// Method
        ////////////////////////////////////////////////////////////////////////////////////////// Static
        //////////////////////////////////////////////////////////////////////////////// Private

        #region 불선명도 양 속성 변경시 콜백 처리하기 - BlurAmountPropertyChangedCallback(d, e)

        /// <summary>
        /// 불선명도 양 속성 변경시 콜백 처리하기
        /// </summary>
        /// <param name="d">의존 객체</param>
        /// <param name="e">이벤트 인자</param>
        private static void BlurAmountPropertyChangedCallback(DependencyObject d, DependencyPropertyChangedEventArgs e)
        {
            CustomCompositionBrush brush = d as CustomCompositionBrush;

            brush.CompositionBrush?.Properties.InsertScalar("Blur.BlurAmount", (float)(double)e.NewValue);
        }

        #endregion
        #region 대비 양 속성 변경시 콜백 처리하기 - ContrastAmountPropertyChangedCallback(d, e)

        /// <summary>
        /// 대비 양 속성 변경시 콜백 처리하기
        /// </summary>
        /// <param name="d">의존 객체</param>
        /// <param name="e">이벤트 인자</param>
        private static void ContrastAmountPropertyChangedCallback(DependencyObject d, DependencyPropertyChangedEventArgs e)
        {
            CustomCompositionBrush brush = d as CustomCompositionBrush;

            brush.CompositionBrush?.Properties.InsertScalar("ContrastEffect.Contrast", (float)(double)e.NewValue);
        }

        #endregion
        #region 채도 양 속성 변경시 콜백 처리하기 - SaturationAmountPropertyChangedCallback(d, e)

        /// <summary>
        /// 채도 양 속성 변경시 콜백 처리하기
        /// </summary>
        /// <param name="d">의존 객체</param>
        /// <param name="e">이벤트 인자</param>
        private static void SaturationAmountPropertyChangedCallback(DependencyObject d, DependencyPropertyChangedEventArgs e)
        {
            CustomCompositionBrush brush = d as CustomCompositionBrush;

            brush.CompositionBrush?.Properties.InsertScalar("SaturationEffect.Saturation", (float)(double)e.NewValue);
        }

        #endregion
        #region 노출 양 속성 변경시 콜백 처리하기 - ExposureAmountPropertyChangedCallback(d, e)

        /// <summary>
        /// 노출 양 속성 변경시 콜백 처리하기
        /// </summary>
        /// <param name="d">의존 객체</param>
        /// <param name="e">이벤트 인자</param>
        private static void ExposureAmountPropertyChangedCallback(DependencyObject d, DependencyPropertyChangedEventArgs e)
        {
            CustomCompositionBrush brush = d as CustomCompositionBrush;

            brush.CompositionBrush?.Properties.InsertScalar("ExposureEffect.Exposure", (float)(double)e.NewValue);
        }

        #endregion
        #region 색조 양 속성 변경시 콜백 처리하기 - TintAmountPropertyChangedCallback(d, e)

        /// <summary>
        /// 색조 양 속성 변경시 콜백 처리하기
        /// </summary>
        /// <param name="d">의존 객체</param>
        /// <param name="e">이벤트 인자</param>
        private static void TintAmountPropertyChangedCallback(DependencyObject d, DependencyPropertyChangedEventArgs e)
        {
            CustomCompositionBrush brush = d as CustomCompositionBrush;

            brush.CompositionBrush?.Properties.InsertScalar("TemperatureAndTintEffect.Tint", (float)(double)e.NewValue);
        }

        #endregion
        #region 온도 양 속성 변경시 콜백 처리하기 - TemperatureAmountPropertyChangedCallback(d, e)

        /// <summary>
        /// 온도 양 속성 변경시 콜백 처리하기
        /// </summary>
        /// <param name="d">의존 객체</param>
        /// <param name="e">이벤트 인자</param>
        private static void TemperatureAmountPropertyChangedCallback(DependencyObject d, DependencyPropertyChangedEventArgs e)
        {
            CustomCompositionBrush brush = d as CustomCompositionBrush;

            brush.CompositionBrush?.Properties.InsertScalar("TemperatureAndTintEffect.Temperature", (float)(double)e.NewValue);
        }

        #endregion

        ////////////////////////////////////////////////////////////////////////////////////////// Instance
        //////////////////////////////////////////////////////////////////////////////// Public

        #region 소스 설정하기 - SetSource(source)

        /// <summary>
        /// 소스 설정하기
        /// </summary>
        /// <param name="source">소스</param>
        public void SetSource(ICanvasImage source)
        {
            this.saturationEffect.Source = source;
        }

        #endregion
        #region 이미지 로드하기 - LoadImage(filePath)

        /// <summary>
        /// 이미지 로드하기
        /// </summary>
        /// <param name="filePath">파일 경로</param>
        public void LoadImage(string filePath)
        {
            Compositor compositor = Window.Current.Compositor;

            this.loadedImageSurface = LoadedImageSurface.StartLoadFromUri(new Uri(filePath));

            this.loadedImageSurface.LoadCompleted += loadedImageSurface_LoadedCompleted;
        }

        #endregion
        #region 이미지 로드하기 - LoadImage(stream)

        /// <summary>
        /// 이미지 로드하기
        /// </summary>
        /// <param name="stream">스트림</param>
        public void LoadImage(IRandomAccessStream stream)
        {
            if(stream != null && this.isImageLoading == false)
            {
                Compositor compositor = Window.Current.Compositor;

                this.isImageLoading = true;

                this.loadedImageSurface = LoadedImageSurface.StartLoadFromStream(stream);       

                this.loadedImageSurface.LoadCompleted += loadedImageSurface_LoadedCompleted;
            }
        }

        #endregion

        //////////////////////////////////////////////////////////////////////////////// Protected

        #region 연결 취소시 처리하기 - OnDisconnected()

        /// <summary>
        /// 연결 취소시 처리하기
        /// </summary>
        protected override void OnDisconnected()
        {
            if(this.loadedImageSurface != null)
            {
                this.loadedImageSurface.Dispose();

                this.loadedImageSurface = null;
            }

            if(CompositionBrush != null)
            {
                CompositionBrush.Dispose();

                CompositionBrush = null;
            }
        }

        #endregion

        //////////////////////////////////////////////////////////////////////////////// Private

        #region 로드된 이미지 표면 로드 완료시 처리하기 - loadedImageSurface_LoadedCompleted(sender, e)

        /// <summary>
        /// 로드된 이미지 표면 로드 완료시 처리하기
        /// </summary>
        /// <param name="sender">이벤트 발생자</param>
        /// <param name="e">이벤트 인자</param>
        private void loadedImageSurface_LoadedCompleted(LoadedImageSurface sender, LoadedImageSourceLoadCompletedEventArgs e)
        {
            this.isImageLoading = false;

            if(e.Status == LoadedImageSourceLoadStatus.Success)
            {
                Compositor compositor = Window.Current.Compositor;

                CompositionSurfaceBrush brush = compositor.CreateSurfaceBrush(this.loadedImageSurface);

                brush.Stretch = CompositionStretch.UniformToFill;

                this.saturationEffect = new SaturationEffect()
                {
                    Name       = "SaturationEffect",
                    Saturation = (float)SaturationAmount,
                    Source     = new CompositionEffectSourceParameter("image")
                };

                this.contrastEffect = new ContrastEffect()
                {
                    Name     = "ContrastEffect",
                    Contrast = (float)ContrastAmount,
                    Source   = this.saturationEffect
                };

                this.exposureEffect = new ExposureEffect()
                {
                    Name     = "ExposureEffect",
                    Source   = this.contrastEffect,
                    Exposure = (float)ExposureAmount,
                };

                this.temperatureAndTintEffect = new TemperatureAndTintEffect()
                {
                    Name        = "TemperatureAndTintEffect",
                    Source      = this.exposureEffect,
                    Temperature = (float)TemperatureAmount,
                    Tint        = (float)TintAmount
                };

                this.gaussianBlurEffect = new GaussianBlurEffect()
                {
                    Name       = "Blur",
                    Source     = this.temperatureAndTintEffect,
                    BlurAmount = (float)BlurAmount,
                    BorderMode = EffectBorderMode.Hard,
                };

                CompositionEffectFactory factory = compositor.CreateEffectFactory
                (
                    this.gaussianBlurEffect,
                    new[]
                    {
                        "SaturationEffect.Saturation",
                        "ExposureEffect.Exposure",
                        "Blur.BlurAmount",
                        "TemperatureAndTintEffect.Temperature",
                        "TemperatureAndTintEffect.Tint",
                        "ContrastEffect.Contrast"
                    }
                );

                this.compositionEffectBrush = factory.CreateBrush();

                this.compositionEffectBrush.SetSourceParameter("image", brush);

                CompositionBrush = this.compositionEffectBrush;
            }
            else
            {
                LoadImage("ms-appx:///Assets/StoreLogo.png");
            }
        }

        #endregion
    }
}

 

▶ DetailPage.xaml

<Page x:Class="TestProject.DetailPage"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:muxc="using:Microsoft.UI.Xaml.Controls"
    xmlns:local="using:TestProject"
    RequestedTheme="Dark">
    <Page.Resources>
        <Flyout x:Key="zoomFlyoutKey">
            <StackPanel>
                <StackPanel.Resources>
                    <Style TargetType="Button">
                        <Setter Property="HorizontalAlignment" Value="Stretch" />
                        <Setter Property="Margin"              Value="0 2"     />
                    </Style>
                </StackPanel.Resources>
                <Slider Name="zoomSlider"
                    Margin="0 5 0 0"
                    StepFrequency="0.1"
                    Width="100"
                    Minimum="0.1"
                    Maximum="5"
                    Value="1"
                    ValueChanged="zoomSlider_ValueChanged" />
                <Button
                    Content="화면 맞춤"
                    Click="{x:Bind FitToScreen}" />
                <Button
                    Content="실제 크기 표시"
                    Click="{x:Bind ShowActualSize}" />
            </StackPanel>
        </Flyout>
        <ControlTemplate x:Key="FancySliderControlTemplateKey" TargetType="Slider"
            xmlns:contract6Present="http://schemas.microsoft.com/winfx/2006/xaml/presentation?IsApiContractPresent(Windows.Foundation.UniversalApiContract,6)"
            xmlns:contract6NotPresent="http://schemas.microsoft.com/winfx/2006/xaml/presentation?IsApiContractNotPresent(Windows.Foundation.UniversalApiContract,6)"
            xmlns:contract7Present="http://schemas.microsoft.com/winfx/2006/xaml/presentation?IsApiContractPresent(Windows.Foundation.UniversalApiContract,7)"
            xmlns:contract7NotPresent="http://schemas.microsoft.com/winfx/2006/xaml/presentation?IsApiContractNotPresent(Windows.Foundation.UniversalApiContract,7)">
            <Grid Margin="{TemplateBinding Padding}">
                <Grid.Resources>
                    <Style x:Key="SliderThumbStyleKey" TargetType="Thumb">
                        <Setter Property="BorderThickness" Value="0"                                     />
                        <Setter Property="Background"      Value="{ThemeResource SliderThumbBackground}" />
                        <Setter Property="Template">
                            <Setter.Value>
                                <ControlTemplate TargetType="Thumb">
                                    <Border
                                        BorderThickness="{TemplateBinding BorderThickness}"
                                        BorderBrush="{TemplateBinding BorderBrush}"
                                        CornerRadius="{ThemeResource SliderThumbCornerRadius}"
                                        Background="{TemplateBinding Background}" />
                                </ControlTemplate>
                            </Setter.Value>
                        </Setter>
                    </Style>
                </Grid.Resources>
                <VisualStateManager.VisualStateGroups>
                    <VisualStateGroup x:Name="CommonStates">
                        <VisualState x:Name="Normal" />
                        <VisualState x:Name="Pressed">
                            <Storyboard>
                                <ObjectAnimationUsingKeyFrames
                                    Storyboard.TargetName="HorizontalTrackRect"
                                    Storyboard.TargetProperty="Fill">
                                    <DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource SliderTrackFillPressed}" />
                                </ObjectAnimationUsingKeyFrames>
                                <ObjectAnimationUsingKeyFrames
                                    Storyboard.TargetName="VerticalTrackRect"
                                    Storyboard.TargetProperty="Fill">
                                    <DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource SliderTrackFillPressed}" />
                                </ObjectAnimationUsingKeyFrames>
                                <ObjectAnimationUsingKeyFrames
                                    Storyboard.TargetName="HorizontalThumb"
                                    Storyboard.TargetProperty="Background">
                                    <DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource SliderThumbBackgroundPressed}" />
                                </ObjectAnimationUsingKeyFrames>
                                <ObjectAnimationUsingKeyFrames
                                    Storyboard.TargetName="VerticalThumb"
                                    Storyboard.TargetProperty="Background">
                                    <DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource SliderThumbBackgroundPressed}" />
                                </ObjectAnimationUsingKeyFrames>
                                <ObjectAnimationUsingKeyFrames
                                    Storyboard.TargetName="SliderContainer"
                                    Storyboard.TargetProperty="Background">
                                    <DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource SliderContainerBackgroundPressed}" />
                                </ObjectAnimationUsingKeyFrames>
                                <ObjectAnimationUsingKeyFrames
                                    Storyboard.TargetName="HorizontalDecreaseRect"
                                    Storyboard.TargetProperty="Fill">
                                    <DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource SliderTrackValueFillPressed}" />
                                </ObjectAnimationUsingKeyFrames>
                                <ObjectAnimationUsingKeyFrames
                                    Storyboard.TargetName="VerticalDecreaseRect"
                                    Storyboard.TargetProperty="Fill">
                                    <DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource SliderTrackValueFillPressed}" />
                                </ObjectAnimationUsingKeyFrames>
                            </Storyboard>
                        </VisualState>
                        <VisualState x:Name="Disabled">
                            <Storyboard>
                                <ObjectAnimationUsingKeyFrames
                                    Storyboard.TargetName="headerContentPresenter"
                                    Storyboard.TargetProperty="Foreground">
                                    <DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource SliderHeaderForegroundDisabled}" />
                                </ObjectAnimationUsingKeyFrames>
                                <ObjectAnimationUsingKeyFrames
                                    Storyboard.TargetName="HorizontalDecreaseRect"
                                    Storyboard.TargetProperty="Fill">
                                    <DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource SliderTrackValueFillDisabled}" />
                                </ObjectAnimationUsingKeyFrames>
                                <ObjectAnimationUsingKeyFrames
                                    Storyboard.TargetName="HorizontalTrackRect"
                                    Storyboard.TargetProperty="Fill">
                                    <DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource SliderTrackFillDisabled}" />
                                </ObjectAnimationUsingKeyFrames>
                                <ObjectAnimationUsingKeyFrames
                                    Storyboard.TargetName="VerticalDecreaseRect"
                                    Storyboard.TargetProperty="Fill">
                                    <DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource SliderTrackValueFillDisabled}" />
                                </ObjectAnimationUsingKeyFrames>
                                <ObjectAnimationUsingKeyFrames
                                    Storyboard.TargetName="VerticalTrackRect"
                                    Storyboard.TargetProperty="Fill">
                                    <DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource SliderTrackFillDisabled}" />
                                </ObjectAnimationUsingKeyFrames>
                                <ObjectAnimationUsingKeyFrames
                                    Storyboard.TargetName="HorizontalThumb"
                                    Storyboard.TargetProperty="Background">
                                    <DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource SliderThumbBackgroundDisabled}" />
                                </ObjectAnimationUsingKeyFrames>
                                <ObjectAnimationUsingKeyFrames
                                    Storyboard.TargetName="VerticalThumb"
                                    Storyboard.TargetProperty="Background">
                                    <DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource SliderThumbBackgroundDisabled}" />
                                </ObjectAnimationUsingKeyFrames>
                                <ObjectAnimationUsingKeyFrames
                                    Storyboard.TargetName="TopTickBar"
                                    Storyboard.TargetProperty="Fill">
                                    <DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource SliderTickBarFillDisabled}" />
                                </ObjectAnimationUsingKeyFrames>
                                <ObjectAnimationUsingKeyFrames
                                    Storyboard.TargetName="BottomTickBar"
                                    Storyboard.TargetProperty="Fill">
                                    <DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource SliderTickBarFillDisabled}" />
                                </ObjectAnimationUsingKeyFrames>
                                <ObjectAnimationUsingKeyFrames
                                    Storyboard.TargetName="LeftTickBar"
                                    Storyboard.TargetProperty="Fill">
                                    <DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource SliderTickBarFillDisabled}" />
                                </ObjectAnimationUsingKeyFrames>
                                <ObjectAnimationUsingKeyFrames
                                    Storyboard.TargetName="RightTickBar"
                                    Storyboard.TargetProperty="Fill">
                                    <DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource SliderTickBarFillDisabled}" />
                                </ObjectAnimationUsingKeyFrames>
                                <ObjectAnimationUsingKeyFrames
                                    Storyboard.TargetName="SliderContainer"
                                    Storyboard.TargetProperty="Background">
                                    <DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource SliderContainerBackgroundDisabled}" />
                                </ObjectAnimationUsingKeyFrames>
                            </Storyboard>
                        </VisualState>
                        <VisualState x:Name="PointerOver">
                            <Storyboard>
                                <ObjectAnimationUsingKeyFrames
                                    Storyboard.TargetName="HorizontalTrackRect"
                                    Storyboard.TargetProperty="Fill">
                                    <DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource SliderTrackFillPointerOver}" />
                                </ObjectAnimationUsingKeyFrames>
                                <ObjectAnimationUsingKeyFrames
                                    Storyboard.TargetName="VerticalTrackRect"
                                    Storyboard.TargetProperty="Fill">
                                    <DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource SliderTrackFillPointerOver}" />
                                </ObjectAnimationUsingKeyFrames>
                                <ObjectAnimationUsingKeyFrames
                                    Storyboard.TargetName="HorizontalThumb"
                                    Storyboard.TargetProperty="Background">
                                    <DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource SliderThumbBackgroundPointerOver}" />
                                </ObjectAnimationUsingKeyFrames>
                                <ObjectAnimationUsingKeyFrames
                                    Storyboard.TargetName="VerticalThumb"
                                    Storyboard.TargetProperty="Background">
                                    <DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource SliderThumbBackgroundPointerOver}" />
                                </ObjectAnimationUsingKeyFrames>
                                <ObjectAnimationUsingKeyFrames
                                    Storyboard.TargetName="SliderContainer"
                                    Storyboard.TargetProperty="Background">
                                    <DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource SliderContainerBackgroundPointerOver}" />
                                </ObjectAnimationUsingKeyFrames>
                                <ObjectAnimationUsingKeyFrames
                                    Storyboard.TargetName="HorizontalDecreaseRect"
                                    Storyboard.TargetProperty="Fill">
                                    <DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource SliderTrackValueFillPointerOver}" />
                                </ObjectAnimationUsingKeyFrames>
                                <ObjectAnimationUsingKeyFrames
                                    Storyboard.TargetName="VerticalDecreaseRect"
                                    Storyboard.TargetProperty="Fill">
                                    <DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource SliderTrackValueFillPointerOver}" />
                                </ObjectAnimationUsingKeyFrames>
                            </Storyboard>
                        </VisualState>
                    </VisualStateGroup>
                    <VisualStateGroup x:Name="FocusEngagementStates">
                        <VisualState x:Name="FocusDisengaged" />
                        <VisualState x:Name="FocusEngagedHorizontal">
                            <Storyboard>
                                <ObjectAnimationUsingKeyFrames
                                    Storyboard.TargetName="SliderContainer"
                                    Storyboard.TargetProperty="(Control.IsTemplateFocusTarget)">
                                    <DiscreteObjectKeyFrame KeyTime="0" Value="False" />
                                </ObjectAnimationUsingKeyFrames>
                                <ObjectAnimationUsingKeyFrames
                                    Storyboard.TargetName="HorizontalThumb"
                                    Storyboard.TargetProperty="(Control.IsTemplateFocusTarget)">
                                    <DiscreteObjectKeyFrame KeyTime="0" Value="True" />
                                </ObjectAnimationUsingKeyFrames>
                            </Storyboard>
                        </VisualState>
                        <VisualState x:Name="FocusEngagedVertical">
                            <Storyboard>
                                <ObjectAnimationUsingKeyFrames
                                    Storyboard.TargetName="SliderContainer"
                                    Storyboard.TargetProperty="(Control.IsTemplateFocusTarget)">
                                    <DiscreteObjectKeyFrame KeyTime="0" Value="False" />
                                </ObjectAnimationUsingKeyFrames>
                                <ObjectAnimationUsingKeyFrames
                                    Storyboard.TargetName="VerticalThumb"
                                    Storyboard.TargetProperty="(Control.IsTemplateFocusTarget)">
                                    <DiscreteObjectKeyFrame KeyTime="0" Value="True" />
                                </ObjectAnimationUsingKeyFrames>
                            </Storyboard>
                        </VisualState>
                    </VisualStateGroup>
                </VisualStateManager.VisualStateGroups>
                <Grid.RowDefinitions>
                    <RowDefinition Height="Auto" />
                    <RowDefinition Height="*"    />
                </Grid.RowDefinitions>
                <ContentPresenter Name="headerContentPresenter" Grid.Row="0"
                    ContentTemplate="{TemplateBinding HeaderTemplate}"
                    Margin="{ThemeResource SliderTopHeaderMargin}"
                    x:DeferLoadStrategy="Lazy"
                    Foreground="{ThemeResource SliderHeaderForeground}"
                    FontWeight="{ThemeResource SliderHeaderThemeFontWeight}"
                    TextWrapping="Wrap"
                    Visibility="Collapsed"
                    Content="{TemplateBinding Header}" />
                <Grid Name="SliderContainer" Grid.Row="1"
                    Background="{ThemeResource SliderContainerBackground}"
                    Control.IsTemplateFocusTarget="True">
                    <Grid Name="HorizontalTemplate"
                        MinHeight="{ThemeResource SliderHorizontalHeight}">
                        <Grid.ColumnDefinitions>
                            <ColumnDefinition Width="Auto" />
                            <ColumnDefinition Width="Auto" />
                            <ColumnDefinition Width="*"    />
                        </Grid.ColumnDefinitions>
                        <Grid.RowDefinitions>
                            <RowDefinition Height="{ThemeResource SliderPreContentMargin}"  />
                            <RowDefinition Height="Auto"                                    />
                            <RowDefinition Height="{ThemeResource SliderPostContentMargin}" />
                        </Grid.RowDefinitions>
                        <Polygon Grid.Row="0" Grid.RowSpan="3" Grid.ColumnSpan="3"
                            HorizontalAlignment="Stretch"
                            VerticalAlignment="Center"
                            Height="20"
                            Fill="{TemplateBinding Background}"
                            Stretch="Fill"
                            Points="0 20 200 20 200 0" />
                        <Rectangle Name="HorizontalTrackRect" Grid.Row="1" Grid.ColumnSpan="3"
                            Height="{ThemeResource SliderTrackThemeHeight}"
                            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}}" />
                        <Rectangle Name="HorizontalDecreaseRect" Grid.Row="1"
                            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}" />
                        <TickBar Name="TopTickBar" Grid.ColumnSpan="3"
                            Margin="0 0 0 4"
                            VerticalAlignment="Bottom"
                            Height="{ThemeResource SliderOutsideTickBarThemeHeight}"
                            Fill="{ThemeResource SliderTickBarFill}"
                            Visibility="Collapsed" />
                        <TickBar Name="HorizontalInlineTickBar" Grid.Row="1" Grid.ColumnSpan="3"
                            Height="{ThemeResource SliderTrackThemeHeight}"
                            Fill="{ThemeResource SliderInlineTickBarFill}"
                            Visibility="Collapsed" />
                        <TickBar Name="BottomTickBar" Grid.Row="2" Grid.ColumnSpan="3"
                            Margin="0 4 0 0"
                            VerticalAlignment="Top"
                            Height="{ThemeResource SliderOutsideTickBarThemeHeight}"
                            Fill="{ThemeResource SliderTickBarFill}"
                            Visibility="Collapsed" />
                        <Thumb Name="HorizontalThumb" Grid.Row="0" Grid.RowSpan="3" Grid.Column="1"
                            Style="{StaticResource SliderThumbStyleKey}"
                            Width="{ThemeResource SliderHorizontalThumbWidth}"
                            Height="{ThemeResource SliderHorizontalThumbHeight}"
                            FocusVisualMargin="-14 -6 -14 -6"
                            AutomationProperties.AccessibilityView="Raw"
                            DataContext="{TemplateBinding Value}" />
                    </Grid>
                    <Grid Name="VerticalTemplate"
                        MinWidth="{ThemeResource SliderVerticalWidth}"
                        Visibility="Collapsed">
                        <Grid.RowDefinitions>
                            <RowDefinition Height="*"    />
                            <RowDefinition Height="Auto" />
                            <RowDefinition Height="Auto" />
                        </Grid.RowDefinitions>
                        <Grid.ColumnDefinitions>
                            <ColumnDefinition Width="{ThemeResource SliderPreContentMargin}"  />
                            <ColumnDefinition Width="Auto"                                    />
                            <ColumnDefinition Width="{ThemeResource SliderPostContentMargin}" />
                        </Grid.ColumnDefinitions>
                        <Rectangle Name="VerticalTrackRect" Grid.RowSpan="3" Grid.Column="1"
                            Width="{ThemeResource SliderTrackThemeHeight}"
                            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 Background}" />
                        <Rectangle Name="VerticalDecreaseRect" Grid.Row="2" Grid.Column="1"
                            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}" />
                        <TickBar Name="LeftTickBar" Grid.RowSpan="3"
                            Margin="0 0 4 0"
                            HorizontalAlignment="Right"
                            Width="{ThemeResource SliderOutsideTickBarThemeHeight}"
                            Fill="{ThemeResource SliderTickBarFill}"
                            Visibility="Collapsed" />
                        <TickBar Name="VerticalInlineTickBar" Grid.RowSpan="3" Grid.Column="1"
                            Width="{ThemeResource SliderTrackThemeHeight}"
                            Fill="{ThemeResource SliderInlineTickBarFill}"
                            Visibility="Collapsed" />
                        <TickBar Name="RightTickBar" Grid.RowSpan="3" Grid.Column="2"
                            Margin="4 0 0 0"
                            HorizontalAlignment="Left"
                            Width="{ThemeResource SliderOutsideTickBarThemeHeight}"
                            Fill="{ThemeResource SliderTickBarFill}"
                            Visibility="Collapsed" />
                        <Thumb Name="VerticalThumb" Grid.Row="1" Grid.Column="0" Grid.ColumnSpan="3"
                            Style="{StaticResource SliderThumbStyleKey}"
                            Width="{ThemeResource SliderVerticalThumbWidth}"
                            Height="{ThemeResource SliderVerticalThumbHeight}"
                            FocusVisualMargin="-6 -14 -6 -14"
                            AutomationProperties.AccessibilityView="Raw"
                            DataContext="{TemplateBinding Value}" />
                    </Grid>
                </Grid>
            </Grid>
        </ControlTemplate>
    </Page.Resources>
    <RelativePanel Background="{ThemeResource ApplicationPageBackgroundThemeBrush}">
        <StackPanel Name="titlePanel" Orientation="Horizontal">
            <TextBlock Name="titleTextBlock"
                Style="{StaticResource TitleTextBlockStyle}"
                Margin="24 0 0 24"
                Text="{x:Bind imageFileInfo.ImageTitle, Mode=OneWay}" />
            <TextBlock
                Style="{StaticResource TitleTextBlockStyle}"
                Visibility="{x:Bind imageFileInfo.NeedsSaved, Mode=OneWay}"
                Text="*" />
        </StackPanel>
        <CommandBar Name="mainCommandBar"
            Background="{ThemeResource ApplicationPageBackgroundThemeBrush}"
            RelativePanel.AlignRightWithPanel="True"
            RelativePanel.AlignTopWithPanel="True"
            OverflowButtonVisibility="Collapsed"
            DefaultLabelPosition="Right">
            <AppBarButton Name="zoomButton"
                Icon="Zoom"
                Label="Zoom"
                Flyout="{StaticResource zoomFlyoutKey}" />
            <AppBarToggleButton Name="editButton"
                Icon="Edit"
                Label="Edit"
                Click="{x:Bind ToggleEditState}" />
        </CommandBar>
        <SplitView Name="mainSplitView"
            DisplayMode="Inline"
            PanePlacement="Right"
            RelativePanel.Below="mainCommandBar"
            RelativePanel.AlignLeftWithPanel="True"
            RelativePanel.AlignRightWithPanel="True"
            RelativePanel.AlignBottomWithPanel="True">
            <Grid>
                <Image Name="targetImage"
                    Stretch="None" />
                <ScrollViewer Name="imageScrollVierwer"
                    HorizontalAlignment="Stretch"
                    VerticalAlignment="Stretch"
                    ZoomMode="Enabled"
                    HorizontalScrollMode="Auto"
                    HorizontalScrollBarVisibility="Auto"
                    ViewChanged="imageScrollViewer_ViewChanged">
                    <Rectangle Name="imageRectangle"
                        Width="{x:Bind imageFileInfo.ImageProperties.Width, Mode=OneWay}"
                        Height="{x:Bind imageFileInfo.ImageProperties.Height, Mode=OneWay}"
                        x:Load="True">
                        <Rectangle.Fill>
                            <local:CustomCompositionBrush x:Name="customCompositionBrush"
                                BlurAmount="{x:Bind imageFileInfo.Blur, Mode=OneWay}"
                                SaturationAmount="{x:Bind imageFileInfo.Saturation, Mode=OneWay}"
                                ContrastAmount="{x:Bind imageFileInfo.Contrast, Mode=OneWay}"
                                ExposureAmount="{x:Bind imageFileInfo.Exposure, Mode=OneWay}"
                                TintAmount="{x:Bind imageFileInfo.Tint, Mode=OneWay}"
                                TemperatureAmount="{x:Bind imageFileInfo.Temperature, Mode=OneWay}" />
                        </Rectangle.Fill>
                    </Rectangle>
                </ScrollViewer>
            </Grid>
            <SplitView.Pane>
                <ScrollViewer>
                    <Grid Name="editControlGrid"
                        Margin="24 48 24 24"
                        HorizontalAlignment="Stretch"
                        x:Load="{x:Bind mainSplitView.IsPaneOpen, Mode=OneWay}">
                        <Grid.Resources>
                            <x:Double x:Key="SliderTrackThemeHeight">4</x:Double>
                            <Style TargetType="Slider" BasedOn="{StaticResource DefaultSliderStyle}">
                                <Setter Property="Margin"          Value="0 0 0 0" />
                                <Setter Property="Padding"         Value="0"       />
                                <Setter Property="MinWidth"        Value="100"     />
                                <Setter Property="StepFrequency"   Value="0.1"     />
                                <Setter Property="TickFrequency"   Value="0.1"     />
                                <Setter Property="Grid.ColumnSpan" Value="2"       />
                            </Style>
                            <Style x:Key="ValueTextBlockStyleKey" TargetType="TextBlock">
                                <Setter Property="HorizontalAlignment" Value="Right" />
                                <Setter Property="Grid.Column"         Value="1"     />
                            </Style>
                        </Grid.Resources>
                        <Grid.ColumnDefinitions>
                            <ColumnDefinition MinWidth="144" />
                            <ColumnDefinition />
                        </Grid.ColumnDefinitions>
                        <Grid.RowDefinitions>
                            <RowDefinition Height="Auto" />
                            <RowDefinition Height="Auto" />
                            <RowDefinition Height="Auto" />
                            <RowDefinition Height="Auto" />
                            <RowDefinition Height="Auto" />
                            <RowDefinition Height="Auto" />
                            <RowDefinition Height="Auto" />
                            <RowDefinition Height="Auto" />
                            <RowDefinition Height="*"    />
                            <RowDefinition Height="*"    />
                        </Grid.RowDefinitions>
                        <TextBox Name="titleEditBox" Grid.ColumnSpan="2"
                            Height="80"
                            Padding="12"
                            Header="제목"
                            Text="{x:Bind imageFileInfo.ImageTitle, Mode=TwoWay}" />
                        <muxc:RatingControl Grid.Row="1" Grid.ColumnSpan="2"
                            Margin="0 0 0 16"
                            Padding="12"
                            Value="{x:Bind imageFileInfo.ImageRating, Mode=TwoWay}" />
                        <Slider Header="노출" Grid.Row="2"
                            Template="{StaticResource FancySliderControlTemplateKey}"
                            Foreground="Transparent"
                            Minimum="-2"
                            Maximum="2"
                            Value="{x:Bind imageFileInfo.Exposure, Mode=TwoWay}">
                            <Slider.Background>
                                <LinearGradientBrush
                                    StartPoint="0 0.5"
                                    EndPoint="1 0.5">
                                    <LinearGradientBrush.GradientStops>
                                        <GradientStop Offset="0" Color="Black" />
                                        <GradientStop Offset="1" Color="White" />
                                    </LinearGradientBrush.GradientStops>
                                </LinearGradientBrush>
                            </Slider.Background>
                        </Slider>
                        <TextBlock Grid.Row="2"
                            Style="{StaticResource ValueTextBlockStyleKey}"
                            Text="{x:Bind imageFileInfo.Exposure.ToString('N', cultureInfo), Mode=OneWay}" />
                        <Slider Header="온도" Grid.Row="3"
                            Template="{StaticResource FancySliderControlTemplateKey}"
                            Foreground="Transparent"
                            Minimum="-1"
                            Maximum="1"
                            Value="{x:Bind imageFileInfo.Temperature, Mode=TwoWay}">
                            <Slider.Background>
                                <LinearGradientBrush
                                    StartPoint="0 0.5"
                                    EndPoint="1 0.5">
                                    <LinearGradientBrush.GradientStops>
                                        <GradientStop Offset="0" Color="Blue"   />
                                        <GradientStop Offset="1" Color="Yellow" />
                                    </LinearGradientBrush.GradientStops>
                                </LinearGradientBrush>
                            </Slider.Background>
                        </Slider>
                        <TextBlock Grid.Row="3"
                            Style="{StaticResource ValueTextBlockStyleKey}"
                            Text="{x:Bind imageFileInfo.Temperature.ToString('N', cultureInfo), Mode=OneWay}" />
                        <Slider Header="색조" Grid.Row="4"
                            Template="{StaticResource FancySliderControlTemplateKey}"
                            Foreground="Transparent"
                            Minimum="-1"
                            Maximum="1"
                            Value="{x:Bind imageFileInfo.Tint, Mode=TwoWay}">
                            <Slider.Background>
                                <LinearGradientBrush
                                    StartPoint="0 0.5"
                                    EndPoint="1 0.5">
                                    <LinearGradientBrush.GradientStops>
                                        <GradientStop Offset="0" Color="Red"   />
                                        <GradientStop Offset="1" Color="Green" />
                                    </LinearGradientBrush.GradientStops>
                                </LinearGradientBrush>
                            </Slider.Background>
                        </Slider>
                        <TextBlock Grid.Row="4"
                            Style="{StaticResource ValueTextBlockStyleKey}"
                            Text="{x:Bind imageFileInfo.Tint.ToString('N', cultureInfo), Mode=OneWay}" />
                        <Slider Header="대비" Grid.Row="5"
                            Foreground="Transparent"
                            Minimum="-1"
                            Maximum="1"
                            Value="{x:Bind imageFileInfo.Contrast, Mode=TwoWay}" />
                        <TextBlock Grid.Row="5"
                            Style="{StaticResource ValueTextBlockStyleKey}"
                            Text="{x:Bind imageFileInfo.Contrast.ToString('N', cultureInfo), Mode=OneWay}" />
                        <Slider Header="채도" Grid.Row="6"
                            Minimum="0"
                            Maximum="1"
                            Value="{x:Bind imageFileInfo.Saturation, Mode=TwoWay}" />
                        <TextBlock Grid.Row="6"
                            Style="{StaticResource ValueTextBlockStyleKey}"
                            Text="{x:Bind imageFileInfo.Saturation.ToString('N', cultureInfo), Mode=OneWay}" />
                        <Slider Header="불선명도" Grid.Row="7"
                            Minimum="0"
                            Maximum="5"
                            Value="{x:Bind imageFileInfo.Blur, Mode=TwoWay}" />
                        <TextBlock Grid.Row="7"
                            Style="{StaticResource ValueTextBlockStyleKey}"
                            Text="{x:Bind imageFileInfo.Blur.ToString('N', cultureInfo), Mode=OneWay}" />
                        <Image Name="smallImage" Grid.Row="8" Grid.ColumnSpan="2"
                            Width="300"
                            Height="200"
                            Loaded="SmallImage_Loaded" />
                        <Grid Grid.Row="9" Grid.ColumnSpan="2"
                            VerticalAlignment="Bottom">
                            <Grid.ColumnDefinitions>
                                <ColumnDefinition />
                                <ColumnDefinition />
                            </Grid.ColumnDefinitions>
                            <Grid.RowDefinitions>
                                <RowDefinition Height="Auto" />
                                <RowDefinition Height="Auto" />
                            </Grid.RowDefinitions>
                            <Button Content="초기화"
                                Margin="0 0 2 0"
                                HorizontalAlignment="Stretch"
                                Click="{x:Bind ResetEffects}" />
                            <Button Content="저장..." Grid.Column="1"
                                Margin="2 0 0 0"
                                HorizontalAlignment="Stretch"
                                Click="{x:Bind ExportImage}" />
                        </Grid>
                    </Grid>
                </ScrollViewer>
            </SplitView.Pane>
        </SplitView>
    </RelativePanel>
</Page>

 

▶ DetailPage.xaml.cs

using Microsoft.Graphics.Canvas;
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Threading.Tasks;
using Windows.Storage;
using Windows.Storage.Pickers;
using Windows.Storage.Streams;
using Windows.UI.Core;
using Windows.UI.Xaml;
using Windows.UI.Xaml.Controls;
using Windows.UI.Xaml.Controls.Primitives;
using Windows.UI.Xaml.Media.Animation;
using Windows.UI.Xaml.Media.Imaging;
using Windows.UI.Xaml.Navigation;

namespace TestProject
{
    /// <summary>
    /// 상세 페이지
    /// </summary>
    public sealed partial class DetailPage : Page
    {
        //////////////////////////////////////////////////////////////////////////////////////////////////// Field
        ////////////////////////////////////////////////////////////////////////////////////////// Private

        #region Field

        /// <summary>
        /// 문화 정보
        /// </summary>
        private CultureInfo cultureInfo = CultureInfo.CurrentCulture;

        /// <summary>
        /// 이미지 파일 정보
        /// </summary>
        private ImageFileInfo imageFileInfo;

        /// <summary>
        /// 미저장 변경시 탐색 가능 여부
        /// </summary>
        private bool canNavigateWithUnsavedChanges = false;

        /// <summary>
        /// 비트맵 이미지
        /// </summary>
        private BitmapImage bitmapImage = null;

        #endregion

        //////////////////////////////////////////////////////////////////////////////////////////////////// Constructor
        ////////////////////////////////////////////////////////////////////////////////////////// Public

        #region 생성자 - DetailPage()

        /// <summary>
        /// 생성자
        /// </summary>
        public DetailPage()
        {
            InitializeComponent();
        }

        #endregion

        //////////////////////////////////////////////////////////////////////////////////////////////////// Method
        ////////////////////////////////////////////////////////////////////////////////////////// Protected
        //////////////////////////////////////////////////////////////////////////////// Function

        #region 탐색되는 경우 처리하기 - OnNavigatedTo(e)

        /// <summary>
        /// 탐색되는 경우 처리하기
        /// </summary>
        /// <param name="e">이벤트 인자</param>
        protected override async void OnNavigatedTo(NavigationEventArgs e)
        {
            this.imageFileInfo = e.Parameter as ImageFileInfo;

            this.canNavigateWithUnsavedChanges = false;

            this.bitmapImage = await this.imageFileInfo.GetBitmapImageAsyc();

            if(this.imageFileInfo != null)
            {
                ConnectedAnimation animation = ConnectedAnimationService.GetForCurrentView().GetAnimation("itemAnimation");

                if(animation != null)
                {
                    this.targetImage.Source = this.bitmapImage;

                    animation.Completed += async (s, _) =>
                    {
                        await LoadBrushAsync();

                        await Task.Delay(300);

                        this.targetImage.Source = null;
                    };

                    animation.TryStart(this.targetImage);
                }

                if (this.bitmapImage.PixelHeight == 0 && this.bitmapImage.PixelWidth == 0)
                {
                    this.editButton.IsEnabled = false;
                    this.zoomButton.IsEnabled = false;
                };
            }

            if(Frame.CanGoBack)
            {
                SystemNavigationManager.GetForCurrentView().AppViewBackButtonVisibility = AppViewBackButtonVisibility.Visible;
            }
            else
            {
                SystemNavigationManager.GetForCurrentView().AppViewBackButtonVisibility = AppViewBackButtonVisibility.Collapsed;
            }

            base.OnNavigatedTo(e);
        }

        #endregion
        #region 탐색하는 경우 처리하기 - OnNavigatingFrom(e)

        /// <summary>
        /// 탐색하는 경우 처리하기
        /// </summary>
        /// <param name="e">이벤트 인자</param>
        protected override void OnNavigatingFrom(NavigatingCancelEventArgs e)
        {
            if(this.imageFileInfo.NeedsSaved && !this.canNavigateWithUnsavedChanges)
            {
                e.Cancel = true;

                ShowSaveDialog(e);
            }
            else
            {
                if(e.NavigationMode == NavigationMode.Back)
                {
                    ConnectedAnimationService.GetForCurrentView().PrepareToAnimate("backAnimation", this.imageRectangle);
                }

                this.canNavigateWithUnsavedChanges = false;

                base.OnNavigatingFrom(e);
            }
        }

        #endregion

        ////////////////////////////////////////////////////////////////////////////////////////// Private
        //////////////////////////////////////////////////////////////////////////////// Event

        #region 확대/축소 슬라이더 값 변경시 처리하기 - zoomSlider_ValueChanged(sender, e)

        /// <summary>
        /// 확대/축소 슬라이더 값 변경시 처리하기
        /// </summary>
        /// <param name="sender">이벤트 발생자</param>
        /// <param name="e">이벤트 인자</param>
        private void zoomSlider_ValueChanged(object sender, RangeBaseValueChangedEventArgs e)
        {
            if(this.imageScrollVierwer != null)
            {
                this.imageScrollVierwer.ChangeView(null, null, (float)e.NewValue);
            }
        }

        #endregion
        #region 이미지 스크롤 뷰어 값 변경시 처리하기 - imageScrollViewer_ViewChanged(sender, e)

        /// <summary>
        /// 이미지 스크롤 뷰어 값 변경시 처리하기
        /// </summary>
        /// <param name="sender">이벤트 발생자</param>
        /// <param name="e">이벤트 인자</param>
        private void imageScrollViewer_ViewChanged(object sender, ScrollViewerViewChangedEventArgs e)
        {
            this.zoomSlider.Value = ((ScrollViewer)sender).ZoomFactor;
        }

        #endregion
        #region 작은 이미지 로드시 처리하기 - SmallImage_Loaded(sender, e)

        /// <summary>
        /// 작은 이미지 로드시 처리하기
        /// </summary>
        /// <param name="sender">이벤트 발생자</param>
        /// <param name="e">이벤트 인자</param>
        private void SmallImage_Loaded(object sender, RoutedEventArgs e)
        {
            this.smallImage.Source = this.bitmapImage;
        }

        #endregion

        //////////////////////////////////////////////////////////////////////////////// Function

        #region 화면에 맞추기 - FitToScreen()

        /// <summary>
        /// 화면에 맞추기
        /// </summary>
        private void FitToScreen()
        {
            float zoomFactor = (float)Math.Min
            (
                this.imageScrollVierwer.ActualWidth  / this.imageFileInfo.ImageProperties.Width,
                this.imageScrollVierwer.ActualHeight / this.imageFileInfo.ImageProperties.Height
            );

            this.imageScrollVierwer.ChangeView(null, null, zoomFactor);
        }

        #endregion
        #region 실제 크기 표시하기 - ShowActualSize()

        /// <summary>
        /// 실제 크기 표시하기
        /// </summary>
        private void ShowActualSize()
        {
            this.imageScrollVierwer.ChangeView(null, null, 1);
        }

        #endregion
        #region 편집 상태 토글하기 - ToggleEditState()

        /// <summary>
        /// 편집 상태 토글하기
        /// </summary>
        private void ToggleEditState()
        {
            this.mainSplitView.IsPaneOpen = !this.mainSplitView.IsPaneOpen;
        }

        #endregion
        #region 효과 초기화하기 - ResetEffects()

        /// <summary>
        /// 효과 초기화하기
        /// </summary>
        private void ResetEffects()
        {
            this.imageFileInfo.Exposure    = 0f;
            this.imageFileInfo.Blur        = 0f;
            this.imageFileInfo.Tint        = 0f;
            this.imageFileInfo.Temperature = 0f;
            this.imageFileInfo.Contrast    = 0f;
            this.imageFileInfo.Saturation  = 1f;
        }

        #endregion
        #region 브러시 로드하기 (비동기) - LoadBrushAsync()

        /// <summary>
        /// 브러시 로드하기 (비동기)
        /// </summary>
        /// <returns>태스크</returns>
        private async Task LoadBrushAsync()
        {
            using(IRandomAccessStream stream = await this.imageFileInfo.ImageFile.OpenReadAsync())
            {
                this.customCompositionBrush.LoadImage(stream);
            }
        }

        #endregion
        #region 이미지 로드하기 (비동기) - LoadImageAsync(imageFile, replaceImage)

        /// <summary>
        /// 이미지 로드하기 (비동기)
        /// </summary>
        /// <param name="storageFile">저장소 파일</param>
        /// <param name="replaceImage">이미지 대체 여부</param>
        /// <returns>태스크</returns>
        private async Task LoadImageAsync(StorageFile storageFile, bool replaceImage)
        {
            this.imageFileInfo.NeedsSaved = false;

            ImageFileInfo newImageFileInfo = await MainPage.GetImageFileInfoAsync(storageFile);

            ResetEffects();

            int index = MainPage.CurrentPage.ImageFileInfoCollection.IndexOf(this.imageFileInfo);

            this.imageFileInfo = newImageFileInfo;

            Bindings.Update();

            UnloadObject(imageRectangle);

            FindName("imgRect");

            await LoadBrushAsync();

            if(replaceImage == true)
            {
                MainPage.CurrentPage.ImageFileInfoCollection.RemoveAt(index);

                MainPage.CurrentPage.ImageFileInfoCollection.Insert(index, this.imageFileInfo);
            }
            else
            {
                MainPage.CurrentPage.ImageFileInfoCollection.Insert(index + 1, this.imageFileInfo);
            }
            
            MainPage.CurrentPage.UpdateCurrentImageFileInfo(this.imageFileInfo);

            this.bitmapImage = await this.imageFileInfo.GetBitmapImageAsyc();

            this.smallImage.Source = this.bitmapImage;
        }

        #endregion
        #region 이미지 내보내기 - ExportImage()

        /// <summary>
        /// 이미지 내보내기
        /// </summary>
        private async void ExportImage()
        {
            CanvasDevice canvasDevice = CanvasDevice.GetSharedDevice();

            using
            (
                CanvasRenderTarget canvasRenderTarget = new CanvasRenderTarget
                (
                    canvasDevice,
                    this.imageFileInfo.ImageProperties.Width,
                    this.imageFileInfo.ImageProperties.Height,
                    96
                )
            )
            {
                using(IRandomAccessStream sourceStream = await this.imageFileInfo.ImageFile.OpenReadAsync())
                {
                    using(CanvasBitmap canvasBitmap = await CanvasBitmap.LoadAsync(canvasRenderTarget, sourceStream, 96))
                    {
                        this.customCompositionBrush.SetSource(canvasBitmap);

                        using(CanvasDrawingSession canvasDrawingSession = canvasRenderTarget.CreateDrawingSession())
                        {
                            canvasDrawingSession.Clear(Windows.UI.Colors.Black);

                            ICanvasImage canvasImage = this.customCompositionBrush.Image;

                            canvasDrawingSession.DrawImage(canvasImage);
                        }

                        FileSavePicker fileSavePicker = new FileSavePicker()
                        {
                            SuggestedStartLocation = PickerLocationId.PicturesLibrary,
                            SuggestedSaveFile      = this.imageFileInfo.ImageFile
                        };

                        fileSavePicker.FileTypeChoices.Add("JPEG files", new List<string>() { ".jpg" });

                        StorageFile storageFile = await fileSavePicker.PickSaveFileAsync();

                        if(storageFile != null)
                        {
                            using(IRandomAccessStream targetStream = await storageFile.OpenAsync(FileAccessMode.ReadWrite))
                            {
                                await canvasRenderTarget.SaveAsync(targetStream, CanvasBitmapFileFormat.Jpeg);
                            }

                            bool replace = false;

                            if(storageFile.IsEqual(this.imageFileInfo.ImageFile))
                            {
                                replace = true;
                            }

                            try
                            {
                                await LoadImageAsync(storageFile, replace);
                            }
                            catch(Exception exception)
                            {
                                if(exception.Message.Contains("0x80070323"))
                                {
                                    await LoadImageAsync(storageFile, replace);
                                }
                            }
                        }
                    }
                }
            }
        }

        #endregion
        #region 저장 대화 상자 표시하기 - ShowSaveDialog(e)

        /// <summary>
        /// 저장 대화 상자 표시하기
        /// </summary>
        /// <param name="e">이벤트 인자</param>
        private async void ShowSaveDialog(NavigatingCancelEventArgs e)
        {
            ContentDialog saveDialog = new ContentDialog()
            {
                Title               = "알림",
                Content             = "저장되지 않은 변경 사항이 있습니다.",
                PrimaryButtonText   = "이동",
                SecondaryButtonText = "이동 취소"
            };

            ContentDialogResult result = await saveDialog.ShowAsync();

            if(result == ContentDialogResult.Primary)
            {
                this.canNavigateWithUnsavedChanges = true;

                ResetEffects();

                Frame.Navigate(e.SourcePageType, e.Parameter);
            }
        }

        #endregion
    }
}

 

▶ MainPage.xaml

<Page x:Name="page" x:Class="TestProject.MainPage"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:animations="using:Microsoft.Toolkit.Uwp.UI.Animations"
    xmlns:muxc="using:Microsoft.UI.Xaml.Controls"
    xmlns:local="using:TestProject"
    NavigationCacheMode="Enabled"
    SizeChanged="{x:Bind Page_SizeChanged}">
    <Page.Resources>
        <x:Int32 x:Key="LargeItemMarginValueKey">8</x:Int32>
        <Thickness x:Key="LargeItemMarginKey">8</Thickness>
        <x:Int32 x:Key="SmallItemMarginKeyValueKey">0</x:Int32>
        <Thickness x:Key="SmallItemMarginKey">0</Thickness>
        <x:Int32 x:Key="DefaultWindowSidePaddingValueKey">16</x:Int32>
        <Flyout x:Key="zoomFlyout">
            <StackPanel>
                <Slider Name="zoomSlider"   
                    Margin="0 5 0 0"
                    TickFrequency="90"
                    SnapsTo="Ticks"
                    Minimum="180"
                    Maximum="540"
                    Value="270"
                    Header="그리드 항목 크기"
                    ValueChanged="{x:Bind Page_SizeChanged}" />
                <ToggleSwitch Name="fitScreenToggleSwitch"
                    Header="화면 맞춤"
                    ToolTipService.ToolTip="Resize images to use available space."
                    Toggled="{x:Bind Page_SizeChanged}" />
            </StackPanel>
        </Flyout>
        <ItemsPanelTemplate x:Key="imageGridViewItemsPanelTemplateKey">
            <ItemsWrapGrid
                HorizontalAlignment="Center"
                Orientation="Horizontal" />
        </ItemsPanelTemplate>
        <DataTemplate x:Key="imageGridViewDefaultItemDataTemplateKey" x:DataType="local:ImageFileInfo">
            <Grid
                Margin="{StaticResource LargeItemMarginKey}"
                Height="{Binding ItemSize, ElementName=page}"
                Width="{Binding ItemSize, ElementName=page}">
                <Grid.RowDefinitions>
                    <RowDefinition Height="*"    />
                    <RowDefinition Height="Auto" />
                </Grid.RowDefinitions>
                <Image Name="itemImage"
                    Stretch="Uniform"
                    Opacity="0" />
                <StackPanel Grid.Row="1"
                    Orientation="Vertical">
                    <TextBlock Text="{x:Bind ImageTitle, Mode=OneWay}"
                        Style="{StaticResource SubtitleTextBlockStyle}"
                        HorizontalAlignment="Center" />
                    <StackPanel
                        HorizontalAlignment="Center"
                        Orientation="Horizontal">
                        <TextBlock
                            Style="{StaticResource CaptionTextBlockStyle}"
                            HorizontalAlignment="Center"
                            Text="{x:Bind ImageFileType}" />
                        <TextBlock
                            Style="{StaticResource CaptionTextBlockStyle}"
                            Margin="8 0 0 0"
                            HorizontalAlignment="Center"
                            Text="{x:Bind ImageDimensions}" />
                    </StackPanel>
                    <muxc:RatingControl
                        IsReadOnly="True"
                        Value="{x:Bind ImageRating, Mode=OneWay}" />
                </StackPanel>
            </Grid>
        </DataTemplate>
        <Style x:Key="imageGridViewDefaultItemContainerStyleKey" TargetType="GridViewItem">
            <Setter Property="Margin"     Value="{StaticResource LargeItemMarginKey}" />
            <Setter Property="Background" Value="Gray"                             />
        </Style>
        <DataTemplate x:Key="imageGridViewSmallItemDataTemplateKey" x:DataType="local:ImageFileInfo">
            <Grid
                Width="{Binding ItemSize, ElementName=page}"
                Height="{Binding ItemSize, ElementName=page}">
                <Image Name="itemImage"
                    Stretch="UniformToFill"
                    Opacity="0">
                    <ToolTipService.ToolTip>
                        <ToolTip Name="tooltip">
                            <StackPanel Grid.Row="1"
                                Orientation="Vertical">
                                <TextBlock
                                    Style="{StaticResource SubtitleTextBlockStyle}"
                                    HorizontalAlignment="Center"
                                    Text="{x:Bind ImageTitle, Mode=OneWay}" />
                                <StackPanel
                                    HorizontalAlignment="Center"
                                    Orientation="Horizontal">
                                    <TextBlock
                                        Style="{StaticResource CaptionTextBlockStyle}"
                                        HorizontalAlignment="Center"
                                        Text="{x:Bind ImageFileType}" />
                                    <TextBlock
                                        Margin="8 0 0 0"
                                        Style="{StaticResource CaptionTextBlockStyle}"
                                        HorizontalAlignment="Center"
                                        Text="{x:Bind ImageDimensions}" />
                                </StackPanel>
                            </StackPanel>
                        </ToolTip>
                    </ToolTipService.ToolTip>
                </Image>
            </Grid>
        </DataTemplate>
        <Style x:Key="imageGridViewSmallItemContainerStyleKey" TargetType="GridViewItem" />
    </Page.Resources>
    <RelativePanel Background="{ThemeResource ApplicationPageBackgroundThemeBrush}">
        <TextBlock Name="titleTextBlock"
            Style="{StaticResource TitleTextBlockStyle}"
            Margin="24 0 0 24"
            Text="컬렉션" />
        <CommandBar Name="mainCommandBar"
            Background="{ThemeResource ApplicationPageBackgroundThemeBrush}"
            RelativePanel.AlignRightWithPanel="True"
            OverflowButtonVisibility="Collapsed"
            DefaultLabelPosition="Right">
            <AppBarButton
                Icon="Zoom"
                Label="확대/축소"
                Flyout="{StaticResource zoomFlyout}" />
        </CommandBar>
        <GridView Name="imageGridView"
            ItemsPanel="{StaticResource imageGridViewItemsPanelTemplateKey}"
            ItemTemplate="{StaticResource imageGridViewDefaultItemDataTemplateKey}"
            ItemContainerStyle="{StaticResource imageGridViewDefaultItemContainerStyleKey}"
            Margin="0 0 0 8"
            RelativePanel.AlignLeftWithPanel="True"
            RelativePanel.AlignRightWithPanel="True"
            RelativePanel.Below="titleTextBlock"
            animations:ReorderGridAnimation.Duration="400"
            IsItemClickEnabled="True"
            ItemsSource="{x:Bind ImageFileInfoCollection, Mode=OneWay}"
            Loaded="{x:Bind imageGridView_Loaded}"
            ContainerContentChanging="imageGridView_ContainerContentChanging"
            ItemClick="imageGridView_ItemClick">
        </GridView>
        <VisualStateManager.VisualStateGroups>
            <VisualStateGroup>
                <VisualState>
                    <VisualState.StateTriggers>
                        <AdaptiveTrigger MinWindowWidth="{StaticResource LargeWindowBreakpointDoubleKey}" />
                    </VisualState.StateTriggers>
                </VisualState>
                <VisualState>
                    <VisualState.StateTriggers>
                        <AdaptiveTrigger MinWindowWidth="{StaticResource MediumWindowBreakpointDoubleKey}" />
                    </VisualState.StateTriggers>
                </VisualState>
                <VisualState>
                    <VisualState.StateTriggers>
                        <AdaptiveTrigger MinWindowWidth="{StaticResource MinimumWindowBreakpointDoubleKey}" />
                    </VisualState.StateTriggers>
                    <VisualState.Setters>
                        <Setter Target="imageGridView.ItemTemplate"       Value="{StaticResource imageGridViewSmallItemDataTemplateKey}"   />
                        <Setter Target="imageGridView.ItemContainerStyle" Value="{StaticResource imageGridViewSmallItemContainerStyleKey}" />
                        <Setter Target="zoomSlider.Minimum"               Value="80"  />
                        <Setter Target="zoomSlider.Maximum"               Value="180" />
                        <Setter Target="zoomSlider.TickFrequency"         Value="20"  />
                        <Setter Target="zoomSlider.Value"                 Value="100" />
                    </VisualState.Setters>
                </VisualState>
            </VisualStateGroup>
        </VisualStateManager.VisualStateGroups>
    </RelativePanel>
</Page>

 

▶ MainPage.xaml.cs

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.FileProperties;
using Windows.Storage.Search;
using Windows.UI.Core;
using Windows.UI.Xaml.Controls;
using Windows.UI.Xaml.Media.Animation;
using Windows.UI.Xaml.Media.Imaging;
using Windows.UI.Xaml.Navigation;

namespace TestProject
{
    /// <summary>
    /// 메인 페이지
    /// </summary>
    public sealed partial class MainPage : Page, INotifyPropertyChanged
    {
        //////////////////////////////////////////////////////////////////////////////////////////////////// Event
        ////////////////////////////////////////////////////////////////////////////////////////// Public

        #region 속성 변경시 이벤트 - PropertyChanged

        /// <summary>
        /// 속성 변경시 이벤트
        /// </summary>
        public event PropertyChangedEventHandler PropertyChanged;

        #endregion

        //////////////////////////////////////////////////////////////////////////////////////////////////// Field
        ////////////////////////////////////////////////////////////////////////////////////////// Static
        //////////////////////////////////////////////////////////////////////////////// Public

        #region Field

        /// <summary>
        /// 현재 페이지
        /// </summary>
        public static MainPage CurrentPage;

        #endregion

        ////////////////////////////////////////////////////////////////////////////////////////// Instance
        //////////////////////////////////////////////////////////////////////////////// Private

        #region Field

        /// <summary>
        /// 항목 크기
        /// </summary>
        private double itemSize;

        /// <summary>
        /// 현재 이미지 파일 정보
        /// </summary>
        private ImageFileInfo currentImageFileInfo;

        #endregion

        ////////////////////////////////////////////////////////////////////////////////////////// Instance
        //////////////////////////////////////////////////////////////////////////////// Private

        #region 이미지 파일 정보 컬렉션 - ImageFileInfoCollection

        /// <summary>
        /// 이미지 파일 정보 컬렉션
        /// </summary>
        public ObservableCollection<ImageFileInfo> ImageFileInfoCollection { get; } = new ObservableCollection<ImageFileInfo>();

        #endregion

        //////////////////////////////////////////////////////////////////////////////////////////////////// Property
        ////////////////////////////////////////////////////////////////////////////////////////// Public

        #region 항목 크기 - ItemSize

        /// <summary>
        /// 항목 크기
        /// </summary>
        public double ItemSize
        {
            get => this.itemSize;
            set
            {
                if(this.itemSize != value)
                {
                    this.itemSize = value;

                    PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(ItemSize)));
                }
            }
        }

        #endregion

        //////////////////////////////////////////////////////////////////////////////////////////////////// Constructor
        ////////////////////////////////////////////////////////////////////////////////////////// Public

        #region 생성자 - MainPage()

        /// <summary>
        /// 생성자
        /// </summary>
        public MainPage()
        {
            InitializeComponent();

            CurrentPage = this;
        }

        #endregion

        //////////////////////////////////////////////////////////////////////////////////////////////////// Method
        ////////////////////////////////////////////////////////////////////////////////////////// Public

        #region 현재 이미지 파일 정보 업데이트하기 - UpdateCurrentImageFileInfo(imageFileInfo)

        /// <summary>
        /// 현재 이미지 파일 정보 업데이트하기
        /// </summary>
        /// <param name="imageFileInfo">이미지 파일 정보</param>
        public void UpdateCurrentImageFileInfo(ImageFileInfo imageFileInfo)
        {
            this.currentImageFileInfo = imageFileInfo;
        }

        #endregion
        #region 이미지 파일 정보 구하기 (비동기) - GetImageFileInfoAsync(storageFile)

        /// <summary>
        /// 이미지 파일 정보 구하기 (비동기)
        /// </summary>
        /// <param name="storageFile">저장소 파일</param>
        /// <returns>이미지 파일 정보 태스크</returns>
        public async static Task<ImageFileInfo> GetImageFileInfoAsync(StorageFile storageFile)
        {
            ImageProperties imageProperties = await storageFile.Properties.GetImagePropertiesAsync();

            ImageFileInfo imageFileInfo = new ImageFileInfo
            (
                imageProperties,
                storageFile,
                storageFile.DisplayName,
                storageFile.DisplayType
            );

            return imageFileInfo;
        }

        #endregion

        ////////////////////////////////////////////////////////////////////////////////////////// Protected
        //////////////////////////////////////////////////////////////////////////////// Function

        #region 탐색되는 경우 처리하기 - OnNavigatedTo(e)

        /// <summary>
        /// 탐색되는 경우 처리하기
        /// </summary>
        /// <param name="e">이벤트 인자</param>
        protected async override void OnNavigatedTo(NavigationEventArgs e)
        {
            SystemNavigationManager.GetForCurrentView().AppViewBackButtonVisibility = AppViewBackButtonVisibility.Collapsed;

            if(ImageFileInfoCollection.Count == 0)
            {
                await AddImageFileInfoAsync();
            }

            base.OnNavigatedTo(e);
        }

        #endregion

        ////////////////////////////////////////////////////////////////////////////////////////// Private
        //////////////////////////////////////////////////////////////////////////////// Event

        #region 페이지 크기 변경시 처리하기 - Page_SizeChanged()

        /// <summary>
        /// 페이지 크기 변경시 처리하기
        /// </summary>
        private void Page_SizeChanged()
        {
            if(this.fitScreenToggleSwitch != null && this.fitScreenToggleSwitch.IsOn == true && this.imageGridView != null && this.zoomSlider != null)
            {
                int    margin            = (int)Resources["LargeItemMarginValueKey"] * 4;
                double gridWidth         = this.imageGridView.ActualWidth - (int)Resources["DefaultWindowSidePaddingValueKey"];
                double ItemWidth         = this.zoomSlider.Value + margin;
                int    columnCount       = (int)Math.Max(gridWidth / ItemWidth, 1);
                double adjustedGridWidth = gridWidth - (columnCount * margin);

                ItemSize = (adjustedGridWidth / columnCount);
            }
            else
            {
                ItemSize = this.zoomSlider.Value;
            }
        }

        #endregion
        #region 이미지 그리드 뷰 로드시 처리하기 - imageGridView_Loaded()

        /// <summary>
        /// 이미지 그리드 뷰 로드시 처리하기
        /// </summary>
        private async void imageGridView_Loaded()
        {
            // 세부 정보 페이지에서 기본 페이지로 다시 탐색하기 위해 연결된 애니메이션을 실행한다.
            if(this.currentImageFileInfo != null)
            {
                this.imageGridView.ScrollIntoView(this.currentImageFileInfo);

                ConnectedAnimation animation = ConnectedAnimationService.GetForCurrentView().GetAnimation("backAnimation");

                if(animation != null)
                {
                    await this.imageGridView.TryStartConnectedAnimationAsync(animation, this.currentImageFileInfo, "itemImage");
                }
            }
        }

        #endregion
        #region 이미지 그리드 뷰 컨테이너 내용 변경시 처리하기 - imageGridView_ContainerContentChanging(sender, e)

        /// <summary>
        /// 이미지 그리드 뷰 컨테이너 내용 변경시 처리하기
        /// </summary>
        /// <param name="sender">이벤트 발생자</param>
        /// <param name="e">이벤트 인자</param>
        private void imageGridView_ContainerContentChanging(ListViewBase sender, ContainerContentChangingEventArgs e)
        {
            if(e.InRecycleQueue)
            {
                Grid rootGrid = e.ItemContainer.ContentTemplateRoot as Grid;

                Image image = (Image)rootGrid.FindName("itemImage");

                image.Source = null;
            }

            if(e.Phase == 0)
            {
                e.RegisterUpdateCallback(ShowImage);

                e.Handled = true;
            }
        }

        #endregion
        #region 이미지 그리드 뷰 항목 클릭시 처리하기 - imageGridView_ItemClick(sender, e)

        /// <summary>
        /// 이미지 그리드 뷰 항목 클릭시 처리하기
        /// </summary>
        /// <param name="sender">이벤트 발생자</param>
        /// <param name="e">이벤트 인자</param>
        private void imageGridView_ItemClick(object sender, ItemClickEventArgs e)
        {
            this.currentImageFileInfo = e.ClickedItem as ImageFileInfo;

            this.imageGridView.PrepareConnectedAnimation("itemAnimation", e.ClickedItem, "itemImage");

            Frame.Navigate(typeof(DetailPage), e.ClickedItem);
        }

        #endregion

        //////////////////////////////////////////////////////////////////////////////// Function

        #region 이미지 파일 정보 추가하기 (비동기) - AddImageFileInfoAsync()

        /// <summary>
        /// 이미지 파일 정보 추가하기 (비동기)
        /// </summary>
        /// <returns>태스크</returns>
        private async Task AddImageFileInfoAsync()
        {
            QueryOptions options = new QueryOptions();

            options.FolderDepth = FolderDepth.Deep;

            options.FileTypeFilter.Add(".jpg");
            options.FileTypeFilter.Add(".png");
            options.FileTypeFilter.Add(".gif");

            StorageFolder applicationFolder = Package.Current.InstalledLocation;

            StorageFolder sampleFolder = await applicationFolder.GetFolderAsync("Asset\\Sample");

            StorageFileQueryResult queryResult = sampleFolder.CreateFileQueryWithOptions(options);

            IReadOnlyList<StorageFile> fileList = await queryResult.GetFilesAsync();

            bool unsupportedFilesFound = false;

            foreach(StorageFile storageFile in fileList)
            {
                if(storageFile.Provider.Id == "computer")
                {
                    ImageFileInfoCollection.Add(await GetImageFileInfoAsync(storageFile));
                }
                else
                {
                    unsupportedFilesFound = true;
                }
            }

            if(unsupportedFilesFound == true)
            {
                ContentDialog contentDialog = new ContentDialog
                {
                    Title           = "미지원 이미지 발견",
                    Content         = "이 샘플 앱은 컴퓨터에 로컬로 저장된 이미지만 지원합니다."                     +
                                      " 라이브러리에서 OneDrive 또는 다른 네트워크 위치에 저장된 파일을 찾았습니다." +
                                      " 해당 이미지를 로드하지 않았습니다.",
                    CloseButtonText = "확인"
                };

                ContentDialogResult dialogResult = await contentDialog.ShowAsync();
            }
        }

        #endregion
        #region 이미지 표시하기 - ShowImage(sender, e)

        /// <summary>
        /// 이미지 표시하기
        /// </summary>
        /// <param name="sender">이벤트 발생자</param>
        /// <param name="e">이벤트 인자</param>
        private async void ShowImage(ListViewBase sender, ContainerContentChangingEventArgs e)
        {
            if(e.Phase == 1)
            {
                Grid rootGrid = e.ItemContainer.ContentTemplateRoot as Grid;

                Image image = (Image)rootGrid.FindName("itemImage");

                image.Opacity = 100;

                ImageFileInfo imageFileInfo = e.Item as ImageFileInfo;

                try
                {
                    image.Source = await imageFileInfo.GetThumbnailBitmapImageAsync();
                }
                catch(Exception)
                {
                    BitmapImage bitmapImage = new BitmapImage();

                    bitmapImage.UriSource = new Uri(image.BaseUri, "Asset/StoreLogo.png");

                    image.Source = bitmapImage;
                }
            }
        }

        #endregion
    }
}

 

▶ App.xaml

<Application x:Class="TestProject.App"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
    <Application.Resources>
        <ResourceDictionary>
            <ResourceDictionary.MergedDictionaries>
                <XamlControlsResources xmlns="using:Microsoft.UI.Xaml.Controls" />
            </ResourceDictionary.MergedDictionaries>
            <x:Double x:Key="MinimumWindowBreakpointDoubleKey">0</x:Double>
            <x:Double x:Key="MediumWindowBreakpointDoubleKey">641</x:Double>
            <x:Double x:Key="LargeWindowBreakpointDoubleKey">1008</x:Double>
        </ResourceDictionary>
    </Application.Resources>
</Application>

 

▶ App.xaml.cs

using System;
using Windows.ApplicationModel;
using Windows.ApplicationModel.Activation;
using Windows.Foundation;
using Windows.Graphics.Display;
using Windows.UI.Core;
using Windows.UI.ViewManagement;
using Windows.UI.Xaml;
using Windows.UI.Xaml.Controls;
using Windows.UI.Xaml.Navigation;

namespace TestProject
{
    /// <summary>
    /// 앱
    /// </summary>
    sealed partial class App : Application
    {
        //////////////////////////////////////////////////////////////////////////////////////////////////// Constructor
        ////////////////////////////////////////////////////////////////////////////////////////// Public

        #region 생성자 - App()

        /// <summary>
        /// 생성자
        /// </summary>
        public App()
        {
            InitializeComponent();

            Suspending += Application_Suspending;
        }

        #endregion

        //////////////////////////////////////////////////////////////////////////////////////////////////// Method
        ////////////////////////////////////////////////////////////////////////////////////////// Protected

        #region 시작시 처리하기 - OnLaunched(e)

        /// <summary>
        /// 시작시 처리하기
        /// </summary>
        /// <param name="e">이벤트 인자</param>
        protected override void OnLaunched(LaunchActivatedEventArgs e)
        {
            SystemNavigationManager.GetForCurrentView().BackRequested += SystemNavigationManager_BackRequested;

            Frame rootFrame = Window.Current.Content as Frame;

            if(rootFrame == null)
            {
                rootFrame = new Frame();

                rootFrame.NavigationFailed += rootFrame_NavigationFailed;

                if(e.PreviousExecutionState == ApplicationExecutionState.Terminated)
                {
                }

                Window.Current.Content = rootFrame;
            }

            if(e.PrelaunchActivated == false)
            {
                if(rootFrame.Content == null)
                {
                    rootFrame.Navigate(typeof(MainPage), e.Arguments);
                }

                Window.Current.Activate();
            }

            #region 윈도우 크기를 설정한다.

            double width  = 800d;
            double height = 600d;

            double dpi = (double)DisplayInformation.GetForCurrentView().LogicalDpi;

            ApplicationView.PreferredLaunchWindowingMode = ApplicationViewWindowingMode.PreferredLaunchViewSize;

            Size windowSize = new Size(width * 96d / dpi, height * 96d / dpi);

            ApplicationView.PreferredLaunchViewSize = windowSize;

            Window.Current.Activate();

            ApplicationView.GetForCurrentView().TryResizeView(windowSize);

            #endregion
        }

        #endregion

        ////////////////////////////////////////////////////////////////////////////////////////// Protected

        #region 애플리케이션 보류시 처리하기 - Application_Suspending(sender, e)

        /// <summary>
        /// 애플리케이션 보류시 처리하기
        /// </summary>
        /// <param name="sender">이벤트 발생자</param>
        /// <param name="e">이벤트 인자</param>
        private void Application_Suspending(object sender, SuspendingEventArgs e)
        {
            SuspendingDeferral deferral = e.SuspendingOperation.GetDeferral();

            deferral.Complete();
        }

        #endregion
        #region 시스템 탐색 관리자 뒤로 이동 요청시 처리하기 - SystemNavigationManager_BackRequested(sender, e)

        /// <summary>
        /// 시스템 탐색 관리자 뒤로 이동 요청시 처리하기
        /// </summary>
        /// <param name="sender">이벤트 발생자</param>
        /// <param name="e">이벤트 인자</param>
        private void SystemNavigationManager_BackRequested(object sender, BackRequestedEventArgs e)
        {
            Frame rootFrame = Window.Current.Content as Frame;

            if(rootFrame == null)
            {
                return;
            }

            if(rootFrame.CanGoBack && e.Handled == false)
            {
                e.Handled = true;

                rootFrame.GoBack();
            }
        }

        #endregion
        #region 루트 프레임 탐색 실패시 처리하기 - rootFrame_NavigationFailed(sender, e)

        /// <summary>
        /// 루트 프레임 탐색 실패시 처리하기
        /// </summary>
        /// <param name="sender">이벤트 발생자</param>
        /// <param name="e">이벤트 인자</param>
        private void rootFrame_NavigationFailed(object sender, NavigationFailedEventArgs e)
        {
            throw new Exception($"페이지 로드를 실패했습니다 : {e.SourcePageType.FullName}");
        }

        #endregion
    }
}
728x90
반응형
Posted by 사용자 icodebroker
TAG , , , ,

댓글을 달아 주세요