Large TextBlock works much faster when placed inside a ViewBox - wpf

Accidentialy found a strange behaviour. A large TextBlock, containing 100k lines, is rendered very slow, resizing and scrolling it takes about a second. But if this TextBlock is placed within a ViewBox with Stretch="None" it is scrolled and resized quite fast.
Something in background is definitely changed with a ViewBox, but what and why?
Window contents
<Grid>
<ScrollViewer>
<Viewbox Stretch="None">
<TextBlock x:Name="TextContainer" HorizontalAlignment="Left" TextAlignment="Left" TextWrapping="Wrap" VerticalAlignment="Center"/>
</Viewbox>
</ScrollViewer>
</Grid>
Code behind, just creating some text
public MainWindow()
{
InitializeComponent();
StringBuilder sb = new StringBuilder();
for(int i = 1; i < 100000; i++)
{
sb.AppendLine($"Line #{i} ABCDEFGHIJKLMNOPQRSTUVWXYZ ABCDEFGHIJKLMNOPQRSTUVWXYZ ABCDEFGHIJKLMNOPQRSTUVWXYZ");
}
this.TextContainer.Text = sb.ToString();
}
Presence of a ScrollView have no effect it is still slow without a ViewBox and fast with it. If I change TextBlock to a TextBox with the same content performance becomes fast in all cases. So it is TextBox specific.
Little update: Clarification. I do not need to display large text amounts with a TextBlock, TextBox or [insert whatever you want]. All I want, is to understand, why it behaves like this? Perhaps this knowledge will help me to find solutions to later quiestions, avoid some troubles or just quench my thirst for knowledge. While I appreciate advices on performance optimizations, it does not answer the question.

When you use a ViewBox, the layout and the rendering is done only once, then, it's only an image that is clipped by the ScrollViewer.
Without the ViewBox, the layout is recalculated at each change (scroll, resize, ...). Which causes a slowdown. Maybe it's possible do disable some virtualization but I'm not sure.

Related

What's better for a WPF game-using a grid or a canvas?

I want to make a game using WPF and I'm not sure what should I use, a grid or a canvas? I know that in a grid everything gets changed once I change the window size mid run, I don't know if I can bypass that and maybe I would even want to make a full screen option, nevertheless, it's not an adventure game just something like Tom The Talking Cat,where you take care of a character and buy things for it. What do you think I should use?(p.s I'm sorry that my English is bad and I'm kind of new in programming so I might sound a bit ignorant but I'm trying to learn to get better)
Any layout which can be done with Canvas, can also be done with Grid.
E.g. take a Canvas, a Grid with single RowDefinition and ColumnDefinition, and some UIElement (Border). The Border can be positioned in Canvas using Top and Left properties, and in the same location of Grid using Margin property:
<Canvas>
<Border Canvas.Left="20" Canvas.Top="10" Width="50" Height="50" Background="Purple"/>
</Canvas>
<Grid>
<Border Margin="20,10,0,0" Width="50" Height="50" Background="Purple"/>
</Grid>
To have more advanced functionality you can even implement your own type of Panel:
public class BraveNewPanel: Panel
{
protected override Size MeasureOverride(Size availableSize)
{
// measure children
}
protected override Size ArrangeOverride(Size finalSize)
{
// arrange children
}
}
Canvas, then you have complete control over everything. If you look at Grid it can interfere with things.

Disable ScrollViewer VerticalScrollBarVisible if content fits

I am currently writing my very first Windows Phone (8) App which also is my very first Xaml Application. So it is likely I just did not find the solution for my problem on my own, because I don't know which words to feed google. I tried, but found nothing useful. I found that one, but it does not help:
How to disable "scroll compression" in ScrollViewer
Here is the important part of my XAML:
<ScrollViewer VerticalScrollBarVisibility="Auto">
<StackPanel VerticalAlignment="Top">
<TextBlock x:Name="InfoText" TextWrapping="Wrap" VerticalAlignment="Top" Text="VersionInfoText"/>
</StackPanel>
</ScrollViewer>
I will programmatically change the content of my TextBlock InfoText. The text might be short enough to fit in completely, or it might be rather long. That is why I embedded it into a ScrollViewer. (By the way, there will be further Controls added to the StackPanel later.)
The ScrollViewer produces these "overbounce" effects if it cannot scroll any further. That is nice if the text is large, but when there is nothing to scroll I don't want this effect to be visilbe.
I tried VerticelScrollBarVisibility="Disable", which successfully disables the effect. Now my question:
Can I automatically (by XAML-Magic) switch between Auto and Disable depending on the Height of my StackPanel and the Hight of my ScrollViewer?
I was hoping Auto would do the trick, but it does not (tested in the VS2013 Emulator WVGA).
In VS2013 setting VerticalScrollBarVisibility="Auto" worked for me.
Try adding this attribute to your ScrollViewer
VerticalScrollMode="Auto"
Also try disabling the HorizontalScrollMode and HorizontalScrollBarVisiblity attributes.
Let me know if this doesn't work. I will then have to make a sample app to see if I can make that work for you. Right now I am just guessing. Try it.
You could dynamically set the SetVerticalScrollBarVisibility to Disabled depends on your InfoText length in your cs code...
if(InfoText.Length() >n)
{
ScrollViewer.SetVerticalScrollBarVisibility(scrollViewer, ScrollBarVisibility.Auto);
}
else
{
ScrollViewer.SetVerticalScrollBarVisibility(scrollViewer, ScrollBarVisibility.Disabled);
}
You can check if TextBlock height is greater than the height of the ScrollViewer.
In xaml:
<ScrollViewer x:Name="TestScrollViewer">
<TextBlock x:Name="InfoText"
Text="Information"
TextWrapping="Wrap"
VerticalAlignment="Top" />
</ScrollViewer>
In cs:
public MainPage()
{
InitializeComponent();
Loaded += (sender, args) =>
{
TestScrollViewer.IsEnabled = InfoText.ActualHeight > TestScrollViewer.ActualHeight;
// OR
TestScrollViewer.VerticalScrollBarVisibility = InfoText.ActualHeight > TestScrollViewer.ActualHeight
? ScrollBarVisibility.Visible
: ScrollBarVisibility.Disabled;
};
}

Problem with TextBlock in ScrollViewer

I'm writing a WP7 app and on a certain page I have a TextBlock with textwrapping. This TextBlock gets its text through binding. I placed a ScrollViewer around the TextBlock for when there are too many lines of text. Here's an example:
<ScrollViewer Margin="0,128,0,0" Name="Scroller">
<TextBlock x:Name="ItemContent" TextWrapping="Wrap" Text="{Binding Content}" HorizontalAlignment="Stretch" VerticalAlignment="Stretch" />
</ScrollViewer>
Now when I scroll to the end of the text, I still can scroll down half a page or more. I have trimmed my text both sides, so that's not the problem either. Is there a way to fix this?
Thanks!
There is limitation on all UIElements that they can't be more than 2048px in either dimension. This is to avoid excessive memory use when creating much more UI than can fit on the screen at once.
I'd advice splitting the text over multiple TextBlocks.
One alternative is to embed the text in a WebBrowser control but this can provide a substandard user experience.
For an example on alternative method see http://blogs.msdn.com/b/priozersk/archive/2010/09/08/creating-scrollable-textblock-for-wp7.aspx

How do I configure a TextBox control to automatically resize itself vertically when text no longer fits on one line?

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);
}
}
}

How can I make elements arranged in a horizontal StackPanel share a common baseline for their text content?

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.

Resources