Customizing printing — GanttChartDataGrid (WPF)

DlhSoft
Gantt chart libraries
5 min readMar 18, 2024

First: the basics

Many times you need to provide printing functionality in your WPF apps, even if we liven in a modern era when paper is not really necessary anymore, e.g. to generate PDF documents on the fly.

Our WPF components from Gantt Chart Light Library do support printing in multiple ways:

First, there is the PrintingTemplate definition which you can set up at the component level in XAML, establishing the way printed pages would look like; in this example, we print page index and page count under the page content, and add some margin and border to the printed output as well:

<pdgcc:GanttChartDataGrid.PrintingTemplate>
<DataTemplate>
<Grid Margin="32">
<Grid.RowDefinitions>
<RowDefinition Height="*"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<Border Grid.Row="0" Margin="0,16" BorderBrush="DarkGray" BorderThickness="1">
<ContentPresenter Content="{Binding Content, Mode=OneTime}"/>
</Border>
<StackPanel Grid.Row="1" Orientation="Horizontal" HorizontalAlignment="Right">
<TextBlock Text="{Binding PageIndex, Mode=OneTime}"/>
<TextBlock Text="/"/>
<TextBlock Text="{Binding PageCount, Mode=OneTime}"/>
</StackPanel>
</Grid>
</DataTemplate>
</pdgcc:GanttChartDataGrid.PrintingTemplate>

Then, you can directly call the Print methods of the component, e.g.:

GanttChartDataGrid.Print("My document");

Or you can customize printing further, such as to default to landscape paper orientation, for example, using a custom PrintDialog and our built-in DocumentPaginator to provide content to print, if you wish:

var dialog = new System.Windows.Controls.PrintDialog();
dialog.PrintTicket.PageOrientation = PageOrientation.Landscape;

var documentPaginator = new DlhSoft.Windows.Controls.GanttChartDataGrid.DocumentPaginator(GanttChartDataGrid);
documentPaginator.PageSize = new Size(dialog.PrintableAreaWidth, dialog.PrintableAreaHeight);
dialog.PrintDocument(documentPaginator, "My document");

Printing on one page

By default, the grid and Gantt chart areas of the component will be printed on separate pages. However, if the total size of printed grid and chart would be small, you’d want to print everything on a single page.

You can do that by exporting and printing a DrawingVisual from the component directly (note the Export call too, passing a delegate to actually print — that will prepare the component’s content ahead of time to ensure printing the correct drawing visual):

GanttChartDataGrid.Export((Action)delegate
{
// Get a DrawingVisual representing the Gantt Chart content.
var exportedVisual = GanttChartDataGrid.GetExportDrawingVisual();
// Apply necessary transforms for the content to fit into the output page.
exportedVisual.Transform = GetPageFittingTransform(dialog);
// Actually print the visual.
var container = new Border();
container.Padding = new Thickness (48, 32, 48, 32);
container.Child = new Rectangle { Fill = new VisualBrush (exportedVisual), Width = exportedSize.Width, Height = exportedSize.Height };
dialog.PrintVisual(container, "My document");
});

The definition of GetPageFittingTransform method called above is here as well:

private TransformGroup GetPageFittingTransform(System.Windows.Controls.PrintDialog printDialog)
{
// Determine scale to apply for page fitting.
var scale = GetPageFittingScaleRatio(printDialog);
// Set up a transform group in order to allow multiple transforms, if needed.
var transformGroup = new TransformGroup();
transformGroup.Children.Add(new ScaleTransform(scale, scale));
// Optionally, add other transforms, such as supplemental translation, scale, or rotation as you need for the output presentation.
return transformGroup;
}

private double GetPageFittingScaleRatio(System.Windows.Controls.PrintDialog printDialog)
{
// Determine the appropriate scale to apply based on export size and printable area size.
var outputSize = GanttChartDataGrid.GetExportSize();
var scaleX = printDialog.PrintableAreaWidth / outputSize.Width;
var scaleY = printDialog.PrintableAreaHeight / outputSize.Height;
var scale = Math.Min(scaleX, scaleY);
return scale;
}

Customizing printing further

Of course, sometimes you’ll want to allow end users to customize printing furthermore, such as to select the timeline to consider, and which grid columns to be printed (to reduce the number of printed pages, given a big project, that would otherwise have a large Gantt chart, for example).

To do so, you can define a custom PrintDialog window too, one that will eventually call printing after all settings have been applied to the component, and ensuring all settings are set back when printing completed.

<Grid Margin="4">
<Grid.RowDefinitions>
<RowDefinition Height="*"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<Grid Grid.Row="0">
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<TextBlock Grid.Row="0" Grid.Column="0" Text="TIMELINE" VerticalAlignment="Center" Style="{StaticResource TextBlockEditForm}"/>
<TextBlock Grid.Row="1" Grid.Column="0" Text="Start date:" VerticalAlignment="Center" Style="{StaticResource TextBlockEditForm}"/>
<DatePicker Grid.Row="1" Grid.Column="1" SelectedDate="{Binding TimelinePageStart, RelativeSource={RelativeSource AncestorType=local:PrintDialog}, UpdateSourceTrigger=LostFocus}"
VerticalAlignment="Center" HorizontalAlignment="Right" Style="{StaticResource DatePickerEdit}"/>
<TextBlock Grid.Row="2" Grid.Column="0" Text="End date:" VerticalAlignment="Center" Style="{StaticResource TextBlockEditForm}"/>
<DatePicker Grid.Row="2" Grid.Column="1" SelectedDate="{Binding TimelinePageFinish, RelativeSource={RelativeSource AncestorType=local:PrintDialog}, UpdateSourceTrigger=LostFocus}"
VerticalAlignment="Center" HorizontalAlignment="Right" Style="{StaticResource DatePickerEdit}"/>
<TextBlock Grid.Row="3" Grid.Column="0" Text="GRID COLUMNS" VerticalAlignment="Center" Margin="4,8,4,4" Style="{StaticResource TextBlockEditForm}"/>
<DataGrid Grid.Row="4" Grid.Column="0" Grid.ColumnSpan="2" x:Name="ColumnsDataGrid" ItemsSource="{Binding GridColumns, RelativeSource={RelativeSource AncestorType=local:PrintDialog}}"
HorizontalAlignment="Stretch" VerticalScrollBarVisibility="Auto" AutoGenerateColumns="False" CanUserAddRows="False" CanUserDeleteRows="False"
Style="{StaticResource DataGridStyle}" RowHeaderWidth="0" MaxHeight="307" Margin="4" BorderThickness="1">
<DataGrid.Columns>
<DataGridTemplateColumn Header="Select">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<CheckBox IsChecked="{Binding IsSelected, UpdateSourceTrigger=PropertyChanged}" HorizontalAlignment="Center"/>
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
<DataGridTextColumn Header="Name" Width="268" Binding="{Binding Header}"/>
</DataGrid.Columns>
</DataGrid>
</Grid>
<StackPanel Grid.Row="1" HorizontalAlignment="Right" Orientation="Horizontal">
<Button x:Name="PrintButton" Content="Print" Margin="4" Padding="10,5" MinWidth="100" IsDefault="True" Click="PrintButton_Click" Style="{StaticResource Button}"/>
<Button x:Name="CloseButton" Content="Cancel" Margin="4" Padding="10,5" MinWidth="100" IsCancel="True" Click="CloseButton_Click" Style="{StaticResource Button}"/>
</StackPanel>
</Grid>

The XAML above represents the content of the custom PrintDialog window that would allow the end user to select a custom timeline and a set of columns to print.

The associated code behind would go like this. First, how we’d open the dialog:

PrintDialog printDialog = new PrintDialog { Owner = this };
printDialog.Load();
printDialog.ShowDialog();

Then, the actual dialog’s implementation — relevant parts loading data:

public MainWindow MainWindow => Owner as MainWindow;
public DlhSoft.Windows.Controls.GanttChartDataGrid GanttChartDataGrid => MainWindow.GanttChartDataGrid;
public List<ColumnSelector> GridColumns { get; set; } = new List<ColumnSelector>();
public DateTime TimelinePageStart { get; set; }
public DateTime TimelinePageFinish { get; set; }

private const double PrintingThreshold = 12000000;

public void Load()
{
TimelinePageStart = GanttChartDataGrid.Items.Any() ? GanttChartDataGrid.GetProjectStart().AddDays(-1) : GanttChartDataGrid.TimelinePageStart;
TimelinePageFinish = GanttChartDataGrid.Items.Any() ? GanttChartDataGrid.GetProjectFinish().AddDays(7) : GanttChartDataGrid.TimelinePageFinish;

foreach (var column in GanttChartDataGrid.Columns)
{
if (column.Header == null) continue;
GridColumns.Add(new ColumnSelector
{
Header = column.Header.ToString() == "" ? "Index" : column.Header.ToString(),
IsSelected = true,
Column = column
});
}
}

Finally, the complete code that is called on Print button — temporarily hiding the items that are not to be printed, and limiting the timeline page to that selected by the end user, printing all content on a single page if it fits, too:

if (TimelinePageStart >= TimelinePageFinish)
{
MessageBox.Show("The selected dates are incorrect. Please choose a valid timeline.", "Information", MessageBoxButton.OK);
return;
}

var oldStart = GanttChartDataGrid.TimelinePageStart;
var oldFinish = GanttChartDataGrid.TimelinePageFinish;

IEnumerable<GanttChartItem> itemsForHiding = null;
IEnumerable<DataGridColumn> visibleColumns = null;
try
{
var dialog = new System.Windows.Controls.PrintDialog();
dialog.PrintTicket.PageOrientation = PageOrientation.Landscape;

visibleColumns = GridColumns.Where(c => c.IsSelected).Select(c => c.Column);
itemsForHiding = GanttChartDataGrid.Items
.Where(i => i.Finish < TimelinePageStart || i.Start > TimelinePageFinish)
.ToArray();

var itemsCount = GanttChartDataGrid.Items.Count - itemsForHiding.Count();
var timelineHours = GanttChartDataGrid.GetEffort(TimelinePageStart, TimelinePageFinish, GanttChartDataGrid.GetVisibilitySchedule()).TotalHours;
var gridWidth = visibleColumns.Sum(c => c.ActualWidth);

if (itemsCount * GanttChartDataGrid.ItemHeight * (gridWidth + timelineHours * GanttChartDataGrid.HourWidth) > PrintingThreshold)
{
MessageBox.Show("The printed output would be too big. Please select a shorter timeline and/or fewer columns.", "Information", MessageBoxButton.OK);
return;
}

if (dialog.ShowDialog() == true)
{
GanttChartDataGrid.SetTimelinePage(TimelinePageStart, TimelinePageFinish);

foreach (var column in GridColumns)
{
if (!column.IsSelected)
column.Column.Visibility = Visibility.Collapsed;
}

foreach (var item in itemsForHiding)
item.IsHidden = true;

var exportedSize = GanttChartDataGrid.GetExportSize();

//Printing on a single page, if the content fits (considering the margins defined in PrintingTemplate as well).
if (exportedSize.Width + 2 * 48 <= dialog.PrintableAreaWidth && exportedSize.Height + 2 * 32 <= dialog.PrintableAreaHeight)
{
GanttChartDataGrid.Export((Action)delegate
{
// Get a DrawingVisual representing the Gantt Chart content.
var exportedVisual = GanttChartDataGrid.GetExportDrawingVisual();
// Apply necessary transforms for the content to fit into the output page.
exportedVisual.Transform = GetPageFittingTransform(dialog);
// Actually print the visual.
var container = new Border();
container.Padding = new Thickness (48, 32, 48, 32);
container.Child = new Rectangle { Fill = new VisualBrush (exportedVisual), Width = exportedSize.Width, Height = exportedSize.Height };
dialog.PrintVisual(container, "Gantt Chart Document");
});
}
else
{
var documentPaginator = new DlhSoft.Windows.Controls.GanttChartDataGrid.DocumentPaginator(GanttChartDataGrid);
documentPaginator.PageSize = new Size(dialog.PrintableAreaWidth, dialog.PrintableAreaHeight);
dialog.PrintDocument(documentPaginator, "Gantt Chart Document");
}

Close();
}
}
finally
{
Dispatcher.BeginInvoke((Action)delegate
{
foreach (var columnSelector in GridColumns)
{
if (!columnSelector.IsSelected)
columnSelector.Column.Visibility = Visibility;
}

GanttChartDataGrid.SetTimelinePage(oldStart, oldFinish);

foreach (var item in itemsForHiding)
item.IsHidden = false;
});
}

Sample code

The full source code of a Printing sample app is available for download, if you wish to check it out further.

--

--