I am trying to write a menu item control that will AutoSize based on the length of text it contains (like the Label control).
To do this, I overrided the GetPreferredSize method to calculate the length of the text:
public override Size GetPreferredSize(Size proposedSize)
{
Size size = TextRenderer.MeasureText(this.Text, this.Font);
int w = size.Width + this.Padding.Left + this.Padding.Right;
int h = size.Height + this.Padding.Top + this.Padding.Bottom;
return new Size(w, h);
}
I then add a bunch of these controls to a containing menu control and try to position them based on the size above:
if (item.AutoSize)
{
item.Size = item.PreferredSize;
}
item.Left = _Left;
item.Top = _Top;
if (this.MenuOrientation == Orientation.Vertical)
{
_Top += item.Size.Height;
}
else
{
_Left += item.Size.Width;
}
this.Controls.Add(item);
However, the sizes returned by PreferredSize and GetPreferredSize aren't the same. For one string, GetPreferredSize returns {Width=147, Height=27}, but PreferredSize returns {Width=105, Height=21}. Because of this the controls overlap instead of appearing beside one another.
I tried overriding MinimumSize instead of GetPreferredSize, but that also got scaled down from what I calculated.
So my question is, what is the correct way to do this? I'd also like to understand the way that AutoSize, PreferredSize, MinimumSize, and MaximumSize are meant to interact. MSDN is little help on this.
Related
I created a code that works, but I'm not sure that it's the best way to place an Image scaled automatically to the available width space. I need to put some content over that image, so I have a LayeredLayout: in the first layer there is the Label created with the following code, on the second layer there is a BorderLayout that has the same size of the Image.
Is the following code fine or is it possible to do better?
Label background = new Label(" ", "NoMarginNoPadding") {
boolean onlyOneTime = false;
#Override
public void paint(Graphics g) {
int labelWidth = this.getWidth();
int labelHeight = labelWidth * bgImage.getHeight() / bgImage.getWidth();
this.setPreferredH(labelHeight);
if (!onlyOneTime) {
onlyOneTime = true;
this.getParent().revalidate();
}
super.paint(g);
}
};
background.getAllStyles().setBackgroundType(Style.BACKGROUND_IMAGE_SCALED_FIT);
background.getAllStyles().setBgImage(bgImage);
Shorter code:
ScaleImageLabel sl = new ScaleImageLabel(bgImage);
sl.setUIID("Container");
You shouldn't override paint to set the preferred size. You should have overriden calcPreferredSize(). For ScaleImageLabel it's already set to the natural size of the image which should be pretty big.
Consider this XAML snippet...
<DockPanel x:Name="TestDockPanel">
<Button x:Name="RightButton" Content="Right" DockPanel.Dock="Right" />
<Button x:Name="FillButton" Content="Fill" />
</DockPanel>
As written, the DockPanel will layout 'RightButton' to the right, then fill the rest of the area with 'FillButton' like this...
We're trying to find a way to style it so that when 'FillButton' has its visibility changed to 'Collapsed', 'RightButton' should now fill the area, like this...
The only way we know how to do this is to physically remove 'FillButton' from the children of 'TestDockPanel' but that requires code-behind, which we're trying to avoid.
Update
Below in an answer, I've posed a solution based on a subclass. However, I'm leaving this open since I'd like something that can be used with any DockPanel (or other subclass) and preferrably applied via a style or an attached behavior. Also, to be clear, a requirement of the solution is that it must be based on a DockPanel, not a grid or other panel.
I created a solution in the form of a DockPanel subclass, but I'm not marking this as the accepted answer because I'm hoping to still find a way to do this via styles or attached behaviors so it can be used with any DockPanel (or other subclass), not just this.
Still, for others, this may be helpful so I'm posting it here.
The code for the full class is below. The meat of the work is in the ArrangeOverride method, which was based on the original logic from DockPanel as extracted from Reflector.
The way the existing logic worked was inside ArrangeOverride, if LastChildFill was set it stored the index of the last child (i.e. the index of the item to be filled) in a variable. If LastChildFill wasn't set, it instead stored 'count' in that variable.
Then when looping through the children performing the actual arrange, if the element being arranged had an index less than the previously stored index, it performed the 'docking' logic, otherwise it performed 'fill' logic.
That meant when LastChildFill was false, every element ran the 'docking' logic since they all had an index below that stored index (which again equals 'count', or highest index + 1). However, when LastChildFill was true, the last element didn't have an index less than the stored index (it was actually equal to it), so that one element ran the 'fill' logic while everything else ran the 'docking' logic.
The change I made was if LastChildFill is set, as above, the stored index starts out pointing to the last child, but I then check the visibility of that element and if it's invisible, I lower the index by one and check again, continuing until I either find a visible element, or I run out of children to check (i.e. if they were all invisible.) That's also why I named the variable 'firstFilledIndex' since technically that, and all elements afterwards use the 'Fill' logic, even though all the elements after it are invisible.
I finally added a new LastVisibleChildFill property to enable or disable my new behavior. As a help to consumers, if you set that to true, it implicitly also set LastChildFill to true for you.
Here's the full code.
public class DockPanelEx : DockPanel
{
public static readonly DependencyProperty LastVisibleChildFillProperty = DependencyProperty.Register(
"LastVisibleChildFill",
typeof(bool),
typeof(DockPanelEx),
new UIPropertyMetadata(true, (s,e) => {
var dockPanelEx = (DockPanelEx)s;
var newValue = (bool)e.NewValue;
if(newValue)
dockPanelEx.LastChildFill = true; // Implicitly enable LastChildFill
// Note: For completeness, we may consider putting in code to set
// LastVisibileChildFill to false if LastChildFill is set to false
}));
/// <summary>
/// Indicates that LastChildFill should fill the last visible child
/// Note: When set to true, automatically also sets LastChildFill to true as well.
/// </summary>
public bool LastVisibleChildFill
{
get { return (bool)GetValue(LastVisibleChildFillProperty); }
set { SetValue(LastVisibleChildFillProperty, value); }
}
protected override Size ArrangeOverride(Size totalAvailableSize)
{
UIElementCollection internalChildren = base.InternalChildren;
int count = internalChildren.Count;
int firstFilledIndex = count;
if(LastChildFill)
{
for(firstFilledIndex = count - 1; firstFilledIndex >= 0; firstFilledIndex--)
{
if(!LastVisibleChildFill || internalChildren[firstFilledIndex].IsVisible)
break;
}
}
double usedLeftEdge = 0.0;
double usedTopEdge = 0.0;
double usedRightEdge = 0.0;
double usedBottomEdge = 0.0;
for (int i = 0; i < count; i++)
{
UIElement element = internalChildren[i];
if (element != null)
{
Size desiredSize = element.DesiredSize;
var finalRect = new Rect(
usedLeftEdge,
usedTopEdge,
Math.Max(0.0, (totalAvailableSize.Width - (usedLeftEdge + usedRightEdge))),
Math.Max(0.0, (totalAvailableSize.Height - (usedTopEdge + usedBottomEdge))));
if (i < firstFilledIndex)
{
switch (GetDock(element))
{
case Dock.Left:
usedLeftEdge += desiredSize.Width;
finalRect.Width = desiredSize.Width;
break;
case Dock.Top:
usedTopEdge += desiredSize.Height;
finalRect.Height = desiredSize.Height;
break;
case Dock.Right:
usedRightEdge += desiredSize.Width;
finalRect.X = Math.Max((double) 0.0, (double) (totalAvailableSize.Width - usedRightEdge));
finalRect.Width = desiredSize.Width;
break;
case Dock.Bottom:
usedBottomEdge += desiredSize.Height;
finalRect.Y = Math.Max((double) 0.0, (double) (totalAvailableSize.Height - usedBottomEdge));
finalRect.Height = desiredSize.Height;
break;
}
}
element.Arrange(finalRect);
}
}
return totalAvailableSize;
}
}
How do you determine the width of the text in a WPF TreeViewItem at run time?
I need to calculate an offset so I can draw a line from one leaf to the leaf of a different TreeView. All the 'width' properties return a size that is way bigger than the space taken up by the actual text of the node. It must be possible because the Select feature doesn't highlight the entire row. I'm writing the client in WPF and Silverlight.
You weren't very specific on the text or the tags, so I'm assuming you're taking about the .Net Framework's TreeViewItem.
There might be easier ways, but one possibility is to use the Graphics.MeasureString method. It gives you the size in pixels of a text when drawn using a specific font.
#mrphil: Sweet aborted fetus, that's scary
myTreeViewItem.Measure(new Size(Double.PositiveInfinity, Double.PositiveInfinity));
Size s = myTreeViewItem.DesiredSize;
return s.Width;
I have two solutions:
A) Uses the visual tree
TreeViewItem selected = (TreeViewItem)dataSourceTreeView.SelectedItem;
double textWidth = 0;
double expanderWidth = 0;
Grid grid = (Grid)VisualTreeHelper.GetChild(selected, 0);
ToggleButton toggleButton = (ToggleButton)VisualTreeHelper.GetChild(grid, 0);
expanderWidth = toggleButton.ActualWidth;
Border bd = (Border)VisualTreeHelper.GetChild(grid, 1);
textWidth = bd.ActualWidth;
B) If you don't want to use the visual tree
TreeViewItem selected = (TreeViewItem)dataSourceTreeView.SelectedItem;
double textWidth = 0;
Typeface typeface = new Typeface(selected.FontFamily,
selected.FontStyle, selected.FontWeight, selected.FontStretch);
GlyphTypeface glyphTypeface;
if (!typeface.TryGetGlyphTypeface(out glyphTypeface))
throw new InvalidOperationException("No glyphtypeface found");
string headerText = (string)selected.Header;
double size = selected.FontSize;
ushort[] glyphIndexes = new ushort[headerText.Length];
double[] advanceWidths = new double[headerText.Length];
for (int n = 0; n < headerText.Length; n++)
{
ushort glyphIndex = glyphTypeface.CharacterToGlyphMap[headerText[n]];
glyphIndexes[n] = glyphIndex;
double width = glyphTypeface.AdvanceWidths[glyphIndex] * size;
advanceWidths[n] = width;
textWidth += width;
}
Using WPF, what is the most efficient way to measure a large number of short strings? Specifically, I'd like to determine the display height of each string, given uniform formatting (same font, size, weight, etc.) and the maximum width the string may occupy?
The most low-level technique (and therefore giving the most scope for creative optimisations) is to use GlyphRuns.
It's not very well documented but I wrote up a little example here:
http://smellegantcode.wordpress.com/2008/07/03/glyphrun-and-so-forth/
The example works out the length of the string as a necessary step before rendering it.
In WPF:
Remember to call Measure() on the TextBlock before reading the DesiredSize property.
If the TextBlock was created on-the-fly, and not yet shown, you have to call Measure() first, like so:
MyTextBlock.Measure(new Size(Double.PositiveInfinity, Double.PositiveInfinity));
return new Size(MyTextBlock.DesiredSize.Width, MyTextBlock.DesiredSize.Height);
In Silverlight:
No need to measure.
return new Size(TextBlock.ActualWidth, TextBlock.ActualHeight);
The complete code looks like this:
public Size MeasureString(string s) {
if (string.IsNullOrEmpty(s)) {
return new Size(0, 0);
}
var TextBlock = new TextBlock() {
Text = s
};
#if SILVERLIGHT
return new Size(TextBlock.ActualWidth, TextBlock.ActualHeight);
#else
TextBlock.Measure(new Size(Double.PositiveInfinity, Double.PositiveInfinity));
return new Size(TextBlock.DesiredSize.Width, TextBlock.DesiredSize.Height);
#endif
}
It is very simple and done by FormattedText class!
Try it.
You can use the DesiredSize property on a rendered TextBox to get the height and width
using System.Windows.Threading;
...
Double TextWidth = 0;
Double TextHeight = 0;
...
MyTextBox.Text = "Words to measure size of";
this.Dispatcher.BeginInvoke(
DispatcherPriority.Background,
new DispatcherOperationCallback(delegate(Object state) {
var size = MyTextBox.DesiredSize;
this.TextWidth = size.Width;
this.TextHeight = size.Height;
return null;
}
) , null);
If you have a large number of strings it may be quicker to first pre-calualte the height and width of every indiviudal letter and symbol in a given font, and then do a calculation based on the string chars. This may not be 100% acurate due to kerning etc
I simply have two grid on top of one another. Given one state of the world, I want grid A to be on top, given another state of the world, I want grid B to be on top. In the old days we could just call grid.BringToFront(), but that doesn't exist anymore, and I can't figure out any way to make that happen.
The best I can figure, I need to create my own custom classes to allow this functionality, but that seems like major overkill for something that used to be so simple.
You can use the Panel.ZIndex property to change the display order of elements in a panel
You have to use the Z index property, and because there are no built-in function to do what you want, I made my own.
The higher the Z value, the 'closer' to front the control is.
So you want to put your control on top without having to set an arbitrary high Z value.
So here is a small function I wrote for myself to do exactly that.
Note: this assume that you are using a Canvas and UserControls.
So you might need to adapt it a little bit if that's not your case.
Basically it will get the index of the control to move, then any control currently above it will go down by 1 and the control to move will be put on top (to maintain hierarchy).
static public void BringToFront(Canvas pParent, UserControl pToMove)
{
try
{
int currentIndex = Canvas.GetZIndex(pToMove);
int zIndex = 0;
int maxZ = 0;
UserControl child;
for (int i = 0; i < pParent.Children.Count; i++)
{
if (pParent.Children[i] is UserControl &&
pParent.Children[i] != pToMove)
{
child = pParent.Children[i] as UserControl;
zIndex = Canvas.GetZIndex(child);
maxZ = Math.Max(maxZ, zIndex);
if (zIndex > currentIndex)
{
Canvas.SetZIndex(child, zIndex - 1);
}
}
}
Canvas.SetZIndex(pToMove, maxZ);
}
catch (Exception ex)
{
}
}
To whom it may concern:
ZIndex property is 0 by default, so if you have (like me) a Canvas with more than 1 element (>4000 Shapes in my case), all will have ZIndex = 0, so changing the ZIndexes with this method will have no effect.
For this to work, I set the ZIndexes to a known value after creating all the elements, so they can be ordered after.
int zIndex = 0;
for (int i = 0; i < canvas.Children.Count; i++) {
UIElement child = canvas.Children[i] as UIElement;
if (canvas.Children[i] is UIElement) Canvas.SetZIndex(child, zIndex++);
}
Instead of stacking the two grids, change the visibility properties so the grid you aren't using is collapsed.
Expanding on the answer from #PicMickael, this will do exactly as they described but with less instructions:
public void BringToFront<T>(T uiElement, Canvas canvas)
{
try
{
foreach (UIElement s in canvas.Children)
{
Canvas.SetZIndex(s, 1);
}
Canvas.SetZIndex(uiElement as UIElement, 2);
}
catch (Exception ex)
{
WriteLog.Error(ex);
}
}