I am somewhat confused about how relative coordinates work in WPF, especially in scenarios with DrawingBrushes.
Let's say I want to paint the background of a square area, which is flexible in it's size. I want to paint the background with a special "shape", let's say a kind of "T" laying on the side, with the vertical stroke going through the middle of the area.
Using relative coordinates (the size of the area is flexible), I came up with the following XAML:
<Window x:Class="MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="MainWindow" Height="722" Width="722" UseLayoutRounding="True">
<Window.Resources>
<DrawingBrush x:Key="EdgeGrid">
<DrawingBrush.Drawing>
<GeometryDrawing>
<GeometryDrawing.Geometry>
<!-- draw a single T laying on the side -->
<GeometryGroup>
<!-- top to bottom -->
<LineGeometry StartPoint="0.5,0.0" EndPoint="0.5,1"/>
<!-- left to right -->
<LineGeometry StartPoint="0.5,0.5" EndPoint="1,0.5"/>
</GeometryGroup>
</GeometryDrawing.Geometry>
<GeometryDrawing.Pen>
<Pen Thickness="0.01" Brush="Black" />
</GeometryDrawing.Pen>
</GeometryDrawing>
</DrawingBrush.Drawing>
</DrawingBrush>
</Window.Resources>
<Grid Name="LayoutRoot">
<Rectangle Width="400" Height="400" Stroke="Black" StrokeThickness="1" Fill="{StaticResource EdgeGrid}">
</Rectangle>
</Grid>
But the result I get looks like this:
(source: bilder-hochladen.net)
Shouldn't the vertical stroke go right through the middle (X coordinate is 0.5)?
And also how can I set the pen thickness to be 1 or 2 pixels in relative mode?
Any ideas?
You'll have to set the ViewboxUnits property of the DrawingBrush to Absolute (instead of the default RelativeToBoundingBox). The Viewbox would still be (0,0,1,1).
See the TileBrush Overview article on MSDN for details about a brush's viewbox and viewport.
<DrawingBrush x:Key="EdgeGrid" ViewboxUnits="Absolute">
<DrawingBrush.Drawing>
<GeometryDrawing>
<GeometryDrawing.Geometry>
<GeometryGroup>
<LineGeometry StartPoint="0.5,0.0" EndPoint="0.5,1"/>
<LineGeometry StartPoint="0.5,0.5" EndPoint="1,0.5"/>
</GeometryGroup>
</GeometryDrawing.Geometry>
<GeometryDrawing.Pen>
<Pen Thickness="0.01" Brush="Black" />
</GeometryDrawing.Pen>
</GeometryDrawing>
</DrawingBrush.Drawing>
</DrawingBrush>
Of course this won't let you define stroke widths in pixels. Making the drawing in absolute coordinates and then putting the whole thing in a Viewbox won't also help much, because strokes would still be scaled.
In order to get a scalable drawing with fixed stroke width, you would have to use a Path element, where you set the StrokeThickness and Data properties and assign a ScaleTransform to the Transform property of the Geometry used as Data.
In your special case of drawing a centered, T-shaped figure with fixed stroke width, you may simply draw two (very long) Lines in a Canvas, where the coordinate origin is centered by putting the Canvas in a 2x2 Grid. You may even choose to have different strokes and stroke widths for the two Lines.
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition/>
<ColumnDefinition/>
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition/>
<RowDefinition/>
</Grid.RowDefinitions>
<Canvas Grid.Column="1" Grid.Row="1">
<Line Y1="-10000" Y2="10000" Stroke="Black" StrokeThickness="1"/>
<Line X2="10000" Stroke="Black" StrokeThickness="1"/>
</Canvas>
</Grid>
To get vertical stroke right you need to do it like this:
<GeometryGroup>
<!-- top to bottom -->
<LineGeometry StartPoint="0.75,0.0"
EndPoint="0.75,1" />
<!-- left to right -->
<LineGeometry StartPoint="0.5,0.5"
EndPoint="1,0.5" />
</GeometryGroup>
But that won't help you with pen thickness. In general, if you want to scale a geometry - first create it using absolute coordinates you like (say in 0-100 range), then put that into ViewBox or use ScaleTransform, like this:
<Viewbox Width="400"
Height="400">
<Path Stroke="Black"
StrokeThickness="1">
<Path.Data>
<GeometryGroup>
<!-- top to bottom -->
<LineGeometry StartPoint="75,0"
EndPoint="75, 100" />
<!-- left to right -->
<LineGeometry StartPoint="50,50"
EndPoint="100, 50" />
</GeometryGroup>
</Path.Data>
</Path>
</Viewbox>
Let's see how the proposed solution would look like.
Let's assume we want to show the shapes in a kind of grid and draw various shapes depending on the data (by selecting an appropriate DateTemplate). For simplicity, in this example, let's draw only one kind of shape (a cross, as in my initial question):
<Window x:Class="MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="MainWindow" Height="722" Width="722" UseLayoutRounding="True">
<ItemsControl ItemsSource="{Binding data}">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<UniformGrid Columns="10" Rows="10">
</UniformGrid>
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate>
<Grid x:Name="Cell">
<Path StrokeThickness="2" Stroke="Blue" SnapsToDevicePixels="True">
<Path.Data>
<GeometryGroup>
<GeometryGroup.Transform>
<ScaleTransform ScaleX="{Binding ElementName=Cell, Path=ActualWidth}" ScaleY="{Binding ElementName=Cell, Path=ActualHeight}"/>
</GeometryGroup.Transform>
<!-- top to bottom -->
<LineGeometry StartPoint="0.5,0.0" EndPoint="0.5,1"/>
</GeometryGroup>
</Path.Data>
</Path>
<Path StrokeThickness="1" Stroke="Black" SnapsToDevicePixels="True">
<Path.Data>
<GeometryGroup>
<GeometryGroup.Transform>
<ScaleTransform ScaleX="{Binding ElementName=Cell, Path=ActualWidth}" ScaleY="{Binding ElementName=Cell, Path=ActualHeight}"/>
</GeometryGroup.Transform>
<!-- left to right -->
<LineGeometry StartPoint="0,0.5" EndPoint="1,0.5"/>
</GeometryGroup>
</Path.Data>
</Path>
</Grid>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</Window>
#Clemens Is this the solution you had in mind? Is this the correct way of doing it?
The only problem I have with the result is that the lines are blurry and there is even a break to be seen in the horizontal line. Is there any solution for this?
Related
I try to get into creation of custom controls with for WPF. I found many good
tutorials and advises on the web so I started width a really simple example to get
my hands dirty and get some practice. I figured out that the issue stumbled across
is not really related to the subject of custom controls. So I extracted the xaml code to a simple wpf form.
<Window x:Class="WpfVerticalAigmentTest.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="MainWindow" Height="200" Width="200">
<Grid>
<Grid Height="40" Background="LightCyan" VerticalAlignment="Center">
<Path Stroke="Red"
StrokeThickness="20" VerticalAlignment="Center" >
<Path.Data>
<LineGeometry StartPoint="0,0" EndPoint="100,0"></LineGeometry>
</Path.Data>
</Path>
</Grid>
</Grid>
My expectation was to get a line centered in the grid and claiming the half of the stroke thickness on each side from the center. But as the linked image shows differs from my expectation.
"Resulting visualization"
So it look like I missed a detail about the line shape or linegeomtry. How do I get the the line displayed as shown in the following image?
"Expected result"
You need to match the Width and Height of the LineGeometry to the Width and Height of the Path and set the VerticalAlignment property to Bottom:
<Grid Height="20" Width="200" Background="LightCyan" VerticalAlignment="Center">
<Path Stroke="Red" StrokeThickness="20" VerticalAlignment="Bottom">
<Path.Data>
<LineGeometry StartPoint="0,0" EndPoint="200,0"></LineGeometry>
</Path.Data>
</Path>
</Grid>
If your goal is your the expectaions, and not the way how u have reached this, I could prefer to you this:
<Grid>
<Grid Height="40" Background="LightCyan" VerticalAlignment="Center">
<Border BorderThickness="10" VerticalAlignment="Center" BorderBrush="Red" />
</Grid>
</Grid>
The problem here is that the starting point of the XY Coordinates of the Path starts on the top left, and the stroke expands in both directions but thereby only makes the Path bigger to the bottom (I can't really tell you why, but that's just what seems to happen).
You can see this pretty good in the Design View:
To work around this simply move your Y Coordinates down half of the stroke size.
<Grid Height="40"
VerticalAlignment="Center"
Background="LightCyan">
<Path VerticalAlignment="Center"
Stroke="Red"
StrokeThickness="20">
<Path.Data>
<LineGeometry StartPoint="0,10" EndPoint="100,10" />
</Path.Data>
</Path>
</Grid>
Or wrap it in another control (Canvas is the commonly used controls for Paths) with the desired height:
<Grid Height="40"
VerticalAlignment="Center"
Background="LightCyan">
<Canvas Height="20" VerticalAlignment="Center">
<Path Stroke="Red"
StrokeThickness="20">
<Path.Data>
<LineGeometry StartPoint="0,10" EndPoint="100,10" />
</Path.Data>
</Path>
</Canvas>
</Grid>
And you are good to go:
I have to show a compass with an arrow inside a circle
I have the following code:
<Page xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<Page.Resources>
<Pen x:Key="BlackPen1" Thickness="1" Brush="Black"></Pen>
</Page.Resources>
<Grid>
<!-- Image for the Circle -->
<Image>
<Image.Source>
<DrawingImage>
<DrawingImage.Drawing>
<GeometryDrawing Pen="{StaticResource BlackPen1}" >
<GeometryDrawing.Geometry>
<GeometryGroup>
<EllipseGeometry RadiusX="50" RadiusY="50"></EllipseGeometry>
</GeometryGroup>
</GeometryDrawing.Geometry>
</GeometryDrawing>
</DrawingImage.Drawing>
</DrawingImage>
</Image.Source>
</Image>
<Path Grid.Row="1" Data="M15,0 L30,40 L0,40Z" Stroke="Black" Fill="Black" StrokeThickness="1" HorizontalAlignment="Center" />
<Line Grid.Row="0" Y1="40" Y2="400" X1="0" X2="0" Stroke="Black" StrokeThickness="5" HorizontalAlignment="Center" />
</Grid>
</Page>
Now I have to transform this whole compass based on the input angle.
One thing I know is If I move my arrow shape into Image type I can tans form this using the following
<Image.RenderTransform>
<RotateTransform Angle="{Binding ElementName=root, Path=Angle}"/>
</Image.RenderTransform>
But I am not able to draw this geometry inside the Image tag.
How to achieve this?
Why would you need to rotate the Ellipse? Surely only the arrow moves in a compass. In order to make that job easier, why don't you create the arrow in just one Path, instead of additionally using a Line element? You could define the same arrow with rotation like this:
<Path Grid.Row="1" Data="M15,0 30,40 18,40 18,400 12,400 12,40 0,40Z" Stroke="Black"
Fill="Black" StrokeThickness="1" HorizontalAlignment="Center"
RenderTransformOrigin="0.5,0.5">
<Path.RenderTransform>
<RotateTransform Angle="{Binding Angle, ElementName=root}" />
</Path.RenderTransform>
</Path>
You can apply the rotation to the whole Grid That contains all of your controls. Don't forget to set RenderTransformOrigin="0.5,0.5" on the element you rotate so it rotates arround the center and not the top left corner.
Or you could add a RotationTransform to each of the elements Path, Line and Ellipse. However in this case Rotation centers will probably be different for each one and it makes it more complicated.
We use vector icons exported from Expression Design in our application, but I just noticed that all empty space in the icons gets trimmed away when they are displayed inside a stretched Image. Since the whole point of having vector icons is to allow them to stretch and scale nicely, this seems very strange.
Here is an example. Both the red and the blue star are designed in a 32 pixels wide document, but the blue star is much smaller. Setting the stretch property to Uniform suddenly makes the two icons the same size.
Is it possible to solve this and keep the whitespace around the icons? I guess a possible workaround would be to add a transparent background rectangle that covers the whole icon area, but this seems like a bad solution to me.
<Window
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
x:Class="GeneralTest.MainWindow"
x:Name="Window"
Title="MainWindow"
Width="640" Height="480">
<Window.Resources>
<DrawingImage x:Key="small_blue_star">
<DrawingImage.Drawing>
<DrawingGroup>
<DrawingGroup.Children>
<GeometryDrawing Brush="Blue" Geometry="F1 M 20.4989,22C 20.4634,22.0177 16.2771,19.0022 16.2416,19.0022C 16.2061,19.0022 12.0021,22.0177 11.9844,22C 11.9489,21.9823 13.4921,17.0332 13.4744,16.9977C 13.4744,16.9623 9.4655,14.0354 9.48323,13.9999C 9.48323,13.9644 14.45,14.0177 14.4855,13.9999C 14.5032,13.9822 16.2061,8.99765 16.2416,8.99765C 16.2771,8.99765 17.9623,13.9822 17.98,13.9999C 18.0155,14.0177 22.9823,13.9644 23,13.9999C 23,14.0354 18.9911,16.9623 18.9911,16.9977C 18.9734,17.0332 20.5166,21.9823 20.4989,22 Z "/>
</DrawingGroup.Children>
</DrawingGroup>
</DrawingImage.Drawing>
</DrawingImage>
<DrawingImage x:Key="big_red_star">
<DrawingImage.Drawing>
<DrawingGroup>
<DrawingGroup.Children>
<GeometryDrawing Brush="Red" Geometry="F1 M 26.0781,32C 25.9941,32.0436 16.0823,24.6277 15.9983,24.6277C 15.9143,24.6277 5.96055,32.0436 5.91855,32C 5.83455,31.9564 9.48848,19.7856 9.44648,19.6983C 9.44648,19.6111 -0.0453207,12.4133 -0.00332647,12.326C -0.00332647,12.2388 11.7564,12.3697 11.8404,12.326C 11.8824,12.2824 15.9143,0.0243702 15.9983,0.0243702C 16.0823,0.0243702 20.0723,12.2824 20.1142,12.326C 20.1982,12.3697 31.958,12.2388 32,12.326C 32,12.4133 22.5082,19.6111 22.5082,19.6983C 22.4662,19.7856 26.1201,31.9564 26.0781,32 Z "/>
</DrawingGroup.Children>
</DrawingGroup>
</DrawingImage.Drawing>
</DrawingImage>
</Window.Resources>
<StackPanel x:Name="LayoutRoot">
<StackPanel Orientation="Horizontal">
<TextBlock Text="Stretch=None" Width="100"/>
<Image Source="{StaticResource big_red_star}" Width="32" Stretch="None"/>
<Image Source="{StaticResource small_blue_star}" Width="32" Stretch="None"/>
</StackPanel>
<StackPanel Orientation="Horizontal">
<TextBlock Text="Stretch=Uniform" Width="100"/>
<Image Source="{StaticResource big_red_star}" Width="32" Stretch="Uniform" />
<Image Source="{StaticResource small_blue_star}" Width="32" Stretch="Uniform"/>
</StackPanel>
</StackPanel>
</Window>
I am trying to create a red circle with a black x through it using XAML.
My problem is that they aren't aligned correctly.
What is the right way to do this?
This is what I've got so far:
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<Image>
<Image.Source>
<DrawingImage>
<DrawingImage.Drawing>
<DrawingGroup>
<GeometryDrawing Brush="Red">
<GeometryDrawing.Pen>
<Pen Brush="Transparent" Thickness="0"/>
</GeometryDrawing.Pen>
<GeometryDrawing.Geometry>
<EllipseGeometry Center="8,8" RadiusX="8" RadiusY="8"/>
</GeometryDrawing.Geometry>
</GeometryDrawing>
<GeometryDrawing>
<GeometryDrawing.Pen>
<Pen Brush="Black" Thickness="2.5"/>
</GeometryDrawing.Pen>
<GeometryDrawing.Geometry>
<PathGeometry>
<PathFigure StartPoint="4,4">
<LineSegment Point="12,12"/>
</PathFigure>
<PathFigure StartPoint="4,12">
<LineSegment Point="12,4"/>
</PathFigure>
</PathGeometry>
</GeometryDrawing.Geometry>
</GeometryDrawing>
</DrawingGroup>
</DrawingImage.Drawing>
</DrawingImage>
</Image.Source>
</Image>
</Grid>
Simply putting an ellipse in the same grid with a black X the X isn't quite centered on the ellipse because the coordinates of each line you draw are really coordinates within the space allotted for it.
I think they needed to be in some sort of geometry or drawing aggregate to give them the same coordinate system. The geometry group and path are aggregators but both require their contents to have the same fill and stroke and the stroke and fill is different for the red circle (no stroke) and the black X (no fill).
The only aggregator that gives common coordinate systems and allows different fills & strokes for its members that I could find was the DrawingGroup.
The string shortcuts that work for creating a Path via its Data property don't appear to work for creating a PathGeometry so all had to be filled in by hand.
OK, so three hundred ways to skin a cat. Without fully understanding your use case I just came up with the fastest way to draw what you requested.
<Grid HorizontalAlignment="Left"
Height="80"
Margin="80,80,0,0"
VerticalAlignment="Top"
Width="80">
<Ellipse Fill="Red"
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch" />
<Path Data="M40,53 L48,69 62,69 49,46 61,24 48,24 C48,24 40,39 40,39 40,39 32,24 32,24 L18,24 30,46 17,69 31,69 z"
Fill="Black"
Margin="15"
Stretch="Fill"
HorizontalAlignment="Center"
VerticalAlignment="Center"
/>
</Grid>
This is probably outside what exactly you're looking for, but hopefully it at least gives you another way to think about it.
I had the same issue when trying to center text within an ellipse. The problem with using something like a TextBlock is that the kerning and escapement of each character is slightly different and so while the TextBlock element itself might be technically centered within the ellipse, this does not mean that the character will be centered in the ellipse. The character always appears to be too low and to the right of center in most situations.
I have had some success by wrapping the TextBlock in a ViewBox. While I am not fully versed in the technical implementation of the ViewBox, the ViewBox appears to wrap the visual rendering of the content which allows me to center that rendering more easily than trying to center to layout elements together.
I also seem to have better luck using an outer element that is of odd width/height rather than even width and height.
<Grid Width="19"
Height="19">
<Ellipse Fill="#FFB1413F"
StrokeThickness="0"
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch" />
<Viewbox HorizontalAlignment="Center"
VerticalAlignment="Stretch">
<TextBlock Text="X"
Margin="1"
FontWeight="Bold"
Foreground="White"
HorizontalAlignment="Center"
VerticalAlignment="Center" />
</Viewbox>
</Grid>
Suppose I need to set an opacity mask on a WPF control that highlights a portion of it in precise position (suppose a 50x50 square at (50;50) position). To do that I create a DrawingGroup containing 2 GeometryDrawing objects: 1 semi-transparent rectangle for the whole actual size of the control and 1 opaque rectangle for highlighted area. Then I create a DrawingBrush from this DrawingGroup, set it's Stretch property to None and set this brush as OpacityMask of the control that needs to be masked.
All this works fine while nothing is "sticking" out of bounds of said control. But if control draws something outside of it's bounds the outer point becomes a starting point from where opacity mask is applied (if the brush is aligned to that side) and the whole mask shifts by that distance resulting in unexpected behavior.
I can't seem to find a way to force mask to be applied from control's bounds or at least get the actual bounds of the control (including sticking parts) so I can adjust my mask accordingly.
Any ideas highly appreciated!
Update: Here's a simple test-case XAML and screenshots demonstrating the issue:
We have 2 nested Borders and Canvas in the last one with the above mentioned square:
<Border Padding="20" Background="DarkGray" Width="240" Height="240">
<Border Background="LightBlue">
<Canvas>
<Rectangle Canvas.Left="50" Canvas.Top="50" Width="50" Height="50"
Stroke="Red" StrokeThickness="2"
Fill="White"
/>
</Canvas>
</Border>
</Border>
Here's how it looks:
(source: ailon.org)
Now we add an OpacityMask to the second border so that every part of it except our square is semi-transparent:
<Border.OpacityMask>
<DrawingBrush Stretch="None" AlignmentX="Left" AlignmentY="Top">
<DrawingBrush.Drawing>
<DrawingGroup>
<GeometryDrawing Brush="#30000000">
<GeometryDrawing.Geometry>
<RectangleGeometry Rect="0,0,200,200" />
</GeometryDrawing.Geometry>
</GeometryDrawing>
<GeometryDrawing Brush="Black">
<GeometryDrawing.Geometry>
<RectangleGeometry Rect="50,50,50,50" />
</GeometryDrawing.Geometry>
</GeometryDrawing>
</DrawingGroup>
</DrawingBrush.Drawing>
</DrawingBrush>
</Border.OpacityMask>
Everything looks as expected:
(source: ailon.org)
And now we add a line to the canvas that sticks 10 pixels out on the left of our border:
<Line X1="-10" Y1="150" X2="120" Y2="150"
Stroke="Red" StrokeThickness="2"
/>
And the mask shifts 10 pixels to the left:
(source: ailon.org)
Update2: As a workaround I add a ridiculously large transparent rectangle outside of bounds and adjust my mask accordingly but that is a really nasty workaround.
Update3: Note: The canvas with rectangle and line is there just as an example of some object that has something outside of it bounds. In context of this sample it should be treated as some sort of a black box. You can't change it's properties to solve the general issue. This would be the same as just moving the line so it doesn't stick out.
Interesting issue indeed - here's what I've figured: The effect you are experiencing seems to be determined by the Viewport concept/behavior of TileBrush (see Viewbox too for the complete picture). Apparently the implicit bounding box of a FrameworkElement (i.e. the Canvas in your case) is affected/expanded by elements sticking out of bounds in a subtle way, that is, the dimensions of the box expand but the coordinate system of the box does not scale, rather expands too into the out of bounds direction.
It might be easier to illustrate that graphically, but due to time constraints I'll just offer a solution first and will explain the steps I've taken for the moment in order to get you started:
Solution:
<Border Background="LightBlue" Width="198" Height="198">
<Border.OpacityMask>
<DrawingBrush Stretch="None" AlignmentX="Center" AlignmentY="Center"
Viewport="-10,0,222,202" ViewportUnits="Absolute">
<DrawingBrush.Drawing>
<DrawingGroup>
<GeometryDrawing Brush="#30000000">
<GeometryDrawing.Geometry>
<RectangleGeometry Rect="-10,0,220,200" />
</GeometryDrawing.Geometry>
</GeometryDrawing>
<GeometryDrawing Brush="Black">...</GeometryDrawing>
</DrawingGroup>
</DrawingBrush.Drawing>
</DrawingBrush>
</Border.OpacityMask>
<Canvas x:Name="myGrid">...</Canvas>
</Border>
Please note that I've adjusted units by +/- 2 pixels here and there for pixel precision without knowing where the offset originates, but I think this can be ignored for the purpose of the example and resolved later if need be.
Explanation:
To simplify the illustration one should usually make all related implied/auto properties explicit first.
The inner border receives auto dimensions of 198 from the outer border (240 - 20 padding - 2 pixels deduced by experiment; don't know their origin, but ignorable right now), that is if you specify this as follows nothing should change, while using other values yields graphical changes:
<Border Background="LightBlue" Width="198" Height="198">...</Border>
Further the default implied Viewport and ViewportUnits like so:
<DrawingBrush Stretch="None" AlignmentX="Left" AlignmentY="Top"
Viewport="0,0,1,1" ViewportUnits="RelativeToBoundingBox">...</DrawingBrush>
You are enforcing the DrawingBrush size by overriding Stretch with None, while keeping the position and dimension of the base tile at default and relative to its bounding box. In addition you (understandably) are overriding AlignmentX/AlignmentY, which determine the placement within the base tile, that is within its bounding box. Resetting those to their defaults of Center is already telling: The mask shifts accordingly, meaning it has to be smaller than the bounding box, else their would be nothing to center within.
This can be taken further by changing ViewportUnits to Absolute, which will yield no graphics at all until the units are properly adjusted of course; again, by experiment the following explicit values are matching the auto ones, while using other values yields graphical changes:
<DrawingBrush Stretch="None" AlignmentX="Center" AlignmentY="Center"
Viewport="0,0,202,202" ViewportUnits="Absolute">...</DrawingBrush>
Now the opacity mask already aligns properly with the control. Obviously there is one problem left though, as the mask is clipping the line now, which is no surprise given its size and the absence of any Stretch effect. Adjusting its size and position accordingly resolves this:
<RectangleGeometry Rect="-10,0,220,200" />
and
<DrawingBrush Stretch="None" AlignmentX="Center" AlignmentY="Center"
Viewport="-10,0,222,202" ViewportUnits="Absolute">...</DrawingBrush>
Finally the opacity mask matches the control bounds as desired!
Supplement:
The required offsets determined by deduction and experiment in the explanation above can be retrieved at runtime by means of the VisualTreeHelper Class:
Rect descendantBounds = VisualTreeHelper.GetDescendantBounds(myGrid);
Depending on your visual element composition and needs you may need to factor in the LayoutInformation Class and build the union of both to get the all-encompassing bounding box:
Rect descendantBounds = VisualTreeHelper.GetDescendantBounds(myGrid);
Rect layoutSlot = LayoutInformation.GetLayoutSlot(myGrid);
Rect boundingBox = descendantBounds;
boundingBox.Union(layoutSlot);
See the following links for more details on both topics:
Windows Presentation Foundation
Graphics Rendering Overview,
especially VisualTreeHelper
Class
The Layout System, especially
Element Bounding Boxes
On your Canvas object add ClipToBounds="True".
<Canvas ClipToBounds="True">
<Rectangle Canvas.Left="50" Canvas.Top="50" Width="50" Height="50"
Stroke="Red" StrokeThickness="2"
Fill="White" />
<Line X1="-10" Y1="150" X2="120" Y2="150"
Stroke="Red" StrokeThickness="2"/>
</Canvas>
One workaround that may be more ideal than your current one would be to simply apply the OpacityMask at a higher level. Using this demo code for example, you could remove the mask from the Border and apply it to the Window instead. With a bit of tweaking it fits properly:
<Window.OpacityMask>
<DrawingBrush AlignmentX="Left" AlignmentY="Top" Stretch="None">
<DrawingBrush.Drawing>
<DrawingGroup>
<GeometryDrawing Brush="#30000000">
<GeometryDrawing.Geometry>
<RectangleGeometry Rect="0,0,300,300"/>
</GeometryDrawing.Geometry>
</GeometryDrawing>
<GeometryDrawing Brush="Black">
<GeometryDrawing.Geometry>
<RectangleGeometry Rect="92,82,50,50"/>
</GeometryDrawing.Geometry>
</GeometryDrawing>
</DrawingGroup>
</DrawingBrush.Drawing>
</DrawingBrush>
</Window.OpacityMask>
You would have to write some code to move the mask when the Window is resized, and for that reason you may be better off generating the mask dynamically in the code-behind.
My question for you is, why do you need to handle geometries that go outside the bounds of your Canvas?
Since you have parts that stick out from the control, one idea is to separate control image from the control mask.
<Window x:Class="WpfApplication1.Window1"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="Window1" Height="300" Width="300">
<Border Padding="20" Background="DarkGray" Width="240" Height="240"> <!-- user container -->
<Grid> <!-- the control -->
<Border Background="LightBlue" HorizontalAlignment="Stretch"> <!-- control mask-->
<Canvas>
<Rectangle Canvas.Left="50" Canvas.Top="50" Width="50" Height="50"
Stroke="Red" StrokeThickness="2"
Fill="White"
/>
<Canvas.OpacityMask>
<DrawingBrush Stretch="None" AlignmentX="Left" AlignmentY="Top" TileMode="None">
<DrawingBrush.Drawing>
<DrawingGroup>
<GeometryDrawing Brush="#30000000">
<GeometryDrawing.Geometry>
<RectangleGeometry Rect="0,0,200,200" />
</GeometryDrawing.Geometry>
</GeometryDrawing>
<GeometryDrawing Brush="Black">
<GeometryDrawing.Geometry>
<RectangleGeometry Rect="50,50,50,50" />
</GeometryDrawing.Geometry>
</GeometryDrawing>
</DrawingGroup>
</DrawingBrush.Drawing>
</DrawingBrush>
</Canvas.OpacityMask>
</Canvas>
</Border>
<Canvas> <!-- control image-->
<Line X1="-10" Y1="150" X2="120" Y2="150" Stroke="Red" StrokeThickness="2"/>
</Canvas>
</Grid>
</Border>
</Window>