From d034952c6a891d45d8892f4c0906a4927f292957 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Fri, 27 Jun 2014 22:10:17 +0200 Subject: [PATCH] Added Image control. --- .../Controls/ImageTests.cs | 107 ++++++++++++++++ .../Perspex.Direct2D1.RenderTests.csproj | 1 + Perspex.Direct2D1.RenderTests/TestBase.cs | 2 +- Perspex.Direct2D1/Direct2D1Platform.cs | 5 + Perspex.Direct2D1/Media/DrawingContext.cs | 12 ++ Perspex.Direct2D1/Media/Imaging/BitmapImpl.cs | 36 +++++- Perspex.Direct2D1/PrimitiveExtensions.cs | 5 + Perspex/Controls/Control.cs | 2 - Perspex/Controls/Image.cs | 115 ++++++++++++++++++ Perspex/Media/IDrawingContext.cs | 4 + Perspex/Media/Imaging/Bitmap.cs | 20 ++- Perspex/Media/Imaging/IBitmap.cs | 17 +++ Perspex/Media/Imaging/RenderTargetBitmap.cs | 2 +- Perspex/Perspex.csproj | 3 + Perspex/Platform/IBitmapImpl.cs | 4 + Perspex/Platform/IPlatformRenderInterface.cs | 2 + Perspex/Rect.cs | 56 +++++++++ Perspex/Size.cs | 22 ++++ Perspex/Thickness.cs | 14 +++ Perspex/Vector.cs | 72 +++++++++++ TestApplication/Program.cs | 6 + TestApplication/TestApplication.csproj | 5 + TestApplication/github_icon.png | Bin 0 -> 17223 bytes .../Image/Image_Stretch_Fill.expected.png | Bin 0 -> 7705 bytes .../Image/Image_Stretch_None.expected.png | Bin 0 -> 3320 bytes .../Image/Image_Stretch_Uniform.expected.png | Bin 0 -> 6939 bytes .../Image_Stretch_UniformToFill.expected.png | Bin 0 -> 7542 bytes TestFiles/Direct2D1/Controls/Image/test.png | Bin 0 -> 17223 bytes 28 files changed, 505 insertions(+), 7 deletions(-) create mode 100644 Perspex.Direct2D1.RenderTests/Controls/ImageTests.cs create mode 100644 Perspex/Controls/Image.cs create mode 100644 Perspex/Media/Imaging/IBitmap.cs create mode 100644 Perspex/Vector.cs create mode 100644 TestApplication/github_icon.png create mode 100644 TestFiles/Direct2D1/Controls/Image/Image_Stretch_Fill.expected.png create mode 100644 TestFiles/Direct2D1/Controls/Image/Image_Stretch_None.expected.png create mode 100644 TestFiles/Direct2D1/Controls/Image/Image_Stretch_Uniform.expected.png create mode 100644 TestFiles/Direct2D1/Controls/Image/Image_Stretch_UniformToFill.expected.png create mode 100644 TestFiles/Direct2D1/Controls/Image/test.png diff --git a/Perspex.Direct2D1.RenderTests/Controls/ImageTests.cs b/Perspex.Direct2D1.RenderTests/Controls/ImageTests.cs new file mode 100644 index 0000000000..b904150570 --- /dev/null +++ b/Perspex.Direct2D1.RenderTests/Controls/ImageTests.cs @@ -0,0 +1,107 @@ +// ----------------------------------------------------------------------- +// +// Copyright 2014 MIT Licence. See licence.md for more information. +// +// ----------------------------------------------------------------------- + +namespace Perspex.Direct2D1.RenderTests.Controls +{ + using System.IO; + using Microsoft.VisualStudio.TestTools.UnitTesting; + using Perspex.Controls; + using Perspex.Layout; + using Perspex.Media; + using Perspex.Media.Imaging; + + [TestClass] + public class ImageTests : TestBase + { + private Bitmap bitmap; + + public ImageTests() + : base(@"Controls\Image") + { + this.bitmap = new Bitmap(Path.Combine(this.OutputPath, "test.png")); + } + + [TestMethod] + public void Image_Stretch_None() + { + Decorator target = new Decorator + { + Padding = new Thickness(20, 8), + Width = 200, + Height = 200, + Content = new Image + { + Background = Brushes.Red, + Source = this.bitmap, + Stretch = Stretch.None, + } + }; + + this.RenderToFile(target); + this.CompareImages(); + } + + [TestMethod] + public void Image_Stretch_Fill() + { + Decorator target = new Decorator + { + Padding = new Thickness(20, 8), + Width = 200, + Height = 200, + Content = new Image + { + Background = Brushes.Red, + Source = this.bitmap, + Stretch = Stretch.Fill, + } + }; + + this.RenderToFile(target); + this.CompareImages(); + } + + [TestMethod] + public void Image_Stretch_Uniform() + { + Decorator target = new Decorator + { + Padding = new Thickness(20, 8), + Width = 200, + Height = 200, + Content = new Image + { + Background = Brushes.Red, + Source = this.bitmap, + Stretch = Stretch.Uniform, + } + }; + + this.RenderToFile(target); + this.CompareImages(); + } + + [TestMethod] + public void Image_Stretch_UniformToFill() + { + Decorator target = new Decorator + { + Padding = new Thickness(20, 8), + Width = 200, + Height = 200, + Content = new Image + { + Background = Brushes.Red, + Source = this.bitmap, + Stretch = Stretch.UniformToFill, + } + }; + + this.RenderToFile(target); + this.CompareImages(); + } + } +} diff --git a/Perspex.Direct2D1.RenderTests/Perspex.Direct2D1.RenderTests.csproj b/Perspex.Direct2D1.RenderTests/Perspex.Direct2D1.RenderTests.csproj index 4dac4719fa..6f019ee28d 100644 --- a/Perspex.Direct2D1.RenderTests/Perspex.Direct2D1.RenderTests.csproj +++ b/Perspex.Direct2D1.RenderTests/Perspex.Direct2D1.RenderTests.csproj @@ -58,6 +58,7 @@ + diff --git a/Perspex.Direct2D1.RenderTests/TestBase.cs b/Perspex.Direct2D1.RenderTests/TestBase.cs index 1ca3108e5f..42893414a7 100644 --- a/Perspex.Direct2D1.RenderTests/TestBase.cs +++ b/Perspex.Direct2D1.RenderTests/TestBase.cs @@ -11,7 +11,7 @@ namespace Perspex.Direct2D1.RenderTests using ImageMagick; using Microsoft.VisualStudio.TestTools.UnitTesting; using Perspex.Controls; - using Perspex.Media; + using Perspex.Media.Imaging; public class TestBase { diff --git a/Perspex.Direct2D1/Direct2D1Platform.cs b/Perspex.Direct2D1/Direct2D1Platform.cs index b0049f02bb..d997bf5787 100644 --- a/Perspex.Direct2D1/Direct2D1Platform.cs +++ b/Perspex.Direct2D1/Direct2D1Platform.cs @@ -57,5 +57,10 @@ namespace Perspex.Direct2D1 { return new StreamGeometryImpl(); } + + public IBitmapImpl LoadBitmap(string fileName) + { + return new BitmapImpl(imagingFactory, fileName); + } } } diff --git a/Perspex.Direct2D1/Media/DrawingContext.cs b/Perspex.Direct2D1/Media/DrawingContext.cs index 00fb0bdaae..a8c3f5b659 100644 --- a/Perspex.Direct2D1/Media/DrawingContext.cs +++ b/Perspex.Direct2D1/Media/DrawingContext.cs @@ -51,6 +51,18 @@ namespace Perspex.Direct2D1.Media this.renderTarget.EndDraw(); } + public void DrawImage(Perspex.Media.Imaging.Bitmap bitmap, double opacity, Rect sourceRect, Rect destRect) + { + BitmapImpl impl = (BitmapImpl)bitmap.PlatformImpl; + Bitmap d2d = impl.GetDirect2DBitmap(this.renderTarget); + this.renderTarget.DrawBitmap( + d2d, + destRect.ToSharpDX(), + (float)opacity, + BitmapInterpolationMode.Linear, + sourceRect.ToSharpDX()); + } + /// /// Draws a line. /// diff --git a/Perspex.Direct2D1/Media/Imaging/BitmapImpl.cs b/Perspex.Direct2D1/Media/Imaging/BitmapImpl.cs index 7426b6446d..7a93a434e2 100644 --- a/Perspex.Direct2D1/Media/Imaging/BitmapImpl.cs +++ b/Perspex.Direct2D1/Media/Imaging/BitmapImpl.cs @@ -15,23 +15,57 @@ namespace Perspex.Direct2D1.Media { private ImagingFactory factory; + private SharpDX.Direct2D1.Bitmap direct2D; + + public BitmapImpl(ImagingFactory factory, string fileName) + { + this.factory = factory; + + using (BitmapDecoder decoder = new BitmapDecoder(factory, fileName, DecodeOptions.CacheOnDemand)) + { + this.WicImpl = new Bitmap(factory, decoder.GetFrame(0), BitmapCreateCacheOption.CacheOnDemand); + } + } + public BitmapImpl(ImagingFactory factory, int width, int height) { this.factory = factory; this.WicImpl = new Bitmap( - factory, + factory, width, height, PixelFormat.Format32bppPBGRA, BitmapCreateCacheOption.CacheOnLoad); } + public int PixelHeight + { + get { return this.WicImpl.Size.Width; } + } + + public int PixelWidth + { + get { return this.WicImpl.Size.Height; } + } + public Bitmap WicImpl { get; private set; } + public SharpDX.Direct2D1.Bitmap GetDirect2DBitmap(SharpDX.Direct2D1.RenderTarget renderTarget) + { + if (this.direct2D == null) + { + FormatConverter converter = new FormatConverter(this.factory); + converter.Initialize(this.WicImpl, PixelFormat.Format32bppPBGRA); + this.direct2D = SharpDX.Direct2D1.Bitmap.FromWicBitmap(renderTarget, converter); + } + + return this.direct2D; + } + public void Save(string fileName) { if (Path.GetExtension(fileName) != ".png") diff --git a/Perspex.Direct2D1/PrimitiveExtensions.cs b/Perspex.Direct2D1/PrimitiveExtensions.cs index d3469d78a0..48704bd6c0 100644 --- a/Perspex.Direct2D1/PrimitiveExtensions.cs +++ b/Perspex.Direct2D1/PrimitiveExtensions.cs @@ -15,6 +15,11 @@ namespace Perspex.Direct2D1 return new Rect(r.X, r.Y, r.Width, r.Height); } + public static RectangleF ToSharpDX(this Rect r) + { + return new RectangleF((float)r.X, (float)r.Y, (float)r.Width, (float)r.Height); + } + public static Vector2 ToSharpDX(this Perspex.Point p) { return new Vector2((float)p.X, (float)p.Y); diff --git a/Perspex/Controls/Control.cs b/Perspex/Controls/Control.cs index 16a342f651..e23df4c4a3 100644 --- a/Perspex/Controls/Control.cs +++ b/Perspex/Controls/Control.cs @@ -8,11 +8,9 @@ namespace Perspex.Controls { using System; using System.Collections.Generic; - using System.Collections.ObjectModel; using System.Linq; using System.Reactive.Linq; using Perspex.Input; - using Perspex.Layout; using Perspex.Media; using Perspex.Styling; using Splat; diff --git a/Perspex/Controls/Image.cs b/Perspex/Controls/Image.cs new file mode 100644 index 0000000000..cb594b3340 --- /dev/null +++ b/Perspex/Controls/Image.cs @@ -0,0 +1,115 @@ +// ----------------------------------------------------------------------- +// +// Copyright 2013 MIT Licence. See licence.md for more information. +// +// ----------------------------------------------------------------------- + +namespace Perspex.Controls +{ + using System; + using Perspex.Media; + using Perspex.Media.Imaging; + + public class Image : Control + { + public static readonly PerspexProperty SourceProperty = + PerspexProperty.Register("Source"); + + public static readonly PerspexProperty StretchProperty = + PerspexProperty.Register("Stretch", Stretch.Uniform); + + public Bitmap Source + { + get { return this.GetValue(SourceProperty); } + set { this.SetValue(SourceProperty, value); } + } + + public Stretch Stretch + { + get { return (Stretch)this.GetValue(StretchProperty); } + set { this.SetValue(StretchProperty, value); } + } + + public override void Render(IDrawingContext drawingContext) + { + Brush background = this.Background; + Bitmap source = this.Source; + + if (background != null) + { + drawingContext.FillRectange(background, new Rect(this.ActualSize)); + } + + if (source != null) + { + Rect viewPort = new Rect(this.ActualSize); + Size sourceSize = new Size(source.PixelWidth, source.PixelHeight); + Vector scale = CalculateScaling(this.ActualSize, sourceSize, this.Stretch); + Size scaledSize = sourceSize * scale; + Rect destRect = viewPort + .Center(new Rect(scaledSize)) + .Intersect(viewPort); + Rect sourceRect = new Rect(sourceSize) + .Center(new Rect(destRect.Size / scale)); + + drawingContext.DrawImage(source, 1, sourceRect, destRect); + } + } + + private static Vector CalculateScaling(Size availableSize, Size imageSize, Stretch stretch) + { + double scaleX = 1; + double scaleY = 1; + + if (stretch != Stretch.None) + { + scaleX = availableSize.Width / imageSize.Width; + scaleY = availableSize.Height / imageSize.Height; + + switch (stretch) + { + case Stretch.Uniform: + scaleX = scaleY = Math.Min(scaleX, scaleY); + break; + case Stretch.UniformToFill: + scaleX = scaleY = Math.Max(scaleX, scaleY); + break; + } + } + + return new Vector(scaleX, scaleY); + } + + protected override Size MeasureOverride(Size availableSize) + { + double width = 0; + double height = 0; + Vector scale = new Vector(); + + if (this.Source != null) + { + width = this.Source.PixelWidth; + height = this.Source.PixelHeight; + + if (this.Width > 0) + { + availableSize = new Size(this.Width, availableSize.Height); + } + + if (this.Height > 0) + { + availableSize = new Size(availableSize.Width, this.Height); + } + + scale = CalculateScaling(availableSize, new Size(width, height), this.Stretch); + } + + return new Size(width * scale.X, height * scale.Y); + } + + protected override Size ArrangeOverride(Size finalSize) + { + return finalSize; + } + } +} diff --git a/Perspex/Media/IDrawingContext.cs b/Perspex/Media/IDrawingContext.cs index c22fbfe172..e36d6be8d2 100644 --- a/Perspex/Media/IDrawingContext.cs +++ b/Perspex/Media/IDrawingContext.cs @@ -7,12 +7,16 @@ namespace Perspex.Media { using System; + using Perspex.Media.Imaging; + /// /// Defines the interface through which drawing occurs. /// public interface IDrawingContext : IDisposable { + void DrawImage(Bitmap source, double opacity, Rect sourceRect, Rect destRect); + /// /// Draws a line. /// diff --git a/Perspex/Media/Imaging/Bitmap.cs b/Perspex/Media/Imaging/Bitmap.cs index 37ab4ea6d0..9e98886f0e 100644 --- a/Perspex/Media/Imaging/Bitmap.cs +++ b/Perspex/Media/Imaging/Bitmap.cs @@ -4,13 +4,19 @@ // // ----------------------------------------------------------------------- -namespace Perspex.Media +namespace Perspex.Media.Imaging { using Perspex.Platform; using Splat; - public class Bitmap + public class Bitmap : IBitmap { + public Bitmap(string fileName) + { + IPlatformRenderInterface factory = Locator.Current.GetService(); + this.PlatformImpl = factory.LoadBitmap(fileName); + } + public Bitmap(int width, int height) { IPlatformRenderInterface factory = Locator.Current.GetService(); @@ -22,6 +28,16 @@ namespace Perspex.Media this.PlatformImpl = impl; } + public int PixelWidth + { + get { return this.PlatformImpl.PixelWidth; } + } + + public int PixelHeight + { + get { return this.PlatformImpl.PixelHeight; } + } + public IBitmapImpl PlatformImpl { get; diff --git a/Perspex/Media/Imaging/IBitmap.cs b/Perspex/Media/Imaging/IBitmap.cs new file mode 100644 index 0000000000..c070f0113f --- /dev/null +++ b/Perspex/Media/Imaging/IBitmap.cs @@ -0,0 +1,17 @@ +// ----------------------------------------------------------------------- +// +// Copyright 2013 MIT Licence. See licence.md for more information. +// +// ----------------------------------------------------------------------- + +namespace Perspex.Media.Imaging +{ + public interface IBitmap + { + int PixelHeight { get; } + + int PixelWidth { get; } + + void Save(string fileName); + } +} \ No newline at end of file diff --git a/Perspex/Media/Imaging/RenderTargetBitmap.cs b/Perspex/Media/Imaging/RenderTargetBitmap.cs index 3edc655a1f..3f436941da 100644 --- a/Perspex/Media/Imaging/RenderTargetBitmap.cs +++ b/Perspex/Media/Imaging/RenderTargetBitmap.cs @@ -4,7 +4,7 @@ // // ----------------------------------------------------------------------- -namespace Perspex.Media +namespace Perspex.Media.Imaging { using Perspex.Platform; using Splat; diff --git a/Perspex/Perspex.csproj b/Perspex/Perspex.csproj index bfa2ffe711..3f03832332 100644 --- a/Perspex/Perspex.csproj +++ b/Perspex/Perspex.csproj @@ -76,6 +76,7 @@ + @@ -105,6 +106,7 @@ + @@ -119,6 +121,7 @@ + diff --git a/Perspex/Platform/IBitmapImpl.cs b/Perspex/Platform/IBitmapImpl.cs index 0bbb5314cb..b6ae7a7ff3 100644 --- a/Perspex/Platform/IBitmapImpl.cs +++ b/Perspex/Platform/IBitmapImpl.cs @@ -8,6 +8,10 @@ namespace Perspex.Platform { public interface IBitmapImpl { + int PixelWidth { get; } + + int PixelHeight { get; } + void Save(string fileName); } } diff --git a/Perspex/Platform/IPlatformRenderInterface.cs b/Perspex/Platform/IPlatformRenderInterface.cs index 71d3c54451..a5bb3be06c 100644 --- a/Perspex/Platform/IPlatformRenderInterface.cs +++ b/Perspex/Platform/IPlatformRenderInterface.cs @@ -20,5 +20,7 @@ namespace Perspex.Platform IRenderer CreateRenderer(IntPtr handle, double width, double height); IRenderTargetBitmapImpl CreateRenderTargetBitmap(int width, int height); + + IBitmapImpl LoadBitmap(string fileName); } } diff --git a/Perspex/Rect.cs b/Perspex/Rect.cs index 84bf9731a5..88f8abfe6b 100644 --- a/Perspex/Rect.cs +++ b/Perspex/Rect.cs @@ -191,6 +191,32 @@ namespace Perspex get { return this.width == 0 && this.height == 0; } } + public static Rect operator *(Rect rect, Vector scale) + { + double centerX = rect.x + rect.width / 2; + double centerY = rect.y + rect.height / 2; + double width = rect.width * scale.X; + double height = rect.height * scale.Y; + return new Rect( + centerX - (width / 2), + centerY - (height / 2), + width, + height); + } + + public static Rect operator /(Rect rect, Vector scale) + { + double centerX = rect.x + rect.width / 2; + double centerY = rect.y + rect.height / 2; + double width = rect.width / scale.X; + double height = rect.height / scale.Y; + return new Rect( + centerX - (width / 2), + centerY - (height / 2), + width, + height); + } + /// /// Determines whether a points in in the bounds of the rectangle. /// @@ -202,6 +228,15 @@ namespace Perspex p.Y >= this.y && p.Y < this.y + this.height; } + public Rect Center(Rect rect) + { + return new Rect( + this.x + ((this.width - rect.width) / 2), + this.y + ((this.height - rect.height) / 2), + rect.width, + rect.height); + } + /// /// Deflates the rectangle. /// @@ -226,6 +261,27 @@ namespace Perspex this.Size.Deflate(thickness)); } + public Rect Intersect(Rect rect) + { + double x = Math.Max(this.x, rect.x); + double y = Math.Max(this.y, rect.y); + double width = Math.Min(this.Right, rect.Right) - x; + double height = Math.Min(this.Bottom, rect.Bottom) - y; + + if (width < 0 || height < 0) + { + return new Rect( + double.PositiveInfinity, + double.PositiveInfinity, + double.NegativeInfinity, + double.NegativeInfinity); + } + else + { + return new Rect(x, y, width, height); + } + } + /// /// Returns the string representation of the rectangle. /// diff --git a/Perspex/Size.cs b/Perspex/Size.cs index e32fe843f8..23e4cab500 100644 --- a/Perspex/Size.cs +++ b/Perspex/Size.cs @@ -73,6 +73,28 @@ namespace Perspex return !(left == right); } + /// + /// Scales a size. + /// + /// The size + /// The scaling factor. + /// The scaled size. + public static Size operator *(Size size, Vector scale) + { + return new Size(size.width * scale.X, size.height * scale.Y); + } + + /// + /// Scales a size. + /// + /// The size + /// The scaling factor. + /// The scaled size. + public static Size operator /(Size size, Vector scale) + { + return new Size(size.width / scale.X, size.height / scale.Y); + } + /// /// Constrains the size. /// diff --git a/Perspex/Thickness.cs b/Perspex/Thickness.cs index 3ae4c67542..65f01fefc2 100644 --- a/Perspex/Thickness.cs +++ b/Perspex/Thickness.cs @@ -41,6 +41,20 @@ namespace Perspex this.left = this.top = this.right = this.bottom = uniformLength; } + /// + /// Initializes a new instance of the structure. + /// + /// The thickness on the left and right. + /// The thickness on the top and bottom. + public Thickness(double horizontal, double vertical) + { + Contract.Requires(horizontal >= 0); + Contract.Requires(vertical >= 0); + + this.left = this.right = horizontal; + this.top = this.bottom = vertical; + } + /// /// Initializes a new instance of the structure. /// diff --git a/Perspex/Vector.cs b/Perspex/Vector.cs new file mode 100644 index 0000000000..279f8004a4 --- /dev/null +++ b/Perspex/Vector.cs @@ -0,0 +1,72 @@ +// ----------------------------------------------------------------------- +// +// Copyright 2014 MIT Licence. See licence.md for more information. +// +// ----------------------------------------------------------------------- + +namespace Perspex +{ + using System.Globalization; + + /// + /// Defines a vector. + /// + public struct Vector + { + /// + /// The X vector. + /// + private double x; + + /// + /// The Y vector. + /// + private double y; + + /// + /// Initializes a new instance of the structure. + /// + /// The X vector. + /// The Y vector. + public Vector(double x, double y) + { + this.x = x; + this.y = y; + } + + /// + /// Gets the X vector. + /// + public double X + { + get { return this.x; } + } + + /// + /// Gets the Y vector. + /// + public double Y + { + get { return this.y; } + } + + public static Vector operator +(Vector a, Vector b) + { + return new Vector(a.x + b.x, a.y + b.y); + } + + public static Vector operator -(Vector a, Vector b) + { + return new Vector(a.x - b.x, a.y - b.y); + } + + /// + /// Returns the string representation of the point. + /// + /// The string representation of the point. + public override string ToString() + { + return string.Format(CultureInfo.InvariantCulture, "{0}, {1}", this.x, this.y); + } + } +} diff --git a/TestApplication/Program.cs b/TestApplication/Program.cs index 325d5da082..1e820bbb29 100644 --- a/TestApplication/Program.cs +++ b/TestApplication/Program.cs @@ -10,6 +10,7 @@ using Perspex.Controls; using Perspex.Input; using Perspex.Layout; using Perspex.Media; +using Perspex.Media.Imaging; using Perspex.Shapes; using Perspex.Styling; using Perspex.Themes.Default; @@ -69,6 +70,11 @@ namespace TestApplication new TextBox { Text = "Hello World!", + }, + new Image + { + Source = new Bitmap("github_icon.png"), + Width = 200, } } } diff --git a/TestApplication/TestApplication.csproj b/TestApplication/TestApplication.csproj index f7524686ca..2b5eb4fd9d 100644 --- a/TestApplication/TestApplication.csproj +++ b/TestApplication/TestApplication.csproj @@ -98,6 +98,11 @@ Perspex + + + PreserveNewest + +