WPF: Custom tooltip arrow placement - c#

I have a custom tooltip style that basically creates a nice black tooltip with an arrow pointing to the location of the item you hovered over.
The problem is that sometimes the tooltip will not always be placed in the correct location (i.e. near window edges) which means the tooltip arrow no longer points at the correct place... Is there anyway around this problem? Or can I create specific styles for each location placement?
<Style x:Key="{x:Type ToolTip}" TargetType="ToolTip">
<Setter Property="OverridesDefaultStyle" Value="true"/>
<Setter Property="HasDropShadow" Value="True"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="ToolTip">
<StackPanel>
<Border CornerRadius="3" HorizontalAlignment="Center" VerticalAlignment="Top" Padding="10,7" BorderThickness="0" Background="#e5323232">
<StackPanel>
<TextBlock FontFamily="Arial" FontSize="12" Text="{TemplateBinding Content}" Foreground="#f0f0f0" />
</StackPanel>
</Border>
<Path Margin="10,0,0,0" Fill="#e5323232" Data="M 0 0 L 6 6 L 12 0 Z"/>
</StackPanel>
</ControlTemplate>
</Setter.Value>
</Setter>

Maybe you could try this, I just set the Placement to Center and added a HorizontalOffset to match the arrow you created in the template.
However that wont center it vertically on the control, so you could make an IValueConverter and calculate the size of the control and divide by 2, or you could add a dummy element to your StackPanel that is the same size as the Border, and that should center the ToolTip without needing any code behind
<Style TargetType="{x:Type ToolTip}">
<Setter Property="OverridesDefaultStyle" Value="true"/>
<Setter Property="HasDropShadow" Value="True"/>
<Setter Property="Placement" Value="Center" />
<!--Offset to the arrow path-->
<Setter Property="HorizontalOffset" Value="15"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type ToolTip}">
<StackPanel>
<Border x:Name="border" CornerRadius="3" HorizontalAlignment="Center" VerticalAlignment="Top" Padding="10,7" BorderThickness="0" Background="#e5323232">
<StackPanel>
<TextBlock FontFamily="Arial" FontSize="12" Text="{TemplateBinding Content}" Foreground="#f0f0f0" />
</StackPanel>
</Border>
<Path Margin="10,0,0,0" Fill="#e5323232" Data="M 0 0 L 6 6 L 12 0 Z"/>
<!--Dummy rectangle same height as tool tip, so it centers on the control-->
<Rectangle Height="{Binding ActualHeight, ElementName=border}" />
</StackPanel>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>

The simplest way of doing it is to use a UIElement that exists in the Control Tree as the PlacementTarget of the Tooltip. This will avoid the Silverlight automated positioning when you get near the window edges:
<StackPanel ToolTipService.ToolTip="{Binding Title, Mode=OneWay,UpdateSourceTrigger=PropertyChanged}"
ToolTipService.Placement="Bottom"
ToolTipService.PlacementTarget="{Binding ElementName=LayoutRoot}">
<TextBlock Text="{Binding Title,Mode=OneWay,UpdateSourceTrigger=PropertyChanged}"/>
</StackPanel>
In this case the tooltip will be positioned always at the Origin of the LayoutRoot element. If you have a fixed path size and the PlacementTarget is always at the same position relative to the control for which you want to show the tooltip then this works fine.
If you need to position the Tooltip relatively to the control that triggers the Tooltip than you have to make the Path Data dynamic and calculate the distance to create a new Path Data in the Tooltip control every time the Tooltip is opened.
For this case you have to handle the Tooltip.IsOpened event and implement this logic. If you're using the PlacementTarget than you always know the direction relatively to your control so this makes it easier to calculate the Path vertices.
Another way which works but it's way more complex is to implement your own Popup that shows up when you move the mouse over your control. You would need to implement a few calculations to get the position of the popup relative to the Control, which is exactly what the Tooltip control does for you. The advantage of this is that you have complete control over the positioning of the tooltip and its appearance.

Related

UserControl positioning understanding

I'm getting close to the understanding of the positioning of the System.Windows.Controls.UserControl objects:
By default, they don't have X or Y coordinates, relative to their container, but there is the possibility to add some, using so-called "Attached Properties".
A typical example of such attached properties are Canvas.Left and Canvas.Top, which mean that, in case the container of the UserControl is a Canvas, then the following happens with (supposingly) its upper left point (pseudo-code):
UserControl_UpperLeft_Point.X = Canvas.Left
UserControl_UpperLeft_Point.Y = Canvas.Top
Now what I'd like to know:
Is my idea correct? Is it indeed the upper left corner which is used?
What if I want to modify this behaviour, let's say into:
int left_Margin = 100;
int top_Margin = 200;
UserControl_UpperLeft_Point.X = Canvas.Left / 2 + left_Margin;
UserControl_UpperLeft_Point.Y = Canvas.Top * 2 + top_Margin;
What if I want to position my UserControl, based on the upper right corner or even the center?
Is my idea correct? Is it indeed the upper left corner which is used?
It is effectively the upper left corner in this case, but actually each of the sides are aligned. Furthermore, the control is aligned by the Canvas, it does not have internal X or Y coordinates that are set. They are given by the attached properties which are sepcific to the Canvas, no other panel. The Canvas internally calculates a rectangle where it draws the UserControl.
Canvas.Left
Gets or sets a value that represents the distance between the left side of an element and the left side of its parent Canvas.
Canvas.Top
Gets or sets a value that represents the distance between the top of an element and the top of its parent Canvas.
It is also important to note, that there is a priority for the attached properties.
If you specify them, the attached properties Canvas.Top or Canvas.Left take priority over Canvas.Bottom or Canvas.Right.
What if I want to modify this behaviour, let's say into: [...]
You would still have to assign the attached properties, but calculate the expression before.
Use a Binding or MultiBinding with a custom value converter that evaluates an expression that is bound or passed as converter parameter or with a specialized value converter to calculate a specific term.
Create your own specialized attached properties that internally assign the calculated value to the Canvas attached properties.
What if I want to position my UserControl, based on the upper right corner
Set the Canvas.Top and Canvas.Right attached properties instead.
<Canvas>
<local:MyUserControl Canvas.Top="0" Canvas.Right="0" Width="50" Height="80"/>
</Canvas>
[...] or even the center?
A Canvas is used for absolute positioning. If you want to center controls, a Grid might be the better choice. If you still want to use a Canvas and it is fixed size, just calculate the center coordinates yourself and set the attached properties accordingly. If it is resizeable and the position of the UserControl position needs to be responsive to that, you could do one of these.
Put a Grid around the Canvas and put the UserControl after it. It will appear on top of the Canvas and will be centered automatically, even on resizing.
Grid>
<Canvas>
<Rectangle Canvas.Top="80" Canvas.Left="20" Fill="Black" Width="50" Height="50"/>
<Rectangle Canvas.Top="300" Canvas.Left="230" Fill="Black" Width="100" Height="80"/>
</Canvas>
<local:MyUserControl Width="50" Height="80"/>
</Grid>
Implement a custom behavior or complicated bindings with converter using the bound ActualSize of the Canvas to set the attached properties, which I do not recommend.
I think for your last question it is more useful to ask why you need this behavior, what you want to achieve. Often there is a much simpler and more suitable solution.
If you want to position a control inside a container you should follow the WPF way.
Align it horizontal and vertical and set the margin, thats it.
<Grid>
<Grid.Resources>
<Style TargetType="Border">
<Setter Property="CornerRadius" Value="5" />
<Setter Property="Width" Value="50" />
<Setter Property="Height" Value="50" />
<Setter Property="Background" Value="Red" />
<Setter Property="TextElement.Foreground" Value="White" />
<Setter Property="TextElement.FontWeight" Value="Bold" />
</Style>
<Style TargetType="TextBlock">
<Setter Property="HorizontalAlignment" Value="Center" />
<Setter Property="VerticalAlignment" Value="Center" />
<Setter Property="TextAlignment" Value="Center" />
</Style>
</Grid.Resources>
<!--#region top-->
<Border
Margin="20,20,0,0"
HorizontalAlignment="Left"
VerticalAlignment="Top">
<TextBlock>
Top<LineBreak />
Left</TextBlock>
</Border>
<Border
Margin="0,20,0,0"
HorizontalAlignment="Center"
VerticalAlignment="Top">
<TextBlock>
Top<LineBreak />
Center</TextBlock>
</Border>
<Border
Margin="0,20,20,0"
HorizontalAlignment="Right"
VerticalAlignment="Top">
<TextBlock>
Top<LineBreak />
Right</TextBlock>
</Border>
<!--#endregion-->
<!--#region center-->
<Border
Margin="20,0,0,0"
HorizontalAlignment="Left"
VerticalAlignment="Center">
<TextBlock>
Center<LineBreak />
Left</TextBlock>
</Border>
<Border
Margin="0,0,0,0"
HorizontalAlignment="Center"
VerticalAlignment="Center">
<TextBlock>
Center<LineBreak />
Center</TextBlock>
</Border>
<Border
Margin="0,0,20,0"
HorizontalAlignment="Right"
VerticalAlignment="Center">
<TextBlock>
Center<LineBreak />
Right</TextBlock>
</Border>
<!--#endregion-->
<!--#region bottom-->
<Border
Margin="0,0,20,20"
HorizontalAlignment="Right"
VerticalAlignment="Bottom">
<TextBlock>
Bottom<LineBreak />
Right</TextBlock>
</Border>
<Border
Margin="0,0,0,20"
HorizontalAlignment="Center"
VerticalAlignment="Bottom">
<TextBlock>
Bottom<LineBreak />
Center</TextBlock>
</Border>
<Border
Margin="20,0,0,20"
HorizontalAlignment="Left"
VerticalAlignment="Bottom">
<TextBlock>
Bottom<LineBreak />
Left</TextBlock>
</Border>
<!--#endregion-->
</Grid>
If you want to have the old style (knwon from WinForms you can align it HorizontalAlignemnt="Top" and VerticalAlignment="Left" and use the Left and Top property of Margin to set the old school Left and Top.

WPF Separator Background Color Stays Gray

I have a WPF menu with a separator that I'd like to be black and 1 pixel wide. However, the separator seems to have a built-in 30 pixel wide buffer space that I can't change the color on. In the example below I set my separator to 50 pixels wide--as you can see the first 30 pixels don't reflect the proper background color. Very annoying! What am I missing?
Here's the XAML:
<Menu DockPanel.Dock="Top" FontSize="45" Height="62" Name="Menu">
<MenuItem Header="_Home" Name="HomeMenuItem" Click="HomeMenuItem_Click" Padding="10,0,10,0"></MenuItem>
<Separator Width="50" Background="Black" Foreground="Black" BorderThickness="0" Margin="0" Padding="0"></Separator>
<MenuItem Header="_Print" Name="PrintMenuItem" Click="PrintMenuItem_Click" Padding="10,0,10,0"></MenuItem>
...
Turns out the ControlTemplate for the menu automatically includes margin for the Separator. Thanks to Rowbear I now understand that the ControlTemplate overrides styles. The solution then was to add a custom template to the Application.Resources. (I'm sure it would have worked just fine to do it in Windows.Resources, etc.)
<Style x:Key="SeparatorStyle" TargetType="{x:Type Separator}">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type Separator}">
<Border Padding="0" Margin="0" BorderThickness="0" Background="#40000000"></Border>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
Then in the main XAML window references the custom ControlTemplate like this:
<Separator Width="1" Style="{StaticResource SeparatorStyle}"></Separator>

How to style a TextBox ScrollBar with my own images?

I was wondering if it is possible to insert my own bitmaps for scrollbar styling. I would like to to use an image for up/down arrows, background and scroller. This is what I have to start with, but I just don't know how to access these mentioned properties:
<TextBox Name="SlideNotes" Foreground="White" FontSize="20" FontWeight="Bold" Background="Transparent" BorderThickness="0" TextWrapping="Wrap" AcceptsReturn="True" ScrollViewer.VerticalScrollBarVisibility="Visible" Grid.Row="27" Grid.Column="11" Grid.ColumnSpan="46" Grid.RowSpan="5">
<TextBox.Resources>
<Style TargetType="{x:Type ScrollBar}">
<Setter Property="Background" Value="Red"/>
</Style>
</TextBox.Resources>
I would appreciate any help.
You can use ImageBrush to set an image to any property that uses Brushes (Background, Foreground, BorderBrush, Fill, etc.).
In your case, you'd use it like this:
<TextBox Name="SlideNotes" Foreground="White" FontSize="20" FontWeight="Bold" Background="Transparent" BorderThickness="0" TextWrapping="Wrap" AcceptsReturn="True" ScrollViewer.VerticalScrollBarVisibility="Visible" Grid.Row="27" Grid.Column="11" Grid.ColumnSpan="46" Grid.RowSpan="5">
<TextBox.Resources>
<Style TargetType="{x:Type ScrollBar}">
<Setter Property="Background">
<Setter.Value>
<ImageBrush ImageSource="yourimage.jpg" Stretch="Fill" />
</Setter.Value>
</Setter>
</Style>
</TextBox.Resources>
</TextBox>
ImageBrush (and all other Brushes that inherit from TileBrush) has a set of properties that control how the image is shown. I've used Stretch="Fill" in my sample, which will make the image stretch to fill all the space available, but you could want it to behave differently.
For instance, this...
<ImageBrush ImageSource="yourimage.jpg" Stretch="None" TileMode="FlipXY" />
Will make your image repeat, with tiles alternatively flipped horizontally and/or vertically.
Toy around with Stretch and TileMode (or even ViewPort, ViewPortUnit, Viewbox and ViewboxUnit, if you feel brave enough) to get the effect you want.
EDIT - As with RepeatButton, the default ScrollBar template seems to pretty much ignore the Background property, most of the time, as well as other properties you could use for customization... This means you'll probably have to override the whole template to customize it to your liking.
Here's one ScrollBar template sample: ScrollBar Styles and Templates
And here's another for the RepeatButton: RepeatButton Styles and Templates
Looks like you're after a ControlTemplate. See here for a nice tutorial on how to style one.
To avoid having to style the entire ScrollViewer, you can instead style only the RepeatButton element, this will make things much easier to work with.
<Style TargetType="RepeatButton">
<Setter Property="Background" Value="Red"/>
<Setter Property="Template">
<Setter.Value>
...
</Setter.Value>
</Setter>
</Style>
You may need to define this style in the Window Resources, instead of the TextBox resources due to the scope.
See the documentation for RepeatButton styles and templates.

Is it possible to provide 'arguments' to Custom control Styles & Templates?

I have 2 buttons, both of which are using a style to keep a consistent UI. However for 1 button, I would like to provide an image for, and the other just text.
I suppose I could create a new style, copy everything over, and the reformat it to my liking, but this seems like waste, is time consuming, and i think i would have to do it for each instance I wish to have a image on a button. I mean, that's fine, but I just want to know if there is an alternative that would make things more elegant.
I think I should somehow be able to push an 'arguement' or data to a style, either in or out of XAML to format the style, or something that would accomplish this (I'm sure the terminology is wrong).
Here is the Button Style:
<Style x:Key="Control_Button" TargetType="{x:Type Button}">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type Button}">
<Grid>
<Image> <!-- Optional Image here --> </Image>
<TextBlock Name="btn" Text="{TemplateBinding Content}" Padding="16" VerticalAlignment="Center">
<TextBlock.TextDecorations>
<TextDecoration Location="Underline" />
</TextBlock.TextDecorations>
</TextBlock>
<!-- FIX ME: not underlined normally -->
</Grid>
<ControlTemplate.Triggers>
<Trigger Property="TextBlock.IsMouseOver" Value="True">
<Setter TargetName="btn" Property="TextDecorations" Value="none" />
<!-- FIX ME: underlined on hover -->
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
What you are asking for is not possible without creating a custom control or user control.
What you should do though is setting the contents of the button the way you like. If you want only a string, you can set it directly:
<Button>my text</Button>
or with a binding:
<Button Content={Binding textProperty} />
To include an image in the button, add a panel as content, in this example I added a StackPanel, but you can also use a Grid or any other element:
<Button>
<Button.Content>
<StackPanel Orientation="Horizontal">
<Image Source="{Binding myImagePath}" />
<TextBlock Text="{Binding myText}" />
</StackPanel>
</Button.Content>
</Button>

Custom validation error template not respecting ZIndex [duplicate]

This question already has answers here:
WPF - How can I place a usercontrol over an AdornedElementPlaceholder?
(2 answers)
Closed 2 years ago.
I have a view defined as a DataTemplate (for an "OrderEntryViewModel") with a Menu, ContentPresenter, and Expander inside of a 3-row grid. The content of the ContentPresenter is binded to another viewModel "OrderViewModel" (for which there's another DataTemplate-defined view). The expander has a ZIndex of 99, so that when it expands UP, it expands OVER any other controls (i.e. the ContentPresenter).
This all works as expected EXCEPT when the ContentPresenter's content (the OrderViewModel) has data errors...My OrderView displays a custom validation error template around controls with invalid data. What happens is, when I expand the expander, all of controls inside the ContentPresenter are covered, but the red border and exclamation point I show are still visible through the expander! I've verified that my expander's ZIndex is 99 and the ContentPresenter's is 0. Can anyone help me with this?
Here's some images to help explain:
First Image shows what the views look like when NOT expanded.
Second Image shows what the views look like when I expand.
I define the validation error template like this:
<ControlTemplate x:Key="ValidationErrorTemplate">
<DockPanel LastChildFill="true">
<Border Background="Red" DockPanel.Dock="right" Margin="5,0,0,0" Width="10" Height="10" CornerRadius="5"
ToolTip="{Binding AdornedElement.(Validation.Errors).CurrentItem.ErrorContent, ElementName=customAdorner}">
<TextBlock Text="!" VerticalAlignment="center" HorizontalAlignment="center" FontWeight="Bold" Foreground="White"/>
</Border>
<AdornedElementPlaceholder x:Name="customAdorner" VerticalAlignment="Center">
<Border BorderBrush="red" BorderThickness="1" />
</AdornedElementPlaceholder>
</DockPanel>
</ControlTemplate>
And assign it to a specific control like this (this is how I do it for my TextBox):
<Style TargetType="{x:Type TextBox}" x:Key="ValidatedStyleTextBox">
<Style.Triggers>
<DataTrigger Binding="{Binding IsLocked}" Value="True">
<Setter Property="Validation.ErrorTemplate" Value="{x:Null}"/>
</DataTrigger>
<DataTrigger Binding="{Binding IsLocked}" Value="False">
<Setter Property="Validation.ErrorTemplate" Value="{StaticResource ValidationErrorTemplate}"/>
</DataTrigger>
</Style.Triggers>
</Style>
This solution worked for me...just added an AdornerDecorator at the same level as my expander, so now, the controls inside of the AdornerDecorator use that layer to display instead of the top level layer in the window

Categories