Radial Sweeping of a Brush - c#

I'm looking for a way to fill something using a radial sweep of a defined brush in WPF. I'm going to break down what I want in a series of images in an attempt to make it clear.
Let's say I define a brush in WPF that looks like so:
I then want to use a slice of that brush like so:
And tile it across the radius of a circle as shown:
Finally, in order to fill the shape, I would like to sweep the brush across all angles of the circle, providing me with a result similar to this:
In this particular case I'm attempting to make concentric circles. I know I could achieve this using RadialGradientBrush but this is an obnoxious solution as in order to precisely control the width of my concentric circles I would need change the number of radial stops based on the size of the circle. To make it worse, if the circle size changes, the radial stops will not change unless I use some kind of converter based on the circle width/height.
I was hoping for a clean solution to do this maybe with paths or something but any other suggestions for making circle slices of controlled size is welcome.

How about this to draw a few concentric circles by use of a Brush?
<Rectangle>
<Rectangle.Fill>
<DrawingBrush Stretch="Uniform">
<DrawingBrush.Drawing>
<GeometryDrawing>
<GeometryDrawing.Pen>
<Pen Brush="Black" Thickness="1"/>
</GeometryDrawing.Pen>
<GeometryDrawing.Geometry>
<GeometryGroup>
<EllipseGeometry RadiusX="1" RadiusY="1"/>
<EllipseGeometry RadiusX="3" RadiusY="3"/>
<EllipseGeometry RadiusX="5" RadiusY="5"/>
<EllipseGeometry RadiusX="7" RadiusY="7"/>
<EllipseGeometry RadiusX="9" RadiusY="9"/>
<EllipseGeometry RadiusX="11" RadiusY="11"/>
</GeometryGroup>
</GeometryDrawing.Geometry>
</GeometryDrawing>
</DrawingBrush.Drawing>
</DrawingBrush>
</Rectangle.Fill>
</Rectangle>

Regarding the concentric circle case, combining Clemens's solution with a converter allows for precisely sized circles whose width can be changed dynamically and the amount of circles is set to as many will fit in the allowed area.
class SizeSpacingToCircleGroupConverter : IMultiValueConverter
{
public object Convert(object[] values, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
if (values == null) return values;
var input = values.OfType<double>().ToArray();
if (input.Length != 3) return values;
var width = input[0];
var height = input[1];
var spacing = input[2];
var halfSpacing = spacing / 2;
var diameter = width > height ? height : width;
var lineCount = (int)Math.Floor((diameter / (2 * spacing)) - 1);
if (lineCount <= 0) return values;
var circles = Enumerable.Range(0, lineCount).Select(i =>
{
var radius = halfSpacing + (i * spacing);
return new EllipseGeometry() { RadiusX = radius, RadiusY = radius };
}).ToArray();
var group = new GeometryGroup();
foreach (var circle in circles) group.Children.Add(circle);
return group;
}
public object[] ConvertBack(object value, Type[] targetTypes, object parameter, System.Globalization.CultureInfo culture)
{
throw new NotImplementedException();
}
}
And the XAML:
<Rectangle Height="{StaticResource Diameter}" Width="{StaticResource Diameter}">
<Rectangle.Fill>
<DrawingBrush Stretch="None">
<DrawingBrush.Drawing>
<GeometryDrawing>
<GeometryDrawing.Pen>
<Pen Brush="{StaticResource ForegroundBrush}" Thickness="{StaticResource SpacingDiv2}"/>
</GeometryDrawing.Pen>
<GeometryDrawing.Geometry>
<MultiBinding Converter="{StaticResource SizeSpacingToCircleGroupConverter}">
<Binding Source="{StaticResource Diameter}" />
<Binding Source="{StaticResource Diameter}" />
<Binding Source="{StaticResource Spacing}" />
</MultiBinding>
</GeometryDrawing.Geometry>
</GeometryDrawing>
</DrawingBrush.Drawing>
</DrawingBrush>
</Rectangle.Fill>
</Rectangle>
In my case I'm just using doubles defined in my resource dictionary but I could easily use a binding from a view model.
I still won't mark anything as the accepted answer however because the question was about a tiled radial sweep which could be useful for other reasons.

Related

Adding arrow heads to a line in WPF

I am adding a line in code in WPF between two ellipses which works fine. I now need to add a triangle as an arrow head indicator at the top and bottom of my line. My problem is that I do not know the mathematical equation to ensure that the triangle is exactly over the top of my line and facing the correct direction. I have attached a image showing the issue, any help would be greatly received.
Given a Line element, you would calculate the angle of its direction (clockwise from north) by
var dx = line.X2 - line.X1;
var dy = line.Y2 - line.Y1;
var angle = 180 * (1 - Math.Atan2(dx, dy) / Math.PI);
or
var angle = Vector.AngleBetween(new Vector(0, -1), new Vector(dx, dy));
You could use that angle for the Angle property of a RotateTransform.
An example:
<Line x:Name="line" X1="100" Y1="100" X2="200" Y2="200"
Stroke="Black" StrokeThickness="3"/>
<Path Fill="Red">
<Path.Data>
<PathGeometry Figures="M0,10 L10,30 -10,30Z">
<PathGeometry.Transform>
<TransformGroup>
<RotateTransform x:Name="rotation"/>
<TranslateTransform
X="{Binding ElementName=line, Path=X2}"
Y="{Binding ElementName=line, Path=Y2}"/>
</TransformGroup>
</PathGeometry.Transform>
</PathGeometry>
</Path.Data>
</Path>
with this code behind:
rotation.Angle = angle;

WPF Canvas Fill

I currently have a WPF windows with a Canvas is 600 x 400. Is it possible to scale or automatically zoom in so that the lines take up as much as the 600x600 as possible?
<Border>
<Canvas x:Name="cMap" Width="600" Height="400">
<Line X1="5" Y1="5" X2 ="10" Y2="10" StrokeThickness="2" Stroke="Black"/>
<Line X1="10" Y1="10" X2 ="15" Y2="25" StrokeThickness="2" Stroke="Black"/>
</Canvas>
</Border>
My intention will be to add lines programmatically via code instead of XAML.
Thanks.
Not sure what is your exact usecase, but you could probably benefit by using ViewBox:
<Border>
<Viewbox Stretch="Uniform">
<Canvas x:Name="cMap" Width="15" Height="25">
<Canvas.LayoutTransform>
<ScaleTransform />
</Canvas.LayoutTransform>
<Line X1="5" Y1="5" X2 ="10" Y2="10" StrokeThickness="2" Stroke="Black"/>
<Line X1="10" Y1="10" X2 ="15" Y2="25" StrokeThickness="2" Stroke="Black"/>
</Canvas>
</Viewbox>
</Border>
Hope this helps you!
To draw lines in code you shoud do something like this:
Line line = new Line();
Thickness thickness = new Thickness(101,-11,362,250);
line.Margin = thickness;
line.Visibility = System.Windows.Visibility.Visible;
line.StrokeThickness = 4;
line.Stroke = System.Windows.Media.Brushes.Black;
line.X1 = 10;
line.X2 = 40;
line.Y1 = 70;
line.Y2 = 70;
and don't forget to add:
myCanvas.Children.Add(line);
to put those line in some place
from: Drawing lines in code using C# and WPF
To resize your canvas please read this:
Canvas is the only panel element that has no inherent layout
characteristics. A Canvas has default Height and Width properties of
zero, unless it is the child of an element that automatically sizes
its child elements. Child elements of a Canvas are never resized, they
are just positioned at their designated coordinates. This provides
flexibility for situations in which inherent sizing constraints or
alignment are not needed or wanted. For cases in which you want child
content to be automatically resized and aligned, it is usually best to
use a Grid element.
So as a solution you would make it inside a GRID or using the following code:
public class CanvasAutoSize : Canvas
{
protected override System.Windows.Size MeasureOverride(System.Windows.Size constraint)
{
base.MeasureOverride(constraint);
double width = base
.InternalChildren
.OfType<UIElement>()
.Max(i => i.DesiredSize.Width + (double)i.GetValue(Canvas.LeftProperty));
double height = base
.InternalChildren
.OfType<UIElement>()
.Max(i => i.DesiredSize.Height + (double)i.GetValue(Canvas.TopProperty));
return new Size(width, height);
}
}
at your XAML:
<local:CanvasAutoSize VerticalAlignment="Top" HorizontalAlignment="Left"></local:CanvasAutoSize>
from: WPF: How to make canvas auto-resize?

Using GeometryDrawing in ItemsControl

In my WPF application, I use an ObservableCollection of a class which contains PointCollection. This point collection is used to draw a polygon for each item in the observable collection. The code below show's how I use template binding to draw all the polys:
<Grid>
<ItemsControl ItemsSource="{Binding GeoLines}">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<Canvas />
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate DataType="{x:Type vm:GeoPointsViewModel}">
<Polygon Stroke="LightSkyBlue" StrokeThickness="0.5" Opacity="0.8">
<Polygon.Style>
<Style TargetType="{x:Type Polygon}">
<Setter Property="Points">
<Setter.Value>
<MultiBinding Converter="{StaticResource pointMultiConverter}">
<Binding Path="ActualWidth" RelativeSource="{RelativeSource FindAncestor, AncestorType={x:Type Canvas}}"/>
<Binding Path="ActualHeight" RelativeSource="{RelativeSource FindAncestor, AncestorType={x:Type Canvas}}"/>
<Binding Path="Points"/>
</MultiBinding>
</Setter.Value>
</Setter>
<Setter Property="Fill" Value="#FF0A0A10"/>
</Style>
</Polygon.Style>
</Polygon>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</Grid>
Although it works perfectly, the performance isn't great since each polygon is a UIElement with all its bells and whistles. To improve performance, I'd like to use GeometryDrawing which has a lower footprint. Unfortunately, I've no idea how to do the data binding in that case. So, my question is how do I implement the code using GeometryDrawing (or whatever is more light weight than Shapes).
I'm not sure if using GeometryDrawing could considerably improve the performance but here's how you can implement it. The result may not look exactly what you want, that's because the default StartPoint of PathFigure:
<DataTemplate DataType="{x:Type vm:GeoPointsViewModel}">
<Border>
<Border.Background>
<DrawingBrush>
<DrawingBrush.Drawing>
<GeometryDrawing Brush="Red">
<GeometryDrawing.Geometry>
<PathGeometry>
<PathFigure IsClosed="True">
<PolyLineSegment Points="{Binding Points}"/>
</PathFigure>
</PathGeometry>
</GeometryDrawing.Geometry>
</GeometryDrawing>
</DrawingBrush.Drawing>
</DrawingBrush>
</Border.Background>
</Border>
</DataTemplate>
I used a simple Brush of Red for the polylines. You can also specify some Pen if you want. Note that the effect of polygon may not look exactly like what your original code produces. I guess you may have some more tweak to get it right.
I ended up using code behind to draw visuals directly. This bypassed a lot of UI layers and improved performance significantly. Any other suggestion to improve this code is still welcome.
private Brush geoBrush = (SolidColorBrush)(new BrushConverter().ConvertFrom("#FF0A0A10"));
private Pen geoPen = new Pen(Brushes.LightSkyBlue, 0.5);
private DropShadowEffect geoDropShadow = new DropShadowEffect
{
Color = Brushes.LightSteelBlue.Color,
BlurRadius = 8.0,
ShadowDepth = 0.0
};
private DrawingVisual GeoVisual = null;
private void UpdateGeoLines()
{
MapProjectionViewModel map = this.DataContext as MapProjectionViewModel;
if (map != null)
{
DrawingVisual visual = new DrawingVisual();
using (DrawingContext dc = visual.RenderOpen())
{
foreach (var item in map.GeoLines)
{
if (item.Points.Count > 1)
{
List<Point> points = new List<Point>();
foreach (var p in item.Points)
{
Point point = new Point(
p.X * canvas.ActualWidth,
p.Y * canvas.ActualHeight);
points.Add(point);
}
StreamGeometry geom = new StreamGeometry();
using (StreamGeometryContext gc = geom.Open())
{
Point p1 = points[0];
// Start new object, filled=true, closed=true
gc.BeginFigure(p1, true, true);
// isStroked=true, isSmoothJoin=true
gc.PolyLineTo(points, true, false);
}
geom.Freeze();
dc.DrawGeometry(geoBrush, geoPen, geom);
}
}
}
visual.Effect = geoDropShadow;
visual.Opacity = 0.8;
canvas.Visuals.Remove(GeoVisual);
canvas.Visuals.Add(visual);
GeoVisual = visual;
}
}

How to draw chess board pattern with Brush on Canvas?

I got Canvas and some Rectangles of different width but same height.
I draw Rectangles on Canvas pragramticaly, width calculated in real time when i draw it.
Some of Rectangles have SolidColorBrush but rest should look like chess board:
I tried to do it like this:
private static Brush CreateBrush()
{
// Create a DrawingBrush
var blackBrush = new DrawingBrush();
// Create a Geometry with white background
// Create a GeometryGroup that will be added to Geometry
var gGroup = new GeometryGroup();
gGroup.Children.Add(new RectangleGeometry(new Rect(0, 0, 10, 10)));
gGroup.Children.Add(new RectangleGeometry(new Rect(10, 10, 10, 10)));
// Create a GeomertyDrawing
var checkers =
new GeometryDrawing(new SolidColorBrush(Colors.Black), null, gGroup);
var checkersDrawingGroup = new DrawingGroup();
checkersDrawingGroup.Children.Add(checkers);
blackBrush.Drawing = checkersDrawingGroup;
// Set Viewport and TileMode
blackBrush.Viewport = new Rect(0, 0, 0.5, 0.5 );
blackBrush.TileMode = TileMode.Tile;
return blackBrush;
}
As far as Rectangles got different size, same DrawingBrush looks different on different Rectangles - its not repeats to fill it, but stretch.
As i understand, the problem is in blackBrush.Viewport = new Rect(0, 0, 0.5, 0.5 ); - i should set diffrent Viewports according to each Rectangle width, but its no acceptable for me. I need to use one brush for all rectangles of different size and texture should look like one picture - same scale for X and Y, but it can repeat many time in one Rectangle
Maybe there is another Brush type or other way to solve this issues?
Fill free to ask, if my post was unclear, also sorry for my english.
add picture for what i need:
(It's not about width, its about texture fill)
The problem is in the Viewport settings, you should set its ViewportUnits to Absolute instead of RelativeToBoundingBox (as by default):
//we have to use a fixed size for the squares
blackBrush.Viewport = new Rect(0, 0, 60, 60);
blackBrush.Viewport.ViewportUnits = BrushMappingMode.Absolute;
Here is an example showing the checkboard using pure XAML:
<Grid>
<Grid.Background>
<DrawingBrush TileMode="Tile" Viewport="0,0,60,60"
ViewportUnits="Absolute">
<DrawingBrush.Drawing>
<GeometryDrawing Brush="Black"
Geometry="M5,5 L0,5 0,10 5,10 5,5 10,5 10,0 5,0 Z"/>
</DrawingBrush.Drawing>
</DrawingBrush>
</Grid.Background>
</Grid>
By using a DrawingGroup with King King's answer, you can specify different colors for each side:
<DrawingBrush.Drawing>
<DrawingGroup>
<GeometryDrawing Brush="Black" Geometry="M5,5 L0,5 0,10 5,10 5,5 10,5 10,0 5,0 Z"/>
<GeometryDrawing Brush="Blue" Geometry="M0,0 L0,5 0,10 0,5, 10,5 10,10 5,10 5,0 Z"/>
</DrawingGroup>
</DrawingBrush.Drawing>

How to draw gridline on WPF Canvas?

I need to build a function drawing gridline on the canvas in WPF:
void DrawGridLine(double startX, double startY, double stepX, double stepY,
double slop, double width, double height)
{
// How to implement draw gridline here?
}
How would I go about this?
You don't really have to "draw" anything with WPF. If you want to draw lines, use the appropriate geometries to draw them.
In your case it could be simple really. You're just drawing a grid so you could just create a DrawingBrush to draw a single grid square and tile it to fill in the rest. To draw your tile, you could think of it as drawing X's. So to have a 20x10 tile (which corresponds to stepX and stepY):
(p.s., the slope slop is redundant since you already have the horizontal and vertical step sizes)
<DrawingBrush x:Key="GridTile" Stretch="None" TileMode="Tile"
Viewport="0,0 20,10" ViewportUnits="Absolute">
<!-- ^^^^^^^^^^^ set the size of the tile-->
<DrawingBrush.Drawing>
<GeometryDrawing>
<GeometryDrawing.Geometry>
<!-- draw a single X -->
<GeometryGroup>
<!-- top-left to bottom-right -->
<LineGeometry StartPoint="0,0" EndPoint="20,10" />
<!-- bottom-left to top-right -->
<LineGeometry StartPoint="0,10" EndPoint="20,0" />
</GeometryGroup>
</GeometryDrawing.Geometry>
<GeometryDrawing.Pen>
<!-- set color and thickness of lines -->
<Pen Thickness="1" Brush="Black" />
</GeometryDrawing.Pen>
</GeometryDrawing>
</DrawingBrush.Drawing>
</DrawingBrush>
That takes care of drawing the lines. Now to be able to draw them offset in your grid from the edges, you need to have another brush where you draw a rectangle with the desired dimensions, filled with your tiles. So to have a starting position of (30, 45) (corresponding to startX and startY) with the width and height, 130x120:
<DrawingBrush x:Key="OffsetGrid" Stretch="None" AlignmentX="Left" AlignmentY="Top">
<DrawingBrush.Transform>
<!-- set the left and top offsets -->
<TranslateTransform X="30" Y="45" />
</DrawingBrush.Transform>
<DrawingBrush.Drawing>
<GeometryDrawing Brush="{StaticResource GridTile}" >
<GeometryDrawing.Geometry>
<!-- set the width and height filled with the tile from the origin -->
<RectangleGeometry Rect="0,0 130,120" />
</GeometryDrawing.Geometry>
</GeometryDrawing>
</DrawingBrush.Drawing>
</DrawingBrush>
Then finally to use it, just set it as the background of your grid (or other panel):
<Grid Background="{StaticResource OffsetGrid}">
<!-- ... -->
</Grid>
Here's how it ends up looking like:
If you want to generate the brush dynamically, here's an equivalent function based on the above XAML:
static Brush CreateGridBrush(Rect bounds, Size tileSize)
{
var gridColor = Brushes.Black;
var gridThickness = 1.0;
var tileRect = new Rect(tileSize);
var gridTile = new DrawingBrush
{
Stretch = Stretch.None,
TileMode = TileMode.Tile,
Viewport = tileRect,
ViewportUnits = BrushMappingMode.Absolute,
Drawing = new GeometryDrawing
{
Pen = new Pen(gridColor, gridThickness),
Geometry = new GeometryGroup
{
Children = new GeometryCollection
{
new LineGeometry(tileRect.TopLeft, tileRect.BottomRight),
new LineGeometry(tileRect.BottomLeft, tileRect.TopRight)
}
}
}
};
var offsetGrid = new DrawingBrush
{
Stretch = Stretch.None,
AlignmentX = AlignmentX.Left,
AlignmentY = AlignmentY.Top,
Transform = new TranslateTransform(bounds.Left, bounds.Top),
Drawing = new GeometryDrawing
{
Geometry = new RectangleGeometry(new Rect(bounds.Size)),
Brush = gridTile
}
};
return offsetGrid;
}

Categories

Resources