Make an image containing shadowed text in WPF and C#


[shadowed text]

Recently I wanted an image containing shadowed text similar to the following to put on my new Favorite Books page. It’s fairly easy to make this kind of text in Microsoft Word and then save an image of it in a file, but I wanted the result to have a transparent background. I could have converted white pixels into transparent ones, but that can cause some problems as described in my post Use transparency when drawing with anti-aliasing in C#.

So I decided to write this program.

This would have been relatively straightforward in a Windows Forms program except for the most important part: drawing the shadowed text. WPF has a DropShadowBitmapEffect class that makes this easy. For some reason, Microsoft has marked that class as obsolete, but until they completely remove it, I’m going to use it!

True to its unofficial slogan, “Twice as flexible and only five times as hard,” the WPF program took a lot longer than it should have, but the result is pretty useful. Here are the main hurdles.

Font Weight

The program’s XAML code is mostly straightforward. The window contains a Grid with eight rows and two columns. The first three rows simply contain labels and text boxes. The next three rows are a bit more interesting.

Ideally the font weight row should contain the allowed font weights: thin, extra-light, light, normal, medium, semi-bold, bold, extra-bold, black, and extra-black. However, when the code sets the display label’s FontWeight property, it must convert those textual values into a proper font weight. The weights are defined to have the numeric values 100, 200, …, 900, and 950 (for extra-black), but even that’s what you need. The FontWeight property must take a FontWeight value that is defined by a property of the FontWeights class. That means you can’t simply set the property equal to the textual value or the numeric value.

The solution is actually fairly simple after you figure it out. When the program loads, it uses the following code to initialize the font weight combo box.

// Build the Font Weight combo box.
private void Window_Loaded(object sender, RoutedEventArgs e)
{
    // Build the weight combo box.
    cboWeight.Items.Add(FontWeights.Thin);
    cboWeight.Items.Add(FontWeights.ExtraLight);
    cboWeight.Items.Add(FontWeights.Light);
    cboWeight.Items.Add(FontWeights.Regular);
    cboWeight.Items.Add(FontWeights.Medium);
    cboWeight.Items.Add(FontWeights.SemiBold);
    cboWeight.Items.Add(FontWeights.Bold);
    cboWeight.Items.Add(FontWeights.ExtraBold);
    cboWeight.Items.Add(FontWeights.Black);
    cboWeight.Items.Add(FontWeights.ExtraBlack);
    cboWeight.SelectedIndex = 3;
}

This code adds the values defined by the FontWeights class to the combo box. Those values’ ToString methods return their textual values, so that is what the combo box displays. Later, when it needs to build the sample font, the code uses the following statement to convert the current combo box selection into the correct FontWeight value.

When the user selects an item in the combo box, the following code executes.

lblResult.FontWeight = (FontWeight)cboWeight.SelectedItem;

Colors

In a Windows Forms application, it would be easy to display a color sample. If the user clicked the sample, the program could display a color dialog to let the user pick any color.

Unfortunately, WPF does not include a color selection dialog. You could build one (a lot of work to do properly) or include enough of the Windows Forms libraries to let you use its dialog. I decided to display sample colors that you can pick.

The following XAML code shows how the program defines the text color samples.

<StackPanel Grid.Row="4" Grid.Column="1" Orientation="Horizontal">
    <StackPanel.Resources>
        <Style TargetType="Canvas">
            <Setter Property="Margin" Value="0,0,3,0"/>
        </Style>
    </StackPanel.Resources>
    <Canvas Width="20" Height="20" Background="Black" MouseDown="TextColor_MouseDown"/>
    <Canvas Width="20" Height="20" Background="White" MouseDown="TextColor_MouseDown"/>
    <Canvas Width="20" Height="20" Background="Green" MouseDown="TextColor_MouseDown"/>
    <Canvas Width="20" Height="20" Background="Blue" MouseDown="TextColor_MouseDown"/>
    <Canvas Width="20" Height="20" Background="Red" MouseDown="TextColor_MouseDown"/>
    <Canvas Width="20" Height="20" Background="Yellow" MouseDown="TextColor_MouseDown"/>
    <Canvas Width="20" Height="20" Background="Orange" MouseDown="TextColor_MouseDown"/>
</StackPanel>

This code starts with a horizontal StackPanel. Its resources add a margin around any Canvas control contained in the StackPanel. (That way you don’t need to add a margin around each sample individually.)

[shadowed text]

The StackPanel contains a sequence of Canvas objects, each having a different Background property. (Note that the second Canvas is white so it’s invisible on the white background used by the program. When you run the program, you need to know it’s there if you want to use white text or a white shadow.)

When you click any of these Canvas controls, the following code executes.

// The text and shadow colors.
private SolidColorBrush TextBrush = Brushes.Black;
private SolidColorBrush ShadowBrush = Brushes.Black;

private void TextColor_MouseDown(object sender, MouseButtonEventArgs e)
{
    Canvas canvas = sender as Canvas;
    TextBrush = (SolidColorBrush)canvas.Background;
    ShowText();
}

This code first defines two form-level SolidColorBrush objects to record the colors used by the shadowed text and its shadow. In the TextColor_MouseDown event handler, we know that the control that raised the event is a Canvas control, so the code converts the sender parameter into a Canvas.

When you set a control’s Background property to a color in XAML code, you are actually setting the property to a SolidColorBrush that displays that color. The event handler simply saves the Background property in the TextBrush variable for later use. However, the Background property has the type Brush (it could hold other kinds of brushes such as gradient brushes or image brushes), so the code casts it into a SolidColorBrush before saving it in the TextBrush variable.

The code that manages the shadow color samples is similar.

Display the Sample

Whenever the user changes one of the sample text parameters, the program calls the following ShowText method to display a sample of the text.

// Display the sample text.
private void ShowText()
{
    if (!this.IsLoaded) return;

    try
    {
        lblResult.Content = txtText.Text;
        lblResult.FontFamily = new FontFamily(txtFont.Text);
        lblResult.FontSize = int.Parse(txtFontSize.Text);
        lblResult.FontWeight = (FontWeight)cboWeight.SelectedItem;
        lblResult.Foreground = TextBrush;

        DropShadowBitmapEffect effect =
            lblResult.BitmapEffect as DropShadowBitmapEffect;
        effect.Color = ShadowBrush.Color;
    }
    catch
    {
    }
}

The method first checks whether the window has finished loading and exits if it has not. This is important because the controls’ event handlers execute when the window first loads and sets their values. For example, when the program sets the initial value for the topmost text box, it triggers that control’s TextChanged event and the event handler calls the ShowText method. The method then tries to access the values of all of the controls, and many of them do not yet exist. When the program tries to access the font name, size, or weight, the corresponding control is null so the program crashes. The sample label also doesn’t exist yet, so if the program tries to display a sample, it crashes.

After it has verified that the window is loaded so all of its controls exist, the ShowText method sets properties for the lblSample Label control.

The method’s final steps create a DropShadowBitmapEffect object and set its Color property. That property is of the Color data type, so the code gets the color from the ShadowBrush variable.

Saving the Result

When you click the Save Image button, the following code executes.

// Save the grdText control's image.
private void btnSave_Click(object sender, RoutedEventArgs e)
{
    SaveFileDialog dlg = new SaveFileDialog();
    dlg.FileName = txtText.Text;        // Default name.
    dlg.DefaultExt = ".png";            // Default extension.
    dlg.Filter = "PNG Files|*.png|All files|*.*";
    dlg.FilterIndex = 0;
    
    // Display the dialog.
    if (dlg.ShowDialog() == true)
    {
        SaveControlImage(grdText, dlg.FileName);
    }
}

This code creates a SaveFileDialog object and sets its FileName to the sample text. It sets the default extension to .png, sets the filter to search for .png files or all files, and selects the first filter. (The one that searches for .png files.) The program does not bother with other kinds of files such as .jpg or .gif files because they don’t support transparency.

The program then displays the dialog. If the user picks a file and clicks Save, the program calls the SaveControlImage method passing it the Grid control grdText, which contains the sample label.

Notice that the code explicitly compares the result of the dialog’s ShowDialog method to the value true. It cannot simply say, if (dlg.ShowDialog()) because that method returns Nullable<bool> instead of a simple bool. I don’t know why they made it return a nullable value because it always returns either true or false, never null, but there you are.

The following code shows how the program defines the grdText control and the sample label that it contains.

<Grid Grid.Row="6" Grid.Column="0" Grid.ColumnSpan="2"
    Background="Transparent" Name="grdText" >
    <Label HorizontalAlignment="Center" VerticalAlignment="Center"
        FontSize="50" FontFamily="Brush Script MT"
        Name="lblResult" Content="Favorite Books">
            <Label.BitmapEffect>
                <DropShadowBitmapEffect Color="Black"
                Direction="-45" ShadowDepth="10" Softness=".7"/>
            </Label.BitmapEffect>
    </Label>
</Grid>

This code defines the grid and sample label. The most important non-obvious thing to notice is that the grid’s Background property is set to Transparent. When the SaveControlImage method saves the grid’s image, it saves the transparent background.

Speaking of the SaveControlImage method, you can read more about it in my post Save WPF control images in C#.

Summary

When all’s said and done, the program isn’t particularly long. It’s getting there that’s the challenge.

Download the example to experiment with it and see additional details. Go to my post Save WPF control images in C# to learn more about the SaveControlImage method.


Download Example   Follow me on Twitter   RSS feed   Donate




About RodStephens

Rod Stephens is a software consultant and author who has written more than 30 books and 250 magazine articles covering C#, Visual Basic, Visual Basic for Applications, Delphi, and Java.
This entry was posted in graphics, image processing and tagged , , , , , , , , . Bookmark the permalink.

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.