Silverlight 甘特图






4.85/5 (27投票s)
使用 Silverlight 2.0 实现甘特图的示例。
引言
本项目演示了如何在 Silverlight 中实现甘特图功能。主要目的是允许用户快速轻松地输入与两个元素相关的数据。在此示例中,这两个元素是日期和人员;但是,代码可以适应其他关系。
甘特图的特性
这个 Silverlight 项目并非像 Microsoft Project 那样的功能齐全的甘特图。它只演示了最基本的功能,并可作为您自己项目的起点。
它包含的特性有:
- 在一个可滚动窗口中显示一整年
- 最多允许为15行创建每个日期的“日期框”
- 可以更改15行中每个日期的文本
- 每个“日期框”都可以调整大小,以增加或减少天数
- 每个“日期框”都可以删除
- 每行的“日期框”不能重叠
- 更改年份时,“日期框”会保存并重新显示,当再次选择该年份时。
布局
Silverlight 甘特图由以下元素组成:
- 工具栏 - 一个 Silverlight
Canvas
控件,可以左右滑动。它滑动到一个名为ToolBarWindow
的“剪裁窗口”后面,该窗口一次只显示工具栏的一部分。 - 年份选择器 - 一个 Silverlight
ComboBox
控件,允许更改年份。 - 滚动条 - 一个 Silverlight
ScrollBar
控件,通过编程方式连接到工具栏。它允许用户移动工具栏。 - 行标签 - 一系列15个 Silverlight
Textbox
控件,为每个工具栏行提供标签。 - 月份框 - 一系列12个 Silverlight 控件,代表月份。
- 日期框 - 一系列 Silverlight 控件,代表月份中的一天。当某天落在周末时,控件会显示阴影。
- 日期框 - 一个 Silverlight 控件,代表放置在工具栏上的日期。
网格
当 Silverlight 甘特控件加载时,它会创建一些默认数据。
#region CreateDefaultData
private void CreateDefaultData()
{
colDateBoxAllYears = new List<datebox>();
DateBox objDateBox = new DateBox(3,
Convert.ToDateTime("1/10/2009"),
Convert.ToDateTime("1/14/2009"));
colDateBoxAllYears.Add(objDateBox);
DateBox objDateBox2 = new DateBox(4,
Convert.ToDateTime("1/5/2009"),
Convert.ToDateTime("1/6/2009"));
colDateBoxAllYears.Add(objDateBox2);
}
#endregion
它实例化两个 DateBox
控件,传入行号以及“开始日期”和“结束日期”。然后将它们添加到名为 colDateBoxAllYears
的通用 List
中。
#region DisplayYear
private void DisplayYear(string strYear)
{
ToolBar.Children.Clear();
List<dateplannermonth> colDatePlannerMonths =
DatePlannerMonth.GetMonths(strYear);
double StartPosition = (double)0;
foreach (DatePlannerMonth objDatePlannerMonth in colDatePlannerMonths)
{
AddMonthToToolbar(objDatePlannerMonth, strYear, StartPosition);
StartPosition = StartPosition + objDatePlannerMonth.MonthWidth;
}
DisplayGridlines();
LoadEventsForYear();
}
#endregion
当调用 DisplayYear
方法时,将使用此集合。此方法会将当前年份的月份和日期添加到 ToolBar
(即 Grid
)中。它还会添加网格线以及 colDateBoxAllYears
集合中当前年份的所有日期。
#region UpdateToolBarPosition
private void UpdateToolBarPosition(Point Point)
{
double dCurrentPosition = (Point.X - StartingDragPoint.X);
if ((dCurrentPosition < 0) & (dCurrentPosition > -8234))
{
Canvas.SetLeft(ToolBar, Point.X - StartingDragPoint.X);
ctlScrollBar.Value = dCurrentPosition * -1;
}
}
#endregion
通过改变 Canvas.SetLeft
的位置来移动 ToolBar
。ctlScrollBar.Value
移动 ScrollBar
控件的位置,使其与 ToolBar
保持同步。
滚动条
ScrollBar
控件使用以下代码移动 ToolBar
:
#region ctlScrollBar_Scroll
private void ctlScrollBar_Scroll(object sender,
System.Windows.Controls.Primitives.ScrollEventArgs e)
{
Point Point = new Point(e.NewValue, 0);
Canvas.SetLeft(ToolBar, Point.X * -1);
}
#endregion
月份和日期
MonthBox
控件主要由一个 Silverlight StackPanel
控件和31个 DayBox
控件组成。当每个月份控件创建时,它的大小会被调整为只显示该月份应有的天数。
#region GetMonthWidth
private static double GetMonthWidth(DateTime dtMonthYear)
{
// 31 days is 744
double dWidth = 744;
// Get the days in the Month
int intDaysInMonth = DateTime.DaysInMonth(dtMonthYear.Year,
dtMonthYear.Month);
// Determine the difference between
// 31 and the current days in the Month
int intDaysToSubtract = (31 - intDaysInMonth);
// If there is a difference subtract the days
if (intDaysToSubtract > 0)
{
// The Width of each day is 24
// Subtract 24 for each day
dWidth = dWidth - (24 * intDaysToSubtract);
}
return dWidth;
}
#endregion
ASP.NET 的 DateTime.DaysInMonth
方法将自动处理闰年等复杂计算。
年份下拉列表
更改年份时,将执行以下代码:
#region dlYear_SelectionChanged
private void dlYear_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
if (colDateBoxAllYears != null)
{
// Get all the entries in the DateBox collections
// that are not in the current Year
List<datebox> colDateBoxNotCurrentYear =
colDateBoxAllYears.AsEnumerable().Where(x => x.DayBoxStart.Year !=
dtCurrentYear.Year).Cast<datebox>().ToList();
// Get all the DateBoxes in the Toolbar canvas (The current year)
List<datebox> colDateBoxCurrentYear =
ToolBar.Children.AsEnumerable().Where(x => x.GetType().Name ==
"DateBox").Cast<datebox>().ToList();
// Build final collection
colDateBoxAllYears = new List<datebox>();
foreach (DateBox objDateBox in colDateBoxNotCurrentYear)
{
colDateBoxAllYears.Add(objDateBox);
}
foreach (DateBox objDateBox in colDateBoxCurrentYear)
{
colDateBoxAllYears.Add(objDateBox);
}
// Set the current year
dtCurrentYear =
Convert.ToDateTime(String.Format("1/1/{0}",
GetSelectedYear()));
DisplayYear(GetSelectedYear());
}
}
#endregion
代码使用 LINQ 从 colDateBoxAllYears
集合中获取所有不属于当前年份的 DateBox
控件;然后使用 LINQ 获取(当前年份的框)ToolBar
上的 DateBox
控件。
然后将两者合并以构建最终集合。该集合保存在 ccolDateBoxAllYears
集合中,然后显示新选定的年份。
DateBox 控件
DateBox
控件用于指示在 ToolBar
上选择的日期。它由一个 Silverlight Rectangle
控件以及其左右两侧的 Silverlight Canvas
控件组成。左右两侧的 Canvas
控件用于确定用户何时尝试拖动控件以使其更宽或更窄。
public DateBox(int parmBoxRow, DateTime parmDayBoxStart, DateTime parmDayBoxStop)
{
// Required to initialize variables
InitializeComponent();
_BoxRow = parmBoxRow;
_DayBoxStart = parmDayBoxStart;
_DayBoxStop = parmDayBoxStop;
TimeSpan tsBoxDaysDifference = _DayBoxStop - _DayBoxStart;
int intBoxDays = tsBoxDaysDifference.Days;
this.BoxSize = (intBoxDays * 24) + 24;
SetToolTip();
}
当控件实例化时,会保存当前的行号以及开始和结束日期,并根据天数设置框的宽度。
#region SetToolTip
private void SetToolTip()
{
ToolTipService.SetToolTip(BoxRetangle, String.Format("{0} - {1}",
_DayBoxStart.ToShortDateString(),
_DayBoxStop.ToShortDateString()));
ToolTipService.SetToolTip(LeftSideHandle, String.Format("{0} - {1}",
_DayBoxStart.ToShortDateString(),
_DayBoxStop.ToShortDateString()));
ToolTipService.SetToolTip(RightSideHandle, String.Format("{0} - {1}",
_DayBoxStart.ToShortDateString(),
_DayBoxStop.ToShortDateString()));
}
#endregion
它还使用 ToolTipService
对象创建当用户将鼠标悬停在元素上时显示的 DateBox
日期的弹出窗口。
private void LeftButton_MouseLeftButtonDown(object sender, MouseButtonEventArgs e)
{
// Get current Mouse position
Point tmpPoint = e.GetPosition(null);
// Build a list of elements at the current mouse position
List<uielement> hits =
(List<uielement>)System.Windows.Media.VisualTreeHelper
.FindElementsInHostCoordinates(tmpPoint, this);
// Get all the DateBoxes in the Toolbar canvas
List<datebox> colDateBox = ToolBar.Children.AsEnumerable().Where(
x => x.GetType().Name == "DateBox").Cast<datebox>().ToList();
// Loop through all the DateBoxes in the Toolbar canvas
foreach (DateBox objDateBox in colDateBox)
{
if (hits.Contains(objDateBox))
{
Point DateBoxPoint = e.GetPosition(objDateBox);
TimeSpan tsBoxDays = objDateBox.DayBoxStop - objDateBox.DayBoxStart;
if (DateBoxPoint.X <= 2 ||
(DateBoxPoint.X >= ((tsBoxDays.Days + 1) * 24) - 2))
{
// Set the objResizingDateBox to the current
// datebox so that it will be
// resized when the mouse button is released
objResizingDateBox = objDateBox;
StartingCanvasDragPoint = GetMousePosition(ToolBar, e);
// Save the side that is being resized
//strResizingDateBoxSide
if (DateBoxPoint.X <= 2)
{
strResizingDateBoxSide = ''Left'';
}
else
{
strResizingDateBoxSide = ''Right'';
}
}
else
{
// The center of the DateBox was clicked. Show the Popup
ShowPopup(objDateBox);
return;
}
}
}
当用户单击 ToolBar
时,使用 FindElementsInHostCoordinates
方法查找当前鼠标位置的所有元素。请注意,水平和垂直网格线具有 IsHitTestVisible="false"
以提高性能(这意味着它们将被忽略)。
如果检测到现有的 DateBox
,代码会检查是否检测到 DateBox
的某一边,因为这将表明用户希望将 DateBox
拖动得更宽或更窄。
// If the mouse did not move - Place a box on the grid
if (CheckTolerance(StartingDragPoint, EndingDragPoint))
{
Point objPoint = BoxClicked(EndingDragPoint);
InsertBox(objPoint, 0);
}
当用户在 ToolBar
上单击后抬起鼠标按钮,并且该位置没有其他元素时,将使用 CheckTolerance
方法确定鼠标在任一方向上是否移动了超过两个“点”。
#region InsertBox
private void InsertBox(Point objPoint, int intDays)
{
int intX = Convert.ToInt32(objPoint.X);
int intY = Convert.ToInt32(objPoint.Y);
// Find position for the Box
Point boxPoint = new Point((intX * 24 - 24), (intY * 24 - 28));
// Only place a box if row is higher than 2
if (intY > 2)
{
DateBox objDateBox = new DateBox(intY, GetBoxDate(intX, GetSelectedYear()),
GetBoxDate(intX + intDays, GetSelectedYear()));
Canvas.SetLeft(objDateBox, boxPoint.X);
Canvas.SetTop(objDateBox, boxPoint.Y);
ToolBar.Children.Add(objDateBox);
}
}
#endregion
如果鼠标未超出“容差”,则会实例化一个新的框,并通过以下方式添加到 Toolbar
中:ToolBar.Children.Add(objDateBox)
。
弹出窗口
当用户直接单击 DateBox
时,将使用 Popup
控件显示 DateBox
的日期范围,并允许用户选择删除 DateBox
。
#region CreatePopup
private void CreatePopup()
{
objDateBoxPopup = new Popup();
objDateBoxPopup.Name = "DeletePopup";
objDateBoxPopup.Child = new DateBoxPopup();
objDateBoxPopup.SetValue(Canvas.LeftProperty, 150d);
objDateBoxPopup.SetValue(Canvas.TopProperty, 150d);
objDateBoxPopup.HorizontalOffset = 25;
objDateBoxPopup.VerticalOffset = 25;
ToolBarWindow.Children.Add(objDateBoxPopup);
objDateBoxPopup.IsOpen = false;
}
#endregion
当 Silverlight 甘特图首次加载时,会创建一个 Popup
控件,并在其中放置一个 DateBoxPopup
控件。
#region ShowPopup
private void ShowPopup(DateBox objDateBox)
{
if (objDateBoxPopup.IsOpen == false)
{
objDateBoxPopup.CaptureMouse();
DateBoxPopup GanttPopUpBox =
(DateBoxPopup)objDateBoxPopup.FindName("GanttPopUpBox");
GanttPopUpBox.objDateBox = objDateBox;
objDateBoxPopup.IsOpen = true;
}
}
#endregion
当 Popup
需要显示时,将 DateBox
的实例设置为 DateBoxPopup
控件(包含在 Popup
中)的属性。
#region btnDelete_Click
private void btnDelete_Click(object sender, RoutedEventArgs e)
{
Canvas ToolBar = (Canvas)this.LayoutRoot.FindName("ToolBar");
ToolBar.Children.Remove(_objDateBox);
ClosePopup();
}
#endregion
如果单击“删除”按钮,DateBoxPopup
控件将已经有一个 DateBox
控件的实例,因此可以通过将其从 ToolBar
中删除来删除它。
#region ClosePopup
private void ClosePopup()
{
Popup objPopup = (Popup)this.LayoutRoot.FindName("DeletePopup");
objPopup.ReleaseMouseCapture();
objPopup.IsOpen = false;
}
#endregion
当 Popup
“关闭”时,只需将其 IsOpen
属性设置为 false
。
鼠标的力量
鼠标是一种非常高效的输入设备。用户移动手的速度比通常打字的速度要快得多。像甘特图这样的控件允许用户通过一次鼠标单击同时指示两件事。在此示例中,单击表示:
- “我想预订这个人在这几天的行程。”
但是,您可以改编此代码来表示其他内容,例如:
- “我想在这些天预订这个房间。”
- “我想把这个产品放在商店的这个区域”(如果您想这样做,则不应使用
MonthBox
;相反,您会在控件顶部显示商店的每个区域)。
为您节省时间
希望此控件能为您实现自己的甘特图功能节省时间。主要因为它执行了必要的计算,以防止 DateBox
重叠。
要保存数据,只需保存 colDateBoxAllYears
集合。要加载数据,只需在启动时将 colDateBoxAllYears
集合传递给控件,然后调用 DisplayYear
方法。