63 votes

Comment ouvrir une fenêtre contextuelle WPF lorsqu'un autre contrôle est cliqué, en utilisant uniquement le balisage XAML ?

J'ai deux contrôles, un TextBlock et un PopUp. Lorsque l'utilisateur clique (MouseDown) sur le bloc de texte, je veux afficher la fenêtre contextuelle. Je pensais pouvoir le faire avec un EventTrigger sur le Popup, mais je ne peux pas utiliser de setters dans un EventTrigger, je ne peux que lancer des storyboards. Je veux faire cela strictement en XAML, car les deux contrôles sont dans un modèle et je ne sais pas comment trouver le popup dans le code.

C'est ce que je veux faire en théorie, mais je ne peux pas le faire parce que vous ne pouvez pas mettre un setter dans un EventTrigger (comme vous pouvez le faire avec un DataTrigger) :

<TextBlock x:Name="CCD">Some text</TextBlock>

<Popup>
    <Popup.Style>
        <Style>
            <Style.Triggers>
                <EventTrigger SourceName="CCD" RoutedEvent="MouseDown">
                    <Setter Property="Popup.IsOpen" Value="True" />
                </EventTrigger>
            </Style.Triggers>
        </Style>
    </Popup.Style>
...

Quelle est la meilleure façon d'afficher une popup strictement dans XAML lorsqu'un événement se produit sur un autre contrôle ?

88voto

John Melville Points 1140

J'ai fait quelque chose de simple, mais ça marche.

J'ai utilisé un ToggleButton classique, que j'ai transformé en bloc de texte en modifiant son modèle de contrôle. Ensuite, j'ai simplement lié la propriété IsChecked du ToggleButton à la propriété IsOpen de la fenêtre popup. La fenêtre contextuelle possède certaines propriétés, comme StaysOpen, qui vous permettent de modifier le comportement de fermeture.

Ce qui suit fonctionne dans XamlPad.

 <StackPanel>
  <ToggleButton Name="button"> 
    <ToggleButton.Template>
      <ControlTemplate TargetType="ToggleButton">
        <TextBlock>Click Me Here!!</TextBlock>
      </ControlTemplate>      
    </ToggleButton.Template>
  </ToggleButton>
  <Popup IsOpen="{Binding IsChecked, ElementName=button}" StaysOpen="False">
    <Border Background="LightYellow">
      <TextBlock>I'm the popup</TextBlock>
    </Border>
  </Popup> 
 </StackPanel>

4 votes

Voir aussi l'approche de @Qwertie - une version plus utile inspirée de celle-ci qui fermera automatiquement le popup lorsque vous faites un alt-tab ou un clic en dehors du popup.

54voto

Qwertie Points 5311

L'approche suivante est la même que celle de Helge Klein, sauf que la fenêtre contextuelle se ferme automatiquement lorsque vous cliquez n'importe où à l'extérieur de la fenêtre contextuelle (y compris le ToggleButton lui-même) :

<ToggleButton x:Name="Btn" IsHitTestVisible="{Binding ElementName=Popup, Path=IsOpen, Mode=OneWay, Converter={local:BoolInverter}}">
    <TextBlock Text="Click here for popup!"/>
</ToggleButton>

<Popup IsOpen="{Binding IsChecked, ElementName=Btn}" x:Name="Popup" StaysOpen="False">
    <Border BorderBrush="Black" BorderThickness="1" Background="LightYellow">
        <CheckBox Content="This is a popup"/>
    </Border>
</Popup>

"BoolInverter" est utilisé dans la liaison IsHitTestVisible de sorte que lorsque vous cliquez à nouveau sur le ToggleButton, la fenêtre contextuelle se ferme :

public class BoolInverter : MarkupExtension, IValueConverter
{
    public override object ProvideValue(IServiceProvider serviceProvider)
    {
        return this;
    }

    public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
    {
        if (value is bool)
            return !(bool)value;
        return value;
    }
    public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
    {
        return Convert(value, targetType, parameter, culture);
    }
}

...qui montre la technique pratique de combinaison de IValueConverter et MarkupExtension en un.

J'ai découvert un problème avec cette technique : WPF est bogué lorsque deux popups sont sur l'écran en même temps. Plus précisément, si votre bouton de basculement se trouve sur le "popup de débordement" d'une barre d'outils, deux popups seront ouverts après que vous aurez cliqué dessus. Il se peut alors que la deuxième fenêtre contextuelle (la vôtre) reste ouverte lorsque vous cliquez sur un autre élément de votre fenêtre. À ce stade, il est difficile de fermer la fenêtre contextuelle. L'utilisateur ne peut pas cliquer à nouveau sur le ToggleButton pour fermer la fenêtre contextuelle car IsHitTestVisible est faux puisque la fenêtre contextuelle est ouverte ! Dans mon application, j'ai dû utiliser quelques astuces pour atténuer ce problème, comme le test suivant sur la fenêtre principale, qui dit (avec la voix de Louis Black) "si la fenêtre pop-up est ouverte et que l'utilisateur clique quelque part en dehors de la fenêtre pop-up, fermez cette foutue fenêtre pop-up" :

PreviewMouseDown += (s, e) =>
{
    if (Popup.IsOpen)
    {
        Point p = e.GetPosition(Popup.Child);
        if (!IsInRange(p.X, 0, ((FrameworkElement)Popup.Child).ActualWidth) ||
            !IsInRange(p.Y, 0, ((FrameworkElement)Popup.Child).ActualHeight))
            Popup.IsOpen = false;
    }
};
// Elsewhere...
public static bool IsInRange(int num, int lo, int hi) => 
    num >= lo && num <= hi;

1 votes

Cela fonctionne très bien, y compris lorsque l'on passe à une autre application ou que l'on clique ailleurs dans la fenêtre.

1 votes

Le paramètre StaysOpen="false" est la clé du comportement de fermeture automatique hors composant. Pourquoi la valeur par défaut est true ?

1 votes

@ChrisDolan Je suis tellement ennuyé par des choses comme les valeurs par défaut qui vont à l'encontre de tous les principes de l'UX que je connais. Donc, je crée un style implicite pour Popup et définir les paramètres par défaut que j'aime, notamment StaysOpen=False .

15voto

BatteryBackupUnit Points 2509

Les utilisations suivantes EventTrigger pour montrer le Popup . Cela signifie que nous n'avons pas besoin d'un ToggleButton pour la liaison d'état. Dans cet exemple, le Click événement d'un Button est utilisé. Vous pouvez l'adapter pour utiliser une autre combinaison élément/événement.

<Button x:Name="OpenPopup">Popup
    <Button.Triggers>
        <EventTrigger RoutedEvent="Button.Click">
            <EventTrigger.Actions>
                <BeginStoryboard>
                    <Storyboard>
                        <BooleanAnimationUsingKeyFrames 
                                 Storyboard.TargetName="ContextPopup" 
                                 Storyboard.TargetProperty="IsOpen">
                            <DiscreteBooleanKeyFrame KeyTime="0:0:0" Value="True" />
                        </BooleanAnimationUsingKeyFrames>
                    </Storyboard>
                </BeginStoryboard>
            </EventTrigger.Actions>
        </EventTrigger>
    </Button.Triggers>
</Button>
<Popup x:Name="ContextPopup"
       PlacementTarget="{Binding ElementName=OpenPopup}"
       StaysOpen="False">
    <Label>Popupcontent...</Label>
</Popup>

Veuillez noter que le Popup fait référence à la Button par son nom et vice versa. Donc x:Name="..." est nécessaire sur les deux, le Popup et le Button .

Il peut en fait être encore simplifié en remplaçant le Storyboard à l'aide d'une SetProperty EventTrigger Action décrite dans ce SO Réponse

8voto

bendewey Points 25437

J'ai eu quelques problèmes avec la partie MouseDown, mais voici un code qui pourrait vous aider à démarrer.

 <Window x:Class="WpfApplication1.Window1"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    Title="Window1" Height="300" Width="300">
    <Grid>
        <Control VerticalAlignment="Top">
            <Control.Template>
                <ControlTemplate>
                    <StackPanel>
                    <TextBox x:Name="MyText"></TextBox>
                    <Popup x:Name="Popup" PopupAnimation="Fade" VerticalAlignment="Top">
    					<Border Background="Red">
    						<TextBlock>Test Popup Content</TextBlock>
    					</Border>
    				</Popup>
                    </StackPanel>
                    <ControlTemplate.Triggers>
                    	<EventTrigger RoutedEvent="UIElement.MouseEnter" SourceName="MyText">
                    		<BeginStoryboard>
                    			<Storyboard>
                    				<BooleanAnimationUsingKeyFrames Storyboard.TargetName="Popup" Storyboard.TargetProperty="(Popup.IsOpen)">
                    					<DiscreteBooleanKeyFrame KeyTime="00:00:00" Value="True"/>
                    				</BooleanAnimationUsingKeyFrames>
                    			</Storyboard>
                    		</BeginStoryboard>
                    	</EventTrigger>
    					<EventTrigger RoutedEvent="UIElement.MouseLeave" SourceName="MyText">
                    		<BeginStoryboard>
                    			<Storyboard>
                    				<BooleanAnimationUsingKeyFrames Storyboard.TargetName="Popup" Storyboard.TargetProperty="(Popup.IsOpen)">
                    					<DiscreteBooleanKeyFrame KeyTime="00:00:00" Value="False"/>
                    				</BooleanAnimationUsingKeyFrames>
                    			</Storyboard>
                    		</BeginStoryboard>
                    	</EventTrigger>
                    </ControlTemplate.Triggers>
                </ControlTemplate>
            </Control.Template>
        </Control>
    </Grid>
</Window>
 

0voto

Mike Points 1

Une autre façon de le faire :

<Border x:Name="Bd" BorderBrush="{TemplateBinding BorderBrush}" BorderThickness="{TemplateBinding BorderThickness}" Background="{TemplateBinding Background}" Padding="{TemplateBinding Padding}" SnapsToDevicePixels="true">
                    <StackPanel>
                        <Image Source="{Binding ProductImage,RelativeSource={RelativeSource TemplatedParent}}" Stretch="Fill" Width="65" Height="85"/>
                        <ContentPresenter HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}" SnapsToDevicePixels="{TemplateBinding SnapsToDevicePixels}" VerticalAlignment="{TemplateBinding VerticalContentAlignment}"/>
                        <Button x:Name="myButton" Width="40" Height="10">
                            <Popup Width="100" Height="70" IsOpen="{Binding ElementName=myButton,Path=IsMouseOver, Mode=OneWay}">
                                <StackPanel Background="Yellow">
                                    <ItemsControl ItemsSource="{Binding Produkt.SubProducts}"/>
                                </StackPanel>
                            </Popup>
                        </Button>
                    </StackPanel>
                </Border>

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X