How do I create an XY Controller like UI in WPF - wpf

I am looking for something like this.
I should be able to drag the co-ordinate around inside the XY graph with a mouse. The position of the co-ordinate determines the X and Y values.
Is there a readily available control I can reuse? If not, how do I go about writing one?

I haven't seen any control like this, i guess you'll have to code it yourself.
There's a few things here to implement, and I'll talk about the graph portion only.
First, you should define a checklist of how this control is supposed to behave (i.e. move lines with cursor only when mousebutton is down), after that is done...well, that's the fun part!
EDIT : OK, now here's a rough Version, and when I say rough, I mean it. I put it into a window rather than a user control, you can just copy paste it into your control. This has many flaws and should be used productively only after fixing all issues that occur.Also, you have to be careful when mixing pixel design with flexible/relative design like Stretch-Alignment. I restricted this to pixel precision by making the window non-resizable.
<Window x:Class="graphedit.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="MainWindow"
x:Name="window"
MouseMove="Window_MouseMove"
Height="400" Width="400"
ResizeMode="NoResize">
<Canvas x:Name="canvas"
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch">
<Canvas.Background>
<RadialGradientBrush>
<GradientStop Color="#333333" Offset="1"></GradientStop>
<GradientStop Color="#666666" Offset="0"></GradientStop>
</RadialGradientBrush>
</Canvas.Background>
<Border BorderThickness="0,0,1,1"
BorderBrush="White"
Margin="0,0,0,0"
Width="{Binding Path=Point.X}"
Height="{Binding Path=Point.Y}"></Border>
<Border BorderThickness="1,1,0,0"
BorderBrush="White"
Margin="{Binding Path=BottomRightBoxMargin}"
Width="{Binding Path=BottomRightBoxDimensions.X}"
Height="{Binding Path=BottomRightBoxDimensions.Y}"></Border>
<Border BorderThickness="1"
BorderBrush="White"
Margin="{Binding Path=GripperMargin}"
Background="DimGray"
CornerRadius="4"
Width="10"
x:Name="gripper"
MouseDown="gripper_MouseDown"
MouseUp="gripper_MouseUp"
Height="10"></Border>
<TextBox Text="{Binding Path=Point.X}" Canvas.Left="174" Canvas.Top="333" Width="42"></TextBox>
<TextBox Text="{Binding Path=Point.Y}" Canvas.Left="232" Canvas.Top="333" Width="45"></TextBox>
<TextBlock Foreground="White" Canvas.Left="162" Canvas.Top="336">X</TextBlock>
<TextBlock Canvas.Left="222" Canvas.Top="336" Foreground="White" Width="13">Y</TextBlock>
</Canvas>
The code-behind looks like the following:
using System.ComponentModel;
using System.Windows;
namespace graphedit
{
/// <summary>
/// Interaction logic for MainWindow.xaml
/// </summary>
public partial class MainWindow : Window, INotifyPropertyChanged
{
private bool isDragging;
private Point mousePositionBeforeMove;
private Point point;
public Point Point
{
get { return this.point; }
set
{
this.point = value;
this.InvokePropertyChanged(null);
}
}
public Thickness BottomRightBoxMargin
{
get
{
Thickness t = new Thickness()
{
Left = this.Point.X,
Top = this.Point.Y
};
return t;
}
}
public Thickness GripperMargin
{
get
{
Thickness t = new Thickness()
{
Left = this.Point.X - 5,
Top = this.Point.Y - 5
};
return t;
}
}
public Point BottomRightBoxDimensions
{
get
{
return new Point(this.Width - this.Point.X,
this.Height - this.Point.Y);
}
}
public MainWindow()
{
InitializeComponent();
this.Point = new Point(100, 80);
this.DataContext = this;
}
public event PropertyChangedEventHandler PropertyChanged;
public void InvokePropertyChanged(string name)
{
PropertyChangedEventArgs args = new PropertyChangedEventArgs(name);
PropertyChangedEventHandler handler = this.PropertyChanged;
if (handler != null)
{
handler(this, args);
}
}
private void gripper_MouseDown(object sender, System.Windows.Input.MouseButtonEventArgs e)
{
this.isDragging = true;
this.mousePositionBeforeMove = e.GetPosition( this.canvas );
}
private void gripper_MouseUp(object sender, System.Windows.Input.MouseButtonEventArgs e)
{
this.isDragging = false;
}
private void Window_MouseMove(object sender, System.Windows.Input.MouseEventArgs e)
{
if(this.isDragging)
{
Point currentMousePosition = e.GetPosition( this.canvas );
double deltaX = currentMousePosition.X - this.mousePositionBeforeMove.X;
double deltaY = currentMousePosition.Y - this.mousePositionBeforeMove.Y;
double newPointX = (this.Point.X + deltaX < 0 ? 0 : (this.Point.X + deltaX > this.Width ? this.Width : this.Point.X + deltaX)) ;
double newPointY = (this.Point.Y + deltaY < 0 ? 0 : (this.Point.Y + deltaY > this.Width ? this.Width : this.Point.Y + deltaY)) ;
this.Point = new Point(newPointX,newPointY);
this.mousePositionBeforeMove = currentMousePosition;
}
}
}
}

you can have a look at the charting library DynamicDataDisplay. It's a library created as a research project by Microsoft (afaik) and might provide the functionality you are looking for.
First, reference the DynamicDataDisplay dll in your project and then create the following namespace in your xaml:
xmlns:d3="http://research.microsoft.com/DynamicDataDisplay/1.0"
Then you can add a ChartPlotter object to the xaml and strip everything you don't need from it (axes, legend, ...). You can use the CursorCoordinateGraph to track the mouse. If you want to change the layout etc, you could use a VerticalRange object.
<d3:ChartPlotter Width="500" Height="300"
MainHorizontalAxisVisibility="Collapsed"
MainVerticalAxisVisibility="Collapsed"
LegendVisibility="Collapsed" NewLegendVisible="False">
<!--This allows you to track the mouse-->
<d3:CursorCoordinateGraph x:Name="cursorGraph"/>
<!-- this range does nothing more then make the background gray,
there are other ways to achieve this too-->
<d3:VerticalRange Value1="-300" Value2="300" Fill="Gray"/>
</d3:ChartPlotter>
If you want to track the position of the mouse, you can either use code-behind:
Point current = cursorGraph.Position;
or bind the Position property to your viewmodel:
<d3:CursorCoordinateGraph x:Name="cursorGraph" Position="{Binding CurrentMousePosition}"/>
If you want to actually fix the position when you click, I guess you'll have to create a new CursorCoordinateGraph in the OnClick or MouseClick event handler for the ChartPlotter and calculate the Point and provide it for the new Graph:
//pseudo code!!!
mainGraph.DoubleClick += HandleDoubleClick;
private void HandleDoubleClick(object sender, MouseButtonEventArgs e){
//get position from current graph:
var position = cursor.Position;
//you have to calculate the "real" position in the chart because
//the Point provided by Position is not the real point, but screen coordinates
//something like cursor.TranslatePoint(cursor.Position, mainGraph);
var newCoord = new CursorCoordinateGraph { Position = position };
mainGraph.Children.Add(newCoord);
}
You might have some work to make it look like you want to. I suggest you browse through the samples provided on the codeplex page and have a look at the discussion page. This library is huge and has a lot of possibilities, but provides little actual documentation though...
Hope this points you in the right direction!

I've made demo of my simple reusable ControllerCanvas control. Hope it will help you. You can set all properties in your XAML.
ControllerCanvas.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;
using System.Windows.Media;
namespace XYControllerDemo
{
public class ControllerCanvas : Canvas
{
#region Dependency Properties
public Point Point
{
get { return (Point)GetValue(PointProperty); }
set
{
if (value.X < 0.0)
{
value.X = 0.0;
}
if (value.Y < 0.0)
{
value.Y = 0.0;
}
if (value.X > this.ActualWidth)
{
value.X = this.ActualWidth;
}
if (value.Y > this.ActualHeight)
{
value.Y = this.ActualHeight;
}
SetValue(PointProperty, value);
}
}
public static readonly DependencyProperty PointProperty =
DependencyProperty.Register("Point", typeof(Point), typeof(ControllerCanvas), new FrameworkPropertyMetadata(new Point(),
FrameworkPropertyMetadataOptions.AffectsRender | FrameworkPropertyMetadataOptions.AffectsMeasure | FrameworkPropertyMetadataOptions.SubPropertiesDoNotAffectRender));
public Brush ControllerStroke
{
get { return (Brush)GetValue(ControllerStrokeProperty); }
set { SetValue(ControllerStrokeProperty, value); }
}
public static readonly DependencyProperty ControllerStrokeProperty =
DependencyProperty.Register("ControllerStroke", typeof(Brush), typeof(ControllerCanvas), new FrameworkPropertyMetadata(Brushes.Red,
FrameworkPropertyMetadataOptions.AffectsRender | FrameworkPropertyMetadataOptions.AffectsMeasure | FrameworkPropertyMetadataOptions.SubPropertiesDoNotAffectRender));
public double ControllerStrokeThickness
{
get { return (double)GetValue(ControllerStrokeThicknessProperty); }
set { SetValue(ControllerStrokeThicknessProperty, value); }
}
public static readonly DependencyProperty ControllerStrokeThicknessProperty =
DependencyProperty.Register("ControllerStrokeThickness", typeof(double), typeof(ControllerCanvas), new FrameworkPropertyMetadata(1.0,
FrameworkPropertyMetadataOptions.AffectsRender | FrameworkPropertyMetadataOptions.AffectsMeasure | FrameworkPropertyMetadataOptions.SubPropertiesDoNotAffectRender));
public Brush GridStroke
{
get { return (Brush)GetValue(GridStrokeProperty); }
set { SetValue(GridStrokeProperty, value); }
}
public static readonly DependencyProperty GridStrokeProperty =
DependencyProperty.Register("GridStroke", typeof(Brush), typeof(ControllerCanvas), new FrameworkPropertyMetadata(Brushes.LightGray,
FrameworkPropertyMetadataOptions.AffectsRender | FrameworkPropertyMetadataOptions.AffectsMeasure | FrameworkPropertyMetadataOptions.SubPropertiesDoNotAffectRender));
public double GridStrokeThickness
{
get { return (double)GetValue(GridStrokeThicknessProperty); }
set { SetValue(GridStrokeThicknessProperty, value); }
}
public static readonly DependencyProperty GridStrokeThicknessProperty =
DependencyProperty.Register("GridStrokeThickness", typeof(double), typeof(ControllerCanvas), new FrameworkPropertyMetadata(1.0,
FrameworkPropertyMetadataOptions.AffectsRender | FrameworkPropertyMetadataOptions.AffectsMeasure | FrameworkPropertyMetadataOptions.SubPropertiesDoNotAffectRender));
public bool GridVisible
{
get { return (bool)GetValue(GridVisibleProperty); }
set { SetValue(GridVisibleProperty, value); }
}
public static readonly DependencyProperty GridVisibleProperty =
DependencyProperty.Register("GridVisible", typeof(bool), typeof(ControllerCanvas), new FrameworkPropertyMetadata(false,
FrameworkPropertyMetadataOptions.AffectsRender | FrameworkPropertyMetadataOptions.AffectsMeasure | FrameworkPropertyMetadataOptions.SubPropertiesDoNotAffectRender));
public Thickness GridMargin
{
get { return (Thickness)GetValue(GridMarginProperty); }
set { SetValue(GridMarginProperty, value); }
}
public static readonly DependencyProperty GridMarginProperty =
DependencyProperty.Register("GridMargin", typeof(Thickness), typeof(ControllerCanvas), new FrameworkPropertyMetadata(new Thickness(0.0, 0.0, 0.0, 0.0),
FrameworkPropertyMetadataOptions.AffectsRender | FrameworkPropertyMetadataOptions.AffectsMeasure | FrameworkPropertyMetadataOptions.SubPropertiesDoNotAffectRender));
public double GridSize
{
get { return (double)GetValue(GridSizeProperty); }
set { SetValue(GridSizeProperty, value); }
}
public static readonly DependencyProperty GridSizeProperty =
DependencyProperty.Register("GridSize", typeof(double), typeof(ControllerCanvas), new FrameworkPropertyMetadata(30.0,
FrameworkPropertyMetadataOptions.AffectsRender | FrameworkPropertyMetadataOptions.AffectsMeasure | FrameworkPropertyMetadataOptions.SubPropertiesDoNotAffectRender));
#endregion
#region Drawing Context
Pen penGrid = null;
Pen penController = null;
Brush previousGridStroke = null;
double previousGridStrokeThickness = double.NaN;
double penGridHalfThickness = double.NaN;
Brush previousControllerStroke = null;
double previousControllerStrokeThickness = double.NaN;
double penControllerHalfThickness = double.NaN;
Point p1 = new Point();
Point p2 = new Point();
Point p3 = new Point();
Point p4 = new Point();
double width = double.NaN;
double height = double.NaN;
void DrawGrid(DrawingContext dc)
{
width = this.ActualWidth;
height = this.ActualHeight;
// draw vertical grid lines
for (double y = GridMargin.Top; y <= height - GridMargin.Bottom; y += GridSize)
{
p1.X = GridMargin.Left;
p1.Y = y;
p2.X = width - GridMargin.Right;
p2.Y = y;
GuidelineSet g = new GuidelineSet();
g.GuidelinesX.Add(p1.X + penGridHalfThickness);
g.GuidelinesX.Add(p2.X + penGridHalfThickness);
g.GuidelinesY.Add(p1.Y + penGridHalfThickness);
g.GuidelinesY.Add(p2.Y + penGridHalfThickness);
dc.PushGuidelineSet(g);
dc.DrawLine(penGrid, p1, p2);
dc.Pop();
}
// draw horizontal grid lines
for (double x = GridMargin.Left; x <= width - GridMargin.Right; x += GridSize)
{
p1.X = x;
p1.Y = GridMargin.Top;
p2.X = x;
p2.Y = height - GridMargin.Bottom;
GuidelineSet g = new GuidelineSet();
g.GuidelinesX.Add(p1.X + penGridHalfThickness);
g.GuidelinesX.Add(p2.X + penGridHalfThickness);
g.GuidelinesY.Add(p1.Y + penGridHalfThickness);
g.GuidelinesY.Add(p2.Y + penGridHalfThickness);
dc.PushGuidelineSet(g);
dc.DrawLine(penGrid, p1, p2);
dc.Pop();
}
}
void DrawController(DrawingContext dc)
{
width = this.ActualWidth;
height = this.ActualHeight;
// draw vertical controller line
p1.X = 0.0;
p1.Y = Point.Y;
p2.X = width;
p2.Y = Point.Y;
GuidelineSet g1 = new GuidelineSet();
g1.GuidelinesX.Add(p1.X + penControllerHalfThickness);
g1.GuidelinesX.Add(p2.X + penControllerHalfThickness);
g1.GuidelinesY.Add(p1.Y + penControllerHalfThickness);
g1.GuidelinesY.Add(p2.Y + penControllerHalfThickness);
dc.PushGuidelineSet(g1);
dc.DrawLine(penController, p1, p2);
dc.Pop();
// draw horizontal controller line
p3.X = Point.X;
p3.Y = 0.0;
p4.X = Point.X;
p4.Y = height;
GuidelineSet g2 = new GuidelineSet();
g2.GuidelinesX.Add(p3.X + penControllerHalfThickness);
g2.GuidelinesX.Add(p4.X + penControllerHalfThickness);
g2.GuidelinesY.Add(p3.Y + penControllerHalfThickness);
g2.GuidelinesY.Add(p4.Y + penControllerHalfThickness);
dc.PushGuidelineSet(g2);
dc.DrawLine(penController, p3, p4);
dc.Pop();
}
protected override void OnRender(DrawingContext dc)
{
base.OnRender(dc);
// create ord update grid pen
if (penGrid == null)
{
penGrid = new Pen(GridStroke, GridStrokeThickness);
previousGridStroke = GridStroke;
previousGridStrokeThickness = GridStrokeThickness;
penGridHalfThickness = penGrid.Thickness / 2.0;
}
else
{
if (GridStroke != previousGridStroke || GridStrokeThickness != previousGridStrokeThickness)
{
previousGridStroke = GridStroke;
previousGridStrokeThickness = GridStrokeThickness;
penGrid.Brush = GridStroke;
penGrid.Thickness = GridStrokeThickness;
penGridHalfThickness = penGrid.Thickness / 2.0;
}
}
// create ord update controller pen
if (penController == null)
{
penController = new Pen(ControllerStroke, ControllerStrokeThickness);
previousControllerStroke = ControllerStroke;
previousControllerStrokeThickness = ControllerStrokeThickness;
penControllerHalfThickness = penController.Thickness / 2.0;
}
else
{
if (ControllerStroke != previousControllerStroke || ControllerStrokeThickness != previousControllerStrokeThickness)
{
previousControllerStroke = ControllerStroke;
previousControllerStrokeThickness = ControllerStrokeThickness;
penController.Brush = ControllerStroke;
penController.Thickness = ControllerStrokeThickness;
penControllerHalfThickness = penController.Thickness / 2.0;
}
}
// drag grid
if (GridVisible)
{
DrawGrid(dc);
}
// draw controller
DrawController(dc);
}
#endregion
#region Mouse Events
protected override void OnMouseLeftButtonDown(System.Windows.Input.MouseButtonEventArgs e)
{
if (!this.IsMouseCaptured)
{
this.Point = e.GetPosition(this);
this.Cursor = Cursors.Hand;
this.CaptureMouse();
}
base.OnMouseLeftButtonDown(e);
}
protected override void OnMouseLeftButtonUp(System.Windows.Input.MouseButtonEventArgs e)
{
if (this.IsMouseCaptured)
{
this.Cursor = Cursors.Arrow;
this.ReleaseMouseCapture();
}
base.OnMouseLeftButtonUp(e);
}
protected override void OnMouseMove(System.Windows.Input.MouseEventArgs e)
{
if (this.IsMouseCaptured)
{
this.Point = e.GetPosition(this);
}
base.OnMouseMove(e);
}
#endregion
}
}
MainWindow.xaml
<Window x:Class="XYControllerDemo.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:XYControllerDemo"
Title="XYControllerDemo" Height="410" Width="680">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="50*"/>
<ColumnDefinition Width="50*"/>
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<local:ControllerCanvas x:Name="controller1"
Margin="10" Grid.Column="0" Grid.Row="0"
Background="Transparent" Width="300" Height="300"
GridMargin="0,0,0,0" GridVisible="True" GridSize="30"
GridStroke="LightGray" GridStrokeThickness="1.0"
ControllerStroke="Red" ControllerStrokeThickness="1.0"
Point="50,50"/>
<TextBox Grid.Row="1" Grid.Column="0" Margin="10" Width="100"
Text="{Binding ElementName=controller1, Path=Point, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"/>
<local:ControllerCanvas x:Name="controller2"
Margin="10" Grid.Column="1" Grid.Row="0"
Background="Transparent" Width="300" Height="300"
GridMargin="0,0,0,0" GridVisible="True" GridSize="30"
GridStroke="LightGray" GridStrokeThickness="1.0"
ControllerStroke="Blue" ControllerStrokeThickness="1.0"
Point="90,250"/>
<TextBox Grid.Row="1" Grid.Column="1" Margin="10" Width="100"
Text="{Binding ElementName=controller2, Path=Point, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"/>
</Grid>
</Window>
MainWindow.xaml.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;
namespace XYControllerDemo
{
/// <summary>
/// Interaction logic for MainWindow.xaml
/// </summary>
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
}
}
}

Related

Wpf Decimals Styling

I want to Style a TextBox with decimal places like this:
How can I do that ?
You can extend the TextBox like this.
using System.Windows;
using System.Windows.Controls;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
public class DecimalTextBox : TextBox
{
public static readonly DependencyProperty FloatColorProperty = DependencyProperty.Register("FloatColor", typeof(Color), typeof(DecimalTextBox), new FrameworkPropertyMetadata(Colors.Red));
public Color FloatColor
{
get { return (Color)GetValue(FloatColorProperty); }
set { SetValue(FloatColorProperty, value); }
}
protected TextBlock _textBlock;
protected FrameworkElement _textBoxView;
public DecimalTextBox()
{
_textBlock = new TextBlock() { Margin = new Thickness(1, 0, 0, 0) };
Loaded += ExTextBox_Loaded;
}
private void ExTextBox_Loaded(object sender, RoutedEventArgs e)
{
Loaded -= ExTextBox_Loaded;
// hide the original drawing visuals, by setting opacity on their parent
var visual = this.GetChildOfType<DrawingVisual>();
_textBoxView = (FrameworkElement)visual.Parent;
_textBoxView.Opacity = 0;
// add textblock to do the text drawing for us
var grid = this.GetChildOfType<Grid>();
if (grid.Children.Count >= 2)
grid.Children.Insert(1, _textBlock);
else
grid.Children.Add(_textBlock);
}
protected override void OnLostKeyboardFocus(KeyboardFocusChangedEventArgs e)
{
base.OnLostKeyboardFocus(e);
_textBoxView.Opacity = 0;
_textBlock.Visibility = Visibility.Visible;
}
protected override void OnGotKeyboardFocus(KeyboardFocusChangedEventArgs e)
{
base.OnGotKeyboardFocus(e);
_textBoxView.Opacity = 1;
_textBlock.Visibility = Visibility.Collapsed;
}
protected override void OnTextChanged(TextChangedEventArgs e)
{
base.OnTextChanged(e);
// making sure text on TextBlock is updated as per TextBox
var dotPos = Text.IndexOf('.');
var textPart1 = dotPos == -1 ? Text : Text.Substring(0, dotPos + 1);
var textPart2 = (dotPos == -1 || dotPos >= (Text.Length-1)) ? null : Text.Substring(dotPos + 1);
_textBlock.Inlines.Clear();
_textBlock.Inlines.Add(new Run {
Text = textPart1,
FontFamily = FontFamily,
FontSize = FontSize,
Foreground = Foreground });
if (textPart2 != null)
_textBlock.Inlines.Add(new Run {
Text = textPart2,
FontFamily = FontFamily,
TextDecorations = System.Windows.TextDecorations.Underline,
BaselineAlignment = BaselineAlignment.TextTop,
FontSize = FontSize * 5/6,
Foreground = new SolidColorBrush(FloatColor) });
}
}
public static class HelperExtensions
{
public static T GetChildOfType<T>(this DependencyObject depObj) where T : DependencyObject
{
if (depObj == null) return null;
for (int i = 0; i < VisualTreeHelper.GetChildrenCount(depObj); i++)
{
var child = VisualTreeHelper.GetChild(depObj, i);
var result = (child as T) ?? GetChildOfType<T>(child);
if (result != null) return result;
}
return null;
}
}
XAML code usage
<local:DecimalTextBox FloatColor="Maroon" />
And your output should look like this:
Update 05/17
Explanation: As you can see from the image, the DecimalTextBox displays the text in formatted mode only when its not focused.
I had initially developed the control to support formatting during edit (which can still be done by commenting the methods OnLostKeyboardFocus, and OnGotKeyboardFocus) - but because of the font-size difference the cursor positioning was getting slightly skewed, which in turn would translate to bad user experience.
Therefore, implemented the swap logic during GotFocus and LostFocus to fix that.
You can't do that with a TextBox, because TextBox only accepts color changes to the entire text. You should try with RichTextBox, that allows loop throug TextRange's. Look at this sample of syntax highlighting with a RichTextBox.
I actually understood how it works and made it:
private void richTextBox_TextChanged(object sender, TextChangedEventArgs e)
{
richTextBox.TextChanged -= this.richTextBox_TextChanged;
if (richTextBox.Document == null)
return;
TextRange documentRange = new TextRange(richTextBox.Document.ContentStart, richTextBox.Document.ContentEnd);
documentRange.ClearAllProperties();
int dotIndex = documentRange.Text.IndexOf(".");
if(dotIndex == -1)
{
richTextBox.TextChanged += this.richTextBox_TextChanged;
return;
}
TextPointer dotStart = GetPoint(richTextBox.Document.ContentStart, dotIndex);
TextPointer dotEnd = dotStart.GetPositionAtOffset(1, LogicalDirection.Forward);
TextRange initRange = new TextRange(richTextBox.Document.ContentStart, dotStart);
TextRange endRange = new TextRange(dotEnd, richTextBox.Document.ContentEnd);
endRange.ApplyPropertyValue(TextElement.ForegroundProperty, new SolidColorBrush(Colors.Red));
richTextBox.TextChanged += this.richTextBox_TextChanged;
}
Suscribe the textbox TextChanged event to this method. You can now set the styles you want to every part of the text like this:
To change the last part (after the dot char) endRange.ApplyPropertyValue(TextElement.ForegroundProperty, new SolidColorBrush(Colors.Red));
To change the first part (before the dot char) is the same but for the initRange variable. If you want to change the 'dot' style, then create a new TextRange with dotStart and dotEnd TextPointers and apply styles to it. You can do other things like change font style, size, etc.
This code result looks like this:
All this is just for style. For checking that is a number is up to you.
I would define a custom control with this main properties:
FloatNumber: the original number, that should be a DependencyProperty to be Bind from the control.
NumberOfDecimalDigits: a number to choose how many decimal digits to represent, that should be a DependencyProperty to be Bind from the control.
FirstPart: a string that will contain the first part of the decimal number
Decimals: a string that will contain the decimal digits of FloatNumber
Of course this is just a scratch, those properties could be implemented better to extract FloatNumber parts.
public partial class DecimalDisplayControl : UserControl, INotifyPropertyChanged
{
public DecimalDisplayControl()
{
InitializeComponent();
(Content as FrameworkElement).DataContext = this;
}
public static readonly DependencyProperty NumberOfDecimalDigitsProperty =
DependencyProperty.Register(
"NumberOfDecimalDigits", typeof(string),
typeof(DecimalDisplayControl), new PropertyMetadata(default(string), OnFloatNumberChanged));
public static readonly DependencyProperty FloatNumberProperty =
DependencyProperty.Register(
"FloatNumber", typeof(string),
typeof(DecimalDisplayControl), new PropertyMetadata(default(string), OnFloatNumberChanged));
private static void OnFloatNumberChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
(d as DecimalDisplayControl).OnFloatNumberChanged();
}
protected void OnFloatNumberChanged()
{
int numberOfDecimalDigits = Convert.ToInt32(NumberOfDecimalDigits);
float fullNumber = Convert.ToSingle(FloatNumber);
float firstPart = (float)Math.Truncate(fullNumber);
float fullDecimalPart = fullNumber - firstPart;
int desideredDecimalPart = (int)(fullDecimalPart * Math.Pow(10, numberOfDecimalDigits));
FirstPart = $"{firstPart}.";
Decimals = desideredDecimalPart.ToString();
}
public string FloatNumber
{
get => (string)GetValue(FloatNumberProperty);
set { SetValue(FloatNumberProperty, value); }
}
public string NumberOfDecimalDigits
{
get => (string)GetValue(NumberOfDecimalDigitsProperty);
set { SetValue(NumberOfDecimalDigitsProperty, value); }
}
private string _firstPart;
public string FirstPart
{
get => _firstPart;
set
{
if (_firstPart == value)
return;
_firstPart = value;
OnPropertyChanged();
}
}
private string _decimals;
public string Decimals
{
get => _decimals;
set
{
if (_decimals == value)
return;
_decimals = value;
OnPropertyChanged();
}
}
public event PropertyChangedEventHandler PropertyChanged;
private void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
Its XAML:
<UserControl
x:Class="WpfApp1.CustomControls.DecimalDisplayControl"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d">
<StackPanel
HorizontalAlignment="Center"
VerticalAlignment="Center"
Orientation="Horizontal">
<TextBlock
FontSize="20"
Foreground="Black"
Text="{Binding FirstPart}" />
<TextBlock
FontSize="10"
Foreground="Red"
Text="{Binding Decimals}"
TextDecorations="Underline" />
</StackPanel>
</UserControl>
Then you can use it in your page and bind a property to make it change dynamically:
<Grid>
<StackPanel VerticalAlignment="Center" Orientation="Vertical">
<customControls:DecimalDisplayControl
HorizontalAlignment="Center"
VerticalAlignment="Center"
NumberOfDecimalDigits="2"
FloatNumber="{Binding MyNumber}" />
<TextBox
Width="200"
VerticalAlignment="Center"
Text="{Binding MyNumber, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" />
</StackPanel>
</Grid>
The final result:

WPF popup with arrow head style

I would like to implement a popup style that looks like the following design:
The gray square represents the UIElement that shows the popup when it is clicked. The popup style is just a border (the easy part) with an arrow head that points to the center of the target element (the hard part). Also the aligment is important, when a control is placed on the right part of the window, the popup should be aligned to the right, otherwise to the left.
Is there an example or some documentation that instruct me how to proceed?
Okay, I have a solution for this. It's frustratingly complex.
If you're just after a plain pop-up, just with a tail, you can probably use chunks of this (ActualLayout and UpdateTail logic). If you're after the whole Help-Tip Experience™ you're in for an unpleasant ride.
I do think that it might be better to go down the Adorner route though (and I'm thinking of re-working this to use adorers). I've spotted some issues and it's still in work. Using pop-ups causes them to appear in the designer on the top of other windows and it's really annoying. I've also noticed they are not positioned correctly on some computers for some weird reasons (but none where I've got Visual Studio installed to properly debug).
It produces something like this:
With the following criteria:
Only a single help tip can be displayed on the screen at each time
If the user changes tab, and the control a help-tip is attached to is no longer visible, the help tip dissapears and the next help-tip is displayed
Once closed, a help-tip of that type won't be displayed again
Help tips can be turned off through one central option
Okay. So, the actual help tip is a usercontrol that's completely transparent and added to the UI. It has a pop-up that's managed using a static class. Here's the control:
<UserControl x:Class="...HelpPopup"
d:DesignHeight="0" d:DesignWidth="0">
<UserControl.Resources>
<BooleanToVisibilityConverter x:Key="BooleanToVisibilityConverter"/>
</UserControl.Resources>
<Canvas>
<Popup x:Name="Popup"
d:DataContext="{d:DesignInstance {x:Null}}"
DataContext="{Binding HelpTip, ElementName=userControl}"
StaysOpen="True" PopupAnimation="Fade"
AllowsTransparency="True"
materialDesign:ShadowAssist.ShadowDepth="Depth3"
Placement="{Binding Placement, ElementName=userControl}"
HorizontalOffset="-10"
VerticalOffset="{Binding VerticalOffset, ElementName=userControl}">
<Grid Margin="0,0,0,0" SnapsToDevicePixels="True">
<Canvas Margin="10">
<local:RoundedCornersPolygon Fill="{StaticResource PrimaryHueDarkBrush}"
SnapsToDevicePixels="True"
ArcRoundness="4"
Points="{Binding PolygonPath, ElementName=userControl}"
Effect="{Binding RelativeSource={RelativeSource FindAncestor, AncestorType=Popup}, Path=(materialDesign:ShadowAssist.ShadowDepth), Converter={x:Static converters:ShadowConverter.Instance}}"/>
</Canvas>
<Border BorderBrush="Transparent" BorderThickness="10,25,10,25">
<Grid x:Name="PopupChild">
<materialDesign:ColorZone Mode="PrimaryDark" Margin="5">
<StackPanel>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="AUTO"/>
</Grid.ColumnDefinitions>
<TextBlock Text="Useful Tip"
FontWeight="Bold"
Margin="2,0,0,0"
Grid.ColumnSpan="2"
VerticalAlignment="Center"/>
<Button Style="{StaticResource MaterialDesignToolButton}" Click="CloseButton_Click" Grid.Column="1" Margin="0" Padding="0" Height="Auto">
<Button.Content>
<materialDesign:PackIcon Kind="CloseCircle" Height="20" Width="20" Foreground="{StaticResource PrimaryHueLightBrush}"/>
</Button.Content>
</Button>
</Grid>
<TextBlock Text="{Binding Message}"
TextWrapping="Wrap"
MaxWidth="300"
Margin="2,4,2,4"/>
<StackPanel Orientation="Horizontal" HorizontalAlignment="Right">
<Button Content="Close" Padding="8,2" Height="Auto" Click="CloseButton_Click"
Margin="2"
Style="{StaticResource MaterialDesignFlatButtonInverted}"/>
<Button Content="Never show again"
Margin="2"
Padding="8,2"
Height="Auto"
Click="NeverShowButton_Click"
Style="{StaticResource MaterialDesignFlatButtonInverted}"/>
</StackPanel>
</StackPanel>
</materialDesign:ColorZone>
</Grid>
</Border>
</Grid>
</Popup>
</Canvas>
</UserControl>
You can change this to style how you want. I used a custom rounded polygon class and MaterialDesign colour zones. Replace these however you want.
Now, the code behind is... well, there's a lot of it, and it's not pleasant:
public enum ActualPlacement { TopLeft, TopRight, BottomLeft, BottomRight }
/// <summary>
/// Interaction logic for HelpPopup.xaml
/// </summary>
public partial class HelpPopup : UserControl, INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
private ActualPlacement actualPlacement = ActualPlacement.TopRight;
public ActualPlacement ActualPlacement
{
get { return actualPlacement; }
internal set
{
if (actualPlacement != value)
{
if (actualPlacement == ActualPlacement.BottomLeft || ActualPlacement == ActualPlacement.BottomRight)
{
Console.WriteLine("-10");
VerticalOffset = 10;
}
else if (actualPlacement == ActualPlacement.TopLeft || ActualPlacement == ActualPlacement.TopRight)
{
VerticalOffset = -10;
Console.WriteLine("10");
}
actualPlacement = value;
UpdateTailPath();
NotifyOfPropertyChange("ActualPlacement");
}
}
}
public void UpdateTailPath()
{
double height = PopupChild.ActualHeight + 30;
double width = PopupChild.ActualWidth;
switch (actualPlacement)
{
case ActualPlacement.TopRight:
polygonPath = "0.5,15.5 " + (width - 0.5) + ",15.5 " + (width - 0.5) + "," + (height - 15.5) +
" 15.5," + (height - 15.5) + " 0.5," + height + " 0.5,15.5"; ;
break;
case ActualPlacement.TopLeft:
polygonPath = "0.5,15.5 " + (width - 0.5) + ",15.5 " + (width - 0.5) + "," + height + " " + (width - 15.5) + "," + (height - 15.5) +
" 0.5," + (height - 15.5) + " 0.5,15.5";
break;
case ActualPlacement.BottomRight:
polygonPath = "0.5,0.5 15.5,15.5 " + (width - 0.5) + ",15.5 " + (width - 0.5) + "," + (height - 15.5) +
" 0.5," + (height - 15.5) + " 0.5,0.5";
break;
case ActualPlacement.BottomLeft:
polygonPath = "0.5,15.5 " + (width - 15.5) + ",15.5 " + (width - 0.5) + ",0.5 " + (width - 0.5) + "," + (height - 15.5) +
" 0.5," + (height - 15.5) + " 0.5,15.5";
break;
}
NotifyOfPropertyChange("PolygonPath");
}
private String polygonPath;
public String PolygonPath
{
get { return polygonPath; }
}
public PlacementMode Placement
{
get { return (PlacementMode)GetValue(PlacementProperty); }
set { SetValue(PlacementProperty, value); }
}
// Using a DependencyProperty as the backing store for Placement. This enables animation, styling, binding, etc...
public static readonly DependencyProperty PlacementProperty =
DependencyProperty.Register("Placement", typeof(PlacementMode), typeof(HelpPopup), new PropertyMetadata(PlacementMode.Top));
public int VerticalOffset
{
get { return (int)GetValue(VerticalOffsetProperty); }
set { SetValue(VerticalOffsetProperty, value); }
}
// Using a DependencyProperty as the backing store for VerticalOffset. This enables animation, styling, binding, etc...
public static readonly DependencyProperty VerticalOffsetProperty =
DependencyProperty.Register("VerticalOffset", typeof(int), typeof(HelpPopup), new PropertyMetadata(-10));
public HelpTip HelpTip
{
get { return (HelpTip)GetValue(HelpTipProperty); }
set { SetValue(HelpTipProperty, value); }
}
// Using a DependencyProperty as the backing store for Message. This enables animation, styling, binding, etc...
public static readonly DependencyProperty HelpTipProperty =
DependencyProperty.Register("HelpTip", typeof(HelpTip), typeof(HelpPopup), new PropertyMetadata(new HelpTip() { Message = "No help message found..." }, HelpTipChanged));
private static void HelpTipChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
if ((d as HelpPopup).HelpTipOnScreenInstance == null)
{
(d as HelpPopup).HelpTipOnScreenInstance = new HelpTipOnScreenInstance((d as HelpPopup));
}
(d as HelpPopup).HelpTipOnScreenInstance.HelpTip = (e.NewValue as HelpTip);
}
private static void HelpTipOnScreenInstance_PropertyChanged(object sender, PropertyChangedEventArgs e)
{
HelpTipOnScreenInstance htosi = sender as HelpTipOnScreenInstance;
if (e.PropertyName.Equals(nameof(htosi.IsOpen)))
{
//open manually to avoid stupid COM errors
if (htosi != null)
{
try
{
htosi.HelpPopup.Popup.IsOpen = htosi.IsOpen;
}
catch (System.ComponentModel.Win32Exception ex)
{
Canvas parent = htosi.HelpPopup.Popup.Parent as Canvas;
htosi.HelpPopup.Popup.IsOpen = false;
parent.Children.Remove(htosi.HelpPopup.Popup);
Application.Current.Dispatcher.BeginInvoke(new Action(() => {
htosi.HelpPopup.Popup.IsOpen = true;
parent.Children.Add(htosi.HelpPopup.Popup);
htosi.HelpPopup.UpdatePositions();
}), DispatcherPriority.SystemIdle);
}
}
}
}
private HelpTipOnScreenInstance helpTipOnScreenInstance;
public HelpTipOnScreenInstance HelpTipOnScreenInstance
{
get { return helpTipOnScreenInstance; }
set
{
if (helpTipOnScreenInstance != value)
{
if (helpTipOnScreenInstance != null)
{
HelpTipOnScreenInstance.PropertyChanged -= HelpTipOnScreenInstance_PropertyChanged;
}
helpTipOnScreenInstance = value;
HelpTipOnScreenInstance.PropertyChanged += HelpTipOnScreenInstance_PropertyChanged;
NotifyOfPropertyChange("HelpTipOnScreenInstance");
}
}
}
private double popupX;
public double PopupX
{
get { return popupX; }
set
{
if (popupX != value)
{
popupX = value;
NotifyOfPropertyChange("PopupX");
}
}
}
private double popupY;
public double PopupY
{
get { return popupY; }
set
{
if (popupY != value)
{
popupY = value;
NotifyOfPropertyChange("PopupY");
}
}
}
private void NotifyOfPropertyChange(string propertyName)
{
PropertyChangedEventHandler handler = PropertyChanged;
if (handler != null)
{
handler(this, new PropertyChangedEventArgs(propertyName));
}
}
public HelpPopup()
{
InitializeComponent();
// Wire up the Loaded handler instead
this.Loaded += new RoutedEventHandler(View1_Loaded);
this.Unloaded += HelpPopup_Unloaded;
Popup.Opened += Popup_Opened;
//PopupChild.LayoutUpdated += HelpPopup_LayoutUpdated;
PopupChild.SizeChanged += HelpPopup_SizeChanged;
UpdatePositions();
}
private void Popup_Opened(object sender, EventArgs e)
{
UpdateTail();
UpdateTailPath();
}
private void HelpPopup_SizeChanged(object sender, SizeChangedEventArgs e)
{
Console.WriteLine(HelpTip.Message + ": " + e.PreviousSize.ToString() + " to " + e.NewSize.ToString());
UpdateTail();
UpdateTailPath();
}
private void HelpPopup_Unloaded(object sender, RoutedEventArgs e)
{
//don't waste resources on never show popups
if (HelpTip.NeverShow)
{
return;
}
HelpTipOnScreenInstance.IsOnscreen = false;
}
/// Provides a way to "dock" the Popup control to the Window
/// so that the popup "sticks" to the window while the window is dragged around.
void View1_Loaded(object sender, RoutedEventArgs e)
{
//don't waste resources on never show popups
if (HelpTip.NeverShow)
{
return;
}
//wait for a few seconds, then set this to on-screen
HelpTipOnScreenInstance.IsOnscreen = true;
//update so tail is facing right direction
UpdateTail();
Window w = Window.GetWindow(this);
// w should not be Null now!
if (null != w)
{
w.LocationChanged += delegate (object sender2, EventArgs args)
{
// "bump" the offset to cause the popup to reposition itself
// on its own
UpdatePositions();
};
// Also handle the window being resized (so the popup's position stays
// relative to its target element if the target element moves upon
// window resize)
w.SizeChanged += delegate (object sender3, SizeChangedEventArgs e2)
{
UpdatePositions();
};
}
}
private void UpdatePositions()
{
var offset = Popup.HorizontalOffset;
Popup.HorizontalOffset = offset + 1;
Popup.HorizontalOffset = offset;
UpdateTail();
}
private void UpdateTail()
{
UIElement container = VisualTreeHelper.GetParent(this) as UIElement;
Point relativeLocation = PopupChild.TranslatePoint(new Point(5, 5), container); //It HAS(!!!) to be this.Child
if (relativeLocation.Y < 0)
{
if (relativeLocation.X < -(PopupChild.ActualWidth-5 / 2))
{
ActualPlacement = ActualPlacement.TopLeft;
}
else
{
ActualPlacement = ActualPlacement.TopRight;
}
}
else
{
if (relativeLocation.X < -(PopupChild.ActualWidth-5 / 2))
{
ActualPlacement = ActualPlacement.BottomLeft;
}
else
{
ActualPlacement = ActualPlacement.BottomRight;
}
}
}
private void CloseButton_Click(object sender, RoutedEventArgs e)
{
lock (HelpTip.Lock)
{
HelpTip.Closed = true;
HelpTipOnScreenInstance.IsOpen = false;
}
}
private void NeverShowButton_Click(object sender, RoutedEventArgs e)
{
lock (HelpTip.Lock)
{
HelpTip.Closed = true;
HelpTip.NeverShow = true;
HelpTipOnScreenInstance.IsOpen = false;
}
}
}
Things to note.
There's "ActualPlacement" to manage the actual placement of the popup, as setting placement is just an advisory to WPF.
UpdateTailPath() is re-drawing the polygon to get the tail in the
correct position after placement has changed.
We have both a HelpTip class that stores the information (title,
content etc), and HelpTipOnScreenInstance which controls whether it's
on screen. The reason for this is we can have multiple help-tip of
the same type on screen and only want to display one.
Various listeners for popup events to trigger tail updates.
We attach to the load and unload events of the usercontrol. This
allows us to keep track of whether the control is on the screen and
whether a help tip should or should not be displayed
(HelpTipOnScreenInstance.IsOnscreen = true).
We also listen for window change events so we can update the position
of the pop-up if the window is resized or moved.
Now, HelpTipOnScreenInstance, and HelpTip:
public class HelpTipOnScreenInstance : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
public object Lock = new Object();
private void NotifyOfPropertyChange(string propertyName)
{
PropertyChangedEventHandler handler = PropertyChanged;
if (handler != null)
{
//handler(this, new PropertyChangedEventArgs(propertyName));
handler(this, new PropertyChangedEventArgs(propertyName));
}
}
private HelpTip helpTip;
public HelpTip HelpTip
{
get { return helpTip; }
set
{
if (helpTip != value)
{
helpTip = value;
NotifyOfPropertyChange("HelpTip");
}
}
}
private bool isOpen = false;
public bool IsOpen
{
get { return isOpen; }
set
{
if (isOpen != value)
{
isOpen = value;
Console.WriteLine("Opening " + HelpTip.Message);
NotifyOfPropertyChange("IsOpen");
}
}
}
private bool isOnscreen = false;
public bool IsOnscreen
{
get { return isOnscreen; }
set
{
if (isOnscreen != value)
{
isOnscreen = value;
NotifyOfPropertyChange("IsOnscreen");
}
}
}
private HelpPopup helpPopup;
public HelpPopup HelpPopup
{
get { return helpPopup; }
set
{
if (helpPopup != value)
{
helpPopup = value;
NotifyOfPropertyChange("HelpPopup");
}
}
}
public HelpTipOnScreenInstance(HelpPopup helpPopup)
{
HelpPopup = helpPopup;
HelpTipManager.AddHelpTip(this);
}
}
public class HelpTip : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
public object Lock = new Object();
private void NotifyOfPropertyChange(string propertyName)
{
PropertyChangedEventHandler handler = PropertyChanged;
if (handler != null)
{
//handler(this, new PropertyChangedEventArgs(propertyName));
handler(this, new PropertyChangedEventArgs(propertyName));
}
}
private String id;
public String ID
{
get { return id; }
set { id = value; }
}
private String message;
public String Message
{
get { return message; }
set
{
if (message != value)
{
message = value;
NotifyOfPropertyChange("Message");
}
}
}
private bool closed;
public bool Closed
{
get { return closed; }
set
{
if (closed != value)
{
closed = value;
NotifyOfPropertyChange("Closed");
}
}
}
public bool NeverShow { get; set; }
}
And then a static manager class that keeps track of what's on screen and what's not, and chooses who get's displayed next:
public static class HelpTipManager
{
public static object Lock = new Object();
private static bool displayHelpTips = false;
public static bool DisplayHelpTips
{
get { return displayHelpTips; }
set {
if (displayHelpTips != value)
{
displayHelpTips = value;
if (displayHelpTips)
{
//open next!
OpenNext();
}
else
{
//stop displaying all
foreach(HelpTipOnScreenInstance helpTip in helpTipsOnScreen)
{
lock (helpTip.HelpTip.Lock)
{
helpTip.IsOpen = false;
}
}
}
}
}
}
private static List<HelpTipOnScreenInstance> helpTips = new List<HelpTipOnScreenInstance>();
private static List<HelpTipOnScreenInstance> helpTipsOnScreen = new List<HelpTipOnScreenInstance>();
private static bool supressOpenNext = false;
public static void AddHelpTip(HelpTipOnScreenInstance helpTip)
{
helpTip.PropertyChanged += HelpTip_PropertyChanged;
helpTips.Add(helpTip);
}
private static void HelpTip_PropertyChanged(object sender, System.ComponentModel.PropertyChangedEventArgs e)
{
HelpTipOnScreenInstance helpTip = sender as HelpTipOnScreenInstance;
if (helpTip != null)
{
//is this on screen or not?
switch (e.PropertyName)
{
case "IsOnscreen":
//Update our onscreen lists and perform related behaviour
if (helpTip.IsOnscreen)
{
AddedToScreen(helpTip);
}
else
{
RemovedFromScreen(helpTip);
}
break;
case "IsOpen":
lock (helpTip.Lock)
{
if (!supressOpenNext)
{
if (!helpTip.IsOpen)
{
OpenNext();
}
}
}
break;
}
}
}
private static void OpenNext()
{
if (DisplayHelpTips)
{
if (helpTipsOnScreen.Count > 0)
{
//check if none of them are open
if (helpTipsOnScreen.Count(ht => ht.IsOpen) == 0)
{
//open the first that's not been closed!
HelpTipOnScreenInstance firstNotClosed = helpTipsOnScreen.FirstOrDefault(ht => !ht.HelpTip.Closed);
if (firstNotClosed != null)
{
lock (firstNotClosed.Lock)
{
firstNotClosed.IsOpen = true;
}
}
}
}
}
}
private static void AddedToScreen(HelpTipOnScreenInstance helpTip)
{
lock (Lock)
{
helpTipsOnScreen.Add(helpTip);
OpenNext();
}
}
private static void RemovedFromScreen(HelpTipOnScreenInstance helpTip)
{
lock (Lock)
{
helpTipsOnScreen.Remove(helpTip);
supressOpenNext = true;
helpTip.IsOpen = false;
//OpenNext();
supressOpenNext = false;
}
}
}
So how to use it? You can add help tip data in your generic.xaml or a resource library like this:
<controls:HelpTip x:Key="KPIGraphMenu" ID="KPIGraphMenu" Message="Right click to change the colour, remove, or move KPI to view as a stacked trace. KPI can also be dragged onto other charts of any type."/>
and use them in the actual application like this, I like to overlay them in a grid with the control they're associated with, using the Alignment to determine where the tail points to:
<controls:HelpPopup HelpTip="{StaticResource KPIGraphMenu}" HorizontalAlignment="Center" VerticalAlignment="Center"/>
I have used CustomPopupPlacementCallback Delegate . I have even considered vertical shifting of your arrow. So, now in this example below, arrow shifts left/right, up/down.
One can use this sample as it is.
Window1.xaml
<Window ...>
<Grid>
<Button Click="Btn_Click" Width="110" Height="25" Content="Button" HorizontalAlignment="Left" Margin="437,26,0,0" VerticalAlignment="Top"/>
<Button Click="Btn_Click" Content="Button" HorizontalAlignment="Left" Margin="10,90,0,0" VerticalAlignment="Top" Width="75"/>
<Button Click="Btn_Click" Content="Button" HorizontalAlignment="Left" Margin="139,146,0,0" VerticalAlignment="Top" Width="75"/>
<Button Click="Btn_Click" Content="Button" HorizontalAlignment="Left" Margin="180,0,0,0" VerticalAlignment="Top" Width="74"/>
<Button Click="Btn_Click" Content="Button" HorizontalAlignment="Left" Margin="224,333,0,0" VerticalAlignment="Top" Width="76"/>
<Button Click="Btn_Click" Content="Button" HorizontalAlignment="Right" VerticalAlignment="Top" Width="75"/>
<Button Click="Btn_Click" Content="Button" HorizontalAlignment="Left" VerticalAlignment="Bottom" Width="75" />
<Button Click="Btn_Click" Content="Button" HorizontalAlignment="Right" VerticalAlignment="Bottom" Width="75" />
<Button Click="Btn_Click" Content="Button" HorizontalAlignment="Left" VerticalAlignment="Top" Width="75" />
<Popup x:Name="Popup1" Placement="Custom" StaysOpen="False" Opened="Popup1_Opened">
<Grid x:Name="Grd" Width="300" Height="100" Background="AliceBlue">
<Canvas x:Name="Cnv">
<Path x:Name="TopArrow" Canvas.Left="50" Canvas.Top="25" Margin="5" Data="M0,0 L-5,-5 L-10,0 z" Fill="Black" Stroke="Black" StrokeThickness="2"/>
<TextBlock Canvas.Top="35" FontSize="18" x:Name="Tb1"/>
</Canvas>
</Grid>
</Popup>
</Grid>
</Window>
Window1.xaml.cs
using System;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Controls.Primitives;
using System.Windows.Shapes;
namespace ...
{
/// <summary>
/// Interaction logic for Window1.xaml
/// </summary>
public partial class Window1 : Window
{
public Window1()
{
InitializeComponent();
Popup1.CustomPopupPlacementCallback =
new CustomPopupPlacementCallback(placePopup);
}
public CustomPopupPlacement[] placePopup(Size popupSize,
Size targetSize,
Point offset)
{
CustomPopupPlacement placement2 =
new CustomPopupPlacement(new Point(-(popupSize.Width - targetSize.Width / 2), targetSize.Height), PopupPrimaryAxis.Vertical);
CustomPopupPlacement placement1 =
new CustomPopupPlacement(new Point(targetSize.Width / 2, targetSize.Height), PopupPrimaryAxis.Vertical);
CustomPopupPlacement placement3 =
new CustomPopupPlacement(new Point(targetSize.Width/2, -popupSize.Height), PopupPrimaryAxis.Horizontal);
CustomPopupPlacement placement4 =
new CustomPopupPlacement(new Point(-(popupSize.Width - targetSize.Width/2), -popupSize.Height), PopupPrimaryAxis.Horizontal);
CustomPopupPlacement[] ttplaces =
new CustomPopupPlacement[] { placement1, placement2, placement3, placement4 };
return ttplaces;
}
private void Btn_Click(object sender, RoutedEventArgs e)
{
Popup1.PlacementTarget = sender as Button;
Popup1.IsOpen = true;
}
private void Popup1_Opened(object sender, EventArgs e)
{
Path arrow = ((Path)Popup1.FindName("TopArrow"));
Grid grd = ((Grid)Popup1.FindName("Grd"));
UIElement elem = (UIElement)Popup1.PlacementTarget;
Point elem_pos_lefttop = elem.PointToScreen(new Point(0, 0));
Point popup_pos_lefttop = grd.PointToScreen(new Point(0, 0));
if ( (elem_pos_lefttop.Y < popup_pos_lefttop.Y )
&&
((elem_pos_lefttop.X > popup_pos_lefttop.X))
)
{
Canvas.SetLeft(arrow, 280);
Canvas.SetTop(arrow, 25);
}
if ((elem_pos_lefttop.Y < popup_pos_lefttop.Y)
&&
((elem_pos_lefttop.X < popup_pos_lefttop.X))
)
{
Canvas.SetLeft(arrow, 30);
Canvas.SetTop(arrow, 25);
}
if ((elem_pos_lefttop.Y > popup_pos_lefttop.Y)
&&
((elem_pos_lefttop.X > popup_pos_lefttop.X))
)
{
Canvas.SetLeft(arrow, 280);
Canvas.SetTop(arrow, 90);
}
if ((elem_pos_lefttop.Y > popup_pos_lefttop.Y)
&&
((elem_pos_lefttop.X < popup_pos_lefttop.X))
)
{
Canvas.SetLeft(arrow, 30);
Canvas.SetTop(arrow, 90);
}
Tb1.Text = String.Format("Element = {0} \r\n Popup = {1}", elem_pos_lefttop, popup_pos_lefttop);
}
}
}
Please tell if this solves your issue.

WPF Windows Process Bar / Pie

Using WPF - how can I create a graph that looks like the Windows Progress bar - doughnut chart?
https://guyterry.files.wordpress.com/2015/07/upgradingrelax.jpg
Try to make it as a user control using ring component from available drag and drop components. Add label and make some properties which you can modify to get certain result.
See this post.
Or try Tutorial
Try this:
using System;
using System.Globalization;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Media;
using System.Windows.Shapes;
namespace WpfApplication1
{
public class CircleProgress : Canvas
{
public static readonly DependencyProperty ValueProperty =
DependencyProperty.Register("Value", typeof(double),
typeof(CircleProgress), new FrameworkPropertyMetadata(180d, OnValueChanged));
public static readonly DependencyProperty MinimumProperty =
DependencyProperty.Register("Minimum", typeof(double),
typeof(CircleProgress), new FrameworkPropertyMetadata(0d, OnValueChanged));
public static readonly DependencyProperty MaximumProperty =
DependencyProperty.Register("Maximum", typeof(double),
typeof(CircleProgress), new FrameworkPropertyMetadata(360d, OnValueChanged));
public static readonly DependencyProperty BackgroundCircleStrokeProperty =
DependencyProperty.Register("BackgroundCircleStroke", typeof(Brush),
typeof(CircleProgress), new FrameworkPropertyMetadata(Brushes.Gray, OnStrokeChanged));
public static readonly DependencyProperty BackgroundCircleStrokeThicknessProperty =
DependencyProperty.Register("BackgroundCircleStrokeThickness", typeof(double),
typeof(CircleProgress), new FrameworkPropertyMetadata(5d, OnStrokeChanged));
public static readonly DependencyProperty MainCircleStrokeProperty =
DependencyProperty.Register("MainCircleStroke", typeof(Brush),
typeof(CircleProgress), new FrameworkPropertyMetadata(Brushes.DeepSkyBlue, OnStrokeChanged));
public static readonly DependencyProperty MainCircleStrokeThicknessProperty =
DependencyProperty.Register("MainCircleStrokeThickness", typeof(double),
typeof(CircleProgress), new FrameworkPropertyMetadata(5d, OnStrokeChanged));
public static readonly DependencyProperty TextStrokeProperty =
DependencyProperty.Register("TextStroke", typeof(Brush),
typeof(CircleProgress), new FrameworkPropertyMetadata(Brushes.Black, OnStrokeChanged));
public double Value
{
get { return (double)GetValue(ValueProperty); }
set { SetValue(ValueProperty, value); }
}
public double Minimum
{
get { return (double)GetValue(MinimumProperty); }
set { SetValue(MinimumProperty, value); }
}
public double Maximum
{
get { return (double)GetValue(MaximumProperty); }
set { SetValue(MaximumProperty, value); }
}
public Brush BackgroundCircleStroke
{
get { return (Brush) GetValue(BackgroundCircleStrokeProperty); }
set { SetValue(BackgroundCircleStrokeProperty, value); }
}
public Brush MainCircleStroke
{
get { return (Brush)GetValue(MainCircleStrokeProperty); }
set { SetValue(MainCircleStrokeProperty, value); }
}
public Brush TextStroke
{
get { return (Brush)GetValue(MainCircleStrokeProperty); }
set { SetValue(MainCircleStrokeProperty, value); }
}
public double BackgroundCircleStrokeThickness
{
get { return (double)GetValue(MaximumProperty); }
set { SetValue(MaximumProperty, value); }
}
public double MainCircleStrokeThickness
{
get { return (double)GetValue(MaximumProperty); }
set { SetValue(MaximumProperty, value); }
}
private readonly Path _backEllipse = new Path()
{
StrokeThickness = 5,
Stroke = Brushes.Gray
};
private readonly Path _mainEllipse = new Path()
{
StrokeThickness = 5,
Stroke = Brushes.DeepSkyBlue
};
private readonly Path _text = new Path()
{
StrokeThickness = 1,
Fill = Brushes.Black
};
private double _radius = 10;
private Point _center = new Point(0,0);
private Point _startPoint = new Point(0,0);
public CircleProgress()
{
_backEllipse.Data = new EllipseGeometry(new Point(Width/2, Height/2), _radius, _radius);
_mainEllipse.Data = new PathGeometry()
{
Figures = new PathFigureCollection()
{
new PathFigure(new Point(Width/2, 0), new PathSegmentCollection()
{
new ArcSegment(
(new RotateTransform(Value*(360/(Maximum - Minimum)), _center.X, _center.Y)).Transform(
_startPoint), new Size(_radius*2, _radius*2), 0, true, SweepDirection.Clockwise, false)
}, false)
}
};
var text = new FormattedText(Value.ToString(CultureInfo.CurrentCulture),
CultureInfo.CurrentCulture,
FlowDirection.LeftToRight,
new Typeface("Palatino"),
0.8* _radius,
Brushes.Black);
_text.Data = text.BuildGeometry(new Point(_center.X - text.Width/2, _center.Y - text.Height/2));
SizeChanged += OnSizeChanged;
Children.Add(_backEllipse);
Children.Add(_mainEllipse);
Children.Add(_text);
}
private void OnSizeChanged(object sender, SizeChangedEventArgs e)
{
var quadSize = e.NewSize.Height <= e.NewSize.Width ? e.NewSize.Height : e.NewSize.Width;
_radius = quadSize / 2;
_center = new Point(e.NewSize.Width/2, e.NewSize.Height / 2);
_startPoint = _center - new Vector(0, _radius);
UpdateCircle(this);
}
/// <summary>
/// Action when Value, Minimum or Maximum changed.
/// </summary>
/// <param name="d">Dependecy object.</param>
/// <param name="e">EventArgs.</param>
private static void OnValueChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
var circle = d as CircleProgress;
UpdateCircle(circle);
}
private static void OnStrokeChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
var circle = d as CircleProgress;
circle._backEllipse.Stroke = circle.BackgroundCircleStroke;
circle._backEllipse.StrokeThickness = circle.BackgroundCircleStrokeThickness;
circle._mainEllipse.Stroke = circle.BackgroundCircleStroke;
circle._mainEllipse.StrokeThickness = circle.BackgroundCircleStrokeThickness;
circle._text.Fill = circle.TextStroke;
}
/// <summary>
/// Update Background and Main circles.
/// </summary>
/// <param name="circle">Reference to CircleProgress control.</param>
private static void UpdateCircle(CircleProgress circle)
{
circle._backEllipse.Data = new EllipseGeometry(circle._center, circle._radius, circle._radius);
if (Math.Abs(circle.Value*(360/(circle.Maximum - circle.Minimum)) - 360) < 0.0001)
circle._mainEllipse.Data = new EllipseGeometry(circle._center, circle._radius, circle._radius);
else
{
circle._mainEllipse.Data = new PathGeometry()
{
Figures = new PathFigureCollection()
{
new PathFigure(circle._startPoint, new PathSegmentCollection()
{
new ArcSegment(
(new RotateTransform(circle.Value*(360/(circle.Maximum - circle.Minimum)),
circle._center.X, circle._center.Y)).Transform(
circle._startPoint), new Size(circle._radius, circle._radius), 0,
!(circle.Value*(360/(circle.Maximum - circle.Minimum)) <= 180), SweepDirection.Clockwise,
true)
}, false)
}
};
}
var text = new FormattedText($"{circle.Value:##}",
CultureInfo.CurrentCulture,
FlowDirection.LeftToRight,
new Typeface("Palatino"),
0.8 * circle._radius,
Brushes.Black);
circle._text.Data = text.BuildGeometry(new Point(circle._center.X - text.Width / 2, circle._center.Y - text.Height / 2));
}
}
}
XAML Usage:
<Window x:Class="WpfApplication1.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:WpfApplication1"
mc:Ignorable="d"
Title="MainWindow" Height="350" Width="525" x:Name="Main">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="472*"/>
<ColumnDefinition Width="45*"/>
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition/>
<RowDefinition Height="27"/>
</Grid.RowDefinitions>
<local:CircleProgress Grid.Row="0" Grid.Column="0" Margin="75" Minimum="{Binding ElementName=MinTextBox, Path=Text}" Maximum="{Binding ElementName=MaxTextBox, Path=Text}" Value="{Binding ElementName=slider, Path=Value}"/>
<Slider x:Name="slider" Grid.Column="1" Grid.Row="0" Orientation="Vertical" TickPlacement="BottomRight" Minimum="{Binding ElementName=MinTextBox, Path=Text}" Maximum="{Binding ElementName=MaxTextBox, Path=Text}"/>
<StackPanel Grid.Column="0" Grid.Row="1" Orientation="Horizontal">
<Label Content="Minimum" />
<TextBox Width="200" x:Name="MinTextBox"/>
<Label Content="Maximum"/>
<TextBox Width="150" x:Name="MaxTextBox"/>
</StackPanel>
</Grid>
</Window>
Here is i've got:

About setbinding in canvas

<StackPanel Height="650" Width="650" Background="Green" HorizontalAlignment="Center" VerticalAlignment="Center">
<ItemsControl Name="Display_Test" Margin="10">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<StackPanel Background="Black" Width="600" Height="600"/>
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate>
<ContentPresenter Content="{Binding}" Margin="15" Height="500" Width="500"/>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</StackPanel>
// c# code start ====================================================================
public partial class MainWindow : Window, INotifyPropertyChanged
{
public MainWindow()
{
this.InitializeComponent();
Canvas canvasSample = new Canvas();
canvasSample.Width = 100;
canvasSample.Height = 100;
canvasSample.Background = new SolidColorBrush(Colors.LightBlue);
CanvasList.Add(canvasSample);
Canvas canvasSample2 = new Canvas();
canvasSample2.Width = 100;
canvasSample2.Height = 100;
canvasSample2.Background = new SolidColorBrush(Colors.LightGreen);
CanvasList.Add(canvasSample2);
Rectangle rectangleSample = new Rectangle();
rectangleSample.Width = 30;
rectangleSample.Height = 30;
RectangleList.Add(rectangleSample);
Rectangle rectangleSample2 = new Rectangle();
rectangleSample2.Width = 30;
rectangleSample2.Height = 30;
RectangleList.Add(rectangleSample2);
Display_Test.ItemsSource = CanvasList;
this.MouseDoubleClick += new MouseButtonEventHandler(MainWindow_MouseDoubleClick);
}
void MainWindow_MouseDoubleClick(object sender, MouseButtonEventArgs e)
{
for (int nCanvasNum = 0; nCanvasNum < CanvasList.Count; nCanvasNum++)
{
Binding conBinding = new Binding()
{
Source = RectangleList
};
CanvasList[nCanvasNum].DataContext = RectangleList;
CanvasList[nCanvasNum].SetBinding(ItemsControl.ItemsSourceProperty, conBinding);
}
}
ObservableCollection<Canvas> _canvasList = new ObservableCollection<Canvas>();
public ObservableCollection<Canvas> CanvasList
{
get { return _canvasList; }
set
{
_canvasList = value;
OnPropertyChanged(new PropertyChangedEventArgs("CanvasList"));
}
}
ObservableCollection<Rectangle> _rectangleList = new ObservableCollection<Rectangle>();
public ObservableCollection<Rectangle> RectangleList
{
get { return _rectangleList; }
set
{
_rectangleList = value;
OnPropertyChanged(new PropertyChangedEventArgs("RegtangleList"));
}
}
#region INotifyPropertyChanged Members
public event PropertyChangedEventHandler PropertyChanged;
protected void OnPropertyChanged(PropertyChangedEventArgs e)
{
if (PropertyChanged != null)
{
PropertyChanged(this, e);
}
}
#endregion
}
// c# code end ====================================================================
when i clicked mousebutton double I want to create RectangleList in CanvasList...
But I'm afraid I can't show that...I think that is problem because of setbinding...
I will wait for your answer....
CanvasList[nCanvasNum].SetBinding(ItemsControl.ItemsSourceProperty, conBinding); won't work bevause Canvas is not derived from ItemsControl and does not have ItemSource property. It has children property but it is not dependency property so you can't bind to it but you can add childrens manually. You can try to use ItemsControl instead of Canvas and set Canvas as a panel for ItemsControl the same way you did it to Show canvases

Translate a WPF canvas at high scale factors isn't smooth away from origin

Imagine you have a canvas that you want to scale to a very high value and then allow "panning."
A good example is a geographic tool that needs to allow panning with "zoom" levels from the extent of the whole earth down to the extent of a few meters.
I've found that if you are scaled in to more than, say 500,000, translating becomes very erratic, but ONLY if you are viewing far from the canvas's 0,0 origin!
I've tried to translate using the RenderTransform of the canvas AND I've tried it by literally moving anothercanvas on top of the scaled canvas. I've also seen the same issue in someone else's sample app online.
The following example code provides for panning (click and drag) at two different zoom locations. If you implement the code, you can hit one button to zoom in to 0,0 where you will find nice, smooth mouse panning. Then use the other button to zoom in to 200, 200 and smooth panning is no more!
Any idea why this is or how one could fix it?
XAML for sample:
<Window x:Class="TestPanZoom.Window1"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="Window1" Height="500" Width="500" Loaded="Window_Loaded">
<Grid PreviewMouseLeftButtonDown="Grid_PreviewMouseLeftButtonDown" MouseMove="Grid_MouseMove">
<Canvas Name="canvas1"></Canvas>
<Button Height="31"
Name="button1"
Click="button1_Click"
Margin="12,12,0,0"
VerticalAlignment="Top"
HorizontalAlignment="Left" Width="270">
Zoom WAY in to 0,0 and get smooth panning
</Button>
<Button Height="31"
Name="button2"
Click="button2_Click"
Margin="12,49,0,0"
VerticalAlignment="Top"
HorizontalAlignment="Left"
Width="270">
Zoom WAY in to 200, 200 -- NO smooth panning
</Button>
</Grid>
</Window>
Code for sample:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;
namespace TestPanZoom
{
/// <summary>
/// Interaction logic for Window1.xaml
/// Demo of an issue with translate transform when scale is very high
/// but ONLY when you are far from the canvas's 0,0 origin.
/// Why? Is their a fix?
/// </summary>
public partial class Window1 : Window
{
Point m_clickPoint;
public Window1()
{
InitializeComponent();
}
private void Window_Loaded(object sender, RoutedEventArgs e)
{
// Add a 2x2 black ellipse centered at 0,0
Ellipse el = new Ellipse();
el.Fill = Brushes.Black;
el.Width = 2;
el.Height = 2;
el.HorizontalAlignment = HorizontalAlignment.Left;
el.VerticalAlignment = VerticalAlignment.Top;
el.Margin = new Thickness(0 - el.Width / 2, 0 - el.Height / 2, 0, 0);
canvas1.Children.Add(el);
// Add a 1x1 red rectangle with its top/left corner at 0,0
Rectangle r = new Rectangle();
r.Fill = Brushes.Red;
r.Width = 1;
r.Height = 1;
r.HorizontalAlignment = HorizontalAlignment.Left;
r.VerticalAlignment = VerticalAlignment.Top;
r.Margin = new Thickness(0, 0, 0, 0);
canvas1.Children.Add(r);
// Add a 2x2 purple ellipse at a point 200,200
Point otherPoint = new Point(200, 200);
el = new Ellipse();
el.Fill = Brushes.Purple;
el.Width = 2;
el.Height = 2;
el.HorizontalAlignment = HorizontalAlignment.Left;
el.VerticalAlignment = VerticalAlignment.Top;
el.Margin = new Thickness(otherPoint.X - el.Width / 2, otherPoint.Y - el.Height / 2, 0, 0);
canvas1.Children.Add(el);
// Add a 1x1 blue rectangle with its top/left corner at 200,200
r = new Rectangle();
r.Fill = Brushes.Blue;
r.Width = 1;
r.Height = 1;
r.HorizontalAlignment = HorizontalAlignment.Left;
r.VerticalAlignment = VerticalAlignment.Top;
r.Margin = new Thickness(otherPoint.X, otherPoint.Y, 0, 0);
canvas1.Children.Add(r);
}
private void Grid_PreviewMouseLeftButtonDown(object sender, MouseButtonEventArgs e)
{
m_clickPoint = e.GetPosition(this);
}
// Pan with the mouse when left-mouse is down
private void Grid_MouseMove(object sender, MouseEventArgs e)
{
if (e.LeftButton == MouseButtonState.Pressed)
{
Point mousePosition = e.GetPosition(this);
double xDiff = mousePosition.X - m_clickPoint.X;
double yDiff = mousePosition.Y - m_clickPoint.Y;
TranslateTransform tt = new TranslateTransform(xDiff, yDiff);
TransformGroup tg = new TransformGroup();
tg.Children.Add(canvas1.RenderTransform);
tg.Children.Add(tt);
canvas1.RenderTransform = tg;
m_clickPoint = e.GetPosition(this);
}
}
private void button1_Click(object sender, RoutedEventArgs e)
{
TransformGroup tg = new TransformGroup();
double scale = 1000000;
double xCenter = 0;
double yCenter = 0;
double xOffset = (canvas1.ActualHeight / 2.0 - xCenter);
double yOffset = (canvas1.ActualWidth / 2.0 - yCenter);
ScaleTransform st = new ScaleTransform(scale, scale);
st.CenterX = xCenter;
st.CenterY = yCenter;
TranslateTransform tt = new TranslateTransform(xOffset, yOffset);
tg.Children.Add(st);
tg.Children.Add(tt);
canvas1.RenderTransform = tg;
}
private void button2_Click(object sender, RoutedEventArgs e)
{
TransformGroup tg = new TransformGroup();
double scale = 1000000;
double xCenter = 200;
double yCenter = 200;
double xOffset = (canvas1.ActualHeight / 2.0 - xCenter);
double yOffset = (canvas1.ActualWidth / 2.0 - yCenter);
ScaleTransform st = new ScaleTransform(scale, scale);
st.CenterX = xCenter;
st.CenterY = yCenter;
TranslateTransform tt = new TranslateTransform(xOffset, yOffset);
tg.Children.Add(st);
tg.Children.Add(tt);
canvas1.RenderTransform = tg;
}
}
}
This is caused by Canvas itself. It is not well perfomed for more advanced rendering. Insted you need to use a Visual class. It's a bit harder yet you gain the advantage of low level rendering.
Solution download
Here is the code: MainWindow.xaml
<Window x:Class="VisualTest.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="VisualLayer" Height="350.4" Width="496.8"
xmlns:local="clr-namespace:VisualTest"
>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"></ColumnDefinition>
<ColumnDefinition></ColumnDefinition>
</Grid.ColumnDefinitions>
<StackPanel Orientation="Vertical">
<Button Name="button1" Click="button1_Click" Margin="5" Padding="5,0">
Zoom WAY in to 0,0
</Button>
<Button Name="button2" Click="button2_Click" Margin="5" Padding="5,0">
Zoom WAY in to 200, 200
</Button>
<Button Name="button3" Click="button3_Click" Margin="5" Padding="5,0">
Zoom back
</Button>
</StackPanel>
<local:DrawingCanvas Grid.Column="1" x:Name="drawingSurface" Background="White" ClipToBounds="True"
MouseLeftButtonDown="drawingSurface_MouseLeftButtonDown"
MouseLeftButtonUp="drawingSurface_MouseLeftButtonUp"
MouseMove="drawingSurface_MouseMove">
</local:DrawingCanvas>
</Grid>
MainWindow.xaml.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;
namespace VisualTest
{
/// <summary>
/// Interaction logic for MainWindow.xaml
/// </summary>
public partial class MainWindow : Window
{
// Variables for dragging shapes.
private bool isDragging = false;
private Vector clickOffset;
private DrawingVisual selectedVisual;
// Drawing constants.
private Brush drawingBrush = Brushes.Black;
private Brush selectedDrawingBrush = Brushes.LightGoldenrodYellow;
private Pen drawingPen = new Pen(Brushes.SteelBlue, 3);
private Size squareSize = new Size(10, 10);
public MainWindow()
{
InitializeComponent();
DrawingVisual v = new DrawingVisual();
DrawSquare(v, new Point(0, 0));
drawingSurface.AddVisual(v);
v = new DrawingVisual();
DrawSquare(v, new Point(200, 200));
drawingSurface.AddVisual(v);
}
private void button1_Click(object sender, RoutedEventArgs e)
{
TransformGroup tg = new TransformGroup();
double scale = 1000000;
double xCenter = 0;
double yCenter = 0;
double xOffset = (drawingSurface.ActualHeight / 2.0 - xCenter);
double yOffset = (drawingSurface.ActualWidth / 2.0 - yCenter);
ScaleTransform st = new ScaleTransform(scale, scale);
st.CenterX = xCenter;
st.CenterY = yCenter;
TranslateTransform tt = new TranslateTransform(xOffset, yOffset);
tg.Children.Add(st);
tg.Children.Add(tt);
drawingSurface.RenderTransform = st;
}
private void button2_Click(object sender, RoutedEventArgs e)
{
TransformGroup tg = new TransformGroup();
double scale = 1000000;
double xCenter = 200;
double yCenter = 200;
double xOffset = (drawingSurface.ActualHeight / 2.0 - xCenter);
double yOffset = (drawingSurface.ActualWidth / 2.0 - yCenter);
ScaleTransform st = new ScaleTransform(scale, scale);
st.CenterX = xCenter;
st.CenterY = yCenter;
TranslateTransform tt = new TranslateTransform(xOffset, yOffset);
tg.Children.Add(st);
tg.Children.Add(tt);
drawingSurface.RenderTransform = st;
}
private void button3_Click(object sender, RoutedEventArgs e)
{
ScaleTransform st = new ScaleTransform(1, 1);
drawingSurface.RenderTransform = st;
}
private void drawingSurface_MouseLeftButtonDown(object sender, MouseButtonEventArgs e)
{
Point pointClicked = e.GetPosition(drawingSurface);
DrawingVisual visual = drawingSurface.GetVisual(pointClicked);
if (visual != null)
{
// Calculate the top-left corner of the square.
// This is done by looking at the current bounds and
// removing half the border (pen thickness).
// An alternate solution would be to store the top-left
// point of every visual in a collection in the
// DrawingCanvas, and provide this point when hit testing.
Point topLeftCorner = new Point(
visual.ContentBounds.TopLeft.X ,
visual.ContentBounds.TopLeft.Y );
DrawSquare(visual, topLeftCorner);
clickOffset = topLeftCorner - pointClicked;
isDragging = true;
if (selectedVisual != null && selectedVisual != visual)
{
// The selection has changed. Clear the previous selection.
ClearSelection();
}
selectedVisual = visual;
}
}
// Rendering the square.
private void DrawSquare(DrawingVisual visual, Point topLeftCorner)
{
using (DrawingContext dc = visual.RenderOpen())
{
Brush brush = drawingBrush;
dc.DrawRectangle(brush, null,
new Rect(topLeftCorner, squareSize));
}
}
private void drawingSurface_MouseLeftButtonUp(object sender, MouseButtonEventArgs e)
{
isDragging = false;
}
private void ClearSelection()
{
Point topLeftCorner = new Point(
selectedVisual.ContentBounds.TopLeft.X ,
selectedVisual.ContentBounds.TopLeft.Y );
DrawSquare(selectedVisual, topLeftCorner);
selectedVisual = null;
}
private void drawingSurface_MouseMove(object sender, MouseEventArgs e)
{
if (isDragging)
{
Point pointDragged = e.GetPosition(drawingSurface) + clickOffset;
DrawSquare(selectedVisual, pointDragged);
}
}
}
}
DrawingCanvas.cs
using System;
using System.Collections.Generic;
using System.Text;
using System.Windows.Media;
using System.Windows.Controls;
using System.Windows;
namespace VisualTest
{
public class DrawingCanvas : Panel
{
private List<Visual> visuals = new List<Visual>();
protected override Visual GetVisualChild(int index)
{
return visuals[index];
}
protected override int VisualChildrenCount
{
get
{
return visuals.Count;
}
}
public void AddVisual(Visual visual)
{
visuals.Add(visual);
base.AddVisualChild(visual);
base.AddLogicalChild(visual);
}
public void DeleteVisual(Visual visual)
{
visuals.Remove(visual);
base.RemoveVisualChild(visual);
base.RemoveLogicalChild(visual);
}
public DrawingVisual GetVisual(Point point)
{
HitTestResult hitResult = VisualTreeHelper.HitTest(this, point);
return hitResult.VisualHit as DrawingVisual;
}
private List<DrawingVisual> hits = new List<DrawingVisual>();
public List<DrawingVisual> GetVisuals(Geometry region)
{
hits.Clear();
GeometryHitTestParameters parameters = new GeometryHitTestParameters(region);
HitTestResultCallback callback = new HitTestResultCallback(this.HitTestCallback);
VisualTreeHelper.HitTest(this, null, callback, parameters);
return hits;
}
private HitTestResultBehavior HitTestCallback(HitTestResult result)
{
GeometryHitTestResult geometryResult = (GeometryHitTestResult)result;
DrawingVisual visual = result.VisualHit as DrawingVisual;
if (visual != null &&
geometryResult.IntersectionDetail == IntersectionDetail.FullyInside)
{
hits.Add(visual);
}
return HitTestResultBehavior.Continue;
}
}
}
Dragging system need to be rewrite. The idea is simple yet implementation is a bit complicated.

Resources