數年前因為某個原因,開始編寫的我的開發助手,一路艱辛,一路堅持,目前仍不斷完善之中,項目地址:https://gitee.com/sqlorm/DevelopAssistant 歡迎大家點贊和支持。 今天想和大家分享一下其中的時間線控制項,這是一個通過GDI繪製和對原有事件重寫來實現的用戶自定義控制項, ...
數年前因為某個原因,開始編寫的我的開發助手,一路艱辛,一路堅持,目前仍不斷完善之中,項目地址:https://gitee.com/sqlorm/DevelopAssistant 歡迎大家點贊和支持。
今天想和大家分享一下其中的時間線控制項,這是一個通過GDI繪製和對原有事件重寫來實現的用戶自定義控制項,界面還算美觀,操作也很簡捷,喜歡的同學不妨收下。
控制項是這樣子的:
沒有內容時界面 管理界面帶編輯功能界面
下麵就來介紹一下關於這個控制項的開發:
第一步、我們創建一個類繼承 UserControl
控制項主要對 OnPaint ,OnMouseClick ,OnMouseMove,OnSizeChanged,OnMouseWheel 方法進行重寫,其中 OnPaint 方法用戶界面元素的繪製,併在該方法裡面計算控制項元素的繪製區域,以便在OnMouseClick 重寫方法里實現元素的點擊事件,OnMouseClick 方法就是實現控制項元素的點擊事件,OnMouseMove 主要實體一些滑鼠特效,例如滑動滑鼠改變背景色,改變控制項預設游標形狀等,OnSizeChanged 方法主要實現當改變控制項大小時控制項控制項滾動條相關屬性的計算和觸發控制項元素重繪及事件區域範圍Rectangle的計算,通過判斷滑鼠點擊的位置屬於哪個元素的區域範圍來確定觸發哪個元素的相關事件,OnMouseWheel 滑鼠滾輪滾動時發生。其實winform下自定義控制項特別是通過GDI繪製來實現的一類基本上都是實現上述幾個事件方法來實現,可以用一張圖來概括:
第二步、定義控制項的內部元素
控制項主要涉及到 月份對象元素: MonthItem ,日期對象元素:DateItem ,時間對象元素:DateTimeItem 他們都繼承自公共對象元素:TimelineItem 他們都有公同的屬性Id (與資料庫表主鍵做關聯),Name 名稱,Tag 其它數據相關綁定的標簽。其次MonthItem和 DateItem 都有 Bound 屬於,用戶保存該元素在控制項中的繪製區域。下麵貼出這三個元素實體類的代碼:
MonthItem:
[Serializable] public class MonthItem : TimelineItem { public DateTime Date { get; set; } public string DateLabel { get; set; } public List<DateItem> List { get; set; } internal Size Size { get; set; } internal Rectangle Bound { get; set; } }
DateItem:
[Serializable] public class DateItem : TimelineItem { public DateTime Date { get; set; } public List<DateTimeItem> List { get; set; } internal Size Size { get; set; } internal Rectangle Bound { get; set; } internal Rectangle AddRect { get; set; } internal Rectangle ClickRect { get; set; } public bool Selected { get; set; } private string _tag; public override string Tag { get { if (string.IsNullOrEmpty(_tag)) { _tag = Date.ToString("yyyyMMdd"); } return _tag; } } }
DateTimeItem :
[Serializable] public class DateTimeItem : TimelineItem { public Image Icon { get; set; } public string Title { get; set; } public string Summary { get; set; } public string Description { get; set; } public string ToolTip { get; set; } public string PersonName { get; set; } public DateTime DateTime { get; set; } public ImportantLevel Level { get; set; } public Timeliness Timeliness { get; set; } public string ResponsiblePerson { get; set; } internal Rectangle EditRect { get; set; } internal Rectangle DeleteRect { get; set; } /// <summary> /// 0 :預設 1:修改 2:刪除 /// </summary> internal int ButtonState { get; set; } public Rectangle ClickRect { get; set; } public bool Selected { get; set; } private string _tag; public override string Tag { get { if (string.IsNullOrEmpty(_tag)) { _tag = DateTime.ToString("yyyyMMddHHmmss"); } return _tag; } } }
第三步、繪製控制項內部的元素
繪製控制項內部的元素主要分為繪製 TimelineItem 一類(包括 MonthItem ,DateItem 和 DateTimeItem)和 控制項的滾動條,一般來講winform 控制項自帶的滾動條都由系統繪製,往往和操作系統息息相關,這裡我們的時間線控制項要適合開發助手相關的主題,所以我們採用在內部自己繪製滾動條,通過主要對OnMouseMove,OnMouseWheel兩相事件方法進行重寫來實現滾動條的控制項。
這裡對TimelineItem 一類的元素繪製主要貼出以下代碼:
/// <summary> /// 計算 TimelineItem 繪製區域 通過對 MonthItem 子元素遞規迴圈計算 整個 MonthItem 元素的繪製區域 /// </summary> /// <param name="g"></param> /// <param name="index"></param> /// <param name="item"></param> /// <returns></returns> private Rectangle MeasureItemBound(Graphics g, int index, MonthItem item) { int itemHeight = 46; if (item.List != null) { foreach (DateItem subItem in item.List) { if (subItem.List != null) { foreach(DateTimeItem subsubItem in subItem.List) { itemHeight = itemHeight + 32; if (!string.IsNullOrEmpty(subsubItem.Summary)) { itemHeight = itemHeight + 26; } } } itemHeight = itemHeight + 32; } } Rectangle rect = new Rectangle(drawPositionOffset.X + padding.Left, drawPositionOffset.Y + position, this.Width - padding.Left - padding.Right - (scrollerBarVisable ? 0 : scrollerBarWidth), itemHeight); position = position + itemHeight; return rect; }
/// <summary> /// 繪製 MonthItem 元素,包括下麵的 DateItem 和 DateTimeItem 子元素 /// </summary> /// <param name="g"></param> /// <param name="index"></param> /// <param name="item"></param> private void DrawTimelineItem(Graphics g, int index, MonthItem item) { StringFormat sf = new StringFormat(); sf.Alignment = StringAlignment.Center; sf.LineAlignment = StringAlignment.Center; // margin Rectangle bound = item.Bound; //g.DrawRectangle(new Pen(SystemColors.ControlDark), bound); g.DrawLine(new Pen(SystemColors.ControlLight) { DashStyle = System.Drawing.Drawing2D.DashStyle.Dot }, 5, bound.Top + 23, bound.Width - 10, bound.Top + 23); Point start = new Point(5 + 18, bound.Top + (index > 0 ? 0 : 5)); Point end = new Point(5 + 18, bound.Bottom - (index < this.DataList.Count - 1 ? 0 : 5)); g.DrawLine(new Pen(SystemColors.ControlLight), start, end); Rectangle iconRect = new Rectangle(5, bound.Top + 5, 36, 36); g.FillEllipse(Brushes.Orange, iconRect); g.DrawString(item.DateLabel, this.Font, Brushes.White, iconRect, sf); if (item.List != null) { StringFormat subSf = new StringFormat(); subSf.LineAlignment = StringAlignment.Center; Font subTitleFont = new Font("仿宋", 12, FontStyle.Bold | FontStyle.Italic) { }; int top = bound.Top + 15; for (int i = 0; i < item.List.Count; i++) { top = top + 32; DateItem subItem = item.List[i]; Rectangle subIconRect = new Rectangle(5 + 12, top + 9, 12, 12); g.FillEllipse(Brushes.Orange, subIconRect); //g.DrawEllipse(new Pen(Color.Orange) { Width=2.0f }, subIconRect); //g.DrawString((i + 1).ToString(), this.Font, Brushes.White, subIconRect, sf); subIconRect.Inflate(-2, -2); g.FillEllipse(Brushes.White, subIconRect); Rectangle subRect = new Rectangle(56, top, bound.Width - 64, 32); if (subItem.Selected) { using (var roundedRectanglePath = CreateRoundedRectanglePath(subRect, 2)) { g.FillPath(new SolidBrush(Color.FromArgb(240, 245, 249)), roundedRectanglePath); } } Rectangle subTitleRect = new Rectangle(56, top, bound.Width - 64 - 30, 32); //g.DrawRectangle(new Pen(Color.Orange), subTitleRect); //g.DrawString((i + 1) + "、" + subItem.Title, this.Font, Brushes.Red, subTitleRect, subSf); Brush subTitleBrush = Brushes.Black; g.DrawString(subItem.Date.ToString("yyyy-MM-dd"), subTitleFont, subTitleBrush, subTitleRect, subSf); //g.FillRectangle(Brushes.Red, subTitleRect); Rectangle subOptionRect = new Rectangle(bound.X + bound.Width - 34 + 4, top + 8, 16, 16); //g.FillRectangle(Brushes.Yellow, subOptionRect); g.DrawImage(this.TimeLineIcons.Images[2], subOptionRect); subItem.AddRect = subOptionRect; subItem.ClickRect = subRect; if (subItem.List != null) { for (int j = 0; j < subItem.List.Count; j++) { top = top + 32; DateTimeItem subsubItem = subItem.List[j]; //Rectangle subsubIconRect = new Rectangle(5 + 14, top + 10, 8, 8); //g.FillEllipse(Brushes.Orange, subsubIconRect); //subsubIconRect.Inflate(-2, -2); //g.FillEllipse(Brushes.White, subsubIconRect); Rectangle DateTimeItemClickRect = new Rectangle(56, top + 2, bound.Width - 64, 28); if (!string.IsNullOrEmpty(subsubItem.Summary)) DateTimeItemClickRect = new Rectangle(DateTimeItemClickRect.X, DateTimeItemClickRect.Y, DateTimeItemClickRect.Width, DateTimeItemClickRect.Height + 32); if (subsubItem.Selected) { using (var roundedRectanglePath = CreateRoundedRectanglePath(DateTimeItemClickRect, 2)) { g.FillPath(new SolidBrush(Color.FromArgb(240, 245, 249)), roundedRectanglePath); } } Brush drawTitleBrush = Brushes.Black; if (subsubItem.Selected) drawTitleBrush = Brushes.Blue; Color drawTitleColor = Color.Black; if (subsubItem.Selected) drawTitleColor = Color.Blue; if (!subsubItem.Selected) { switch (subsubItem.Level) { case ImportantLevel.Important: drawTitleColor = Color.Orange; break; case ImportantLevel.MoreImportant: drawTitleColor = Color.Brown; break; case ImportantLevel.MostImportant: drawTitleColor = Color.Red; break; } } //Rectangle subsubImgRect = new Rectangle(56 + 0, top + 7, 16, 16); ////g.FillEllipse(Brushes.Red, subsubImgRect); //g.DrawImage(this.TimeLineIcons.Images[2], subsubImgRect); Brush itemIconBrush = Brushes.Red; switch (subsubItem.Timeliness) { case Timeliness.Normal: itemIconBrush = Brushes.Green; break; case Timeliness.Yellow: itemIconBrush = Brushes.Yellow; break; case Timeliness.Orange: itemIconBrush = Brushes.Orange; break; case Timeliness.Red: itemIconBrush = Brushes.Red; break; case Timeliness.Dark: itemIconBrush = Brushes.Gray; break; case Timeliness.Black: itemIconBrush = Brushes.Black; break; } int m = 20; Rectangle subsubImgRect = Rectangle.Empty; if (subsubItem.Icon != null) { if (subsubItem.Icon.Height == 16) { m = 20; subsubImgRect = new Rectangle(56 + 0, top + 3, 16, 16); } if (subsubItem.Icon.Height == 24) { m = 28; subsubImgRect = new Rectangle(56 + 0, top + 3, 24, 24); } else { throw new Exception("只支持16*16、24*24大小的圖標"); } g.DrawImage(subsubItem.Icon, subsubImgRect); } else { if(!string.IsNullOrEmpty(subsubItem.PersonName)) { m = 28; subsubImgRect = new Rectangle(56 + 0, top + 3, 24, 24); } else { m = 20; subsubImgRect = new Rectangle(56 + 0, top + 7, 16, 16); } g.FillEllipse(itemIconBrush, subsubImgRect); if (!string.IsNullOrEmpty(subsubItem.PersonName)) { Brush showNameBrush = Brushes.White; Font showNameFont = new Font("微軟雅黑", 8, FontStyle.Bold); if (itemIconBrush == Brushes.Red || itemIconBrush == Brushes.Yellow) { showNameBrush = Brushes.Black; } g.DrawString(subsubItem.PersonName, showNameFont, showNameBrush, subsubImgRect, sf); } } //Rectangle subsubTitleRect = new Rectangle(56 + 20, top, bound.Width - 84 - 60, 32); Rectangle subsubTitleRect = new Rectangle(56 + m, top, bound.Width - 84 - 60, 32); //g.DrawRectangle(new Pen(Color.Orange), subsubTitleRect); //g.DrawString((i + 1) + "、" + subItem.Title, this.Font, Brushes.Red, subTitleRect, subSf); //g.DrawString(subsubItem.Title, this.Font, drawTitleBrush, subsubTitleRect, subSf); TextRenderer.DrawText(g, subsubItem.Title, this.Font, subsubTitleRect, drawTitleColor, TextFormatFlags.Left | TextFormatFlags.WordEllipsis | TextFormatFlags.VerticalCenter); if (_isEditModel && subsubItem.Selected) { //繪製刪除按扭 Size subsubTitleSize = TextRenderer.MeasureText(g, subsubItem.Title, this.Font); subsubItem.EditRect = new Rectangle(subsubTitleRect.X + subsubTitleSize.Width + 2, top + 8, 16, 16); subsubItem.DeleteRect = new Rectangle(subsubTitleRect.X + subsubTitleSize.Width + 2 + 16 + 4, top + 8, 16, 16); } Rectangle subsubTimeRect = new Rectangle(bound.Width - 64, top, 56, 32); //g.FillRectangle(Brushes.Green, subsubTimeRect); //g.DrawString((i + 1) + "、" + subItem.Title, this.Font, Brushes.Red, subTitleRect, subSf); g.DrawString(subsubItem.DateTime.ToString("HH:mm:ss"), this.Font, drawTitleBrush, subsubTimeRect, subSf); if (!string.IsNullOrEmpty(subsubItem.Summary)) { Font drawSummaryFont = this.Font; Brush drawSummaryBrush = Brushes.Gray; Color drawSummaryColor = Color.Gray; if (subsubItem.Selected) { drawSummaryBrush = new SolidBrush(SystemColors.ControlDark); //drawSummaryFont = new Font(this.Font, FontStyle.Italic); drawSummaryColor = SystemColors.ControlDark; } top = top + 32; Rectangle subsubSummaryRect = new Rectangle(56, top, bound.Width - 64, 26); //g.DrawRectangle(Pens.Red, subsubSummaryRect); //g.DrawString(subsubItem.Summary, drawSummaryFont, drawSummaryBrush, subsubSummaryRect, subSf); TextRenderer.DrawText(g, subsubItem.Summary, drawSummaryFont, subsubSummaryRect, drawSummaryColor, TextFormatFlags.Left | TextFormatFlags.WordEllipsis | TextFormatFlags.VerticalCenter); } subsubItem.ClickRect = DateTimeItemClickRect; g.DrawLine(new Pen(SystemColors.ControlLight) { DashStyle = System.Drawing.Drawing2D.DashStyle.Dot }, 56, top + 32, bound.Width - 10, top + 32); if (_isEditModel && subsubItem.Selected) { //switch (subsubItem.ButtonState) //{ // case 1: // g.FillRectangle(new SolidBrush(Color.Orange), subsubItem.EditRect); // g.FillRectangle(new SolidBrush(Color.FromArgb(240, 245, 249)), subsubItem.DeleteRect); // break; // case 2: // g.FillRectangle(new SolidBrush(Color.FromArgb(240, 245, 249)), subsubItem.EditRect); // g.FillRectangle(new SolidBrush(Color.Orange), subsubItem.DeleteRect); // break; // default: // g.FillRectangle(new SolidBrush(Color.FromArgb(240, 245, 249)), subsubItem.EditRect); // g.FillRectangle(new SolidBrush(Color.FromArgb(240, 245, 249)), subsubItem.DeleteRect); // break; //} g.FillRectangle(new SolidBrush(Color.FromArgb(240, 245, 249)), subsubItem.EditRect); g.FillRectangle(new SolidBrush(Color.FromArgb(240, 245, 249)), subsubItem.DeleteRect); //繪製編輯按扭 g.DrawImage(this.TimeLineIcons.Images[0], subsubItem.EditRect); //繪製刪除按扭 g.DrawImage(this.TimeLineIcons.Images[1], subsubItem.DeleteRect); } } } } } StringFormat sf2 = new StringFormat(); sf2.LineAlignment = StringAlignment.Center; Rectangle itemTitleRect = new Rectangle(56, bound.Top + 5, bound.Width - 64, 36); //g.DrawRectangle(new Pen(Color.Orange), itemTitleRect); //g.DrawString("共計 " + (item.List == null ? 0 : item.List.Count()) + " 個事項", this.Font, Brushes.Black, itemTitleRect, sf2); }
繪製滾動條這裡先貼一張不帶上下箭頭的滾動條截圖
主要涉及到的計算如下:
/// <summary> /// 計算 Thumb 的高 /// </summary> /// <returns></returns> private int GetThumbHeight() { int disHeight = this.BorderStyle == NBorderStyle.None ? this.Height : this.Height - 2; if (MaxnumHeight == 0 || MaxnumHeight <= disHeight) return disHeight; int thumbHeight = (int)(disHeight * 1.0d / MaxnumHeight * disHeight); if (thumbHeight < 20) thumbHeight = 20; largeChange = DisplayRectangle.Height - thumbHeight; return thumbHeight; } /// <summary> /// 繪製 Thumb 及計算 Thumb 的位置 /// </summary> /// <param name="g"></param> private void DrawScrollThumb(Graphics g) { int thumbOffsetY = (int)(scrollerBarValue * 1.0 / scrollerBarMaxnum * (scrollerRect.Height - thumbRect.Height)); thumbRect = new Rectangle(scrollerRect.X, scrollerRect.Y + thumbOffsetY, scrollerRect.Width, scrollerThumbHeight); g.FillRectangle(Brushes.Gray, thumbRect); } /// <summary> /// 計算所以元素 累加起來總的高度(包話不可見部分) /// </summary> private int MaxnumHeight { get { int maxnum = 0; Graphics g = null; if (this.DataList != null) { for (int i = 0; i < this.DataList.Count; i++) { var item = this.DataList[i]; maxnum += MeasureItemBound(g, i, item).Height; } } return maxnum; } }
第四步、處理控制項中的事件
首先我們重寫一個Click事件對外開放,usercontrol 自控制項本身就有一個click事件,這裡我們在定義事件的屬性前面添加 new 關鍵字表達用新的事件屬性來替換掉原有的click事件。代碼如下:
private static readonly object itemEventObject = new object(); /// <summary> /// 重寫一個Click事件對外開放 /// </summary> public new event EventHandler<TimeLineEventArgs> Click { add { Events.AddHandler(itemEventObject, value); } remove { Events.RemoveHandler(itemEventObject, value); } }
這裡可以看到我們定義了一個 TimeLineEventArgs 實體類,裡面主要有 Command 命令 Data 數據兩個屬性,在控制項內類我們通過判斷點擊滑鼠的位置來判斷觸發哪個元素,哪種操