I've the following code:
<ItemsControl ItemsSource="{Binding SubItems}">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<WrapPanel Orientation="Horizontal"></WrapPanel>
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate>
<Grid Margin="10">
<Grid.RowDefinitions>
<RowDefinition Height="*"></RowDefinition>
<RowDefinition Height="Auto"></RowDefinition>
</Grid.RowDefinitions>
<Image Source="{Binding Image}" ></Image>
<TextBlock Text="{Binding Name}" Grid.Row="1" HorizontalAlignment="Center"/>
</Grid>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
Currently if I run this code, every item(grid) tries to take the full space available and I've only 1-2 items visible over the 20+ I've in my SubItems collections.
If I set a MaxWidth to my Grid, I see all of them, but when I maximize the window, I've a lot of free space.
If I don't set any width, I've this:
If I set a width and increase the size, I've this:
The goal is to have something like the second case, but without having to set a width, and having it scale if I increase the window size.
Edit2
I tried with UniformGrid, but two issues. With two elements, it seems it absolutely wants to have 4 column and 3 rows. Even if would be better with 3 column 4 rows:
Also, when the window is reduced, the images are cut:
If nothing else will help, consider writing your own panel. I don't have time now for a complete solution, but consider this.
First, tiling rectangle with squares the way you want is not quite trivial. This is known as packing problem and solutions are often hard to find (depends on the concrete problem). I have taken algorithm to find approximate tile size from this question: Max square size for unknown number inside rectangle.
When you have square size for given width and height of your panel, the rest is easier:
public class AdjustableWrapPanel : Panel {
protected override Size MeasureOverride(Size availableSize) {
// get tile size
var tileSize = GetTileSize((int) availableSize.Width, (int) availableSize.Height, this.InternalChildren.Count);
foreach (UIElement child in this.InternalChildren) {
// measure each child with a square it should occupy
child.Measure(new Size(tileSize, tileSize));
}
return availableSize;
}
protected override Size ArrangeOverride(Size finalSize) {
var tileSize = GetTileSize((int)finalSize.Width, (int)finalSize.Height, this.InternalChildren.Count);
int x = 0, y = 0;
foreach (UIElement child in this.InternalChildren)
{
// arrange in square
child.Arrange(new Rect(new Point(x,y), new Size(tileSize, tileSize)));
x += tileSize;
if (x + tileSize >= finalSize.Width) {
// if need to move on next row - do that
x = 0;
y += tileSize;
}
}
return finalSize;
}
int GetTileSize(int width, int height, int tileCount)
{
if (width*height < tileCount) {
return 0;
}
// come up with an initial guess
double aspect = (double)height / width;
double xf = Math.Sqrt(tileCount / aspect);
double yf = xf * aspect;
int x = (int)Math.Max(1.0, Math.Floor(xf));
int y = (int)Math.Max(1.0, Math.Floor(yf));
int x_size = (int)Math.Floor((double)width / x);
int y_size = (int)Math.Floor((double)height / y);
int tileSize = Math.Min(x_size, y_size);
// test our guess:
x = (int)Math.Floor((double)width / tileSize);
y = (int)Math.Floor((double)height / tileSize);
if (x * y < tileCount) // we guessed too high
{
if (((x + 1) * y < tileCount) && (x * (y + 1) < tileCount))
{
// case 2: the upper bound is correct
// compute the tileSize that will
// result in (x+1)*(y+1) tiles
x_size = (int)Math.Floor((double)width / (x + 1));
y_size = (int)Math.Floor((double)height / (y + 1));
tileSize = Math.Min(x_size, y_size);
}
else
{
// case 3: solve an equation to determine
// the final x and y dimensions
// and then compute the tileSize
// that results in those dimensions
int test_x = (int)Math.Ceiling((double)tileCount / y);
int test_y = (int)Math.Ceiling((double)tileCount / x);
x_size = (int)Math.Min(Math.Floor((double)width / test_x), Math.Floor((double)height / y));
y_size = (int)Math.Min(Math.Floor((double)width / x), Math.Floor((double)height / test_y));
tileSize = Math.Max(x_size, y_size);
}
}
return tileSize;
}
}
You can try this.
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<UniformGrid />
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
This is rather ambiguous but have you tried using Blend for Visual Studio? It is very good at assisting in, not only debugging, but also designing the UI for a WPF application. In the long run, it may be best as you don't have to maintain any custom controls/bindings.
Create Your DataTemplate like this:
<DataTemplate>
<Grid Height="{Binding RelativeSource={RelativeSource Self},Path=ActualWidth,Mode=OneWay}">
<Grid.Width>
<MultiBinding Converter="{StaticResource Converter}">
<Binding RelativeSource="{RelativeSource AncestorType=ItemsControl}" Path="ActualWidth" Mode="OneWay" />
<Binding RelativeSource="{RelativeSource AncestorType=ItemsControl}" Path="ActualHeight" Mode="OneWay" />
<Binding RelativeSource="{RelativeSource AncestorType=ItemsControl}" Path="DataContext.SubItems.Count" />
<Binding RelativeSource="{RelativeSource AncestorType=ItemsControl}" Path="ActualWidth" />
</MultiBinding>
</Grid.Width>
<Grid.RowDefinitions>
Converter:
public class Converter : IMultiValueConverter
{
public object Convert(object[] values, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
double TotalWidth = System.Convert.ToDouble(values[0]), TotalHeight = System.Convert.ToDouble(values[1]);
int TotalItems = System.Convert.ToInt32(values[2]);
var TotalArea = TotalWidth * TotalHeight;
var AreasOfAnItem = TotalArea / TotalItems;
var SideOfitem = Math.Sqrt(AreasOfAnItem);
var ItemsInCurrentWidth = Math.Floor(TotalWidth / SideOfitem);
var ItemsInCurrentHeight = Math.Floor(TotalHeight / SideOfitem);
while (ItemsInCurrentWidth * ItemsInCurrentHeight < TotalItems)
{
SideOfitem -= 1;//Keep decreasing the side of item unless every item is fit in current shape of window
ItemsInCurrentWidth = Math.Floor(TotalWidth / SideOfitem);
ItemsInCurrentHeight = Math.Floor(TotalHeight / SideOfitem);
}
return SideOfitem;
}
public object[] ConvertBack(object value, Type[] targetTypes, object parameter, System.Globalization.CultureInfo culture)
{
return null;
}
}
Explanation of Logic: The approach is very simple. Calculate the area of the ItemsControl and divide the area equal in all items. That is also the best scenario possible visually. So if we have 20 items in your list and area is 2000 unit(square shape) then each item gets 100 unit of area to render.
Now the tricky part is area of ItemsControl can't be in square shape always but items will be in square shape always. So if we want to display all items without any area getting trimmed by overflow we need to reduce area of every items till its fit in current shape. The while loop in Converter does that by calculating if all items are fully visible or not. If all items are not fully visible it knows the size needs to be reduced.
NOTE: Every item will be of same Height & Width(square area). That's why Height of Grid is bound to Width of Grid, we
need not to calculate that.
OutPut:
Full Screen:
You need to change your RowDefinition to look something more like this;
<RowDefinition Height="*"/>
One of your rows is set to Auto, this will attempt to fill only the space it needs. The other is set to *, this will automatically stretch to fill all the space it can.
Notice there is also no need to type </RowDefinition> , you can simply end in />. This link might be of particular use to you;
Difference Between * and auto
Related
I have a Border with a Content of TextBlock that I want to be perfectly centered both horizontally and vertically. No matter what I try it never looks centered. What am I missing?
Using the code below the top of the text is 19px below the border, the bottom of the text is 5px above the border. It's also off center left or right depending on the Text value which I assume is related to the font.
The solution should work for varying text (1-31) with any font.
Code
<Grid Width="50" Height="50">
<Border BorderThickness="1" BorderBrush="Black">
<TextBlock Text="13" VerticalAlignment="Center" HorizontalAlignment="Center" FontSize="50"/>
</Border>
</Grid>
Result
Well then, challenge accepted ;-) This solution is based on the following idea:
Fit the TextBlock inside the border and make sure the entire text is rendered, even if not visible.
Render the text into a bitmap.
Detect the glyphs (i.e. characters) inside the bitmap to get the pixel-exact position.
Update the UI layout so the text is centered inside the border.
If possible, allow simple, generic usage.
1. TextBlock inside border / fully rendered
This is simple once you realize that the entire content of a ScrollViewer is rendered, so here is my UserControl XAML:
<UserControl x:Class="WpfApplication4.CenteredText"
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">
<Grid>
<ScrollViewer x:Name="scroll"
IsHitTestVisible="False"
VerticalScrollBarVisibility="Hidden"
HorizontalScrollBarVisibility="Hidden" />
</Grid>
</UserControl>
With the code behind as:
public partial class CenteredText : UserControl
{
public CenteredText()
{
InitializeComponent();
}
public static readonly DependencyProperty ElementProperty = DependencyProperty
.Register("Element", typeof(FrameworkElement), typeof(CenteredText),
new PropertyMetadata(OnElementChanged));
private static void OnElementChanged(DependencyObject d,
DependencyPropertyChangedEventArgs e)
{
var elem = e.NewValue as FrameworkElement;
var ct = d as CenteredText;
if(elem != null)
{
elem.Loaded += ct.Content_Loaded;
ct.scroll.Content = elem;
}
}
public FrameworkElement Element
{
get { return (FrameworkElement)GetValue(ElementProperty); }
set { SetValue(ElementProperty, value); }
}
void Content_Loaded(object sender, RoutedEventArgs e) /*...*/
}
This control is basically a ContentControlwhich allows to handle the Loaded event of the content generically. There may be a simpler way to do this, I'm not sure.
2. Render to Bitmap
This one is simple. In the Content_Loaded() method:
void Content_Loaded(object sender, RoutedEventArgs e)
{
FrameworkElement elem = sender as FrameworkElement;
int w = (int)elem.ActualWidth;
int h = (int)elem.ActualHeight;
var rtb = new RenderTargetBitmap(w, h, 96, 96, PixelFormats.Pbgra32);
rtb.Render(elem);
/* glyph detection ... */
}
3. Detect the glyphs
This is surprisingly easy since a TextBlock is rendered with fully transparent background by default and we are only interested in bounding rectangle. This is done in a separate method:
bool TryFindGlyphs(BitmapSource src, out Rect rc)
{
int left = int.MaxValue;
int toRight = -1;
int top = int.MaxValue;
int toBottom = -1;
int w = src.PixelWidth;
int h = src.PixelHeight;
uint[] buf = new uint[w * h];
src.CopyPixels(buf, w * sizeof(uint), 0);
for (int y = 0; y < h; y++)
{
for (int x = 0; x < w; x++)
{
// background is assumed to be fully transparent, i.e. 0x00000000 in Pbgra
if (buf[x + y * w] != 0)
{
if (x < left) left = x;
if (x > toRight) toRight = x;
if (y < top) top = y;
if (y > toBottom) toBottom = y;
}
}
}
rc = new Rect(left, top, toRight - left, toBottom - top);
return (toRight > left) && (toBottom > top);
}
The above method tries to find the leftmost, rightmost, topmost and bottommost pixel which is not transparent and returns the results as a Rect in the output parameter.
4. Update Layout
This is done later in the Content_Loaded method:
void Content_Loaded(object sender, RoutedEventArgs e)
{
/* render to bitmap ... */
Rect rc;
if (TryFindGlyphs(rtb, out rc))
{
if (rc.Height > this.scroll.ActualHeight || rc.Width > this.scroll.ActualWidth)
{
return; // todo: error handling
}
double desiredV = rc.Top - 0.5 * (this.scroll.ActualHeight - rc.Height);
double desiredH = rc.Left - 0.5 * (this.scroll.ActualWidth - rc.Width);
if (desiredV > 0)
{
this.scroll.ScrollToVerticalOffset(desiredV);
}
else
{
elem.Margin = new Thickness(elem.Margin.Left, elem.Margin.Top - desiredV,
elem.Margin.Right, elem.Margin.Bottom);
}
if (desiredH > 0)
{
this.scroll.ScrollToHorizontalOffset(desiredH);
}
else
{
elem.Margin = new Thickness(elem.Margin.Left - desiredH, elem.Margin.Top,
elem.Margin.Right, elem.Margin.Bottom);
}
}
}
This UI is updated using the following strategy:
Compute the desired offset between the border and the glyph rectangle in both directions
If the desired offset is positive, it means that the text needs to move up (or left in the horizontal case) so we can scroll down (right) by the desired offset.
If the desired offset is negative, it means that the text needs to move down (or right in the horizontal case). This cannot be done by scrolling since the TextBlock is top-left-aligned (by default) and the ScrollViewer is still at the initial (top/left) position. There is a simple solution though: Add the desired offset to the Margin of the TextBlock.
5. Simple Usage
The CenteredText control is used as follows:
<Border BorderBrush="Black" BorderThickness="1" Width="150" Height="150">
<local:CenteredText>
<local:CenteredText.Element>
<TextBlock Text="31" FontSize="150" />
</local:CenteredText.Element>
</local:CenteredText>
</Border>
Results
For border size 150x150 and FontSize 150:
For border size 150x150 and FontSize 50:
For border size 50x50 and FontSize 50:
Note: There is a 1-pixel error where the space to the left of the text is 1 pixel thicker or thinner than the space to the right. Same with the top / bottom spacing. This happens if the border has an even width and the rendered text an odd width (no sub-pixel perfectness is provided, sorry)
Conclusion
The presented solution should work up to a 1-pixel error with any Font, FontSize and Text and is simple to use.
And if you haven't noticed yet, very limited assumptions were made about the FrameworkElement which is used with the Elem property of the CenteredText control. So this should also work with any element which has transparent background and needs (near-)perfect centering.
What you are talking about is related to the specific font (and characters within that font) that you are using. Different fonts will have different baselines, heights and other attributes. In order to combat that, just use Padding on the Border or Margin on the TextBlock to make it fit where you want it:
<Grid Width="50" Height="50">
<Border BorderThickness="1" BorderBrush="Black">
<TextBlock Text="13" VerticalAlignment="Center" HorizontalAlignment="Center"
FontSize="50" Margin="0,0,3,14" />
</Border>
</Grid>
Note: You can also use the TextBlock.TextAlignment Property to make adjustments to the horizontal alignment of text content.
I'd add this as a comment but I haven't got enough reputation :P
It looks off center because the height and width you have specified for the grid (50x50) is too small to house a font size of 50. Either increase the size to 100x100 or lower the font size to something smaller.
To demonstrate that they will be perfectly aligned in the center by doing this - view this code in visual studio somewhere. You will see the numbers of these textblocks overlap perfectly.
<Grid Height="100" Width="100">
<Border BorderThickness="1" BorderBrush="Black" >
<TextBlock Text="13" VerticalAlignment="Center" HorizontalAlignment="Center" FontSize="50"/>
</Border>
<Border BorderThickness="1" BorderBrush="Black" >
<TextBlock Text="31" VerticalAlignment="Center" HorizontalAlignment="Center" FontSize="50"/>
</Border>
</Grid>
I hope this helps you out :)
Definition: Having 2D-array of string (about 10 columns, 1,600 rows, fixed length of 7-char) serving as a data source for WPF .NET 4.0 Grid control, the following code snippet has been used in order to populate the Grid with labels displaying values from array. Note: Grid was added to XAML and passed to the function PopulateGrid (see Listing 1.). The visual output is essentially a tabular data representation in read-only mode (no need for two-way binding).
Problem: Performance is a key issue. It took a mind-boggling 3...5 sec to complete this operation running on powerful Intel-i3/8GB-DDR3 PC; therefore, this WPF Grid performance is, IMHO, at least an order of magnitude slower than expected, based on comparison with similar controls/tasks in, e.g. regular WinForm data-aware controls, or even Excel worksheet.
Question 1: if there is a way to improve the performance of WPF Grid in scenario described above? Please direct your answer/potential improvement to the code snippet provided below in Listing 1 and Listing 2.
Question 1a: proposed solution could implement data binding to additional data-aware control, like for example DataGrid to DataTable. I've added string[,] to DataTable dt converter in Listing 2, so that additional control's DataContext (or ItemsSource, whatever) property could be bound to dt.DefaultView. So, in the simplest form, could you please provide a compact (desirably about couple lines of code as it was done in old-style data-aware controls) and efficient (performance-wise) solution on data-binding of WPF DataGrid to DataTable object ?
Many Thanks.
Listing 1. Procedure to populate WPF Grid GridOut from 2D string[,] Values
#region Populate grid with 2D-array values
/// <summary>
/// Populate grid with 2D-array values
/// </summary>
/// <param name="Values">string[,]</param>
/// <param name="GridOut">Grid</param>
private void PopulateGrid(string[,] Values, Grid GridOut)
{
try
{
#region clear grid, then add ColumnDefinitions/RowsDefinitions
GridOut.Children.Clear();
GridOut.ColumnDefinitions.Clear();
GridOut.RowDefinitions.Clear();
// get column num
int _columns = Values.GetUpperBound(1) + 1;
// add ColumnDefinitions
for (int i = 0; i < _columns; i++)
{
GridOut.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(1, GridUnitType.Star) });
}
// get rows num
int _rows = Values.GetUpperBound(0) + 1;
// add RowDefinitions
for (int i = 0; i < _rows; i++)
{
GridOut.RowDefinitions.Add(new RowDefinition { Height = new GridLength(1, GridUnitType.Star) });
}
#endregion
#region populate grid w/labels
// populate grid w/labels
for (int i = 0; i < _rows; i++)
{
for (int j = 0; j < _columns; j++)
{
// new Label control
Label _lblValue = new Label();
// assign value to Label
_lblValue.Content = Values[i, j].ToString();
// add Label to GRid
GridOut.Children.Add(_lblValue);
Grid.SetRow(_lblValue, i);
Grid.SetColumn(_lblValue, j);
}
}
#endregion
}
catch
{
GridOut.Children.Clear();
GridOut.ColumnDefinitions.Clear();
GridOut.RowDefinitions.Clear();
}
}
#endregion
Listing 2. string[,] to DataTable conversion
#region internal: Convert string[,] to DataTable
/// <summary>
/// Convert string[,] to DataTable
/// </summary>
/// <param name="arrString">string[,]</param>
/// <returns>DataTable</returns>
internal static DataTable Array2DataTable(string[,] arrString)
{
DataTable _dt = new DataTable();
try
{
// get column num
int _columns = arrString.GetUpperBound(1) + 1;
// get rows num
int _rows = arrString.GetUpperBound(0) + 1;
// add columns to DataTable
for (int i = 0; i < _columns; i++)
{
_dt.Columns.Add(i.ToString(), typeof(string));
}
// add rows to DataTable
for (int i = 0; i < _rows; i++)
{
DataRow _dr = _dt.NewRow();
for (int j = 0; j < _columns; j++)
{
_dr[j] = arrString[i,j];
}
_dt.Rows.Add(_dr);
}
return _dt;
}
catch { throw; }
}
#endregion
Note 2. It's recommended to replace Label control w/TextBlock using its Text property instead of Content as in case of Label. It will speed up the execution a little bit, plus the code snippet will be forward compatible with VS 2012 for Win 8, which doesn't include Label.
Note 3: So far I've tried binding DataGrid to DataTable (see XAML in Listing 3), but performance is very poor (grdOut is a nested Grid, that was used as a container for tabular data; _dataGrid is a data-aware object type of DataGrid).
Listing 3. DataGrid binding to DataTable: performance was poor, so I've removed that ScrollViewer and not it's running OK.
<ScrollViewer ScrollViewer.CanContentScroll="True" VerticalScrollBarVisibility="Auto" >
<Grid Name="grdOut">
<DataGrid AutoGenerateColumns="True" Name="_dataGrid" ItemsSource="{Binding Path=.}" />
</Grid>
</ScrollViewer>
Ok. Delete all your code and start all over.
This is my take on a "Dynamic Grid" of Labels with X number of rows and Y number of columns based off a 2D string array:
<Window x:Class="MiscSamples.LabelsGrid"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="LabelsGrid" Height="300" Width="300">
<DockPanel>
<Button DockPanel.Dock="Top" Content="Fill" Click="Fill"/>
<ItemsControl ItemsSource="{Binding Items}"
ScrollViewer.HorizontalScrollBarVisibility="Auto"
ScrollViewer.VerticalScrollBarVisibility="Auto"
ScrollViewer.CanContentScroll="true"
ScrollViewer.PanningMode="Both">
<ItemsControl.Template>
<ControlTemplate>
<ScrollViewer>
<ItemsPresenter/>
</ScrollViewer>
</ControlTemplate>
</ItemsControl.Template>
<ItemsControl.ItemTemplate>
<DataTemplate>
<ItemsControl ItemsSource="{Binding Items}">
<ItemsControl.ItemTemplate>
<DataTemplate>
<Label Content="{Binding}"/>
</DataTemplate>
</ItemsControl.ItemTemplate>
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<UniformGrid Rows="1"/>
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
</ItemsControl>
</DataTemplate>
</ItemsControl.ItemTemplate>
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<VirtualizingStackPanel VirtualizationMode="Recycling" IsVirtualizing="True"/>
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
</ItemsControl>
</DockPanel>
</Window>
Code Behind:
public partial class LabelsGrid : Window
{
private LabelsGridViewModel ViewModel { get; set; }
public LabelsGrid()
{
InitializeComponent();
DataContext = ViewModel = new LabelsGridViewModel();
}
private void Fill(object sender, RoutedEventArgs e)
{
var array = new string[1600,20];
for (int i = 0; i < 1600; i++)
{
for (int j = 0; j < 20; j++)
{
array[i, j] = "Item" + i + "-" + j;
}
}
ViewModel.PopulateGrid(array);
}
}
ViewModel:
public class LabelsGridViewModel: PropertyChangedBase
{
public ObservableCollection<LabelGridItem> Items { get; set; }
public LabelsGridViewModel()
{
Items = new ObservableCollection<LabelGridItem>();
}
public void PopulateGrid(string[,] values)
{
Items.Clear();
var cols = values.GetUpperBound(1) + 1;
int rows = values.GetUpperBound(0) + 1;
for (int i = 0; i < rows; i++)
{
var item = new LabelGridItem();
for (int j = 0; j < cols; j++)
{
item.Items.Add(values[i, j]);
}
Items.Add(item);
}
}
}
Data Item:
public class LabelGridItem: PropertyChangedBase
{
public ObservableCollection<string> Items { get; set; }
public LabelGridItem()
{
Items = new ObservableCollection<string>();
}
}
PropertyChangedBase class (MVVM Helper)
public class PropertyChangedBase:INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
protected virtual void OnPropertyChanged(string propertyName)
{
Application.Current.Dispatcher.BeginInvoke((Action) (() =>
{
PropertyChangedEventHandler handler = PropertyChanged;
if (handler != null) handler(this, new PropertyChangedEventArgs(propertyName));
}));
}
}
Result:
Performance is AWESOME. Notice I'm using 20 columns instead of the 10 you suggested. The Filling of the grid is IMMEDIATE when you click the button. I'm sure performance is much better than crappy dinosaur winforms due to Built-in UI Virtualization.
The UI is defined in XAML, as opposed to creating UI elements in procedural code, which is a bad practice.
The UI and data are kept separate, thus increasing maintainability and scalability and cleanliness.
Copy and paste my code in a File -> New -> WPF Application and see the results for yourself.
Also, keep in mind that if you're only going to display text, you'd better use a TextBlock instead of a Label, which is a much lightweight Text element.
WPF rocks, even if at edge cases it might present performance degradation, it's still 12837091723 better than anything currently in existence.
Edit:
I went ahead and added 0 zeros to the row count (160000). Performance is still acceptable. It took less than 1 second to populate the Grid.
Notice that the "Columns" are NOT being virtualized in my example. This can lead to performance issues if there's a big number of them, but that's not what you described.
Edit2:
Based on your comments and clarifications, I made a new example, this time based in a System.Data.DataTable. No ObservableCollections, no async stuff (there was nothing async in my previous example anyways). And just 10 columns. Horizontal Scrollbar was there due to the fact that the window was too small (Width="300") and was not enough to show the data. WPF is resolution independent, unlike dinosaur frameworks, and it shows scrollbars when needed, but also stretches the content to the available space (you can see this by resizing the window, etc).
I also put the array initializing code in the Window's constructor (to deal with the lack of INotifyPropertyChanged) so it's going to take a little bit more to load and show it, and I noticed this sample using System.Data.DataTable is slightly slower than the previous one.
However, I must warn you that Binding to Non-INotifyPropertyChanged objects may cause a Memory Leak.
Still, you will NOT be able to use a simple Grid control, because it does not do UI Virtualization. If you want a Virtualizing Grid, you will have to implement it yourself.
You will also NOT be able to use a winforms approach to this. It's simply irrelevant and useless in WPF.
<ItemsControl ItemsSource="{Binding Rows}"
ScrollViewer.HorizontalScrollBarVisibility="Auto"
ScrollViewer.VerticalScrollBarVisibility="Auto"
ScrollViewer.CanContentScroll="true"
ScrollViewer.PanningMode="Both">
<ItemsControl.Template>
<ControlTemplate>
<ScrollViewer>
<ItemsPresenter/>
</ScrollViewer>
</ControlTemplate>
</ItemsControl.Template>
<ItemsControl.ItemTemplate>
<DataTemplate>
<ItemsControl ItemsSource="{Binding ItemArray}">
<ItemsControl.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding}"/>
</DataTemplate>
</ItemsControl.ItemTemplate>
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<UniformGrid Rows="1"/>
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
</ItemsControl>
</DataTemplate>
</ItemsControl.ItemTemplate>
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<VirtualizingStackPanel VirtualizationMode="Recycling" IsVirtualizing="True"/>
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
</ItemsControl>
Code Behind:
public partial class LabelsGrid : Window
{
public LabelsGrid()
{
var array = new string[160000, 10];
for (int i = 0; i < 160000; i++)
{
for (int j = 0; j < 10; j++)
{
array[i, j] = "Item" + i + "-" + j;
}
}
DataContext = Array2DataTable(array);
InitializeComponent();
}
internal static DataTable Array2DataTable(string[,] arrString)
{
//... Your same exact code here
}
}
Bottom line is to do something in WPF you have to do it the WPF way. It's not just a UI framework, it's more of an Application Framework by itself.
Edit3:
<DataGrid AutoGenerateColumns="True" ItemsSource="{Binding}"/>
DataContext = Array2DataTable(array).DefaultView;
Works perfectly fine for me. Loading time is not noticeable with 160000 rows. What .Net framework version are you using?
I am new to WPF and am trying to write a clickable zoom-pan image control. I already have a zoom-pan image which seems to work:
<Border Name="border" ClipToBounds="True">
<Canvas>
<Image Name ="image">
Source="{Binding Path=Source}"
MouseLeftButtonDown="image_MouseLeftButtonDown"
MouseLeftButtonUp="image_MouseLeftButtonUp"
MouseMove="image_MouseMove"
MouseWheel="image_MouseWheel">
</Image>
</Canvas>
</Border>
For the mouse and wheel events I used this post: http://www.codeproject.com/Articles/168176/Zooming-and-panning-in-WPF-with-fixed-focus
I am writing the clickable control by inheriting from ZoomPanImage and adding an event for LeftMouseUp.
public class ClickableImage : PanZoomImage
{
public event Action<Point> Click;
//...
protected override void OnMouseLeftButtonUp(MouseButtonEventArgs e)
{
base.OnMouseLeftButtonUp(e);
// ... all sorts of checks to distinguish click from mouse move
if (Click != null)
{
Click(ControlToImage(mouseUpCoordinates));
}
}
protected Point ControlToImage(Point controlPixel)
{
//this is where i am stuck...
}
}
My problem is that I can't seem to calculate the correct image coordinates given the control coordinates. I need to take into account that the image can be zoomed and panned and that the window itself can be resized.
I tried using the rendering transform. When I zoom and pan the image I update the transform. And when I try to convert control coordinates to image coordinates I use the inverse transform:
Point imagePixel = image.RenderTransform.Inverse.Transform(controlPixel);
But this didn't work. One of the problems is that the Transform starts as Identity while in fact the image is stretched uniformly to the control's size.
Thanks,
Dina
Here's how I solved it. As Clemens suggested, I set the image stretch mode to none.
<Image Name="image" RenderOptions.BitmapScalingMode="NearestNeighbor" Stretch="None"
Source="{Binding Path=Source}"
MouseLeftButtonDown="image_MouseLeftButtonDown"
MouseLeftButtonUp="image_MouseLeftButtonUp"
MouseMove="image_MouseMove"
MouseWheel="image_MouseWheel"
Loaded="image_Loaded">
<Image.ContextMenu>
<ContextMenu>
<MenuItem Header="Fit to window" Click="FitToWindow_MenuItem_Click"></MenuItem>
</ContextMenu>
</Image.ContextMenu>
</Image>
This means that when the image is loaded into the window, you can only see part of it - depending on the window size. This is bad, but what's important is that the transform is identity and you can now manually set it such that the image is fully shown in the window.
private void FitViewToWindow()
{
if (Source == null)
throw new InvalidOperationException("Source not set");
BitmapSource bitmapSource = Source as BitmapSource;
if (bitmapSource == null)
throw new InvalidOperationException("Unsupported Image Source Type");
if (border.ActualWidth <= 0 || border.ActualHeight <= 0)
return;
double scaleX = border.ActualWidth / bitmapSource.PixelWidth;
double scaleY = border.ActualHeight / bitmapSource.PixelHeight;
double scale = Math.Min(scaleX, scaleY);
Matrix m = Matrix.Identity;
m.ScaleAtPrepend(scale, scale, 0, 0);
double centerX = (border.ActualWidth - bitmapSource.PixelWidth * scale) / 2;
double centerY = (border.ActualHeight - bitmapSource.PixelHeight * scale) / 2;
m.Translate(centerX, centerY);
image.RenderTransform = new MatrixTransform(m);
}
This function should be called upon loading the image and upon changing the source of the image. As for resizing the window - as long as you keep track of the transform, you will be able to convert coordinate systems correctly. For example, here's what I do for window resize:
protected override void OnRenderSizeChanged(SizeChangedInfo sizeInfo)
{
base.OnRenderSizeChanged(sizeInfo);
//center the image in the new size
if (sizeInfo.PreviousSize.Width <= 0 || sizeInfo.PreviousSize.Height <= 0)
return;
Matrix m = image.RenderTransform.Value;
double offsetX = (sizeInfo.NewSize.Width - sizeInfo.PreviousSize.Width) / 2;
double offsetY = (sizeInfo.NewSize.Height - sizeInfo.PreviousSize.Height) / 2;
m.Translate(offsetX, offsetY);
image.RenderTransform = new MatrixTransform(m);
}
Essentially I want a wrapPanel, but I would like items to snap to a grid rather than be pressed up to the left, so I can get a nice uniform looking grid, that automatically consumes available space.
WrapPanel handles the resize part.
WPF.Contrib.AutoGrid handles a nice automatic grid.
Anyone got a control that combines them?
My use case is I have a series of somewhat irregularly shaped controls. I would like them to appear in nice columns so the wrap panel should snap to the next "tabstop" when placing a control
When I read your question I assumed you wanted something like this:
public class UniformWrapPanel : WrapPanel
{
protected override Size MeasureOverride(Size constraint)
{
if(Orientation == Orientation.Horizontal)
ItemWidth = Children.Select(element =>
{
element.Measure(constraint);
return element.DesiredWidth;
}).Max();
else
... same for vertical ...
return base.MeasureOverride(constraint);
}
}
but I see someone else has already implemented a "UniformWrapPanel" and from your comments you indicate this is not what you were looking for.
The comment I don't understand is:
I want it to not force items to be a given size, but use their already existing size and therefore determine column widths automatically
Can you please provide an example to illustrate how you want things laid out with varying sizes? A picture might be nice. You also mention "tabstop" but don't give any definition of what that would be.
Here is some code that I whipped up based on some of the other controls that are close. It does a decent job of doing the layout, although it has an issue where grand-child controls do not fill up all their available space.
protected override Size ArrangeOverride(Size finalSize)
{
double rowY = 0;
int col = 0;
double currentRowHeight = 0;
foreach (UIElement child in Children)
{
var initialSize = child.DesiredSize;
int colspan = (int) Math.Ceiling(initialSize.Width/ ColumnSize);
Console.WriteLine(colspan);
double width = colspan * ColumnSize;
if (col > 0 && (col * ColumnSize) + width > constrainedSize.Width)
{
rowY += currentRowHeight;
col = 0;
currentRowHeight = 0;
}
var childRect = new Rect(col * ColumnSize, rowY, width, initialSize.Height);
child.Arrange(childRect);
currentRowHeight = Math.Max(currentRowHeight, initialSize.Height);
col+=colspan;
}
return finalSize;
}
Size constrainedSize;
protected override Size MeasureOverride(Size constraint)
{
constrainedSize = constraint;
return base.MeasureOverride(constraint);
}
Try setting ItemWidth (or ItemHeight) property of the WrapPanel:
<WrapPanel ItemWidth="48">
<TextBlock Text="H" Background="Red"/>
<TextBlock Text="e" Background="Orange"/>
<TextBlock Text="l" Background="Yellow"/>
<TextBlock Text="l" Background="Green"/>
<TextBlock Text="o" Background="Blue"/>
<TextBlock Text="!" Background="Violet"/>
</WrapPanel>
I am new to WPF, so the answer to the following question might be obvious, however it isn't to me.
I need to display an image where users can set markers on (As an example: You might want to mark a person's face on a photograph with a rectangle), however the markers need to keep their relative position when scaling the image.
Currently I am doing this by using a Canvas and setting an ImageBrush as Background. This displays the image and I can add elements like a Label (as replacement for a rectangle) on top of the image. But when I set a label like this, it's position is absolute and so when the underlying picture is scaled (because the user drags the window larger) the Label stays at it's absolute position (say, 100,100) instead of moving to the new position that keeps it "in sync" with the underlying image.
To cut the matter short: When I set a marker on a person's eye, it shouldn't be on the person's ear after scaling the window.
Any suggestions on how to do that in WPF? Maybe Canvas is the wrong approach in the first place? I could keep a collection of markers in code and recalculate their position every time the window gets resized, but I hope there is a way to let WPF do that work for me :-)
I am interested in hearing your opinions on this.
Thanks
Okay that seems to work. Here's what I did:
Wrote a custom converter
Every time a user clicks on the canvas, I create a new Label (will exchange that with a UserComponent later), create bindings using my converter class and do the initial calculations to get the relative position to the canvas from the absolute position of the mouse pointer
Here's some sample code for the converter:
public class PercentageConverter : IValueConverter
{
/// <summary>
/// Calculates absolute position values of an element given the dimensions of the container and the relative
/// position of the element, expressed as percentage
/// </summary>
/// <param name="value">Dimension value of the container (width or height)</param>
/// <param name="parameter">The percentage used to calculate new absolute value</param>
/// <returns>parameter * value as Double</returns>
public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
//input is percentage
//output is double
double containerValue = System.Convert.ToDouble(value, culture.NumberFormat);
double perc;
if (parameter is String)
{
perc = double.Parse(parameter as String, culture.NumberFormat);
}
else
{
perc = (double)parameter;
}
double coord = containerValue * perc;
return coord;
}
/// <summary>
/// Calculates relative position (expressed as percentage) of an element to its container given its current absolute position
/// as well as the dimensions of the container
/// </summary>
/// <param name="value">Absolute value of the container (width or height)</param>
/// <param name="parameter">X- or Y-position of the element</param>
/// <returns>parameter / value as double</returns>
public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
//output is percentage
//input is double
double containerValue = System.Convert.ToDouble(value, culture.NumberFormat);
double coord = double.Parse(parameter as String, culture.NumberFormat);
double perc = coord / containerValue;
return perc;
}
}
And here's how you can create bindings in XAML (note that my canvas is declared as <Canvas x:Name="canvas" ... >):
<Label Background="Red" ClipToBounds="True" Height="22" Name="label1" Width="60"
Canvas.Left="{Binding Converter={StaticResource PercentageConverter}, ElementName=canvas, Path=ActualWidth, ConverterParameter=0.25}"
Canvas.Top="{Binding Converter={StaticResource PercentageConverter}, ElementName=canvas, Path=ActualHeight, ConverterParameter=0.65}">Marker 1</Label>
More useful, however, is to create Labels in code:
private void canvas_MouseDown(object sender, MouseButtonEventArgs e)
{
var mousePos = Mouse.GetPosition(canvas);
var converter = new PercentageConverter();
//Convert mouse position to relative position
double xPerc = (double)converter.ConvertBack(canvas.ActualWidth, typeof(Double), mousePos.X.ToString(), Thread.CurrentThread.CurrentCulture);
double yPerc = (double)converter.ConvertBack(canvas.ActualHeight, typeof(Double), mousePos.Y.ToString(), Thread.CurrentThread.CurrentCulture);
Label label = new Label { Content = "Label", Background = (Brush)new BrushConverter().ConvertFromString("Red")};
//Do binding for x-coordinates
Binding posBindX = new Binding();
posBindX.Converter = new PercentageConverter();
posBindX.ConverterParameter = xPerc;
posBindX.Source = canvas;
posBindX.Path = new PropertyPath("ActualWidth");
label.SetBinding(Canvas.LeftProperty, posBindX);
//Do binding for y-coordinates
Binding posBindY = new Binding();
posBindY.Converter = new PercentageConverter();
posBindY.ConverterParameter = yPerc;
posBindY.Source = canvas;
posBindY.Path = new PropertyPath("ActualHeight");
label.SetBinding(Canvas.TopProperty, posBindY);
canvas.Children.Add(label);
}
So basically, it's almost like my first idea: Use relative position instead of absolute and recalculate all positions on every resize, only this way it's being done by WPF. Just what I wanted, thanks Martin!
Note however, that these examples only work if the Image inside the ImageBrush has exactly the same dimensions as the surrounding Canvas, because this relative positioning does not take margins etc into account. I will have to tune that
Of the top of my head you could write a converter class that would take in a percentage and return an absolute position. As an example if your window was 200 X 200 and you placed the label at 100 X 100 when you scale the window to 400 X 400 the label would stay where it is (as per your original question). However if you used a converter so that instead you could set the labels position to 50% of its parent container's size then as the window scaled the label would move with it.
You may also need to use the same converter for width and height so that it increased in size to match as well.
Sorry for the lack of detail, if I get a chance I'll edit this with example code in a little while.
Edited to add
This question gives some code for a percentage converter.
Although this post is old and already answered, it can still be helpful to others so I will add my answer.
I came up with two ways for maintaining a relative position for elements in a Canvas
MultiValueConverter
Attached Properties
The idea is to provide two values (x,y) in range [0,1] that will define the relative position of the element with respect to the top-left corner of the Canvas. These (x,y) values will be used to calculate and set the correct Canvas.Left and Canvas.Top values.
In order to place the center of the element at a relative position, we will need the ActualWidth and ActualHeight of the Canvas and the element.
MultiValueConverter
The MultiValueConverter RelativePositionConverter:
This converter can be used to relatively position the X and/or Y position when binding with Canvas.Left and Canvas.Top.
public class RelativePositionConverter : IMultiValueConverter
{
public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
{
if (values?.Length < 2
|| !(values[0] is double relativePosition)
|| !(values[1] is double size)
|| !(parameter is string)
|| !double.TryParse((string)parameter, out double relativeToValue))
{
return DependencyProperty.UnsetValue;
}
return relativePosition * relativeToValue - size / 2;
}
public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
}
Example usage of RelativePositionConverter:
A Canvas width and height are binded to an Image. The Canvas has a child element - an Ellipse that maintains a relative position with the Canvas (and Image).
<Grid Margin="10">
<Image x:Name="image" Source="Images/example-graph.png" />
<Canvas Background="#337EEBE8" Width="{Binding ElementName=image, Path=ActualWidth}" Height="{Binding ElementName=image, Path=ActualHeight}">
<Ellipse Width="35" Height="35" StrokeThickness="5" Fill="#D8FFFFFF" Stroke="#FFFBF73C">
<Canvas.Left>
<MultiBinding Converter="{StaticResource RelativePositionConverter}" ConverterParameter="0.461">
<Binding RelativeSource="{RelativeSource FindAncestor, AncestorType=Canvas}" Path="ActualWidth" />
<Binding RelativeSource="{RelativeSource Self}" Path="ActualWidth" />
</MultiBinding>
</Canvas.Left>
<Canvas.Top>
<MultiBinding Converter="{StaticResource RelativePositionConverter}" ConverterParameter="0.392">
<Binding RelativeSource="{RelativeSource FindAncestor, AncestorType=Canvas}" Path="ActualHeight" />
<Binding RelativeSource="{RelativeSource Self}" Path="ActualHeight" />
</MultiBinding>
</Canvas.Top>
</Ellipse>
</Canvas>
</Grid>
Attached Properties
The Attached Properties RelativeXProperty, RelativeYProperty and RelativePositionProperty:
RelativeXProperty and RelativeYProperty can be used to control the X and/or Y relative positioning with two separate attached properties.
RelativePositionProperty can be used to control the X and Y relative positioning with a single attached property.
public static class CanvasExtensions
{
public static readonly DependencyProperty RelativeXProperty =
DependencyProperty.RegisterAttached("RelativeX", typeof(double), typeof(CanvasExtensions), new PropertyMetadata(0.0, new PropertyChangedCallback(OnRelativeXChanged)));
public static readonly DependencyProperty RelativeYProperty =
DependencyProperty.RegisterAttached("RelativeY", typeof(double), typeof(CanvasExtensions), new PropertyMetadata(0.0, new PropertyChangedCallback(OnRelativeYChanged)));
public static readonly DependencyProperty RelativePositionProperty =
DependencyProperty.RegisterAttached("RelativePosition", typeof(Point), typeof(CanvasExtensions), new PropertyMetadata(new Point(0, 0), new PropertyChangedCallback(OnRelativePositionChanged)));
public static double GetRelativeX(DependencyObject obj)
{
return (double)obj.GetValue(RelativeXProperty);
}
public static void SetRelativeX(DependencyObject obj, double value)
{
obj.SetValue(RelativeXProperty, value);
}
public static double GetRelativeY(DependencyObject obj)
{
return (double)obj.GetValue(RelativeYProperty);
}
public static void SetRelativeY(DependencyObject obj, double value)
{
obj.SetValue(RelativeYProperty, value);
}
public static Point GetRelativePosition(DependencyObject obj)
{
return (Point)obj.GetValue(RelativePositionProperty);
}
public static void SetRelativePosition(DependencyObject obj, Point value)
{
obj.SetValue(RelativePositionProperty, value);
}
private static void OnRelativeXChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
if (!(d is FrameworkElement element)) return;
if (!(VisualTreeHelper.GetParent(element) is Canvas canvas)) return;
canvas.SizeChanged += (s, arg) =>
{
double relativeXPosition = GetRelativeX(element);
double xPosition = relativeXPosition * canvas.ActualWidth - element.ActualWidth / 2;
Canvas.SetLeft(element, xPosition);
};
}
private static void OnRelativeYChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
if (!(d is FrameworkElement element)) return;
if (!(VisualTreeHelper.GetParent(element) is Canvas canvas)) return;
canvas.SizeChanged += (s, arg) =>
{
double relativeYPosition = GetRelativeY(element);
double yPosition = relativeYPosition * canvas.ActualHeight - element.ActualHeight / 2;
Canvas.SetTop(element, yPosition);
};
}
private static void OnRelativePositionChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
if (!(d is FrameworkElement element)) return;
if (!(VisualTreeHelper.GetParent(element) is Canvas canvas)) return;
canvas.SizeChanged += (s, arg) =>
{
Point relativePosition = GetRelativePosition(element);
double xPosition = relativePosition.X * canvas.ActualWidth - element.ActualWidth / 2;
double yPosition = relativePosition.Y * canvas.ActualHeight - element.ActualHeight / 2;
Canvas.SetLeft(element, xPosition);
Canvas.SetTop(element, yPosition);
};
}
}
Example usage of RelativeXProperty and RelativeYProperty:
<Grid Margin="10">
<Image x:Name="image" Source="Images/example-graph.png" />
<Canvas Background="#337EEBE8" Width="{Binding ElementName=image, Path=ActualWidth}" Height="{Binding ElementName=image, Path=ActualHeight}">
<Ellipse Width="35" Height="35" StrokeThickness="5" Fill="#D8FFFFFF" Stroke="#FFFBF73C"
local:CanvasExtensions.RelativeX="0.461"
local:CanvasExtensions.RelativeY="0.392">
</Ellipse>
</Canvas>
</Grid>
Example usage of RelativePositionProperty:
<Grid Margin="10">
<Image x:Name="image" Source="Images/example-graph.png" />
<Canvas Background="#337EEBE8" Width="{Binding ElementName=image, Path=ActualWidth}" Height="{Binding ElementName=image, Path=ActualHeight}">
<Ellipse Width="35" Height="35" StrokeThickness="5" Fill="#D8FFFFFF" Stroke="#FFFBF73C"
local:CanvasExtensions.RelativePosition="0.461,0.392">
</Ellipse>
</Canvas>
</Grid>
And hear is how it looks:
The Ellipse that is a child of a Canvas maintains a relative position with respect to the Canvas (and an Image).