12

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:

no mask
(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:

masked
(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:

shifted mask
(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.

Glorfindel
  • 21,988
  • 13
  • 81
  • 109
Alan Mendelevich
  • 3,591
  • 4
  • 32
  • 50
  • 3
    It would be helpful to see your code for this. – Charlie Aug 19 '09 at 16:52
  • 1
    Added sample code and screenshots – Alan Mendelevich Aug 20 '09 at 06:20
  • 1
    +1 for adding a detailed reproduction case. – Steffen Opel Aug 25 '09 at 09:08
  • Ur problem occurs because the border shifts left due to the line drawn(which u already know :) ).. so how about u try binding the rectangle to the border or to it's opacity mask ? (don't really know if it is possible).. in case it is..i believe it would solve your problem. – Madi D. Aug 27 '09 at 10:47
  • My example is just to show that OpacityMask is applied to the whole bounds of the control (including any parts that stick outside of the bounds). And I can't find a way to change this behavior or at least find a way to get those "actual" bounds. It's not a real life example which needs to be solved. I can solve this particular task. But I need a universal solution because every time I find a workaround I hit the wall again some time later. – Alan Mendelevich Aug 27 '09 at 12:58

4 Answers4

8

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:

Steffen Opel
  • 63,899
  • 11
  • 192
  • 211
  • 1
    Thanks for the detailed and really working solution! I've yet to figure out why this works (read on Viewport concepts) but it seems to work perfectly (and without specifying Border dimensions too). I'm going to accept your answer now. There's one unfinished problem though: currently I don't mind the line being clipped and your solution works perfectly for me now, but in general case scenario I still don't know a way to determine that necessary "-10" pixel offset automatically. – Alan Mendelevich Aug 31 '09 at 12:18
  • 1
    Somehow I expected this remaining issue ;) I've found a few minutes now and supplemented my answer with a method to determine these offsets at runtime at least. You might take it further from there via bindings eventually, but given the complexities of the rendering process outlined in the mentioned articles this might as well trigger other issues, e.g. performance wise. – Steffen Opel Sep 02 '09 at 11:48
1

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>
Crispy
  • 5,557
  • 3
  • 30
  • 35
  • 1
    Yes, this solves the issue in this particular test case but that wasn't the point. 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. – Alan Mendelevich Aug 27 '09 at 06:07
  • Yea I wasn't exactly sure if that would help or not, I couldn't find a solution other then that one. Good luck. – Crispy Aug 27 '09 at 13:45
  • Wow. Though this didn't solve the OP's problem, it certainly solved mine...and very simply. +1 – Daniel Mar 08 '11 at 21:53
0

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?

Charlie
  • 15,069
  • 3
  • 64
  • 70
  • In general case I can not mess with higher level containers from within my control. I don't "own" higher level objects and can't mess with their properties like I see fit. And anyway in this example the line can stick even outside window bounds and I'm not sure that the same issue wont happen. As for your question there are many possible scenarios but to make it simple I can give you one super simple: if you draw a thick line from (0;0) it will stick out to the left anyway because of the way lines are drawn by default in WPF. – Alan Mendelevich Aug 26 '09 at 06:01
0

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>
jyoung
  • 5,071
  • 4
  • 30
  • 47
  • You've tried to change the task again. As I said in the question the inner canvas with rectangle and line is just an example of some content that has parts that are sticking out of it's bounds. It should be treated like a black box which we don't have control of. Suppose that we don't know what it is and we can't change it. Otherwise the task becomes trivial. – Alan Mendelevich Aug 31 '09 at 05:40