My development assistant, who started writing a few years ago for some reason, worked hard and persisted all the way, but is still improving. Project address: https://gitee.com/sqlorm/DevelopAssistant Welcome your compliment and support.
Today, I want to share with you the timeline control, which is a user-defined control implemented by GDI drawing and rewriting the original events. It has a beautiful interface and is easy to operate. Your favorite classmates might like to accept it.
The control looks like this:)


The following describes the development of this control:
First, we create a class inheritance UserControl
The control overrides the OnPaint, OnMouseClick, OnMouseMove, OnSizeChanged, OnMouseWheel methods, where the OnPaint method draws user interface elements and calculates the drawing area of the control elements in order to implement the click events of the elements in the OnMouseClick override method, OnMouseClThe ick method is to implement the click event of the control elements. OnMouseMove main entity has some mouse effects, such as sliding the mouse to change the background color, changing the default cursor shape of the control, etc. OnSizeChanged method mainly implements the calculation of control scrollbar related properties and triggers the redrawing of control elements when the size of the control is changed.Event Area Range Rectangle calculates the event related to which element is triggered by determining which element's area range the mouse clicks on, which occurs when the OnMouseWheel mouse wheel rolls.In fact, the custom controls under winform, especially those implemented by GDI drawing, are basically implemented by the above several event methods, which can be summarized in one diagram:
Step 2, Define the internal elements of the control
The control mainly refers to month object elements: MonthItem, Date object elements: DateItem, time object elements: DateTimeItem they all inherit from common object elements: TimelineItem they all have the same attribute Id (associated with the primary key of the database table), Name name, tag of other data-related bindings of Tag.Next, both MonthItem and DateItem have a Bound, and the user saves the drawing area of the element in the control.The code for these three element entity classes is posted below:*
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 :Default 1: Modify 2: Delete /// </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; } } }
Step 3: Draw the elements inside the control
The elements inside the drawing control are mainly divided into drawing TimelineItem (including MonthItem, DateItem and DateTimeItem) and scrollbars of the control. Generally speaking, the scrollbars that come with winform controls are drawn by the system and are often closely related to the operating system. Here our timeline control is suitable for development assistance.Hand-related themes, so we use our own scrollbar drawing internally to implement scrollbar controls by mainly rewriting the OnMouseMove, OnMouseWheel two-phase event methods.
Drawing elements like TimelineItem here mainly pastes the following code:
/// <summary> /// Calculation TimelineItem Draw area by MonthItem Subelement recursive loop calculates the entire MonthItem Drawing area of element /// </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> /// Draw MonthItem Elements, including the following DateItem and DateTimeItem Child Elements /// </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("Imitate Song", 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("Only 16 supported*16,24*24 Size Icon"); } 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("Microsoft YaHei", 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) { //Draw Delete Torsion 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); //Draw Edit Torsion g.DrawImage(this.TimeLineIcons.Images[0], subsubItem.EditRect); //Draw Delete Torsion 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("Total " + (item.List == null ? 0 : item.List.Count()) + " Items", this.Font, Brushes.Black, itemTitleRect, sf2); }
Draw a scrollbar here First paste a scrollbar screenshot without up and down arrows.
The main calculations involved are as follows:
/// <summary> /// Calculation Thumb High /// </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> /// Draw Thumb And calculation Thumb Location /// </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> /// Calculate the total height of the elements so they add up(Package invisible part) /// </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; } }
Step 4, Handle events in the control
First, we rewrite a Click event that is open to the public. usercontrol has a click event from the control itself. Here, we add a new keyword expression before defining the properties of the event to replace the original click event with a new event attribute.The code is as follows:
private static readonly object itemEventObject = new object(); /// <summary> /// Rewrite one Click Events Open to the World /// </summary> public new event EventHandler<TimeLineEventArgs> Click { add { Events.AddHandler(itemEventObject, value); } remove { Events.RemoveHandler(itemEventObject, value); } }
Here you can see that we have defined a TimeLineEventArgs entity class, which has two main properties: Command command and Data data. In the control class, we determine which element is triggered and which type of action event is triggered by clicking the mouse. The control is added, edited and deleted in edit modeThe three event types are passed to an external call through Command with the following implementation code:
protected override void OnMouseClick(MouseEventArgs e) { base.OnMouseClick(e); if (!newItemModel) { //Whether to continue selected as DateItem Skip when DateTimeItem loop bool isChidrenContinueForeach = true; foreach(MonthItem monthItem in DataList) { foreach (DateItem dateItem in monthItem.List) { if (dateItem.ClickRect.Contains(e.Location)) { var eventArg = new TimeLineEventArgs(dateItem); if (_isEditModel && dateItem.AddRect != Rectangle.Empty && dateItem.AddRect.Contains(e.Location)) { eventArg.Command = "new"; } DoItemClick(eventArg); isChidrenContinueForeach = false; } foreach (DateTimeItem dateTimeItem in dateItem.List) { if (isChidrenContinueForeach && dateTimeItem.ClickRect.Contains(e.Location)) { var eventArg = new TimeLineEventArgs(dateTimeItem); eventArg.Command = "detail"; if (_isEditModel && dateTimeItem.EditRect != Rectangle.Empty && dateTimeItem.EditRect.Contains(e.Location)) { eventArg.Command = "edit"; } if (_isEditModel && dateTimeItem.DeleteRect != Rectangle.Empty && dateTimeItem.DeleteRect.Contains(e.Location)) { eventArg.Command = "delete"; } DoItemClick(eventArg); isChidrenContinueForeach = false; } if (!isChidrenContinueForeach) break; } if (!isChidrenContinueForeach) break; } } } else { if (newItemRect.Contains(e.Location)) { var eventArg = new TimeLineEventArgs(null); eventArg.Command = "new"; DoItemClick(eventArg); } } }
The scrollbar event code handled in the control is as follows:
/// <summary> /// When sliding a scrollbar, OnMouseMove trigger /// </summary> /// <param name="e"></param> private void DoMouseScrolling(MouseEventArgs e) { if (!scrollerBarVisable) return; int d = mouseDownOffset.Y + e.Location.Y - mouseDownPos.Y; scrollerBarValue = (int)(d * 1.0 / (scrollerRect.Height - thumbRect.Height) * scrollerBarMaxnum); if (scrollerBarValue < 0) { scrollerBarValue = 0; } if (scrollerBarValue > 100) { scrollerBarValue = 100; } int drawOffsetY = (int)(-scrollerBarValue * 1.0 / scrollerBarMaxnum * (MaxnumHeight + smallChange - this.Height)); drawPositionOffset = new Point(0, drawOffsetY); this.Invalidate(); } /// <summary> /// Handling mouse wheel events by OnMouseWheel trigger /// </summary> /// <param name="e"></param> private void DoMouseWheel(MouseEventArgs e) { if (!scrollerBarVisable) return; int olePositionOffsetY = drawPositionOffset.Y; int mouseWheelScrollLines = e.Delta / NativeMethods.WHEEL_DELTA; int newPositionOffsetY = drawPositionOffset.Y + mouseWheelScrollLines * smallChange; int d = newPositionOffsetY - olePositionOffsetY; scrollerBarValue += (int)(-d * 1.0 / (MaxnumHeight + smallChange - this.Height) * scrollerBarMaxnum); if (scrollerBarValue < 0) { scrollerBarValue = 0; } if (scrollerBarValue > 100) { scrollerBarValue = 100; } int drawOffsetY = (int)(-scrollerBarValue * 1.0 / scrollerBarMaxnum * (MaxnumHeight + smallChange - this.Height)); drawPositionOffset = new Point(0, drawOffsetY); this.Invalidate(); }
Step 5, Control Performance Optimization
Optimizing the performance of a control mainly depends on determining whether the element is in the visual area or not. If the element and its subelements are not drawn in the visual area, especially when the control has hundreds or even thousands of elements, the drawing of the elements is expensive (memory usage and CPU calculation), and often a controlThe number of elements that an object can display in its visual area is limited.*
/// <summary> /// Method of determining whether or not a visible area is visible /// </summary> /// <param name="bound"></param> /// <returns></returns> private bool IsRectangleVisible(Rectangle bound) { bool isItemDrawModel = true; if (bound.Bottom < this.DisplayRectangle.Top || bound.Top > this.DisplayRectangle.Bottom) { isItemDrawModel = false; } return isItemDrawModel; } /// <summary> /// Add judgment to drawing elements, skip not drawing within visual range /// </summary> /// <param name="g"></param> private void DrawTimelineItems(Graphics g) { position = 0; g.SmoothingMode = System.Drawing.Drawing2D.SmoothingMode.HighQuality; if (DataList != null) { for (int index = 0; index < DataList.Count; index++) { MonthItem item = DataList[index]; item.Bound = MeasureItemBound(g, index, item); if (IsRectangleVisible(item.Bound)) { DrawTimelineItem(g, index, item); } } } g.SmoothingMode = System.Drawing.Drawing2D.SmoothingMode.Default; }
The above is the development process of time control, in which the interface design refers to the personal homepage dynamics of open source Chinese code cloud:
Complete code can be downloaded by clicking on the address above to enter the link, which feels good and I hope you can give a compliment. Thank you!