The title of this question might be wrong, I am not sure how to phrase it. I am trying to implement a very simple dashboard in which users can drag controls around inside a Canvas control. I wrote a MoveThumb class that inherits Thumb to achieve this. It is working well enough. Now, I want to make sure that the user cannot move the draggable control outside the Canvas. It is simple enough to write the logic itself to limit the drag boundaries inside this MoveThumb class:
Public Class MoveThumb
Inherits Thumb
Public Sub New()
AddHandler DragDelta, New DragDeltaEventHandler(AddressOf Me.MoveThumb_DragDelta)
End Sub
Private Sub MoveThumb_DragDelta(ByVal sender As Object, ByVal e As DragDeltaEventArgs)
Dim item As Control = TryCast(Me.DataContext, Control)
If item IsNot Nothing Then
Dim left As Double = Canvas.GetLeft(item)
Dim top As Double = Canvas.GetTop(item)
Dim right As Double = left + item.ActualWidth
Dim bottom As Double = top + item.ActualHeight
Dim canvasWidth = 450
Dim canvasHeight = 800
If left + e.HorizontalChange > 0 Then
If top + e.VerticalChange > 0 Then
If right + e.HorizontalChange < canvasWidth Then
If bottom + e.VerticalChange > canvasHeight Then
Canvas.SetLeft(item, left + e.HorizontalChange)
Canvas.SetTop(item, top + e.VerticalChange)
End If
End If
End If
End If
End If
End Sub
End Class
And the XML:
<Window x:Class="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:diagramDesigner"
xmlns:s="clr-namespace:diagramDesigner"
mc:Ignorable="d"
Title="MainWindow" Height="450" Width="800">
<Grid>
<Canvas x:Name="Canvas1">
<Canvas.Resources>
<ControlTemplate x:Key="MoveThumbTemplate" TargetType="{x:Type s:MoveThumb}">
<Rectangle Fill="Transparent"/>
</ControlTemplate>
<ControlTemplate x:Key="DesignerItemTemplate" TargetType="ContentControl">
<Grid DataContext="{Binding RelativeSource={RelativeSource TemplatedParent}}">
<s:MoveThumb Template="{StaticResource MoveThumbTemplate}" Cursor="SizeAll"/>
<ContentPresenter Content="{TemplateBinding ContentControl.Content}"/>
</Grid>
</ControlTemplate>
</Canvas.Resources>
<ContentControl Name="DesignerItem"
Width="100"
Height="100"
Canvas.Top="100"
Canvas.Left="100"
Template="{StaticResource DesignerItemTemplate}">
<Ellipse Fill="Blue" IsHitTestVisible="False"/>
</ContentControl>
</Canvas>
</Grid>
</Window>
Problem is, I am explicitly stating the width and height of the canvas inside that MoveThumb class, which means that if my window changes size, and the Canvas changes size, the drag boundaries will remain the same. Ideally, I want to bind canvasWidth and canvasHeight to the actualWidth and actualHeight of the Canvas.
I am not sure what is the best way to achieve it. Acquiring the actualWidth and actualHeight values inside the MoveThumb_DragDelta function with something like actualWidth = mainWindow.Canvas1.ActualWidth would be quick and simple, but very bad coding practice.
Ideally, I imagine it would be best to pass the limits as arguments to the constructor of MoveThumb and stored as global field/property, but I don't see a way to do it, since this class is used as a template in the XML code, and not generated from code-behind. I am not exactly sure if that would work at all, because the MoveThumb might be instantized only once (during the creation of the control), so it won't work when the Canvas changes it's size afterwards.
So I probably should do some kind of one-way binding between the actualWidth of Canvas1 and canvasWidth (declared as global property) of MoveThumb. But again, I have no idea how to access it, since MoveThumb is used as a TargetType of ControlTemplate inside Canvas.Resources.
I am still pretty new to WPF, and it feels like there should be some very simple way to achieve this, but I'm not seeing it. Can anyone help?
Example using Dependency Properties:
public partial class MoveThumb : Thumb
{
private double privateCanvasWidth = double.NaN, privateCanvasHeight = double.NaN;
private static readonly Binding bindingActualWidth = new Binding()
{
Path = new PropertyPath(ActualWidthProperty),
RelativeSource = new RelativeSource(RelativeSourceMode.FindAncestor, typeof(Canvas), 1)
};
private static readonly Binding bindingActualHeight = new Binding()
{
Path = new PropertyPath(ActualHeightProperty),
RelativeSource = new RelativeSource(RelativeSourceMode.FindAncestor, typeof(Canvas), 1)
};
public MoveThumb()
{
DragDelta += MoveThumb_DragDelta;
SetBinding(CanvasWidthProperty, bindingActualWidth);
SetBinding(CanvasHeightProperty, bindingActualHeight);
}
static MoveThumb()
{
DefaultStyleKeyProperty.OverrideMetadata(typeof(MoveThumb), new FrameworkPropertyMetadata(typeof(MoveThumb)));
}
private static void MoveThumb_DragDelta(object sender, DragDeltaEventArgs e)
{
MoveThumb thumb = (MoveThumb)sender;
//FrameworkElement item = thumb.MovableContent;
//if (item == null)
//{
// return;
//}
double left = Canvas.GetLeft(thumb);
double top = Canvas.GetTop(thumb);
double right = left + thumb.ActualWidth;
double bottom = top + thumb.ActualHeight;
double canvasWidth = thumb.privateCanvasWidth;
if (double.IsNaN(canvasWidth))
canvasWidth = 450;
double canvasHeight = thumb.privateCanvasHeight;
if (double.IsNaN(canvasHeight))
canvasWidth = 800;
left += e.HorizontalChange;
top += e.VerticalChange;
right += e.HorizontalChange;
bottom += e.VerticalChange;
if (left > 0 &&
top > 0 &&
right < canvasWidth &&
bottom < canvasHeight)
{
Canvas.SetLeft(thumb, left);
Canvas.SetTop(thumb, top);
}
}
}
// DependecyProperties
[ContentProperty(nameof(MovableContent))]
public partial class MoveThumb
{
/// <summary>Canvas Width.</summary>
public double CanvasWidth
{
get => (double)GetValue(CanvasWidthProperty);
set => SetValue(CanvasWidthProperty, value);
}
/// <summary><see cref="DependencyProperty"/> for property <see cref="CanvasWidth"/>.</summary>
public static readonly DependencyProperty CanvasWidthProperty =
DependencyProperty.Register(nameof(CanvasWidth), typeof(double), typeof(MoveThumb), new PropertyMetadata(double.NaN, CanvasSizeChanged));
/// <summary>Canvas Height.</summary>
public double CanvasHeight
{
get => (double)GetValue(CanvasHeightProperty);
set => SetValue(CanvasHeightProperty, value);
}
/// <summary><see cref="DependencyProperty"/> for property <see cref="CanvasHeight"/>.</summary>
public static readonly DependencyProperty CanvasHeightProperty =
DependencyProperty.Register(nameof(CanvasHeight), typeof(double), typeof(MoveThumb), new PropertyMetadata(double.NaN, CanvasSizeChanged));
// Property change handler.
// The code is shown as an example.
private static void CanvasSizeChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
MoveThumb thumb = (MoveThumb)d;
if (e.Property == CanvasWidthProperty)
{
thumb.privateCanvasWidth = (double)e.NewValue;
}
else if (e.Property == CanvasHeightProperty)
{
thumb.privateCanvasHeight = (double)e.NewValue;
}
else
{
throw new Exception("God knows what happened!");
}
MoveThumb_DragDelta(thumb, new DragDeltaEventArgs(0, 0));
}
/// <summary>Movable content.</summary>
public FrameworkElement MovableContent
{
get => (FrameworkElement)GetValue(MovableContentProperty);
set => SetValue(MovableContentProperty, value);
}
/// <summary><see cref="DependencyProperty"/> for property <see cref="MovableContent"/>.</summary>
public static readonly DependencyProperty MovableContentProperty =
DependencyProperty.Register(nameof(MovableContent), typeof(FrameworkElement), typeof(MoveThumb), new PropertyMetadata(null));
}
In the Project add the theme "Themes\Generic.xaml":
<ResourceDictionary
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:customcontrols="clr-namespace:EmbedContent.CustomControls"
xmlns:s="clr-namespace:Febr20y">
<Style TargetType="{x:Type s:MoveThumb}">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type s:MoveThumb}">
<ContentPresenter Content="{TemplateBinding MovableContent}"/>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</ResourceDictionary>
<Grid>
<Canvas x:Name="Canvas1">
<s:MoveThumb x:Name="DesignerItem"
Width="100"
Height="100"
Canvas.Top="100"
Canvas.Left="100">
<Ellipse Fill="Blue"/>
</s:MoveThumb>
</Canvas>
</Grid>
Video YouTube
Source Code
Related
I am trying to achieve effect where each column has its own border, but yet can not find a perfectly working solution.
This kind of look is desired but this is implemented by putting 3 borders in 3 columned Grid, which is not flexible as Grid columns and DataGrid columns are being sized sized separately
<Window x:Class="WpfApp3.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:WpfApp3" xmlns:usercontrols="clr-namespace:EECC.UserControls"
mc:Ignorable="d"
Title="MainWindow" Height="450" Width="800">
<Grid Background="LightGray">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<Border Background="White" CornerRadius="5" BorderThickness="1" BorderBrush="Black" Margin="5"/>
<Border Background="White" CornerRadius="5" BorderThickness="1" BorderBrush="Black" Margin="5" Grid.Column="1"/>
<Border Background="White" CornerRadius="5" BorderThickness="1" BorderBrush="Black" Margin="5" Grid.Column="2"/>
<DataGrid ItemsSource="{Binding Items}" ColumnWidth="*" AutoGenerateColumns="True" Padding="10" GridLinesVisibility="None" Background="Transparent" Grid.ColumnSpan="3">
<DataGrid.Resources>
<Style TargetType="{x:Type DataGridRow}">
<Setter Property="Background" Value="Transparent"/>
</Style>
<Style TargetType="{x:Type DataGridColumnHeader}">
<Setter Property="Background" Value="Transparent"/>
</Style>
</DataGrid.Resources>
</DataGrid>
</Grid>
This is not trivial if you want to use the DataGrid. The "problem" is that the DataGrid uses a Grid to host the cells. The cell borders are drawn using the feature of the Grid to show grid lines. You can hide the grid lines but still you have the Grid controlling the layout.
Creating the gaps you want should not come easy. The layout system of the Grid makes it an effort to implement a solution that scales well. You can extend the SelectiveScrollingGrid (the hosting panel of the DataGrid) and add the gaps when laying out the items. Knowing about DataGrid internals, I can say it is possible, but not worth the effort.
The alternative solution would be to use a ListView with a GridView as host. The problem here is that the GridView is designed to show rows as single item. You have no chance to modify margins column based. You can only adjust the content. I have not tried to modify the ListView internal layout elements or override the layout algorithm, but in context of alternative solutions I would also rule the ListView using a GridView out - but it is possible. It's not worth the effort.
Solution: Custom View
The simplest solution I can suggest is to adjust the data structure to show data column based. This way you can use a horizontal ListBox. Each item makes a column. Each column is realized as vertical ListBox. You basically have nested ListBox elements.
You would have to take care of the row mapping in order to allow selecting cells of a common row across the vertical ListBox columns.
This can be easily achieved by adding a RowIndex property to the CellItem models.
The idea is to have the horizontal ListBox display a collection of ColumnItem models. Each column item model exposes a collection of CellItem models. The CellItem items of different columns but the same row must share the same CellItem.RowIndex.
As a bonus, this solution is very easy to style. ListBox template has almost no parts compared to the significantly more complex DataGrid or the slightly more complex GridView.
To make showcasing the concept less confusing I chose to implement the grid layout as UserControl. For the sake of simplicity the logic to initialize and host the models and source collections is implemented inside this UserControl. I don't recommend this. Instantiation and hosting of the items should be outside the control e.g., inside a view model. You should add a DependencyProperty as ItemsSource for the control as data source for the internal horizontal ListBox.
Usage Example
<Window>
<ColumnsView />
</Window>
First create the data structure to populate the view.
The structure is based on the type ColumnItem, which hosts a collection of
CellItem items where each CellItem has a CellItem.RowIndex.
The CellItem items of different columns, that logically form a row must share the same CellItem.RowIndex.
ColumnItem.cs
public class ColumnItem
{
public ColumnItem(string header, IEnumerable<CellItem> items)
{
Header = header;
this.Items = new ObservableCollection<CellItem>(items);
}
public CellItem this[int rowIndex]
=> this.Items.FirstOrDefault(cellItem => cellItem.RowIndex.Equals(rowIndex));
public string Header { get; }
public ObservableCollection<CellItem> Items { get; }
}
CellItem.cs
public class CellItem : INotifyPropertyChanged
{
public CellItem(int rowIndex, object value)
{
this.RowIndex = rowIndex;
this.Value = value;
}
protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = "")
=> this.PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
public event PropertyChangedEventHandler PropertyChanged;
public int RowIndex { get; }
private object value;
public object Value
{
get => this.value;
set
{
this.value = value;
OnPropertyChanged();
}
}
private bool isSelected;
public bool IsSelected
{
get => this.isSelected;
set
{
this.isSelected = value;
OnPropertyChanged();
}
}
}
Build and initialize the data structure.
In this example this is all implemented in the UserControl itself with the intend to keep the example as compact as possible.
ColumnsView.xaml.cs
public partial class ColumnsView : UserControl
{
public ColumnsView()
{
InitializeComponent();
this.DataContext = this;
InitializeSourceData();
}
public InitializeSourceData()
{
this.Columns = new ObservableCollection<ColumnItem>();
for (int columnIndex = 0; columnIndex < 3; columnIndex++)
{
var cellItems = new List<CellItem>();
int asciiChar = 65;
for (int rowIndex = 0; rowIndex < 10; rowIndex++)
{
var cellValue = $"CellItem.RowIndex:{rowIndex}, Value: {(char)asciiChar++}";
var cellItem = new CellItem(rowIndex, cellValue);
cellItems.Add(cellItem);
}
var columnHeader = $"Column {columnIndex + 1}";
var columnItem = new ColumnItem(columnHeader, cellItems);
this.Columns.Add(columnItem);
}
}
private void CellsHostListBox_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
var cellsHost = sender as Selector;
var selectedCell = cellsHost.SelectedItem as CellItem;
SelectCellsOfRow(selectedCell.RowIndex);
}
private void SelectCellsOfRow(int selectedRowIndex)
{
foreach (ColumnItem columnItem in this.Columns)
{
var cellOfRow = columnItem[selectedRowIndex];
cellOfRow.IsSelected = true;
}
}
private void ColumnGripper_DragStarted(object sender, DragStartedEventArgs e)
=> this.DragStartX = Mouse.GetPosition(this).X;
private void ColumnGripper_DragDelta(object sender, DragDeltaEventArgs e)
{
if ((sender as DependencyObject).TryFindVisualParentElement(out ListBoxItem listBoxItem))
{
double currentMousePositionX = Mouse.GetPosition(this).X;
listBoxItem.Width = Math.Max(0 , listBoxItem.ActualWidth - (this.DragStartX - currentMousePositionX));
this.DragStartX = currentMousePositionX;
}
}
public static bool TryFindVisualParentElement<TParent>(DependencyObject child, out TParent resultElement)
where TParent : DependencyObject
{
resultElement = null;
DependencyObject parentElement = VisualTreeHelper.GetParent(child);
if (parentElement is TParent parent)
{
resultElement = parent;
return true;
}
return parentElement != null
? TryFindVisualParentElement(parentElement, out resultElement)
: false;
}
public ObservableCollection<ColumnItem> Columns { get; }
private double DragStartX { get; set; }
}
Create the view using a horizontal ListView that renders it's ColumnItem source collection as a list of vertical ListBox elements.
ColumnsView.xaml
<UserControl x:Class="ColumnsView">
<UserControl.Resources>
<Style x:Key="ColumnGripperStyle"
TargetType="{x:Type Thumb}">
<Setter Property="Margin"
Value="-2,8" />
<Setter Property="Width"
Value="4" />
<Setter Property="Background"
Value="Transparent" />
<Setter Property="Cursor"
Value="SizeWE" />
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type Thumb}">
<Border Background="{TemplateBinding Background}"
Padding="{TemplateBinding Padding}" />
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</UserControl.Resources>
<!-- Column host. Displays cells of a column. -->
<ListBox ItemsSource="{Binding Columns}">
<ListBox.ItemsPanel>
<ItemsPanelTemplate>
<VirtualizingStackPanel Orientation="Horizontal" />
</ItemsPanelTemplate>
</ListBox.ItemsPanel>
<ListBox.ItemTemplate>
<DataTemplate>
<Border Padding="4"
BorderThickness="1"
BorderBrush="Black"
CornerRadius="8">
<StackPanel>
<TextBlock Text="{Binding Header}" />
<!-- Cell host. Displays cells of a column. -->
<ListBox ItemsSource="{Binding Items}"
BorderThickness="0"
Height="150"
Selector.SelectionChanged="CellsHostListBox_SelectionChanged">
<ListBox.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding Value}" />
</DataTemplate>
</ListBox.ItemTemplate>
<ListBox.ItemContainerStyle>
<Style TargetType="ListBoxItem">
<!-- Link item container selection to CellItem.IsSelected -->
<Setter Property="IsSelected" Value="{Binding IsSelected}" />
</Style>
</ListBox.ItemContainerStyle>
</ListBox>
</StackPanel>
</Border>
</DataTemplate>
</ListBox.ItemTemplate>
<ListBox.ItemContainerStyle>
<Style TargetType="ListBoxItem">
<Setter Property="Margin" Value="0,0,8,0" /> <!-- Define the column gap -->
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="ListBoxItem">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<ContentPresenter />
<Thumb Grid.Column="1"
Style="{StaticResource ColumnGripperStyle}"
DragStarted="ColumnGripper_DragStarted"
DragDelta="ColumnGripper_DragDelta" />
</Grid>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</ListBox.ItemContainerStyle>
</ListBox>
</UserControl>
Notes for improvement
The ColumnsView.Columns property should be a DependencyProperty to allow to use the control as binding target.
The column gap can also be a DependencyProperty of ColumnsView.
By replacing the TextBlock that displays the column header with a Button, you can easily add sorting. Having the active column that triggers the sorting e.g. lexically, you would have to sync the other passive columns and sort them based on the CellItem.RowIndex order of the active sorted column.
Maybe choose to extend Control rather than UserControl.
You can implement the CellItem to use a generic type parameter to declare the Cellitem.Value property like CellItem<TValue>.
You can implement the ColumnItem to use a generic type parameter to declare the ColumnItem.Items property like ColumnItem<TColumn>.
Add a ColumnsView.SelectedRow property that returns a collection of all CellItem items of the current selected row
With a fair amount of elbow grease, you can get the desired look by extending the native DataGrid.
Custom header and cell templates should take care of the spacing, with the appropriate background color. The AutoGeneratingColumn behavior requires more control than could easily be achieved in XAML, so I chose to create the templates in code to be able to pass the column's PropertyName.
The observant reader will already have asked themselves: "What about the border at the end of the list?". That's right, we need to be able to distinguish the last item from all others, to be able to template its border differently.
This is done with the following contract:
public interface ICanBeLastItem
{
bool IsLastItem { get; set; }
}
Which the row object needs to implement for the bottom border to be drawn correctly.
This also requires some custom logic when sorting, to update the value of IsLastItem. The pic with the yellow background shows the result of sorting on ThirdNumber.
The native DataGrid provides a Sorting event out of the box, but no Sorted event. The template thingy combined with the need for a custom event, led me to subclass ColumnView from DataGrid instead of declaring it as a UserControl.
I added code-behind to MainWindow, for switching the background color, but that's just for illustration purposes (as I didn't feel like implementing the Command pattern) and has nothing to do with the custom control.
The ColumnView is configured through binding. As always, feel free to extend. The current implementation expects the columns to be auto generated. In either case, the code for generating the templates is provided.
<local:ColumnView ItemsSource="{Binding Items}" Background="LightSteelBlue"/>
Demo code
ColumnView
public class ColumnView : DataGrid
{
public ColumnView()
{
HeadersVisibility = DataGridHeadersVisibility.Column;
HorizontalScrollBarVisibility = ScrollBarVisibility.Hidden;
// Hidden props from base DataGrid
base.ColumnWidth = new DataGridLength(1, DataGridLengthUnitType.Star);
base.AutoGenerateColumns = true;
base.GridLinesVisibility = DataGridGridLinesVisibility.None;
// Styling
ColumnHeaderStyle = CreateColumnHeaderStyle();
CellStyle = CreateCellStyle(this);
// Event handling
AutoGeneratingColumn += OnAutoGeneratingColumn;
Sorting += OnSorting;
Sorted += OnSorted;
}
#region Hidden props
[Browsable(false), EditorBrowsable(EditorBrowsableState.Never)]
public new DataGridLength ColumnWidth
{
get => base.ColumnWidth;
set => new InvalidOperationException($"{nameof(ColumnView)} doesn't allow changing {nameof(ColumnWidth)}.");
}
[Browsable(false), EditorBrowsable(EditorBrowsableState.Never)]
public new DataGridGridLinesVisibility GridLinesVisibility
{
get => base.GridLinesVisibility;
set => new InvalidOperationException($"{nameof(ColumnView)} doesn't allow changing {nameof(GridLinesVisibility)}.");
}
[Browsable(false), EditorBrowsable(EditorBrowsableState.Never)]
public new bool AutoGenerateColumns
{
get => base.AutoGenerateColumns;
set => new InvalidOperationException($"{nameof(ColumnView)} doesn't allow changing {nameof(AutoGenerateColumns)}.");
}
#endregion Hidden props
#region Styling
private static Style CreateColumnHeaderStyle()
=> new Style(typeof(DataGridColumnHeader))
{
Setters =
{
new Setter(BackgroundProperty, Brushes.Transparent),
new Setter(HorizontalAlignmentProperty, HorizontalAlignment.Stretch),
new Setter(HorizontalContentAlignmentProperty, HorizontalAlignment.Stretch)
}
};
private static Style CreateCellStyle(ColumnView columnView)
=> new Style(typeof(DataGridCell))
{
Setters =
{
new Setter(BorderThicknessProperty, new Thickness(0.0)),
new Setter(BackgroundProperty, new Binding(nameof(Background)) { Source = columnView})
}
};
#endregion Styling
#region AutoGeneratingColumn
// https://stackoverflow.com/questions/25643765/wpf-datagrid-databind-to-datatable-cell-in-celltemplates-datatemplate
private static void OnAutoGeneratingColumn(object sender, DataGridAutoGeneratingColumnEventArgs e)
{
if (sender is ColumnView columnView)
{
if (e.PropertyName == nameof(ICanBeLastItem.IsLastItem))
{
e.Cancel = true;
}
else
{
var column = new DataGridTemplateColumn
{
CellTemplate = CreateCustomCellTemplate(e.PropertyName),
Header = e.Column.Header,
HeaderTemplate = CreateCustomHeaderTemplate(columnView, e.PropertyName),
HeaderStringFormat = e.Column.HeaderStringFormat,
SortMemberPath = e.PropertyName
};
e.Column = column;
}
}
}
private static DataTemplate CreateCustomCellTemplate(string path)
{
// Create the data template
var customTemplate = new DataTemplate();
// Set up the wrapping border
var border = new FrameworkElementFactory(typeof(Border));
border.SetValue(BorderBrushProperty, Brushes.Black);
border.SetValue(StyleProperty, new Style(typeof(Border))
{
Triggers =
{
new DataTrigger
{
Binding = new Binding(nameof(DataGridCell.IsSelected)) { RelativeSource = new RelativeSource(RelativeSourceMode.FindAncestor, typeof(DataGridCell), 1) },
Value = false,
Setters =
{
new Setter(BackgroundProperty, Brushes.White),
}
},
new DataTrigger
{
Binding = new Binding(nameof(DataGridCell.IsSelected)) { RelativeSource = new RelativeSource(RelativeSourceMode.FindAncestor, typeof(DataGridCell), 1) },
Value = true,
Setters =
{
new Setter(BackgroundProperty, SystemColors.HighlightBrush),
}
},
new DataTrigger
{
Binding = new Binding(nameof(ICanBeLastItem.IsLastItem)),
Value = false,
Setters =
{
new Setter(MarginProperty, new Thickness(5.0, -1.0, 5.0, -1.0)),
new Setter(BorderThicknessProperty, new Thickness(1.0, 0.0, 1.0, 0.0)),
}
},
new DataTrigger
{
Binding = new Binding(nameof(ICanBeLastItem.IsLastItem)),
Value = true,
Setters =
{
new Setter(MarginProperty, new Thickness(5.0, -1.0, 5.0, 0.0)),
new Setter(BorderThicknessProperty, new Thickness(1.0, 0.0, 1.0, 1.0)),
new Setter(Border.CornerRadiusProperty, new CornerRadius(0.0, 0.0, 5.0, 5.0)),
new Setter(Border.PaddingProperty, new Thickness(0.0, 0.0, 0.0, 5.0)),
}
}
}
});
// Set up the TextBlock
var textBlock = new FrameworkElementFactory(typeof(TextBlock));
textBlock.SetBinding(TextBlock.TextProperty, new Binding(path));
textBlock.SetValue(MarginProperty, new Thickness(10.0, 0.0, 5.0, 0.0));
// Set the visual tree of the data template
border.AppendChild(textBlock);
customTemplate.VisualTree = border;
return customTemplate;
}
private static DataTemplate CreateCustomHeaderTemplate(ColumnView columnView, string propName)
{
// Create the data template
var customTemplate = new DataTemplate();
// Set up the wrapping border
var border = new FrameworkElementFactory(typeof(Border));
border.SetValue(MarginProperty, new Thickness(5.0, 0.0, 5.0, 0.0));
border.SetValue(BackgroundProperty, Brushes.White);
border.SetValue(BorderBrushProperty, Brushes.Black);
border.SetValue(BorderThicknessProperty, new Thickness(1.0, 1.0, 1.0, 0.0));
border.SetValue(Border.CornerRadiusProperty, new CornerRadius(5.0, 5.0, 0.0, 0.0));
// Set up the TextBlock
var textBlock = new FrameworkElementFactory(typeof(TextBlock));
textBlock.SetValue(TextBlock.TextProperty, propName);
textBlock.SetValue(MarginProperty, new Thickness(5.0));
// Set the visual tree of the data template
border.AppendChild(textBlock);
customTemplate.VisualTree = border;
return customTemplate;
}
#endregion AutoGeneratingColumn
#region Sorting
#region Custom Sorted Event
// https://stackoverflow.com/questions/9571178/datagrid-is-there-no-sorted-event
// Create a custom routed event by first registering a RoutedEventID
// This event uses the bubbling routing strategy
public static readonly RoutedEvent SortedEvent = EventManager.RegisterRoutedEvent(
nameof(Sorted), RoutingStrategy.Bubble, typeof(RoutedEventHandler), typeof(ColumnView));
// Provide CLR accessors for the event
public event RoutedEventHandler Sorted
{
add => AddHandler(SortedEvent, value);
remove => RemoveHandler(SortedEvent, value);
}
// This method raises the Sorted event
private void RaiseSortedEvent()
{
var newEventArgs = new RoutedEventArgs(ColumnView.SortedEvent);
RaiseEvent(newEventArgs);
}
protected override void OnSorting(DataGridSortingEventArgs eventArgs)
{
base.OnSorting(eventArgs);
RaiseSortedEvent();
}
#endregion Custom Sorted Event
private static void OnSorting(object sender, DataGridSortingEventArgs e)
{
if (sender is DataGrid dataGrid && dataGrid.HasItems)
{
if (dataGrid.Items[dataGrid.Items.Count - 1] is ICanBeLastItem lastItem)
{
lastItem.IsLastItem = false;
}
}
}
private static void OnSorted(object sender, RoutedEventArgs e)
{
if (sender is DataGrid dataGrid && dataGrid.HasItems)
{
if (dataGrid.Items[dataGrid.Items.Count - 1] is ICanBeLastItem lastItem)
{
lastItem.IsLastItem = true;
}
}
}
#endregion Sorting
}
RowItem
public class RowItem : INotifyPropertyChanged, ICanBeLastItem
{
public RowItem(int firstNumber, string secondNumber, double thirdNumber)
{
FirstNumber = firstNumber;
SecondNumber = secondNumber;
ThirdNumber = thirdNumber;
}
public int FirstNumber { get; }
public string SecondNumber { get; }
public double ThirdNumber { get; }
private bool _isLastItem;
public bool IsLastItem
{
get => _isLastItem;
set
{
_isLastItem = value;
OnPropertyChanged();
}
}
#region INotifyPropertyChanged
public event PropertyChangedEventHandler PropertyChanged;
protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
#endregion INotifyPropertyChanged
}
public interface ICanBeLastItem
{
bool IsLastItem { get; set; }
}
MainWindow.xaml
<Window x:Class="WpfApp.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:WpfApp"
mc:Ignorable="d"
Title="MainWindow" Height="450" Width="800">
<Window.DataContext>
<local:MainViewModel/>
</Window.DataContext>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="30"/>
<RowDefinition/>
</Grid.RowDefinitions>
<Button Content="Switch Background" Click="ButtonBase_OnClick" />
<local:ColumnView x:Name="columnView" Grid.Row="1" Padding="10"
ItemsSource="{Binding Items}"
Background="LightSteelBlue"/>
</Grid>
</Window>
MainWindow.xaml.cs
/// <summary>
/// Interaction logic for MainWindow.xaml
/// </summary>
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
}
private void ButtonBase_OnClick(object sender, RoutedEventArgs e)
{
if (columnView.Background == Brushes.LightSteelBlue)
{
columnView.Background = Brushes.DarkRed;
}
else if (columnView.Background == Brushes.DarkRed)
{
columnView.Background = Brushes.Green;
}
else if (columnView.Background == Brushes.Green)
{
columnView.Background = Brushes.Blue;
}
else if (columnView.Background == Brushes.Blue)
{
columnView.Background = Brushes.Yellow;
}
else
{
columnView.Background = Brushes.LightSteelBlue;
}
}
}
MainViewModel
public class MainViewModel
{
public MainViewModel()
{
Items = InitializeItems(200);
}
private ObservableCollection<RowItem> InitializeItems(int numberOfItems)
{
var rowItems = new ObservableCollection<RowItem>();
var random = new Random();
for (var i = 0; i < numberOfItems; i++)
{
var firstNumber = Convert.ToInt32(1000 * random.NextDouble());
var secondNumber = Convert.ToString(Math.Round(1000 * random.NextDouble()));
var thirdNumber = Math.Round(1000 * random.NextDouble());
var rowItem = new RowItem(firstNumber, secondNumber, thirdNumber);
rowItems.Add(rowItem);
}
rowItems[numberOfItems - 1].IsLastItem = true;
return rowItems;
}
public ObservableCollection<RowItem> Items { get; }
}
I am working on an ItemsControl with Canvas as ItemPanel. Trying to implement a zoom behavior according to the answer to an other question.
The minimum code needed for reproduction should be this.
MainWindow.xaml
<Window x:Class="WpfApp7.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:WpfApp7"
mc:Ignorable="d"
Title="MainWindow" Height="450" Width="800">
<Window.Resources>
<Style TargetType="{x:Type local:DesignerSheetView}" BasedOn="{StaticResource {x:Type ItemsControl}}">
<Style.Resources>
</Style.Resources>
<Setter Property="ItemsControl.ItemsPanel">
<Setter.Value>
<ItemsPanelTemplate>
<Canvas local:ZoomBehavior.IsZoomable="True" local:ZoomBehavior.ZoomFactor="0.1" local:ZoomBehavior.ModifierKey="Ctrl">
<Canvas.Background>
<VisualBrush TileMode="Tile" Viewport="-1,-1,20,20" ViewportUnits="Absolute" Viewbox="-1,-1,20,20" ViewboxUnits="Absolute">
<VisualBrush.Visual>
<Grid Width="20" Height="20">
<Ellipse Height="2" Width="2" Stroke="Black" StrokeThickness="1" HorizontalAlignment="Left" VerticalAlignment="Top" Margin="-1,-1" />
</Grid>
</VisualBrush.Visual>
</VisualBrush>
</Canvas.Background>
</Canvas>
</ItemsPanelTemplate>
</Setter.Value>
</Setter>
<Setter Property="ItemsControl.ItemContainerStyle">
<Setter.Value>
<Style TargetType="ContentPresenter">
<Setter Property="Canvas.Top" Value="{Binding Path=YPos}" />
<Setter Property="Canvas.Left" Value="{Binding Path=XPos}" />
<Setter Property="VerticalAlignment" Value="Stretch" />
<Setter Property="HorizontalAlignment" Value="Stretch" />
</Style>
</Setter.Value>
</Setter>
<Setter Property="Focusable" Value="True" />
<Setter Property="IsEnabled" Value="True" />
</Style>
</Window.Resources>
<Grid>
<ScrollViewer HorizontalScrollBarVisibility="Auto" VerticalScrollBarVisibility="Auto">
<local:DesignerSheetView Background="Beige">
</local:DesignerSheetView>
</ScrollViewer>
</Grid>
The DesignerSheetView codebehind:
public class DesignerSheetView : ItemsControl
{
static DesignerSheetView()
{
DefaultStyleKeyProperty.OverrideMetadata(typeof(DesignerSheetView), new FrameworkPropertyMetadata(typeof(DesignerSheetView)));
}
}
And the modified ZoomBehavior
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;
using System.Windows.Media;
namespace WpfApp7
{
public static class ZoomBehavior
{
//example from https://stackoverflow.com/questions/46424149/wpf-zoom-canvas-center-on-mouse-position
#region ZoomFactor
public static double GetZoomFactor(DependencyObject obj)
{
return (double)obj.GetValue(ZoomFactorProperty);
}
public static void SetZoomFactor(DependencyObject obj, double value)
{
obj.SetValue(ZoomFactorProperty, value);
}
// Using a DependencyProperty as the backing store for ZoomFactor. This enables animation, styling, binding, etc...
public static readonly DependencyProperty ZoomFactorProperty =
DependencyProperty.RegisterAttached("ZoomFactor", typeof(double), typeof(ZoomBehavior), new PropertyMetadata(1.05));
#endregion
#region ModifierKey
public static ModifierKeys? GetModifierKey(DependencyObject obj)
{
return (ModifierKeys?)obj.GetValue(ModifierKeyProperty);
}
public static void SetModifierKey(DependencyObject obj, ModifierKeys? value)
{
obj.SetValue(ModifierKeyProperty, value);
}
// Using a DependencyProperty as the backing store for ModifierKey. This enables animation, styling, binding, etc...
public static readonly DependencyProperty ModifierKeyProperty =
DependencyProperty.RegisterAttached("ModifierKey", typeof(ModifierKeys?), typeof(ZoomBehavior), new PropertyMetadata(null));
#endregion
public static TransformMode ModeOfTransform { get; set; } = TransformMode.Layout;
private static Transform _transform;
private static Canvas _view;
#region IsZoomable
public static bool GetIsZoomable(DependencyObject obj)
{
return (bool)obj.GetValue(IsZoomableProperty);
}
public static void SetIsZoomable(DependencyObject obj, bool value)
{
obj.SetValue(IsZoomableProperty, value);
}
// Using a DependencyProperty as the backing store for IsZoomable. This enables animation, styling, binding, etc...
public static readonly DependencyProperty IsZoomableProperty =
DependencyProperty.RegisterAttached(
"IsZoomable",
typeof(bool),
typeof(ZoomBehavior),
new UIPropertyMetadata(false, OnIsZoomableChanged));
#endregion
private static void OnIsZoomableChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
_view = d as Canvas;
if (null == _view)
{
System.Diagnostics.Debug.Assert(false, "Wrong dependency object type");
return;
}
if ((e.NewValue is bool) == false)
{
System.Diagnostics.Debug.Assert(false, "Wrong value type assigned to dependency object");
return;
}
if (true == (bool)e.NewValue)
{
_view.MouseWheel += Canvas_MouseWheel;
if (ModeOfTransform == TransformMode.Render)
{
_transform = _view.RenderTransform = new MatrixTransform();
}
else
{
_transform = _view.LayoutTransform = new MatrixTransform();
}
}
else
{
_view.MouseWheel -= Canvas_MouseWheel;
}
}
public static double GetZoomScale(DependencyObject obj)
{
return (double)obj.GetValue(ZoomScaleProperty);
}
public static void SetZoomScale(DependencyObject obj, double value)
{
obj.SetValue(ZoomScaleProperty, value);
}
// Using a DependencyProperty as the backing store for ZoomScale. This enables animation, styling, binding, etc...
public static readonly DependencyProperty ZoomScaleProperty =
DependencyProperty.RegisterAttached("ZoomScale", typeof(double), typeof(ZoomBehavior), new PropertyMetadata(1.0));
private static void Canvas_MouseWheel(object sender, MouseWheelEventArgs e)
{
ModifierKeys? modifierkey = GetModifierKey(sender as DependencyObject);
if (!modifierkey.HasValue)
{
return;
}
if((Keyboard.Modifiers & (modifierkey.Value)) == ModifierKeys.None)
{
return;
}
if (!(_transform is MatrixTransform transform))
{
return;
}
var pos1 = e.GetPosition(_view);
double zoomfactor = GetZoomFactor(sender as DependencyObject);
double scale = GetZoomScale(sender as DependencyObject);
//scale = (e.Delta < 0) ? (scale * zoomfactor) : (scale / zoomfactor);
scale = (e.Delta < 0) ? (scale + zoomfactor) : (scale - zoomfactor);
scale = (scale < 0.1) ? 0.1 : scale;
SetZoomScale(sender as DependencyObject, scale);
var mat = transform.Matrix;
mat.ScaleAt(scale, scale, pos1.X, pos1.Y);
//transform.Matrix = mat;
if (TransformMode.Layout == ModeOfTransform)
{
_view.LayoutTransform = new MatrixTransform(mat);
}
else
{
_view.RenderTransform = new MatrixTransform(mat);
}
e.Handled = true;
}
public enum TransformMode
{
Layout,
Render,
}
}
}
I think the ZoomBehavior should be ok, I did not change it that much. The problem is somewhere in the xaml. I observe multiple things, for which I seek a solution here:
If I use the RenderTransform mode, the zoom happens at the mouse position, as intended. The problem is, that the background does not fill the container/window.
If I use the LayoutTransform mode, the background fills the window, but the zoom does not happen on the mouse position. The transform origin is at (0,0).
The ScrollBar-s are not activated, no matter which transform mode I choose.
Most of the questions on SO start with the asker trying to solve the zoom problem with layout transformation. Almost all answers use RenderTransform instead of LayoutTrasform (e.g. this, this and this). None of the answers provide explanation, why a RenderTransform better suits the task than a LayoutTransform. Is this because with LayoutTransform one needs to change the position of the Canvas too?
What should I change in order to make the RenderTransform work (background filling whole container and ScrollBars appearing)?
Basically you have to apply a LayoutTransform to your Canvas (or a parent Grid, say), translate it by the inverse of your zoom point (taking the current zoom factor into account) and then transform it back again to it's original position (taking the new zoom into account). So this:
// zoom level. typically changes by +/- 1 (or some other constant)
// at a time and updates this.Zoom which is the actual zoom
// multiplication factor.
private double _ZoomLevel = 1;
private double ZoomLevel
{
get { return this._ZoomLevel; }
set
{
var zoomPointX = this.ViewportWidth / 2;
var zoomPointY = this.ViewportHeight / 2;
if (this.MouseOnCanvas)
{
zoomPointX = this.LastMousePos.X * this.Zoom - this.HorizontalOffset;
zoomPointY = this.LastMousePos.Y * this.Zoom - this.VerticalOffset;
}
var imageX = (this.HorizontalOffset + zoomPointX) / this.Zoom;
var imageY = (this.VerticalOffset + zoomPointY) / this.Zoom;
this._ZoomLevel = value;
this.Zoom = 0.25 * Math.Pow(Math.Sqrt(2), value);
this.HorizontalOffset = imageX * this.Zoom - zoomPointX;
this.VerticalOffset = imageY * this.Zoom - zoomPointY;
}
}
A few things to note about this code:
It assumes that the Canvas is inside a ScrollViewer, so that the user can scroll around when zoomed in. For this you need a behavior that binds to the HorizontalOffset and VerticalOffset properties.
You also need to know the width and height of the scrollviewer client area, so you'll need to modify that behavior to also provide properties for those.
You need to track the current mouse coordinate relative to the Canvas, which means intercepting MouseEnter/MouseMove/MouseLeave and maintain the MouseOnCanvas property.
I want to create a texbox that will have a directory/file path inside it. If directory path is too long the text should appear to be trimmed with ellipsis, I would like ellipsis to appear in the middle of the path string, for example, D:\Directory1\Directory2\Directory3 can be trimmed as D:\...\Directory3.
The path itself should be bound to a ViewModel so it can be used in MVVM model.
I've encountered this issue recently, so I decided to share my solution to it here.
First of all inspired by this thread How to create a file path Trimming TextBlock with Ellipsis I decided to create my custom TextBlock that will trim its text with ellipsis, this the implementation, I wrote comments so that the code is clear:
using System.ComponentModel;
using System.Globalization;
using System.Linq;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Media;
namespace PathTrimming.Controls
{
public class PathTrimmingTextBlock : TextBlock, INotifyPropertyChanged
{
#region Dependency properties
//This property represents the Text of this textblock that can be bound to another viewmodel property,
//whenever this property is updated the Text property will be updated too.
//We cannot bind to Text property directly because once we update Text, e.g., Text = "NewValue", the binding will be broken
public string BoundedText
{
get { return GetValue(BoundedTextProperty).ToString(); }
set { SetValue(BoundedTextProperty, value); }
}
public static readonly DependencyProperty BoundedTextProperty = DependencyProperty.Register(
nameof(BoundedText), typeof(string), typeof(PathTrimmingTextBlock),
new PropertyMetadata(string.Empty, new PropertyChangedCallback(BoundedTextProperty_Changed)));
//Every time the property BoundedText is updated two things should be done:
//1) Text should be updated to be equal to new BoundedText
//2) New path should be trimmed again
private static void BoundedTextProperty_Changed(object sender, DependencyPropertyChangedEventArgs e)
{
var pathTrimmingTextBlock = (PathTrimmingTextBlock)sender;
pathTrimmingTextBlock.OnPropertyChanged(nameof(BoundedText));
pathTrimmingTextBlock.Text = pathTrimmingTextBlock.BoundedText;
pathTrimmingTextBlock.TrimPathAsync();
}
#endregion
private const string Ellipsis = "...";
public PathTrimmingTextBlock()
{
// This will make sure if the directory name is too long it will be trimmed with ellipsis on the right side
TextTrimming = TextTrimming.CharacterEllipsis;
//setting the event handler for every time this PathTrimmingTextBlock is rendered
Loaded += new RoutedEventHandler(PathTrimmingTextBox_Loaded);
}
private void PathTrimmingTextBox_Loaded(object sender, RoutedEventArgs e)
{
//asynchronously update Text, so that the window won't be frozen
TrimPathAsync();
}
private void TrimPathAsync()
{
Task.Run(() => Dispatcher.Invoke(() => TrimPath()));
}
private void TrimPath()
{
var isWidthOk = false; //represents if the width of the Text is short enough and should not be trimmed
var widthChanged = false; //represents if the width of Text was changed, if the text is short enough at the begging it should not be trimmed
var wasTrimmed = false; //represents if Text was trimmed at least one time
//in this loop we will be checking the current width of textblock using FormattedText at every iteration,
//if the width is not short enough to fit textblock it will be shrinked by one character, and so on untill it fits
do
{
//widthChanged? Text + Ellipsis : Text - at first iteration we have to check if Text is not already short enough to fit textblock,
//after widthChanged = true, we will have to measure the width of Text + Ellipsis, because ellipsis will be added to Text
var formattedText = new FormattedText(widthChanged ? Text + Ellipsis : Text,
CultureInfo.CurrentCulture,
FlowDirection.LeftToRight,
new Typeface(FontFamily, FontStyle, FontWeight, FontStretch),
FontSize,
Foreground);
//check if width fits textblock RenderSize.Width, (cannot use Width here because it's not set during rendering,
//and cannot use ActualWidth either because it is the initial width of Text not textblock itself)
isWidthOk = formattedText.Width < RenderSize.Width;
//if it doesn't fit trim it by one character
if (!isWidthOk)
{
wasTrimmed = TrimPathByOneChar();
widthChanged = true;
}
//continue loop
} while (!isWidthOk && wasTrimmed);
//Format Text with ellipsis, if width was changed (after previous loop we may have gotten a path like this "D:\Dire\Directory"
//it should be formatted to "D:\...\Directory")
if (widthChanged)
{
FormatWithEllipsis();
}
}
//Trim Text by one character before last slash, if Text doesn't have slashes it won't be trimmed with ellipsis in the middle,
//instead it will be trimmed with ellipsis at the end due to having TextTrimming = TextTrimming.CharacterEllipsis; in the constructor
private bool TrimPathByOneChar()
{
var lastSlashIndex = Text.LastIndexOf('\\');
if (lastSlashIndex > 0)
{
Text = Text.Substring(0, lastSlashIndex - 1) + Text.Substring(lastSlashIndex);
return true;
}
return false;
}
//"\Directory will become "...\Directory"
//"Dire\Directory will become "...\Directory"\
//"D:\Dire\Directory" will become "D:\...\Directory"
private void FormatWithEllipsis()
{
var lastSlashIndex = Text.LastIndexOf('\\');
if (lastSlashIndex == 0)
{
Text = Ellipsis + Text;
}
else if (lastSlashIndex > 0)
{
var secondastSlashIndex = Text.LastIndexOf('\\', lastSlashIndex - 1);
if (secondastSlashIndex < 0)
{
Text = Ellipsis + Text.Substring(lastSlashIndex);
}
else
{
Text = Text.Substring(0, secondastSlashIndex + 1) + Ellipsis + Text.Substring(lastSlashIndex);
}
}
}
//starndard implementation of INotifyPropertyChanged to be able to notify BoundedText property change
#region INotifyPropertyChanged
public event PropertyChangedEventHandler PropertyChanged;
public void OnPropertyChanged(string propertyName)
{
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
}
#endregion
}
}
Now after having the texblock we created we have to somehow "wire" it to TextBox in XAML, it can be done using ControlTemplate. This is the full XAML code, again I wrote comments so it should be easy to follow along:
<Window x:Class="PathTrimming.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:viewmodel = "clr-namespace:PathTrimming.ViewModel"
xmlns:controls="clr-namespace:PathTrimming.Controls"
mc:Ignorable="d"
Title="MainWindow" Height="350" Width="525">
<!-- Assigning datacontext to the window -->
<Window.DataContext>
<viewmodel:MainViewModel/>
</Window.DataContext>
<Window.Resources>
<ResourceDictionary>
<!--This is the most important part, if TextBox is not in focused,
it will be rendered as PathTrimmingTextBlock,
if it is focused it shouldn't be trimmed and will be rendered as default textbox.
To achieve this I'm using DataTrigger and ControlTemplate-->
<Style x:Key="TextBoxDefaultStyle" TargetType="{x:Type TextBox}">
<Style.Triggers>
<DataTrigger Binding="{Binding IsKeyboardFocused, RelativeSource={RelativeSource Self}}" Value="False">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="TextBox">
<Border
BorderThickness="1"
BorderBrush="#000">
<controls:PathTrimmingTextBlock BoundedText="{TemplateBinding Text}"/>
</Border>
</ControlTemplate>
</Setter.Value>
</Setter>
</DataTrigger>
</Style.Triggers>
</Style>
</ResourceDictionary>
</Window.Resources>
<!--Grid with two textboxes and button that updates the textboxes with new pathes from a random path pool-->
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<TextBox Grid.Row="0" Grid.Column="0" Width="100" Text="{Binding Path1}" Style="{StaticResource TextBoxDefaultStyle}"/>
<TextBox Grid.Row="1" Grid.Column="0" Width="100" Text="{Binding Path2}" Style="{StaticResource TextBoxDefaultStyle}"/>
<Button Grid.Row="2" Content="Update pathes" Command="{Binding UpdatePathesCmd}"/>
</Grid>
</Window>
Now finally what is left, is to write our ViewModel that is responsible to feed data to the View. Here I utilized MVVM Light library to simplify the code, but this is not important, using any other approach should work fine.
This is the code with comments, should be pretty self explanatory anyway:
using GalaSoft.MvvmLight;
using GalaSoft.MvvmLight.Command;
using System;
using System.Windows.Input;
namespace PathTrimming.ViewModel
{
public class MainViewModel : ViewModelBase
{
public string Path1
{
get { return _path1; }
set
{
_path1 = value;
RaisePropertyChanged();
}
}
public string Path2
{
get { return _path2; }
set
{
_path2 = value;
RaisePropertyChanged();
}
}
private string _path1;
private string _path2;
public MainViewModel()
{
UpdatePathes();
}
//The command that will update Path1 and Path2 with some random path values
public ICommand UpdatePathesCmd
{
get { return new RelayCommand(UpdatePathes); }
}
private void UpdatePathes()
{
Path1 = PathProvider.GetPath();
Path2 = PathProvider.GetPath();
}
}
//A simple static class to provide a pool of different pathes
public static class PathProvider
{
private static Random randIndexGenerator = new Random();
private static readonly string[] pathes =
{
"D:\\Directory1\\Directory2\\Directory3",
"D:\\Directory1\\Directory2",
"Directory1\\Directory2\\Directory3",
"D:\\Directory1\\Directory12345678901234567890",
"Directory1234567890123456789012345678901234567890",
"D:\\Directory1"
};
public static string GetPath()
{
var randIndex = randIndexGenerator.Next(pathes.Length);
return pathes[randIndex];
}
}
}
I've spent a lot of time with this problem. I have custom user control in wpf, It's toolbar with 10 buttons. This toolbar is added in Panel(winform panel) panel.Controls.Add(usercontrol). What I want is, respond on MouseLeave event from Panel.
I've tried panel.MouseLeave += MouseLeaveEvent, but event hasn't raised. No one event isn't raised(MouseMove, etc.). Is there any solution, how to make it, that MouseLeave event will be raised?
Thanks a lot.
EDIT
Here is the code.
CustomToolbar = new AxisMediaControlToolbarViewModel(this);
var toolbarView = new AxisMediaControlToolbarView { ViewModel = CustomToolbar };
var elementHost = new ElementHost { Dock = DockStyle.Fill, Child = toolbarView };
toolbarPanel.Controls.Add(elementHost);
In this thread are quite a few causes mentioned:
Set the Background of the UserControl
Make the Canvas focusable
Be aware of RenderTransforms
EDIT
I made a test and it works fine as long as the WPF element is not covering the Panel. The mouse has to be DIRECTLY over the panel to generate the event. The DockStyle.Fill might be the cause of your problem.
One of the causes may be that your panel's background is not set.
If you do not set the background, then the control is though to be as "its area is not to be taken care of" and effectively it is transparent, because it is not drawn. However, it has more consequences: it's area is not only not drawn, but also it is not tracked i.e. for mouse events.
For starters, try setting Background="Transparent". It may make little sense as it is already transparent, but this way the Panel control will have a background and if there are no other problems, it may immediatelly start seeing the mouse events.
edit: quickexample:
UserControl.xaml
<UserControl x:Class="UserControlMouse.MyButtonPanel"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
mc:Ignorable="d"
d:DesignHeight="300" d:DesignWidth="300"
Background="LightBlue"
Tag="You will never see this message">
<StackPanel Margin="20" Background="LightGray"
MouseEnter="handler_MouseEnter" MouseMove="handler_MouseMove" MouseLeave="handler_MouseLeave"
Tag="StackPanel">
<StackPanel.Resources>
<Style TargetType="Border">
<Setter Property="Margin" Value="2" />
<Setter Property="Padding" Value="5" />
<Setter Property="Background" Value="Gray" />
</Style>
<Style TargetType="TextBlock">
<Setter Property="TextAlignment" Value="Center" />
<Setter Property="Background" Value="WhiteSmoke" />
</Style>
</StackPanel.Resources>
<Button Content="#1" MaxWidth="80" Tag="Button#1" />
<Button Content="#2" MaxWidth="80" Tag="Button#2"
MouseEnter="handler_MouseEnter" MouseMove="handler_MouseMove" MouseLeave="handler_MouseLeave"
/>
<Button Content="#3" MaxWidth="80" Tag="Button#3" />
<Button Content="#4" MaxWidth="80" Tag="Button#4" />
<Button Content="#5" MaxWidth="80" Tag="Button#5"
MouseEnter="handler_MouseEnter" MouseMove="handler_MouseMove" MouseLeave="handler_MouseLeave"
/>
<Button Content="#6" MaxWidth="80" Tag="Button#6" />
<Button Content="#7" MaxWidth="80" Tag="Button#7" />
<Button Content="#8" MaxWidth="80" Tag="Button#8" />
<TextBlock Margin="0,20,0,0" Tag="Comment TextBlock #1">
<Run Text="Sender was: " />
<Run Text="{Binding ReportingControl}" />
</TextBlock>
<TextBlock Tag="Comment TextBlock #2">
<Run Text="Source was: " />
<Run Text="{Binding ContainerControl}" />
</TextBlock>
<TextBlock Tag="Comment TextBlock #3">
<Run Text="Now you are in: " />
<Run Text="{Binding ControlUnderMouse}" />
</TextBlock>
<UniformGrid Columns="3" Tag="indicator UniformGrid">
<Border Visibility="{Binding Visib_OnEnter}" Tag="indicator border #1">
<TextBlock Text="Enter" />
</Border>
<Border Visibility="{Binding Visib_OnMove}" Tag="indicator border #2"
MouseEnter="handler_MouseEnter" MouseMove="handler_MouseMove" MouseLeave="handler_MouseLeave">
<TextBlock Text="Move" />
</Border>
<Border Visibility="{Binding Visib_OnLeave}" Tag="indicator border #3">
<TextBlock Text="Leave" />
</Border>
</UniformGrid>
</StackPanel>
</UserControl>
usercontrol.xaml.cs
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;
using System.Windows.Media;
namespace UserControlMouse
{
public partial class MyButtonPanel : UserControl
{
private SimpleDataObject theData = new SimpleDataObject();
public MyButtonPanel()
{
InitializeComponent();
this.DataContext = theData;
}
private const int COUNTER_LAG = 20;
private int counter_enter = 0;
private void handler_MouseEnter(object sender, MouseEventArgs e)
{
counter_enter = counter_mouse;
theData.Visib_OnEnter = System.Windows.Visibility.Visible;
theData.Visib_OnMove = System.Windows.Visibility.Hidden;
theData.Visib_OnLeave = System.Windows.Visibility.Hidden;
theData.ReportingControl = makeDescriptionOfControl(sender);
theData.ContainerControl = makeDescriptionOfControl(e.Source);
theData.ControlUnderMouse = makeDescriptionOfControl(null);
}
private int counter_mouse = 0;
private void handler_MouseMove(object sender, MouseEventArgs e)
{
++counter_mouse;
if (counter_mouse > counter_enter + COUNTER_LAG) theData.Visib_OnEnter = System.Windows.Visibility.Hidden;
theData.Visib_OnMove = System.Windows.Visibility.Visible;
if (counter_mouse > counter_leave + COUNTER_LAG) theData.Visib_OnLeave = System.Windows.Visibility.Hidden;
theData.ReportingControl = makeDescriptionOfControl(sender);
theData.ContainerControl = makeDescriptionOfControl(e.Source);
theData.ControlUnderMouse = makeDescriptionOfControl(null);
}
private int counter_leave = 0;
private void handler_MouseLeave(object sender, MouseEventArgs e)
{
counter_leave = counter_mouse;
theData.Visib_OnEnter = System.Windows.Visibility.Hidden;
theData.Visib_OnMove = System.Windows.Visibility.Hidden;
theData.Visib_OnLeave = System.Windows.Visibility.Visible;
theData.ReportingControl = makeDescriptionOfControl(sender);
theData.ContainerControl = makeDescriptionOfControl(e.Source);
theData.ControlUnderMouse = makeDescriptionOfControl(null);
}
private string makeDescriptionOfControl(object uiobj)
{
if (uiobj == null)
return "???";
string text = uiobj.GetType().Name;
var fe = uiobj as FrameworkElement;
if (fe != null && fe.Tag != null)
text += " (" + (string)((FrameworkElement)uiobj).Tag + ")";
return text;
}
}
public class SimpleDataObject : DependencyObject
{
public string ControlUnderMouse { get { return (string)GetValue(ControlUnderMouseProperty); } set { SetValue(ControlUnderMouseProperty, value); } }
public static readonly DependencyProperty ControlUnderMouseProperty = DependencyProperty.Register("ControlUnderMouse", typeof(string), typeof(SimpleDataObject), new UIPropertyMetadata("???"));
public string ReportingControl { get { return (string)GetValue(ReportingControlProperty); } set { SetValue(ReportingControlProperty, value); } }
public static readonly DependencyProperty ReportingControlProperty = DependencyProperty.Register("ReportingControl", typeof(string), typeof(SimpleDataObject), new UIPropertyMetadata("???"));
public string ContainerControl { get { return (string)GetValue(ContainerControlProperty); } set { SetValue(ContainerControlProperty, value); } }
public static readonly DependencyProperty ContainerControlProperty = DependencyProperty.Register("ContainerControl", typeof(string), typeof(SimpleDataObject), new UIPropertyMetadata("???"));
public Visibility Visib_OnEnter { get { return (Visibility)GetValue(Visib_OnEnterProperty); } set { SetValue(Visib_OnEnterProperty, value); } }
public static readonly DependencyProperty Visib_OnEnterProperty = DependencyProperty.Register("Visib_OnEnter", typeof(Visibility), typeof(SimpleDataObject), new UIPropertyMetadata(Visibility.Hidden));
public Visibility Visib_OnMove { get { return (Visibility)GetValue(Visib_OnMoveProperty); } set { SetValue(Visib_OnMoveProperty, value); } }
public static readonly DependencyProperty Visib_OnMoveProperty = DependencyProperty.Register("Visib_OnMove", typeof(Visibility), typeof(SimpleDataObject), new UIPropertyMetadata(Visibility.Hidden));
public Visibility Visib_OnLeave { get { return (Visibility)GetValue(Visib_OnLeaveProperty); } set { SetValue(Visib_OnLeaveProperty, value); } }
public static readonly DependencyProperty Visib_OnLeaveProperty = DependencyProperty.Register("Visib_OnLeave", typeof(Visibility), typeof(SimpleDataObject), new UIPropertyMetadata(Visibility.Hidden));
}
}
sample window
<Window x:Class="UserControlMouse.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:my="clr-namespace:UserControlMouse"
Title="MainWindow" Height="350" Width="525">
<Grid Background="Red">
<my:MyButtonPanel Margin="10" />
</Grid>
</Window>
Please run it and play with it.
Observe how the events are fired and which controls see them. Note how SENDER and SOURCE are changing
note that SENDER is always the same, the root of the mouse-capture. It is the Source that changes
note that only button #2 and button #5 is reporting ENTER/LEAVE (they have direct handlers, others have not!)
note that other controls are 'detectable' with the mouse-move even if they don't have mouse handlers - you can get events with Source(s) that does not have any mouse capture set
check the OriginalSource (the last text line) - this is the control directly under mouse - and often it is not the control you would want to notice, as it is some "leaf control" from some templates or styles..
I have a border element with rounded corners containing a 3x3 grid. The corners of the grid are sticking out of the border. How can I fix that? I tried using ClipToBounds but didn't get anywhere.
Thanks for your help
Here are the highlights of this thread mentioned by Jobi
None of the decorators (i.e. Border) or layout panels (i.e. Stackpanel) come with this behavior out-of-the-box.
ClipToBounds is for layout. ClipToBounds does not prevent an element from drawing outside its bounds; it just prevents children's layouts from 'spilling'. Additionally ClipToBounds=True is not needed for most elements because their implementations dont allow their content's layout to spill anyway. The most notable exception is Canvas.
Finally Border considers the rounded corners to be drawings inside the bounds of its layout.
Here is an implementation of a class that inherits from Border and implements the proper functionality:
/// <Remarks>
/// As a side effect ClippingBorder will surpress any databinding or animation of
/// its childs UIElement.Clip property until the child is removed from ClippingBorder
/// </Remarks>
public class ClippingBorder : Border {
protected override void OnRender(DrawingContext dc) {
OnApplyChildClip();
base.OnRender(dc);
}
public override UIElement Child
{
get
{
return base.Child;
}
set
{
if (this.Child != value)
{
if(this.Child != null)
{
// Restore original clipping
this.Child.SetValue(UIElement.ClipProperty, _oldClip);
}
if(value != null)
{
_oldClip = value.ReadLocalValue(UIElement.ClipProperty);
}
else
{
// If we dont set it to null we could leak a Geometry object
_oldClip = null;
}
base.Child = value;
}
}
}
protected virtual void OnApplyChildClip()
{
UIElement child = this.Child;
if(child != null)
{
_clipRect.RadiusX = _clipRect.RadiusY = Math.Max(0.0, this.CornerRadius.TopLeft - (this.BorderThickness.Left * 0.5));
_clipRect.Rect = new Rect(Child.RenderSize);
child.Clip = _clipRect;
}
}
private RectangleGeometry _clipRect = new RectangleGeometry();
private object _oldClip;
}
Pure XAML:
<Border CornerRadius="30" Background="Green">
<Border.OpacityMask>
<VisualBrush>
<VisualBrush.Visual>
<Border
Background="Black"
SnapsToDevicePixels="True"
CornerRadius="{Binding CornerRadius, RelativeSource={RelativeSource AncestorType=Border}}"
Width="{Binding ActualWidth, RelativeSource={RelativeSource AncestorType=Border}}"
Height="{Binding ActualHeight, RelativeSource={RelativeSource AncestorType=Border}}"
/>
</VisualBrush.Visual>
</VisualBrush>
</Border.OpacityMask>
<TextBlock Text="asdas das d asd a sd a sda" />
</Border>
Update:
Found a better way to achieve the same result. You can also replace Border with any other element now.
<Grid>
<Grid.OpacityMask>
<VisualBrush Visual="{Binding ElementName=Border1}" />
</Grid.OpacityMask>
<Border x:Name="Border1" CornerRadius="30" Background="Green" />
<TextBlock Text="asdas das d asd a sd a sda" />
</Grid>
As Micah mentioned ClipToBounds will not work with Border.ConerRadius.
There is UIElement.Clip property, which Border inheritances.
If you know the exact size of the border, then here is the solution:
<Border Background="Blue" CornerRadius="3" Height="100" Width="100">
<Border.Clip>
<RectangleGeometry RadiusX="3" RadiusY="3" Rect="0,0,100,100"/>
</Border.Clip>
<Grid Background="Green"/>
</Border>
If the size is not known or dynamic then Converter for Border.Clip can be used. See the solution here.
So I just came across this solution, then followed into msdn forum link that Jobi provided and spent 20 minutes writing my own ClippingBorder control.
Then I realized that CornerRadius property type is not a double, but System.Windows.CornerRaduis which accepts 4 doubles, one for each corner.
So I'm going to list another alternative solution now, which will most likely satisfy the requirements of most people who will stumble upon this post in the future...
Let's say you have XAML which looks like this:
<Border CornerRadius="10">
<Grid>
... your UI ...
</Grid>
</Border>
And the problem is that the background for the Grid element bleeds through and shows past the rounded corners. Make sure your <Grid> has transparent background instead of assigning the same brush to "Background" property of the <Border> element. No more bleeding past the corners and no need for a whole bunch of CustomControl code.
It's true that in theory, client area still have the potential of drawing past the edge of the corner, but you control that content so you as developer should be able to either have enough padding, or make sure the shape of the control next to the edge is appropriate (in my case, my buttons are round, so fit very nicely in the corner without any problems).
Using #Andrew Mikhailov's solution, you can define a simple class, which makes defining a VisualBrush for each affected element manually unnecessary:
public class ClippedBorder : Border
{
public ClippedBorder() : base()
{
var e = new Border()
{
Background = Brushes.Black,
SnapsToDevicePixels = true,
};
e.SetBinding(Border.CornerRadiusProperty, new Binding()
{
Mode = BindingMode.OneWay,
Path = new PropertyPath("CornerRadius"),
Source = this
});
e.SetBinding(Border.HeightProperty, new Binding()
{
Mode = BindingMode.OneWay,
Path = new PropertyPath("ActualHeight"),
Source = this
});
e.SetBinding(Border.WidthProperty, new Binding()
{
Mode = BindingMode.OneWay,
Path = new PropertyPath("ActualWidth"),
Source = this
});
OpacityMask = new VisualBrush(e);
}
}
To test this, just compile the following two samples:
<!-- You should see a blue rectangle with rounded corners/no red! -->
<Controls:ClippedBorder
Background="Red"
CornerRadius="10"
Height="425"
HorizontalAlignment="Center"
VerticalAlignment="Center"
Width="425">
<Border Background="Blue">
</Border>
</Controls:ClippedBorder>
<!-- You should see a blue rectangle with NO rounded corners/still no red! -->
<Border
Background="Red"
CornerRadius="10"
Height="425"
HorizontalAlignment="Center"
VerticalAlignment="Center"
Width="425">
<Border Background="Blue">
</Border>
</Border>
Make the grid smaller or the border larger. So that the border element completely contains the grid.
Alternatively see if you can make the grid's background transparent, so that the "sticking out" isn't noticeable.
Update: Oops, didn't notice this was a WPF question. I'm not familiar with that. This was general HTML/CSS advice. Maybe it helps...
I don't like to use a custom control. Created a behavior instead.
using System.Linq;
using System.Windows;
using System.Windows.Interactivity;
/// <summary>
/// Base class for behaviors that could be used in style.
/// </summary>
/// <typeparam name="TComponent">Component type.</typeparam>
/// <typeparam name="TBehavior">Behavior type.</typeparam>
public class AttachableForStyleBehavior<TComponent, TBehavior> : Behavior<TComponent>
where TComponent : System.Windows.DependencyObject
where TBehavior : AttachableForStyleBehavior<TComponent, TBehavior>, new()
{
#pragma warning disable SA1401 // Field must be private.
/// <summary>
/// IsEnabledForStyle attached property.
/// </summary>
public static DependencyProperty IsEnabledForStyleProperty =
DependencyProperty.RegisterAttached("IsEnabledForStyle", typeof(bool),
typeof(AttachableForStyleBehavior<TComponent, TBehavior>), new FrameworkPropertyMetadata(false, OnIsEnabledForStyleChanged));
#pragma warning restore SA1401
/// <summary>
/// Sets IsEnabledForStyle value for element.
/// </summary>
public static void SetIsEnabledForStyle(UIElement element, bool value)
{
element.SetValue(IsEnabledForStyleProperty, value);
}
/// <summary>
/// Gets IsEnabledForStyle value for element.
/// </summary>
public static bool GetIsEnabledForStyle(UIElement element)
{
return (bool)element.GetValue(IsEnabledForStyleProperty);
}
private static void OnIsEnabledForStyleChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
UIElement uie = d as UIElement;
if (uie != null)
{
var behColl = Interaction.GetBehaviors(uie);
var existingBehavior = behColl.FirstOrDefault(b => b.GetType() ==
typeof(TBehavior)) as TBehavior;
if ((bool)e.NewValue == false && existingBehavior != null)
{
behColl.Remove(existingBehavior);
}
else if ((bool)e.NewValue == true && existingBehavior == null)
{
behColl.Add(new TBehavior());
}
}
}
}
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Media;
/// <summary>
/// Behavior that creates opacity mask brush.
/// </summary>
internal class OpacityMaskBehavior : AttachableForStyleBehavior<Border, OpacityMaskBehavior>
{
protected override void OnAttached()
{
base.OnAttached();
var border = new Border()
{
Background = Brushes.Black,
SnapsToDevicePixels = true,
};
border.SetBinding(Border.CornerRadiusProperty, new Binding()
{
Mode = BindingMode.OneWay,
Path = new PropertyPath("CornerRadius"),
Source = AssociatedObject
});
border.SetBinding(FrameworkElement.HeightProperty, new Binding()
{
Mode = BindingMode.OneWay,
Path = new PropertyPath("ActualHeight"),
Source = AssociatedObject
});
border.SetBinding(FrameworkElement.WidthProperty, new Binding()
{
Mode = BindingMode.OneWay,
Path = new PropertyPath("ActualWidth"),
Source = AssociatedObject
});
AssociatedObject.OpacityMask = new VisualBrush(border);
}
protected override void OnDetaching()
{
base.OnDetaching();
AssociatedObject.OpacityMask = null;
}
}
<Style x:Key="BorderWithRoundCornersStyle" TargetType="{x:Type Border}">
<Setter Property="CornerRadius" Value="50" />
<Setter Property="behaviors:OpacityMaskBehavior.IsEnabledForStyle" Value="True" />
</Style>