WPF Context Menus

context menu is a menu that pops up at some point on your interface when you click the right mouse button. It’s not attached to the menu bar at the top. As an example, we’ll examine the code we used for the printing demo in the last post, and in the process add a feature to it that illustrates some of the properties of context menus.

A quick reminder of what this application does. It displays an A4-sized Canvas on which the user could add TextBoxes by right-clicking with the mouse and selecting ‘Add TextBox’ from the popup menu. It also allows you to add multiple pages and print them, but we dealt with that aspect in the last post.

The popup menu on the Canvas is a simple example of a context menu. We can add the menu by inserting a bit of XAML code:

        <Canvas Name="pageCanvas" Margin="0" HorizontalAlignment="Center" VerticalAlignment="Center"
                Height="1122.51968503937" Width="793.700787401575" Background="White" MouseRightButtonDown="pageCanvas_MouseRightButtonDown">
            <Canvas.ContextMenu>
                <ContextMenu Name="pageCanvasContextMenu">
                    <MenuItem x:Name="addTextBoxMenuItem"  Header="Add TextBox" Click="addTextBoxMenuItem_Click"/>
                </ContextMenu>
            </Canvas.ContextMenu>
        </Canvas>

Here we define the context menu in the Canvas.ContextMenu section. The MenuItems that are added to a context menu are the same as those added to a Menu in a fixed menu bar, so there’s no mystery here. The Header is the text that appears on the MenuItem, and we add an event handler for clicking on the MenuItem in the usual way.

One thing is worthy of note however. Annoyingly, the RoutedEventArgs parameter that is passed to the event handler doesn’t contain the position of the mouse when the menu was clicked, so we need to get that another way. What I’ve done here is add a MouseRightButtonDown event handler to the Canvas itself. We extract the mouse coordinates in that event handler and save them for use in the handler for the context menu, since we want to add the TextBox at the mouse location. The code for MouseRightButtonDown is:

    double rightX, rightY;
    private void pageCanvas_MouseRightButtonDown(object sender, MouseButtonEventArgs e)
    {
      rightX = e.GetPosition(pageCanvas).X;
      rightY = e.GetPosition(pageCanvas).Y;
    }

Now we can write the code for inserting the TextBox:

    private void addTextBoxMenuItem_Click(object sender, RoutedEventArgs e)
    {
      document.AddTextArea(RightX, RightY);
      DrawPage();
    }

The program defines a Document object which contains a list of TextAreas, as mentioned in the previous post. Each TextArea contains some text and the coordinates where the corresponding TextBox should be drawn. The link between TextArea (a non-graphical class) and TextBox (the WPF Control) is done via styles and data binding, but we won’t go into the details here, since we want to concentrate on the context menu. If you want the full code, you can download it via the link at the end.

That’s how you add a context menu to an object (such as a Canvas) that doesn’t have one by default. However, suppose you want to add a context menu to the TextBox itself. For example, the text you enter into the TextBox might make frequent use of some Unicode characters that aren’t available on your keyboard. It’s a pain to have to find these characters somewhere else (for example, in the ‘charmap’ application that comes with Windows) and copy and paste them into your program. In this example, we’ll add context menu items to the TextBox that allow the fractions ½ and ¼ to be added to the TextBox.

There’s one problem that confronts us, though. If you right-click on a standard TextBox, you’ll see that it already has a context menu containing the cut, copy and paste commands. How do we add our custom commands to the existing ones (or replace the existing ones)?

We need to define the custom context menu as a Window resource, so in the XAML we can write:

    <Window.Resources>
        <ContextMenu x:Key="TextBoxContextMenu" Background="White">
            <MenuItem Command="ApplicationCommands.Copy" />
            <MenuItem Command="ApplicationCommands.Cut" />
            <MenuItem Command="ApplicationCommands.Paste" />
            <MenuItem x:Name="insertHalfMenuItem" Header="Insert ½" Click="insertHalfMenuItem_Click"/>
            <MenuItem x:Name="insertQuarterMenuItem" Header="Insert ¼" Click="insertQuarterMenuItem_Click"/>
        </ContextMenu>
        <Style x:Key="TextBoxStyle" TargetType="TextBox">
            <Setter Property="AcceptsReturn" Value="True"/>
            <Setter Property="TextWrapping" Value="WrapWithOverflow"/>
            <Setter Property="BorderBrush" Value="RosyBrown"/>
            <Setter Property="FontSize" Value="12"/>
            <Setter Property="FontWeight" Value="Normal"/>
            <Setter Property="Text" Value="{Binding Content}" />
            <Setter Property="Height" Value="200"/>
            <Setter Property="Width" Value="150"/>
            <Setter Property="ContextMenu" Value="{StaticResource TextBoxContextMenu}"/>
        </Style>
    </Window.Resources>

We’ve given the complete resources used in this program. The top bit defines the ContextMenu, in which we’ve retained the three default commands and added two extra ones to allow us to insert fractions into the text.

In the Style, we set up the visual properties of the TextBox, and at the end, we define its ContextMenu property as the ContextMenu we’ve just created. When we create the TextBox in code, we assign its Style to be TextBoxStyle, which will attach the desired ContextMenu to it. This is done in the DrawPage() method in the MainWindow class:

    public void DrawPage(Canvas canvas)
    {
      canvas.Children.Clear();
      Page page = document.CurrentPage;
      foreach (TextArea textArea in page.TextBoxList)
      {
        TextBox textBox = new TextBox();
        textBox.Style = (Style)FindResource("TextBoxStyle");
        textBox.DataContext = textArea;
        Canvas.SetLeft(textBox, textArea.X);
        Canvas.SetTop(textBox, textArea.Y);
        canvas.Children.Add(textBox);
      }
    }

The TextBox is created and has its Style set. Its position on the Canvas uses the coordinates we got from the MouseRightButtonDown event handler earlier.

One final problem remains to be solved. As you’ve seen above, it is the TextArea objects that are stored in the Document, and the TextBoxes are created on the fly whenever we want to draw a page. How can we get hold of the TextBox in order to add some text to it in response to a context menu click? The TextBox doesn’t have an object name that is accessible outside the DrawPage() method, so it seems we’re a bit stuck.

If we examine the code for the context menu’s event handlers, we can see how to do it.

    private void insertHalfMenuItem_Click(object sender, RoutedEventArgs e)
    {
      addStringToTextBox(sender, "½");
    }

    private void insertQuarterMenuItem_Click(object sender, RoutedEventArgs e)
    {
      addStringToTextBox(sender, "¼");
    }

    private void addStringToTextBox(object sender, string text)
    {
      MenuItem contextMenuItem = (MenuItem)sender;
      TextBox stampBox = ((ContextMenu)contextMenuItem.Parent).PlacementTarget as TextBox;
      int caretIndex = stampBox.CaretIndex;
      stampBox.Text = stampBox.Text.Insert(stampBox.CaretIndex, text);
      stampBox.CaretIndex = caretIndex + 1;
    }

The sender of the context menu event is the MenuItem that was clicked. The Parent of this MenuItem is the ContextMenu object itself, and its PlacementTarget is the UIElement that owns it. So by this roundabout route we can get the TextBox over which the mouse was right-clicked to produce the ContextMenu. The last bit of code makes sure the fraction is inserted at the correct location in the TextBox’s text by retrieving the CaretIndex. After the text has been inserted we position the CaretIndex just after it so the user can carry on typing in the usual way.

Get source code here.

Advertisements
Post a comment or leave a trackback: Trackback URL.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s

%d bloggers like this: