Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support to create cursors. #284

Merged
merged 5 commits into from
Apr 26, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
</ItemGroup>
<!-- UWP Items include -->
<ItemGroup Condition="'$(_SdkShortFrameworkIdentifier)' == 'uap'">
<Compile Remove="Converter\PackIconKindToImageConverterBase.cs;PackIconImageExtension.cs" />
<Compile Remove="Converter\PackIconKindToImageConverterBase.cs;PackIconImageExtension.cs;PackIconCursorExtension.cs;PackIconCursorHelper.cs" />
<EmbeddedResource Include="**\*.rd.xml" />
<Page Include="**\*.xaml" Exclude="**\bin\**\*.xaml;**\obj\**\*.xaml" SubType="Designer" Generator="MSBuild:Compile" />
<Compile Update="**\*.xaml.cs" DependentUpon="%(Filename)" />
Expand Down
39 changes: 39 additions & 0 deletions src/MahApps.Metro.IconPacks.Core/PackIconCursorExtension.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
using System.Windows;
using System.Windows.Input;
using System.Windows.Media;

namespace MahApps.Metro.IconPacks
{
public interface IPackIconCursorExtension
{
/// <summary>
/// Get or set the hot spot point of the cursor (from (0,0) to (255,255)). Default point is upper left corner (0,0)
/// </summary>
Point HotSpot { get; set; }

/// <summary>
/// Get or set cursor width (1 to 256). Default value is 32
/// </summary>
double Width { get; set; }

/// <summary>
/// Get or set cursor height (1 to 256). Default value is 32
/// </summary>
double Height { get; set; }

/// <summary>
/// Get or set cursor outline color. If not set a black brush will be used.
/// </summary>
/// <remarks>
/// If you don't want to draw an outline stroke, set <see cref="StrokeThickness"/> property to 0.
/// If you set the <see cref="StrokeBrush"/> property to transparent, the outline will not be drawn,
/// but its thickness will be cut from the fill area.
/// </remarks>
Brush StrokeBrush { get; set; }

/// <summary>
/// Get or set cursor outline thickness. Default value is 0.5
/// </summary>
double StrokeThickness { get; set; }
}
}
197 changes: 197 additions & 0 deletions src/MahApps.Metro.IconPacks.Core/PackIconCursorHelper.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,197 @@
using System;
using System.Windows;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.IO;

namespace MahApps.Metro.IconPacks
{
public static class PackIconCursorHelper
{
/// <summary>
/// Get the <see cref="Geometry"/> that will be used to create the cursor
/// </summary>
/// <param name="pathData"> String data of icon geometry.</param>
/// <param name="transformGroup"> Transformation group of the icon (we get it from <see cref="BasePackIconImageExtension.GetPathData(object)"/>).</param>
/// <param name="width"> Width of cursor (optional, between 1 and 256, default is 32)</param>
/// <param name="height"> Width of cursor (optional, between 1 and 256, default is 32)</param>
/// <returns>
/// A <see cref="Geometry"/> object that contains the icon to be used in the cursor.
/// The <see cref="Geometry"/> has all transformations to be scaled to desired size and aligned to top left corner.
/// </returns>
public static Geometry GetCursorGeometry(string pathData, TransformGroup transformGroup, double width = 32, double height = 32)
{
if (width < 1 || width > 256)
{
throw new ArgumentOutOfRangeException(nameof(width) + " should be between 1 and 256.");
}
if (height < 1 || height > 256)
{
throw new ArgumentOutOfRangeException(nameof(height) + " should be between 1 and 256.");
}

Geometry geometry = Geometry.Parse(pathData); // This geometry is frozen and can not be modified
geometry = geometry.Clone(); // Create a modifiable copy of geometry.

// Apply icon pack transformation
geometry.Transform = transformGroup;

// The resulting geometry bounds may not be aligned to (0,0) point. We need to shifted.
Rect rect = geometry.Bounds;
transformGroup.Children.Add(new TranslateTransform(-rect.X, -rect.Y));

// Apply the requested size.
var aspectRatio = Math.Min(width / rect.Width, height / rect.Height);
transformGroup.Children.Add(new ScaleTransform(aspectRatio, aspectRatio));


geometry.Freeze();
return geometry;
}

/// <summary>
/// Get <see cref="Cursor"/> from a <see cref="Geometry"/> object
/// </summary>
/// <param name="geometry"> Cursor <see cref="Geometry"/></param>
/// <param name="brush"> Fill <see cref="Brush"/>. If not set a white brush will be used.</param>
/// <param name="strokeBrush"> Outline <see cref="Brush"/>. If not set a black brush will be used.</param>
/// <param name="strokeThickness"> Outline thickness. Default is 0.5</param>
/// <param name="hotspot">Hot spot point of the cursor (from (0,0) to (255,255)). Default is (0,0)</param>
/// <returns>
/// A <see cref="Cursor"/>.
/// </returns>
public static Cursor GeometryToCursor(Geometry geometry, Brush brush = null, Brush strokeBrush = null, double strokeThickness = 0.5, Point hotspot = default)
{
if (hotspot.X < 0 || hotspot.X > 255)
{
throw new ArgumentOutOfRangeException("hotspot.X should be between 0 and 255.");
}
if (hotspot.Y < 0 || hotspot.Y > 255)
{
throw new ArgumentOutOfRangeException("hotspot.Y should be between 0 and 255.");
}

Pen pen = new Pen(strokeBrush ?? DefaultStrokeBrush, strokeThickness);
brush = brush ?? DefaultBrush;

var visual = new DrawingVisual();
using (DrawingContext dc = visual.RenderOpen())
{
dc.DrawGeometry(brush, pen, geometry);
}
Rect rect = visual.ContentBounds;

int width = (int)rect.Width;
if (width < 1)
{
width = (int)DefaultWidth;
}
int height = (int)rect.Height;
if (height < 1)
{
height = (int)DefaultHeight;
}

RenderTargetBitmap renderTargetBitmap = new RenderTargetBitmap(width, height, 96, 96, PixelFormats.Pbgra32);
renderTargetBitmap.Render(visual);
return BitmapSourceToCursor(renderTargetBitmap, hotspot);
}

/// <summary>
/// Convert a <see cref="BitmapSource"/> to a <see cref="Cursor"/> object.
/// </summary>
/// <param name="bitmapSource">A <see cref="BitmapSource"/> to be converted.</param>
/// <param name="hotSpot"> A <see cref="Point"/> of cursor hot spot</param>
/// <returns>
/// A <see cref="Cursor"/>.
/// </returns>
public static Cursor BitmapSourceToCursor(BitmapSource bitmapSource, Point hotSpot)
{
if (hotSpot.X < 0 || hotSpot.X > 255)
{
throw new ArgumentOutOfRangeException("hotspot.X should be between 0 and 255.");
}
if (hotSpot.Y < 0 || hotSpot.Y > 255)
{
throw new ArgumentOutOfRangeException("hotspot.Y should be between 0 and 255.");
}

if (bitmapSource.Width < 1 || bitmapSource.Width > 256)
{
throw new ArgumentOutOfRangeException("BitmapSource.Width should be between 1 and 256.");
}
if (bitmapSource.Height < 1 || bitmapSource.Height > 256)
{
throw new ArgumentOutOfRangeException("BitmapSource.Height should be between 1 and 256.");
}

double width = bitmapSource.Width == 256 ? 0 : bitmapSource.Width;
double height = bitmapSource.Height == 256 ? 0 : bitmapSource.Height;

// Taken from https://stackoverflow.com/questions/46805/custom-cursor-in-wpf/27077188#27077188

var pngStream = new MemoryStream();
PngBitmapEncoder png = new PngBitmapEncoder();
png.Frames.Add(BitmapFrame.Create(bitmapSource));
png.Save(pngStream);

// write cursor header info
var cursorStream = new MemoryStream();
cursorStream.Write(new byte[2] { 0x00, 0x00 }, 0, 2); // ICONDIR: Reserved. Must always be 0.
cursorStream.Write(new byte[2] { 0x02, 0x00 }, 0, 2); // ICONDIR: Specifies image type: 1 for icon (.ICO) image, 2 for cursor (.CUR) image. Other values are invalid
cursorStream.Write(new byte[2] { 0x01, 0x00 }, 0, 2); // ICONDIR: Specifies number of images in the file.
cursorStream.Write(new byte[1] { (byte)width }, 0, 1); // ICONDIRENTRY: Specifies image width in pixels. Can be any number between 0 and 255. Value 0 means image width is 256 pixels.
cursorStream.Write(new byte[1] { (byte)height }, 0, 1); // ICONDIRENTRY: Specifies image height in pixels. Can be any number between 0 and 255. Value 0 means image height is 256 pixels.
cursorStream.Write(new byte[1] { 0x00 }, 0, 1); // ICONDIRENTRY: Specifies number of colors in the color palette. Should be 0 if the image does not use a color palette.
cursorStream.Write(new byte[1] { 0x00 }, 0, 1); // ICONDIRENTRY: Reserved. Should be 0.
cursorStream.Write(new byte[2] { (byte)hotSpot.X, 0x00 }, 0, 2); // ICONDIRENTRY: Specifies the horizontal coordinates of the hot spot in number of pixels from the left.
cursorStream.Write(new byte[2] { (byte)hotSpot.Y, 0x00 }, 0, 2); // ICONDIRENTRY: Specifies the vertical coordinates of the hot spot in number of pixels from the top.
cursorStream.Write(new byte[4] { // ICONDIRENTRY: Specifies the size of the image's data in bytes
(byte)(pngStream.Length & 0x000000FF),
(byte)((pngStream.Length & 0x0000FF00) >> 0x08),
(byte)((pngStream.Length & 0x00FF0000) >> 0x10),
(byte)((pngStream.Length & 0xFF000000) >> 0x18)
}, 0, 4);
cursorStream.Write(new byte[4] { // ICONDIRENTRY: Specifies the offset of BMP or PNG data from the beginning of the ICO/CUR file
(byte)0x16,
(byte)0x00,
(byte)0x00,
(byte)0x00,
}, 0, 4);

// copy PNG stream to cursor stream
pngStream.Seek(0, SeekOrigin.Begin);
pngStream.CopyTo(cursorStream);

// return cursor stream
cursorStream.Seek(0, SeekOrigin.Begin);
return new Cursor(cursorStream);
}

/// <summary>
/// Get or set the default width of cursor (32 by default).
/// </summary>
public static double DefaultWidth { get; set; } = 32;

/// <summary>
/// Get or set the default height of cursor (32 by default).
/// </summary>
public static double DefaultHeight { get; set; } = 32;

/// <summary>
/// Get or set the default stroke thickness of cursor (1.0 by default).
/// </summary>
public static double DefaultStrokeThickness { get; set; } = 1.0;

/// <summary>
/// Get or set the default stroke brush of cursor (Black by default).
/// </summary>
public static Brush DefaultStrokeBrush { get; set; } = Brushes.Black;

/// <summary>
/// Get or set the default fill brush of cursor (Black by default).
/// </summary>
public static Brush DefaultBrush { get; set; } = Brushes.Black;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
using System;
using System.Windows;
using System.Windows.Input;
using System.Windows.Markup;
using System.Windows.Media;

namespace MahApps.Metro.IconPacks
{
[MarkupExtensionReturnType(typeof(Cursor))]
public class BootstrapIconsCursorExtension : BootstrapIconsImageExtension, IPackIconCursorExtension
{
public BootstrapIconsCursorExtension() : base() => base.Brush = PackIconCursorHelper.DefaultBrush;
public BootstrapIconsCursorExtension(PackIconBootstrapIconsKind kind) : base(kind) => base.Brush = PackIconCursorHelper.DefaultBrush;

/// <inheritdoc/>
public Point HotSpot { get; set; }
/// <inheritdoc/>
public double Width { get; set; } = PackIconCursorHelper.DefaultWidth;
/// <inheritdoc/>
public double Height { get; set; } = PackIconCursorHelper.DefaultHeight;
/// <inheritdoc/>
public Brush StrokeBrush { get; set; }
/// <inheritdoc/>
public double StrokeThickness { get; set; } = PackIconCursorHelper.DefaultStrokeThickness;

public override object ProvideValue(IServiceProvider serviceProvider)
{
TransformGroup transformGroup = (TransformGroup)GetTransformGroup(this.Kind);
Geometry geometry = PackIconCursorHelper.GetCursorGeometry(GetPathData(this.Kind), transformGroup, Width, Height);
return PackIconCursorHelper.GeometryToCursor(geometry, Brush, StrokeBrush, StrokeThickness, HotSpot);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
using System;
using System.Windows;
using System.Windows.Input;
using System.Windows.Markup;
using System.Windows.Media;

namespace MahApps.Metro.IconPacks
{
[MarkupExtensionReturnType(typeof(Cursor))]
public class BoxIconsCursorExtension : BoxIconsImageExtension, IPackIconCursorExtension
{
public BoxIconsCursorExtension() : base() => base.Brush = PackIconCursorHelper.DefaultBrush;
public BoxIconsCursorExtension(PackIconBoxIconsKind kind) : base(kind) => base.Brush = PackIconCursorHelper.DefaultBrush;

/// <inheritdoc/>
public Point HotSpot { get; set; }
/// <inheritdoc/>
public double Width { get; set; } = PackIconCursorHelper.DefaultWidth;
/// <inheritdoc/>
public double Height { get; set; } = PackIconCursorHelper.DefaultHeight;
/// <inheritdoc/>
public Brush StrokeBrush { get; set; }
/// <inheritdoc/>
public double StrokeThickness { get; set; } = PackIconCursorHelper.DefaultStrokeThickness;

public override object ProvideValue(IServiceProvider serviceProvider)
{
TransformGroup transformGroup = (TransformGroup)GetTransformGroup(this.Kind);
Geometry geometry = PackIconCursorHelper.GetCursorGeometry(GetPathData(this.Kind), transformGroup, Width, Height);
return PackIconCursorHelper.GeometryToCursor(geometry, Brush, StrokeBrush, StrokeThickness, HotSpot);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
using System;
using System.Windows;
using System.Windows.Input;
using System.Windows.Markup;
using System.Windows.Media;

namespace MahApps.Metro.IconPacks
{
[MarkupExtensionReturnType(typeof(Cursor))]
public class CodiconsCursorExtension : CodiconsImageExtension, IPackIconCursorExtension
{
public CodiconsCursorExtension() : base() => base.Brush = PackIconCursorHelper.DefaultBrush;
public CodiconsCursorExtension(PackIconCodiconsKind kind) : base(kind) => base.Brush = PackIconCursorHelper.DefaultBrush;

/// <inheritdoc/>
public Point HotSpot { get; set; }
/// <inheritdoc/>
public double Width { get; set; } = PackIconCursorHelper.DefaultWidth;
/// <inheritdoc/>
public double Height { get; set; } = PackIconCursorHelper.DefaultHeight;
/// <inheritdoc/>
public Brush StrokeBrush { get; set; }
/// <inheritdoc/>
public double StrokeThickness { get; set; } = PackIconCursorHelper.DefaultStrokeThickness;

public override object ProvideValue(IServiceProvider serviceProvider)
{
TransformGroup transformGroup = (TransformGroup)GetTransformGroup(this.Kind);
Geometry geometry = PackIconCursorHelper.GetCursorGeometry(GetPathData(this.Kind), transformGroup, Width, Height);
return PackIconCursorHelper.GeometryToCursor(geometry, Brush, StrokeBrush, StrokeThickness, HotSpot);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
using System;
using System.Windows;
using System.Windows.Input;
using System.Windows.Markup;
using System.Windows.Media;

namespace MahApps.Metro.IconPacks
{
[MarkupExtensionReturnType(typeof(Cursor))]
public class CooliconsCursorExtension : CooliconsImageExtension, IPackIconCursorExtension
{
public CooliconsCursorExtension() : base() => base.Brush = PackIconCursorHelper.DefaultBrush;
public CooliconsCursorExtension(PackIconCooliconsKind kind) : base(kind) => base.Brush = PackIconCursorHelper.DefaultBrush;

/// <inheritdoc/>
public Point HotSpot { get; set; }
/// <inheritdoc/>
public double Width { get; set; } = PackIconCursorHelper.DefaultWidth;
/// <inheritdoc/>
public double Height { get; set; } = PackIconCursorHelper.DefaultHeight;
/// <inheritdoc/>
public Brush StrokeBrush { get; set; }
/// <inheritdoc/>
public double StrokeThickness { get; set; } = PackIconCursorHelper.DefaultStrokeThickness;

public override object ProvideValue(IServiceProvider serviceProvider)
{
TransformGroup transformGroup = (TransformGroup)GetTransformGroup(this.Kind);
Geometry geometry = PackIconCursorHelper.GetCursorGeometry(GetPathData(this.Kind), transformGroup, Width, Height);
return PackIconCursorHelper.GeometryToCursor(geometry, Brush, StrokeBrush, StrokeThickness, HotSpot);
}
}
}
Loading
Loading