How to create transparent text with opaque background in WPF C#

916 Views Asked by At

Anyone have an idea of how to even approach this?

I dont have any code to present as I don`t have a clue where to start, thats why Im posting this.

My plan is to dynamically load a string array of some 7000 words over the WPF App WrapWindow with hidden background image. The text should be transparent to expose the image behind (something like Tag Cloud). I have attached link to the example which will help you to visualize the problem.

I have tried using ImageBrush but it does not support direct content (i.e. a string). I have also tried using Opacity Masks but without any success.

To give you an idea, below is the image and link to the similar unanswered topic but with an effect Im trying to achieve.

transparent text with opaque background

enter image description here

Any ideas are welcomed.

Thanks

OK, so this is what I have currently and what I was able to achieve with single hard coded string.

Simple xaml, image as background and WrapPanel which gets dynamically populated by the TEST STRING in form of a button.

<Grid>
   <Image Source="/Images/test.jpg" Stretch="Fill"/>
   <WrapPanel x:Name="WrapPan"/>
</Grid>

And just a draft copy of the code behind.

public partial class MainWindow : Window
    {
        public MainWindow()
        {
            InitializeComponent();
            LoadData();
        }

        string[] test = { "TEST STRING", "TEST STRING", "TEST STRING", "TEST STRING", "TEST STRING",
        "TEST STRING", "TEST STRING", "TEST STRING", "TEST STRING", "TEST STRING",
        "TEST STRING", "TEST STRING", "TEST STRING", "TEST STRING", "TEST STRING",
        "TEST STRING", "TEST STRING", "TEST STRING", "TEST STRING", "TEST STRING",
        "TEST STRING", "TEST STRING", "TEST STRING", "TEST STRING", "TEST STRING",
        "TEST STRING", "TEST STRING", "TEST STRING", "TEST STRING", "TEST STRING",
        "TEST STRING", "TEST STRING", "TEST STRING", "TEST STRING", "TEST STRING",
        "TEST STRING", "TEST STRING", "TEST STRING", "TEST STRING", "TEST STRING",
        "TEST STRING", "TEST STRING", "TEST STRING", "TEST STRING", "TEST STRING",};

        private void LoadData()
        {
            for (int i = 0; i < test.Length; i++)
            {
                Button BtnWithName = new Button
                {
                    Style = (Style)FindResource("MaterialDesignFlatButton"),
                    Content = test[i].ToString(),
                    Padding = new Thickness(0, 0, 5, 0),
                    Height = 12,
                    Command = DialogHost.OpenDialogCommand,
                    FontSize = 10,
                    Foreground = Brushes.White,
                    Opacity = 1,
                    FontFamily = new FontFamily("Arial Black"),
                    ToolTip = test[i].ToString(),

                };

                Button BtnClose = new Button
                {
                    Content = "Close",
                    Command = DialogHost.CloseDialogCommand,
                    Width = 100
                };

                Grid myGrid = new Grid();
                myGrid.Height = 200;
                myGrid.Width = 200;

                RowDefinition gridRow1 = new RowDefinition();
                RowDefinition gridRow2 = new RowDefinition();

                myGrid.RowDefinitions.Add(gridRow1);
                myGrid.RowDefinitions.Add(gridRow2);

                TextBlock textBlock = new TextBlock();
                textBlock.HorizontalAlignment = HorizontalAlignment.Center;
                textBlock.Text = test[i].ToString();

                StackPanel sp = new StackPanel();
                sp.HorizontalAlignment = HorizontalAlignment.Center;
                sp.Children.Add(textBlock);
                myGrid.Children.Add(sp);
                Grid.SetRow(sp, 0);

                myGrid.Children.Add(BtnClose);
                Grid.SetRow(BtnClose, 1);

                myGrid.VerticalAlignment = VerticalAlignment.Center;


                BtnWithName.CommandParameter = myGrid;
                WrapPan.Children.Add(BtnWithName);

            }
        }   
    }

And this gives me the output below:

enter image description here

Then, after some experimentation I have fund the way to apply image opacity mask with VisulaBrush set to TextBlock which give me the desired effect. Now the question remains of how I will dynamically do the same thing as above with an array of strings but with the effect as below?

       <Image Source="/Images/test.jpg" Stretch="Fill">
            <Image.Effect>
                <BlurEffect Radius="0"/>
            </Image.Effect>
            <Image.OpacityMask>
                <VisualBrush>
                    <VisualBrush.Visual >
                        <TextBlock Text="test" />
                    </VisualBrush.Visual>
                </VisualBrush>
            </Image.OpacityMask>
        </Image>

enter image description here

BrushVisual can have only one Item so I placed the WrapPanel in MaterialDesign DialogHost. I can now populate the WrapPanel with my strings. The problem I have now is that whatever I do I cant control the Text size on my Button and the more buttons I put the image gets distorted and squeezed (image below) and stretches or compacts when Im resizing the window.

<Image Source="/Images/test.jpg" Stretch="Fill">
    <Image.Effect>
        <BlurEffect Radius="0"/>
    </Image.Effect>
   <Image.OpacityMask>
        <VisualBrush >
            <VisualBrush.Visual>
                <materialDesign:DialogHost>
                    <WrapPanel  x:Name="WrapPan"/>
                </materialDesign:DialogHost>
            </VisualBrush.Visual>
        </VisualBrush>
    </Image.OpacityMask> 
</Image>

enter image description here

1

There are 1 best solutions below

1
John Schruben On

You could clip a canvas with the geometry of the text outline. Here is a class that outlines text as a shape.

public class OutlinedText : Shape
{
    public static readonly DependencyProperty TextProperty =
       DependencyProperty.Register(
       "Text",
       typeof(string),
       typeof(OutlinedText));

    private Geometry testGeometry;

    public OutlinedText()
    {
        FontFamily = new FontFamily("Arial");
        FontWeight = FontWeights.ExtraBold;
        FontStyle = FontStyles.Normal;
        this.FontSize = 14;
    }

    public FontFamily FontFamily { get; set; }

    public int FontSize { get; set; }

    public FontStyle FontStyle { get; set; }

    public FontWeight FontWeight { get; set; }

    public Point Origin { get; private set; }

    public string Text
    {
        get { return (string)GetValue(TextProperty); }
        set { this.SetValue(TextProperty, value); }
    }

    protected override Geometry DefiningGeometry => this.testGeometry ?? Geometry.Empty;

    public static void OnPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) => ((OutlinedText)d).CreateTextGeometry();

    protected override Size MeasureOverride(Size availableSize)
    {
        if (this.testGeometry == null)
        {
            this.CreateTextGeometry();
        }

        if (this.testGeometry.Bounds == Rect.Empty)
        {
            return new Size(0, 0);
        }

        return new Size(Math.Min(availableSize.Width, this.testGeometry.Bounds.Width), Math.Min(availableSize.Height, this.testGeometry.Bounds.Height));
    }

    private void CreateTextGeometry()
    {
        var formattedText = new FormattedText(
            this.Text,
            Thread.CurrentThread.CurrentUICulture,
            FlowDirection.LeftToRight,
            new Typeface(FontFamily, FontStyle, FontWeight, FontStretches.Normal),
            this.FontSize,
            Brushes.Black);
        this.testGeometry = formattedText.BuildGeometry(this.Origin);
    }
}

Using it in xaml as normal

<local:OutlinedText
        Fill="Transparent"
        FontSize="16"
        Stroke="Black"
        StrokeThickness="1"
        Text="Test text"
        Visibility="Visible" />

So if you had a canvas with a white background you can clip it with the inverse of your text path. To invert a clip create a path that combines the path geometry of the text with a rect of the same dimensions as the canvas. Use the combined geometry mode of "exclude".

 <Canvas
        x:Name="canvas"
        Background="White"
        Clip="{Binding ElementName=path, Path=RenderedGeometry}" />

    <Path Name="path" Fill="White">
        <Path.Data>
            <CombinedGeometry
                Geometry1="{Binding ElementName=rect, Path=RenderedGeometry}"
                Geometry2="{Binding ElementName=ot, Path=RenderedGeometry}"
                GeometryCombineMode="Exclude" />
        </Path.Data>
    </Path>
    <local:OutlinedText
        x:Name="ot"
        HorizontalAlignment="Center"
        VerticalAlignment="Center"
        Panel.ZIndex="1"
        Fill="Red"
        FontSize="16"
        Stroke="Black"
        StrokeThickness="1"
        Text="Test text"
        Visibility="Hidden" />
    <Rectangle
        x:Name="rect"
        Width="{Binding ElementName=canvas, Path=ActualWidth}"
        Height="{Binding ElementName=canvas, Path=ActualHeight}" />