Showing posts with label wpf. Show all posts
Showing posts with label wpf. Show all posts

June 10, 2010

ListView Horizontal and Vertical Gridlines

Recently, in WPF application I was developing I was asked to show gridlines in the ListView with GridView. I thought it was piece of cake, just turn on some property like ShowGridlines. I was totally wrong! It absolutely wasn't as easy. After half an hour of searching I figured out that there were two feasible solutions available for vertical lines (fortunately, horizontal lines are the matter of setting ListViewItem border):
  1. How Do I Set Up Grid Lines for my ListView?
  2. WPF ListView Vertical Lines (Horizontal as Bonus
First is only ok, if you have only few fields in your ListView, it becomes too tedious to set cell template for each cell. Second is quite cumbersome since it requires setting each vertical gridline manually. What if I have 10 grids with 10-15 columns each? Should I create a style for each of the grids? Or should I use code-generation to get border around each cell template? Fortunately, I found out a more elegant solution after I spotted GridViewRowPresenter in the ListViewItem template. After spending some time with Reflector, I figured out how to implement custom ancestor of GridViewRowPresenter to draw vertical lines.
Here is how. It requires a bit of understanding a bit of WPF inner infrastructure namely overriding Measure and Arrage layout passes and Visuals’s VisualChildrenCount and GetVisualChild. The main job ofGridViewRowPresenter  is to arrange cells according to column order and sizes. That is where I plug-in my additional logic of arranging vertical gridlines.
protected override Size ArrangeOverride(Size arrangeSize)
{
    var size = base.ArrangeOverride(arrangeSize);
    var children = Children.ToList();
    EnsureLines(children.Count);
    for (var i = 0; i < _lines.Count; i++)
    {
        var child = children[i];
        var x = child.TransformToAncestor(this).Transform(new Point(child.ActualWidth, 0)).X + child.Margin.Right;
        var rect = new Rect(x, -Margin.Top, 1, size.Height + Margin.Top + Margin.Bottom);
        var line = _lines[i];
        line.Measure(rect.Size);
        line.Arrange(rect);
    }
    return size;
}
It simply calls base’s ArrangeOverride method and then iterates through logical children arranging gridlines after each of the child. It takes margins into account and requires ListViewItem HorizontalContentAlignment to be set to Stretch in order to work properly. Here are two helper methods/properties used:
private IEnumerable<FrameworkElement> Children
{
    get { return LogicalTreeHelper.GetChildren(this).OfType<FrameworkElement>(); }
}

Basically, gets logical children of the presenter. And:
private void EnsureLines(int count)
{
    count = count - _lines.Count;
    for (var i = 0; i < count; i++)
    {
        var line = (FrameworkElement) Activator.CreateInstance(SeparatorStyle.TargetType);
        line.Style = SeparatorStyle;
        AddVisualChild(line);
        _lines.Add(line);
    }
}
Which creates elements that would be used as separator. Activator is included just for flexibility that allows you to use any FrameworkElement not just Rectangle or Border. What would be used as separator is governed by SeparatorStyle property. By default it is set to Rectangle with Fill property set.
static GridViewRowPresenterWithGridLines()
{
    DefaultSeparatorStyle = new Style(typeof (Rectangle));
    DefaultSeparatorStyle.Setters.Add(new Setter(Shape.FillProperty, SystemColors.ControlLightBrush));
    SeparatorStyleProperty = DependencyProperty.Register("SeparatorStyle", typeof (Style), typeof (GridViewRowPresenterWithGridLines),
                                                            new UIPropertyMetadata(DefaultSeparatorStyle, SeparatorStyleChanged));
}
All above can be as well hardcoded as simple as:
line = new Rectangle{Fill=Brushes.LightGray};

One other thing required is to tell WPF that you have added additional visuals (lines) to your control. I do it by overriding following methods:
protected override int VisualChildrenCount
{
    get { return base.VisualChildrenCount + _lines.Count; }
}

protected override Visual GetVisualChild(int index)
{
    var count = base.VisualChildrenCount;
    return index < count ? base.GetVisualChild(index) : _lines[index - count];
}

That is it! Now just modify the ItemContainerStyle of the ListView:
<ListView.ItemContainerStyle>
    <Style TargetType="{x:Type ListViewItem}">
        <Setter Property="Margin" Value="2,0,0,0"/>
        <Setter Property="Padding" Value="0,2"/>
        <Setter Property="BorderBrush" Value="LightGray"/>
        <Setter Property="BorderThickness" Value="0,0,0,1"/>
        <Setter Property="HorizontalContentAlignment" Value="Stretch"/>
        <Setter Property="Template">
            <Setter.Value>
                <ControlTemplate TargetType="{x:Type ListViewItem}">
                    <Border BorderBrush="{TemplateBinding BorderBrush}" 
                            BorderThickness="{TemplateBinding BorderThickness}" 
                            Background="{TemplateBinding Background}">
                        <GridLines:GridViewRowPresenterWithGridLines 
                            Columns="{TemplateBinding GridView.ColumnCollection}"
                            Margin="{TemplateBinding Padding}" />
                        <!-- Try setting the SeparatorStyle property of presenter
                            SeparatorStyle="{StaticResource SeparatorStyle}" 
                        -->
                    </Border>
                </ControlTemplate>
            </Setter.Value>
        </Setter>
        <Style.Triggers>
            <Trigger Property="IsSelected" Value="True">
                <Setter Property="Background" Value="{DynamicResource {x:Static SystemColors.ControlDarkBrushKey}}"/>
            </Trigger>
            <Trigger Property="IsEnabled" Value="False">
                <Setter Property="Background" Value="{DynamicResource {x:Static SystemColors.ControlBrushKey}}"/>
            </Trigger>
        </Style.Triggers>
    </Style>
</ListView.ItemContainerStyle>

Download source code

June 30, 2009

Stretch Panel

I believe that Silverlight missing panel that will stretch children proportionally to their size. For example:


As you can see button with longer text appears longer, all buttons filling available client area. So I decide to implement StretchPanel, by inheriting from Panel and overriding MeasureOverride and ArrageOverride method.
First, we need Orientation. So I added Orientation dependency property to my controls (the same that StackPanel has). During the calculation we will need to operate either on Width or on Height of property of Size construct. And additionally in arrange method will need to move either on x or on y axis. Instead of checking Orientation value when I need to decide on which side to operate I created following delegates that are assigned in OnOrientationChanged method and then used elsewhere.
private delegate Point GetOffsetPointDelegate(double offset);
private delegate double GetSideDelegate(Size size);
private delegate void SetSideDelegate(ref Size size, double value);
We need 3 delegate types: to get side, to set side (notice ref since Size is value type), and to get offset point. We also need 2 delegates for each delegate type: to get primary side (width for horizontal) and secondary side (height for horizontal). Here is implementation of delegates along with Orientation dependency property:
private GetSideDelegate GetOtherSide;
private GetSideDelegate GetSide;
private SetSideDelegate SetOtherSide;
private SetSideDelegate SetSide;
private GetOffsetPointDelegate GetOffsetPoint;

public StretchPanel()
{
    SetDelegates(Orientation);
}

private void SetDelegates(Orientation value)
{
    GetSide = (value == Orientation.Horizontal) ? GetWidth : new GetSideDelegate(GetHeight);
    GetOtherSide = (value == Orientation.Horizontal) ? GetHeight : new GetSideDelegate(GetWidth);
    SetSide = (value == Orientation.Horizontal) ? SetWidth : new SetSideDelegate(SetHeight);
    SetOtherSide = (value == Orientation.Horizontal) ? SetHeight : new SetSideDelegate(SetWidth);
    GetOffsetPoint = (value == Orientation.Horizontal) ? GetXOffset : new GetOffsetPointDelegate(GetYOffset);
}

#region Orientation Property 

public static readonly DependencyProperty OrientationProperty =
    DependencyProperty.Register(
        "Orientation", typeof (Orientation), typeof (StretchPanel),
        new PropertyMetadata(Orientation.Horizontal, OnOrientationChanged));

public Orientation Orientation
{
    get { return (Orientation) GetValue(OrientationProperty); }
    set { SetValue(OrientationProperty, value); }
}

private static void OnOrientationChanged(DependencyObject o, DependencyPropertyChangedEventArgs e)
{
    ((StretchPanel) o).OnOrientationChanged((Orientation) e.NewValue, (Orientation) e.OldValue);
}

private void OnOrientationChanged(Orientation newValue, Orientation oldValue)
{
    SetDelegates(newValue);
    UpdateLayout();
}

#endregion

private static double GetWidth(Size size)
{
    return size.Width;
}

private static double GetHeight(Size size)
{
    return size.Height;
}

private static void SetWidth(ref Size size, double value)
{
    size.Width = value;
}

private static void SetHeight(ref Size size, double value)
{
    size.Height = value;
}

private static Point GetXOffset(double offset)
{
    return new Point(offset, 0);
}

private static Point GetYOffset(double offset)
{
    return new Point(0, offset);
}

All we need is to implement MeasureOverride and ArrageOverride methods.
protected override Size MeasureOverride(Size availableSize)
{
    var size = new Size();
    foreach (UIElement child in Children)
    {
        child.Measure(availableSize);
        SetSide(ref size, GetSide(size) + GetSide(child.DesiredSize));
        SetOtherSide(ref size, Math.Max(GetOtherSide(size), GetOtherSide(child.DesiredSize)));
    }
    _measureSize = size;
    return size;
}

protected override Size ArrangeOverride(Size finalSize)
{
    double offset = 0;
    foreach (UIElement child in Children)
    {
        double side = Math.Floor(GetSide(child.DesiredSize)/GetSide(_measureSize)*GetSide(finalSize));
        var final = new Size();
        SetSide(ref final, side);
        SetOtherSide(ref final, GetOtherSide(finalSize));
        child.Arrange(new Rect(GetOffsetPoint(offset), final));
        offset += side;
    }
    return finalSize;
}
In the measure method we measure all the children. And then sum up all desired size along primary axis and get the maximum value on the secondary axis. Then we save that value (_meassureSize) for later use and return it to caller. In arrange method we calculate side length as ration of desired side length on measure side length multiplied by final size side length. This will make child appear proportional to its content among others.
Markup for example above is following:
<Controls:StretchPanel xmlns:Controls="clr-namespace:Samples.Controls" 
                       Orientation="Horizontal" Height="30">
    <Button Content="Short Text" />
    <Button Content="Shorter" />
    <Button Content="Long Long Text" />
    <Button Content="Very Very Long Long Text" />
</Controls:StretchPanel>
Download source code