Blog Archives
WPF Datagrid formatting (part 2, advanced)
We stick to the previous DataGrid example and enhance it now.
The improvements/additions are:
- Cells are vertically centered now.
- Copy/paste includes the header text.
ClipboardCopyMode="IncludeHeader"
- Templates cannot be copied/pasted. The DataGrid does not know what property it has to read. Therefore a ClipboardContentBinding was added.
ClipboardContentBinding="{Binding Birthday}
- A yellow smiley is drawn on a Canvas with ellipses and a Bézier curve.
- The birthday string is formatted.
- The DataGrid rows use alternating colors.
- CheckBoxes are centered in the cells.
- A bit closer to hardcore: DatePicker
The method to remove all borders requires slightly more know-how. The required information was taken from my earlier post WPF Control Templates (part 1). Also the background color of the DatePickerTextBox is made transparent. This is done without defining a new template for the DatePicker.The XAML definition
<DatePicker SelectedDate="{Binding Birthday, UpdateSourceTrigger=PropertyChanged, Mode=TwoWay}" BorderThickness="0" Loaded="DataGrid_DatePicker_Loaded" />
calls:
private void DataGrid_DatePicker_Loaded(object sender, RoutedEventArgs e) {...}
which in turn calls:
private static void RemoveBorders(DependencyObject xDependencyObject) {...}
- RowHeaders were added. On the internet you can find a lot of ToggleButton examples. You face the same code roots over and over again. To avoid coming up with a similar example I used a button with a +/- sign instead. This way you can easily change the code and replace the text by custom images.
- My advice here is: Play with the FrozenColumnCount property. I am sure you will need it someday.
- This example uses more templates than the last one.
- RowDetailsTemplate was added. This enables expanding DataGrid rows to eg. show or enter additional information.
- UpdateSourceTrigger makes sure you can see DataGridCell changes immediately on the RowDetailsTemplate.
To achieve this, the class Person needs to inherit INotifyPropertyChanged.
<Window x:Class="WpfDatagrid.MainWindow" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" Language="en-GB" Loaded="Window_Loaded" Closed="Window_Closed" Title="MainWindow" Height="350" Width="525"> <Window.Resources> <!-- DataGrid: header style --> <Style TargetType="{x:Type DataGridColumnHeader}"> <Setter Property="VerticalContentAlignment" Value="Center" /> <Setter Property="SeparatorBrush" Value="WhiteSmoke" /> <Setter Property="FontWeight" Value="Bold" /> </Style> <!--DataGrid: vertical/horizontal text alignment --> <Style x:Key="AlignRight" TargetType="{x:Type TextBlock}"> <Setter Property="HorizontalAlignment" Value="Right" /> <Setter Property="VerticalAlignment" Value="Center" /> </Style> <Style x:Key="AlignLeft" TargetType="{x:Type TextBlock}"> <Setter Property="HorizontalAlignment" Value="Left" /> <Setter Property="VerticalAlignment" Value="Center" /> </Style> <!--DataGrid: center the CheckBox --> <Style x:Key="AlignCheckBox" TargetType="{x:Type DataGridCell}"> <Setter Property="Template"> <Setter.Value> <ControlTemplate TargetType="{x:Type DataGridCell}"> <Grid Background="{TemplateBinding Background}"> <ContentPresenter VerticalAlignment="Center" HorizontalAlignment="Center" /> </Grid> </ControlTemplate> </Setter.Value> </Setter> </Style> <!-- DataGrid: template for expandable area --> <DataTemplate x:Key="TemplateRowDetails"> <DockPanel> <Canvas DockPanel.Dock="Left" Width="60"> <Canvas.Background> <LinearGradientBrush EndPoint="0.5,1" StartPoint="0.5,0"> <GradientStop Color="#FF061246" Offset="0.497"/> <GradientStop Color="#FF7F7FC9" Offset="1"/> </LinearGradientBrush> </Canvas.Background> <Ellipse Fill="Yellow" Height="50" Width="50" StrokeThickness="2" Stroke="Black" Canvas.Left="5" Canvas.Top="5" /> <Ellipse Fill="Black" Height="12" Width="8" Canvas.Left="17" Canvas.Top="20" /> <Ellipse Fill="Black" Height="12" Width="8" Canvas.Left="37" Canvas.Top="20" /> <Path Stroke="Black" StrokeThickness="3"> <Path.Data> <PathGeometry> <PathGeometry.Figures> <PathFigureCollection> <PathFigure StartPoint="15,37"> <PathFigure.Segments> <PathSegmentCollection> <QuadraticBezierSegment Point1="30,52" Point2="45,37" /> </PathSegmentCollection> </PathFigure.Segments> </PathFigure> </PathFigureCollection> </PathGeometry.Figures> </PathGeometry> </Path.Data> </Path> </Canvas> <TextBlock DockPanel.Dock="Top" FontSize="12" FontWeight="Bold" Text="{Binding FirstName}" /> <TextBlock DockPanel.Dock="Top" FontSize="12" FontWeight="Bold" Text="{Binding LastName}" /> <TextBlock DockPanel.Dock="Top" FontSize="12" FontWeight="Bold" Text="{Binding Birthday, StringFormat={}{0:dd MMMM yyyy}}" /> <TextBlock DockPanel.Dock="Top" FontSize="12" FontWeight="Bold" Text="{Binding Homepage}" /> </DockPanel> </DataTemplate> </Window.Resources> <Grid> <!-- advice: play with the property FrozenColumnCount --> <DataGrid AlternatingRowBackground="PeachPuff" AutoGenerateColumns="False" ItemsSource="{Binding}" CanUserAddRows="False" CanUserReorderColumns="True" CanUserResizeColumns="True" CanUserResizeRows="False" SelectionUnit="Cell" SelectionMode="Extended" ClipboardCopyMode="IncludeHeader" RowDetailsTemplate="{StaticResource TemplateRowDetails}" ColumnHeaderHeight="{Binding RowHeight, RelativeSource={RelativeSource Self}}"> <DataGrid.RowStyle> <Style TargetType="DataGridRow"> <Setter Property="DetailsVisibility" Value="Collapsed" /> </Style> </DataGrid.RowStyle> <!-- DataGrid: RowHeader --> <DataGrid.RowHeaderTemplate> <DataTemplate> <Button Click="DataGridRowHeader_Button_Click" Cursor="Hand" HorizontalAlignment="Center" > <Button.Style> <Style TargetType="Button"> <Style.Setters> <Setter Property="VerticalAlignment" Value="Top" /> <Setter Property="Content" Value="+" /> <Setter Property="FontStretch" Value="UltraExpanded" /> <Setter Property="FontWeight" Value="Bold" /> <Setter Property="Height" Value="20" /> <Setter Property= "Width" Value="20" /> <!-- <Setter Property="Width" Value="{Binding RelativeSource={RelativeSource Self}, Path=ActualHeight}" /> --> </Style.Setters> <Style.Triggers> <DataTrigger Binding="{Binding RelativeSource={RelativeSource Mode=FindAncestor,AncestorType={x:Type DataGridRow}},Path=DetailsVisibility}" Value="Visible"> <Setter Property="Background" Value="Salmon" /> <Setter Property="Content" Value="-" /> <Setter Property="Height" Value="86" /> </DataTrigger> </Style.Triggers> </Style> </Button.Style> </Button> </DataTemplate> </DataGrid.RowHeaderTemplate> <!-- DataGrid: Row color when selected --> <DataGrid.CellStyle> <Style> <Style.Setters> <Setter Property="DataGridCell.VerticalContentAlignment" Value="Center" /> </Style.Setters> <Style.Triggers> <Trigger Property="DataGridCell.IsSelected" Value="True"> <Setter Property="DataGridCell.Background" Value="SteelBlue" /> </Trigger> </Style.Triggers> </Style> </DataGrid.CellStyle> <DataGrid.Columns> <!-- Column: Alive --> <DataGridCheckBoxColumn Header="Alive" Binding="{Binding Alive, UpdateSourceTrigger=PropertyChanged, Mode=TwoWay}" CellStyle="{StaticResource AlignCheckBox}" /> <!-- Column: Name --> <DataGridTextColumn Header="Name" Binding="{Binding FirstName, UpdateSourceTrigger=PropertyChanged, Mode=TwoWay}" ElementStyle="{StaticResource AlignLeft}" /> <!-- Column: LastName --> <DataGridTextColumn Header="LastName" Binding="{Binding LastName, UpdateSourceTrigger=PropertyChanged, Mode=TwoWay}" ElementStyle="{StaticResource AlignLeft}" /> <!-- Column: Birthday --> <DataGridTemplateColumn Header="Birthday" SortMemberPath="Birthday.Day" ClipboardContentBinding="{Binding Birthday}"> <DataGridTemplateColumn.CellTemplate> <DataTemplate> <DatePicker SelectedDate="{Binding Birthday, UpdateSourceTrigger=PropertyChanged, Mode=TwoWay}" BorderThickness="0" Loaded="DataGrid_DatePicker_Loaded" /> </DataTemplate> </DataGridTemplateColumn.CellTemplate> </DataGridTemplateColumn> <!-- Column: Age --> <DataGridTextColumn Header="Age" Binding="{Binding Age, StringFormat=N2}" ElementStyle="{StaticResource AlignRight}" IsReadOnly="True" /> <!-- Column: Homepage --> <DataGridHyperlinkColumn Header="Homepage" Binding="{Binding Homepage}" IsReadOnly="True"> <DataGridHyperlinkColumn.ElementStyle> <Style> <EventSetter Event="Hyperlink.Click" Handler="Hyperlink_Clicked"/> <Setter Property="TextBlock.HorizontalAlignment" Value="Left" /> <Setter Property="TextBlock.VerticalAlignment" Value="Center" /> </Style> </DataGridHyperlinkColumn.ElementStyle> </DataGridHyperlinkColumn> </DataGrid.Columns> </DataGrid> </Grid> </Window>
using System; using System.Collections.Generic; using System.ComponentModel; using System.Diagnostics; using System.Windows; using System.Windows.Controls; using System.Windows.Controls.Primitives; using System.Windows.Documents; using System.Windows.Media; namespace WpfDatagrid { public partial class MainWindow : Window { public class Person : INotifyPropertyChanged { public event PropertyChangedEventHandler PropertyChanged; protected void OnPropertyChanged(string xName) { PropertyChangedEventHandler h = PropertyChanged; if (h == null) return; h(this, new PropertyChangedEventArgs(xName)); } // private bool _Alive; public bool Alive { get { return _Alive; } set { _Alive = value; OnPropertyChanged("Alive"); } } private string _FirstName; public string FirstName { get { return _FirstName; } set { _FirstName = value; OnPropertyChanged("FirstName"); } } private string _LastName; public string LastName { get { return _LastName; } set { _LastName = value; OnPropertyChanged("LastName"); } } public double Age { get { return DateTime.Now.Subtract(Birthday).TotalDays / 365; } } public string Homepage { get; set; } private DateTime _Birthday; public DateTime Birthday { get { return _Birthday; } set { _Birthday = value; OnPropertyChanged("Birthday"); } } } // class public MainWindow() { InitializeComponent(); } // // set the window DataContext private void Window_Loaded(object sender, RoutedEventArgs e) { List<Person> lPersons = new List<Person>(); lPersons.Add(new Person() { FirstName = "Liza", LastName = "Minnelli", Birthday = new DateTime(1946, 03, 12), Alive = true, Homepage = "www.officiallizaminnelli.com" }); lPersons.Add(new Person() { FirstName = "Bastian", LastName = "Ohta", Birthday = new DateTime(1975, 03, 13), Alive = true, Homepage = "www.ohta.de" }); lPersons.Add(new Person() { FirstName = "Albert", LastName = "Einstein", Birthday = new DateTime(1879, 03, 14), Alive = false, Homepage = "www.alberteinsteinsite.com" }); lPersons.Add(new Person() { FirstName = "Coenraad", LastName = "van Houten", Birthday = new DateTime(1801, 03, 15), Alive = false, Homepage = "www.vanhoutendrinks.com" }); lPersons.Add(new Person() { FirstName = "Andrew", LastName = "Miller-Jones", Birthday = new DateTime(1910, 03, 16), Alive = false, Homepage = "dead as a Dodo" }); lPersons.Add(new Person() { FirstName = "Gottlieb", LastName = "Daimler", Birthday = new DateTime(1834, 03, 17), Alive = false, Homepage = "www.daimler.com" }); lPersons.Add(new Person() { FirstName = "Rudolf", LastName = "Diesel", Birthday = new DateTime(1858, 03, 18), Alive = false, Homepage = "http://en.wikipedia.org/wiki/Rudolf_Diesel" }); DataContext = lPersons; } // // exit the application private void Window_Closed(object sender, EventArgs e) { Application.Current.Shutdown(0); } // // open the hyperlink in a browser private void Hyperlink_Clicked(object sender, RoutedEventArgs e) { try { Hyperlink lHyperlink = e.OriginalSource as Hyperlink; string lUri = lHyperlink.NavigateUri.OriginalString; Process.Start(lUri); } catch (Exception ex) { MessageBox.Show(ex.Message); } } // // find the correct DataGridRow and set the DetailsVisibility private void DataGridRowHeader_Button_Click(object sender, RoutedEventArgs e) { DependencyObject lDependencyObject = e.OriginalSource as DependencyObject; //Button lButton = lDependencyObject as Button; //if (lButton == null) return; while (!(lDependencyObject is DataGridRow) && lDependencyObject != null) lDependencyObject = VisualTreeHelper.GetParent(lDependencyObject); DataGridRow lRow = lDependencyObject as DataGridRow; if (lRow == null) return; //lRow.IsSelected = (lRow.DetailsVisibility != Visibility.Visible); lRow.DetailsVisibility = lRow.DetailsVisibility == System.Windows.Visibility.Collapsed ? Visibility.Visible : Visibility.Collapsed; Console.WriteLine(lRow.ActualHeight); } // private void DataGrid_DatePicker_Loaded(object sender, RoutedEventArgs e) { // get the DatePicker control DatePicker lDatePicker = sender as DatePicker; lDatePicker.VerticalContentAlignment = System.Windows.VerticalAlignment.Center; // find the inner textbox and adjust the Background colour DatePickerTextBox lInnerTextBox = lDatePicker.Template.FindName("PART_TextBox", lDatePicker) as DatePickerTextBox; lInnerTextBox.Background = Brushes.Transparent; lInnerTextBox.VerticalContentAlignment = System.Windows.VerticalAlignment.Center; lInnerTextBox.Height = lDatePicker.ActualHeight - 2; // remove watermark ContentControl lWatermark = lInnerTextBox.Template.FindName("PART_Watermark", lInnerTextBox) as ContentControl; lWatermark.IsHitTestVisible = false; lWatermark.Focusable = false; lWatermark.Visibility = System.Windows.Visibility.Collapsed; lWatermark.Opacity = 0; // just as demo ContentControl lContentHost = lInnerTextBox.Template.FindName("PART_ContentHost", lInnerTextBox) as ContentControl; // remove ugly borders RemoveBorders(lInnerTextBox); // hardcore 🙂 } // private static void RemoveBorders(DependencyObject xDependencyObject) { for (int i = 0, n = VisualTreeHelper.GetChildrenCount(xDependencyObject); i < n; i++) { DependencyObject lDependencyObject = VisualTreeHelper.GetChild(xDependencyObject, i); RemoveBorders(lDependencyObject); Border lBorder = lDependencyObject as Border; if (lBorder == null) continue; lBorder.BorderBrush = Brushes.Transparent; } } // } // class } // namespace