I want to have an image as the background of a grid. By default, the image should be right- and bottom-aligned. The image should also uniformly stretch to the height of the container. However, if the container gets resized such that the width is thinner than the size of the image, the image starts resizing to become smaller. Instead of this happening, I want to clip the image so that parts from the right start getting hidden.
Here is my image XAML right now:
<Image Source="blahblah.png"
HorizontalAlignment="Right"
VerticalAlignment="Bottom"
Stretch="Uniform" />
Here is the behavior I want described in a picture (the black border is the container size):
Try a mix of different panels, like this:
<Grid x:Name="mainGrid" >
<Grid x:Name="backgroundSpacer" HorizontalAlignment="Stretch"
MinWidth="{Binding ElementName=backgroundImage, Path=ActualWidth}" >
<StackPanel x:Name="backgroundPanel"
HorizontalAlignment="Right" Orientation="Horizontal" >
<Image x:Name="backgroundImage" Stretch="Fill"
Source="http://programming.enthuses.me/1.png" />
</StackPanel>
</Grid>
...
</Grid>
Here, we put the image in a horizontal (and right-aligned) StackPanel. The StackPanel stretches vertically by default, and a feature of the StackPanel is that it tells its child items that they have unlimited width. Therefore, the Image is always rendered as tall as it can be.
Next, we have a "backgroundSpacer" which normally stretches across the whole container grid, but through the MinWidth binding we make sure it is always wide enough to contain the whole image. If "mainGrid" gets too thin, "backgroundSpacer" gets clipped, and WPF's default clipping behavior clips off its right side.
I don't see a way either by just setting some "magic" properties. I would implement a panel that hosts your image and manages the desired behaviour (scaling and positioning). That's how the panel looks like:
public class ImagePanel : Panel
{
public ImagePanel ()
{
}
protected override Size MeasureOverride (Size availableSize)
{
if (InternalChildren.Count > 0)
{
FrameworkElement child = (FrameworkElement)InternalChildren[0];
var childMaxSize = new Size (double.PositiveInfinity, availableSize.Height);
child.Measure (childMaxSize);
}
return availableSize;
}
protected override Size ArrangeOverride (Size finalSize)
{
if (InternalChildren.Count > 0)
{
FrameworkElement child = (FrameworkElement)InternalChildren[0];
double x = finalSize.Width - child.DesiredSize.Width;
if (x < 0)
{
x = 0;
}
child.Arrange (new Rect (new Point (x, 0), child.DesiredSize));
}
return finalSize; // Returns the final Arranged size
}
}
Use it this way:
<local:ImagePanel>
<Image Source="Image.jpg" Fill="Uniform"/>
</local:ImagePanel>
Have you ever tried used a ViewBox Element? Put this on a Parent with only MaxWidth/Width defined and set the ClipToBounds to True. For more information about this Control (That helps a lot when we talking about resizing):
http://msdn.microsoft.com/pt-br/library/system.windows.controls.viewbox(v=vs.110).aspx
Related
I want to achieve a very well known behavior seen in the browser when you have an image to display that is larger then the monitor:
Originally, the image is displayed fitting inside the window area, and the mouse cursor is a magnifying glass with a "+" icon;
If you click, two things happen:
a. The image is displayed with its native pixel size;
b. Scroll bars appear;
I want this effect with a larger-than-screen UniformGrid. For that, I can use ViewBox. I have already got what I want putting the control inside a ViewBox with Stretch.Uniform property, and upon MouseLeftButtonDown event it toggles between Stretch.None and Stretch.Uniform, just like the large image in browser analogy, only without scroll bars.
Now if I add the ScrollViewer (ViewBox -> ScrollViewer -> UniformGrid), the effect doesn't work anymore, because the ScrollViewer always displays the (larger than window) MyUserControl with its native resolution, that is, clipped and with scroll bars activated, while I would like to alternate between this and a "fitting in ViewBox" version.
Here is how I get the resizing, but the ScrollViewer never displays:
<Viewbox x:Name="vbox" Stretch="None">
<ScrollViewer x:Name="scroll" VerticalScrollBarVisibility="Auto" HorizontalScrollBarVisibility="Auto" >
<UniformGrid x:Name="ugrid" Columns="2" MouseLeftButtonDown="UniformGrid_MouseLeftButtonDown">
<local:AtlasMasculinoAnterior/>
<local:AtlasMasculinoPosterior/>
</UniformGrid>
</ScrollViewer>
</Viewbox>
And if change the order, then the Scroll bars always display and the zoom doesn't toggle upon mouse click (although the event fires):
<ScrollViewer x:Name="scroll" VerticalScrollBarVisibility="Auto" HorizontalScrollBarVisibility="Auto" >
<Viewbox x:Name="vbox" Stretch="None">
<UniformGrid x:Name="ugrid" Columns="2" MouseLeftButtonDown="UniformGrid_MouseLeftButtonDown">
<local:AtlasMasculinoAnterior/>
<local:AtlasMasculinoPosterior/>
</UniformGrid>
</Viewbox>
</ScrollViewer>
And here the code behind event:
private void UniformGrid_MouseLeftButtonDown(object sender, MouseButtonEventArgs e)
{
if (vbox.Stretch == Stretch.None)
{
vbox.Stretch = Stretch.Uniform;
}
else
vbox.Stretch = Stretch.None;
}
So what am I doing wrong, or what should I do so that the intended behavior works?
The way I see it, I would like to alternate between having the control in a ViewBox (Stretch.Uniform) and having the control inside a ScrollViewer, but I wonder how to have the same effect with both elements being part of the layout tree (one inside another), or even if I should, move the UniformGrid in and out of containers I would manipulate programmatically in code behind.
Got it to work in sort of a hackish way, by having a Grid with both a ViewBox and a ScrollViewer, and putting the UniformGrid inside one of them in XAML. Then, in code-behind, I programmatically detach the UniformGrid from its present container, and attach it to the other (using a boolean flag to control where it is, but that is debatable):
<Grid x:Name="grid">
<ScrollViewer x:Name="scroll" HorizontalScrollBarVisibility="Auto" VerticalScrollBarVisibility="Auto"/>
<Viewbox x:Name="viewbox" Stretch="Uniform">
<UniformGrid x:Name="ugrid" Columns="2" MouseLeftButtonDown="UniformGrid_MouseLeftButtonDown">
<local:AtlasMasculinoAnterior/>
<local:AtlasMasculinoPosterior/>
</UniformGrid>
</Viewbox>
</Grid>
and
bool atlasfullscreen = false;
private void UniformGrid_MouseLeftButtonDown(object sender, MouseButtonEventArgs e)
{
UniformGrid ug = sender as UniformGrid;
if (atlasfullscreen)
{
scroll.Content = null;
viewbox.Child = ug;
atlasfullscreen = false;
}
else
{
viewbox.Child = null;
scroll.Content = ug;
atlasfullscreen = true;
}
}
I had a similar use case where I had an item that I needed to alternate between Stretch.None and Stretch.Uniform, and when Stretch.None, I needed the scrollbars to be visible.
What I finally figured out was that when I set Stretch.None, I needed to set the ScrollViewer's Width & Height to the ViewBox's parent ActualWidth / Height, and when Stretch.Uniform, I needed to clear the ScollViewer's width and height.
So using your original XAML, plus the new Grid, here's the new XAML:
<Grid x:Name="grid">
<Viewbox x:Name="vbox"
Stretch="Uniform">
<ScrollViewer x:Name="scroll"
HorizontalScrollBarVisibility="Auto"
VerticalScrollBarVisibility="Auto">
<UniformGrid x:Name="ugrid"
Columns="2"
MouseLeftButtonDown="UniformGrid_MouseLeftButtonDown">
<local:AtlasMasculinoAnterior />
<local:AtlasMasculinoPosterior />
</UniformGrid>
</ScrollViewer>
</Viewbox>
</Grid>
New code behind:
private void UniformGrid_MouseLeftButtonDown(object sender, MouseButtonEventArgs e)
{
if (vbox.Stretch == Stretch.None)
{
vbox.Stretch = Stretch.Uniform;
scroll.Width = double.NaN;
scroll.Height = double.NaN;
}
else
{
vbox.Stretch = Stretch.None;
scroll.Width = grid.ActualWidth;
scroll.Height = grid.ActualHeight;
}
}
You might need to tweak the above example for how the Viewbox now being in a grid - but for my use case with similar XAML / code I got mine working without having to constantly move the child from the Viewbox to another control and back again.
So in summary: when Viewbox.Stretch = Uniform, set scrollviewer's width / height to double.NaN, and when Viewbox.Stretch = None, set scrollviewer's width / height to Viewbox.Parent.ActualWidth / Height.
I have a rectangle on a canvas that the user can resize, move and so on to make a selection.
I also have an image the size of the screen behind the canvas (basically a screenshot).
I'd like to translate the selection (the rectangle) in the canvas to a 1:1 selection in the image (I want the image directly behind the rectangle) given I have the rectangle's Canvas.Top, Canvas.Left, Width, Height.
<Grid Name="MainGrid" SnapsToDevicePixels="False" HorizontalAlignment="Stretch" VerticalAlignment="Stretch">
<Image x:Name="MainImage" Stretch="None" RenderOptions.BitmapScalingMode="HighQuality"/>
<Border Background="Black" Opacity="0.4" HorizontalAlignment="Stretch" VerticalAlignment="Stretch"/>
<Canvas Name="MainCanvas" Width="{Binding Source={x:Static SystemParameters.PrimaryScreenWidth}}" Height="{Binding Source={x:Static SystemParameters.PrimaryScreenHeight}}" Background="Transparent">
<ContentControl Name="SelectionRect" />
</ContentControl>
</Canvas>
</Grid>
I tried doing this: (MainImage is the image under the canvas)
Rect rect = new Rect(Canvas.GetLeft(SelectionRect), Canvas.GetTop(SelectionRect), SelectionRect.Width, SelectionRect.Height);
Rect from_rect = SelectionRect.TransformToVisual(this).TransformBounds(rect);
BitmapSource cropped_bitmap = new CroppedBitmap(MainImage.Source as BitmapSource,
new Int32Rect((int)from_rect.X, (int)from_rect.Y, (int)from_rect.Width, (int)from_rect.Height));
SelectionRectImageSource = cropped_bitmap;
But the image I get (SelectionRectImageSource) is a moved aside version of the actual pixels behind the selection rectangle.
So basically, I don't understand how these transformations work and how I should use them if at all.
Example:
Thanks a lot!
Dolev.
Looks like you need to correct for the DPI difference between the image (usually 72dpi) and the presentation source (usually 96dpi). Additionally, your first Rect should not be offset by Canvas.Left and Canvas.Top; TransformToVisual will take care of the relative offset for you.
var source = (BitmapSource)MainImage.Source;
var selectionRect = new Rect(SelectionRect.RenderSize);
var sourceRect = SelectionRect.TransformToVisual(MainImage)
.TransformBounds(selectionRect);
var xMultiplier = source.PixelWidth / MainImage.ActualWidth;
var yMultiplier = source.PixelHeight / MainImage.ActualHeight;
sourceRect.Scale(xMultiplier, yMultiplier);
var croppedBitmap = new CroppedBitmap(
source,
new Int32Rect(
(int)sourceRect.X,
(int)sourceRect.Y,
(int)sourceRect.Width,
(int)sourceRect.Height));
SelectionRectImageSource= croppedBitmap;
Depending on where this code resides, you may also need to transform the selection rectangle to MainImage instead of this (as I did).
Also, in case MainImage.Source is smaller than the actual MainImage control, you should probably set the horizontal and vertical alignments of MainImage to Left and Top, respectively, less your translated rectangle end up outside the bounds of the source image. You'll need to clamp the selection rectangle to the dimensions of MainImage too.
I have subclassed the image control (to display images at their true pixel size, regardless of resolution setting in metadata) and I want to implement scrolling and zooming, by using a ScaleTransform (within a LayoutTransform). This works fine at 100%, but when scaled the scrolling size stays at the size of the 100% image.
My XAML is:
<ScrollViewer Name="imgScrollViewer" VerticalScrollBarVisibility="Auto" HorizontalScrollBarVisibility="Auto">
<PL:DpiAgnosticImage x:Name="Ctrl_ImgMain" Stretch="None" HorizontalAlignment="Left" MouseWheel="Ctrl_ImgMain_MouseWheel" VerticalAlignment="Top" >
<Image.LayoutTransform>
<ScaleTransform ScaleX="{Binding ZoomLevel}" ScaleY="{Binding ZoomLevel}">
</ScaleTransform>
</Image.LayoutTransform>
</PL:DpiAgnosticImage>
</ScrollViewer>
The C# for my DpiAgnosticImage class is:
class DpiAgnosticImage : Image
{
protected override Size MeasureOverride(Size constraint)
{
var bitmapImage = Source as BitmapImage;
var desiredSize = bitmapImage == null
? base.MeasureOverride(constraint)
: new Size(bitmapImage.PixelWidth, bitmapImage.PixelHeight);
return desiredSize;
}
protected override Size ArrangeOverride(Size finalSize)
{
return new Size(Math.Round(DesiredSize.Width), Math.Round(DesiredSize.Height));
}
}
I've searched around but the closest that I have found to this problem suggests using LayoutTransform - which I already am.
I suspect that I need to implement something to change the scrollviewer size when the image is scaled, but I am struggling to find out what is required. Any suggestions would be appreciated.
Your derived image control works fine if you simply don't override ArrangeOverride.
Apparently the DesiredSize property gets the size calculated by MeasureOverride and subsequently transformed by LayoutTransform. This might by obvious during layout, although it doesn't seem to be well documented in the MSDN of DesiredSize and MeasureOverride.
Found this in Charles Petzold, "Applications=Code+Markup", p.831:
The DesiredSize property of an element's child reflects the
LayoutTransform, ...
Anyway, if you need to override ArrangeOverride for any other reason, simply return the value of the finalSize argument.
How do I configure a TextBox control to automatically resize itself vertically when text no longer fits on one line?
For example, in the following XAML:
<DockPanel LastChildFill="True" Margin="0,0,0,0">
<Border Name="dataGridHeader"
DataContext="{Binding Descriptor.Filter}"
DockPanel.Dock="Top"
BorderThickness="1"
Style="{StaticResource ChamelionBorder}">
<Border
Padding="5"
BorderThickness="1,1,0,0"
BorderBrush="{DynamicResource {ComponentResourceKey TypeInTargetAssembly=dc:NavigationPane,
ResourceId={x:Static dc:NavigationPaneColors.NavPaneTitleBorder}}}">
<StackPanel Orientation="Horizontal">
<TextBlock
Name="DataGridTitle"
FontSize="14"
FontWeight="Bold"
Foreground="{DynamicResource {ComponentResourceKey
TypeInTargetAssembly=dc:NavigationPane,
ResourceId={x:Static dc:NavigationPaneColors.NavPaneTitleForeground}}}"/>
<StackPanel Margin="5,0" Orientation="Horizontal"
Visibility="{Binding IsFilterEnabled, FallbackValue=Collapsed, Mode=OneWay, Converter={StaticResource BooleanToVisibility}}"
IsEnabled="{Binding IsFilterEnabled, FallbackValue=false}" >
<TextBlock />
<TextBox
Name="VerticallyExpandMe"
Padding="0, 0, 0, 0"
Margin="10,2,10,-1"
AcceptsReturn="True"
VerticalAlignment="Center"
Text="{Binding QueryString}"
Foreground="{DynamicResource {ComponentResourceKey
TypeInTargetAssembly=dc:NavigationPane,
ResourceId={x:Static dc:NavigationPaneColors.NavPaneTitleForeground}}}">
</TextBox>
</StackPanel>
</StackPanel>
</Border>
</Border>
</DockPanel>
The TextBox control named "VerticallyExpandMe" needs to automatically expand vertically when the text bound to it does not fit on one line. With AcceptsReturn set to true, TextBox expands vertically if I press enter within it, but I want it do do this automatically.
Although Andre Luus's suggestion is basically correct, it won't actually work here, because your layout will defeat text wrapping. I'll explain why.
Fundamentally, the problem is this: text wrapping only does anything when an element's width is constrained, but your TextBox has unconstrained width because it's a descendant of a horizontal StackPanel. (Well, two horizontal stack panels. Possibly more, depending on the context from which you took your example.) Since the width is unconstrained, the TextBox has no idea when it is supposed to start wrapping, and so it will never wrap, even if you enable wrapping. You need to do two things: constrain its width and enable wrapping.
Here's a more detailed explanation.
Your example contains a lot of detail irrelevant to the problem. Here's a version I've trimmed down somewhat to make it easier to explain what's wrong:
<StackPanel Orientation="Horizontal">
<TextBlock Name="DataGridTitle" />
<StackPanel
Margin="5,0"
Orientation="Horizontal"
>
<TextBlock />
<TextBox
Name="VerticallyExpandMe"
Margin="10,2,10,-1"
AcceptsReturn="True"
VerticalAlignment="Center"
Text="{Binding QueryString}"
>
</TextBox>
</StackPanel>
</StackPanel>
So I've removed your containing DockPanel and the two nested Border elements inside of that, because they're neither part of the problem nor relevant to the solution. So I'm starting at the pair of nested StackPanel elements in your example. And I've also removed most of the attributes because most of them are also not relevant to the layout.
This looks a bit weird - having two nested horizontal stack panels like this looks redundant, but it does actually make sense in your original if you need to make the nested one visible or invisible at runtime. But it makes it easier to see the problem.
(The empty TextBlock tag is also weird, but that's exactly as it appears in your original. That doesn't appear to be doing anything useful.)
And here's the problem: your TextBox is inside some horizontal StackPanel elements, meaning its width is unconstrained - you have inadvertently told the text box that it is free to grow to any width, regardless of how much space is actually available.
A StackPanel will always perform layout that is unconstrained in the direction of stacking. So when it comes to lay out that TextBox, it'll pass in a horizontal size of double.PositiveInfinity to the TextBox. So the TextBox will always think it has more space than it needs. Moreover, when a child of a StackPanel asks for more space than is actually available, the StackPanel lies, and pretends to give it that much space, but then crops it.
(This is the price you pay for the extreme simplicity of StackPanel - it's simple to the point of being bone-headed, because it will happily construct layouts that don't actually fit. You should only use StackPanel if either you really do have unlimited space because you're inside a ScrollViewer, or you are certain that you have sufficiently few items that you're not going to run out of space, or if you don't care about items running off the end of the panel when they get too large and you don't want the layout system to try to do anything more clever than simply cropping the content.)
So turning on text wrapping won't help here, because the StackPanel will always pretend that there's more than enough space for the text.
You need a different layout structure. Stack panels are the wrong thing to use because they will not impose the layout constraint you need to get text wrapping to kick in.
Here's a simple example that does roughly what you want:
<Grid VerticalAlignment="Top">
<DockPanel>
<TextBlock
x:Name="DataGridTitle"
VerticalAlignment="Top"
DockPanel.Dock="Left"
/>
<TextBox
Name="VerticallyExpandMe"
AcceptsReturn="True"
TextWrapping="Wrap"
Text="{Binding QueryString}"
>
</TextBox>
</DockPanel>
</Grid>
If you create a brand new WPF application and paste that in as the content of the main window, you should find it does what you want - the TextBox starts out one line tall, fills the available width, and if you type text in, it'll grow one line at a time as you add more text.
Of course, layout behaviour is always sensitive to context, so it may not be enough to just throw that into the middle of your existing application. That will work if pasted into a fixed-size space (e.g. as the body of a window), but will not work correctly if you paste it into a context where width is unconstrained. (E.g., inside a ScrollViewer, or inside a horizontal StackPanel.)
So if this doesn't work for you, it'll be because of other things wrong elsewhere in your layout - possibly yet more StackPanel elements elsewhere. From the look of your example, it's probably worth spending some time thinking about what you really need in your layout and simplifying it - the presence of negative margins, and elements that don't appear to do anything like that empty TextBlock are usually indicative of an over-complicated layout. And unnecessary complexity in a layout makes it much hard to achieve the effects you're looking for.
Alternatively, you could constrain your TextBlock's Width by binding it to a parent's ActualWidth, for example:
<TextBlock Width="{Binding ElementName=*ParentElement*, Path=ActualWidth}"
Height="Auto" />
This will force it to resize its height automatically too.
Use MaxWidth and TextWrapping="WrapWithOverflow".
I'm using another simple approach that allows me not to change the document layout.
The main idea is not to set the control Width before it starts changing. For TextBoxes, I handle the SizeChanged event:
<TextBox TextWrapping="Wrap" SizeChanged="TextBox_SizeChanged" />
private void TextBox_SizeChanged(object sender, SizeChangedEventArgs e)
{
FrameworkElement box = (FrameworkElement)sender;
if (e.PreviousSize.Width == 0 || box.Width < e.PreviousSize.Width)
return;
box.Width = e.PreviousSize.Width;
}
You can use this class which extends TextBlock. It does auto-shrinking and takes MaxHeight / MaxWidth into consideration:
public class TextBlockAutoShrink : TextBlock
{
private double _defaultMargin = 6;
private Typeface _typeface;
static TextBlockAutoShrink()
{
TextBlock.TextProperty.OverrideMetadata(typeof(TextBlockAutoShrink), new FrameworkPropertyMetadata(new PropertyChangedCallback(TextPropertyChanged)));
}
public TextBlockAutoShrink() : base()
{
_typeface = new Typeface(this.FontFamily, this.FontStyle, this.FontWeight, this.FontStretch, this.FontFamily);
base.DataContextChanged += new DependencyPropertyChangedEventHandler(TextBlockAutoShrink_DataContextChanged);
}
private static void TextPropertyChanged(DependencyObject sender, DependencyPropertyChangedEventArgs args)
{
var t = sender as TextBlockAutoShrink;
if (t != null)
{
t.FitSize();
}
}
void TextBlockAutoShrink_DataContextChanged(object sender, DependencyPropertyChangedEventArgs e)
{
FitSize();
}
protected override void OnRenderSizeChanged(SizeChangedInfo sizeInfo)
{
FitSize();
base.OnRenderSizeChanged(sizeInfo);
}
private void FitSize()
{
FrameworkElement parent = this.Parent as FrameworkElement;
if (parent != null)
{
var targetWidthSize = this.FontSize;
var targetHeightSize = this.FontSize;
var maxWidth = double.IsInfinity(this.MaxWidth) ? parent.ActualWidth : this.MaxWidth;
var maxHeight = double.IsInfinity(this.MaxHeight) ? parent.ActualHeight : this.MaxHeight;
if (this.ActualWidth > maxWidth)
{
targetWidthSize = (double)(this.FontSize * (maxWidth / (this.ActualWidth + _defaultMargin)));
}
if (this.ActualHeight > maxHeight)
{
var ratio = maxHeight / (this.ActualHeight);
// Normalize due to Height miscalculation. We do it step by step repeatedly until the requested height is reached. Once the fontsize is changed, this event is re-raised
// And the ActualHeight is lowered a bit more until it doesnt enter the enclosing If block.
ratio = (1 - ratio > 0.04) ? Math.Sqrt(ratio) : ratio;
targetHeightSize = (double)(this.FontSize * ratio);
}
this.FontSize = Math.Min(targetWidthSize, targetHeightSize);
}
}
}
Here's a trivial example of the problem I'm having:
<StackPanel Orientation="Horizontal">
<Label>Foo</Label>
<TextBox>Bar</TextBox>
<ComboBox>
<TextBlock>Baz</TextBlock>
<TextBlock>Bat</TextBlock>
</ComboBox>
<TextBlock>Plugh</TextBlock>
<TextBlock VerticalAlignment="Bottom">XYZZY</TextBlock>
</StackPanel>
Every one of those elements except the TextBox and ComboBox vertically position the text they contain differently, and it looks plain ugly.
I can line the text in these elements up by specifying a Margin for each. That works, except that the margin is in pixels, and not relative to the resolution of the display or the font size or any of the other things that are going to be variable.
I'm not even sure how I'd calculate the correct bottom margin for a control at runtime.
What's the best way to do this?
The problem
So as I understand it the problem is that you want to lay out controls horizontally in a StackPanel and align to the top, but have the text in each control line up. Additionally, you don't want to have to set something for every control: either a Style or a Margin.
The basic approach
The root of the problem is that different controls have different amounts of "overhead" between the boundary of the control and the text within. When these controls are aligned at the top, the text within appears in different locations.
So what we want to do is apply an vertical offset that's customized to each control. This should work for all font sizes and all DPIs: WPF works in device-independent measures of length.
Automating the process
Now we can apply a Margin to get our offset, but that means we need to maintain this on every control in the StackPanel.
How do we automate this? Unfortunately it would be very difficult to get a bulletproof solution; it's possible to override a control's template, which would change the amount of layout overhead in the control. But it's possible to cook up a control that can save a lot of manual alignment work, as long as we can associate a control type (TextBox, Label, etc) with a given offset.
The solution
There are several different approaches you could take, but I think that this is a layout problem and needs some custom Measure and Arrange logic:
public class AlignStackPanel : StackPanel
{
public bool AlignTop { get; set; }
protected override Size MeasureOverride(Size constraint)
{
Size stackDesiredSize = new Size();
UIElementCollection children = InternalChildren;
Size layoutSlotSize = constraint;
bool fHorizontal = (Orientation == Orientation.Horizontal);
if (fHorizontal)
{
layoutSlotSize.Width = Double.PositiveInfinity;
}
else
{
layoutSlotSize.Height = Double.PositiveInfinity;
}
for (int i = 0, count = children.Count; i < count; ++i)
{
// Get next child.
UIElement child = children[i];
if (child == null) { continue; }
// Accumulate child size.
if (fHorizontal)
{
// Find the offset needed to line up the text and give the child a little less room.
double offset = GetStackElementOffset(child);
child.Measure(new Size(Double.PositiveInfinity, constraint.Height - offset));
Size childDesiredSize = child.DesiredSize;
stackDesiredSize.Width += childDesiredSize.Width;
stackDesiredSize.Height = Math.Max(stackDesiredSize.Height, childDesiredSize.Height + GetStackElementOffset(child));
}
else
{
child.Measure(layoutSlotSize);
Size childDesiredSize = child.DesiredSize;
stackDesiredSize.Width = Math.Max(stackDesiredSize.Width, childDesiredSize.Width);
stackDesiredSize.Height += childDesiredSize.Height;
}
}
return stackDesiredSize;
}
protected override Size ArrangeOverride(Size arrangeSize)
{
UIElementCollection children = this.Children;
bool fHorizontal = (Orientation == Orientation.Horizontal);
Rect rcChild = new Rect(arrangeSize);
double previousChildSize = 0.0;
for (int i = 0, count = children.Count; i < count; ++i)
{
UIElement child = children[i];
if (child == null) { continue; }
if (fHorizontal)
{
double offset = GetStackElementOffset(child);
if (this.AlignTop)
{
rcChild.Y = offset;
}
rcChild.X += previousChildSize;
previousChildSize = child.DesiredSize.Width;
rcChild.Width = previousChildSize;
rcChild.Height = Math.Max(arrangeSize.Height - offset, child.DesiredSize.Height);
}
else
{
rcChild.Y += previousChildSize;
previousChildSize = child.DesiredSize.Height;
rcChild.Height = previousChildSize;
rcChild.Width = Math.Max(arrangeSize.Width, child.DesiredSize.Width);
}
child.Arrange(rcChild);
}
return arrangeSize;
}
private static double GetStackElementOffset(UIElement stackElement)
{
if (stackElement is TextBlock)
{
return 5;
}
if (stackElement is Label)
{
return 0;
}
if (stackElement is TextBox)
{
return 2;
}
if (stackElement is ComboBox)
{
return 2;
}
return 0;
}
}
I started from the StackPanel's Measure and Arrange methods, then stripped out references to scrolling and ETW events and added the spacing buffer needed based on the type of element present. The logic only affects horizontal stack panels.
The AlignTop property controls whether the spacing will make text align to the top or bottom.
The numbers needed to align the text may change if the controls get a custom template, but you don't need to put a different Margin or Style on each element in the collection. Another advantage is that you can now specify Margin on the child controls without interfering with the alignment.
Results:
<local:AlignStackPanel Orientation="Horizontal" AlignTop="True" >
<Label>Foo</Label>
<TextBox>Bar</TextBox>
<ComboBox SelectedIndex="0">
<TextBlock>Baz</TextBlock>
<TextBlock>Bat</TextBlock>
</ComboBox>
<TextBlock>Plugh</TextBlock>
</local:AlignStackPanel>
AlignTop="False":
That works, except that the margin is in pixels, and not relative to the resolution of the display or the font size or any of the other things that are going to be variable.
Your assumptions are incorrect. (I know, because I used to have the same assumptions and the same concerns.)
Not actually pixels
First of all, the margin isn't in pixels. (You already think I'm crazy, right?) From the docs for FrameworkElement.Margin:
The default unit for a Thickness measure is device-independent unit (1/96th inch).
I think previous versions of the documentation tended to call this a "pixel" or, later, a "device-independent pixel". Over time, they've come to realize that this terminology was a huge mistake, because WPF doesn't actually do anything in terms of physical pixels -- they were using the term to mean something new, but their audience was assuming it meant what it always had. So now the docs tend to avoid the confusion by shying away from any reference to "pixels"; they now use "device-independent unit" instead.
If your computer's display settings are set to 96dpi (the default Windows setting), then these device-independent units will correspond one-to-one with pixels. But if you've set your display settings to 120dpi (called "large fonts" in previous versions of Windows), your WPF element with Height="96" will actually be 120 physical pixels high.
So your assumption that the margin will "not [be] relative to the resolution of the display" is incorrect. You can verify this yourself by writing your WPF app, then switching to 120dpi or 144dpi and running your app, and observing that everything still lines up. Your concern that the margin is "not relative to the resolution of the display" turns out to be a non-issue.
(In Windows Vista, you switch to 120dpi by right-clicking the desktop > Personalize, and clicking the "Adjust font size (DPI)" link in the sidebar. I believe it's something similar in Windows 7. Beware that this requires a reboot every time you change it.)
Font size doesn't matter
As for the font size, that's also a non-issue. Here's how you can prove it. Paste the following XAML into Kaxaml or any other WPF editor:
<StackPanel Orientation="Horizontal" VerticalAlignment="Top">
<ComboBox SelectedIndex="0">
<TextBlock Background="Blue">Foo</TextBlock>
</ComboBox>
<ComboBox SelectedIndex="0" FontSize="100pt">
<TextBlock Background="Blue">Foo</TextBlock>
</ComboBox>
</StackPanel>
Observe that the thickness of the ComboBox chrome is not affected by the font size. The distance from the top of the ComboBox to the top of the TextBlock is exactly the same, whether you're using the default font size or a totally extreme font size. The combobox's built-in margin is constant.
It doesn't even matter if you use different fonts, as long as you use the same font for both the label and the ComboBox content, and the same font size, font style, etc. The tops of the labels will line up, and if the tops line up, the baselines will too.
So yes, use margins
I know, it sounds sloppy. But WPF doesn't have built-in baseline alignment, and margins are the mechanism they gave us to deal with this sort of problem. And they made it so margins would work.
Here's a tip. When I was first testing this, I wasn't convinced that the combobox's chrome would correspond exactly to a 3-pixel top margin -- after all, many things in WPF, including and especially font sizes, are measured in exact, non-integral sizes and then snapped to device pixels -- how could I know that things wouldn't be misaligned at 120dpi or 144dpi screen settings due to rounding?
The answer turns out to be easy: you paste a mockup of your code into Kaxaml, and then you zoom in (there's a zoom slider bar in the lower left of the window). If everything still lines up even when you're zoomed in, then you're okay.
Paste the following code into Kaxaml, and then start zooming in, to prove to yourself that margins really are the way to go. If the red overlay lines up with the top of the blue labels at 100% zoom, and also at 125% zoom (120dpi) and 150% zoom (144dpi), then you can be pretty sure it'll work with anything. I've tried it, and in the case of ComboBox, I can tell you that they did use an integral size for the chrome. A top margin of 3 will get your label to line up with the ComboBox text every time.
(If you don't want to use Kaxaml, you can just add a temporary ScaleTransform to your XAML to scale it to 1.25 or 1.5, and make sure things still line up. That will work even if your preferred XAML editor doesn't have a zoom feature.)
<Grid>
<StackPanel Orientation="Horizontal" VerticalAlignment="Top">
<TextBlock Background="Blue" VerticalAlignment="Top" Margin="0 3 0 0">Label:</TextBlock>
<ComboBox SelectedIndex="0">
<TextBlock Background="Blue">Combobox</TextBlock>
</ComboBox>
</StackPanel>
<Rectangle Fill="#6F00" Height="3" VerticalAlignment="Top"/>
</Grid>
At 100%:
At 125%:
At 150%:
They always line up. Margins are the way to go.
Every UIElement have some internal padding attached to it which is different for label,textblock and any other control. I think setting padding for each control will do for you.
**
Margin specifies space relative to
other UIElement in pixels which may
not be consistent on resizing or any
other operation whereas padding is
internal for each UIElement which will
remain unaffected on resizing of
window.
**
<StackPanel Orientation="Horizontal">
<Label Padding="10">Foo</Label>
<TextBox Padding="10">Bar</TextBox>
<ComboBox Padding="10">
<TextBlock>Baz</TextBlock>
<TextBlock>Bat</TextBlock>
</ComboBox>
<TextBlock Padding="10">Plugh</TextBlock>
<TextBlock Padding="10" VerticalAlignment="Bottom">XYZZY</TextBlock>
</StackPanel>
Here, i provide an internal uniform padding of size 10 to every control, you can always play with it to change it with respect to left,top,right,bottom padding sizes.
See the above attached screenshots for reference (1) Without Padding and (2) With Padding
I hope this might be of any help...
VerticalContentAlignment & HorizontalContentAlignment, then specify padding and margin of 0 for each child control.
How I ended up solving this was to use fixed-size margins and padding.
The real problem that I was having was that I was letting users change the font size within the application. This seemed like a good idea to someone who was coming to this problem from the perspective of Windows Forms. But it screwed up all of the layout; margins and padding that looked just fine with 12pt text looked terrible with 36pt text.
From a WPF perspective, though, a much easier (and better) way to accomplish what I was really trying for - an UI whose size the user could adjust to suit his/her taste - was to just put a ScaleTransform over the view, and bind its ScaleX and ScaleY to the value of a slider.
This not only gives users much more fine-grained control over the size of their UI, it also means that all of the alignment and tweaking done to get things lined up correctly still works irrespective of the size of the UI.
May be this will help:
<Window x:Class="Wpfcrm.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:Wpfcrm"
mc:Ignorable="d"
Title="Business" Height="600" Width="1000" WindowStartupLocation="CenterScreen" ResizeMode="NoResize">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="434*"/>
<ColumnDefinition Width="51*"/>
<ColumnDefinition Width="510*"/>
</Grid.ColumnDefinitions>
<StackPanel x:Name="mainPanel" Orientation="Vertical" Grid.ColumnSpan="3">
<StackPanel.Background>
<RadialGradientBrush>
<GradientStop Color="Black" Offset="0"/>
<GradientStop Color="White"/>
<GradientStop Color="White"/>
</RadialGradientBrush>
</StackPanel.Background>
<DataGrid Name="grdUsers" ColumnWidth="*" Margin="0,-20,0,273" Height="272">
</DataGrid>
</StackPanel>
<StackPanel Orientation="Horizontal" Grid.ColumnSpan="3">
<TextBox Name="txtName" Text="Name" Width="203" Margin="70,262,0,277"/>
<TextBox x:Name="txtPass" Text="Pass" Width="205" Margin="70,262,0,277"/>
<TextBox x:Name="txtPosition" Text="Position" Width="205" Margin="70,262,0,277"/>
</StackPanel>
<StackPanel Orientation="Vertical" VerticalAlignment="Bottom" Height="217" Grid.ColumnSpan="3" Margin="263,0,297,0">
<Button Name="btnUpdate" Content="Update" Height="46" FontSize="24" FontWeight="Bold" FontFamily="Comic Sans MS" Margin="82,0,140,0" BorderThickness="1">
<Button.Background>
<LinearGradientBrush EndPoint="0.5,1" StartPoint="0.5,0">
<GradientStop Color="Black"/>
<GradientStop Color="#FF19A0AE" Offset="0.551"/>
</LinearGradientBrush>
</Button.Background>
</Button>
</StackPanel>
</Grid>
</Window>
This is tricky as ComboBox and TextBlock have different internal margins. In such circumstances, I always left everything to have VerticalAlignment as Center that does not look very great but yes quite acceptable.
Alternative is you create your own CustomControl derived from ComboBox and initialize its margin in constructor and reuse it everywhere.