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(this, MessageToken.LoadShowContent, obj => SubContent = obj); + Messenger.Default.Register(this, MessageToken.LoadShowContent, obj => + { + if (SubContent is IDisposable disposable) + { + disposable.Dispose(); + } + SubContent = obj; + }); Messenger.Default.Register(this, MessageToken.ClearLeftSelected, obj => { if (_listBoxItemCurrent == null) return;