Reputation: 129
I've got a silver border with text on top that I would like to exclude(cut text away from the border) to see through to the background behind the border, that has a dropshadow bitmapeffect applied. Is this possible, without going to photoshop to create an image that basically does the same thing, without being as flexible?
If possible, how would I go about accomplishing such a task?
Upvotes: 4
Views: 728
Reputation: 84657
I played around with this a bit and ended up with the following.
The tricky part was to "invert" OpacityMask for the Border. I created a class that derives from Image that adds a few Dependency Properties like Text, FontFamily and EmSize. Then it turns the Text into a Geometry using the approach from this link.
You can play around with Text, FontFamily, EmSize, Width and Height until you get the result you require. You can of course also add additional DPs to InvertOpacityText to increase flexibility.
I ended up with this
<Grid Background="Blue">
<Border Background="Gray" CornerRadius="8,8,8,8" Width="240" Height="220">
<Border.Effect>
<DropShadowEffect ShadowDepth="10"
Direction="310"
Color="Black"
Opacity="0.8"
BlurRadius="4"/>
</Border.Effect>
<Border.OpacityMask>
<VisualBrush>
<VisualBrush.Visual>
<local:InvertOpacityText Text=" Now Playing"
EmSize="70"
Stretch="Fill"
Width="510"
Height="414"
FontFamily="Broadway">
<local:InvertOpacityText.LayoutTransform>
<RotateTransform Angle="-90"/>
</local:InvertOpacityText.LayoutTransform>
</local:InvertOpacityText>
</VisualBrush.Visual>
</VisualBrush>
</Border.OpacityMask>
<Image Margin="45,5,5,5" Source="C:\PhilCollins.png"/>
</Border>
</Grid>
InvertOpacityText.cs
public class InvertOpacityText : Image
{
public static readonly DependencyProperty TextProperty =
DependencyProperty.Register("Text",
typeof(string),
typeof(InvertOpacityText),
new FrameworkPropertyMetadata(string.Empty,
TargetPropertyChanged));
public static readonly DependencyProperty EmSizeProperty =
DependencyProperty.Register("EmSize",
typeof(double),
typeof(InvertOpacityText),
new FrameworkPropertyMetadata(70.0,
TargetPropertyChanged));
public static readonly DependencyProperty FontFamilyProperty =
DependencyProperty.Register("FontFamily",
typeof(FontFamily),
typeof(InvertOpacityText),
new FrameworkPropertyMetadata(new FontFamily(),
TargetPropertyChanged));
private static void TargetPropertyChanged(DependencyObject source, DependencyPropertyChangedEventArgs e)
{
InvertOpacityText invertOpacityText = (InvertOpacityText)source;
invertOpacityText.OnTextChanged();
}
public string Text
{
get { return (string)base.GetValue(TextProperty); }
set { base.SetValue(TextProperty, value); }
}
public double EmSize
{
get { return (double)base.GetValue(EmSizeProperty); }
set { base.SetValue(EmSizeProperty, value); }
}
public FontFamily FontFamily
{
get { return (FontFamily)base.GetValue(FontFamilyProperty); }
set { base.SetValue(FontFamilyProperty, value); }
}
private void OnTextChanged()
{
if (Source == null)
{
Source = CreateBitmapSource();
}
FormattedText tx = new FormattedText(Text,
Thread.CurrentThread.CurrentUICulture,
FlowDirection.LeftToRight,
new Typeface(FontFamily,
FontStyles.Normal,
FontWeights.Bold,
FontStretches.Normal),
EmSize,
Brushes.Black);
Geometry textGeom = tx.BuildGeometry(new Point(0, 0));
Rect boundingRect = new Rect(new Point(-100000, -100000), new Point(100000, 100000));
RectangleGeometry boundingGeom = new RectangleGeometry(boundingRect);
GeometryGroup group = new GeometryGroup();
group.Children.Add(boundingGeom);
group.Children.Add(textGeom);
Clip = group;
}
private BitmapSource CreateBitmapSource()
{
int width = 128;
int height = width;
int stride = width / 8;
byte[] pixels = new byte[height * stride];
List<System.Windows.Media.Color> colors = new List<System.Windows.Media.Color>();
colors.Add(System.Windows.Media.Colors.Red);
colors.Add(System.Windows.Media.Colors.Blue);
colors.Add(System.Windows.Media.Colors.Green);
BitmapPalette myPalette = new BitmapPalette(colors);
return BitmapSource.Create(width,
height,
96,
96,
PixelFormats.Indexed1,
myPalette,
pixels,
stride);
}
}
Upvotes: 3
Reputation: 34240
Here's a solution that works by creating a new FrameworkElement
called HollowTextBlock
that fills its allocated size with its Background
and clips away the Text
leaving it transparent. A full-fledged implementation would require supporting a lot more properties but this proves that the concept works.
First, here is some sample XAML:
<Grid>
<Grid>
<Grid.Background>
<LinearGradientBrush StartPoint="0,0" EndPoint="1,0">
<GradientStop Color="#FF0000" Offset="0" />
<GradientStop Color="#0000FF" Offset="1" />
</LinearGradientBrush>
</Grid.Background>
</Grid>
<Grid>
<local:HollowTextBlock Width="200" Height="50" Text="Hello, world!" Background="White" HorizontalAlignment="Center"/>
</Grid>
</Grid>
which defines a colorful gradient background behind the text so we can see that it's working. Then we create our HollowTextBlock
and in the prototype Width
, Height
, Text
and Background
must all be specified.
Then here is our implementation of HollowTextBlock
:
public class HollowTextBlock : FrameworkElement
{
public string Text
{
get { return (string)GetValue(TextProperty); }
set { SetValue(TextProperty, value); }
}
public static readonly DependencyProperty TextProperty =
DependencyProperty.Register("Text", typeof(string), typeof(HollowTextBlock), new UIPropertyMetadata(string.Empty));
public Brush Background
{
get { return (Brush)GetValue(BackgroundProperty); }
set { SetValue(BackgroundProperty, value); }
}
public static readonly DependencyProperty BackgroundProperty =
TextElement.BackgroundProperty.AddOwner(typeof(HollowTextBlock), new FrameworkPropertyMetadata(null, FrameworkPropertyMetadataOptions.AffectsRender));
protected override void OnRender(DrawingContext drawingContext)
{
base.OnRender(drawingContext);
var extent = new RectangleGeometry(new Rect(0.0, 0.0, RenderSize.Width, RenderSize.Height));
var face = new Typeface("Arial");
var size = 32;
var ft = new FormattedText(Text, Thread.CurrentThread.CurrentUICulture, FlowDirection.LeftToRight, face, size, Brushes.Black);
var hole = ft.BuildGeometry(new Point((RenderSize.Width - ft.Width) / 2, (RenderSize.Height - ft.Height) / 2));
var combined = new CombinedGeometry(GeometryCombineMode.Exclude, extent, hole);
drawingContext.PushClip(combined);
drawingContext.DrawRectangle(Background, null, new Rect(0.0, 0.0, RenderSize.Width, RenderSize.Height));
drawingContext.Pop();
}
}
and the rendered output looks like this:
N.B. When using it you have to be careful not to put it on top of something that will obscure the real background that you would like to show through.
Upvotes: 2