Precise OpacityMask - PullRequest
       41

Precise OpacityMask

12 голосов
/ 19 августа 2009

Предположим, мне нужно установить маску непрозрачности для элемента управления WPF, которая выделяет его часть в точном положении (предположим, квадрат 50х50 в (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 )

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

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

Ответы [ 4 ]

8 голосов
/ 31 августа 2009

Действительно интересная проблема - вот что я понял: эффект, который вы испытываете, кажется, определяется концепцией / поведением 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);

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

1 голос
/ 26 августа 2009

В свой объект 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>
0 голосов
/ 30 августа 2009

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

<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>
0 голосов
/ 26 августа 2009

Один из обходных путей, который может быть более идеальным, чем ваш нынешний, - просто применить 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?

...