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

Gantt 图表在 .NET Windows 应用程序中

starIconstarIcon
emptyStarIcon
starIcon
emptyStarIconemptyStarIcon

2.44/5 (17投票s)

2002 年 8 月 22 日

12分钟阅读

viewsIcon

141647

downloadIcon

1876

在本文中,我将介绍一个用于在 .NET Windows 应用程序中显示 Gantt 图表的库及其用法。

引言

在本文中,我将展示如何使用我编写的一个库在 .NET Windows 应用程序中显示 Gantt 图表,该库可以使这项工作变得简单。当您需要显示一段时间内的机器活动等内容时,Gantt 图表会很有用。通常会有一个或多个列指示时间,以及一个或多个列表示每个实体(例如机器)。这些列中的每一个都代表某种活动。在本文中,我将把第一类列称为固定部分,第二类列称为项目部分。

例如,假设我们想显示一个为期 20 天的图表,并且我们希望每天显示每小时的活动情况。在这种情况下,固定部分由两列组成。我们将它们称为日期和小时。在 Gantt 图表中,我们将有 480 行(20 天 * 24 小时/天)。在我们的示例中,我们将有两台机器。第一台名为*First*,第二台名为*Second*。对于每台机器,我们希望表示三种活动,例如*Programmed*(机器已编程用于某种活动)、*Potential*(机器可能可用)或*Maintenance*(机器因维护而不可用)。为了简单起见,我们不关注这三种活动相互作用的逻辑(例如,我们假设*Programmed*可以设置为 true,而*Potential*可以设置为 true 或 false,尽管这通常不是一个好的假设)。

生成的 Gantt 图表将类似于下图所示。

固定部分由*Date*和*Hour*列组成。我们的机器是*First*和*Second*,对于每台机器,我们都有一个由*Programmed*、*Potential*和*Maintenance*列组成的项目部分。

上面的图像显示了我们将要编写的测试应用程序的输出。这个演示应用程序使用了我编写的三个库。在本文中,我们将重点介绍 `GanttLib`。第二个库名为 `CustomControls`,包含一个具有打印支持和其他功能的扩展 `DataGrid`。第三个库名为 `ObjectDumper`,包含用于将对象数据转储到日志文件中进行调试的有用类。

此代码主要用 Visual Basic 编写,因为我当时是为真正的 VB.NET 应用程序编写的。没有任何理由您不能在 C# 或 J# 应用程序中使用它。下面的代码也用 VB.NET 编写,但只需稍作修改(主要因为语法不同),它就可以在其他 .NET 语言中正常工作。

Gantt 库

本文中使用的主要库称为 `GanttLib`,它包含许多类。其中最重要的两个是 `GanttGrid` 和 `GanttData`。`GanttGrid` 是一个继承自 `DataGridEx`(我在 `CustomControls` 库中定义的扩展 `DataGrid`)的类,而 `GanttData` 是一个包含用于构建 Gantt 图表的任何定义的类。通常,在使用 `GanttLib` 的任何应用程序中,您都需要为这些类中的每一个实例化一个对象。然后,您需要初始化 `GanttData` 对象,告诉它固定部分是如何形成的、实体是什么以及项目部分是什么。

最后,您将把 `GanttGrid` 对象的 `GanttData` 属性设置为您的 `GanttData` 对象。此时,如果您显示承载 `GanttGrid` 的窗体,您将看到如上图所示的网格。显然,图表中的数据尚未初始化。现在让我们专注于示例应用程序。

示例应用程序

我们要做的第一件事是创建一个新的*Windows Application*项目和一个窗体。然后,如果需要,我们可以在此窗体中添加一些菜单。通常,主窗体可以具有*FormBorderStyle*设置为*Sizable*。

然后,我们需要向我们的项目添加对 `CustomControls`、`ObjectDumper` 和 `GanttLib` 库的引用(如果我们已经拥有这些文件的已编译版本)。另一种获得相同结果的有用方法是将现有项目 *CustomControls.vbproj*、*ObjectDumper.vbproj* 和 *GanttLib.vbproj* 添加到我们的解决方案中。如果我们选择第二种方式,我们必须在我们的主项目中添加对 `GanttLib` 和 `CustomControls` 项目的引用。请注意,`GanttLib` 引用了 `CustomControl` 和 `ObjectDumper` 项目。(如果您创建一个包含这三个项目的空白解决方案,某些引用可能会丢失。)此时构建我们的解决方案是一个好主意,以确保所有引用都正常,并构建承载 `GanttGrid` 的库。

现在是添加 `GanttGrid` 对象和 `GanttData` 对象的时候了。最简单的方法是通过 Visual Studio 中的设计器。我们也可以通过代码执行相同的操作(如果您想这样做,只需查看 `InitializeComponent` 中的代码并将其复制到您编写的函数中),但我认为最简单的方法在任何应用程序中都效果很好。首先,我们可以向 Visual Studio 工具箱添加一个新选项卡:我们称之为 `GanttLib`。右键单击此选项卡,选择*Customize Toolbox*。然后选择*.*NET Framework Components*,最后选择*Browse*。现在找到文件 `GanttLib.dll`(使用解决方案构建,通常位于 `GanttLib` 目录的*bin*文件夹中)并选择它。如果我们关闭所有打开的对话框并单击*OK*,我们将在 `GanttLib` 选项卡中找到两个控件。它们是 `GanttData` 和 `GanttGrid`。如果我们只需将这两个控件拖放到窗体设计器中,我们将获得这两个类的两个实例(如果我们需要一个有意义的名称,我们可以重命名它们)。通常,`GanttGrid` 的*Dock*属性设置为*Fill*。现在,我们可以选择性地(稍后我们将通过代码执行此操作)使用属性窗口将 `objGanttGrid` 的 `GanttData` 属性设置为 `objGanttData`。

下图显示了我们的项目以及主窗体中的两个控件。

在窗体构造函数中,调用 `InitializeComponent` 之后,我们可以添加一个调用来初始化 `GanttData` 对象的函数,如下面的代码所示。此函数有两个参数:第一个是 Gantt 图表创建的初始日期,第二个是天数。

Public Sub New()
  MyBase.New()
  'This call is required by the Windows Form Designer.

  InitializeComponent()
  'Added Code 

  Init(DateTime.Now, 20)
End Sub 
Private Sub Init(ByVal InitialDate As DateTime, ByVal nDays As Integer)
  ...
End Sub

现在让我们专注于我们要编写的代码,以初始化 `GanttData` 对象。我们需要定义项目、固定列和项目列。项目是最容易定义的。我们需要将 `ItemNames` 属性设置为一个包含项目名称的字符串数组,如下面的代码所示。

objGanttData.ItemNames = New String() {"First", "Second"}

固定列和项目列通过名为 `FixedFieldDefinitions` 和 `ItemFieldDefinitions` 的属性定义。这些属性是 `FixedColumnDefinition` 和 `ItemColumnDefinition` 对象的数组。我们需要定义这两种对象的两个数组,初始化它们,并将这两个属性设置为这些数组。

如前所述,我们有一个由两项组成的固定部分(`Date` 和 `Hour`),因此我们定义了一个包含两个元素的 `FixedFields` 数组。项目部分由三个元素组成(`Programmed`、`Potential` 和 `Maintenance`),因此 `ItemFileds` 数组可以定义为包含三个元素。

在我们的示例应用程序中,我们将展示在 Gantt 图表中定义隐藏列的可能性。如果我们只想显示一个布尔字段,而该字段实际上是某种类型的代码,这可能很有用。例如,“Programmed”表示机器已编程用于某种活动,但我们无法或不想显示是什么活动(可能是机器正在处理某种订单),并且可以使用隐藏字段来存储活动代码。此活动代码可用于通过数据库查找显示自定义工具提示(见下文)。因此,我们将定义包含四个元素的 `ItemFields` 数组。

`ItemColumnDefinition` 和 `FixedColumnDefinition` 对象有一个无参构造函数,但通常,如果您需要最基本的功能,此构造函数是无用的。让我们看一下代码。

Dim FixedFields(1) As FixedColumnDefinition
Dim cFDate As New FixedColumnDefinition("Date", _ 
   GetType(System.DateTime), False, _
   Nothing, nDays, True, InitialDate, Nothing, Nothing, 100)
Dim cFHour As New FixedColumnDefinition("Hour", _ 
   GetType(Integer), False, Nothing, _
   24, True, 0, Nothing, Nothing, 50)
FixedFields(0) = (cFDate)
FixedFields(1) = (cFHour)
Dim ItemFields(3) As ItemColumnDefinition
Dim cProgrammed As ItemColumnDefinition
Dim cPotential As ItemColumnDefinition
Dim cMaintenance As ItemColumnDefinition
Dim cCounter As ItemColumnDefinition
cProgrammed = New ItemColumnDefinition("Programmed", _ 
   GetType(Boolean), False, Nothing, False, False, 90, False)
cProgrammed.Color = Color.Red
cPotential = New ItemColumnDefinition("Potential", _ 
   GetType(Boolean), False, Nothing, False, True, 90, False)
cPotential.Color = Color.Green
cMaintenance = New ItemColumnDefinition("Maintenance", _ 
   GetType(Boolean), False, Nothing, False, False, 90, False)
cMaintenance.Color = Color.Blue
cCounter = New ItemColumnDefinition("Counter", _ 
   GetType(Integer), True, Nothing, False, -1, 90, False)
ItemFields(0) = (cProgrammed)
ItemFields(1) = (cPotential)
ItemFields(2) = (cMaintenance)
ItemFields(3) = (cCounter)
objGanttData.ItemFieldDefinitions = ItemFields
objGanttData.FixedFieldDefinitions = FixedFields

如您所见,用于定义 `FixedFieldDefinition` 对象和 `ItemFieldDefinition` 对象的构造函数相当复杂。

下面的代码显示了这些构造函数的原型

'FixedColumnDefinition Constructor: 

Public Sub New(ByVal Name As String, _
  ByVal Type As System.Type, _
  ByVal Hidden As Boolean, _
  ByVal HeaderCreationFunction As CreateHeaderText, _
  ByVal NumberOfElements As Integer, _
  ByVal IsTimeColumn As Boolean, _
  ByVal InitialValue As Object, _
  ByVal EvaluateFunction As EvaluateValue, _
  ByVal InitObject As Object, _
  ByVal PreferredWidth As Integer)

'ItemColumnDefinition Constructor:

Public Sub New(ByVal Name As String, _
  ByVal Type As System.Type, _
  ByVal Hidden As Boolean, _
  ByVal HeaderCreationFunction As CreateHeaderText, _
  ByVal AllowDbNull As Boolean, _
  ByVal DefaultValue As Object, _
  ByVal PreferredWidth As Integer, _
  ByVal IsReadOnly As Boolean)

让我们从 `FixedColumnDefinition` 构造函数开始。

  • `Name` 是此列的名称(在我们的示例中,为 `Date` 或 `Hour`)。
  • `Type` 是此列的类型(在我们的示例中,`Date` 是一个包含 `DateTime` 值的列,而 `Hour` 是一个 Integer 列)。
  • `Hidden` 表示此列不得显示。
  • `HeaderCreationFunction` 是库调用以生成列标题文本的函数。如果为此参数提供了 null 值,则使用默认函数。
  • `NumberOfElements` 是(顾名思义)为此列生成的元素数量(在我们的示例中,`Hour` 列包含 24 个元素,而 `Date` 列包含的列数是可变的)。
  • `IsTimeColumn` 表示后续列必须为该列的每个值重复行。
  • `InitialValue` 是图表中的初始值。
  • `EvaluateFunction` 是用于为图表中的每一行生成后续值的函数。如果为此参数提供了 null 值,则使用默认函数。
  • `InitObject` 是在每次调用 `EvaluateFunction` 时传递给它的一个参数。
  • `PreferredWidth` 是此列的默认宽度。

`ItemColumnDefinition` 构造函数具有以下参数。

  • `Name` 是此列的名称(在我们的示例中为 `Programmed`、`Potential`、`Maintenance` 或 `Counter`)。
  • `Type` 是此列的类型(在我们的示例中,`Counter` 是一个 Integer,其他列是 Boolean)。
  • `Hidden` 表示此列不得显示。
  • `HeaderCreationFunction` 是库调用以生成列标题文本的函数。如果为此参数提供了 null 值,则使用默认函数。
  • `AllowDbNull` 表示此列可以取 `DbNull` 值。
  • `DefaultValue` 是每一行的图表中的初始值。
  • `PreferredWidth` 是此列的默认宽度。
  • `IsReadonly` 表示此列无法修改。

作为最后一步,在我们可以构建应用程序之前,我们必须设置 `GanttGrid` 对象的 `GanttData` 属性。

objGanttGrid.GanttData = objGanttData

为 Gantt 图表添加功能

考虑到 `GanttGrid` 继承自 `DataGridEx`,我们可以例如为我们的应用程序添加打印功能,调用 `PageSetup`、`PrintPreview` 和 `Print` 方法,如下面的 `PrintPreview` 方法所示。

Private Sub mnuPrintPreview_Click(ByVal sender _ 
  As System.Object, ByVal e As System.EventArgs) _ 
  Handles mnuPrintPreview.Click
  
  Dim obj, obj2 As Object
  obj = objGanttGrid.DataSource
  If TypeOf (obj) Is DataView Then
    obj2 = CType(obj, DataView).Table
  Else
    obj2 = obj
    obj = Nothing
  End If
  Me.objGanttGrid.PageSettings = CustomControls.PageSetup.PageSettings
  objGanttGrid.PrintPreview(CType(obj, DataView), CType(obj2, DataTable), _
    CType(Me.BindingContext(objGanttGrid.DataSource), CurrencyManager), 25, _
    "Do you want to view other pages?")
End Sub

名为 `MouseOverNotificationEnabled` 的属性可用于启用或禁用基于鼠标指针位置的当前单元格通知。如果启用了通知,每秒都会检查鼠标位置,并可能触发一个名为 `MouseOverNotification` 的事件。这对于显示基于鼠标位置的自定义工具提示非常有用,您可以在以下代码中看到

Private Sub Init(ByVal InitialDate As DateTime, ByVal nDays As Integer)
  CreateMyToolTip()
  objGanttGrid.MouseOverNotificationEnabled = True
  ...
End Sub 
Private objToolTip As ToolTip
Private Sub CreateMyToolTip()
' Create the ToolTip and associate with the Form container.

objToolTip = New ToolTip() 
' Set up the delays for the ToolTip.

objToolTip.AutoPopDelay = 5000
objToolTip.InitialDelay = 500
objToolTip.ReshowDelay = 500
' Force the ToolTip text to be displayed whether or not the form is active.

objToolTip.ShowAlways = True
objGanttGrid.SetToolTip(objToolTip, Nothing)
End Sub
Private Sub objDataGrid_MouseOverNotification(ByVal sender As Object, _
    ByVal e As CustomControls.CellSelectedEventArgs) _
    Handles objGanttGrid.MouseOverNotification
  objGanttGrid.SetToolTip(objToolTip, "(" & e.Row & "," & e.Column & ")")
End Sub

如您所见,在 `Init` 函数中,我们只是调用了一个名为 `CreateMyToolTip` 的函数,并启用了关于鼠标指针所在的单元格的通知。事件处理程序简单地使用行号和列号设置工具提示文本。

另一个可以添加的简单功能是拖放。`GanttGrid` 类公开了一个名为 `CellDragDrop` 的事件,可用于在相同类型的项目单元格之间实现复制功能。拖放功能必须使用鼠标右键,因为鼠标左键已用于修改单元格值。以下代码显示了如何实现此功能

Private Sub objGanttGrid_CellDragDrop(ByVal Source As Object,  _
    ByVal Args As GanttLib.CellDragDropEventArgs) _
    Handles objGanttGrid.CellDragDrop
objGanttData.DataTable.Rows(Args.Destination.Row).Item(Args.Destination.Column) _
 = objGanttData.DataTable.Rows(Args.Source.Row).Item(Args.Source.Column)
End Sub

操作 Gantt 数据

读取和写入项目数据以将值保存在数据库中或用有意义的数据初始化 Gantt 图表可能会很有用。要完全理解用于操作数据的方法,我们需要检查 `GanttLib` 实现的一些底层细节。

`GanttGrid` 主要是一个从 `DataGridEx` 派生的类,并使用数据绑定来显示其数据。`GanttData` 是一个继承自 `System.ComponentModel.Component` 的类。这是必要的,因为该类必须在 Visual Studio 工具箱中可见。主要地,该类包含一个 `DataSet`,用于与 `DataGridEx` 进行数据绑定。该类有一个无参构造函数,用于创建一个没有关联数据的空类。还有一个构造函数初始化 `FixedFieldDefinitions`、`ItemFieldDefinitions` 和 `ItemNames`。此构造函数创建 `DataSet` 并使用其数据进行初始化。此操作通过调用私有方法 `MakeDataSet` 来完成。如果您调用无参构造函数,则创建 `DataSet` 所需的任何初始化值都未准备好,因此此构造函数不调用 `MakeDataSet`。在这种情况下,当您通过 `FixedFieldDefinitions`、`ItemFieldDefinitions` 和 `ItemNames` 属性设置数据时,会调用 `MakeDataSet`。如果所有数据都已准备好,则会创建 `DataSet` 和 `DataTable`,否则 `MakeDataSet` 方法将简单地返回。一旦 `DataSet` 准备好,将触发 `DataSetReady` 事件。

当您在 `GanttGrid` 对象中设置 `GanttData` 属性时,这会导致创建一个 `TableStyle`,并将 `DataSet` 中的 `DataTable` 与 `GanttGrid` 进行数据绑定。这样,`GanttData` 对象中的数据就可以通过网格可见。

主要地,您可以通过 `GanttGrid` 对象的 `GetTable` 方法获取用于数据绑定的 `DataTable` 对象的引用。通过这种方式,您可以以传统方式操作数据。这种方法是可能的,但很容易出错。建议的操作数据的方法是通过 `GanttData` 对象的一些属性。

下表总结了最有用的方法

方法 描述
Public Property ItemNames() As String() 获取或设置用于创建 `DataSet` 的项目名称
Public ReadOnly Property ItemName(ByVal ItemNumber As Integer) As String 根据索引获取项目名称
Public ReadOnly Property NumberOfItems() As Integer 返回项目集合中的项目数量
Public Property ModifiedFlag() As Boolean 获取或设置一个布尔值,指示 `DataTable` 中数据的修改状态
Public Property FixedFieldDefinitions() As FixedColumnDefinition() 获取或设置用于创建 `DataSet` 的固定字段定义
Public Property ItemFieldDefinitions() As ItemColumnDefinition() 获取或设置用于创建 `DataSet` 的项目字段定义
Public ReadOnly Property NumberOfFixedFields() As Integer 返回固定字段的数量
Public ReadOnly Property NumberOfFieldsForItem() As Int32 返回项目字段的数量
Public Property FixedFieldValue(ByVal FieldName As String, ByVal Row As Integer) As Object 返回指定固定 `FieldName` 在给定 `Row` 下的表值
Public Property FieldValue(ByVal ItemNumber As Integer, ByVal FieldName As String, ByVal Row As Integer) As Object 返回指定 `Row` 下,给定 `ItemNumber` 和 `FieldName` 的表值

下面的代码显示了如何随机初始化 Gantt 图表。

Public Sub New()
  ...
  InitData()
End Sub 
Private Sub InitData()
  Dim nr As Integer
  Dim r As Integer
  Dim rnd As New System.Random(DateTime.Now.Millisecond)
  Dim b As Boolean
  nr = objGanttData.DataTable.Rows.Count - 1
  For r = 0 To nr
    b = IIf(rnd.Next(0, 1000) Mod 2 = 0, True, False)
    objGanttData.FieldValue(0, "Programmed", r) = b
    b = IIf(rnd.Next(0, 1000) Mod 2 = 0, True, False)
    objGanttData.FieldValue(0, "Potential", r) = b
    b = IIf(rnd.Next(0, 1000) Mod 2 = 0, True, False)
    objGanttData.FieldValue(0, "Maintenance", r) = b
    objGanttData.FieldValue(0, "Counter", r) = r
    b = IIf(rnd.Next(0, 1000) Mod 2 = 0, True, False)
    objGanttData.FieldValue(1, "Programmed", r) = b
    b = IIf(rnd.Next(0, 1000) Mod 2 = 0, True, False)
    objGanttData.FieldValue(1, "Potential", r) = b
    b = IIf(rnd.Next(0, 1000) Mod 2 = 0, True, False)
    objGanttData.FieldValue(1, "Maintenance", r) = b
    objGanttData.FieldValue(1, "Counter", r) = r
  Next
End Sub

最终注释

如果您想了解有关此库使用的 API 的更多详细信息,我唯一能建议的是仔细查阅 MSDN。您可以在此处找到有关 .NET Framework 类的所有详细信息。

另一个注意事项是可以通过 Visual Studio 设计器完全创建简单的 Gantt 图表。您可以通过图形编辑器完全创建简单的图表,插入 `ItemNames`、`FixedFieldDefinitions` 和 `ItemFieldDefinitions` 的值。

发送电子邮件给我 以获取升级、问题、发现的错误等。谢谢。

© . All rights reserved.