I working on a simple imageviewer app. I control the Stretch property on the binding based on ViewModel property.
The problem occurs when I change the Stretch attribute based on a 'Combobox', bound to ViewModel, and the image 'cuts off' the corners of a wide image when using 'UniformToFill'. Hence to use of a ScrollViewer to be able to scroll the image content.
The problem is the ScrollViewer doesn't seem to show up scrollbars for me to be able to scroll.
WPF Markup:
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<!-- Other Grids removed -->
<Grid Name="Container" Grid.Column="2" Grid.Row="0" Grid.RowSpan="2">
<ScrollViewer HorizontalScrollBarVisibility="Visible" VerticalScrollBarVisibility="Visible">
<Image Source="{Binding SelectedPhoto.Value.Image}"
Stretch="{Binding ImageStretch}" Name="PhotoImage" />
</ScrollViewer>
</Grid>
I understand if I set a fixed Height and Width to ScrollViewer and Image, it will work. But I want to do it Dynamically:
The ScrollView Will have Height and Width from Parent 'Grid(Contaioner)' Control.
The Image will have Height and Width from itself, but take Stretch to account in that calculation.
Possible to solve with ActualHeight, ActualWidth? And a DependecyProperty?
This is almost impossible, Or I should say it doesn't make a lot of sense to expect ScrollViewer to know the boundaries of an image with Stretch = UniformToFill. According to MSDN:
UniformToFill:
The content (your Image) is resized to fill the destination dimensions (window or grid) while it
preserves its native aspect ratio. If the aspect ratio of the
destination rectangle differs from the source, the source content is
clipped to fit in the destination dimensions (Therefore the image will be cutted off).
So I think what we really need here is to use Uniform + Proper Scaling instead of UniformToFill.
The solution is when Stretch is set to UniformToFill it must set to Uniform and then Image.Width = image actual width * scalingParam and Image.Height= image actual height * scalingParam, where scalingParam = Grid.Width (or Height) / image actual width (or Height). This way ScrollViewer boundaries will be the same as the image scaled size.
I've provided a working solution to give you an Idea, I'm not sure how suitable would it be for your case but here it is:
First I defined a simple view-model for my Images:
public class ImageViewModel: INotifyPropertyChanged
{
// implementation of INotifyPropertyChanged ...
private BitmapFrame _bitmapFrame;
public ImageViewModel(string path, Stretch stretch)
{
// determining the actual size of the image.
_bitmapFrame = BitmapFrame.Create(new Uri(path), BitmapCreateOptions.DelayCreation, BitmapCacheOption.None);
Width = _bitmapFrame.PixelWidth;
Height = _bitmapFrame.PixelHeight;
Scale = 1;
Stretch = stretch;
}
public int Width { get; set; }
public int Height { get; set; }
double _scale;
public double Scale
{
get
{
return _scale;
}
set
{
_scale = value;
OnPropertyChanged("Scale");
}
}
Stretch _stretch;
public Stretch Stretch
{
get
{
return _stretch;
}
set
{
_stretch = value;
OnPropertyChanged("Stretch");
}
}
}
In the above code BitmapFrame is used to determine the actual size of the image.
Then I did some initializations in my Mainwindow (or main view-model):
// currently displaying image
ImageViewModel _imageVm;
public ImageViewModel ImageVM
{
get
{
return _imageVm;
}
set
{
_imageVm = value;
OnPropertyChanged("ImageVM");
}
}
// currently selected stretch type
Stretch _stretch;
public Stretch CurrentStretch
{
get
{
return _stretch;
}
set
{
_stretch = value;
//ImageVM should be notified to refresh UI bindings
ImageVM.Stretch = _stretch;
OnPropertyChanged("ImageVM");
OnPropertyChanged("CurrentStretch");
}
}
// a list of Stretch types
public List<Stretch> StretchList { get; set; }
public string ImagePath { get; set; }
public MainWindow()
{
InitializeComponent();
DataContext = this;
// sample image path
ImagePath = #"C:\Users\...\YourFile.png";
StretchList = new List<Stretch>();
StretchList.Add( Stretch.None);
StretchList.Add( Stretch.Fill);
StretchList.Add( Stretch.Uniform);
StretchList.Add( Stretch.UniformToFill);
ImageVM = new ImageViewModel(ImagePath, Stretch.None);
CurrentStretch = StretchList[0];
}
My Xaml looks like this:
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="*"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<Grid Grid.Row="0" Grid.Column="0" >
<Grid.Resources>
<local:MultiConverter x:Key="multiC"/>
</Grid.Resources>
<ScrollViewer HorizontalScrollBarVisibility="Visible" VerticalScrollBarVisibility="Visible">
<Image Source="{Binding ImagePath}" Name="PhotoImage">
<Image.Stretch>
<MultiBinding Converter="{StaticResource multiC}">
<Binding Path="ImageVM" />
<Binding RelativeSource="{RelativeSource AncestorType=Window}" Path="ActualWidth"/>
<Binding RelativeSource="{RelativeSource AncestorType=Window}" Path="ActualHeight"/>
</MultiBinding>
</Image.Stretch>
<Image.LayoutTransform>
<ScaleTransform ScaleX="{Binding ImageVM.Scale}" ScaleY="{Binding ImageVM.Scale}"
CenterX="0.5" CenterY="0.5" />
</Image.LayoutTransform>
</Image>
</ScrollViewer>
</Grid>
<ComboBox Grid.Row="2" Grid.Column="0" ItemsSource="{Binding StretchList}" SelectedItem="{Binding CurrentStretch}" DisplayMemberPath="."/>
</Grid>
As you can see, I've used a multi-value converter that takes 3 arguments: current image view-model and window width and height. This arguments were used to calculate current size of the area that image fills. Also I've used ScaleTransform to scale that area to the calculated size. This is the code for multi-value converter:
public class MultiConverter : IMultiValueConverter
{
public object Convert(
object[] values, Type targetType, object parameter, CultureInfo culture)
{
if (values[0] is ImageViewModel)
{
var imageVm = (ImageViewModel)values[0];
// if user selects UniformToFill
if (imageVm.Stretch == Stretch.UniformToFill)
{
var windowWidth = (double)values[1];
var windowHeight = (double)values[2];
var scaleX = windowWidth / (double)imageVm.Width;
var scaleY = windowHeight / (double)imageVm.Height;
// since it's "uniform" Max(scaleX, scaleY) is used for scaling in both horizontal and vertical directions
imageVm.Scale = Math.Max(scaleX, scaleY);
// "UniformToFill" is actually "Uniform + Proper Scaling"
return Stretch.Uniform;
}
// if user selects other stretch types
// remove scaling
imageVm.Scale = 1;
return imageVm.Stretch;
}
return Binding.DoNothing;
}
public object[] ConvertBack(
object value, Type[] targetTypes, object parameter, CultureInfo culture)
{
throw new NotSupportedException();
}
}
So ultimately i took a discussion with some co-workers and we agreed that we need to fix the problem before a fix. In other words replace Stretch attribute combined with scrollviewer with something more robust that will support extent ability.
The solution I came up with will work for now, and a better solution to the whole problem will be preformed next scrum sprint.
Solution
A custom dependencyproperty that will control width and height depending on stretch attribute currently present on element.
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<Grid Grid.Column="2" Grid.Row="0" Grid.RowSpan="2">
<ScrollViewer HorizontalScrollBarVisibility="Auto" VerticalScrollBarVisibility="Auto">
<Image Name="PhotoImage"
Source="{Binding SelectedPhoto.Value.Image}"
Stretch="{Binding ImageStretch, NotifyOnTargetUpdated=True}}"
extensions:ImageExtensions.ChangeWidthHeightDynamically="True"/>
</ScrollViewer>
</Grid>
Dependency Property
public static bool GetChangeWidthHeightDynamically(DependencyObject obj)
{
return (bool)obj.GetValue(ChangeWidthHeightDynamicallyProperty);
}
public static void SetChangeWidthHeightDynamically(DependencyObject obj, bool value)
{
obj.SetValue(ChangeWidthHeightDynamicallyProperty, value);
}
public static readonly DependencyProperty ChangeWidthHeightDynamicallyProperty =
DependencyProperty.RegisterAttached("ChangeWidthHeightDynamically", typeof(bool), typeof(ImageExtensions), new PropertyMetadata(false, OnChangeWidthHeightDynamically));
private static void OnChangeWidthHeightDynamically(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
var image = d as Image;
if (image == null)
return;
image.SizeChanged += Image_SizeChanged;
image.TargetUpdated += Updated;
}
private static void Updated(object sender, DataTransferEventArgs e)
{
//Reset Width and Height attribute to Auto when Target updates
Image image = sender as Image;
if (image == null)
return;
image.Width = double.NaN;
image.Height = double.NaN;
}
private static void Image_SizeChanged(object sender, SizeChangedEventArgs e)
{
var image = sender as Image;
if (image == null)
return;
image.Measure(new Size(Double.PositiveInfinity, Double.PositiveInfinity));
if (Math.Abs(image.ActualHeight) <= 0 || Math.Abs(image.ActualWidth) <= 0)
return;
switch (image.Stretch)
{
case Stretch.Uniform:
{
image.Width = Double.NaN;
image.Height = Double.NaN;
break;
}
case Stretch.None:
{
image.Width = image.RenderSize.Width;
image.Height = image.RenderSize.Height;
break;
}
case Stretch.UniformToFill:
{
image.Width = image.ActualWidth;
image.Height = image.ActualHeight;
break;
}
default:
{
image.Width = double.NaN;
image.Height = double.NaN;
break;
}
}
}
The problem may come from the rest of your layout - If the Grid is contained in an infinitely resizable container (a Grid Column/Row set to Auto, a StackPanel, another ScrollViewer...), it will grow with the Image. And so will do the ScrollViewer, instead of activating the scroll bars.
Related
I have a User control. It has Some textboxes. I need to hide a single column in that control and if require I need to set as visible. It's like, setting visibility property to a textbox visible/hidden/collapsed. Same thing I need to do in a Column for a user control.
Here is my code.
UserControl Xaml:
<UserControl x:Class="UserControls.UserControl"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<Grid>
<Grid.RowDefinitions >
<RowDefinition Height="45"/>
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="15*" />
<ColumnDefinition Width="10*" />
<ColumnDefinition Width="10*" />
</Grid.ColumnDefinitions>
<TextBox Name="txt1"
Text=""
Grid.Row="0" Grid.Column="0"/>
<TextBox Name="txt1"
Text=""
Grid.Row="0" Grid.Column="1"/>
//....some othr controls..
</Grid>
</UserControl>
Window1.cs:
public partial class Window1
{
public Window1()
{
InitializeComponent();
var uc = new UserControl();
grid1.RowDefinitions.Add(new RowDefinition());
Grid.SetRow(uc, grid1.RowDefinitions.Count - 1);
grid1.Children.Add(uc);
}
}
I need to hide the Column 1. How might I conceal this column and if require I need to Enable this column too. Any offer assistance??
Column cannot be hidden by itself. Nevertheless you can wrap elements in column to one panel and then set its visibility to hidden/collapsed. You can also select all UIelements from column and subsequently set their visibility
var elements = Grid.Children.OfType<FrameworkElement>().Where(x => Grid.GetColumn(x) == ColumnNumber).ToList();
elements.ForEach(x => x.Visibility = System.Windows.Visibility.Collapsed);
EDIT
I created Attached Properties for you problem. I have simple grid panel with 2 elements inside
<Grid local:MyGrid.IsHidden="True" local:MyGrid.ColumnNumber="0">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<TextBlock Text="1" Grid.Column="0"/>
<TextBlock Text="2" Grid.Column="1" Name="tekst"/>
</Grid>
class MyGrid looks as follows:
public class MyGrid
{
public static void SetIsHidden(DependencyObject obj, bool val)
{
obj.SetValue(IsHiddenProperty, val);
}
public static bool GetIsHidden(DependencyObject obj)
{
return (bool)obj.GetValue(IsHiddenProperty);
}
public static void SetColumnNumber(DependencyObject obj, int val)
{
obj.SetValue(ColumnNumberProperty, val);
}
public static int GetColumnNumber(DependencyObject obj)
{
return (int)obj.GetValue(ColumnNumberProperty);
}
public static readonly DependencyProperty IsHiddenProperty = DependencyProperty.RegisterAttached("IsHidden", typeof(bool), typeof(MyGrid));
public static readonly DependencyProperty ColumnNumberProperty = DependencyProperty.RegisterAttached("ColumnNumber", typeof(int), typeof(MyGrid),
new FrameworkPropertyMetadata(-1, new PropertyChangedCallback((x, y) =>
{
if (x is Grid && GetIsHidden(x))
((Grid)x).Loaded += MyGrid_Loaded;
})));
static void MyGrid_Loaded(object sender, RoutedEventArgs e)
{
if (GetColumnNumber((DependencyObject)sender) >= 0 && GetColumnNumber((DependencyObject)sender) <= ((Grid)sender).ColumnDefinitions.Count - 1)
{
var elements = ((Grid)sender).Children.OfType<FrameworkElement>().Where(z => Grid.GetColumn(z) == GetColumnNumber((DependencyObject)sender)).ToList();
elements.ForEach(s => s.Visibility = System.Windows.Visibility.Collapsed);
}
}
}
Now if you set local:MyGrid.IsHidden to True and insert valid ColumnNumber local:MyGrid.ColumnNumber all UI elements will be hidden.
Standard view
If you set
local:MyGrid.IsHidden="True" local:MyGrid.ColumnNumber="0"
For settings
local:MyGrid.IsHidden="False" local:MyGrid.ColumnNumber="0"
everyting stays normal
As you can see from the ColumnDefinition Class page on MSDN, there is no Visibility property. Therefore, it is not possible to set the Visibililty of a Grid.Column. The customary way to do this would be to set the Visibility on each of the controls in that column.
The standard way to set the Visibility on one or more controls from other controls is to provide one or more bool properties:
public bool IsVisible { get; set; } // Implement INotifyPropertychanged interface here
Then we can data bind that property to the Control.Visibillity property of the relevant controls:
<TextBox Name="txt1" Text="" Grid.Row="0" Grid.Column="0" Visibillity="{
Binding IsVisible, Converter={StaticResource BooleanToVisibilityConverter}}" />
You can use the same Binding on each of the controls in the column, so that they can all be hidden at once:
// Hide column controls
IsVisible = false;
// Show column controls
IsVisible = true;
Finally, if you create a DependencyProperty for the IsVisible property, then you would be able to data bind to it from outside of your UserControl:
<Local:YourUserControl IsVisible="{Binding IsVisibleInMainWindowDataContext}" />
Then from object set as MainWindow.DataContext:
// Hide column controls
IsVisibleInMainWindowDataContext = false;
// Show column controls
IsVisibleInMainWindowDataContext = true;
I'm organizing my grid with RowDefinitions and ColumnDefinition, but forever when I want add a new RowDefinition in before actual any RowDefinition, I need reorganize Grid.Row of all controls
I saw RowDefinition and ColumnDefinition has a Name property, so I think is possible define Grid.Row with RowDefinition name or not? If is possible, How do
<Grid>
<Grid.RowDefinitions>
<RowDefinition Name="RowDictTitle" Height="27"/>
<RowDefinition Name="RowSearchWord" Height="27"/>
<RowDefinition/>
<RowDefinition Height="50"/>
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="1*"/>
<ColumnDefinition Width="2*"/>
</Grid.ColumnDefinitions>
<!--Row 1-->
<TextBlock Text="Word:" VerticalAlignment="Center" Margin="10,0,0,0" Grid.Row="1"/>
<TextBox Name="Search" Grid.ColumnSpan="2" Margin="50,2,10,2"/>
<!--Row 2-->
<ListBox Name="Words" Grid.Row="2" Margin="10"/>
</Grid>
I want make below
<TextBlock Text="Word" Grid.Row="RowSearchWord"/>
Disclaimer: This answer is kind of a self-advertisement within the constraints alluded to by this meta post. It advertises a free open source project that I (at the time of writing this) do not earn any money with. The only gain is the knowledge that my time for writing the described control was not wasted if it helps some future visitors of this SO question.
I had exactly the same thoughts. That is why, not too long ago, I wrote a custom grid class that uses named columns and rows.
I put it on Codeplex under the MIT license: Name-Based Grid project
With that control, you can rewrite your Xaml source code as follows:
<nbg:NameBasedGrid>
<nbg:NameBasedGrid.RowDefinitions>
<nbg:ColumnOrRow Name="RowDictTitle" Height="27"/>
<nbg:ColumnOrRow Name="RowSearchWord" Height="27"/>
<nbg:ColumnOrRow Name="List"/>
<nbg:ColumnOrRow Height="50"/>
</nbg:NameBasedGrid.RowDefinitions>
<nbg:NameBasedGrid.ColumnDefinitions>
<nbg:ColumnOrRow Width="1*" Name="Left"/>
<nbg:ColumnOrRow Width="2*" Name="Right"/>
</nbg:NameBasedGrid.ColumnDefinitions>
<!--Row 1-->
<TextBlock Text="Word:" VerticalAlignment="Center" Margin="10,0,0,0" nbg:NameBasedGrid.Column="Left" nbg:NameBasedGrid.Row="RowSearchWord"/>
<TextBox Name="Search" nbg:NameBasedGrid.Column="Left" nbg:NameBasedGrid.Row="RowDictTitle" nbg:NameBasedGrid.ExtendToColumn="Right" Margin="50,2,10,2"/>
<!--Row 2-->
<ListBox Name="Words" nbg:NameBasedGrid.Column="Left" nbg:NameBasedGrid.Row="List" Margin="10"/>
</nbg:NameBasedGrid>
Advantage: You will be able to reference columns and rows (including column and row spans!) by name - no more counting of columns or rows, no more updating column or row spans when the layout changes.
Disadvantage: You will need to explicitly state names for all columns and rows, as numerical references are not supported at all in NameBasedGrid.
Nice idea but since the Grid.Row attached property is an integer this is not possible.
See http://msdn.microsoft.com/en-us/library/system.windows.controls.grid.row.aspx
However, it may possible to create a helper that takes the name of the grid row, finds the row object and returns its row index.
I was looking for the same thing. Could not find exacly what I was looking for so i came up with my own solution using attached properties.
I created a specialized grid with attached properties for RowName and ColumnName.
(In this example i only implemented RowName)
using System.Windows;
using System.Windows.Controls;
namespace GridNamedRows.CustomControl
{
public class MyGrid: Grid
{
public static readonly DependencyProperty RowNameProperty =
DependencyProperty.RegisterAttached(
"RowName",
typeof(string),
typeof(MyGrid),
new FrameworkPropertyMetadata(
"",
FrameworkPropertyMetadataOptions.AffectsParentArrange,
new PropertyChangedCallback(RowNameChanged)),
new ValidateValueCallback(IsStringNotNull));
private static bool IsStringNotNull(object value)
{
return (value as string) != null;
}
private static void RowNameChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
if (e.NewValue == null)
{
return;
}
if (!(d is UIElement)) return;
Grid parent = ((FrameworkElement)d).Parent as Grid;
if (parent == null) return;
//Find rowname
for (int i = 0; i < parent.RowDefinitions.Count; i++)
{
if (parent.RowDefinitions[i].Name == e.NewValue.ToString())
{
Grid.SetRow((UIElement)d, i);
break;
}
}
}
public static string GetRowName(DependencyObject target)
{
return (string)target.GetValue(RowNameProperty);
}
public static void SetRowName(DependencyObject target, string value)
{
target.SetValue(RowNameProperty, value);
}
}
}
It can be used in xaml like this.
<Window xmlns:CustomControl="clr-namespace:GridNamedRows.CustomControl" x:Class="GridNamedRows.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="MainWindow" Height="350" Width="525">
<CustomControl:MyGrid>
<Grid.RowDefinitions>
<RowDefinition Name="firstRow"/>
<RowDefinition Name="secondRow"/>
<RowDefinition Name="thirdRow"/>
</Grid.RowDefinitions>
<TextBox Text="one" CustomControl:MyGrid.RowName="secondRow"/>
<TextBox Text="two" Grid.Row="2"/>
<TextBox Text="three" CustomControl:MyGrid.RowName="firstRow"/>
</CustomControl:MyGrid>
</Window>
It does not display correctly in the designer but works in runtime.
Along the lines of the other answers I came up with this attached property solution that does not require using a custom Grid.
The code is largely redundant (for row & column) and can be used like this:
<Grid>
<Grid.RowDefinitions>
<RowDefinition x:Name="ThisRow"/>
<RowDefinition x:Name="ThatRow"/>
<RowDefinition x:Name="AnotherRow"/>
</Grid.RowDefinitions>
<TextBlock helpers:GridHelper.RowName="ThisRow" Text="..."/>
<TextBlock helpers:GridHelper.RowName="AnotherRow" Text="..."/>
<TextBlock helpers:GridHelper.RowName="ThatRow" Text="..."/>
</Grid>
GridHelper.cs:
public class GridHelper
{
public static string GetRowName(DependencyObject obj)
{
return (string)obj.GetValue(RowNameProperty);
}
public static void SetRowName(DependencyObject obj, string value)
{
obj.SetValue(RowNameProperty, value);
}
public static readonly DependencyProperty RowNameProperty =
DependencyProperty.RegisterAttached("RowName", typeof(string), typeof(GridHelper), new FrameworkPropertyMetadata(string.Empty, GridHelper.OnRowNamePropertyChanged));
public static void OnRowNamePropertyChanged(object sender, DependencyPropertyChangedEventArgs e)
{
var name = e.NewValue?.ToString();
if (string.IsNullOrEmpty(name)) return;
if (!(sender is FrameworkElement fe)) return;
if (!(fe.Parent is Grid grid)) return;
for (int i = 0; i < grid.RowDefinitions.Count; i++)
{
var rd = grid.RowDefinitions[i];
if (rd.Name.Equals(name))
{
Grid.SetRow(fe, i);
return;
}
}
throw new ArgumentException("Invalid RowName: " + name);
}
public static string GetColumnName(DependencyObject obj)
{
return (string)obj.GetValue(ColumnNameProperty);
}
public static void SetColumnName(DependencyObject obj, string value)
{
obj.SetValue(ColumnNameProperty, value);
}
public static readonly DependencyProperty ColumnNameProperty =
DependencyProperty.RegisterAttached("ColumnName", typeof(string), typeof(GridHelper), new FrameworkPropertyMetadata(string.Empty, GridHelper.OnColumnNamePropertyChanged));
public static void OnColumnNamePropertyChanged(object sender, DependencyPropertyChangedEventArgs e)
{
var name = e.NewValue?.ToString();
if (string.IsNullOrEmpty(name)) return;
if (!(sender is FrameworkElement fe)) return;
if (!(fe.Parent is Grid grid)) return;
for (int i = 0; i < grid.ColumnDefinitions.Count; i++)
{
var cd = grid.ColumnDefinitions[i];
if (cd.Name.Equals(name))
{
Grid.SetColumn(fe, i);
return;
}
}
throw new ArgumentException("Invalid ColumnName: " + name);
}
}
Note: This also may not work in the designer - I've never tried using it...
I would like to use a Window to display one (or optionally two) image(s) in a grid row.
This image could be quite big, so I set the Stretch property to "UniformToFill" and embed the grid into a scrollviewer.
My Image is of app. 800 x 400 px and if I try to load it into my window, it is not displayed in complete width (the horiz. scrollbar stops before the end of the image).
I would like the image(s) to fill the available window area, but to be able to scroll to see it completely. What is wrong?
Thanks for your help!
tabina
This is my code:
The .xaml:
<Window x:Class="Wpf.Dialogs.ImageBox"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="ImageBox" Topmost="True" WindowStartupLocation="CenterOwner" Width="800" Height="600">
<ScrollViewer VerticalScrollBarVisibility="Auto" HorizontalScrollBarVisibility="Auto" CanContentScroll="True">
<Grid x:Name="gridImages">
<Grid.RowDefinitions>
<RowDefinition Height="*" />
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<Image Grid.Row="0" x:Name="img1" Stretch="UniformToFill"/>
<Image Grid.Row="1" x:Name="img2" Stretch="UniformToFill"/>
</Grid>
</ScrollViewer>
</Window>
The code behind:
using System;
using System.IO;
using System.Windows;
using System.Windows.Media.Imaging;
namespace Wpf.Dialogs
{
public partial class ImageBox : Window
{
public ImageBox() : this("Image", string.Empty, 800, 600)
{
}
public ImageBox(string title, string image, int width, int height)
: this(title, new string[] { image }, width, height)
{
}
public ImageBox(string title, string[] images, int width, int height)
{
InitializeComponent();
this.Title = title;
this.Image = images;
}
public string[] Image
{
set
{
if (value != null)
{
var bim = CreateBitmap(value[0]);
this.img1.Source = bim;
if (value.Length == 2)
{
var bi = CreateBitmap(value[1]);
if (bi != null)
{
this.img2.Source = bi;
}
}
else
{
this.img2.Source = null;
}
}
}
}
private BitmapImage CreateBitmap(string file)
{
if (File.Exists(file))
{
var bmp = new BitmapImage();
bmp.BeginInit();
bmp.UriSource = new Uri(file);
bmp.CacheOption = BitmapCacheOption.OnLoad;
bmp.EndInit();
return bmp;
}
else
{
return null;
}
}
}
}
You can't use ScrollViewer and at the same time with Stretch="UniformToFill". Use Grid and Stretch="UniformToFill" or ScrollViewer and Grid.
I've got a StackPanel, and it's perfect for laying out columns that the user adds at runtime. But I'd like the columns to be resizable, and I was reading about the GridSplitter control. Here's what I'm wondering:
Is the GridSplitter the wpf replacement for the WinForms splitter? In other words, is this the de facto way to allow the users to resize regions of the window?
Does it only work inside of a Grid? If I have items inside a stackpanel or a dockpanel, can I still use a gridsplitter the way I used the splitter in WinForms?
If I have to use a Grid, how can I make it behave just like a StackPanel? (hope it doesn't come to that)
GridSplitter only works in a Grid and is the easiest way to allow users to resize controls.
What do you mean that you want your grid (with gridsplitters) to behave just like a stackpanel? A stackpanel will exactly fit each of its children while a grid with gridsplitters will leave it up to the user.
Below is a user control which allows items to be added as columns. Between columns are grid splitters. Users can click on Delete button to remove added columns and columns can be added using behind code. Let me know if that's what you were looking for.
User control SmartGrid XAML:
<UserControl x:Class="SmartGridDemo.SmartGrid"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<Grid Name="_grid">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
</Grid>
</UserControl>
User control SmartGrid code behind:
using System;
using System.Linq;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Media;
namespace SmartGridDemo
{
public partial class SmartGrid : UserControl
{
public SmartGrid()
{
InitializeComponent();
}
public void Add(UIElement child)
{
int columnIndex = _grid.ColumnDefinitions.Count();
_grid.ColumnDefinitions.Add(
new ColumnDefinition()
{
Width = new GridLength(columnIndex == 0 ? 0 :5)
});
GridSplitter gridSplitter =
new GridSplitter()
{
HorizontalAlignment = HorizontalAlignment.Stretch,
VerticalAlignment = VerticalAlignment.Stretch,
ResizeDirection = GridResizeDirection.Columns,
Background = Brushes.Black
};
_grid.Children.Add(gridSplitter);
Grid.SetColumn(gridSplitter, columnIndex);
Grid.SetRow(gridSplitter, 0);
Grid.SetRowSpan(gridSplitter, 2);
columnIndex++;
_grid.ColumnDefinitions.Add(new ColumnDefinition() { Width = GridLength.Auto });
Button button = new Button();
button.Content = "Delete";
button.Tag = new TagTuple() {Child = child, GridSplitter = gridSplitter};
button.Click += new RoutedEventHandler(DeleteButton_Click);
_grid.Children.Add(button);
Grid.SetColumn(button, columnIndex);
Grid.SetRow(button, 0);
_grid.Children.Add(child);
Grid.SetColumn(child, columnIndex);
Grid.SetRow(child, 1);
}
private void DeleteButton_Click(object sender, RoutedEventArgs e)
{
Button button = sender as Button;
int columnIndex = Grid.GetColumn(button);
TagTuple tagTuple = button.Tag as TagTuple;
_grid.Children.Remove(tagTuple.GridSplitter);
_grid.Children.Remove(tagTuple.Child);
_grid.Children.Remove(button as UIElement);
_grid.ColumnDefinitions.RemoveAt(_grid.ColumnDefinitions.Count() - 1);
_grid.ColumnDefinitions.RemoveAt(_grid.ColumnDefinitions.Count() - 1);
foreach (UIElement child in _grid.Children)
{
int columnIndexForChild = Grid.GetColumn(child);
if (columnIndexForChild > columnIndex)
{
Grid.SetColumn(child, columnIndexForChild - 2);
}
}
}
private class TagTuple
{
public GridSplitter GridSplitter { get; set; }
public UIElement Child { get; set; }
}
}
}
Demo code, add some text in the TextBox and hit Add button to add new columns, XAML:
<Window x:Class="SmartGridDemo.Window1"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:SmartGridDemo"
Title="SmartGridDemo" Height="300" Width="300">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<TextBox Name="_texBox" Grid.Row="0" Grid.Column="0" />
<Button Content="Add" Click="AddButton_Click" Grid.Row="0" Grid.Column="1" />
<local:SmartGrid x:Name="_smartGrid" Grid.Row="1" Grid.Column="0" Grid.ColumnSpan="2" />
</Grid>
</Window>
Demo, behind code:
using System;
using System.Windows;
using System.Windows.Controls;
namespace SmartGridDemo
{
public partial class Window1 : Window
{
public Window1()
{
InitializeComponent();
_smartGrid.Add(new TextBlock() { Text = "AAA" });
_smartGrid.Add(new TextBlock() { Text = "BBB" });
_smartGrid.Add(new TextBlock() { Text = "CCC" });
_smartGrid.Add(new TextBlock() { Text = "DDD" });
_smartGrid.Add(new TextBlock() { Text = "EEE" });
_smartGrid.Add(new TextBlock() { Text = "FFF" });
}
private void AddButton_Click(object sender, RoutedEventArgs e)
{
_smartGrid.Add(new TextBlock() { Text = _texBox.Text });
}
}
}
Here is an open source WPF SplitContainer:
[http://wpfsplitcontainer.codeplex.com/]
I creating a control for WPF, and I have a question for you WPF gurus out there.
I want my control to be able to expand to fit a resizable window.
In my control, I have a list box that I want to expand with the window. I also have other controls around the list box (buttons, text, etc).
I want to be able to set a minimum size on my control, but I want the window to be able to be sized smaller by creating scroll bars for viewing the control.
This creates nested scroll areas: One for the list box and a ScrollViewer wrapping the whole control.
Now, if the list box is set to auto size, it will never have a scroll bar because it is always drawn full size within the ScrollViewer.
I only want the control to scroll if the content can't get any smaller, otherwise I don't want to scroll the control; instead I want to scroll the list box inside the control.
How can I alter the default behavior of the ScrollViewer class? I tried inheriting from the ScrollViewer class and overriding the MeasureOverride and ArrangeOverride classes, but I couldn't figure out how to measure and arrange the child properly. It appears that the arrange has to affect the ScrollContentPresenter somehow, not the actual content child.
Any help/suggestions would be much appreciated.
I've created a class to work around this problem:
public class RestrictDesiredSize : Decorator
{
Size lastArrangeSize = new Size(double.PositiveInfinity, double.PositiveInfinity);
protected override Size MeasureOverride(Size constraint)
{
Debug.WriteLine("Measure: " + constraint);
base.MeasureOverride(new Size(Math.Min(lastArrangeSize.Width, constraint.Width),
Math.Min(lastArrangeSize.Height, constraint.Height)));
return new Size(0, 0);
}
protected override Size ArrangeOverride(Size arrangeSize)
{
Debug.WriteLine("Arrange: " + arrangeSize);
if (lastArrangeSize != arrangeSize) {
lastArrangeSize = arrangeSize;
base.MeasureOverride(arrangeSize);
}
return base.ArrangeOverride(arrangeSize);
}
}
It will always return a desired size of (0,0), even if the containing element wants to be bigger.
Usage:
<local:RestrictDesiredSize MinWidth="200" MinHeight="200">
<ListBox />
</local>
You problem arises, because Controls within a ScrollViewer have virtually unlimited space available. Therefore your inner ListBox thinks it can avoid scrolling by taking up the complete height necessary to display all its elements. Of course in your case that behaviour has the unwanted side effect of exercising the outer ScrollViewer too much.
The objective therefore is to get the ListBox to use the visible height within the ScrollViewer iff there is enough of it and a certain minimal height otherwise. To achieve this, the most direct way is to inherit from ScrollViewer and override MeasureOverride() to pass an appropriately sized availableSize (that is the given availableSize blown up to the minimal size instead of the "usual" infinity) to the Visuals found by using VisualChildrenCount and GetVisualChild(int).
I used Daniels solution. That works great. Thank you.
Then I added two boolean dependency properties to the decorator class: KeepWidth and KeepHeight. So the new feature can be suppressed for one dimension.
This requires a change in MeasureOverride:
protected override Size MeasureOverride(Size constraint)
{
var innerWidth = Math.Min(this._lastArrangeSize.Width, constraint.Width);
var innerHeight = Math.Min(this._lastArrangeSize.Height, constraint.Height);
base.MeasureOverride(new Size(innerWidth, innerHeight));
var outerWidth = KeepWidth ? Child.DesiredSize.Width : 0;
var outerHeight = KeepHeight ? Child.DesiredSize.Height : 0;
return new Size(outerWidth, outerHeight);
}
While I wouldn't recommend creating a UI that requires outer scroll bars you can accomplish this pretty easily:
<Window
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
>
<ScrollViewer HorizontalScrollBarVisibility="Auto"
VerticalScrollBarVisibility="Auto">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<ListBox Grid.Row="0" Grid.RowSpan="3" Grid.Column="0" MinWidth="200"/>
<Button Grid.Row="0" Grid.Column="1" Content="Button1"/>
<Button Grid.Row="1" Grid.Column="1" Content="Button2"/>
<Button Grid.Row="2" Grid.Column="1" Content="Button3"/>
</Grid>
</ScrollViewer>
</Window>
I don't really recommend this. WPF provides exceptional layout systems, like Grid, and you should try to allow the app to resize itself as needed. Perhaps you can set a MinWidth/MinHeight on the window itself to prevent this resizing?
Create a method in the code-behind that sets the ListBox's MaxHeight to the height of whatever control is containing it and other controls. If the Listbox has any controls/margins/padding above or below it, subtract their heights from the container height assigned to MaxHeight. Call this method in the main windows "loaded" and "window resize" event handlers.
This should give you the best of both worlds. You are giving the ListBox a "fixed" size that will cause it to scroll in spite of the fact that the main window has its own scrollbar.
for 2 ScrollViewer
public class ScrollExt: ScrollViewer
{
Size lastArrangeSize = new Size(double.PositiveInfinity, double.PositiveInfinity);
public ScrollExt()
{
}
protected override Size MeasureOverride(Size constraint)
{
base.MeasureOverride(new Size(Math.Min(lastArrangeSize.Width, constraint.Width),
Math.Min(lastArrangeSize.Height, constraint.Height)));
return new Size(0, 0);
}
protected override Size ArrangeOverride(Size arrangeSize)
{
if (lastArrangeSize != arrangeSize)
{
lastArrangeSize = arrangeSize;
base.MeasureOverride(arrangeSize);
}
return base.ArrangeOverride(arrangeSize);
}
}
code:
<ScrollViewer HorizontalScrollBarVisibility="Auto" VerticalScrollBarVisibility="Auto">
<Grid >
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<TextBlock Background="Beige" Width="600" Text="Example"/>
<Grid Grid.Column="1" x:Name="grid">
<Grid Grid.Column="1" Margin="25" Background="Green">
<local:ScrollExt HorizontalScrollBarVisibility="Auto" VerticalScrollBarVisibility="Auto">
<Grid Width="10000" Margin="25" Background="Red" />
</local:ScrollExt>
</Grid>
</Grid>
</Grid>
</ScrollViewer>
I ended up combining Daniels answer and Heiner's answer. I decided to post the entire solution to make it easier for people to adopt this if needed. Here's my decorator class:
public class RestrictDesiredSizeDecorator : Decorator
{
public static readonly DependencyProperty KeepWidth;
public static readonly DependencyProperty KeepHeight;
#region Dependency property setters and getters
public static void SetKeepWidth(UIElement element, bool value)
{
element.SetValue(KeepWidth, value);
}
public static bool GetKeepWidth(UIElement element)
{
return (bool)element.GetValue(KeepWidth);
}
public static void SetKeepHeight(UIElement element, bool value)
{
element.SetValue(KeepHeight, value);
}
public static bool GetKeepHeight(UIElement element)
{
return (bool)element.GetValue(KeepHeight);
}
#endregion
private Size _lastArrangeSize = new Size(double.PositiveInfinity, double.PositiveInfinity);
static RestrictDesiredSizeDecorator()
{
KeepWidth = DependencyProperty.RegisterAttached(
nameof(KeepWidth),
typeof(bool),
typeof(RestrictDesiredSizeDecorator));
KeepHeight = DependencyProperty.RegisterAttached(
nameof(KeepHeight),
typeof(bool),
typeof(RestrictDesiredSizeDecorator));
}
protected override Size MeasureOverride(Size constraint)
{
Debug.WriteLine("Measure: " + constraint);
var keepWidth = GetValue(KeepWidth) as bool? ?? false;
var keepHeight = GetValue(KeepHeight) as bool? ?? false;
var innerWidth = keepWidth ? constraint.Width : Math.Min(this._lastArrangeSize.Width, constraint.Width);
var innerHeight = keepHeight ? constraint.Height : Math.Min(this._lastArrangeSize.Height, constraint.Height);
base.MeasureOverride(new Size(innerWidth, innerHeight));
var outerWidth = keepWidth ? Child.DesiredSize.Width : 0;
var outerHeight = keepHeight ? Child.DesiredSize.Height : 0;
return new Size(outerWidth, outerHeight);
}
protected override Size ArrangeOverride(Size arrangeSize)
{
Debug.WriteLine("Arrange: " + arrangeSize);
if (_lastArrangeSize != arrangeSize)
{
_lastArrangeSize = arrangeSize;
base.MeasureOverride(arrangeSize);
}
return base.ArrangeOverride(arrangeSize);
}
}
and here's how I use it in the xaml:
<ScrollViewer>
<StackPanel Orientation="Vertical">
<Whatever />
<decorators:RestrictDesiredSizeDecorator MinWidth="100" KeepHeight="True">
<TextBox
Text="{Binding Comment, UpdateSourceTrigger=PropertyChanged}"
Height="Auto"
MaxHeight="360"
VerticalScrollBarVisibility="Auto"
HorizontalScrollBarVisibility="Auto"
AcceptsReturn="True"
AcceptsTab="True"
TextWrapping="WrapWithOverflow"
/>
</decorators:RestrictDesiredSizeDecorator>
<Whatever />
</StackPanel>
</ScrollViewer
The above creates a textbox that will grow vertically (until it hits MaxHeight) but will match the parent's width without growing the outer ScrollViewer. Resizing the window/ScrollViewer to less than 100 wide will force the outer ScrollViewer to show the horizontal scroll bars. Other controls with inner ScrollViewers can be used as well, including complex grids.