Подтвердить что ты не робот

Точная непрозрачная маска

Предположим, мне нужно установить маску непрозрачности для элемента управления WPF, которая выделяет его часть в точном положении (предположим, квадрат 50x50 в (50; 50) положении). Для этого я создаю DrawingGroup, содержащую 2 объекта GeometryDrawing: 1 полупрозрачный прямоугольник для всего фактического размера элемента управления и 1 непрозрачный прямоугольник для выделенной области. Затем я создаю DrawingBrush из этой DrawingGroup, присваиваю ему свойство Stretch значение None и задаю эту кисть как OpacityMask для элемента управления, который необходимо замаскировать.

Все это работает нормально, пока ничто не "торчит" за пределы указанного контроля. Но если элемент управления рисует что-то вне его границ, внешняя точка становится отправной точкой, из которой применяется маска непрозрачности (если кисть выровнена по этой стороне), и вся маска смещается на это расстояние, что приводит к неожиданному поведению.

Кажется, я не могу найти способ применить маску из границ элемента управления или, по крайней мере, получить фактические границы элемента управления (включая прилипающие части), чтобы я мог соответствующим образом настроить свою маску.

Любые идеи высоко ценятся!

Обновление: вот простой тестовый пример XAML и скриншоты, демонстрирующие проблему:

У нас есть 2 вложенных Границы и Холст в последнем с вышеупомянутым квадратом:

<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>

Вот как это выглядит:

no mask
(источник: ailon.org)

Теперь мы добавляем OpacityMask ко второй границе, чтобы каждая его часть, кроме нашего квадрата, была полупрозрачной:

<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>

Все выглядит как положено:

masked
(источник: ailon.org)

И теперь мы добавляем к холсту линию, которая будет на 10 пикселей слева от нашей границы:

<Line X1="-10" Y1="150" X2="120" Y2="150"
      Stroke="Red" StrokeThickness="2" 
      />

И маска смещается на 10 пикселей влево:

shifted mask
(источник: ailon.org)

Обновление 2: В качестве обходного пути я добавляю смехотворно большой прозрачный прямоугольник за пределы границ и корректирую свою маску соответственно, но это действительно неприятный обходной путь.

Обновление 3: Примечание: холст с прямоугольником и линией приведен в качестве примера некоторого объекта, у которого есть что-то вне его границ. В контексте этого примера это следует рассматривать как своего рода черный ящик. Вы не можете изменить его свойства, чтобы решить общую проблему. Это было бы то же самое, что просто перемещать линию, чтобы она не торчала.

4b9b3361

Ответ 1

Интересная проблема - вот что я понял: эффект, который вы испытываете, по-видимому, определяется Viewport концепция/поведение TileBrush (см. Viewbox тоже для полная картина). По-видимому, неявный ограничивающий прямоугольник FrameworkElement (т.е. Canvas в вашем случае) затрагивается/расширяется элементами, торчащими из границ тонким способом, т.е. размеры окна расширяются, но система координат поля не масштабируется, скорее также расширяется в направлении вне границ.

Это может быть проще проиллюстрировать это графически, но из-за ограничений по времени я только предлагаю решение в первую очередь и объясню шаги, которые я предпринял на данный момент, чтобы вы начали:


Решение:

<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>

Обратите внимание, что я скорректировал единицы на +/- 2 пикселя здесь и там для точности пикселей, не зная, откуда происходит смещение, но я думаю, что это можно проигнорировать для целей примера и позже решить, если потребуется.


Объяснение

Чтобы упростить иллюстрацию, сначала следует сначала сделать все связанные с ним явные/авто свойства явными.

Внутренняя граница получает автоматические размеры 198 с внешней границы (240 - 20 отступов - 2 пикселя, выведенных экспериментом, не знаю их происхождения, но сейчас невежественных), то есть если вы укажете это следующим образом, ничего не должно изменение, а использование других значений дает графические изменения:

<Border Background="LightBlue" Width="198" Height="198">...</Border>

Далее по умолчанию подразумевается Viewport и ViewportUnits вот так:

<DrawingBrush Stretch="None" AlignmentX="Left" AlignmentY="Top" 
    Viewport="0,0,1,1" ViewportUnits="RelativeToBoundingBox">...</DrawingBrush>

Вы выполняете размер DrawingBrush, переопределяя Stretch с None, сохраняя при этом позицию и размер базовой плитки по умолчанию и относительно его ограничивающего блока. Кроме того, вы (понятно) переопределяете AlignmentX/AlignmentY, которые определяют размещение в базовой плите, которая находится в пределах ограничивающей рамки. Сбрасывание этих значений по умолчанию Center уже говорит: Маска сдвигается соответственно, то есть она должна быть меньше, чем ограничивающая рамка, иначе им нечего было бы центрировать внутри.

Это можно сделать, изменив ViewportUnits на Absolute, что не даст никакой графики вообще, пока устройства не будут правильно настроены конечно; опять же, экспериментом следующие явные значения сопоставляют автоматические, в то время как другие значения дают графические изменения:

<DrawingBrush Stretch="None" AlignmentX="Center" AlignmentY="Center" 
    Viewport="0,0,202,202" ViewportUnits="Absolute">...</DrawingBrush>

Теперь маска непрозрачности уже правильно выравнивается с элементом управления. Очевидно, остается одна проблема, поскольку маска сейчас обрезает линию, что неудивительно, учитывая ее размер и отсутствие каких-либо Stretch эффект. Соответственно, регулировка его размера и положения разрешает следующее:

<RectangleGeometry Rect="-10,0,220,200" />

и

<DrawingBrush Stretch="None" AlignmentX="Center" AlignmentY="Center" 
    Viewport="-10,0,222,202" ViewportUnits="Absolute">...</DrawingBrush>

Наконец, маска непрозрачности соответствует границам управления по желанию!


Дополнение:

Необходимые смещения, определенные выводом и экспериментом в приведенном выше объяснении, могут быть получены во время выполнения с помощью VisualTreeHelper Class:

Rect descendantBounds = VisualTreeHelper.GetDescendantBounds(myGrid);

В зависимости от состава и потребностей визуального элемента вам может потребоваться включить LayoutInformation Class и построить объединение обоих, чтобы получить все -средняя ограничивающая рамка:

Rect descendantBounds = VisualTreeHelper.GetDescendantBounds(myGrid);
Rect layoutSlot = LayoutInformation.GetLayoutSlot(myGrid);
Rect boundingBox = descendantBounds;
boundingBox.Union(layoutSlot);

Для получения более подробной информации по обеим темам см. следующие ссылки:

Ответ 2

На вашем объекте Canvas добавьте 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>

Ответ 3

Один способ обхода, который может быть более идеальным, чем ваш текущий, - просто применить OpacityMask на более высоком уровне. Например, используя этот демонстрационный код, вы можете удалить маску из Border и применить ее к Window. С небольшим количеством настроек он подходит правильно:

<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>

Вам нужно будет написать код для перемещения маски при изменении размера Window, и по этой причине вам может быть лучше сгенерировать маску динамически в коде.

Мой вопрос для вас: зачем вам обрабатывать геометрии, выходящие за пределы вашего Canvas?

Ответ 4

Поскольку у вас есть части, которые торчат из элемента управления, одна идея состоит в том, чтобы отделить управляющее изображение от маски управления.

<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>