// COPYRIGHT (C) Tom. ALL RIGHTS RESERVED. // THE AntdUI PROJECT IS AN WINFORM LIBRARY LICENSED UNDER THE Apache-2.0 License. // LICENSED UNDER THE Apache License, VERSION 2.0 (THE "License") // YOU MAY NOT USE THIS FILE EXCEPT IN COMPLIANCE WITH THE License. // YOU MAY OBTAIN A COPY OF THE LICENSE AT // // http://www.apache.org/licenses/LICENSE-2.0 // // UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING, SOFTWARE // DISTRIBUTED UNDER THE LICENSE IS DISTRIBUTED ON AN "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, EITHER EXPRESS OR IMPLIED. // SEE THE LICENSE FOR THE SPECIFIC LANGUAGE GOVERNING PERMISSIONS AND // LIMITATIONS UNDER THE License. // GITEE: https://gitee.com/antdui/AntdUI // GITHUB: https://github.com/AntdUI/AntdUI // CSDN: https://blog.csdn.net/v_132 // QQ: 17379620 using System; using System.Collections.Generic; using System.ComponentModel; using System.Drawing; using System.Threading; using System.Windows.Forms; namespace AntdUI { /// /// Carousel 走马灯 /// /// 旋转木马,一组轮播的区域。 [Description("Carousel 走马灯")] [ToolboxItem(true)] [DefaultProperty("Image")] [DefaultEvent("SelectIndexChanged")] [Designer(typeof(IControlDesigner))] public class Carousel : IControl { #region 属性 /// /// 手势滑动 /// [Description("手势滑动"), Category("行为"), DefaultValue(true)] public bool Touch { get; set; } = true; /// /// 滑动到外面 /// [Description("滑动到外面"), Category("行为"), DefaultValue(false)] public bool TouchOut { get; set; } = false; bool autoplay = false; /// /// 自动切换 /// [Description("自动切换"), Category("行为"), DefaultValue(false)] public bool Autoplay { get => autoplay; set { if (autoplay == value) return; autoplay = value; if (value) new Thread(LongTime) { IsBackground = true }.Start(); } } /// /// 自动切换延迟(s) /// [Description("自动切换延迟(s)"), Category("行为"), DefaultValue(4)] public int Autodelay { get; set; } = 4; /// /// 面板指示点大小 /// [Description("面板指示点大小"), Category("面板"), DefaultValue(typeof(Size), "28, 4")] public Size DotSize { get; set; } = new Size(28, 4); /// /// 面板指示点边距 /// [Description("面板指示点边距"), Category("面板"), DefaultValue(12)] public int DotMargin { get; set; } = 12; TAlignMini dotPosition = TAlignMini.None; bool dotPV = false; /// /// 面板指示点位置 /// [Description("面板指示点位置"), Category("面板"), DefaultValue(TAlignMini.None)] public TAlignMini DotPosition { get => dotPosition; set { if (dotPosition == value) return; dotPosition = value; dotPV = (value == TAlignMini.Left || value == TAlignMini.Right); Invalidate(); } } int radius = 0; /// /// 圆角 /// [Description("圆角"), Category("外观"), DefaultValue(0)] public int Radius { get => radius; set { if (radius == value) return; radius = value; Invalidate(); } } bool round = false; /// /// 圆角样式 /// [Description("圆角样式"), Category("外观"), DefaultValue(false)] public bool Round { get => round; set { if (round == value) return; round = value; Invalidate(); } } CarouselItemCollection? items; /// /// 图片集合 /// [DesignerSerializationVisibility(DesignerSerializationVisibility.Content)] [Description("图片集合"), Category("数据")] public CarouselItemCollection Image { get { items ??= new CarouselItemCollection(this); return items; } set => items = value.BindData(this); } TFit imageFit = TFit.Cover; /// /// 图片布局 /// [Description("图片布局"), Category("外观"), DefaultValue(TFit.Cover)] public TFit ImageFit { get => imageFit; set { if (imageFit == value) return; imageFit = value; Invalidate(); } } private int selectIndex = 0; /// /// 选择序号 /// [Description("选择序号"), Category("数据"), DefaultValue(0)] public int SelectIndex { get => selectIndex; set { if (autoplay) now = DateTime.Now.AddSeconds(Autodelay); if (selectIndex == value) return; if (items != null) { if (items.ListExceed(value)) { selectIndex = 0; return; } SetSelectIndex(value); } else selectIndex = 0; } } /// /// SelectIndex 属性值更改时发生 /// [Description("SelectIndex 属性值更改时发生"), Category("行为")] public event IntEventHandler? SelectIndexChanged = null; #region SetSelectIndex void SetSelectIndex(int value, bool auto = false) { if (dotPV) SetSelectIndexVertical(value, auto); else SetSelectIndexHorizontal(value, auto); } void SetSelectIndexVertical(int value, bool auto = false) { int height = ClientRectangle.Height - Padding.Vertical; if (items != null && IsHandleCreated && Config.Animation) { ThreadChange?.Dispose(); AnimationChangeAuto = false; float end = value * height; int len = items.Count; bool left = value * height > AnimationChangeValue; AnimationChangeMax = len * height; AnimationChangeMaxWH = AnimationChangeMax - height; AnimationChange = true; var old = selectIndex; selectIndex = value; SelectIndexChanged?.Invoke(this, new IntEventArgs(value)); var speed = Math.Abs(end - AnimationChangeValue) / 50F; if (speed < 8) speed = 8F; if (left) { float modera = end - height * 0.05F; ThreadChange = new ITask(this, () => { AnimationChangeValue = AnimationChangeValue.Calculate(Speed(speed, modera)); if (AnimationChangeValue > end) { AnimationChangeValue = end; return false; } Invalidate(); return true; }, 10, () => { AnimationChange = false; Invalidate(); }); } else { if (auto && value == 0 && len > 2 && old == len - 1) { AnimationChangeAuto = true; end = len * height; float modera = end - height * 0.05F; ThreadChange = new ITask(this, () => { AnimationChangeValue = AnimationChangeValue.Calculate(Speed(speed, modera)); if (AnimationChangeValue > end) { AnimationChangeValue = end; return false; } Invalidate(); return true; }, 10, () => { AnimationChange = false; AnimationChangeValue = 0; Invalidate(); }); } else { float modera = end + height * 0.05F; ThreadChange = new ITask(this, () => { AnimationChangeValue -= Speed2(speed, modera); if (AnimationChangeValue <= end) { AnimationChangeValue = end; return false; } Invalidate(); return true; }, 10, () => { AnimationChange = false; Invalidate(); }); } } } else { selectIndex = value; AnimationChangeValue = value * height; Invalidate(); } } void SetSelectIndexHorizontal(int value, bool auto = false) { int width = ClientRectangle.Width - Padding.Horizontal; if (items != null && IsHandleCreated && Config.Animation) { ThreadChange?.Dispose(); AnimationChangeAuto = false; float end = value * width; int len = items.Count; bool left = value * width > AnimationChangeValue; AnimationChangeMax = len * width; AnimationChangeMaxWH = AnimationChangeMax - width; AnimationChange = true; var old = selectIndex; selectIndex = value; SelectIndexChanged?.Invoke(this, new IntEventArgs(value)); var speed = Math.Abs(end - AnimationChangeValue) / 50F; if (speed < 8) speed = 8F; if (left) { float modera = end - width * 0.05F; ThreadChange = new ITask(this, () => { AnimationChangeValue = AnimationChangeValue.Calculate(Speed(speed, modera)); if (AnimationChangeValue > end) { AnimationChangeValue = end; return false; } Invalidate(); return true; }, 10, () => { AnimationChange = false; Invalidate(); }); } else { if (auto && value == 0 && len > 2 && old == len - 1) { AnimationChangeAuto = true; end = len * width; float modera = end - width * 0.05F; ThreadChange = new ITask(this, () => { AnimationChangeValue = AnimationChangeValue.Calculate(Speed(speed, modera)); if (AnimationChangeValue > end) { AnimationChangeValue = end; return false; } Invalidate(); return true; }, 10, () => { AnimationChange = false; AnimationChangeValue = 0; Invalidate(); }); } else { float modera = end + width * 0.05F; ThreadChange = new ITask(this, () => { AnimationChangeValue -= Speed2(speed, modera); if (AnimationChangeValue <= end) { AnimationChangeValue = end; return false; } Invalidate(); return true; }, 10, () => { AnimationChange = false; Invalidate(); }); } } } else { selectIndex = value; AnimationChangeValue = value * width; Invalidate(); } } #endregion #region 动画 DateTime now = DateTime.Now; internal float Speed(float speed, float modera) { if (modera < AnimationChangeValue) return 0.8F; return speed; } internal float Speed2(float speed, float modera) { if (modera > AnimationChangeValue) return 0.8F; return speed; } protected override void OnHandleCreated(EventArgs e) { if (dotPV) { int height = ClientRectangle.Height; AnimationChangeValue = selectIndex * height; } else { int width = ClientRectangle.Width; AnimationChangeValue = selectIndex * width; } base.OnHandleCreated(e); } bool AnimationChangeAuto = false; int AnimationChangeMax = 0; int AnimationChangeMaxWH = 0; float AnimationChangeValue = 0F; bool AnimationChange = false; protected override void Dispose(bool disposing) { ThreadChange?.Dispose(); base.Dispose(disposing); } ITask? ThreadChange = null; void LongTime() { while (autoplay) { if (Autodelay > 0) Thread.Sleep(Autodelay * 1000); else Thread.Sleep(1000); if (!down && !ExtraMouseHover && DateTime.Now > now) { if (items == null) continue; if (selectIndex >= items.Count - 1) SetSelectIndex(0, true); else SetSelectIndex(selectIndex + 1, true); } } } #endregion protected override void OnSizeChanged(EventArgs e) { ChangeImg(); base.OnSizeChanged(e); } CarouselDotItem[] dot_list = new CarouselDotItem[0]; internal void ChangeImg() { if (DotPosition == TAlignMini.None || items == null || items.Count == 0) return; var _rect = ClientRectangle; if (_rect.Width == 0 || _rect.Height == 0) return; bmp?.Dispose(); bmp = null; var rect = _rect.PaddingRect(Padding); int len = items.Count; var list = new List(len); if (DotPosition == TAlignMini.Top || DotPosition == TAlignMini.Bottom) { int dot_size = DotSize.Width * len, y = DotPosition == TAlignMini.Bottom ? rect.Y + rect.Height - (DotMargin + DotSize.Height) : rect.Y + DotMargin, y2 = DotPosition == TAlignMini.Bottom ? rect.Y + rect.Height - (DotMargin + DotSize.Height) - DotMargin / 2 : rect.Y + DotMargin / 2; int temp_x = rect.X + (rect.Width - dot_size) / 2; for (int i = 0; i < len; i++) { list.Add(new CarouselDotItem { i = i, rect_fill = new RectangleF(temp_x, y2, DotSize.Width, DotMargin), rect_action = new RectangleF(temp_x + 2, y, DotSize.Width - 4, DotSize.Height), rect = new RectangleF(temp_x + 4, y, DotSize.Width - 8, DotSize.Height) }); temp_x += DotSize.Width; } } else { int dot_size = DotSize.Width * len, x = DotPosition == TAlignMini.Right ? rect.X + rect.Width - (DotMargin + DotSize.Height) : rect.X + DotMargin, x2 = DotPosition == TAlignMini.Right ? rect.X + rect.Width - (DotMargin + DotSize.Height) - DotMargin / 2 : rect.X + DotMargin / 2; int temp_y = rect.Y + (rect.Height - dot_size) / 2; for (int i = 0; i < len; i++) { list.Add(new CarouselDotItem { i = i, rect_fill = new RectangleF(x2, temp_y, DotMargin, DotSize.Width), rect_action = new RectangleF(x, temp_y + 2, DotSize.Height, DotSize.Width - 4), rect = new RectangleF(x, temp_y + 4, DotSize.Height, DotSize.Width - 8) }); temp_y += DotSize.Width; } } dot_list = list.ToArray(); } #endregion #region 渲染 string? bmpcode = null; Bitmap? bmp = null; protected override void OnPaint(PaintEventArgs e) { if (items == null || items.Count == 0) return; var _rect = ClientRectangle; if (_rect.Width == 0 || _rect.Height == 0) return; var rect = _rect.PaddingRect(Padding); var g = e.Graphics.High(); int len = items.Count; var image = items[selectIndex].Img; float _radius = radius * Config.Dpi; if (image != null) { if (AnimationChange) { if (dotPV) { var select_range = SelectRangeVertical(len, rect); if (bmp == null || bmpcode != select_range.i) { bmpcode = select_range.i; bmp = PaintBmpVertical(items, select_range, rect, _radius); } g.DrawImage(bmp, rect.X, (int)(rect.Y - AnimationChangeValue), bmp.Width, bmp.Height); } else { var select_range = SelectRangeHorizontal(len, rect); if (bmp == null || bmpcode != select_range.i) { bmpcode = select_range.i; bmp = PaintBmpHorizontal(items, select_range, rect, _radius); } g.DrawImage(bmp, (int)(rect.X - AnimationChangeValue), rect.Y, bmp.Width, bmp.Height); } } else g.PaintImg(rect, image, imageFit, _radius, round); } if (dot_list.Length > 0) { using (var brush = new SolidBrush(Style.Db.BgBase)) using (var brush2 = new SolidBrush(Color.FromArgb(77, brush.Color))) { if (round || radius > 0) { foreach (var it in dot_list) { if (it.i == selectIndex) { using (var path = it.rect_action.RoundPath(DotSize.Height)) g.FillPath(brush, path); } else { using (var path = it.rect.RoundPath(DotSize.Height)) g.FillPath(brush2, path); } } } else { foreach (var it in dot_list) { if (it.i == selectIndex) g.FillRectangle(brush, it.rect_action); else g.FillRectangle(brush2, it.rect); } } } } this.PaintBadge(g); base.OnPaint(e); } Bitmap PaintBmpVertical(CarouselItemCollection items, CarouselRectPanel select_range, Rectangle rect, float radius) { bmpcode = select_range.i; Bitmap bmp; if (AnimationChangeAuto) { bmp = new Bitmap(rect.Width, AnimationChangeMax + rect.Height); using (var g2 = Graphics.FromImage(bmp).High()) { PaintBmp(items, select_range, g2, radius); var bmo = items[0].Img; if (bmo != null) g2.PaintImg(new RectangleF(AnimationChangeMax, 0, rect.Width, rect.Height), bmo, imageFit, radius, round); } } else { bmp = new Bitmap(rect.Width, AnimationChangeMax); using (var g2 = Graphics.FromImage(bmp).High()) { PaintBmp(items, select_range, g2, radius); } } return bmp; } Bitmap PaintBmpHorizontal(CarouselItemCollection items, CarouselRectPanel select_range, Rectangle rect, float radius) { bmpcode = select_range.i; Bitmap bmp; if (AnimationChangeAuto) { bmp = new Bitmap(AnimationChangeMax + rect.Width, rect.Height); using (var g2 = Graphics.FromImage(bmp).High()) { PaintBmp(items, select_range, g2, radius); var bmo = items[0].Img; if (bmo != null) g2.PaintImg(new RectangleF(AnimationChangeMax, 0, rect.Width, rect.Height), bmo, imageFit, radius, round); } } else { bmp = new Bitmap(AnimationChangeMax, rect.Height); using (var g2 = Graphics.FromImage(bmp).High()) { PaintBmp(items, select_range, g2, radius); } } return bmp; } void PaintBmp(CarouselItemCollection items, CarouselRectPanel select_range, Graphics g2, float radius) { foreach (var it in select_range.list) { var bmo = items[it.i].Img; if (bmo != null) g2.PaintImg(it.rect, bmo, imageFit, radius, round); } } #region SelectRange 选择脏渲染序号 CarouselRectPanel SelectRangeVertical(int len, Rectangle rect) { var r = new CarouselRectPanel { list = new List(len) }; var indes = new List(len); int temp = 0; for (int i = 0; i < len; i++) { var rect1 = new RectangleF(0, temp, rect.Width, rect.Height); if (rect1.Contains(0, AnimationChangeValue)) { indes.Add(i); r.list.Add(new CarouselRect { i = i, rect = rect1, }); } if (i < len - 1) { var rect2 = new RectangleF(0, temp + rect.Height, rect.Width, rect.Height); if (rect2.Contains(0, AnimationChangeValue + rect.Height)) { indes.Add(i + 1); r.list.Add(new CarouselRect { i = i + 1, rect = rect2, }); } } temp += rect.Height; if (temp > AnimationChangeValue + rect.Height) break; } if (r.list.Count == 0 && AnimationChangeValue < 0) { indes.Add(0); r.list.Add(new CarouselRect { i = 0, rect = new RectangleF(0, 0, rect.Width, rect.Height), }); } r.i = string.Join("", indes); return r; } CarouselRectPanel SelectRangeHorizontal(int len, Rectangle rect) { var r = new CarouselRectPanel { list = new List(len) }; var indes = new List(len); int temp = 0; for (int i = 0; i < len; i++) { var rect1 = new RectangleF(temp, 0, rect.Width, rect.Height); if (rect1.Contains(AnimationChangeValue, 0)) { indes.Add(i); r.list.Add(new CarouselRect { i = i, rect = rect1, }); } if (i < len - 1) { var rect2 = new RectangleF(temp + rect.Width, 0, rect.Width, rect.Height); if (rect2.Contains(AnimationChangeValue + rect.Width, 0)) { indes.Add(i + 1); r.list.Add(new CarouselRect { i = i + 1, rect = rect2, }); } } temp += rect.Width; if (temp > AnimationChangeValue + rect.Width) break; } if (r.list.Count == 0 && AnimationChangeValue < 0) { indes.Add(0); r.list.Add(new CarouselRect { i = 0, rect = new RectangleF(0, 0, rect.Width, rect.Height), }); } r.i = string.Join("", indes); return r; } #endregion #region SelectRangeOne CarouselRect? SelectRangeOneVertical(int len, Rectangle rect) { var r = new List(len); int temp = 0; for (int i = 0; i < len; i++) { int cen = (temp + rect.Height / 2); if (AnimationChangeValue > cen - rect.Height && AnimationChangeValue < cen + rect.Height) { var prog = AnimationChangeValue / cen; r.Add(new CarouselRect { p = prog, i = i, rect = new RectangleF(0, temp, rect.Width, rect.Height), }); } temp += rect.Height; if (temp > AnimationChangeValue + rect.Height) break; } if (r.Count > 0) { r.Sort((x, y) => x.p.CompareTo(y.p)); return r[0]; } return null; } CarouselRect? SelectRangeOneHorizontal(int len, Rectangle rect) { var r = new List(len); int temp = 0; for (int i = 0; i < len; i++) { int cen = (temp + rect.Width / 2); if (AnimationChangeValue > cen - rect.Width && AnimationChangeValue < cen + rect.Width) { var prog = AnimationChangeValue / cen; r.Add(new CarouselRect { p = prog, i = i, rect = new RectangleF(temp, 0, rect.Width, rect.Height), }); } temp += rect.Width; if (temp > AnimationChangeValue + rect.Width) break; } if (r.Count > 0) { r.Sort((x, y) => x.p.CompareTo(y.p)); return r[0]; } return null; } #endregion #endregion #region 鼠标 bool _mouseHover = false; bool ExtraMouseHover { get => _mouseHover; set { if (_mouseHover == value) return; _mouseHover = value; var enabled = Enabled; SetCursor(value && enabled); if (!value && autoplay) now = DateTime.Now.AddSeconds(Autodelay); } } protected override void OnMouseEnter(EventArgs e) { base.OnMouseEnter(e); ExtraMouseHover = true; } protected override void OnMouseLeave(EventArgs e) { base.OnMouseLeave(e); ExtraMouseHover = false; } protected override void OnLeave(EventArgs e) { base.OnLeave(e); ExtraMouseHover = false; } bool down = false; float tvaluexy = 0; protected override void OnMouseDown(MouseEventArgs e) { if (e.Button == MouseButtons.Left && items != null) { if (dot_list.Length > 0) { for (int i = 0; i < dot_list.Length; i++) { if (dot_list[i].rect_fill.Contains(e.Location)) { SetSelectIndex(dot_list[i].i); return; } } } if (Touch) { int len = items.Count; if (dotPV) { tvaluexy = AnimationChangeValue + e.Y; int height = ClientRectangle.Height; AnimationChangeMax = len * height; AnimationChangeMaxWH = AnimationChangeMax - height; } else { tvaluexy = AnimationChangeValue + e.X; int width = ClientRectangle.Width; AnimationChangeMax = len * width; AnimationChangeMaxWH = AnimationChangeMax - width; } down = true; } } base.OnMouseDown(e); } protected override void OnMouseMove(MouseEventArgs e) { if (down) { var val = tvaluexy - (dotPV ? e.Y : e.X); if (!TouchOut) { if (val < 0) val = 0; else if (val > AnimationChangeMaxWH) val = AnimationChangeMaxWH; } AnimationChange = true; AnimationChangeValue = val; Invalidate(); } base.OnMouseMove(e); } protected override void OnMouseUp(MouseEventArgs e) { if (down) { if (items == null) { down = false; return; } int len = items.Count; var rect = ClientRectangle; AnimationChange = false; if (dotPV) { var val = tvaluexy - e.Y; var select_range = SelectRangeOneVertical(len, rect); if (select_range != null) SetSelectIndexVertical(select_range.i); else if (val > AnimationChangeMax - rect.Height) SetSelectIndexVertical(items.Count - 1); else if (val < 0) SetSelectIndexVertical(0); } else { var val = tvaluexy - e.X; var select_range = SelectRangeOneHorizontal(len, rect); if (select_range != null) SetSelectIndexHorizontal(select_range.i); else if (val > AnimationChangeMax - rect.Width) SetSelectIndexHorizontal(items.Count - 1); else if (val < 0) SetSelectIndexHorizontal(0); } Invalidate(); } down = false; base.OnMouseUp(e); } protected override void OnMouseWheel(MouseEventArgs e) { if (e.Delta > 0) { if (selectIndex <= 0) return; SetSelectIndex(selectIndex - 1); } else { if (items == null || selectIndex >= items.Count - 1) return; SetSelectIndex(selectIndex + 1); } base.OnMouseWheel(e); } #endregion } public class CarouselItemCollection : iCollection { public CarouselItemCollection(Carousel it) { BindData(it); } internal CarouselItemCollection BindData(Carousel it) { action = render => { it.ChangeImg(); it.Invalidate(); }; return this; } } public class CarouselItem : NotifyProperty { Image? img; /// /// 图片 /// [Description("图片"), Category("外观"), DefaultValue(null)] public Image? Img { get => img; set { if (img == value) return; img = value; OnPropertyChanged("Img"); } } /// /// 用户定义数据 /// [Description("用户定义数据"), Category("数据"), DefaultValue(null)] public object? Tag { get; set; } } internal class CarouselRectPanel { public string i { get; set; } public List list { get; set; } } internal class CarouselRect { public int i { get; set; } public float p { get; set; } public RectangleF rect { get; set; } } internal class CarouselDotItem : CarouselRect { public RectangleF rect_action { get; set; } public RectangleF rect_fill { get; set; } } }