65.9K
CodeProject 正在变化。 阅读更多。
Home

Silverlight 甘特图

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.85/5 (27投票s)

2009年2月14日

MIT

6分钟阅读

viewsIcon

96729

downloadIcon

3094

使用 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 的位置来移动 ToolBarctlScrollBar.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 方法。

© . All rights reserved.