打造WPF项目管理工具:甘特图(Gantt Chart)实战指南
引言:项目管理的可视化痛点与解决方案
你是否还在为跟踪WPF项目进度而烦恼?Excel表格难以直观展示任务依赖关系,传统时间线工具缺乏交互性,团队协作时进度更新滞后?本文将使用HandyControl控件库,从零构建一个功能完备的甘特图(Gantt Chart)组件,帮助开发者在WPF应用中实现专业级项目进度可视化。
读完本文你将掌握:
- 甘特图核心数据结构设计与任务依赖表达
- 使用Canvas与自定义Panel实现时间轴与任务条渲染
- 基于HandyControl扩展控件实现交互功能(拖拽、缩放、进度更新)
- 完整项目管理工具的整合方案与性能优化策略
甘特图核心原理与架构设计
甘特图(Gantt Chart)概念解析
甘特图是一种水平条形图(Horizontal Bar Chart),由亨利·L·甘特(Henry L. Gantt)在1917年发明,用于展示项目进度与任务时间关系。在WPF环境中,甘特图通常包含以下核心元素:
核心技术选型与项目结构
基于HandyControl控件库,我们将采用以下技术组合实现甘特图:
| 功能模块 | 技术实现 | HandyControl组件支持 |
|---|---|---|
| 时间轴渲染 | Canvas + 自定义绘制 | ScrollViewer、Watermark |
| 任务行布局 | 自定义Panel(GanttPanel) | RelativePanel、SimplePanel |
| 任务条控件 | 自定义UserControl | Badge、ProgressButton |
| 交互功能 | 拖拽行为(Interaction) | DragElementBehavior |
| 数据管理 | ObservableCollection | - |
| 样式主题 | ResourceDictionary | Theme、Brushes |
数据模型设计:任务与时间轴的核心结构
GanttTask数据模型实现
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Windows.Media;
namespace HandyControlDemo.ViewModel
{
public enum TaskStatus
{
NotStarted,
InProgress,
Completed,
Delayed,
OnHold
}
public class GanttTask : INotifyPropertyChanged
{
private string _id;
private string _name;
private DateTime _start;
private DateTime _end;
private double _progress;
private string _resource;
private TaskStatus _status;
private Brush _color;
public string Id
{
get => _id;
set { _id = value; OnPropertyChanged(); }
}
public string Name
{
get => _name;
set { _name = value; OnPropertyChanged(); }
}
public DateTime Start
{
get => _start;
set { _start = value; OnPropertyChanged(); OnPropertyChanged(nameof(Duration)); }
}
public DateTime End
{
get => _end;
set { _end = value; OnPropertyChanged(); OnPropertyChanged(nameof(Duration)); }
}
public double Duration => (End - Start).TotalDays;
public double Progress
{
get => _progress;
set { _progress = Math.Clamp(value, 0, 1); OnPropertyChanged(); }
}
public string Resource
{
get => _resource;
set { _resource = value; OnPropertyChanged(); }
}
public ObservableCollection<string> Dependencies { get; set; } = new();
public TaskStatus Status
{
get => _status;
set { _status = value; OnPropertyChanged(); OnPropertyChanged(nameof(Color)); }
}
public Brush Color
{
get => _color ?? GetDefaultColor();
set { _color = value; OnPropertyChanged(); }
}
private Brush GetDefaultColor()
{
return Status switch
{
TaskStatus.InProgress => Brushes.DodgerBlue,
TaskStatus.Completed => Brushes.LimeGreen,
TaskStatus.Delayed => Brushes.OrangeRed,
TaskStatus.OnHold => Brushes.Gray,
_ => Brushes.LightGray
};
}
public event PropertyChangedEventHandler PropertyChanged;
protected virtual void OnPropertyChanged(string propertyName = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
}
时间轴(TimeAxis)计算引擎
时间轴是甘特图的核心基础设施,负责将日期时间映射为画布坐标:
public class TimeAxis
{
public enum TimeUnit { Day, Week, Month, Quarter, Year }
public TimeUnit Unit { get; set; } = TimeUnit.Day;
public double CellWidth { get; set; } = 60; // 每个时间单位的像素宽度
public DateTime StartDate { get; private set; }
public DateTime EndDate { get; private set; }
public List<DateTime> MajorTicks { get; } = new();
public List<DateTime> MinorTicks { get; } = new();
public void UpdateRange(DateTime start, DateTime end, double viewportWidth)
{
StartDate = start;
EndDate = end;
// 自动调整时间单位以适应视图宽度
var totalDays = (end - start).TotalDays;
if (totalDays > 365 && viewportWidth / totalDays < 0.5)
Unit = TimeUnit.Month;
else if (totalDays > 60 && viewportWidth / totalDays < 2)
Unit = TimeUnit.Week;
else
Unit = TimeUnit.Day;
CalculateTicks();
}
public void CalculateTicks()
{
MajorTicks.Clear();
MinorTicks.Clear();
var current = StartDate.Date;
while (current <= EndDate)
{
MajorTicks.Add(current);
// 添加次要刻度
switch (Unit)
{
case TimeUnit.Month:
AddWeekTicks(current);
break;
case TimeUnit.Week:
AddDailyTicks(current, 7);
break;
case TimeUnit.Day:
AddHourTicks(current);
break;
}
current = AddUnit(current);
}
}
// 日期到像素位置的转换
public double GetPosition(DateTime date)
{
var totalUnits = Unit switch
{
TimeUnit.Day => (EndDate - StartDate).TotalDays,
TimeUnit.Week => (EndDate - StartDate).TotalDays / 7,
TimeUnit.Month => (EndDate.Year - StartDate.Year) * 12 + EndDate.Month - StartDate.Month,
_ => (EndDate - StartDate).TotalDays
};
var dateValue = Unit switch
{
TimeUnit.Day => (date - StartDate).TotalDays,
TimeUnit.Week => (date - StartDate).TotalDays / 7,
TimeUnit.Month => (date.Year - StartDate.Year) * 12 + date.Month - StartDate.Month,
_ => (date - StartDate).TotalDays
};
return (dateValue / totalUnits) * (CellWidth * GetUnitsPerMajorTick());
}
// 像素位置到日期的转换
public DateTime GetDate(double position)
{
var totalWidth = CellWidth * GetUnitsPerMajorTick();
var ratio = position / totalWidth;
var totalSpan = EndDate - StartDate;
return StartDate.AddTicks((long)(totalSpan.Ticks * ratio));
}
private DateTime AddUnit(DateTime date) => Unit switch
{
TimeUnit.Day => date.AddDays(1),
TimeUnit.Week => date.AddDays(7),
TimeUnit.Month => date.AddMonths(1),
TimeUnit.Quarter => date.AddMonths(3),
TimeUnit.Year => date.AddYears(1),
_ => date.AddDays(1)
};
private int GetUnitsPerMajorTick() => Unit switch
{
TimeUnit.Day => 1,
TimeUnit.Week => 7,
TimeUnit.Month => DateTime.DaysInMonth(StartDate.Year, StartDate.Month),
_ => 1
};
// 辅助方法:添加次要刻度
private void AddDailyTicks(DateTime start, int count)
{
for (var i = 1; i < count; i++)
{
MinorTicks.Add(start.AddDays(i));
}
}
private void AddWeekTicks(DateTime monthStart)
{
var monthEnd = monthStart.AddMonths(1).AddDays(-1);
var firstDay = monthStart;
while (firstDay.DayOfWeek != DayOfWeek.Monday) firstDay = firstDay.AddDays(1);
var current = firstDay;
while (current < monthEnd)
{
MinorTicks.Add(current);
current = current.AddDays(7);
}
}
private void AddHourTicks(DateTime day)
{
for (var i = 1; i < 24; i += 6) // 每6小时一个次要刻度
{
MinorTicks.Add(day.AddHours(i));
}
}
}
甘特图控件实现:从画布到交互
自定义GanttPanel布局面板
基于HandyControl的Panel扩展能力,实现甘特图的任务布局:
using System.Windows;
using System.Windows.Controls;
using HandyControl.Controls;
public class GanttPanel : Panel
{
public static readonly DependencyProperty RowHeightProperty = DependencyProperty.Register(
nameof(RowHeight), typeof(double), typeof(GanttPanel),
new PropertyMetadata(40.0, OnRowHeightChanged));
public static readonly DependencyProperty TimeAxisProperty = DependencyProperty.Register(
nameof(TimeAxis), typeof(TimeAxis), typeof(GanttPanel),
new PropertyMetadata(null, OnTimeAxisChanged));
public double RowHeight
{
get => (double)GetValue(RowHeightProperty);
set => SetValue(RowHeightProperty, value);
}
public TimeAxis TimeAxis
{
get => (TimeAxis)GetValue(TimeAxisProperty);
set => SetValue(TimeAxisProperty, value);
}
private static void OnRowHeightChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
((GanttPanel)d).InvalidateArrange();
}
private static void OnTimeAxisChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
((GanttPanel)d).InvalidateMeasure();
}
protected override Size MeasureOverride(Size availableSize)
{
var totalHeight = Children.Count * RowHeight;
var totalWidth = TimeAxis?.CellWidth * 30 ?? availableSize.Width; // 默认30个时间单位宽度
foreach (var child in Children)
{
if (child is FrameworkElement element && element.DataContext is GanttTask task)
{
// 测量任务条宽度
var taskWidth = TimeAxis.GetPosition(task.End) - TimeAxis.GetPosition(task.Start);
element.Measure(new Size(taskWidth, RowHeight - 8)); // 留出边距
}
else
{
child.Measure(availableSize);
}
}
return new Size(totalWidth, totalHeight);
}
protected override Size ArrangeOverride(Size finalSize)
{
if (TimeAxis == null) return finalSize;
for (var i = 0; i < Children.Count; i++)
{
var child = Children[i];
if (child is FrameworkElement element && element.DataContext is GanttTask task)
{
var x = TimeAxis.GetPosition(task.Start);
var width = TimeAxis.GetPosition(task.End) - x;
var y = i * RowHeight + 4; // 顶部边距
element.Arrange(new Rect(x, y, width, RowHeight - 8));
}
else
{
child.Arrange(new Rect(0, i * RowHeight, finalSize.Width, RowHeight));
}
}
return finalSize;
}
}
甘特图主控件XAML实现
结合Canvas和GanttPanel,构建完整的甘特图视图:
<UserControl x:Class="HandyControlDemo.UserControl.GanttChartControl"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:hc="https://handyorg.github.io/handycontrol"
xmlns:local="clr-namespace:HandyControlDemo.UserControl"
xmlns:vm="clr-namespace:HandyControlDemo.ViewModel"
DataContext="{Binding GanttViewModel, Source={StaticResource Locator}}">
<Grid>
<!-- 头部时间轴 -->
<Grid.RowDefinitions>
<RowDefinition Height="60"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<!-- 时间轴头部 -->
<ScrollViewer Grid.Row="0" HorizontalScrollBarVisibility="Hidden" VerticalScrollBarVisibility="Disabled">
<Canvas x:Name="TimeAxisCanvas" Height="60">
<!-- 主要刻度线和标签 -->
<ItemsControl ItemsSource="{Binding TimeAxis.MajorTicks}">
<ItemsControl.ItemTemplate>
<DataTemplate DataType="{x:Type sys:DateTime}">
<StackPanel Orientation="Vertical"
Canvas.Left="{Binding Path=., Converter={StaticResource TimeToXConverter}}">
<Line Y1="0" Y2="10" Stroke="Gray" StrokeThickness="1"/>
<TextBlock Text="{Binding StringFormat='MM/dd'}" Margin="0,10,0,0"
HorizontalAlignment="Center"/>
</StackPanel>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
<!-- 次要刻度线 -->
<ItemsControl ItemsSource="{Binding TimeAxis.MinorTicks}">
<ItemsControl.ItemTemplate>
<DataTemplate DataType="{x:Type sys:DateTime}">
<Line Canvas.Left="{Binding Path=., Converter={StaticResource TimeToXConverter}}"
Y1="0" Y2="5" Stroke="LightGray" StrokeThickness="1"/>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
<!-- 今天标记线 -->
<Line Canvas.Left="{Binding TodayPosition}" Y1="0" Y2="60"
Stroke="Red" StrokeThickness="2" StrokeDashArray="4 2"/>
</Canvas>
</ScrollViewer>
<!-- 主内容区域 -->
<ScrollViewer Grid.Row="1" HorizontalScrollBarVisibility="Auto" VerticalScrollBarVisibility="Auto"
x:Name="MainScrollViewer" ScrollChanged="OnScrollChanged">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="200"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<!-- 任务名称列 -->
<Border Grid.Column="0" Background="#F5F5F5" BorderThickness="0,0,1,0" BorderBrush="LightGray">
<ItemsControl ItemsSource="{Binding Tasks}">
<ItemsControl.ItemTemplate>
<DataTemplate DataType="{x:Type vm:GanttTask}">
<Grid Height="{Binding RowHeight, Source={StaticResource GanttPanel}}">
<Border BorderThickness="0,0,0,1" BorderBrush="LightGray">
<StackPanel Margin="10">
<TextBlock Text="{Binding Name}" FontWeight="Medium"/>
<TextBlock Text="{Binding Resource}" FontSize="12" Foreground="Gray"/>
</StackPanel>
</Border>
</Grid>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</Border>
<!-- 甘特图任务区域 -->
<local:GanttPanel Grid.Column="1" x:Name="GanttPanel"
Tasks="{Binding Tasks}" TimeAxis="{Binding TimeAxis}"
RowHeight="60">
<!-- 背景网格线 -->
<Canvas>
<!-- 水平线 -->
<ItemsControl ItemsSource="{Binding Tasks}">
<ItemsControl.ItemTemplate>
<DataTemplate>
<Line Y1="{Binding Path=Index, Converter={StaticResource RowToYConverter}}"
Y2="{Binding Path=Index, Converter={StaticResource RowToYConverter}}"
X2="{Binding MaxWidth}" Stroke="LightGray" StrokeThickness="1"/>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
<!-- 垂直线 (绑定到次要刻度) -->
<ItemsControl ItemsSource="{Binding TimeAxis.MinorTicks}">
<ItemsControl.ItemTemplate>
<DataTemplate>
<Line X1="{Binding Path=., Converter={StaticResource TimeToXConverter}}"
X2="{Binding Path=., Converter={StaticResource TimeToXConverter}}"
Y2="{Binding TotalHeight}" Stroke="LightGray" StrokeThickness="0.5"/>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</Canvas>
<!-- 任务条 -->
<ItemsControl ItemsSource="{Binding Tasks}">
<ItemsControl.ItemTemplate>
<DataTemplate DataType="{x:Type vm:GanttTask}">
<local:GanttTaskBar Task="{Binding}"
DragDelta="OnTaskBarDragDelta"
ProgressChanged="OnTaskProgressChanged"/>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
<!-- 依赖关系线 -->
<Canvas>
<ItemsControl ItemsSource="{Binding DependencyLines}">
<ItemsControl.ItemTemplate>
<DataTemplate DataType="{x:Type vm:DependencyLine}">
<Path Data="{Binding Path=., Converter={StaticResource DependencyToPathConverter}}"
Stroke="Gray" StrokeThickness="1.5" StrokeDashArray="5 3"/>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</Canvas>
</local:GanttPanel>
</Grid>
</ScrollViewer>
</Grid>
</UserControl>
任务条控件与交互实现
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;
using System.Windows.Media;
using HandyControl.Controls;
using HandyControlDemo.ViewModel;
public class GanttTaskBar : UserControl
{
public static readonly DependencyProperty TaskProperty = DependencyProperty.Register(
nameof(Task), typeof(GanttTask), typeof(GanttTaskBar),
new FrameworkPropertyMetadata(OnTaskChanged));
public static readonly DependencyProperty ProgressProperty = DependencyProperty.Register(
nameof(Progress), typeof(double), typeof(GanttTaskBar),
new FrameworkPropertyMetadata(0.0, FrameworkPropertyMetadataOptions.BindsTwoWayByDefault));
public GanttTask Task
{
get => (GanttTask)GetValue(TaskProperty);
set => SetValue(TaskProperty, value);
}
public double Progress
{
get => (double)GetValue(ProgressProperty);
set => SetValue(ProgressProperty, value);
}
public event RoutedEventHandler ProgressChanged;
public event DragDeltaEventHandler DragDelta;
private Border _taskBorder;
private Grid _progressGrid;
private Thumb _dragThumb;
private Thumb _progressThumb;
public GanttTaskBar()
{
DefaultStyleKey = typeof(GanttTaskBar);
Loaded += OnLoaded;
}
private static void OnTaskChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
var control = (GanttTaskBar)d;
var task = (GanttTask)e.NewValue;
control.Progress = task.Progress;
}
private void OnLoaded(object sender, RoutedEventArgs e)
{
_taskBorder = GetTemplateChild("TaskBorder") as Border;
_progressGrid = GetTemplateChild("ProgressGrid") as Grid;
_dragThumb = GetTemplateChild("DragThumb") as Thumb;
_progressThumb = GetTemplateChild("ProgressThumb") as Thumb;
if (_dragThumb != null)
{
_dragThumb.DragDelta += (s, e) => DragDelta?.Invoke(this, e);
}
if (_progressThumb != null)
{
_progressThumb.DragDelta += OnProgressDragDelta;
}
UpdateVisualState();
}
private void OnProgressDragDelta(object sender, DragDeltaEventArgs e)
{
// 计算新进度
var parent = VisualTreeHelper.GetParent(this) as Canvas;
if (parent == null) return;
var taskWidth = _taskBorder.ActualWidth;
if (taskWidth <= 0) return;
// 计算进度(相对于任务条宽度的比例)
var delta = e.HorizontalChange / taskWidth;
Progress = Math.Clamp(Progress + delta, 0, 1);
// 更新进度条宽度
_progressGrid.Width = _taskBorder.ActualWidth * Progress;
// 触发进度变化事件
ProgressChanged?.Invoke(this, new RoutedEventArgs());
}
private void UpdateVisualState()
{
if (_taskBorder == null || Task == null) return;
// 设置任务条颜色
_taskBorder.Background = Task.Color;
// 设置进度条颜色(比任务条颜色深一些)
if (_progressGrid != null && _progressGrid.Children.Count > 0
&& _progressGrid.Children[0] is Border progressBorder)
{
progressBorder.Background = new SolidColorBrush(
Color.FromArgb(
255,
(byte)(Task.Color.GetValue(Brush.ColorProperty).R * 0.8),
(byte)(Task.Color.GetValue(Brush.ColorProperty).G * 0.8),
(byte)(Task.Color.GetValue(Brush.ColorProperty).B * 0.8)
)
);
}
}
public override void OnApplyTemplate()
{
base.OnApplyTemplate();
UpdateVisualState();
}
}
甘特图样式与主题整合
HandyControl主题适配
通过HandyControl的资源字典,使甘特图融入整体应用风格:
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:HandyControlDemo.UserControl">
<Style TargetType="local:GanttTaskBar">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="local:GanttTaskBar">
<Border x:Name="TaskBorder"
Background="{Binding Task.Color}"
CornerRadius="3"
BorderBrush="Gray"
BorderThickness="1"
Height="30"
Margin="2">
<Grid x:Name="ProgressGrid" HorizontalAlignment="Left" Background="{Binding Task.Color, Converter={StaticResource DarkenBrushConverter}}">
<StackPanel Orientation="Horizontal" Margin="5">
<TextBlock Text="{Binding Task.Name}" Foreground="White"
VerticalAlignment="Center" MaxWidth="{Binding ActualWidth, RelativeSource={RelativeSource AncestorType=Border}}"/>
</StackPanel>
<Thumb x:Name="ProgressThumb" Width="10" HorizontalAlignment="Right"
Background="White" BorderBrush="Gray" BorderThickness="1"
Cursor="Hand">
<Thumb.Style>
<Style TargetType="Thumb">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="Thumb">
<Border Background="{TemplateBinding Background}"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}"
CornerRadius="1"/>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</Thumb.Style>
</Thumb>
</Grid>
<Thumb x:Name="DragThumb" Background="Transparent"/>
</Border>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
<!-- 时间到X坐标的转换器 -->
<local:TimeToXConverter x:Key="TimeToXConverter"/>
<!-- 行索引到Y坐标的转换器 -->
<local:RowToYConverter x:Key="RowToYConverter"/>
<!-- 依赖关系到路径的转换器 -->
<local:DependencyToPathConverter x:Key="DependencyToPathConverter"/>
<!-- 颜色加深转换器 -->
<local:DarkenBrushConverter x:Key="DarkenBrushConverter"/>
</ResourceDictionary>
交互逻辑实现
// 甘特图用户控件的后台代码
public partial class GanttChartControl : UserControl
{
public GanttChartControl()
{
InitializeComponent();
DataContextChanged += OnDataContextChanged;
}
private void OnDataContextChanged(object sender, DependencyPropertyChangedEventArgs e)
{
if (DataContext is GanttViewModel vm)
{
vm.PropertyChanged += (s, args) =>
{
if (args.PropertyName == nameof(vm.TimeAxis))
{
vm.TimeAxis.MajorTicks.CollectionChanged += (s, e) => UpdateTimeAxis();
vm.TimeAxis.MinorTicks.CollectionChanged += (s, e) => UpdateTimeAxis();
UpdateTimeAxis();
}
};
}
}
private void UpdateTimeAxis()
{
TimeAxisCanvas.Width = GanttPanel.DesiredSize.Width;
TodayPosition = GanttPanel.TimeAxis.GetPosition(DateTime.Today);
}
private void OnScrollChanged(object sender, ScrollChangedEventArgs e)
{
// 同步时间轴和任务区域的水平滚动
TimeAxisCanvas.RenderTransform = new TranslateTransform(-MainScrollViewer.HorizontalOffset, 0);
}
private void OnTaskBarDragDelta(object sender, DragDeltaEventArgs e)
{
var taskBar = (GanttTaskBar)sender;
var task = taskBar.Task;
if (task == null) return;
// 计算时间偏移量(像素到天数的转换)
var daysPerPixel = (GanttPanel.TimeAxis.EndDate - GanttPanel.TimeAxis.StartDate).TotalDays /
GanttPanel.TimeAxis.CellWidth;
var dayOffset = e.HorizontalChange * daysPerPixel;
// 更新任务时间
task.Start = task.Start.AddDays(dayOffset);
task.End = task.End.AddDays(dayOffset);
// 刷新布局
GanttPanel.InvalidateArrange();
}
private void OnTaskProgressChanged(object sender, RoutedEventArgs e)
{
var taskBar = (GanttTaskBar)sender;
var task = taskBar.Task;
if (task != null)
{
task.Progress = taskBar.Progress;
// 根据进度自动更新状态
if (task.Progress >= 1)
task.Status = TaskStatus.Completed;
else if (task.Progress > 0)
task.Status = TaskStatus.InProgress;
}
}
public double TodayPosition { get; set; }
}
功能扩展与性能优化
高级功能实现
1. 任务依赖关系管理
public class DependencyManager
{
private readonly ObservableCollection<GanttTask> _tasks;
private readonly ObservableCollection<DependencyLine> _dependencyLines;
private readonly TimeAxis _timeAxis;
private double _rowHeight;
public DependencyManager(ObservableCollection<GanttTask> tasks,
ObservableCollection<DependencyLine> dependencyLines,
TimeAxis timeAxis, double rowHeight)
{
_tasks = tasks;
_dependencyLines = dependencyLines;
_timeAxis = timeAxis;
_rowHeight = rowHeight;
_tasks.CollectionChanged += OnTasksChanged;
foreach (var task in _tasks)
{
task.PropertyChanged += OnTaskPropertyChanged;
task.Dependencies.CollectionChanged += (s, e) => UpdateDependencies();
}
}
private void OnTasksChanged(object sender, NotifyCollectionChangedEventArgs e)
{
if (e.NewItems != null)
{
foreach (GanttTask task in e.NewItems)
{
task.PropertyChanged += OnTaskPropertyChanged;
task.Dependencies.CollectionChanged += (s, e) => UpdateDependencies();
}
}
if (e.OldItems != null)
{
foreach (GanttTask task in e.OldItems)
{
task.PropertyChanged -= OnTaskPropertyChanged;
task.Dependencies.CollectionChanged -= (s, e) => UpdateDependencies();
}
}
UpdateDependencies();
}
private void OnTaskPropertyChanged(object sender, PropertyChangedEventArgs e)
{
if (e.PropertyName is nameof(GanttTask.Start) or nameof(GanttTask.End))
{
UpdateDependencies();
}
}
public void UpdateDependencies()
{
_dependencyLines.Clear();
for (var i = 0; i < _tasks.Count; i++)
{
var task = _tasks[i];
foreach (var dependencyId in task.Dependencies)
{
var predecessor = _tasks.FirstOrDefault(t => t.Id == dependencyId);
if (predecessor == null) continue;
var predecessorIndex = _tasks.IndexOf(predecessor);
if (predecessorIndex == -1) continue;
// 创建依赖线对象
_dependencyLines.Add(new DependencyLine
{
StartTask = predecessor,
EndTask = task,
StartRow = predecessorIndex,
EndRow = i
});
}
}
}
}
// 依赖线数据结构
public class DependencyLine
{
public GanttTask StartTask { get; set; }
public GanttTask EndTask { get; set; }
public int StartRow { get; set; }
public int EndRow { get; set; }
}
// 依赖线到路径的转换器
public class DependencyToPathConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
var line = (DependencyLine)value;
if (line.StartTask == null || line.EndTask == null) return null;
// 获取甘特图面板
var ganttPanel = parameter as GanttPanel;
if (ganttPanel == null) return null;
// 计算开始点(前置任务的结束点)
var startX = ganttPanel.TimeAxis.GetPosition(line.StartTask.End);
var startY = line.StartRow * ganttPanel.RowHeight + ganttPanel.RowHeight / 2;
// 计算结束点(当前任务的开始点)
var endX = ganttPanel.TimeAxis.GetPosition(line.EndTask.Start);
var endY = line.EndRow * ganttPanel.RowHeight + ganttPanel.RowHeight / 2;
// 创建贝塞尔曲线路径
var pathGeometry = new PathGeometry();
var pathFigure = new PathFigure { StartPoint = new Point(startX, startY) };
// 添加贝塞尔曲线段
var controlPoint1X = startX + Math.Abs(endX - startX) / 3;
var controlPoint2X = endX - Math.Abs(endX - startX) / 3;
pathFigure.Segments.Add(new BezierSegment(
new Point(controlPoint1X, startY),
new Point(controlPoint2X, endY),
new Point(endX, endY),
true));
pathGeometry.Figures.Add(pathFigure);
return pathGeometry;
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
}
2. 甘特图缩放控制
public class ZoomController
{
private readonly TimeAxis _timeAxis;
private double _currentZoom = 1.0;
private readonly double _minZoom = 0.5;
private readonly double _maxZoom = 3.0;
private readonly double _zoomStep = 0.2;
public ZoomController(TimeAxis timeAxis)
{
_timeAxis = timeAxis;
}
public double ZoomLevel
{
get => _currentZoom;
set
{
_currentZoom = Math.Clamp(value, _minZoom, _maxZoom);
UpdateZoom();
}
}
public void ZoomIn() => ZoomLevel += _zoomStep;
public void ZoomOut() => ZoomLevel -= _zoomStep;
public void ResetZoom() => ZoomLevel = 1.0;
private void UpdateZoom()
{
// 调整时间轴的单元格宽度
_timeAxis.CellWidth = 60 * _currentZoom;
// 刷新时间轴
_timeAxis.CalculateTicks();
}
// 支持鼠标滚轮缩放
public void HandleMouseWheel(MouseWheelEventArgs e)
{
if (e.Delta > 0)
ZoomIn();
else
ZoomOut();
}
}
性能优化策略
1. 数据虚拟化
对于大型项目(超过100个任务),使用HandyControl的VirtualizingPanel实现数据虚拟化:
<!-- 优化的任务列表控件 -->
<ItemsControl ItemsSource="{Binding Tasks}">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<hc:VirtualizingPanel IsVirtualizing="True"
VirtualizationMode="Recycling"
CacheLengthUnit="Item"
CacheLength="10"/>
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<!-- ItemTemplate 保持不变 -->
</ItemsControl>
2. 按需加载与缓存
public class GanttDataService
{
private readonly ObservableCollection<GanttTask> _tasks;
private readonly IProjectRepository _repository;
private bool _isLoading;
private DateTime _lastLoadDate;
private readonly TimeSpan _cacheDuration = TimeSpan.FromMinutes(5);
public GanttDataService(ObservableCollection<GanttTask> tasks, IProjectRepository repository)
{
_tasks = tasks;
_repository = repository;
}
public async Task LoadTasksAsync(string projectId, DateTime startDate, DateTime endDate)
{
// 检查缓存
if (DateTime.Now - _lastLoadDate < _cacheDuration && _tasks.Count > 0)
return;
if (_isLoading) return;
_isLoading = true;
try
{
// 显示加载状态
_tasks.Clear();
_tasks.Add(new GanttTask { Name = "Loading tasks..." });
// 异步加载数据
var tasks = await _repository.GetTasksAsync(projectId, startDate, endDate);
// 更新UI
_tasks.Clear();
foreach (var task in tasks)
{
_tasks.Add(task);
}
_lastLoadDate = DateTime.Now;
}
catch (Exception ex)
{
_tasks.Clear();
_tasks.Add(new GanttTask { Name = $"Error loading tasks: {ex.Message}" });
}
finally
{
_isLoading = false;
}
}
// 实现增量加载
public async Task LoadMoreTasksAsync(DateTime newEndDate)
{
if (_isLoading) return;
_isLoading = true;
try
{
var lastTask = _tasks.LastOrDefault();
if (lastTask == null) return;
var newTasks = await _repository.GetTasksAsync(
projectId: "current",
startDate: lastTask.End,
endDate: newEndDate);
foreach (var task in newTasks)
{
_tasks.Add(task);
}
}
finally
{
_isLoading = false;
}
}
}
项目整合与应用场景
ViewModel整合
public class GanttViewModel : ViewModelBase
{
private readonly TimeAxis _timeAxis = new();
private readonly ObservableCollection<GanttTask> _tasks = new();
private readonly ObservableCollection<DependencyLine> _dependencyLines = new();
private readonly DependencyManager _dependencyManager;
private readonly ZoomController _zoomController;
private readonly GanttDataService _dataService;
private string _selectedProjectId;
private bool _isLoading;
public GanttViewModel()
{
_dependencyManager = new DependencyManager(_tasks, _dependencyLines, _timeAxis, RowHeight);
_zoomController = new ZoomController(_timeAxis);
_dataService = new GanttDataService(_tasks, new ProjectRepository());
// 初始化时间轴(显示当前日期前后30天)
var today = DateTime.Today;
_timeAxis.UpdateRange(today.AddDays(-15), today.AddDays(45), 1000);
// 命令初始化
LoadSampleDataCommand = new RelayCommand(LoadSampleData);
ZoomInCommand = new RelayCommand(() => _zoomController.ZoomIn());
ZoomOutCommand = new RelayCommand(() => _zoomController.ZoomOut());
ResetZoomCommand = new RelayCommand(() => _zoomController.ResetZoom());
ScrollToTodayCommand = new RelayCommand(ScrollToToday);
// 加载示例数据
LoadSampleData();
}
public ICommand LoadSampleDataCommand { get; }
public ICommand ZoomInCommand { get; }
public ICommand ZoomOutCommand { get; }
public ICommand ResetZoomCommand { get; }
public ICommand ScrollToTodayCommand { get; }
public TimeAxis TimeAxis => _timeAxis;
public ObservableCollection<GanttTask> Tasks => _tasks;
public ObservableCollection<DependencyLine> DependencyLines => _dependencyLines;
public double RowHeight { get; set; } = 60;
public bool IsLoading
{
get => _isLoading;
set => Set(ref _isLoading, value);
}
public void LoadSampleData()
{
_tasks.Clear();
var today = DateTime.Today;
// 添加示例任务
var task1 = new GanttTask
{
Id = "T1",
Name = "需求分析",
Start = today.AddDays(-10),
End = today.AddDays(-5),
Progress = 1,
Resource = "产品组",
Status = TaskStatus.Completed
};
var task2 = new GanttTask
{
Id = "T2",
Name = "UI设计",
Start = today.AddDays(-8),
End = today.AddDays(2),
Progress = 0.8,
Resource = "设计组",
Status = TaskStatus.InProgress
};
task2.Dependencies.Add("T1");
var task3 = new GanttTask
{
Id = "T3",
Name = "架构设计",
Start = today.AddDays(-5),
End = today.AddDays(5),
Progress = 0.6,
Resource = "架构师",
Status = TaskStatus.InProgress
};
task3.Dependencies.Add("T1");
var task4 = new GanttTask
{
Id = "T4",
Name = "前端开发",
Start = today.AddDays(1),
End = today.AddDays(15),
Progress = 0.3,
Resource = "前端组",
Status = TaskStatus.InProgress
};
task4.Dependencies.Add("T2");
var task5 = new GanttTask
{
Id = "T5",
Name = "后端开发",
Start = today.AddDays(3),
End = today.AddDays(20),
Progress = 0.2,
Resource = "后端组",
Status = TaskStatus.InProgress
};
task5.Dependencies.Add("T3");
var task6 = new GanttTask
{
Id = "T6",
Name = "测试",
Start = today.AddDays(16),
End = today.AddDays(25),
Progress = 0,
Resource = "测试组",
Status = TaskStatus.NotStarted
};
task6.Dependencies.Add("T4");
task6.Dependencies.Add("T5");
var task7 = new GanttTask
{
Id = "T7",
Name = "部署上线",
Start = today.AddDays(23),
End = today.AddDays(28),
Progress = 0,
Resource = "运维组",
Status = TaskStatus.NotStarted
};
task7.Dependencies.Add("T6");
_tasks.Add(task1);
_tasks.Add(task2);
_tasks.Add(task3);
_tasks.Add(task4);
_tasks.Add(task5);
_tasks.Add(task6);
_tasks.Add(task7);
}
public void ScrollToToday()
{
// 在视图中实现滚动到今天的逻辑
EventAggregator.Instance.SendMessage(new ScrollToTodayMessage());
}
public void HandleMouseWheel(MouseWheelEventArgs e)
{
_zoomController.HandleMouseWheel(e);
}
}
完整应用整合
// 主窗口XAML
<Window x:Class="HandyControlDemo.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:hc="https://handyorg.github.io/handycontrol"
xmlns:local="clr-namespace:HandyControlDemo"
Title="项目管理工具 - 甘特图视图" Height="800" Width="1200">
<hc:WindowChrome.AllowsTransparency="True">
<hc:WindowChrome ContentMargin="0" CornerRadius="4" GlassFrameThickness="1"/>
</hc:WindowChrome.AllowsTransparency>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<!-- 工具栏 -->
<hc:ToolBar Grid.Row="0" Background="{DynamicResource RegionBrush}">
<hc:Button GroupName="Action" Command="{Binding GanttViewModel.LoadSampleDataCommand}"
Icon="{StaticResource RefreshGeometry}" Content="刷新数据"/>
<hc:Separator Margin="8,0"/>
<hc:Button GroupName="Zoom" Command="{Binding GanttViewModel.ZoomInCommand}"
Icon="{StaticResource ZoomInGeometry}" ToolTip="放大"/>
<hc:Button GroupName="Zoom" Command="{Binding GanttViewModel.ZoomOutCommand}"
Icon="{StaticResource ZoomOutGeometry}" ToolTip="缩小"/>
<hc:Button GroupName="Zoom" Command="{Binding GanttViewModel.ResetZoomCommand}"
Icon="{StaticResource ZoomResetGeometry}" ToolTip="重置视图"/>
<hc:Separator Margin="8,0"/>
<hc:Button Command="{Binding GanttViewModel.ScrollToTodayCommand}"
Content="今日" Icon="{StaticResource CalendarGeometry}"/>
<hc:Separator Margin="8,0"/>
<hc:ComboBox Width="150" hc:InfoElement.Placeholder="选择项目">
<hc:ComboBoxItem Content="WPF组件库开发"/>
<hc:ComboBoxItem Content="移动端APP项目"/>
<hc:ComboBoxItem Content="企业网站重构"/>
</hc:ComboBox>
<hc:Separator Margin="8,0" HorizontalAlignment="Stretch"/>
<hc:Button Style="{StaticResource ButtonPrimary}" Content="导出甘特图"
Icon="{StaticResource ExportGeometry}"/>
</hc:ToolBar>
<!-- 甘特图控件 -->
<local:GanttChartControl Grid.Row="1" Margin="10"/>
</Grid>
</Window>
结论与扩展方向
本文基于HandyControl控件库实现了一个功能完备的甘特图组件,包括任务展示、进度跟踪、依赖关系管理和交互操作等核心功能。通过自定义Panel和Canvas的组合使用,我们成功构建了高性能的时间轴布局系统,并通过HandyControl的Thumb、ScrollViewer等控件实现了流畅的拖拽交互体验。
进一步扩展方向
- 资源负载视图 - 基于现有数据扩展资源分配热力图,显示团队成员工作量分布
- 关键路径分析 - 实现项目关键路径算法,高亮显示影响项目总工期的任务序列
- 多项目视图 - 支持同时显示多个项目的甘特图,实现跨项目资源协调
- 导出功能 - 添加PDF/PNG导出和Microsoft Project文件格式支持
- 时间线比较 - 实现基线计划与实际进度的对比视图,支持进度偏差分析
甘特图作为项目管理的核心工具,其实现涉及数据结构设计、可视化渲染和交互体验等多个方面。通过本文介绍的方法,开发者可以快速将这一功能集成到WPF应用中,为用户提供专业级的项目进度管理体验。
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考



