WPF TimeLine Custom Control
This is a custom WPF control and not a UserForm or a standard control with some extra properties in XAML. It is a proper class, which you would usually place in a library.
All Controls inherit from the FrameworkElement class. Some inherit from the Control Class, which is derived from the FrameworkElement class. In this case here I am deriving our custom control from the Canvas class, which in turn is derived from the Panel class and inherently FrameworkElement class. Once again I am not re-inventing the wheel and use existing functionality. Therefore the code remains pretty short. I am basically overriding the OnRender() event of the Canvas class.
This effects the design-time and run-time, which means you can see immediate results while you add the control in the XAML/Blend editor and of course when your C# application is running.
Just in case you are interested in further reading for design-time enhancements: WpfTutorial
The code in the MainWindow is not required. This is just a demonstration on how to change the display at run-time. A timer is used to make the change obvious. The code execution would be too fast otherwise. You wouldn’t see the first TimeLine graph, just the second. The method SetTimeSeries() triggers the change by invalidating the Canvas control (method InvalidateVisual()).
You can see a similar initialization in the TimeLineControl constructor. This data can be seen at design-time. You could also code that part in the XAML code, but this is not the purpose here. The control should not need any additional XAML in its ‘distributable’ version. Implement the TimeLineControl as it is. That is the beauty of overriding OnRender() rather than using WPF templates.
The inner control paddings and other settings were hard-coded. You can change these. Hard-coding made the example easier to understand – no redundant code to confuse you. Feel free to replace this part with flexible user settings or calculated values.
<Window xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" mc:Ignorable="d" x:Class="TimeLine.MainWindow" xmlns:local="clr-namespace:TimeLine" Title="TimeLineControl" Height="130" Width="525"> <DockPanel> <local:TimeLineControl x:Name="MyTimeLineControl" /> </DockPanel> </Window>
using System; using System.Collections.Generic; using System.Timers; using System.Windows; using System.Windows.Documents; namespace TimeLine { public partial class MainWindow : Window { public MainWindow() { InitializeComponent(); Timer lTimer = new Timer(3000.0); lTimer.Elapsed += Timer_Elapsed; lTimer.Start(); } // constructor void Timer_Elapsed(object xSender, ElapsedEventArgs e) { Timer lTimer = xSender as Timer; if (lTimer == null) return; lTimer.Stop(); // demo: how to change the TimeLine List<TimeEvent> lList = new List<TimeEvent>(); AddEvent(lList, new DateTime(2015, 03, 01), ""); AddEvent(lList, new DateTime(2015, 03, 06), "exD Brown-Forman Corp"); AddEvent(lList, new DateTime(2015, 03, 10), "exD UniFirst Corp"); AddEvent(lList, new DateTime(2015, 03, 11), "exD Worthington Industries Inc"); AddEvent(lList, new DateTime(2015, 03, 12), "exD Garmin Ltd"); AddEvent(lList, new DateTime(2015, 03, 18), "exD Republic Bank Corp"); AddEvent(lList, new DateTime(2015, 03, 23), "exD STMicroelectronics NV"); AddEvent(lList, new DateTime(2015, 03, 31), ""); MyTimeLineControl.SetTimeSeries(lList); } // constructor private void AddEvent(List<TimeEvent> xList, DateTime xDate, string xText) { TimeEvent lEvent = new TimeEvent(); lEvent.Date = xDate; lEvent.TextRed = xDate.ToString("dd MMMyy"); lEvent.TextBlack = xText; xList.Add(lEvent); } // } // class } // namespace
using System; using System.Collections.Generic; using System.Globalization; using System.Linq; using System.Windows; using System.Windows.Controls; using System.Windows.Media; using System.Windows.Shapes; namespace TimeLine { public class TimeLineControl : Canvas { private IEnumerable<TimeEvent> _TimeSeries; public TimeLineControl() { Background = Brushes.White; List<TimeEvent> lList = new List<TimeEvent>(); lList.Add(new TimeEvent { Date = DateTime.Today, TextRed = ":(", TextBlack = "Today" }); lList.Add(new TimeEvent { Date = DateTime.Today.AddDays(1), TextRed = "", TextBlack = "Tomorrow" }); lList.Add(new TimeEvent { Date = DateTime.Today.AddDays(7), TextRed = "", TextBlack = "Next week" }); lList.Add(new TimeEvent { Date = DateTime.Today.AddDays(14), TextRed = "", TextBlack = "Fortnight" }); lList.Add(new TimeEvent { Date = DateTime.Today.AddMonths(1), TextRed = ":)", TextBlack = "NextMonth" }); _TimeSeries = lList; } // constructor public void SetTimeSeries(IEnumerable<TimeEvent> xList) { if (!Dispatcher.CheckAccess()) { Dispatcher.Invoke(() => SetTimeSeries(xList)); return; } _TimeSeries = xList; InvalidateVisual(); } // // some hardcoding to keep the example code simple const double cPaddingLeft = 15.0; const double cPaddingRight = 50.0; const double cPaddingBottom = 17.0; const double cMarkerLength = 10.0; const double cTextOffset = 5.0; // distance to rotation point const double cTextSpace = 7.0; // distance between the red and the black text const double cAngle = 45.0; // rotation angle (0.0 <= cAngle <= 180.0) protected override void OnRender(DrawingContext xDrawingContext) { base.OnRender(xDrawingContext); double lLeft = cPaddingLeft; double lRight = ActualWidth - cPaddingRight; double lBottom = ActualHeight - cPaddingBottom; double lWidth = ActualWidth - cPaddingLeft - cPaddingRight; Point p1 = new Point(lLeft, lBottom); Point p2 = new Point(lRight, lBottom); // draw the X-Axis Pen lPen = new Pen(Brushes.Black, 3.0); lPen.DashStyle = DashStyles.Solid; xDrawingContext.DrawLine(lPen, p1, p2); // determine range DateTime lMin = _TimeSeries.Min(x => x.Date); DateTime lMax = _TimeSeries.Max(x => x.Date); double lDateRange = lMax.Subtract(lMin).TotalDays; foreach (TimeEvent t in _TimeSeries) { double lRelativeX = t.Date.Subtract(lMin).TotalDays / lDateRange; double lAbsoluteX = lRelativeX * lWidth + lLeft; // convert to canvas coordinates // draw the X-Axis marker p1 = new Point(lAbsoluteX, lBottom); p2 = new Point(lAbsoluteX, lBottom - cMarkerLength); xDrawingContext.DrawLine(lPen, p1, p2); // write the text with a 45 degrees angle Point lRotationCenter = p2; double lTextWidth = DrawText(xDrawingContext, t.TextRed, lRotationCenter, cTextOffset, Brushes.Red); // red text DrawText(xDrawingContext, t.TextBlack, lRotationCenter, lTextWidth + cTextOffset + cTextSpace, Brushes.Black); // black text } } // /// <returns>the width of the text</returns> private double DrawText(DrawingContext xDrawingContext, string xText, Point xPoint, double xOffset, SolidColorBrush xBrush) { Typeface lTypeface = new Typeface("Arial"); CultureInfo lCultureInfo = CultureInfo.CurrentCulture; FormattedText lText = new FormattedText(xText, lCultureInfo, FlowDirection.LeftToRight, lTypeface, 10.0, xBrush); RotateTransform lRotateTransform = new RotateTransform(-cAngle, xPoint.X, xPoint.Y); xDrawingContext.PushTransform(lRotateTransform); xDrawingContext.DrawText(lText, new Point(xPoint.X + xOffset, xPoint.Y - lText.Height / 2.0)); xDrawingContext.Pop(); //return new Point(xPoint.X + lText.Height / 2.0 * Math.Sin(cAngle) + lText.Width * Math.Cos(cAngle) , xPoint.Y - lText.Width * Math.Sin(cAngle)); return lText.Width; } // private void DrawLine(double x1, double x2, double y1, double y2) { Line lLine = new Line(); lLine.Stroke = Brushes.LightSteelBlue; lLine.X1 = x1; lLine.X2 = x2; lLine.Y1 = y1; lLine.Y2 = y2; lLine.HorizontalAlignment = HorizontalAlignment.Left; lLine.VerticalAlignment = VerticalAlignment.Center; lLine.StrokeThickness = 2; Children.Add(lLine); } // } // class } // namespace
using System; namespace TimeLine { public class TimeEvent { public DateTime Date; public string TextRed; public string TextBlack; } // class } // namespace
Posted on January 31, 2015, in Basic, C#, Charts, WPF and tagged angle, C#, C-sharp, chart, color, Control, custom control, design time, Drawingcontext, DrawText, programming, RotateTransform, runtime, Source code, WPF. Bookmark the permalink. 5 Comments.
This is really nice. I’d like to see (or help with) custom styling for the timeline and event labels, too. Good work!
Nice control! I’m not shure if I’m going to need this, but it looks realy cool!
I am not sure how old this content is, I tried to build the TimeLine control against .NET 4.5 and it cannot fine “TimeEvent”. Is there a class missing here? If so would it be possible to get this?
Thanks.
Ooops, not good 😦
The class is simply defining data. It seems that you can see all its member fields in the method AddEvent().
These are public DateTime Date, public string TextRed, public string TextBlack.
I am suffering a lot from sleep deprivation at the moment. It is a kind of
downside of success. So, allow me a few days to add the class.
There should only be these 3 public fields/properties.
amended