90

I have a ListViewItem that I am applying a Style to and I would like to put a dotted grey line as the bottom Border.

How can I do this in WPF? I can only see solid color brushes.

Dave Clemmer
  • 3,741
  • 12
  • 49
  • 72
dan
  • 5,664
  • 8
  • 45
  • 59
  • Did you check this http://stackoverflow.com/questions/1630022/dotted-border-on-listboxitem-in-wpf – Alex Aza Jun 01 '11 at 01:51
  • 1
    No - Thanks for that. You don't know of a _simple_ way do you? It seems like a bit of a hack. – dan Jun 01 '11 at 02:58
  • Related post, and probably the best answer http://stackoverflow.com/questions/14936002/dotted-border-around-a-button – jv_ Oct 25 '15 at 02:43

7 Answers7

127

This worked great in our application, allowing us to use a real Border and not mess around with Rectangles:

<Border BorderThickness="1,0,1,1">
   <Border.BorderBrush>
      <DrawingBrush Viewport="0,0,8,8" ViewportUnits="Absolute" TileMode="Tile">
         <DrawingBrush.Drawing>
            <DrawingGroup>
               <GeometryDrawing Brush="Black">
                  <GeometryDrawing.Geometry>
                     <GeometryGroup>
                        <RectangleGeometry Rect="0,0,50,50" />
                        <RectangleGeometry Rect="50,50,50,50" />
                     </GeometryGroup>
                  </GeometryDrawing.Geometry>
               </GeometryDrawing>
            </DrawingGroup>
         </DrawingBrush.Drawing>
      </DrawingBrush>
   </Border.BorderBrush>

   <TextBlock Text="Content Goes Here!" Margin="5"/>
</Border>

Note that the Viewport determines the size of the dashes in the lines. In this case, it generates eight-pixel dashes. Viewport="0,0,4,4" would give you four-pixel dashes.

Rand Scullard
  • 3,145
  • 1
  • 22
  • 18
  • how can this be used on other elements which needs the same style. – Charanraj Golla Sep 29 '13 at 08:42
  • You can define a style that includes the DrawingBrush and then apply that style to whatever elements you want. – Rand Scullard Sep 29 '13 at 17:49
  • The two rectangles are actually aligned in such a way that this pattern works all around the border, horizontal and vertical, left and right. (Better don't try to use it for non-rectangular lines though...) – ygoe Feb 26 '14 at 16:06
  • 1
    Done this way could the dashes offset still be animated? – jrandomuser Sep 24 '14 at 14:44
  • unapproved best solution.:) – Mark Feb 07 '17 at 02:40
  • It's pretty clever to draw a checkboard and use that as the border, but it only works for a width of 1, and even then it can look fuzzy at times. I've tried to get rid of this with the options here but have not succeeded: http://stackoverflow.com/questions/5585082/how-to-turn-off-the-anti-aliasing-in-wpf-shapes – Chris Mar 27 '17 at 19:45
  • This is how it works: https://prnt.sc/pWytkHZJt2ea This is with the border of 50 px (just to see how it fills). This looks to support 1 px border only. Moreover, the rectangle size may be fractional (like 10.3 px), so sometimes the border is filled with something absolutely unexpected. – Rustem Zinnatullin Jun 20 '22 at 14:52
106

You can create a dotted or dashes line using a rectangle like in the code below

<Rectangle Stroke="#FF000000" Height="1" StrokeThickness="1" StrokeDashArray="4 4"
                                                       SnapsToDevicePixels="True"/>

Get started with this and customize your listview according to your scenario

biju
  • 17,554
  • 10
  • 59
  • 95
39

A bit late to the party, but the following solution worked for me. It is slightly simpler/better than both other solutions:

<Border BorderThickness="1">
  <Border.BorderBrush>
    <VisualBrush>
      <VisualBrush.Visual>
        <Rectangle StrokeDashArray="4 2" Stroke="Gray" StrokeThickness="1"
                  Width="{Binding RelativeSource={RelativeSource AncestorType={x:Type Border}}, Path=ActualWidth}"
                  Height="{Binding RelativeSource={RelativeSource AncestorType={x:Type Border}}, Path=ActualHeight}"/>
      </VisualBrush.Visual>
    </VisualBrush>
  </Border.BorderBrush>

  <TextBlock Text="Whatever" />
</Border>
dotNET
  • 33,414
  • 24
  • 162
  • 251
  • Brilliance. The 1st answer was poor with rounded corners. This one works excellently. Just set RadiusX/Y on the Rectangle to the same CornerRadius that's on the Border. – Bill Tarbell Feb 16 '18 at 05:49
  • Should be preferred answer. I prefer this answer as it's the cleanest, shortest and most readable (intuitive) solution. – Erik Bongers May 20 '19 at 10:12
  • 2
    ...however! If you use this brush as a `staticresource` or `dynamicresource`, something goes wrong. I assume due to the `Width` and `Height` bindings failing. The 1st answer *does* work as a reusable resource. Pity. I really like this solution. – Erik Bongers May 28 '19 at 20:25
6

Xaml

<Grid>
<Grid.RowDefinitions><RowDefinition Height="auto"/></Grid.RowDefinitions>
<Grid.ColumnDefinitions><ColumnDefinition Width="auto"/></Grid.ColumnDefinitions>
<Rectangle RadiusX="9" RadiusY="9" Fill="White" Stroke="Black" StrokeDashArray="1,2"/>
<TextBlock Padding = "4,2" Text="Whatever"/>
</Grid>
Dreia Ria
  • 59
  • 1
  • 3
2

Our team got this as a requirement lately and we solved it by creating a custom control, DashedBorder which extends Border and adds the dashed border feature.

It has 3 new dependency properties

  • UseDashedBorder (bool)
  • DashedBorderBrush (Brush)
  • StrokeDashArray (DoubleCollection)

Usable like this

<controls:DashedBorder UseDashedBorder="True"
                       DashedBorderBrush="#878787"
                       StrokeDashArray="2 1"
                       Background="#EBEBEB"                               
                       BorderThickness="3"
                       CornerRadius="10 10 10 10">
    <TextBlock Text="Dashed Border"
               Margin="6 2 6 2"/>
</controls:DashedBorder>

And produces a result like this enter image description here

When UseDashedBorder is set to true it will create a VisualBrush with 2 rectangles and set that as BorderBrush (that's why we need an extra property for the color of the actual BorderBrush). The first one is to create the dashing and the second of is to fill in the gaps with the Background of the border.

It maps the Rectangle dashing properties to the DashedBorder properties like this

  • StrokeDashArray => StrokeDashArray
  • Stroke => DashedBorderBrush
  • StrokeThickness => BorderThickness.Left
  • RadiusX => CornerRadius.TopLeft
  • RadiusY => CornerRadius.TopLeft
  • Width => ActualWidth
  • Height => ActualHeight

DashedBorder.cs

    public class DashedBorder : Border
    {
        private static DoubleCollection? emptyDoubleCollection;
        private static DoubleCollection EmptyDoubleCollection()
        {
            if (emptyDoubleCollection == null)
            {
                DoubleCollection doubleCollection = new DoubleCollection();
                doubleCollection.Freeze();
                emptyDoubleCollection = doubleCollection;
            }
            return emptyDoubleCollection;
        }

        public static readonly DependencyProperty UseDashedBorderProperty =
          DependencyProperty.Register(nameof(UseDashedBorder),
                                      typeof(bool),
                                      typeof(DashedBorder),
                                      new FrameworkPropertyMetadata(false, OnUseDashedBorderChanged));

        public static readonly DependencyProperty DashedBorderBrushProperty =
          DependencyProperty.Register(nameof(DashedBorderBrush),
                                      typeof(Brush),
                                      typeof(DashedBorder),
                                      new FrameworkPropertyMetadata(null));

        public static readonly DependencyProperty StrokeDashArrayProperty =
          DependencyProperty.Register(nameof(StrokeDashArray),
                                      typeof(DoubleCollection),
                                      typeof(DashedBorder),
                                      new FrameworkPropertyMetadata(EmptyDoubleCollection()));

        private static void OnUseDashedBorderChanged(DependencyObject target, DependencyPropertyChangedEventArgs e)
        {
            DashedBorder dashedBorder = (DashedBorder)target;
            dashedBorder.UseDashedBorderChanged();
        }

        private Rectangle GetBoundRectangle()
        {
            Rectangle rectangle = new Rectangle();

            rectangle.SetBinding(Rectangle.StrokeThicknessProperty, new Binding() { Source = this, Path = new PropertyPath("BorderThickness.Left") });
            rectangle.SetBinding(Rectangle.RadiusXProperty, new Binding() { Source = this, Path = new PropertyPath("CornerRadius.TopLeft") });
            rectangle.SetBinding(Rectangle.RadiusYProperty, new Binding() { Source = this, Path = new PropertyPath("CornerRadius.TopLeft") });
            rectangle.SetBinding(Rectangle.WidthProperty, new Binding() { Source = this, Path = new PropertyPath(ActualWidthProperty) });
            rectangle.SetBinding(Rectangle.HeightProperty, new Binding() { Source = this, Path = new PropertyPath(ActualHeightProperty) });

            return rectangle;
        }

        private Rectangle GetBackgroundRectangle()
        {
            Rectangle rectangle = GetBoundRectangle();
            rectangle.SetBinding(Rectangle.StrokeProperty, new Binding() { Source = this, Path = new PropertyPath(BackgroundProperty) });
            return rectangle;
        }

        private Rectangle GetDashedRectangle()
        {
            Rectangle rectangle = GetBoundRectangle();
            rectangle.SetBinding(Rectangle.StrokeDashArrayProperty, new Binding() { Source = this, Path = new PropertyPath(StrokeDashArrayProperty) });
            rectangle.SetBinding(Rectangle.StrokeProperty, new Binding() { Source = this, Path = new PropertyPath(DashedBorderBrushProperty) });
            Panel.SetZIndex(rectangle, 2);
            return rectangle;
        }

        private VisualBrush CreateDashedBorderBrush()
        {
            VisualBrush dashedBorderBrush = new VisualBrush();
            Grid grid = new Grid();
            Rectangle backgroundRectangle = GetBackgroundRectangle();
            Rectangle dashedRectangle = GetDashedRectangle();
            grid.Children.Add(backgroundRectangle);
            grid.Children.Add(dashedRectangle);
            dashedBorderBrush.Visual = grid;
            return dashedBorderBrush;
        }

        private void UseDashedBorderChanged()
        {
            if (UseDashedBorder)
            {
                BorderBrush = CreateDashedBorderBrush();
            }
            else
            {
                ClearValue(BorderBrushProperty);
            }
        }

        public bool UseDashedBorder
        {
            get { return (bool)GetValue(UseDashedBorderProperty); }
            set { SetValue(UseDashedBorderProperty, value); }
        }

        public Brush DashedBorderBrush
        {
            get { return (Brush)GetValue(DashedBorderBrushProperty); }
            set { SetValue(DashedBorderBrushProperty, value); }
        }

        public DoubleCollection StrokeDashArray
        {
            get { return (DoubleCollection)GetValue(StrokeDashArrayProperty); }
            set { SetValue(StrokeDashArrayProperty, value); }
        }
    }
Fredrik Hedblad
  • 83,499
  • 23
  • 264
  • 266
0

Working on a user control.... I have been trying a storyboard for a marching ants border. The basic grid with a rectangle and text works fine since there is no interaction. When trying to put a button inside the grid, then either the rectangle or button is visible but never both of them.

From another post: Advanced XAML Animation effects. Pulse, Marching ants, Rotations. Alerts

Using dotNet's solution for the VisualBrush shifted the rectangle to the border with a button inside. This worked perfectly.

<UserControl.Resources>
    <ResourceDictionary>
        <Style TargetType="{x:Type TextBlock}" x:Key="LOC_DG_Cell_Mid" BasedOn="{StaticResource DG_TextBlock_Mid}" >
            <Setter Property="Margin" Value="5 0"/>
        </Style>
        <Storyboard x:Key="MarchingAnts">
            <DoubleAnimation BeginTime="00:00:00"
                Storyboard.TargetName="AlertBox"                                
                Storyboard.TargetProperty="StrokeThickness"
                To="4"
                Duration="0:0:0.25" />
            <!-- If you want to run counter-clockwise, just swap the 'From' and 'To' values. -->
            <DoubleAnimation BeginTime="00:00:00" RepeatBehavior="Forever" Storyboard.TargetName="AlertBox" Storyboard.TargetProperty="StrokeDashOffset" 
                            Duration="0:3:0" From="1000" To="0"/>
        </Storyboard>
    </ResourceDictionary>

</UserControl.Resources>
<UserControl.Triggers>
    <EventTrigger RoutedEvent="FrameworkElement.Loaded">
        <BeginStoryboard Storyboard="{StaticResource MarchingAnts}"/>
    </EventTrigger>
</UserControl.Triggers>

<Grid>
    <Border BorderThickness="1">
        <Border.BorderBrush>
            <VisualBrush>
                <VisualBrush.Visual>
                    <Rectangle x:Name="AlertBox" Stroke="Red" StrokeDashOffset="2" StrokeDashArray="5" Margin="5"
                      Width="{Binding RelativeSource={RelativeSource AncestorType={x:Type Border}}, Path=ActualWidth}"
                      Height="{Binding RelativeSource={RelativeSource AncestorType={x:Type Border}}, Path=ActualHeight}"/>
                </VisualBrush.Visual>
            </VisualBrush>
        </Border.BorderBrush>

        <Button x:Name="FinishedButton" Padding="0 5" Margin="0" Style="{StaticResource IconButton}" >
            <StackPanel Orientation="Horizontal" >
                <Label Style="{StaticResource ButtonLabel}" Content="Processing has Finished" />
            </StackPanel>
        </Button>
    </Border>
</Grid>
Galactic
  • 400
  • 4
  • 14
0

If you are looking for pixel perfect dashed lines

Note that there is no shadow/blur at the end of of each line

public static class DashBrushFactory
    {
        public static Brush CreateBrush(double dpiScale, SolidColorBrush solidColorBrush)
        {
            const double dashLength = 4;
            const double dashSpace = 4;

            double dashLengthPixelSnapped = SnapToPixel(dashLength, dpiScale);
            double dashSpacePixelSnapped = SnapToPixel(dashSpace, dpiScale);

            ImageBrush imageBrush = new ImageBrush();
            DrawingImage drawingImage = new DrawingImage();
            GeometryDrawing geometryDrawing = new GeometryDrawing();
            GeometryGroup geometryGroup = new GeometryGroup();
            RectangleGeometry rectangleGeometry1 = new RectangleGeometry();
            RectangleGeometry rectangleGeometry2 = new RectangleGeometry();

            rectangleGeometry1.Rect = new Rect(0, 0, dashLengthPixelSnapped, dashLengthPixelSnapped);
            rectangleGeometry2.Rect = new Rect(dashLengthPixelSnapped, dashLengthPixelSnapped, dashSpacePixelSnapped, dashSpacePixelSnapped);

            rectangleGeometry1.Freeze();
            rectangleGeometry2.Freeze();

            geometryGroup.Children.Add(rectangleGeometry1);
            geometryGroup.Children.Add(rectangleGeometry2);
            geometryGroup.Freeze();

            geometryDrawing.Brush = solidColorBrush;
            geometryDrawing.Geometry = geometryGroup;
            geometryDrawing.Freeze();

            drawingImage.Drawing = geometryDrawing;
            drawingImage.Freeze();
            imageBrush.TileMode = TileMode.Tile;
            imageBrush.ViewportUnits = BrushMappingMode.Absolute;
            imageBrush.Viewport = new Rect(0, 0, dashLengthPixelSnapped * 2, dashSpacePixelSnapped * 2);
            imageBrush.ImageSource = drawingImage;
            imageBrush.Freeze();

            return imageBrush;
        }

        private static double SnapToPixel(double value, double dpiScale)
        {
            double newValue;

            // If DPI == 1, don't use DPI-aware rounding.
            if (DoubleUtil.AreClose(dpiScale, 1.0) == false)
            {
                newValue = Math.Round(value * dpiScale) / dpiScale;
                // If rounding produces a value unacceptable to layout (NaN, Infinity or MaxValue), use the original value.
                if (DoubleUtil.IsNaN(newValue) ||
                    Double.IsInfinity(newValue) ||
                    DoubleUtil.AreClose(newValue, Double.MaxValue))
                {
                    newValue = value;
                }
            }
            else
            {
                newValue = Math.Round(value);
            }

            return newValue;
        }
    }

https://referencesource.microsoft.com/#WindowsBase/Shared/MS/Internal/DoubleUtil.cs

ekalchev
  • 762
  • 7
  • 24