I created a ribbon in WPF using .NET 4.0 framework. I am trying to make use of ToolTipTitle, ToolTipDescription and ToolTipFooterDescription. Since my tooltips will have hyperlinks, how can I make it so that when the mouse hovers over the tooltip, the tooltip stays open?
<rib:RibbonMenuButton ToolTipTitle="Title" ToolTipDescription="My Description" ToolTipFooterDescription="My Footer With a Link">
A good example of this functionality is with Microsoft Excel. When you hover over a ribbon button, an advanced tool tip will display to the user, and if the user hovers over the tooltip, it will remain open. I am trying to mimic that functionality.
I've updated my answer, the below is a well tested solution to my problem and will mimic Microsoft Office's tooltip generation:
First, create the popup control:
<Popup x:Class="WPF.Tooltip" AllowsTransparency="True"
x:Name="TT_Popup_Control"
xmlns:WPF="clr-namespace:Project_Namespace.WPF"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="clr-namespace:Project_Namespace"
mc:Ignorable="d"
d:DesignHeight="100" d:DesignWidth="300">
<Grid>
<Grid.Resources>
<Style TargetType="TextBlock">
<Setter Property="Margin" Value="5" />
<Setter Property="FontFamily" Value="Sans Serif" />
<Setter Property="FontSize" Value="11" />
<Setter Property="TextWrapping" Value="Wrap" />
</Style>
<Style TargetType="{x:Type TextBlock}" x:Key="WrappingStyle">
<Setter Property="TextWrapping" Value="Wrap"/>
</Style>
</Grid.Resources>
<Border BorderBrush="LightGray" BorderThickness="1,1,0,0">
<Border
BorderThickness="0,0,15,15"
BorderBrush="Transparent"
CornerRadius="0"
Margin="0"
>
<Border.Effect>
<DropShadowEffect BlurRadius="10" Opacity="0.8" ShadowDepth="5" Direction="-40" RenderingBias="Quality" />
</Border.Effect>
<StackPanel Background="#efefef">
<Grid Margin="5,0,5,0">
<Grid.RowDefinitions>
<RowDefinition Height="*" />
<RowDefinition Height="*" />
<RowDefinition Height="*" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="1*" />
<ColumnDefinition Width="3*" />
</Grid.ColumnDefinitions>
<!-- Title -->
<TextBlock Grid.ColumnSpan="2" Grid.Row="0" FontWeight="Bold" Text="{Binding Title, ElementName=TT_Popup_Control}" />
<!-- Description -->
<TextBlock Grid.Column="0" Grid.Row="1">
<ContentPresenter Content="{Binding Image, ElementName=TT_Popup_Control}" />
</TextBlock>
<TextBlock Grid.Column="1" Grid.Row="1">
<ContentPresenter Content="{Binding Desc, ElementName=TT_Popup_Control}">
<ContentPresenter.Resources>
<Style TargetType="{x:Type TextBlock}" BasedOn="{StaticResource WrappingStyle}"/>
</ContentPresenter.Resources>
</ContentPresenter>
</TextBlock>
<Separator Grid.ColumnSpan="2" Grid.Row="2" />
<!-- Image -->
<TextBlock Grid.ColumnSpan="2" Grid.Row="3">
<Image Margin="0,0,10,0" Width="16" Source="pack://application:,,,/Path/To/Image/help_icon.png" />
<TextBlock Margin="0,-5,0,0">
<Hyperlink RequestNavigate="Hyperlink_RequestNavigate" NavigateUri="{Binding Footer, ElementName=TT_Popup_Control}">Tell Me More</Hyperlink>
</TextBlock>
</TextBlock>
</Grid>
</StackPanel>
</Border>
</Border>
</Grid>
</Popup>
Next, create the code behind for this control. For those of you who want the C# equivalent, you can head here. This is where I got the reference for creating a control template.
Imports System.Windows
Namespace WPF
Public Class Tooltip
Public Shared ReadOnly TitleProperty As DependencyProperty = DependencyProperty.Register("Title", GetType(Object), GetType(WPF.Tooltip), New PropertyMetadata(vbNull))
Public Shared ReadOnly DescProperty As DependencyProperty = DependencyProperty.Register("Desc", GetType(Object), GetType(WPF.Tooltip), New PropertyMetadata(vbNull))
Public Shared ReadOnly FooterProperty As DependencyProperty = DependencyProperty.Register("Footer", GetType(Object), GetType(WPF.Tooltip), New PropertyMetadata(vbNull))
Public Shared ReadOnly ImageProperty As DependencyProperty = DependencyProperty.Register("Image", GetType(Object), GetType(WPF.Tooltip), New PropertyMetadata(vbNull))
Public Property Title As Object
Get
Return GetValue(TitleProperty)
End Get
Set(value As Object)
SetValue(TitleProperty, value)
End Set
End Property
Public Property Desc As Object
Get
Return GetValue(DescProperty)
End Get
Set(value As Object)
SetValue(DescProperty, value)
End Set
End Property
Public Property Footer As Object
Get
Return GetValue(FooterProperty)
End Get
Set(value As Object)
SetValue(FooterProperty, value)
End Set
End Property
Public Property Image As Object
Get
Return GetValue(ImageProperty)
End Get
Set(value As Object)
SetValue(ImageProperty, value)
End Set
End Property
Private Sub Hyperlink_RequestNavigate(sender As Object, e As Navigation.RequestNavigateEventArgs)
System.Diagnostics.Process.Start(e.Uri.ToString())
End Sub
End Class
End Namespace
The third step is to create the popup functionality, which is best done via styles. I created my style as a resource dictionary (you can call it PopupStyle.xaml, but you can also embed your style directly within the popup xaml control markup:
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:Themes="clr-namespace:Microsoft.Windows.Themes;assembly=PresentationFramework.Classic"
xmlns:System="clr-namespace:System;assembly=mscorlib">
<Style x:Key="TT_Popup" TargetType="Popup">
<Setter Property="VerticalOffset" Value="20" />
<Setter Property="MaxWidth" Value="500" />
<Setter Property="MinWidth" Value="50" />
<Style.Triggers>
<DataTrigger Binding="{Binding PlacementTarget.IsMouseOver, RelativeSource={RelativeSource Self}}" Value="True">
<DataTrigger.EnterActions>
<BeginStoryboard x:Name="OpenPopupStoryBoard" >
<Storyboard>
<BooleanAnimationUsingKeyFrames Storyboard.TargetProperty="IsOpen" FillBehavior="HoldEnd">
<DiscreteBooleanKeyFrame KeyTime="0:0:1.00" Value="True"/>
</BooleanAnimationUsingKeyFrames>
</Storyboard>
</BeginStoryboard>
</DataTrigger.EnterActions>
<DataTrigger.ExitActions>
<PauseStoryboard BeginStoryboardName="OpenPopupStoryBoard"/>
<BeginStoryboard x:Name="ClosePopupStoryBoard">
<Storyboard>
<BooleanAnimationUsingKeyFrames Storyboard.TargetProperty="IsOpen" FillBehavior="HoldEnd">
<DiscreteBooleanKeyFrame KeyTime="0:0:0.35" Value="False"/>
</BooleanAnimationUsingKeyFrames>
</Storyboard>
</BeginStoryboard>
</DataTrigger.ExitActions>
</DataTrigger>
<DataTrigger Binding="{Binding PlacementTarget.IsMouseCaptured, RelativeSource={RelativeSource Self}}" Value="True">
<DataTrigger.EnterActions>
<PauseStoryboard BeginStoryboardName="OpenPopupStoryBoard"/>
<BeginStoryboard x:Name="CloseImmediatelyPopupStoryBoard" >
<Storyboard>
<BooleanAnimationUsingKeyFrames Storyboard.TargetProperty="IsOpen" FillBehavior="HoldEnd">
<DiscreteBooleanKeyFrame KeyTime="0:0:0.0" Value="False"/>
</BooleanAnimationUsingKeyFrames>
</Storyboard>
</BeginStoryboard>
</DataTrigger.EnterActions>
</DataTrigger>
<DataTrigger Binding="{Binding PlacementTarget.IsMouseOver, RelativeSource={RelativeSource Self}}" Value="False">
<DataTrigger.ExitActions>
<RemoveStoryboard BeginStoryboardName="CloseImmediatelyPopupStoryBoard" />
</DataTrigger.ExitActions>
</DataTrigger>
<Trigger Property="IsMouseOver" Value="True">
<Trigger.EnterActions>
<PauseStoryboard BeginStoryboardName="ClosePopupStoryBoard" />
</Trigger.EnterActions>
<Trigger.ExitActions>
<PauseStoryboard BeginStoryboardName="OpenPopupStoryBoard"/>
<ResumeStoryboard BeginStoryboardName="ClosePopupStoryBoard" />
</Trigger.ExitActions>
</Trigger>
</Style.Triggers>
</Style>
</ResourceDictionary>
And finally, you must call your newly created ToolTip control in your Xaml markup like so:
<UserControl xmlns:WPF="clr-namespace:Project_Namespace.WPF"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
>
<UserControl.Resources>
<ResourceDictionary.MergedDictionaries>
<ResourceDictionary Source="PopupStyle.xaml"/>
</ResourceDictionary.MergedDictionaries>
</ResourceDictionary>
</UserControl.Resources>
<Grid>
<Button x:Name="button_name" />
<!-- Tooltips -->
<WPF:Tooltip Style="{StaticResource TT_Popup}" PlacementTarget="{Binding ElementName=button_name}">
<WPF:Tooltip.Title>
Tooltip Title
</WPF:Tooltip.Title>
<WPF:Tooltip.Image>
<Image Source="pack://application:,,,/Path/To/Image.png" />
</WPF:Tooltip.Image>
<WPF:Tooltip.Desc>
Your Description here
</WPF:Tooltip.Desc>
<WPF:Tooltip.Footer>
www.example.com
</WPF:Tooltip.Footer>
</WPF:Tooltip>
</Grid>
</UserControl
So this may be a complicated way to achieve this functionality, however once implemented, these tooltips are much better than the default tooltips, especially if you require user interaction.
Related
I have this old software of mine which I made without following any particular pattern. At the time I found myself stuck with the following problem:
I'm creating some buttons and put them in a Grid. Those button are dynamically created and their creation logic is the following:
Button btn = new Button();
btn.Template = this.FindResource("template") as ControlTemplate;
btn.ContentTemplate = this.FindResource("contentTemplate") as DataTemplate;
var binding = new Binding
{
Source = sourceItem;
}
btn.SetBinding(Button.ContentProperty, binding);
grid.Children.Add(btn);
The sourceItem's class implements INotifyPropertyChanged has two properties:
public class SourceItemClass: INotifyPropertyChanged
{
private bool _online;
public virtual bool Online
{
get => _online;
protected set
{
if (_online != value)
{
_online = value;
NotifyPropertyChanged();
}
}
}
private bool _error;
public virtual bool Error
{
get => _error;
protected set
{
if (_error!= value)
{
_error = value;
NotifyPropertyChanged();
}
}
}
public event PropertyChangedEventHandler PropertyChanged;
protected void NotifyPropertyChanged([System.Runtime.CompilerServices.CallerMemberName] string propertyName = "")
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
Which I bound to the button content template like this:
<DataTemplate x:Key="contentTemplate" DataType="{x:Type classes:SourceItemClass}">
<Grid>
<Grid.RowDefinitions>
<RowDefinition></RowDefinition>
<RowDefinition></RowDefinition>
</Grid.RowDefinitions>
<TextBlock Grid.Row="0" Text="Some text"/>
<Grid Grid.Row="1">
<Grid.RowDefinitions>
<RowDefinition></RowDefinition>
<RowDefinition></RowDefinition>
</Grid.RowDefinitions>
<TextBlock Grid.Row="1" Text="Some text" />
<Grid.Style>
<Style>
<Style.Triggers>
<DataTrigger Binding="{Binding Path= Online}" Value="true">
<Setter Property="Grid.Background" Value="{StaticResource bckgndImg}"/>
</DataTrigger>
</Style.Triggers>
</Style>
</Grid.Style>
</Grid>
<Grid.Style>
<Style>
<Style.Triggers>
<DataTrigger Binding="{Binding Path=Online}" Value="True">
<Setter Property="TextBlock.Foreground" Value="Black"/>
</DataTrigger>
<DataTrigger Binding="{Binding Path=Online}" Value="False">
<Setter Property="TextBlock.Foreground" Value="red"/>
</DataTrigger>
</Style.Triggers>
</Style>
</Grid.Style>
</Grid>
</DataTemplate>
And to the control template like this:
<ControlTemplate x:Key="template" TargetType="{x:Type Button}">
<Border x:Name="buttonBorder">
<VisualStateManager.VisualStateGroups>
<VisualStateGroup Name="CommonStates">
<VisualState Name="Normal">
<Storyboard>
<ThicknessAnimation Storyboard.TargetName="buttonBorder" Storyboard.TargetProperty="BorderThickness"
To="{TemplateBinding BorderThickness}"
Duration="0:0:0.1"/>
<ColorAnimation Storyboard.TargetName="buttonBorder"
Storyboard.TargetProperty="BorderBrush.Color"
To="{TemplateBinding BorderBrush}"
Duration="0:0:0.1"/>
<ThicknessAnimation Storyboard.TargetName="buttonBorder"
Storyboard.TargetProperty="Padding"
To="{TemplateBinding Padding}"
Duration="0:0:0.1"/>
<ThicknessAnimation Storyboard.TargetName="buttonContentPresenter"
Storyboard.TargetProperty="Margin"
To="{TemplateBinding Margin}"
Duration="0:0:0.1"/>
</Storyboard>
</VisualState>
<VisualState Name="MouseOver">
<Storyboard>
<ThicknessAnimation Storyboard.TargetName="buttonBorder" Storyboard.TargetProperty="BorderThickness"
To="3"
Duration="0:0:0.1"/>
<ColorAnimation Storyboard.TargetName="buttonBorder" Storyboard.TargetProperty="BorderBrush.Color"
To="Orange"
Duration="0:0:0.1"/>
<ThicknessAnimation Storyboard.TargetName="buttonBorder" Storyboard.TargetProperty="Padding"
To="5"
Duration="0:0:0.1"/>
<ThicknessAnimation Storyboard.TargetName="buttonContentPresenter" Storyboard.TargetProperty="Margin"
To="-8"
Duration="0:0:0.1"/>
</Storyboard>
</VisualState>
</VisualStateGroup>
</VisualStateManager.VisualStateGroups>
<ContentPresenter x:Name="buttonContentPresenter" HorizontalAlignment="Stretch" VerticalAlignment="Stretch"/>
</Border>
<ControlTemplate.Triggers>
<MultiDataTrigger>
<MultiDataTrigger.Conditions>
<Condition Binding="{Binding Content.Online, RelativeSource={RelativeSource Self}}" Value="false"/>
</MultiDataTrigger.Conditions>
<Setter TargetName="buttonBorder" Property="Background" Value="{StaticResource img1}"/>
</MultiDataTrigger>
<MultiDataTrigger>
<MultiDataTrigger.Conditions>
<Condition Binding="{Binding Content.Online, RelativeSource={RelativeSource Self}}" Value="True"/>
<Condition Binding="{Binding Content.Error, RelativeSource={RelativeSource Self}}" Value="True"/>
</MultiDataTrigger.Conditions>
<Setter TargetName="buttonBorder" Property="Background" Value="{StaticResource img2}"/>
</MultiDataTrigger>
</ControlTemplate.Triggers>
</ControlTemplate>
Problem is: the UI doesn't update when either Online and Error change their values.
I'm trying to figure thins out since too long at this point. Right now I'm periodically recreating buttons, which is not good.
What am I missing?
Wouldn't it be better to use an ItemsCollection in XAML rather than directly in code?
Something like:
<ItemsControl ItemsSource="{Binding ButtonDefinitions}">
<ItemsControl.ItemTemplate>
<DataTemplate>
<Button Content="{Binding Name}">
<Button.Template>
<!-- template goes here... -->
</Button.Template>
<Button.ContentTemplate>
<!-- content template goes here... -->
</Button.ContentTemplate>
</Button>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
Or if the templates are stored in a ResourceDictionary:
<ItemsControl ItemsSource="{Binding ButtonDefinitions}">
<ItemsControl.ItemTemplate>
<DataTemplate>
<Button Content="{Binding Name}"
Template="{StaticResource template}"
ContentTemplate="{StaticResource contentTemplate}"/>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
This keeps your UI separate to your logic.
I assume that the problem is the binding in the ControlTemplate.
But the provided code should be simplified first.
I do not see a problem with the Button instantiation in the code behind.
I attach a sample code that creates the button in the XAML.
I suggest you start with my sample code that shows how to bind in a control that has both a ContentTemplate and a ControlTemplate.
Hope it helps..
<Window.Resources>
<DataTemplate x:Key="contentTemplate" DataType="{x:Type local:SourceItemClass}">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<TextBlock Grid.Row="0" Text="Message From Content Template"/>
<TextBlock Grid.Row="1" Text="{Binding Online}"/>
</Grid>
</DataTemplate>
<ControlTemplate x:Key="template" TargetType="Button">
<Border BorderBrush="Aqua" BorderThickness="6">
<StackPanel>
<ContentPresenter x:Name="buttonContentPresenter" HorizontalAlignment="Stretch" VerticalAlignment="Stretch"/>
<TextBlock Text="Message From Template"/>
<TextBlock Text="{Binding Content.Online,RelativeSource={RelativeSource TemplatedParent}}"/>
</StackPanel>
</Border>
</ControlTemplate>
</Window.Resources>
<Grid Name="grid">
<Grid.ColumnDefinitions>
<ColumnDefinition/>
<ColumnDefinition/>
</Grid.ColumnDefinitions>
<Button Grid.Column="0" Click="Button_Click">Change Online</Button>
<Button Grid.Column="1" Content="{Binding }"
ContentTemplate="{StaticResource ResourceKey=contentTemplate}"
Template="{StaticResource template}"
/>
</Grid>
I have a ListBox using a DataTemplate (Grid with 2 TextBlock and 1 TextBox)
<ListBox Name="lbHistory" HorizontalContentAlignment="Stretch" MouseDoubleClick="LbHistory_MouseDoubleClick" Margin="10,0,0,0" ItemContainerStyle="{DynamicResource _ListBoxItemStyle}">
<ListBox.ItemTemplate>
<DataTemplate>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="50"/>
<ColumnDefinition Width="130"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="20"/>
</Grid.RowDefinitions>
<TextBlock VerticalAlignment="Center" Grid.Column="0" Text="{Binding Direction}"/>
<TextBlock VerticalAlignment="Center" Grid.Column="1" Text="{Binding TimeStamp, StringFormat=s}"/>
<TextBox VerticalContentAlignment="Center" Grid.Column="2" Text="{Binding Message}" IsReadOnly="True" IsReadOnlyCaretVisible="True" BorderThickness="0"/>
</Grid>
</DataTemplate>
</ListBox.ItemTemplate>
...
Styles to make selected item bold
<Window.Resources>
<Style x:Key="_ListBoxItemStyle" TargetType="ListBoxItem">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="ListBoxItem">
<Border Name="_Border"
Padding="2"
SnapsToDevicePixels="true">
<ContentPresenter />
</Border>
<ControlTemplate.Triggers>
<!-- This removes the blue selection around the item which looks ugly with the white text box
Also sets the selected item to bold -->
<Trigger Property="IsSelected" Value="true">
<Setter TargetName="_Border" Property="Background" Value="Transparent"/>
<Setter Property="FontWeight" Value="Bold" />
</Trigger>
<!-- This ensures that a clicked textbox passes the selection event through -->
<!-- Deselects the row. Better solution see below -->
<!--<Trigger Property="IsKeyboardFocusWithin" Value="true">
<Setter Property="IsSelected" Value="true" />
</Trigger>-->
<!-- This ensures that a clicked textbox passes the selection event through -->
<!-- https://stackoverflow.com/questions/15366806/wpf-setting-isselected-for-listbox-when-textbox-has-focus-without-losing-selec/15383435#15383435 -->
<EventTrigger RoutedEvent="PreviewGotKeyboardFocus">
<BeginStoryboard>
<Storyboard>
<BooleanAnimationUsingKeyFrames Storyboard.TargetProperty="(ListBoxItem.IsSelected)">
<DiscreteBooleanKeyFrame KeyTime="0" Value="True"/>
</BooleanAnimationUsingKeyFrames>
</Storyboard>
</BeginStoryboard>
</EventTrigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</Window.Resources>
The TextBox can be larger than the ListBox. The ListBox then shows a scrollbar. This works fine.
When I click into the TextBox and select the text via mouse (Click and hold LMB, move mouse to the right), the ListBox automatically scrolls to the right so that the end of the selected text is visible.
How can I achieve the same behavior using keyboard's curser keys (with or without holding shift key to select)?
Due to DataTemplate, TextBox can not be assigned to a member variable (Name="myTextBox" does not create a member variable)
Using TextBox's SelectionChanged and ListBox.ScrollIntoView(sender) does not work as the TextBox is already in the view
I have placed a popup on a button mouseover.Each time when i mouse over that button my custom designed popup is being showed perfectly.Butit doesn't point the button perfectly. How to do so ..?
Now my popup looks like
I want that arrow mark to point that help button How to acheive it..
Here is my code for the button and popup in xaml
<telerik:RadButton Name="btnH" Grid.Column="1" HorizontalAlignment="Left" Margin="444,56,0,0" Grid.Row="2" VerticalAlignment="Top"
Width="23" Height="23" BorderThickness="6" BorderBrush="#4E4E4E">
<Image Source="Images/help.png" />
<telerik:RadButton.Triggers>
<EventTrigger RoutedEvent="MouseEnter">
<BeginStoryboard>
<Storyboard TargetName="TooltipPopup" TargetProperty="IsOpen">
<BooleanAnimationUsingKeyFrames FillBehavior="HoldEnd">
<DiscreteBooleanKeyFrame KeyTime="00:00:00" Value="True" />
</BooleanAnimationUsingKeyFrames>
</Storyboard>
</BeginStoryboard>
</EventTrigger>
and below is the custom usercontrol xaml which amn calling in popup
<UserControl
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:telerik="http://schemas.telerik.com/2008/xaml/presentation" x:Class="WPFTest.UCToolTip"
mc:Ignorable="d" Height="231.493" Width="362.075"
Background="Transparent" >
<UserControl.Resources>
<Style TargetType="{x:Type Hyperlink}">
<Setter Property="TextBlock.TextDecorations" Value="{x:Null}" />
</Style>
</UserControl.Resources>
<Grid Margin="10,0,0,0">
<Grid.RowDefinitions>
<RowDefinition/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<Grid Background="red" Margin="0,0,182,133">
</Grid>
<Polygon
Points="0.5,0 15,0, 0,30" Stroke="Orange" Fill="Orange" Margin="0,98,0,101" />
</Grid>
use this style for your Popup:
<Style TargetType="Popup">
<Style.Triggers>
<Trigger Property="IsOpen" Value="true">
<Setter Property="PlacementTarget" Value="{Binding ElementName=btnH }" />
<Setter Property="Placement" Value="Top" />
<Setter Property="VerticalOffset" Value="-5" />
<Setter Property="HorizontalOffset" Value="5" />
</Trigger>
</Style.Triggers>
</Style>
I think that Kylo pointed you to the right answer. If you get rid of the margin in the usercontrol it should work.
This is the code for the usercontrol.
<UserControl
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:telerik="http://schemas.telerik.com/2008/xaml/presentation" x:Class="WpfTest.UCToolTip"
mc:Ignorable="d" Height="130" Width="180"
Background="Transparent" >
<UserControl.Resources>
<Style TargetType="{x:Type Hyperlink}">
<Setter Property="TextBlock.TextDecorations" Value="{x:Null}" />
</Style>
</UserControl.Resources>
<Grid Margin="10,0,0,0">
<Grid.RowDefinitions>
<RowDefinition/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<Grid Background="red" Margin="0,0,0,32">
</Grid>
<Polygon Points="0.5,0 15,0, 0,30" Stroke="Orange" Fill="Orange" Margin="0,98,0,0" />
</Grid>
</UserControl>
And the code for the window.
<Window x:Class="WpfTest.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:telerik="http://schemas.telerik.com/2008/xaml/presentation"
xmlns:uc="clr-namespace:WpfTest"
Title="MainWindow" Height="350" Width="525">
<Window.Resources>
<Style TargetType="Popup">
<Style.Triggers>
<Trigger Property="IsOpen" Value="true">
<Setter Property="PlacementTarget" Value="{Binding ElementName=btnH }" />
<Setter Property="Placement" Value="Top" />
<Setter Property="VerticalOffset" Value="0" />
<Setter Property="HorizontalOffset" Value="145" />
</Trigger>
</Style.Triggers>
</Style>
</Window.Resources>
<Grid>
<telerik:RadButton Name="btnH" Grid.Column="1" HorizontalAlignment="Left" Margin="300,175,0,0" Grid.Row="2" VerticalAlignment="Top"
Width="50" Height="23" BorderThickness="6" BorderBrush="#4E4E4E">
<Image Source="Images/help.png" />
<telerik:RadButton.Triggers>
<EventTrigger RoutedEvent="MouseEnter">
<BeginStoryboard>
<Storyboard TargetName="TooltipPopup" TargetProperty="IsOpen">
<BooleanAnimationUsingKeyFrames FillBehavior="HoldEnd">
<DiscreteBooleanKeyFrame KeyTime="00:00:00" Value="True" />
</BooleanAnimationUsingKeyFrames>
</Storyboard>
</BeginStoryboard>
</EventTrigger>
<EventTrigger RoutedEvent="MouseLeave">
<BeginStoryboard>
<Storyboard TargetName="TooltipPopup" TargetProperty="IsOpen">
<BooleanAnimationUsingKeyFrames FillBehavior="HoldEnd">
<DiscreteBooleanKeyFrame KeyTime="00:00:00" Value="False" />
</BooleanAnimationUsingKeyFrames>
</Storyboard>
</BeginStoryboard>
</EventTrigger>
</telerik:RadButton.Triggers>
</telerik:RadButton>
<Popup x:Name="TooltipPopup" AllowsTransparency="True">
<StackPanel>
<uc:UCToolTip></uc:UCToolTip>
</StackPanel>
</Popup>
</Grid>
</Window>
Here is a small library with balloons for WPF that I think does what you want.
Usage:
<geometry:Balloon ConnectorAngle="25"
CornerRadius="15"
PlacementOptions="Bottom, Center"
PlacementTarget="{Binding ElementName=Target}">
<!-- content here -->
</geometry:Balloon>
My WPF application has a UserControl in it called AlarmItem. Here's the XAML:
<UserControl x:Class="CarSystem.CustomControls.AlarmItem"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:cs="clr-namespace:CarSystem.CustomControls"
mc:Ignorable="d"
d:DesignHeight="100" d:DesignWidth="100"
DataContext="{Binding Path=Alarm, RelativeSource={RelativeSource Self}}">
<UserControl.Resources>
<Style TargetType="{x:Type cs:AlarmItem}">
<Setter Property="IsFlashing" Value="False" />
<Setter Property="FlashColor" Value="{DynamicResource NotPendingAlarmBorder}" />
<Style.Triggers>
<DataTrigger Binding="{Binding Path=IsExpired}" Value="True">
<Setter Property="FlashColor" Value="{DynamicResource ExpiredAlarmBorder}" />
<Setter Property="IsFlashing" Value="True" />
</DataTrigger>
<DataTrigger Binding="{Binding Path=IsPending}" Value="True">
<Setter Property="IsFlashing" Value="True" />
</DataTrigger>
<DataTrigger Binding="{Binding Path=IsWhiteListAlarm}" Value="True">
<Setter Property="FlashColor" Value="{DynamicResource WhiteListAlarmBorder}" />
</DataTrigger>
</Style.Triggers>
</Style>
<Style x:Key="AlarmItemBorderStyle" TargetType="Border">
<Setter Property="BorderThickness" Value="2" />
<Style.Triggers>
<DataTrigger Binding="{Binding Path=IsExpired}" Value="True">
<Setter Property="BorderThickness" Value="4" />
</DataTrigger>
<DataTrigger Binding="{Binding Path=IsPending}" Value="True">
<Setter Property="BorderThickness" Value="4" />
</DataTrigger>
</Style.Triggers>
</Style>
</UserControl.Resources>
<Border HorizontalAlignment="Center"
Margin="5"
Height="100"
Name="Border"
Style="{StaticResource AlarmItemBorderStyle}"
VerticalAlignment="Center"
Width="100">
<Border.Resources>
<Storyboard x:Key="FlashingStoryboard" AutoReverse="False" RepeatBehavior="Forever">
<ColorAnimationUsingKeyFrames BeginTime="00:00:00"
Duration="00:00:01"
Storyboard.TargetName="Border"
Storyboard.TargetProperty="(Border.BorderBrush).(SolidColorBrush.Color)">
<DiscreteColorKeyFrame KeyTime="00:00:00" Value="Black" />
<DiscreteColorKeyFrame KeyTime="00:00:00.5" Value="{Binding Path=FlashColor, RelativeSource={RelativeSource AncestorType={x:Type cs:AlarmItem}}}" />
</ColorAnimationUsingKeyFrames>
</Storyboard>
</Border.Resources>
<VisualStateManager.VisualStateGroups>
<VisualStateGroup x:Name="FlashStates">
<VisualState x:Name="FlashingOn"
Storyboard="{StaticResource ResourceKey=FlashingStoryboard}" />
<VisualState x:Name="FlashingOff" />
</VisualStateGroup>
</VisualStateManager.VisualStateGroups>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="*" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<Image Grid.Row="0"
Name="AlarmImage"
Source="{Binding Path=Image, RelativeSource={RelativeSource AncestorType={x:Type cs:AlarmItem}}}"
Stretch="Fill" />
<cs:ResponseTimer Expired="Timer_Expired"
Grid.Row="1"
HideIfExpired="True"
IsTabStop="False"
MinHeight="10"
x:Name="TheTimer"
TimeoutPeriod="00:02:30"
VerticalAlignment="Bottom" />
</Grid>
</Border>
</UserControl>
The code behind for this control has the following code in it that runs when the IsFlashing property changes:
private void OnIsFlashingChanged( object sender, bool flashNow ) {
if ( flashNow ) {
if ( !VisualStateManager.GoToElementState( Border, "FlashingOn", true ) ) {
Log.Debug( "AlarmItem.xaml.cs: Visual State Manager transition failed." );
}
} else {
if ( !VisualStateManager.GoToElementState( Border, "FlashingOff", true ) ) {
Log.Debug( "AlarmItem.xaml.cs: Visual State Manager transition failed." );
}
}
}
private static void OnIsFlashingInvalidated( DependencyObject d, DependencyPropertyChangedEventArgs e ) {
AlarmItem alarmItem = (AlarmItem) d;
alarmItem.OnIsFlashingChanged( d, (bool) e.NewValue );
}
The line that sets the visual state for the Border to FlashingOn throws the following InvalidOperationException when it executes:
System.InvalidOperationException: 'BorderBrush' property does not point to a DependencyObject in path '(0).(1)'.
What have I done wrong? I recently added the Style and the triggers that set the IsFlashing property; in the past, I did it in the code-behind and this error did not occur then.
After much research, trial & error, and heartache, the reason I'm getting the error is because the Border class's BorderBrush property is a Brush, not a SolidColorBrush. Brush is an abstract type that SolidColorBrush and all the various gradient brush classes descend from.
I have a very simple TabItem template, and a single MultiTrigger with a SourceName attribute used on one Condition. The following XAML throws NullReferenceException when started, with no helpful info which would help me to fix the problem.
The strangest thing about this is the code works great if you remove the SourceName attribute. Or, if you leave the SourceName attribute, but remove MultiTrigger.EnterActions and use standard Setters instead, then it works as well. Only the combination of SourceName attribute and MultiTrigger.EnterActions throws NullReferenceException for no obvious reason. So what's wrong with this?
<Window x:Class="WpfApplication2.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="MainWindow" Height="350" Width="525" WindowStartupLocation="CenterScreen">
<Window.Resources>
<Style TargetType="TabItem">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="TabItem">
<ControlTemplate.Resources>
<Storyboard x:Key="Storyboard_TabItem_Hover">
<DoubleAnimationUsingKeyFrames Storyboard.TargetName="background" Storyboard.TargetProperty="Opacity">
<SplineDoubleKeyFrame KeyTime="00:00:00.3" Value="0.1" />
</DoubleAnimationUsingKeyFrames>
</Storyboard>
</ControlTemplate.Resources>
<Border x:Name="background" BorderBrush="Red" BorderThickness="1" Background="Yellow">
<Label Grid.Column="1" Content="{TemplateBinding Header}" />
</Border>
<ControlTemplate.Triggers>
<MultiTrigger>
<MultiTrigger.Conditions>
<Condition Property="IsMouseOver" Value="True" SourceName="background" />
<Condition Property="IsSelected" Value="False" />
</MultiTrigger.Conditions>
<MultiTrigger.EnterActions>
<BeginStoryboard x:Name="sbHover" Storyboard="{StaticResource Storyboard_TabItem_Hover}"/>
</MultiTrigger.EnterActions>
</MultiTrigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</Window.Resources>
<TabControl Margin="10">
<TabItem Header="Tab 1" />
<TabItem Header="Tab 2" />
<TabItem Header="Tab 3" />
<TabItem Header="Tab 4" />
</TabControl>
</Window>
Update
As Greg Sansom pointed out, there is a simple workaround using MultiDataTrigger and Binding. However, I would still like to know why is the exception being thrown in the first place. I've searched Google and MSDN like crazy but haven't found anything. So what's the problem?
You can work around the problem by changing the MultiTrigger to a MultiDataTrigger, and specifying Binding instead of SourceName:
<MultiDataTrigger.Conditions>
<Condition Binding="{Binding ElementName=bg,Path=IsMouseOver}"
Value="True" />
<Condition Binding="{Binding RelativeSource={RelativeSource Self},
Path=IsSelected}" Value="False" />
</MultiDataTrigger.Conditions>