Bienvenue à Blogs CodeS-SourceS Identification | Inscription | Aide

Benjamin Roux

Silverlight Expert - Silverlight Fan - MVP Client App Dev

Actualités

  • Mon statut Live Messenger :





    View Benjamin Roux's profile on LinkedIn



    Benjamin Roux's Facebook Profile


    MVP Blog Badge.
[Silverlight] Gestion du menu contextuel

silverlight Bonjour à tous,

Une question qui revient souvent sur les forums dédiés à Silverlight est la gestion du menu contextuel (clic droit). La réponse actuelle est qu’il n’est pas possible nativement de surcharger les menu contextuel à la manière de Flash. Il semblerait qu’il faille attendre une future version de Silverlight…

En revanche je vais ici vous montrer une solution “passable” permettant de passer outre le clic droit classique. Il faut savoir que le code suivant fonctionne correctement sous IE mais pose quelques soucis sous Firefox. En effet, sous ce dernier, le menu classique s’affiche suivi du menu custom. Si quelqu’un connait une solution il peut se manifester dans les commentaires.

Pour rendre le tout facilement utilisable j’ai créé une classe RightClickService permettant de créer facilement des menu contextuels pour toute sorte de contrôle.

Tout d’abord voici le ContextMenuItem et le ContextMenu.

[TemplateVisualState(Name = "Normal", GroupName = "CommonStates"),
TemplateVisualState(Name = "Focused", GroupName = "FocusStates"),
TemplateVisualState(Name = "MouseOver", GroupName = "CommonStates"),
TemplateVisualState(Name = "Disabled", GroupName = "CommonStates"),
TemplateVisualState(Name = "Unselected", GroupName = "SelectionStates"),
TemplateVisualState(Name = "Selected", GroupName = "SelectionStates"),
TemplateVisualState(Name = "SelectedUnfocused", GroupName = "SelectionStates"),
TemplateVisualState(Name = "Unfocused", GroupName = "FocusStates")]
public class ContextMenuItem : ContentControl
{
    public event MouseButtonEventHandler Click;
 
    public ContextMenuItem()
    {
        this.DefaultStyleKey = typeof(ContextMenuItem);
        this.MouseLeftButtonDown += delegate(object sender, MouseButtonEventArgs e)
        {
            if (Click != null) Click(this, e);
        };
    }
 
    public override void OnApplyTemplate()
    {
        base.OnApplyTemplate();
        VisualStateManager.GoToState(this, "Normal", false);
    }
 
    protected override void OnMouseLeftButtonUp(MouseButtonEventArgs e)
    {
        if (!e.Handled)
        {
            base.OnMouseLeftButtonUp(e);
            e.Handled = true;
            if (RightClickService.Popup != null) RightClickService.Popup.IsOpen = false;
        }
    }
 
    protected override void OnMouseEnter(MouseEventArgs e)
    {
        base.OnMouseEnter(e);
        VisualStateManager.GoToState(this, "MouseOver", false);
    }
 
    protected override void OnMouseLeave(MouseEventArgs e)
    {
        base.OnMouseLeave(e);
        VisualStateManager.GoToState(this, "Normal", false);
    }
}

Il s’agit tout simplement d’un contrôle simple ressemblant à un ListBoxItem. J’ai également créé un event Click se déclanchant lors d’un MouseLeftButtonDown.

La thème du contrôle est le suivant :

<ResourceDictionary
  xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
  xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
  xmlns:local="clr-namespace:ContextMenu"    
  xmlns:vsm="clr-namespace:System.Windows;assembly=System.Windows">
    <Style TargetType="local:ContextMenu">
        <Setter Property="Padding" Value="1"/>
        <Setter Property="Background" Value="#FFFFFFFF" />
        <Setter Property="Foreground" Value="#FF000000"/>
        <Setter Property="HorizontalContentAlignment" Value="Left" />
        <Setter Property="VerticalContentAlignment" Value="Top" />
        <Setter Property="IsTabStop" Value="False" />
        <Setter Property="BorderThickness" Value="1" />
        <Setter Property="TabNavigation" Value="Once" />
        <Setter Property="BorderBrush">
            <Setter.Value>
                <LinearGradientBrush EndPoint="0.5,1" StartPoint="0.5,0">
                    <GradientStop Color="#FFA3AEB9" Offset="0"/>
                    <GradientStop Color="#FF8399A9" Offset="0.375"/>
                    <GradientStop Color="#FF718597" Offset="0.375"/>
                    <GradientStop Color="#FF617584" Offset="1"/>
                </LinearGradientBrush>
            </Setter.Value>
        </Setter>
        <Setter Property="Template">
            <Setter.Value>
                <ControlTemplate TargetType="local:ContextMenu">
                    <Border CornerRadius="2" 
                            BorderBrush="{TemplateBinding BorderBrush}"
                            BorderThickness="{TemplateBinding BorderThickness}">
                        <ItemsPresenter />
                    </Border>
                </ControlTemplate>
            </Setter.Value>
        </Setter>
    </Style>
 
    <Style TargetType="local:ContextMenuItem">
        <Setter Property="Padding" Value="3" />
        <Setter Property="HorizontalContentAlignment" Value="Left" />
        <Setter Property="VerticalContentAlignment" Value="Top" />
        <Setter Property="Background" Value="Transparent" />
        <Setter Property="BorderThickness" Value="1"/>
        <Setter Property="Template">
            <Setter.Value>
                <ControlTemplate TargetType="local:ContextMenuItem">
                    <Grid Background="{TemplateBinding Background}">
                        <vsm:VisualStateManager.VisualStateGroups>
                            <vsm:VisualStateGroup x:Name="CommonStates">
                                <vsm:VisualState x:Name="Normal" />
                                <vsm:VisualState x:Name="MouseOver">
                                    <Storyboard>
                                        <DoubleAnimation Storyboard.TargetName="fillColor" Storyboard.TargetProperty="Opacity" Duration="0" To=".35"/>
                                    </Storyboard>
                                </vsm:VisualState>
                                <vsm:VisualState x:Name="Disabled">
                                    <Storyboard>
                                        <DoubleAnimation Storyboard.TargetName="contentPresenter" Storyboard.TargetProperty="Opacity" Duration="0" To=".55" />
                                    </Storyboard>
                                </vsm:VisualState>
                            </vsm:VisualStateGroup>
                            <vsm:VisualStateGroup x:Name="SelectionStates">
                                <vsm:VisualState x:Name="Unselected" />
                                <vsm:VisualState x:Name="Selected">
                                    <Storyboard>
                                        <DoubleAnimation Storyboard.TargetName="fillColor2" Storyboard.TargetProperty="Opacity" Duration="0" To=".75"/>
                                    </Storyboard>
                                </vsm:VisualState>
                            </vsm:VisualStateGroup>
                            <vsm:VisualStateGroup x:Name="FocusStates">
                                <vsm:VisualState x:Name="Focused">
                                    <Storyboard>
                                        <ObjectAnimationUsingKeyFrames Storyboard.TargetName="FocusVisualElement" Storyboard.TargetProperty="Visibility" Duration="0">
                                            <DiscreteObjectKeyFrame KeyTime="0">
                                                <DiscreteObjectKeyFrame.Value>
                                                    <Visibility>Visible</Visibility>
                                                </DiscreteObjectKeyFrame.Value>
                                            </DiscreteObjectKeyFrame>
                                        </ObjectAnimationUsingKeyFrames>
                                    </Storyboard>
                                </vsm:VisualState>
                                <vsm:VisualState x:Name="Unfocused"/>
                            </vsm:VisualStateGroup>
                        </vsm:VisualStateManager.VisualStateGroups>
                        <Rectangle Fill="LightGray" IsHitTestVisible="False" RadiusX="1" RadiusY="1" />
                        <Rectangle x:Name="fillColor" Opacity="0" Fill="#FFBADDE9" IsHitTestVisible="False" RadiusX="1" RadiusY="1"/>
                        <Rectangle x:Name="fillColor2" Opacity="0" Fill="#FFBADDE9" IsHitTestVisible="False" RadiusX="1" RadiusY="1"/>
                        <ContentPresenter
                    x:Name="contentPresenter"
                    Content="{TemplateBinding Content}"
                    ContentTemplate="{TemplateBinding ContentTemplate}"
                    HorizontalAlignment="Left"
                    Margin="{TemplateBinding Padding}"/>
                        <Rectangle x:Name="FocusVisualElement" Stroke="#FF6DBDD1" StrokeThickness="1" Visibility="Collapsed" RadiusX="1" RadiusY="1" />
                    </Grid>
                </ControlTemplate>
            </Setter.Value>
        </Setter>
    </Style>
 
</ResourceDictionary>

Le ContextMenu :

public class ContextMenu : ItemsControl
{
    public ContextMenu()
    {
        base.DefaultStyleKey = typeof(ContextMenu);
    }
 
    public override void OnApplyTemplate()
    {
        base.OnApplyTemplate();
    }
 
    protected override DependencyObject GetContainerForItemOverride()
    {
        return new ContextMenuItem();
    }
 
    protected override bool IsItemItsOwnContainerOverride(object item)
    {
        return (item is ContextMenuItem);
    }
}

Ce contrôle hérite de ItemsControl, et surcharge les méthodes GetContainerForItemOverride et IsItemItsOwnContainerOverride pour définir le contrôle à utiliser pour l’affichage si on n’utilise pas directement un ContextMenuItem.

Voici maintenant la classe utile : RightClickService.

public class RightClickService
{
    private static FrameworkElement mRootVisual = null;
 
    internal static FrameworkElement RootVisual
    {
        get
        {
            SetRootVisual();
            return mRootVisual;
        }
    }
 
    private static void SetRootVisual()
    {
        if (mRootVisual == null && Application.Current != null)
        {
            mRootVisual = Application.Current.RootVisual as FrameworkElement;
        }
    }
 
    #region Attached Property
 
    public static readonly DependencyProperty ContextMenuProperty =
        DependencyProperty.RegisterAttached("ContextMenu", typeof(ContextMenu), typeof(RightClickService), null);
 
    public static void SetContextMenu(UIElement element, ContextMenu value)
    {
        element.SetValue(ContextMenuProperty, value);
    }
    public static ContextMenu GetContextMenu(UIElement element)
    {
        return (ContextMenu)element.GetValue(ContextMenuProperty);
    }
 
    #endregion
 
    static RightClickService()
    {
        if (Application.Current.Host.Settings.Windowless == false) throw new Exception("Your SL plugin must be initialize with Windowless to true");
 
        if (HtmlPage.IsEnabled) HtmlPage.Document.AttachEvent("oncontextmenu", RightClickService.OnContextMenu);
    }
 
    private static void OnContextMenu(object sender, HtmlEventArgs e)
    {
        IEnumerable<UIElement> elements = GetControls(e.OffsetX, e.OffsetY);
 
        if (elements != null)
        {
            foreach (UIElement element in elements)
            {
                ContextMenu menu = RightClickService.GetContextMenu(element);
 
                if (menu != null)
                {
                    PerformPlacement(menu, e.OffsetX, e.OffsetY);
                    break;
                }
            }
        }
 
        e.PreventDefault();
    }
 
    internal static Popup Popup { get; set; }
 
    private static void PerformPlacement(FrameworkElement content, int x, int y)
    {
        Canvas elementOutside = new Canvas();
        Canvas childCanvas = new Canvas();
 
        elementOutside.Background = new SolidColorBrush(Colors.Transparent);
 
        if (Popup != null)
        {
            Popup.IsOpen = false;
            if (Popup.Child is Canvas) ((Canvas)Popup.Child).Children.Clear();
        }
        Popup = new Popup();
 
        Popup.Child = childCanvas;
 
        elementOutside.MouseLeftButtonDown += new MouseButtonEventHandler((o, e) => Popup.IsOpen = false);
        elementOutside.Width = Application.Current.Host.Content.ActualWidth;
        elementOutside.Height = Application.Current.Host.Content.ActualHeight;
 
        childCanvas.Children.Add(elementOutside);
        childCanvas.Children.Add(content);
 
        Canvas.SetLeft(content, x);
        Canvas.SetTop(content, y);
 
        Popup.IsOpen = true;
    }
 
    private static IEnumerable<UIElement> GetControls(int x, int y)
    {
        return VisualTreeHelper.FindElementsInHostCoordinates(new Point(x, y), RootVisual);
    }
}

Il suffit simplement passer par l’évènement Javascript oncontextmenu.

Afin de spécifier le menu contextuel de chaque contrôle je passe par une attached property. Lorsque le clic droit est détecté je récupère tout d’abord le contrôle en utilisant les coordonnée de la souris et le VisualTreeHelper, puis le menu associé au contrôle. J’affiche ensuite le menu dans un Popup.

Petite astuce : j’utilise un canvas invisible faisant toute la taille de l’application permettant de fermer le menu lorsque l’utilisateur clique en dehors de celui-ci.

C’est terminé pour le contrôle.

L’utilisation est assez simple.

<UserControl x:Class="TestSilverlight.Page"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" 
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:cm="clr-namespace:ContextMenu;assembly=ContextMenu"
    Width="400" Height="300">
    <Grid x:Name="LayoutRoot" Background="White">
        <Image Source="silverlight.png">
            <cm:RightClickService.ContextMenu>
                <cm:ContextMenu>
                    <cm:ContextMenuItem Content="Save As" Click="SaveImage" />
                    <cm:ContextMenuItem Content="View Image" Click="ViewImage" />
                </cm:ContextMenu>
            </cm:RightClickService.ContextMenu>
        </Image>
    </Grid>
</UserControl>

Je rappelle juste que ce code ne fonctionne pas extrêmement bien sous Firefox.

Voici le lien pour les sources.

Have fun :)

Ce post vous a plu ? Ajoutez le dans vos favoris pour ne pas perdre de temps à le retrouver le jour où vous en aurez besoin :
Posted: jeudi 21 mai 2009 23:20 par broux
Classé sous : , ,

Commentaires

Arnaud Auroux a dit :

Nice job ;)

# mai 22, 2009 10:46

nk54 a dit :

Avec silverlight 3, il y a le composant Liquid (gratuit) de Vectorlight (je crois) qui permet de faire des jolies menus vite fait :)

Tu as un screen du rendu ? (si t'as ça de côté hein ^^)

Merci.

# juillet 7, 2009 11:49

broux a dit :

Pour le rendu, vu que j'ai utilisé le template d'une Listbox faut juste imaginer une listbox qui apparait quand tu fais clic droit.

# juillet 9, 2009 08:25
Les commentaires anonymes sont désactivés

Les 10 derniers blogs postés

- [SharePoint] Les sessions TechDays 2012… par Le blog de Patrick [MVP SharePoint] le il y a 4 heures et 22 minutes

- TechDays Paris 2012 : Session pleinière jour 3 par Blog Technique de Romelard Fabrice le 02-09-2012, 11:01

- Mishra Reader : un lecteur RSS très Zune Style en Open Source ! par Cyril Sansus le 02-09-2012, 08:28

- [framework 4] Les Tasks et le Thread UI par Fathi Bellahcene le 02-09-2012, 00:33

- Workflow Foundation 3 a un pied dans la tombe par Blog de Jérémy Jeanson le 02-08-2012, 22:15

- TechDays Paris 2012 : Nouvelles tendances du poste de travail - Bring Your own PC par Blog Technique de Romelard Fabrice le 02-08-2012, 19:42

- TechDays Paris 2012 : System Center Service Manager 2012 Vue d’ensemble par Blog Technique de Romelard Fabrice le 02-08-2012, 17:32

- TechDays Paris 2012 : Pleinière second jour par Blog Technique de Romelard Fabrice le 02-08-2012, 16:23

- TechDays Paris 2012 : Retour d'expérience sur la mise en place d'un Cloud Privé par Blog Technique de Romelard Fabrice le 02-08-2012, 16:04

- TechDays Paris 2012 : Comment SharePoint a sauvé mes TechDays par Blog Technique de Romelard Fabrice le 02-07-2012, 23:59