How to change a mouse cursor shape for entire canvas? - c#

I have WPF window with several textboxes, buttons, and canvas where I draw a graph. I would like to have cross cursor over the canvas, because this way user could point interesting area in more "scientific" way :-)
Oddly, when I set cursor to cross for a canvas, it is still a standard arrow instead, but when the mouse is over any line or polyline I drew on the canvas it is cross.
So how to set the cursor for entire canvas (including "empty" space, where nothing is drawn)?

To change a mouse cursor shape for entire canvas add a transparent background to your canvas.
Here is a sample:
<Canvas Grid.Row="2" Background="Transparent">
<Polyline Points="25,25 0,50 25,75 50,50 25,25 25,0"
Stroke="Blue" StrokeThickness="10"
Canvas.Left="75" Canvas.Top="50">
<Polyline.RenderTransform>
<RotateTransform CenterX="0" CenterY="0" Angle="45" />
</Polyline.RenderTransform>
</Polyline>
<Canvas.Style>
<Style TargetType="{x:Type Canvas}">
<Style.Triggers>
<Trigger Property="IsMouseOver" Value="True" >
<Setter Property="Cursor" Value="Cross" />
</Trigger>
</Style.Triggers>
</Style>
</Canvas.Style>
</Canvas>

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 - Diamond-shaped button style with text inside

How to make diamond-shaped button style with text that is displayed horizontally like this
I did this, but don't know what to do next:
<Style x:Key="Button">
<Setter Property="Button.Height" Value="40"/>
<Setter Property="Button.Width" Value="40"/>
<Setter Property="Button.RenderTransform">
<Setter.Value>
<RotateTransform Angle="45"/>
</Setter.Value>
</Setter>
</Style>
The easiest option is to not define your text content utilizing the Button control Content dependency property, but instead define your Text in a separate control such as the obvious TextBlock, since the content is also going to be rotated 45 degrees due to the render transform.
In your style, if you want your button to look exactly as the one in the link that you provided, you should also set the following dependency properties: BorderThickness, BorderBrush (Black) and Background (White).
<Grid.Resources>
<Style TargetType="Button">
<Setter Property="Height" Value="100"/>
<Setter Property="Width" Value="100"/>
<Setter Property="BorderThickness" Value="4"/>
<Setter Property="BorderBrush" Value="Black"/>
<Setter Property="Background" Value="White"/>
<Setter Property="RenderTransform">
<Setter.Value>
<RotateTransform Angle="45"/>
</Setter.Value>
</Setter>
</Style>
</Grid.Resources>
Place the pair Button/TextBlock inside the same layout area, and overlap the Button control with the TextBlock. Depending on the dimensions to which you set your Button control element, you can easily play with the Margin dependency property on the TextBlock control and put it exactly on your desired place.
This would be my option if I were to choose a Grid as the layout container for these 2 control definitions.
If instead I choose to use a Canvas, I would utilize the attached properties Canvas.Left and Canvas.Top, like this
<Canvas Margin="100,0,0,0">
<Button x:Name="myButton"/>
<TextBlock Text="TextTextText" Canvas.Left="-40" Canvas.Top="60"/>
</Canvas>
You could actually have an CLR property calculating these two distances, based on your Button dimensions and perform binding to these two attached properties. For this situation, I set those values for the offsets for a Button dimension of (Width: 100, Height: 100).
PS. The code provided is working for UWP (I am more used to it :p), so I am not particularly 100% sure that the solution works right of the bat for WPF.
You could try rotating the button by 45 degrees like you did and then adding a TextBlock which is rotated -45 degrees to keep it horizontal. I'm sure there are better ways but this should work.
<Button>
<TextBlock Text="Testing">
<TextBlock.RenderTransform>
<TransformGroup>
<ScaleTransform/>
<SkewTransform/>
<RotateTransform Angle="-45"/>
<TranslateTransform/>
</TransformGroup>
</TextBlock.RenderTransform>
</TextBlock>
<Button.RenderTransform>
<TransformGroup>
<ScaleTransform/>
<SkewTransform/>
<RotateTransform Angle="45"/>
<TranslateTransform/>
</TransformGroup>
</Button.RenderTransform>
</Button>

WPF: Custom tooltip arrow placement

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.

WPF: Making the entire "block" of a path clickable

I have a special ControlTemplate for some of my Buttons.
<ControlTemplate TargetType="{x:Type Button}">
<Path Name="ThePath" Fill="White" Stretch="UniformToFill"
Width="12" Height="12" Stroke="White"
StrokeThickness="4"
Data="M1.5,1.5 L10.5,10.5 M1.5,10.5 L10.5,1.5"/>
<ControlTemplate.Triggers>
<Trigger Property="IsFocused" Value="True">
<Setter Property="Fill" Value="#afa" TargetName="ThePath"/>
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
This works fine, but since I'm using a Path (in this case, it's just shaped like a fat X), exactly the path is clickable, not the small space between the corners of the X. Is there any automagic thing I can use to make the entire "block" of the X clickable?
I've considered wrapping the path in a rectangular object, but I'd just like to make sure I'm not missing something trivial.
Aviad P. is correct. This is what I do:
<Border Background="Transparent">
<Path ... />
</Border>
This works because when "hit testing" to determine where a mouse click should be routed, the "Transparent" brush is considered as if it was a regular color.

WPF Expander Border Animation affects all contained controls?

I'm using a WPF Expander to display a number of analog process variables.
I want to make the expander's border 'glow' (or flash) when one of these variables enters a 'warning' or 'alarm' state.
To achieve this, I'm using some data triggers bound to a couple of boolean properties in my view model ('AnalogWarningActive' and 'AnalogAlarmActive'). The data triggers kick off a storyboard that animates the expander border opacity.
The data triggers work as I'd expect: the proper border colour appears and the opacity animation begins. However, there are 2 problems:
The opacity of the entire expander (and all contained controls) is changing and not just the opacity of its border.
When the 'AnalogWarningActive' and 'AnalogAlarmActive' tags return to False, the border disappears but the opactiy animation continues indefinitely (ie. the entire expander continues to fade in and out).
Here is the xaml I'm using:
<SolidColorBrush x:Key="AnalogAlarmBrush" Color="#FFFF8080" />
<SolidColorBrush x:Key="AnalogWarningBrush" Color="#FFFFFF80" />
<Storyboard x:Key="AlarmBorderFlasher" AutoReverse="True" RepeatBehavior="Forever">
<DoubleAnimation
Storyboard.TargetProperty="(Border.Opacity)"
From="1.0" To="0.4"
Duration="0:0:0.8" />
</Storyboard>
<Expander Header="Test Data" IsExpanded="True">
<Expander.Style>
<Style TargetType="{x:Type Expander}">
<Style.Triggers>
<DataTrigger Binding="{Binding Path=AnalogWarningActive}" Value="True" >
<DataTrigger.EnterActions>
<BeginStoryboard Name="WarningBorderStoryboard" Storyboard="{StaticResource AlarmBorderFlasher}" />
</DataTrigger.EnterActions>
<DataTrigger.ExitActions>
<StopStoryboard BeginStoryboardName="WarningBorderStoryboard" />
</DataTrigger.ExitActions>
<DataTrigger.Setters>
<Setter Property="BorderBrush" Value="{StaticResource AnalogWarningBrush}" />
<Setter Property="BorderThickness" Value="4" />
</DataTrigger.Setters>
</DataTrigger>
<DataTrigger Binding="{Binding Path=AnalogAlarmActive}" Value="True" >
<DataTrigger.EnterActions>
<BeginStoryboard Name="AlarmBorderStoryboard" Storyboard="{StaticResource AlarmBorderFlasher}" />
</DataTrigger.EnterActions>
<DataTrigger.ExitActions>
<StopStoryboard BeginStoryboardName="AlarmBorderStoryboard" />
</DataTrigger.ExitActions>
<DataTrigger.Setters>
<Setter Property="BorderBrush" Value="{StaticResource AnalogAlarmBrush}" />
<Setter Property="BorderThickness" Value="4" />
</DataTrigger.Setters>
</DataTrigger>
</Style.Triggers>
</Style>
</Expander.Style>
<!-- snipped the contents of the expander (a tabcontrol and a few text boxes, labels, etc)-->
</Expander>
Question #1
The Opacity setting on a Visual such as Border affects that Visual and all its descendants. That is why setting Border.Opacity makes everything disappear.
You have two choices: 1. Animate the border Stroke property, or 2. Change the content to not be a descendant of Border.
Animating the Stroke property is trivial to code, but has the disadvantage that not all brushes are easily animatable to transparent and back. For example, this is difficult with gradient brushes. Also, if your borders were a variety of colors and you didn't want to go to 100% transparent there is no good way to animate the Stroke without changing the color.
Changing the content to not be a descendant of Border is very simple, and is what I would be inclined to do in most cases. Simply replace:
<Border x:Name="MyBorder" Stroke="Red" StrokeThickness="3" CornerRadius="6">
<my:ContentHere />
</Border>
with this:
<Grid>
<Border x:Name="MyBorder" Stroke="Red" StrokeThickness="3" Stroke="Red" CornerRadius="6">
<Border Stroke="Transparent" StrokeThickness="3" CornerRadius="Red">
<my:ContentHere />
</Border>
</Grid>
Now the visible border's opacity can be animated, while the transparent one controls the layout and clipping of the child.
If you don't have any CornerRadius or other funny business, you can just set a margin on the content and forego the transparent border:
<Grid>
<Border x:Name="MyBorder" Stroke="Red" StrokeThickness="3" />
<my:ContentHere Margin="3" />
</Grid>
Question #2
At first glance I don't see any problem with your XAML that would cause the animation to keep running after the triggering value goes back to false, but I didn't look very closely. From what I noticed I would think it would stop the animation at the current value, not leave it running.
You might try replacing StopStoryboard with RemoveStoryboard. RemoveStoryboard should reset the animated properties back to their original values.

Categories