diff --git a/HandyControl/Controls/GifImage.cs b/HandyControl/Controls/GifImage.cs
new file mode 100644
index 00000000..9c7a91de
--- /dev/null
+++ b/HandyControl/Controls/GifImage.cs
@@ -0,0 +1,461 @@
+using System;
+using System.ComponentModel;
+using System.IO;
+using System.Linq;
+using System.Runtime.InteropServices;
+using System.Runtime.Versioning;
+using System.Security.Permissions;
+using System.Windows;
+using System.Windows.Controls;
+using System.Windows.Interop;
+using System.Windows.Media;
+using System.Windows.Media.Imaging;
+using System.Windows.Resources;
+using HandyControl.Data;
+using HandyControl.Tools;
+
+namespace HandyControl.Controls
+{
+ public class GifImage : Image, IDisposable
+ {
+ private static readonly Guid GifGuid = new Guid("{b96b3caa-0728-11d3-9d7b-0000f81ef32e}");
+
+ internal IntPtr NativeImage;
+
+ private byte[] _rawData;
+
+ private bool _isStart;
+
+ private bool _isLoaded;
+
+ static GifImage()
+ {
+ VisibilityProperty.OverrideMetadata(typeof(GifImage), new PropertyMetadata(default(Visibility), OnVisibilityChanged));
+ }
+
+ private static void OnVisibilityChanged(DependencyObject s, DependencyPropertyChangedEventArgs e)
+ {
+ var ctl = (GifImage)s;
+ var v = (Visibility)e.NewValue;
+ if (v != Visibility.Visible)
+ {
+ ctl.StopAnimate();
+ }
+ else if (!ctl._isStart)
+ {
+ ctl.StartAnimate();
+ }
+ }
+
+ ~GifImage() => Dispose(false);
+
+ public GifImage()
+ {
+ Loaded += (s, e) =>
+ {
+ if (DesignerProperties.GetIsInDesignMode(this)) return;
+ if (_isLoaded) return;
+ _isLoaded = true;
+ if (Source is BitmapImage image)
+ {
+ var uri = image.UriSource;
+ GetGifStreamFromPack(uri);
+ StartAnimate();
+ }
+ };
+ }
+
+ public GifImage(string filename)
+ {
+ Source = null;
+ CreateSourceFromFile(filename);
+ StartAnimate();
+ }
+
+ public GifImage(Stream stream)
+ {
+ Source = null;
+ CreateSourceFromStream(stream);
+ StartAnimate();
+ }
+
+ public void Dispose()
+ {
+ Dispose(true);
+ GC.SuppressFinalize(this);
+ }
+
+ protected virtual void Dispose(bool disposing)
+ {
+ if (NativeImage == IntPtr.Zero) return;
+ try
+ {
+ StopAnimate();
+ ExternDllHelper.Gdip.GdipDisposeImage(new HandleRef(this, NativeImage));
+ }
+ catch
+ {
+ // ignored
+ }
+ finally
+ {
+ NativeImage = IntPtr.Zero;
+ }
+ }
+
+ private void CreateSourceFromFile(string filename)
+ {
+ filename = Path.GetFullPath(filename);
+
+ var status = ExternDllHelper.Gdip.GdipCreateBitmapFromFile(filename, out var bitmap);
+
+ if (status != ExternDllHelper.Gdip.Ok)
+ throw ExternDllHelper.Gdip.StatusException(status);
+
+ status = ExternDllHelper.Gdip.GdipImageForceValidation(new HandleRef(null, bitmap));
+
+ if (status != ExternDllHelper.Gdip.Ok)
+ {
+ ExternDllHelper.Gdip.GdipDisposeImage(new HandleRef(null, bitmap));
+ throw ExternDllHelper.Gdip.StatusException(status);
+ }
+
+ SetNativeImage(bitmap);
+
+ EnsureSave(this, filename, null);
+ }
+
+ private void CreateSourceFromStream(Stream stream)
+ {
+ if (stream == null)
+ throw new ArgumentException("stream null");
+
+ var status = ExternDllHelper.Gdip.GdipCreateBitmapFromStream(new GPStream(stream), out var bitmap);
+
+ if (status != ExternDllHelper.Gdip.Ok)
+ throw ExternDllHelper.Gdip.StatusException(status);
+
+ status = ExternDllHelper.Gdip.GdipImageForceValidation(new HandleRef(null, bitmap));
+
+ if (status != ExternDllHelper.Gdip.Ok)
+ {
+ ExternDllHelper.Gdip.GdipDisposeImage(new HandleRef(null, bitmap));
+ throw ExternDllHelper.Gdip.StatusException(status);
+ }
+
+ SetNativeImage(bitmap);
+
+ EnsureSave(this, null, stream);
+ }
+
+ private void GetGifStreamFromPack(Uri uri)
+ {
+ try
+ {
+ StreamResourceInfo streamInfo;
+
+ if (!uri.IsAbsoluteUri)
+ {
+ streamInfo = Application.GetContentStream(uri) ?? Application.GetResourceStream(uri);
+ }
+ else
+ {
+ if (uri.GetLeftPart(UriPartial.Authority).Contains("siteoforigin"))
+ {
+ streamInfo = Application.GetRemoteStream(uri);
+ }
+ else
+ {
+ streamInfo = Application.GetContentStream(uri) ?? Application.GetResourceStream(uri);
+ }
+ }
+ if (streamInfo == null)
+ throw new FileNotFoundException("Resource not found.", uri.ToString());
+ CreateSourceFromStream(streamInfo.Stream);
+ }
+ catch
+ {
+ // ignored
+ }
+ }
+
+ [ResourceExposure(ResourceScope.None)]
+ [ResourceConsumption(ResourceScope.Machine, ResourceScope.Machine)]
+ internal static void EnsureSave(GifImage image, string filename, Stream dataStream)
+ {
+ if (!image.RawGuid.Equals(GifGuid)) return;
+ var animatedGif = false;
+
+ var dimensions = image.FrameDimensionsList;
+ if (dimensions.Select(guid => new GifFrameDimension(guid)).Contains(GifFrameDimension.Time))
+ {
+ animatedGif = image.GetFrameCount(GifFrameDimension.Time) > 1;
+ }
+
+ if (!animatedGif) return;
+
+ try
+ {
+ Stream created = null;
+ long lastPos = 0;
+ if (dataStream != null)
+ {
+ lastPos = dataStream.Position;
+ dataStream.Position = 0;
+ }
+
+ try
+ {
+ if (dataStream == null)
+ {
+ created = dataStream = File.OpenRead(filename);
+ }
+
+ image._rawData = new byte[(int)dataStream.Length];
+ dataStream.Read(image._rawData, 0, (int)dataStream.Length);
+ }
+ finally
+ {
+ if (created != null)
+ {
+ created.Close();
+ }
+ else
+ {
+ // ReSharper disable once PossibleNullReferenceException
+ dataStream.Position = lastPos;
+ }
+ }
+ }
+ // possible exceptions for reading the filename
+ catch (UnauthorizedAccessException)
+ {
+ }
+ catch (DirectoryNotFoundException)
+ {
+ }
+ catch (IOException)
+ {
+ }
+ // possible exceptions for setting/getting the position inside dataStream
+ catch (NotSupportedException)
+ {
+ }
+ catch (ObjectDisposedException)
+ {
+ }
+ // possible exception when reading stuff into dataStream
+ catch (ArgumentException)
+ {
+ }
+ }
+
+ internal void SetNativeImage(IntPtr handle)
+ {
+ if (handle == IntPtr.Zero)
+ throw new ArgumentException("NativeHandle0");
+
+ NativeImage = handle;
+ }
+
+ internal Guid RawGuid
+ {
+ get
+ {
+ var guid = new Guid();
+
+ var status = ExternDllHelper.Gdip.GdipGetImageRawFormat(new HandleRef(this, NativeImage), ref guid);
+
+ if (status != ExternDllHelper.Gdip.Ok)
+ throw ExternDllHelper.Gdip.StatusException(status);
+
+ return guid;
+ }
+ }
+
+ internal void StartAnimate()
+ {
+ ImageAnimator.Animate(this, OnFrameChanged);
+ _isStart = true;
+ }
+
+ internal void StopAnimate()
+ {
+ ImageAnimator.StopAnimate(this, OnFrameChanged);
+ _isStart = false;
+ }
+
+ internal Guid[] FrameDimensionsList
+ {
+ get
+ {
+ var status = ExternDllHelper.Gdip.GdipImageGetFrameDimensionsCount(new HandleRef(this, NativeImage), out var count);
+
+ if (status != ExternDllHelper.Gdip.Ok)
+ {
+ throw ExternDllHelper.Gdip.StatusException(status);
+ }
+
+ if (count <= 0)
+ {
+ return new Guid[0];
+ }
+
+ var size = Marshal.SizeOf(typeof(Guid));
+
+ var buffer = Marshal.AllocHGlobal(checked(size * count));
+ if (buffer == IntPtr.Zero)
+ {
+ throw ExternDllHelper.Gdip.StatusException(ExternDllHelper.Gdip.OutOfMemory);
+ }
+
+ status = ExternDllHelper.Gdip.GdipImageGetFrameDimensionsList(new HandleRef(this, NativeImage), buffer, count);
+
+ if (status != ExternDllHelper.Gdip.Ok)
+ {
+ Marshal.FreeHGlobal(buffer);
+ throw ExternDllHelper.Gdip.StatusException(status);
+ }
+
+ var guids = new Guid[count];
+
+ try
+ {
+ for (int i = 0; i < count; i++)
+ {
+ guids[i] = (Guid)ExternDllHelper.PtrToStructure((IntPtr)((long)buffer + size * i), typeof(Guid));
+ }
+ }
+ finally
+ {
+ Marshal.FreeHGlobal(buffer);
+ }
+
+ return guids;
+ }
+ }
+
+ internal int GetFrameCount(GifFrameDimension dimension)
+ {
+ var count = new[] { 0 };
+
+ var dimensionId = dimension.Guid;
+ var status = ExternDllHelper.Gdip.GdipImageGetFrameCount(new HandleRef(this, NativeImage), ref dimensionId, count);
+
+ if (status != ExternDllHelper.Gdip.Ok)
+ throw ExternDllHelper.Gdip.StatusException(status);
+
+ return count[0];
+ }
+
+ internal GifPropertyItem GetPropertyItem(int propid)
+ {
+ GifPropertyItem propitem;
+
+ var status = ExternDllHelper.Gdip.GdipGetPropertyItemSize(new HandleRef(this, NativeImage), propid, out var size);
+
+ if (status != ExternDllHelper.Gdip.Ok)
+ throw ExternDllHelper.Gdip.StatusException(status);
+
+ if (size == 0)
+ return null;
+
+ var propdata = Marshal.AllocHGlobal(size);
+
+ if (propdata == IntPtr.Zero)
+ throw ExternDllHelper.Gdip.StatusException(ExternDllHelper.Gdip.OutOfMemory);
+
+ status = ExternDllHelper.Gdip.GdipGetPropertyItem(new HandleRef(this, NativeImage), propid, size, propdata);
+
+ try
+ {
+ if (status != ExternDllHelper.Gdip.Ok)
+ {
+ throw ExternDllHelper.Gdip.StatusException(status);
+ }
+
+ propitem = GifPropertyItemInternal.ConvertFromMemory(propdata, 1)[0];
+ }
+ finally
+ {
+ Marshal.FreeHGlobal(propdata);
+ }
+
+ return propitem;
+ }
+
+ internal int SelectActiveFrame(GifFrameDimension dimension, int frameIndex)
+ {
+ var count = new[] { 0 };
+
+ var dimensionId = dimension.Guid;
+ var status = ExternDllHelper.Gdip.GdipImageSelectActiveFrame(new HandleRef(this, NativeImage), ref dimensionId, frameIndex);
+
+ if (status != ExternDllHelper.Gdip.Ok)
+ throw ExternDllHelper.Gdip.StatusException(status);
+
+ return count[0];
+ }
+
+ [SecurityPermission(SecurityAction.LinkDemand, Flags = SecurityPermissionFlag.UnmanagedCode)]
+ [EditorBrowsable(EditorBrowsableState.Advanced)]
+ [ResourceExposure(ResourceScope.Machine)]
+ [ResourceConsumption(ResourceScope.Machine)]
+ internal IntPtr GetHbitmap() => GetHbitmap(Colors.LightGray);
+
+ [SecurityPermission(SecurityAction.LinkDemand, Flags = SecurityPermissionFlag.UnmanagedCode)]
+ [EditorBrowsable(EditorBrowsableState.Advanced)]
+ [ResourceExposure(ResourceScope.Machine)]
+ [ResourceConsumption(ResourceScope.Machine)]
+ internal IntPtr GetHbitmap(Color background)
+ {
+ var status = ExternDllHelper.Gdip.GdipCreateHBITMAPFromBitmap(new HandleRef(this, NativeImage), out var hBitmap,
+ ColorHelper.ToWin32(background));
+ if (status == 2 && (Width >= short.MaxValue || Height >= short.MaxValue))
+ {
+ throw new ArgumentException("GdiplusInvalidSize");
+ }
+
+ if (status != ExternDllHelper.Gdip.Ok && NativeImage != IntPtr.Zero)
+ {
+ throw ExternDllHelper.Gdip.StatusException(status);
+ }
+
+ return hBitmap;
+ }
+
+ private void GetBitmapSource()
+ {
+ var handle = IntPtr.Zero;
+
+ try
+ {
+ handle = GetHbitmap();
+ Source = Imaging.CreateBitmapSourceFromHBitmap(handle, IntPtr.Zero, Int32Rect.Empty,
+ BitmapSizeOptions.FromEmptyOptions());
+ }
+ catch
+ {
+ // ignored
+ }
+ finally
+ {
+ if (handle != IntPtr.Zero)
+ {
+ ExternDllHelper.DeleteObject(handle);
+ }
+ }
+ }
+
+ private void OnFrameChanged(object sender, EventArgs e)
+ {
+ Dispatcher.BeginInvoke(new Action(() =>
+ {
+ ImageAnimator.UpdateFrames();
+ Source?.Freeze();
+ GetBitmapSource();
+ InvalidateVisual();
+ }));
+ }
+ }
+}
\ No newline at end of file
diff --git a/HandyControl/Data/CancelRoutedEventArgs.cs b/HandyControl/Data/Args/CancelRoutedEventArgs.cs
similarity index 100%
rename from HandyControl/Data/CancelRoutedEventArgs.cs
rename to HandyControl/Data/Args/CancelRoutedEventArgs.cs
diff --git a/HandyControl/Data/FunctionEventArgs!1.cs b/HandyControl/Data/Args/FunctionEventArgs!1.cs
similarity index 100%
rename from HandyControl/Data/FunctionEventArgs!1.cs
rename to HandyControl/Data/Args/FunctionEventArgs!1.cs
diff --git a/HandyControl/Data/Gif/GPStream.cs b/HandyControl/Data/Gif/GPStream.cs
new file mode 100644
index 00000000..4374b4f6
--- /dev/null
+++ b/HandyControl/Data/Gif/GPStream.cs
@@ -0,0 +1,271 @@
+using System;
+using System.Diagnostics.CodeAnalysis;
+using System.IO;
+using System.Runtime.InteropServices;
+using System.Security.Permissions;
+using HandyControl.Tools;
+
+namespace HandyControl.Data
+{
+ // ReSharper disable once InconsistentNaming
+ internal class GPStream : ExternDllHelper.IStream
+ {
+ protected Stream DataStream;
+
+ // to support seeking ahead of the stream length...
+ long _virtualPosition = -1;
+
+ internal GPStream(Stream stream)
+ {
+ if (!stream.CanSeek)
+ {
+ const int readBlock = 256;
+ var bytes = new byte[readBlock];
+ int readLen;
+ var current = 0;
+ do
+ {
+ if (bytes.Length < current + readBlock)
+ {
+ var newData = new byte[bytes.Length * 2];
+ Array.Copy(bytes, newData, bytes.Length);
+ bytes = newData;
+ }
+ readLen = stream.Read(bytes, current, readBlock);
+ current += readLen;
+ } while (readLen != 0);
+
+ DataStream = new MemoryStream(bytes);
+ }
+ else
+ {
+ DataStream = stream;
+ }
+ }
+
+ private void ActualizeVirtualPosition()
+ {
+ if (_virtualPosition == -1) return;
+
+ if (_virtualPosition > DataStream.Length)
+ DataStream.SetLength(_virtualPosition);
+
+ DataStream.Position = _virtualPosition;
+
+ _virtualPosition = -1;
+ }
+
+ public virtual ExternDllHelper.IStream Clone()
+ {
+ NotImplemented();
+ return null;
+ }
+
+ public virtual void Commit(int grfCommitFlags)
+ {
+ DataStream.Flush();
+ // Extend the length of the file if needed.
+ ActualizeVirtualPosition();
+ }
+
+ [
+ UIPermission(SecurityAction.Demand, Window = UIPermissionWindow.AllWindows),
+ SecurityPermission(SecurityAction.Assert, Flags = SecurityPermissionFlag.UnmanagedCode)
+ ]
+ public virtual long CopyTo(ExternDllHelper.IStream pstm, long cb, long[] pcbRead)
+ {
+
+ const int bufsize = 4096;
+ var buffer = Marshal.AllocHGlobal(bufsize);
+ if (buffer == IntPtr.Zero) throw new OutOfMemoryException();
+ long written = 0;
+ try
+ {
+ while (written < cb)
+ {
+ var toRead = bufsize;
+ if (written + toRead > cb) toRead = (int)(cb - written);
+ var read = Read(buffer, toRead);
+ if (read == 0) break;
+ if (pstm.Write(buffer, read) != read)
+ {
+ throw EFail("Wrote an incorrect number of bytes");
+ }
+ written += read;
+ }
+ }
+ finally
+ {
+ Marshal.FreeHGlobal(buffer);
+ }
+ if (pcbRead != null && pcbRead.Length > 0)
+ {
+ pcbRead[0] = written;
+ }
+
+ return written;
+ }
+
+ public virtual Stream GetDataStream()
+ {
+ return DataStream;
+ }
+
+ public virtual void LockRegion(long libOffset, long cb, int dwLockType)
+ {
+ }
+
+ protected static ExternalException EFail(string msg)
+ {
+ throw new ExternalException(msg, ExternDllHelper.E_FAIL);
+ }
+
+ protected static void NotImplemented()
+ {
+ throw new ExternalException("NotImplemented");
+ }
+
+ public virtual int Read(IntPtr buf, int length)
+ {
+ var buffer = new byte[length];
+ var count = Read(buffer, length);
+ Marshal.Copy(buffer, 0, buf, length);
+ return count;
+ }
+
+ public virtual int Read(byte[] buffer, int length)
+ {
+ ActualizeVirtualPosition();
+ return DataStream.Read(buffer, 0, length);
+ }
+
+ public virtual void Revert()
+ {
+ NotImplemented();
+ }
+
+ public virtual long Seek(long offset, int origin)
+ {
+ var pos = _virtualPosition;
+ if (_virtualPosition == -1)
+ {
+ pos = DataStream.Position;
+ }
+ var len = DataStream.Length;
+ switch (origin)
+ {
+ case ExternDllHelper.StreamConsts.STREAM_SEEK_SET:
+ if (offset <= len)
+ {
+ DataStream.Position = offset;
+ _virtualPosition = -1;
+ }
+ else
+ {
+ _virtualPosition = offset;
+ }
+ break;
+ case ExternDllHelper.StreamConsts.STREAM_SEEK_END:
+ if (offset <= 0)
+ {
+ DataStream.Position = len + offset;
+ _virtualPosition = -1;
+ }
+ else
+ {
+ _virtualPosition = len + offset;
+ }
+ break;
+ case ExternDllHelper.StreamConsts.STREAM_SEEK_CUR:
+ if (offset + pos <= len)
+ {
+ DataStream.Position = pos + offset;
+ _virtualPosition = -1;
+ }
+ else
+ {
+ _virtualPosition = offset + pos;
+ }
+ break;
+ }
+ return _virtualPosition != -1 ? _virtualPosition : DataStream.Position;
+ }
+
+ public virtual void SetSize(long value)
+ {
+ DataStream.SetLength(value);
+ }
+
+ public void Stat(IntPtr pstatstg, int grfStatFlag)
+ {
+ var stats = new STATSTG {cbSize = DataStream.Length};
+ Marshal.StructureToPtr(stats, pstatstg, true);
+ }
+
+ public virtual void UnlockRegion(long libOffset, long cb, int dwLockType)
+ {
+ }
+
+ public virtual int Write(IntPtr buf, int length)
+ {
+ var buffer = new byte[length];
+ Marshal.Copy(buf, buffer, 0, length);
+ return Write(buffer, length);
+ }
+
+ public virtual int Write(byte[] buffer, int length)
+ {
+ ActualizeVirtualPosition();
+ DataStream.Write(buffer, 0, length);
+ return length;
+ }
+
+ [StructLayout(LayoutKind.Sequential)]
+ [SuppressMessage("ReSharper", "InconsistentNaming")]
+ public class STATSTG
+ {
+
+ [SuppressMessage("Microsoft.Reliability", "CA2006:UseSafeHandleToEncapsulateNativeResources")]
+ public IntPtr pwcsName = IntPtr.Zero;
+ public int type;
+ [MarshalAs(UnmanagedType.I8)]
+ public long cbSize;
+ [MarshalAs(UnmanagedType.I8)]
+ public long mtime;
+ [MarshalAs(UnmanagedType.I8)]
+ public long ctime;
+ [MarshalAs(UnmanagedType.I8)]
+ public long atime;
+ [MarshalAs(UnmanagedType.I4)]
+ public int grfMode;
+ [MarshalAs(UnmanagedType.I4)]
+ public int grfLocksSupported;
+
+ public int clsid_data1;
+ [MarshalAs(UnmanagedType.I2)]
+ public short clsid_data2;
+ [MarshalAs(UnmanagedType.I2)]
+ public short clsid_data3;
+ [MarshalAs(UnmanagedType.U1)]
+ public byte clsid_b0;
+ [MarshalAs(UnmanagedType.U1)]
+ public byte clsid_b1;
+ [MarshalAs(UnmanagedType.U1)]
+ public byte clsid_b2;
+ [MarshalAs(UnmanagedType.U1)]
+ public byte clsid_b3;
+ [MarshalAs(UnmanagedType.U1)]
+ public byte clsid_b4;
+ [MarshalAs(UnmanagedType.U1)]
+ public byte clsid_b5;
+ [MarshalAs(UnmanagedType.U1)]
+ public byte clsid_b6;
+ [MarshalAs(UnmanagedType.U1)]
+ public byte clsid_b7;
+ [MarshalAs(UnmanagedType.I4)]
+ public int grfStateBits;
+ [MarshalAs(UnmanagedType.I4)]
+ public int reserved;
+ }
+ }
+}
\ No newline at end of file
diff --git a/HandyControl/Data/Gif/GifFrameDimension.cs b/HandyControl/Data/Gif/GifFrameDimension.cs
new file mode 100644
index 00000000..cc2a6366
--- /dev/null
+++ b/HandyControl/Data/Gif/GifFrameDimension.cs
@@ -0,0 +1,29 @@
+using System;
+
+namespace HandyControl.Data
+{
+ internal class GifFrameDimension
+ {
+ public GifFrameDimension(Guid guid) => Guid = guid;
+
+ public Guid Guid { get; }
+
+ public static GifFrameDimension Time { get; } = new GifFrameDimension(new Guid("{6aedbd6d-3fb5-418a-83a6-7f45229dc872}"));
+
+ public static GifFrameDimension Resolution { get; } = new GifFrameDimension(new Guid("{84236f7b-3bd3-428f-8dab-4ea1439ca315}"));
+
+ public static GifFrameDimension Page { get; } = new GifFrameDimension(new Guid("{7462dc86-6180-4c7e-8e3f-ee7333a7a483}"));
+
+ public override bool Equals(object o) => o is GifFrameDimension format && Guid == format.Guid;
+
+ public override int GetHashCode() => Guid.GetHashCode();
+
+ public override string ToString()
+ {
+ if (Equals(this, Time)) return "Time";
+ if (Equals(this, Resolution)) return "Resolution";
+ if (Equals(this, Page)) return "Page";
+ return "[FrameDimension: " + Guid + "]";
+ }
+ }
+}
\ No newline at end of file
diff --git a/HandyControl/Data/Gif/GifImageInfo.cs b/HandyControl/Data/Gif/GifImageInfo.cs
new file mode 100644
index 00000000..6a786fec
--- /dev/null
+++ b/HandyControl/Data/Gif/GifImageInfo.cs
@@ -0,0 +1,97 @@
+using System;
+using HandyControl.Controls;
+
+namespace HandyControl.Data
+{
+ public class GifImageInfo
+ {
+ private const int PropertyTagFrameDelay = 0x5100;
+
+ private int _frame;
+
+ private readonly int[] _frameDelay;
+
+ public int FrameCount { get; }
+
+ internal GifImage Image { get; }
+
+ public bool Animated { get; }
+
+ public EventHandler FrameChangedHandler { get; set; }
+
+ internal int FrameTimer { get; set; }
+
+ public bool FrameDirty { get; private set; }
+
+ public int Frame
+ {
+ get => _frame;
+ set
+ {
+ if (_frame != value)
+ {
+ if (value < 0 || value >= FrameCount)
+ {
+ throw new ArgumentException("InvalidFrame");
+ }
+
+ if (Animated)
+ {
+ _frame = value;
+ FrameDirty = true;
+
+ OnFrameChanged(EventArgs.Empty);
+ }
+ }
+ }
+ }
+
+ public GifImageInfo(GifImage image)
+ {
+ Image = image;
+ Animated = ImageAnimator.CanAnimate(image);
+
+ if (Animated)
+ {
+ FrameCount = image.GetFrameCount(GifFrameDimension.Time);
+
+ var frameDelayItem = image.GetPropertyItem(PropertyTagFrameDelay);
+
+ // If the image does not have a frame delay, we just return 0.
+ //
+ if (frameDelayItem != null)
+ {
+ // Convert the frame delay from byte[] to int
+ //
+ var values = frameDelayItem.Value;
+ _frameDelay = new int[FrameCount];
+ for (int i = 0; i < FrameCount; ++i)
+ {
+ _frameDelay[i] = values[i * 4] + 256 * values[i * 4 + 1] + 256 * 256 * values[i * 4 + 2] + 256 * 256 * 256 * values[i * 4 + 3];
+ }
+ }
+ }
+ else
+ {
+ FrameCount = 1;
+ }
+ if (_frameDelay == null)
+ {
+ _frameDelay = new int[FrameCount];
+ }
+ }
+
+ public int FrameDelay(int frame) => _frameDelay[frame];
+
+ internal void UpdateFrame()
+ {
+ if (FrameDirty)
+ {
+ Image.SelectActiveFrame(GifFrameDimension.Time, Frame);
+ FrameDirty = false;
+ }
+ }
+
+ protected void OnFrameChanged(EventArgs e) => FrameChangedHandler?.Invoke(Image, e);
+ }
+}
\ No newline at end of file
diff --git a/HandyControl/Data/Gif/GifPropertyItem.cs b/HandyControl/Data/Gif/GifPropertyItem.cs
new file mode 100644
index 00000000..e6061635
--- /dev/null
+++ b/HandyControl/Data/Gif/GifPropertyItem.cs
@@ -0,0 +1,13 @@
+namespace HandyControl.Data
+{
+ public class GifPropertyItem
+ {
+ public int Id { get; set; }
+
+ public int Len { get; set; }
+
+ public short Type { get; set; }
+
+ public byte[] Value { get; set; }
+ }
+}
\ No newline at end of file
diff --git a/HandyControl/Data/Gif/GifPropertyItemInternal.cs b/HandyControl/Data/Gif/GifPropertyItemInternal.cs
new file mode 100644
index 00000000..c2643091
--- /dev/null
+++ b/HandyControl/Data/Gif/GifPropertyItemInternal.cs
@@ -0,0 +1,114 @@
+using System;
+using System.Runtime.InteropServices;
+using HandyControl.Tools;
+
+namespace HandyControl.Data
+{
+ [StructLayout(LayoutKind.Sequential)]
+ public class GifPropertyItemInternal : IDisposable
+ {
+ public int id;
+ public int len;
+ public short type;
+ public IntPtr value = IntPtr.Zero;
+
+ internal GifPropertyItemInternal()
+ {
+ }
+
+ public byte[] Value
+ {
+ get
+ {
+ if (len == 0)
+ return null;
+
+ var bytes = new byte[len];
+
+ Marshal.Copy(value,
+ bytes,
+ 0,
+ len);
+ return bytes;
+ }
+ }
+
+ public void Dispose()
+ {
+ Dispose(true);
+ }
+
+ ~GifPropertyItemInternal()
+ {
+ Dispose(false);
+ }
+
+ private void Dispose(bool disposing)
+ {
+ if (value != IntPtr.Zero)
+ {
+ Marshal.FreeHGlobal(value);
+ value = IntPtr.Zero;
+ }
+
+ if (disposing)
+ GC.SuppressFinalize(this);
+ }
+
+ internal static GifPropertyItemInternal ConvertFromPropertyItem(GifPropertyItem propItem)
+ {
+ var propItemInternal = new GifPropertyItemInternal
+ {
+ id = propItem.Id,
+ len = 0,
+ type = propItem.Type
+ };
+
+ var propItemValue = propItem.Value;
+ if (propItemValue != null)
+ {
+ var length = propItemValue.Length;
+ propItemInternal.len = length;
+ propItemInternal.value = Marshal.AllocHGlobal(length);
+ Marshal.Copy(propItemValue, 0, propItemInternal.value, length);
+ }
+
+ return propItemInternal;
+ }
+
+ internal static GifPropertyItem[] ConvertFromMemory(IntPtr propdata, int count)
+ {
+ var props = new GifPropertyItem[count];
+
+ for (var i = 0; i < count; i++)
+ {
+ GifPropertyItemInternal propcopy = null;
+ try
+ {
+ propcopy = (GifPropertyItemInternal) ExternDllHelper.PtrToStructure(propdata,
+ typeof(GifPropertyItemInternal));
+
+ props[i] = new GifPropertyItem
+ {
+ Id = propcopy.id,
+ Len = propcopy.len,
+ Type = propcopy.type,
+ Value = propcopy.Value
+ };
+
+ // this calls Marshal.Copy and creates a copy of the original memory into a byte array.
+
+ propcopy.value = IntPtr.Zero; // we dont actually own this memory so dont free it.
+ }
+ finally
+ {
+ propcopy?.Dispose();
+ }
+
+ propdata = (IntPtr) ((long) propdata + Marshal.SizeOf(typeof(GifPropertyItemInternal)));
+ }
+
+ return props;
+ }
+ }
+}
\ No newline at end of file
diff --git a/HandyControl/Data/OperationResult!1.cs b/HandyControl/Data/Operation/OperationResult!1.cs
similarity index 100%
rename from HandyControl/Data/OperationResult!1.cs
rename to HandyControl/Data/Operation/OperationResult!1.cs
diff --git a/HandyControl/Data/OperationResult.cs b/HandyControl/Data/Operation/OperationResult.cs
similarity index 100%
rename from HandyControl/Data/OperationResult.cs
rename to HandyControl/Data/Operation/OperationResult.cs
diff --git a/HandyControl/HandyControl.csproj b/HandyControl/HandyControl.csproj
index 9f9179d0..9f632c61 100644
--- a/HandyControl/HandyControl.csproj
+++ b/HandyControl/HandyControl.csproj
@@ -49,6 +49,7 @@
+
@@ -78,15 +79,22 @@
-
+
+
+
+
+
+
+
+
-
-
+
+
True
True
@@ -124,6 +132,7 @@
+
@@ -141,7 +150,7 @@
-
+
diff --git a/HandyControl/Tools/ColorHelper.cs b/HandyControl/Tools/ColorHelper.cs
new file mode 100644
index 00000000..6163f7bc
--- /dev/null
+++ b/HandyControl/Tools/ColorHelper.cs
@@ -0,0 +1,13 @@
+using System.Windows.Media;
+
+namespace HandyControl.Tools
+{
+ public class ColorHelper
+ {
+ private const int Win32RedShift = 0;
+ private const int Win32GreenShift = 8;
+ private const int Win32BlueShift = 16;
+
+ public static int ToWin32(Color c) => c.R << Win32RedShift | c.G << Win32GreenShift | c.B << Win32BlueShift;
+ }
+}
\ No newline at end of file
diff --git a/HandyControl/Tools/ExternDllHelper.cs b/HandyControl/Tools/ExternDllHelper.cs
new file mode 100644
index 00000000..c1e327ea
--- /dev/null
+++ b/HandyControl/Tools/ExternDllHelper.cs
@@ -0,0 +1,332 @@
+using System;
+using System.Diagnostics.CodeAnalysis;
+using System.IO;
+using System.Runtime.CompilerServices;
+using System.Runtime.ConstrainedExecution;
+using System.Runtime.InteropServices;
+using System.Runtime.Versioning;
+using System.Security.Permissions;
+using System.Threading;
+
+namespace HandyControl.Tools
+{
+ internal class ExternDllHelper
+ {
+ private const string Gdi32 = "gdi32.dll";
+
+ // ReSharper disable once InconsistentNaming
+ public const int E_FAIL = unchecked((int) 0x80004005);
+
+ [ReflectionPermission(SecurityAction.Assert, Unrestricted = true), SecurityPermission(SecurityAction.Assert, Flags = SecurityPermissionFlag.UnmanagedCode)]
+ public static object PtrToStructure(IntPtr lparam, Type cls) => Marshal.PtrToStructure(lparam, cls);
+
+ [DllImport(Gdi32, CharSet = CharSet.Auto, SetLastError = true)]
+ [return: MarshalAs(UnmanagedType.Bool)]
+ public static extern bool DeleteObject(IntPtr hObject);
+
+ [ComImport, Guid("0000000C-0000-0000-C000-000000000046"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
+ public interface IStream
+ {
+ int Read(
+ [In]
+ IntPtr buf,
+ [In]
+ int len);
+
+
+ int Write(
+ [In]
+ IntPtr buf,
+ [In]
+ int len);
+
+ [return: MarshalAs(UnmanagedType.I8)]
+ long Seek(
+ [In, MarshalAs(UnmanagedType.I8)]
+ long dlibMove,
+ [In]
+ int dwOrigin);
+
+
+ void SetSize(
+ [In, MarshalAs(UnmanagedType.I8)]
+ long libNewSize);
+
+ [return: MarshalAs(UnmanagedType.I8)]
+ long CopyTo(
+ [In, MarshalAs(UnmanagedType.Interface)]
+ IStream pstm,
+ [In, MarshalAs(UnmanagedType.I8)]
+ long cb,
+ [Out, MarshalAs(UnmanagedType.LPArray)]
+ long[] pcbRead);
+
+
+ void Commit(
+ [In]
+ int grfCommitFlags);
+
+
+ void Revert();
+
+
+ void LockRegion(
+ [In, MarshalAs(UnmanagedType.I8)]
+ long libOffset,
+ [In, MarshalAs(UnmanagedType.I8)]
+ long cb,
+ [In]
+ int dwLockType);
+
+
+ void UnlockRegion(
+ [In, MarshalAs(UnmanagedType.I8)]
+ long libOffset,
+ [In, MarshalAs(UnmanagedType.I8)]
+ long cb,
+ [In]
+ int dwLockType);
+
+
+ void Stat(
+ [In]
+ IntPtr pStatstg,
+ [In]
+ int grfStatFlag);
+
+ [return: MarshalAs(UnmanagedType.Interface)]
+ IStream Clone();
+ }
+
+ [SuppressMessage("ReSharper", "InconsistentNaming")]
+ public class StreamConsts
+ {
+ public const int LOCK_WRITE = 0x1;
+ public const int LOCK_EXCLUSIVE = 0x2;
+ public const int LOCK_ONLYONCE = 0x4;
+ public const int STATFLAG_DEFAULT = 0x0;
+ public const int STATFLAG_NONAME = 0x1;
+ public const int STATFLAG_NOOPEN = 0x2;
+ public const int STGC_DEFAULT = 0x0;
+ public const int STGC_OVERWRITE = 0x1;
+ public const int STGC_ONLYIFCURRENT = 0x2;
+ public const int STGC_DANGEROUSLYCOMMITMERELYTODISKCACHE = 0x4;
+ public const int STREAM_SEEK_SET = 0x0;
+ public const int STREAM_SEEK_CUR = 0x1;
+ public const int STREAM_SEEK_END = 0x2;
+ }
+
+ internal class Gdip
+ {
+ private const string Gdiplus = "gdiplus.dll";
+
+ private const string ThreadDataSlotName = "system.drawing.threaddata";
+
+ private static IntPtr InitToken;
+
+ private static bool Initialized => InitToken != IntPtr.Zero;
+
+ static Gdip()
+ {
+ Initialize();
+ }
+
+ [StructLayout(LayoutKind.Sequential)]
+ private struct StartupInput
+ {
+ private int GdiplusVersion;
+
+ private readonly IntPtr DebugEventCallback;
+
+ private bool SuppressBackgroundThread;
+
+ private bool SuppressExternalCodecs;
+
+ public static StartupInput GetDefault()
+ {
+ var result = new StartupInput
+ {
+ GdiplusVersion = 1,
+ SuppressBackgroundThread = false,
+ SuppressExternalCodecs = false
+ };
+ return result;
+ }
+ }
+
+ [StructLayout(LayoutKind.Sequential)]
+ private struct StartupOutput
+ {
+ private readonly IntPtr hook;
+
+ private readonly IntPtr unhook;
+ }
+
+ [ResourceExposure(ResourceScope.None)]
+ [ResourceConsumption(ResourceScope.AppDomain, ResourceScope.AppDomain)]
+ [SuppressMessage("Microsoft.Performance", "CA1804:RemoveUnusedLocals")]
+ private static void Initialize()
+ {
+ var input = StartupInput.GetDefault();
+
+ var status = GdiplusStartup(out InitToken, ref input, out _);
+
+ if (status != Ok)
+ {
+ throw StatusException(status);
+ }
+
+ var currentDomain = AppDomain.CurrentDomain;
+ currentDomain.ProcessExit += OnProcessExit;
+
+ if (!currentDomain.IsDefaultAppDomain())
+ {
+ currentDomain.DomainUnload += OnProcessExit;
+ }
+ }
+
+ [PrePrepareMethod]
+ [ResourceExposure(ResourceScope.AppDomain)]
+ [ResourceConsumption(ResourceScope.AppDomain)]
+ private static void OnProcessExit(object sender, EventArgs e) => Shutdown();
+
+ [SuppressMessage("Microsoft.Reliability", "CA2001:AvoidCallingProblematicMethods")]
+ [ResourceExposure(ResourceScope.AppDomain)]
+ [ResourceConsumption(ResourceScope.AppDomain)]
+ private static void Shutdown()
+ {
+ if (Initialized)
+ {
+ ClearThreadData();
+ // unhook our shutdown handlers as we do not need to shut down more than once
+ var currentDomain = AppDomain.CurrentDomain;
+ currentDomain.ProcessExit -= OnProcessExit;
+ if (!currentDomain.IsDefaultAppDomain())
+ {
+ currentDomain.DomainUnload -= OnProcessExit;
+ }
+ }
+ }
+
+ [MethodImpl(MethodImplOptions.NoInlining)]
+ private static void ClearThreadData()
+ {
+ var slot = Thread.GetNamedDataSlot(ThreadDataSlotName);
+ Thread.SetData(slot, null);
+ }
+
+ //----------------------------------------------------------------------------------------
+ // Status codes
+ //----------------------------------------------------------------------------------------
+ internal const int Ok = 0;
+ internal const int GenericError = 1;
+ internal const int InvalidParameter = 2;
+ internal const int OutOfMemory = 3;
+ internal const int ObjectBusy = 4;
+ internal const int InsufficientBuffer = 5;
+ internal const int NotImplemented = 6;
+ internal const int Win32Error = 7;
+ internal const int WrongState = 8;
+ internal const int Aborted = 9;
+ internal const int FileNotFound = 10;
+ internal const int ValueOverflow = 11;
+ internal const int AccessDenied = 12;
+ internal const int UnknownImageFormat = 13;
+ internal const int FontFamilyNotFound = 14;
+ internal const int FontStyleNotFound = 15;
+ internal const int NotTrueTypeFont = 16;
+ internal const int UnsupportedGdiplusVersion = 17;
+ internal const int GdiplusNotInitialized = 18;
+ internal const int PropertyNotFound = 19;
+ internal const int PropertyNotSupported = 20;
+
+ [DllImport(Gdiplus, SetLastError = true, ExactSpelling = true, CharSet = CharSet.Unicode)]
+ [ResourceExposure(ResourceScope.None)]
+ internal static extern int GdipImageGetFrameDimensionsCount(HandleRef image, out int count);
+
+ internal static Exception StatusException(int status)
+ {
+ switch (status)
+ {
+ case GenericError: return new ExternalException("GdiplusGenericError");
+ case InvalidParameter: return new ArgumentException("GdiplusInvalidParameter");
+ case OutOfMemory: return new OutOfMemoryException("GdiplusOutOfMemory");
+ case ObjectBusy: return new InvalidOperationException("GdiplusObjectBusy");
+ case InsufficientBuffer: return new OutOfMemoryException("GdiplusInsufficientBuffer");
+ case NotImplemented: return new NotImplementedException("GdiplusNotImplemented");
+ case Win32Error: return new ExternalException("GdiplusGenericError");
+ case WrongState: return new InvalidOperationException("GdiplusWrongState");
+ case Aborted: return new ExternalException("GdiplusAborted");
+ case FileNotFound: return new FileNotFoundException("GdiplusFileNotFound");
+ case ValueOverflow: return new OverflowException("GdiplusOverflow");
+ case AccessDenied: return new ExternalException("GdiplusAccessDenied");
+ case UnknownImageFormat: return new ArgumentException("GdiplusUnknownImageFormat");
+ case PropertyNotFound: return new ArgumentException("GdiplusPropertyNotFoundError");
+ case PropertyNotSupported: return new ArgumentException("GdiplusPropertyNotSupportedError");
+ case FontFamilyNotFound: return new ArgumentException("GdiplusFontFamilyNotFound");
+ case FontStyleNotFound: return new ArgumentException("GdiplusFontStyleNotFound");
+ case NotTrueTypeFont: return new ArgumentException("GdiplusNotTrueTypeFont_NoName");
+ case UnsupportedGdiplusVersion: return new ExternalException("GdiplusUnsupportedGdiplusVersion");
+ case GdiplusNotInitialized: return new ExternalException("GdiplusNotInitialized");
+ }
+
+ return new ExternalException("GdiplusUnknown");
+ }
+
+ [DllImport(Gdiplus, SetLastError = true, ExactSpelling = true, CharSet = CharSet.Unicode)]
+ [ResourceExposure(ResourceScope.None)]
+ internal static extern int GdipImageGetFrameDimensionsList(HandleRef image, IntPtr buffer, int count);
+
+ [DllImport(Gdiplus, SetLastError = true, ExactSpelling = true, CharSet = CharSet.Unicode)]
+ [ResourceExposure(ResourceScope.None)]
+ internal static extern int GdipImageGetFrameCount(HandleRef image, ref Guid dimensionId, int[] count);
+
+ [DllImport(Gdiplus, SetLastError = true, ExactSpelling = true, CharSet = CharSet.Unicode)]
+ [ResourceExposure(ResourceScope.None)]
+ internal static extern int GdipGetPropertyItemSize(HandleRef image, int propid, out int size);
+
+ [DllImport(Gdiplus, SetLastError = true, ExactSpelling = true, CharSet = CharSet.Unicode)]
+ [ResourceExposure(ResourceScope.None)]
+ internal static extern int GdipGetPropertyItem(HandleRef image, int propid, int size, IntPtr buffer);
+
+ [DllImport(Gdiplus, SetLastError = true, ExactSpelling = true, CharSet = CharSet.Unicode)]
+ [ResourceExposure(ResourceScope.Machine)]
+ internal static extern int GdipCreateHBITMAPFromBitmap(HandleRef nativeBitmap, out IntPtr hbitmap, int argbBackground);
+
+ [DllImport(Gdiplus, SetLastError = true, ExactSpelling = true, CharSet = CharSet.Unicode)]
+ [ResourceExposure(ResourceScope.None)]
+ internal static extern int GdipImageSelectActiveFrame(HandleRef image, ref Guid dimensionId, int frameIndex);
+
+ [DllImport(Gdiplus, SetLastError = true, ExactSpelling = true, CharSet = CharSet.Unicode)]
+ [ResourceExposure(ResourceScope.Machine)]
+ internal static extern int GdipCreateBitmapFromFile(string filename, out IntPtr bitmap);
+
+ [DllImport(Gdiplus, SetLastError = true, ExactSpelling = true, CharSet = CharSet.Unicode)]
+ [ResourceExposure(ResourceScope.None)]
+ internal static extern int GdipImageForceValidation(HandleRef image);
+
+ [DllImport(Gdiplus, SetLastError = true, ExactSpelling = true, EntryPoint = "GdipDisposeImage", CharSet = CharSet.Unicode)]
+ [ResourceExposure(ResourceScope.None)]
+ private static extern int IntGdipDisposeImage(HandleRef image);
+
+ internal static int GdipDisposeImage(HandleRef image)
+ {
+ if (!Initialized) return Ok;
+ var result = IntGdipDisposeImage(image);
+ return result;
+ }
+
+ [DllImport(Gdiplus, SetLastError = true, ExactSpelling = true, CharSet = CharSet.Unicode)]
+ [ResourceExposure(ResourceScope.Process)]
+ private static extern int GdiplusStartup(out IntPtr token, ref StartupInput input, out StartupOutput output);
+
+ [DllImport(Gdiplus, SetLastError = true, ExactSpelling = true, CharSet = CharSet.Unicode)]
+ [ResourceExposure(ResourceScope.None)]
+ internal static extern int GdipGetImageRawFormat(HandleRef image, ref Guid format);
+
+ [DllImport(Gdiplus, SetLastError = true, ExactSpelling = true, CharSet = CharSet.Unicode)]
+ [ResourceExposure(ResourceScope.Machine)]
+ internal static extern int GdipCreateBitmapFromStream(IStream stream, out IntPtr bitmap);
+ }
+ }
+}
\ No newline at end of file
diff --git a/HandyControl/Tools/GifImageAnimator.cs b/HandyControl/Tools/GifImageAnimator.cs
new file mode 100644
index 00000000..17e2219c
--- /dev/null
+++ b/HandyControl/Tools/GifImageAnimator.cs
@@ -0,0 +1,271 @@
+using System;
+using System.Collections.Generic;
+using System.Diagnostics.CodeAnalysis;
+using System.Linq;
+using System.Threading;
+using HandyControl.Controls;
+
+namespace HandyControl.Data
+{
+ internal class ImageAnimator
+ {
+ private static List ImageInfoList;
+
+ private static readonly ReaderWriterLock RwImgListLock = new ReaderWriterLock();
+
+ private static Thread AnimationThread;
+
+ private static bool AnyFrameDirty;
+
+ [ThreadStatic]
+ private static int ThreadWriterLockWaitCount;
+
+ public static bool CanAnimate(GifImage image)
+ {
+ if (image == null)
+ {
+ return false;
+ }
+
+ lock (image)
+ {
+ var dimensions = image.FrameDimensionsList;
+
+ if (dimensions.Select(guid => new GifFrameDimension(guid)).Contains(GifFrameDimension.Time))
+ {
+ return image.GetFrameCount(GifFrameDimension.Time) > 1;
+ }
+ }
+
+ return false;
+ }
+
+ public static void Animate(GifImage image, EventHandler onFrameChangedHandler)
+ {
+ if (image == null)
+ {
+ return;
+ }
+
+ GifImageInfo imageInfo;
+
+ // See comment in the class header about locking the image ref.
+ lock (image)
+ {
+ // could we avoid creating an ImageInfo object if FrameCount == 1 ?
+ imageInfo = new GifImageInfo(image);
+ }
+
+ // If the image is already animating, stop animating it
+ StopAnimate(image, onFrameChangedHandler);
+
+ // Acquire a writer lock to modify the image info list. If the thread has a reader lock we need to upgrade
+ // it to a writer lock; acquiring a reader lock in this case would block the thread on itself.
+ // If the thread already has a writer lock its ref count will be incremented w/o placing the request in the
+ // writer queue. See ReaderWriterLock.AcquireWriterLock method in the MSDN.
+
+ var readerLockHeld = RwImgListLock.IsReaderLockHeld;
+ var lockDowngradeCookie = new LockCookie();
+
+ ThreadWriterLockWaitCount++;
+
+ try
+ {
+ if (readerLockHeld)
+ {
+ lockDowngradeCookie = RwImgListLock.UpgradeToWriterLock(Timeout.Infinite);
+ }
+ else
+ {
+ RwImgListLock.AcquireWriterLock(Timeout.Infinite);
+ }
+ }
+ finally
+ {
+ ThreadWriterLockWaitCount--;
+ }
+
+ try
+ {
+ if (imageInfo.Animated)
+ {
+ // Construct the image array
+ //
+ if (ImageInfoList == null)
+ {
+ ImageInfoList = new List();
+ }
+
+ // Add the new image
+ //
+ imageInfo.FrameChangedHandler = onFrameChangedHandler;
+ ImageInfoList.Add(imageInfo);
+
+ // Construct a new timer thread if we haven't already
+ //
+ if (AnimationThread == null)
+ {
+ AnimationThread = new Thread(AnimateImages50Ms)
+ {
+ Name = typeof(ImageAnimator).Name,
+ IsBackground = true
+ };
+ AnimationThread.Start();
+ }
+ }
+ }
+ finally
+ {
+ if (readerLockHeld)
+ {
+ RwImgListLock.DowngradeFromWriterLock(ref lockDowngradeCookie);
+ }
+ else
+ {
+ RwImgListLock.ReleaseWriterLock();
+ }
+ }
+ }
+
+ public static void StopAnimate(GifImage image, EventHandler onFrameChangedHandler)
+ {
+ // Make sure we have a list of images
+ if (image == null || ImageInfoList == null)
+ {
+ return;
+ }
+
+ // Acquire a writer lock to modify the image info list - See comments on Animate() about this locking.
+
+ var readerLockHeld = RwImgListLock.IsReaderLockHeld;
+ var lockDowngradeCookie = new LockCookie();
+
+ ThreadWriterLockWaitCount++;
+
+ try
+ {
+ if (readerLockHeld)
+ {
+ lockDowngradeCookie = RwImgListLock.UpgradeToWriterLock(Timeout.Infinite);
+ }
+ else
+ {
+ RwImgListLock.AcquireWriterLock(Timeout.Infinite);
+ }
+ }
+ finally
+ {
+ ThreadWriterLockWaitCount--;
+ }
+
+ try
+ {
+ // Find the corresponding reference and remove it
+ for (int i = 0; i < ImageInfoList.Count; i++)
+ {
+ var imageInfo = ImageInfoList[i];
+
+ if (Equals(image, imageInfo.Image))
+ {
+ if ((onFrameChangedHandler == imageInfo.FrameChangedHandler) || (onFrameChangedHandler != null && onFrameChangedHandler.Equals(imageInfo.FrameChangedHandler)))
+ {
+ ImageInfoList.Remove(imageInfo);
+ }
+ break;
+ }
+ }
+ }
+ finally
+ {
+ if (readerLockHeld)
+ {
+ RwImgListLock.DowngradeFromWriterLock(ref lockDowngradeCookie);
+ }
+ else
+ {
+ RwImgListLock.ReleaseWriterLock();
+ }
+ }
+ }
+
+ public static void UpdateFrames()
+ {
+ if (!AnyFrameDirty || ImageInfoList == null)
+ {
+ return;
+ }
+ if (ThreadWriterLockWaitCount > 0)
+ {
+ // Cannot acquire reader lock at this time, frames update will be missed.
+ return;
+ }
+
+ RwImgListLock.AcquireReaderLock(Timeout.Infinite);
+
+ try
+ {
+ foreach (var imageInfo in ImageInfoList)
+ {
+ // See comment in the class header about locking the image ref.
+ lock (imageInfo.Image)
+ {
+ imageInfo.UpdateFrame();
+ }
+ }
+ AnyFrameDirty = false;
+ }
+ finally
+ {
+ RwImgListLock.ReleaseReaderLock();
+ }
+ }
+
+ [SuppressMessage("Microsoft.Performance", "CA1804:RemoveUnusedLocals")]
+ private static void AnimateImages50Ms()
+ {
+ while (true)
+ {
+ // Acquire reader-lock to access imageInfoList, elemens in the list can be modified w/o needing a writer-lock.
+ // Observe that we don't need to check if the thread is waiting or a writer lock here since the thread this
+ // method runs in never acquires a writer lock.
+ RwImgListLock.AcquireReaderLock(Timeout.Infinite);
+ try
+ {
+ foreach (var imageInfo in ImageInfoList)
+ {
+ // Frame delay is measured in 1/100ths of a second. This thread
+ // sleeps for 50 ms = 5/100ths of a second between frame updates,
+ // so we increase the frame delay count 5/100ths of a second
+ // at a time.
+ //
+ imageInfo.FrameTimer += 5;
+ if (imageInfo.FrameTimer >= imageInfo.FrameDelay(imageInfo.Frame))
+ {
+ imageInfo.FrameTimer = 0;
+
+ if (imageInfo.Frame + 1 < imageInfo.FrameCount)
+ {
+ imageInfo.Frame++;
+ }
+ else
+ {
+ imageInfo.Frame = 0;
+ }
+
+ if (imageInfo.FrameDirty)
+ {
+ AnyFrameDirty = true;
+ }
+ }
+ }
+ }
+ finally
+ {
+ RwImgListLock.ReleaseReaderLock();
+ }
+
+ Thread.Sleep(50);
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/HandyControlDemo/Data/MessageToken.cs b/HandyControlDemo/Data/MessageToken.cs
index cec6fd20..8f205798 100644
--- a/HandyControlDemo/Data/MessageToken.cs
+++ b/HandyControlDemo/Data/MessageToken.cs
@@ -54,6 +54,8 @@
public static readonly string TagDemoCtl = nameof(TagDemoCtl);
+ public static readonly string GifImageDemoCtl = nameof(GifImageDemoCtl);
+
public static readonly string ButtonDemoCtl = nameof(ButtonDemoCtl);
public static readonly string ToggleButtonDemoCtl = nameof(ToggleButtonDemoCtl);
diff --git a/HandyControlDemo/Data/MessageToken.tt b/HandyControlDemo/Data/MessageToken.tt
index ad670bea..9bbcd97a 100644
--- a/HandyControlDemo/Data/MessageToken.tt
+++ b/HandyControlDemo/Data/MessageToken.tt
@@ -29,7 +29,8 @@ var controlList = new List
"DateTimePickerDemoCtl",
"CalendarWithClockDemoCtl",
"SearchBarDemoCtl",
- "TagDemoCtl"
+ "TagDemoCtl",
+ "GifImageDemoCtl"
};
var styleList = new List
{
diff --git a/HandyControlDemo/HandyControlDemo.csproj b/HandyControlDemo/HandyControlDemo.csproj
index e2b3e522..275c70b5 100644
--- a/HandyControlDemo/HandyControlDemo.csproj
+++ b/HandyControlDemo/HandyControlDemo.csproj
@@ -129,6 +129,9 @@
DateTimePickerDemoCtl.xaml
+
+ GifImageDemoCtl.xaml
+
GrowlDemoCtl.xaml
@@ -383,6 +386,10 @@
Designer
MSBuild:Compile
+
+ Designer
+ MSBuild:Compile
+
Designer
MSBuild:Compile
@@ -599,6 +606,8 @@
TextTemplatingFileGenerator
MessageToken.cs
+
+
diff --git a/HandyControlDemo/Properties/Langs/Lang.Designer.cs b/HandyControlDemo/Properties/Langs/Lang.Designer.cs
index 262255af..4c27efc1 100644
--- a/HandyControlDemo/Properties/Langs/Lang.Designer.cs
+++ b/HandyControlDemo/Properties/Langs/Lang.Designer.cs
@@ -47,7 +47,7 @@ namespace HandyControlDemo.Properties.Langs {
}
///
- /// 重写当前线程的 CurrentUICulture 属性
+ /// 使用此强类型资源类,为所有资源查找
/// 重写当前线程的 CurrentUICulture 属性。
///
[global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
@@ -384,6 +384,15 @@ namespace HandyControlDemo.Properties.Langs {
}
}
+ ///
+ /// 查找类似 Gif图片 的本地化字符串。
+ ///
+ public static string GifImage {
+ get {
+ return ResourceManager.GetString("GifImage", resourceCulture);
+ }
+ }
+
///
/// 查找类似 分组框 的本地化字符串。
///
diff --git a/HandyControlDemo/Properties/Langs/Lang.en.resx b/HandyControlDemo/Properties/Langs/Lang.en.resx
index 4b1f050b..8e97e24f 100644
--- a/HandyControlDemo/Properties/Langs/Lang.en.resx
+++ b/HandyControlDemo/Properties/Langs/Lang.en.resx
@@ -429,4 +429,7 @@
Tag
+
+ GifImage
+
\ No newline at end of file
diff --git a/HandyControlDemo/Properties/Langs/Lang.resx b/HandyControlDemo/Properties/Langs/Lang.resx
index ddc9a2b9..a81739de 100644
--- a/HandyControlDemo/Properties/Langs/Lang.resx
+++ b/HandyControlDemo/Properties/Langs/Lang.resx
@@ -409,4 +409,7 @@
标签
+
+ Gif图片
+
\ No newline at end of file
diff --git a/HandyControlDemo/Resources/Img/LeftMainContent/Animation_16x.png b/HandyControlDemo/Resources/Img/LeftMainContent/Animation_16x.png
new file mode 100644
index 00000000..e4b21749
Binary files /dev/null and b/HandyControlDemo/Resources/Img/LeftMainContent/Animation_16x.png differ
diff --git a/HandyControlDemo/Resources/Img/car_chase.gif b/HandyControlDemo/Resources/Img/car_chase.gif
new file mode 100644
index 00000000..ff9290f2
Binary files /dev/null and b/HandyControlDemo/Resources/Img/car_chase.gif differ
diff --git a/HandyControlDemo/UserControl/Controls/GifImageDemoCtl.xaml b/HandyControlDemo/UserControl/Controls/GifImageDemoCtl.xaml
new file mode 100644
index 00000000..7efbaaae
--- /dev/null
+++ b/HandyControlDemo/UserControl/Controls/GifImageDemoCtl.xaml
@@ -0,0 +1,13 @@
+
+
+
+
+
+
+
+
+
diff --git a/HandyControlDemo/UserControl/Controls/GifImageDemoCtl.xaml.cs b/HandyControlDemo/UserControl/Controls/GifImageDemoCtl.xaml.cs
new file mode 100644
index 00000000..315e0296
--- /dev/null
+++ b/HandyControlDemo/UserControl/Controls/GifImageDemoCtl.xaml.cs
@@ -0,0 +1,17 @@
+using System;
+
+namespace HandyControlDemo.UserControl
+{
+ public partial class GifImageDemoCtl : IDisposable
+ {
+ public GifImageDemoCtl()
+ {
+ InitializeComponent();
+ }
+
+ public void Dispose()
+ {
+ GifImageMain.Dispose();
+ }
+ }
+}
diff --git a/HandyControlDemo/UserControl/Main/LeftMainContent.xaml b/HandyControlDemo/UserControl/Main/LeftMainContent.xaml
index 75a10011..5e53f45a 100644
--- a/HandyControlDemo/UserControl/Main/LeftMainContent.xaml
+++ b/HandyControlDemo/UserControl/Main/LeftMainContent.xaml
@@ -199,6 +199,11 @@
+
+
+
+
+
diff --git a/HandyControlDemo/ViewModel/MainViewModel.cs b/HandyControlDemo/ViewModel/MainViewModel.cs
index 2b50a40f..f5fe132f 100644
--- a/HandyControlDemo/ViewModel/MainViewModel.cs
+++ b/HandyControlDemo/ViewModel/MainViewModel.cs
@@ -38,7 +38,14 @@ namespace HandyControlDemo.ViewModel
public MainViewModel(DataService dataService)
{
- Messenger.Default.Register