I am working on implementing an ISrollInfo interface for a custom control. Simply put, I have a label in my custom control under a Canvas. I would like the label to "stay in place" when my custom control is scrolled. That is, the label needs to be always visible no matter the scrolling offset.
Now, to as a test I added this sample code
protected override Size MeasureOverride(Size constraint)
{
return new Size(1000, 50);
}
protected override Size ArrangeOverride(Size arrangeBounds)
{
double x = 50;
double y = 50;
label1.Arrange(new Rect(new Point(x, y), new Size(1000, 50)));
return arrangeBounds;
}
When I test the control (my control is put inside a ScrollViewer), the label is hidden (before and after I use the scrollbar). If I remove the override for ArrangeOverride, the label appears and scrolls around as I use the scrollbar.
Any ideas as to what I am missing?
Found it, my ArrangeOverride is on the UserControl, where I specifically only arrange the label, the canvas doesn't get arranged (i.e postion and size is not set). Now, since the label is in the canvas, you can't see it.
Related
I'm trying to design a Popup which will appear on the bottom-right corner of its PlacementTarget
Let's admit that you set its PlacementTarget to a Window, well, the Popup will act as classic toaster notifications.
Given the fact that WPF is not smart enough to provide us a "corner" solution, I'm trying to implement a new control, inheriting from Popup , which will place itself at the appropriate location.
Here is my first idea: work on Loaded event to determine where should I place the Popup.
Problem? I don't want to give any fixed dimensions to the popup, which is supposed to size itself according to the text displayed.
However, I can't get the ActualWidth property when Loaded event is raised.
I can't have it either when Opened event is raised.
Here is the draft code so far:
public class ExceptionPopup : Popup
{
public ExceptionPopup()
{
InitializeComponent();
Loaded += new RoutedEventHandler(ExceptionPopup_Loaded);
}
void ExceptionPopup_Loaded(object sender, RoutedEventArgs e)
{
if (PlacementTarget != null)
{
if (PlacementTarget is FrameworkElement)
{
parentWidth = (PlacementTarget as FrameworkElement).ActualWidth;
parentHeight = (PlacementTarget as FrameworkElement).ActualHeight;
}
}
}
protected override void OnOpened(EventArgs e)
{
this.HorizontalOffset = parentWidth;
this.VerticalOffset = parentHeight;
base.OnOpened(e);
}
}
Is there any other event I could use to catch what I want here?
I'd basically like to set HorizontalOffset to parentWidth - ActualWidth/2 , same for height :)
Any idea?
Thanks!
Usually I set the PlacementTarget to either Bottom or Right, then apply a RenderTransform which shifts the Popup by the remaining value.
For example, I might use Placement=Bottom, then use a RenderTransform to shift the popup (Window.Width - Popup.Width) to the right, and Popup.Height upwards. You might not even need to re-adjust based on the Popup Height/Width becauase MSDN says that Popups are not allowed to be displayed off screen, and it will automatically adjust their placement to keep them visible
Be sure you use a RenderTransform instead of a LayoutTransform, because RenderTransforms get applied after the Popup gets Rendered, so the ActualHeight and ActualWidth will be greater than 0.
Ok, for illustrative purposes, below I created a subclass of ListBoxItem and a subclass of ListBox which uses it as its container by overriding both IsItemItsOwnContainerOverride and GetContainerForItemOverride.
Now when the window first appears, as expected, MeasureOverride is called on every ListBoxItem (with Infinity,Infinity) followed by ArrangeOverride being called on every item.
However, when resizing the ListBox, only ArrangeOverride is called on the ListBoxItem, not MeasureOverride even though the metadata for the width property is set to AffectsMeasure.
NotE: I know I can get around this by setting ScrollViewer.HorizontalScrollbarVisibility to 'Disabled' in which case MeasureOverride does get called as expected because that scroll setting forces the items to match the width of the listbox and thus naturally would re-fire. However, I'm still trying to figure out why Measure isn't called by default anyway because the metadata for the Width property has the AffectsMeasure flag set and the width is changing via the ArrangeOverride step.
Is that flag just a hint for its container and in the case of a control placed in a ScrollViewer it's ignored? My guess is that unless you disable the scrolling, the controls have an infinite area available to them, so once they are measured, there's no need to re-measure them again. Disable the horizontal scrolling however and you're stating the width isn't unlimited, hence the MeasureOverride is called again. But that's just a guess, albeit a logical one.
Here's example code to play with. Create a new WPF project and paste this in the window's CodeBehind and look at the debug output. Next, set the HorizontalScrollbarVisibility flag and you'll see that it does get called.
public partial class MainWindow : Window
{
public MainWindow(){
InitializeComponent();
var items = new List<object>(){ "This is a really, really, really, really long sentence"};
var lbx = new ListBoxEx { ItemsSource = items };
this.Content = lbx;
}
}
public class ListBoxEx : ListBox
{
protected override bool IsItemItsOwnContainerOverride(object item){
return (item is ListBoxItemEx);
}
protected override DependencyObject GetContainerForItemOverride(){
return new ListBoxItemEx();
}
}
public class ListBoxItemEx : ListBoxItem
{
protected override Size MeasureOverride(Size availableSize){
Console.WriteLine("MeasureOverride called with " + availableSize);
return base.MeasureOverride(availableSize);
}
protected override Size ArrangeOverride(Size finalSize){
Console.WriteLine("ArrangeOverride called with " + finalSize);
return base.ArrangeOverride(finalSize);
}
}
Ok, I think the answer is it's not firing because the Measure pass is affecting the panel, not the items, and the panel itself hasn't resized so there's no reason to re-measure its children.
However, when setting ScrollViewer.HorizontalScrollbarVisibility to Disabled, the width of the panel tracks the width of the ListBox, not its contents, therefore the children do need to be re-measured, hence that's why they were in that case.
first time post for me :)
I'm having this issue with the width of my UserControl that i just can't seem to figure out.
Essentially i have a UserControl that contains an ItemsControl (panle is a horizontal StackPanel). The item's DataTemplate is a standard Button.
I bind a List of strings to the ItemsControl, and each string gets bound to the Content property of the Button. Works fine till now.
What i need to do is to keep track of the width of each single item (Button with string inside) in the ItemsControl even if they are not rendered. I need to do this to dynamically (while resizing) remove items that exceed the maximum width of the UserControl, and add them back again if there is enough space.
To achieve this i measure the width of each string using the following function:
private double GetTextWidth(string text)
{
FormattedText ft = new FormattedText(text, CultureInfo.CurrentCulture, FlowDirection.LeftToRight, new Typeface(this.FontFamily, this.FontStyle, this.FontWeight, this.FontStretch), this.FontSize, this.Foreground);
ft.Trimming = TextTrimming.CharacterEllipsis;
return ft.WidthIncludingTrailingWhitespace;
}
I then manually add a margin of 6 to the returned value, this margin is the spacing needed by the Button.
Summing all the generated values from the strings in my List i should get almost the same width as my UserControl. Since all layouts are stretched (even the UserControl in the Window is), there is no margin or padding going on (as far as i can see) and there is only a single Border which has a width of 1.
Problem is that this doesn't work, it requires me to manually add a pretty large value (45-65 units) to the calculated width to get it to work "pixel-perfect".
I obviously tried to find the root of this problem, but was unable.
First i thought it's caused by a DIP <-> Pixel problem, but that doesn't seem the case.
I measured the width of a single string and then the width of the button containing the same string. In all scenarios the first and the second differ by 6 units.
There is no visible space between the buttons so i really can't explain where so much overhead is coming from.
The only thing i noticed is that the value i need to add changes if i change the FontSize, but that's pretty obvious...
Probably i'm missing something big, any ideas?
Thanks for reading!
If I understand you correctly, you want to hide items that are not completely visible on the screen.
I've done this in the past with a helper method that will tell me if a control is fully visible or not, and that sets the control's visibility to Hidden if the control is not fully visible. I implemented it in the Loaded and SizeChanged events.
Here's the helper class which returned the rendered control's visibility:
public enum ControlVisibility
{
Hidden,
Partial,
Full,
FullHeightPartialWidth,
FullWidthPartialHeight
}
/// <summary>
/// Checks to see if an object is rendered visible within a parent container
/// </summary>
/// <param name="child">UI element of child object</param>
/// <param name="parent">UI Element of parent object</param>
/// <returns>ControlVisibility Enum</returns>
public static ControlVisibility IsObjectVisibleInContainer(
FrameworkElement child, UIElement parent)
{
GeneralTransform childTransform = child.TransformToAncestor(parent);
Rect childSize = childTransform.TransformBounds(
new Rect(new Point(0, 0), new Point(child.Width, child.Height)));
Rect result = Rect.Intersect(
new Rect(new Point(0, 0), parent.RenderSize), childSize);
if (result == Rect.Empty)
{
return ControlVisibility.Hidden;
}
if (result.Height == childSize.Height && result.Width == childSize.Width)
{
return ControlVisibility.Full;
}
if (result.Height == childSize.Height)
{
return ControlVisibility.FullHeightPartialWidth;
}
if (result.Width == childSize.Width)
{
return ControlVisibility.FullWidthPartialHeight;
}
return ControlVisibility.Partial;
}
The code-behind the Loaded and SizeChanged events looked something like this:
ControlVisibility ctrlVisibility=
WPFHelpers.IsObjectVisibleInContainer(button, parent);
if (ctrlVisibility == ControlVisibility.Full
|| isVisible == ControlVisibility.FullWidthPartialHeight)
{
button.Visibility = Visibility.Visible;
}
else
{
button.Visibility = Visibility.Hidden;
}
When using a wpf textbox without explicit height and width values, and when there is space available to expand, the textbox resizes as you type.
However when I change the border thickness, it does not recalculate it and for very thick borders, part of the text is covered by the border. How do I explicitly precipitate a recalc?
Coincidently I am using a derived custom textbox class so I should know when the border thickness changes.
This bug must be some optimization gone wrong
Overriding Metadata for BorderThickness or adding a Dependency Property that affects Measure, Arrange or Render don't help
Undocking and Redocking from the parent container had no effect either
Even Undocking from the parent container and Redocking into a new container won't help if the space it is given in the new container is exactly the same as the space that it had in the old container
It seems like the size is only re-calculated once Text, Width, Height or available space changes. I looked around with Reflector but things get pretty complex down there so I couldn't find the source for this.
Here is a small workaround that listens to changes in BorderThickness and in the changed event handler, make a small change to the Width and once it is updated, change it right back
public class MyTextBox : TextBox
{
public MyTextBox()
{
DependencyPropertyDescriptor borderThickness
= DependencyPropertyDescriptor.FromProperty(MyTextBox.BorderThicknessProperty, typeof(MyTextBox));
borderThickness.AddValueChanged(this, OnBorderThicknessChanged);
}
void OnBorderThicknessChanged(object sender, EventArgs e)
{
double width = this.Width;
SizeChangedEventHandler eventHandler = null;
eventHandler = new SizeChangedEventHandler(delegate
{
this.Width = width;
this.SizeChanged -= eventHandler;
});
this.SizeChanged += eventHandler;
this.Width = this.ActualWidth + 0.00000001;
}
}
First of all, this looks like a bug.
If the problem is that dynamic changes of the border thickness are not taken into account, you can perhaps make a workaround by creating a dependency property with AffectsMeasure in FrameworkPropertyMetadata, and bind it to the border thickness. Hope this quirk helps.
If the static setting of the border thickness are not taken into account, you can try to replace the TextBox's default template with your own (correct) version.
I created an adorner on a WPF line element, because there was neet to add some text.
Now, when this line is moved, the adorner does not "follow" the line automatically. In fact, it does not refresh itsef:
here black curves is the Control drawing, and the red "120 m" is the adorner one.
Some code
void SegmentLine_Loaded(object sender, RoutedEventArgs e)
{
AdornerLayer aLayer = AdornerLayer.GetAdornerLayer(this);
if (aLayer != null)
{
aLayer.Add(new TextAdorner(this));
}
}
class TextAdorner : Adorner
{
public TextAdorner(UIElement adornedElement)
: base(adornedElement)
{
}
protected override void OnRender(DrawingContext drawingContext)
{
SegmentLine segment = (this.AdornedElement as SegmentLine);
if (segment != null)
{
Rect segmentBounds = new Rect(segment.DesiredSize);
var midPoint = new Point(
(segment.X1 + segment.X2) / 2.0,
(segment.Y1 + segment.Y2) / 2.0);
var lineFont = // get line font as Font
FormattedText ft = new FormattedText(
string.Format("{0} m", segment.Distance),
Thread.CurrentThread.CurrentCulture,
System.Windows.FlowDirection.LeftToRight,
new Typeface(lineFont.FontFamily.ToString()),
ligneFont.Size, Brushes.Red);
drawingContext.DrawText(ft, midPoint);
}
}
}
Why MeasureOverride, etc aren't being called
Your adorner's MeasureOverride, ArrangeOverride, and OnRender aren't being called because your SegmentLine control is never changing size or position:
Since your SegmentLine doesn't implement MeasureOverride, it always has the default size assigned by the layout engine.
Since your SegmentLine doesn't implement ArrangeOverride or manipulate any transforms, its position is always exactly the upper-left corner of the container.
The Adorner's MeasureOverride, ArrangeOverride and OnRender are only called by WPF under these conditions:
The AdornedElement changes size or position (this the most common case), or
One of the Adorner's properties chagnes and that property is marked AffectsMeasure, AffectsArrange, or AffectsRender, or
You call InvalidateMeasure(), InvalidateArrange(), or InvalidateVisuaul() on the adorner.
Because your SegmentLine never changes size or position, case 1 doesn't apply. Since you don't have any such properties on the Adorner and don't call InvalidateMeasure(), InvalidateArrange() or InvalidateVisual(), the other cases don't apply either.
Precise rules for Adorner re-measure
Here are the precise rules for when an adorned element change triggers a call to Adorner.MeasureOverride:
The adorned element must force a layout pass by invalidating its Measure or Arrange in response to some event. This could be triggered automatically by a change to a DependencyProperty with AffectsMeasure or AffectsArrange, or by a direct call to InvalidateMeasure(), InvalidateArrange() or InvalidateVisual().
The adorned element's Measure and Arrange methods must not be called directly from user code between the invalidation and the layout pass. In other words, you must wait for the layout manager to do the job.
The adorned element must make a non-trivial change to either its RenderSize or its Transform.
The combination of all transforms between the AdornerLayer and the adorned element must be affine. This will generally be the case as long as you are not using 3D.
Your SegmentLine is just drawing the line in a new place rather than updating its own dimensions, thereby omitting my requirement #3 above.
Recommendation
Normally I would recommend your adorner have AffectsRender DependencyProperties bound to the SegmentLine's properties, so any time X1, Y1, etc change in the SegmentLine they are also updated in the Adorner which causes the Adorner to re-render. This provides a very clean interface, since the adorner can be used on any control that has properties X1, Y1, etc, but it is less efficient than tightly coupling them.
In your case the adorner is clearly tightly bound to your SegmentLine, so I think it makes just as much sense to call InvalidateVisual() on the adorner from the SegmentLine's OnRender(), like this:
public class SegmentLine : Shape
{
TextAdorner adorner;
...
protected override void OnRender(DrawingContext drawingContext)
{
base.OnRender(drawingContext);
if(adorner==null)
{
var layer = AdornerLayer.GetAdornerLayer(this); if(layer==null) return;
adorner = new TextAdorner(this);
... set other adorner properties and events ...
layer.Add(adorner);
}
adorner.InvalidateVisual();
}
}
Note that this doesn't deal with the situation where the SegmentLine is removed from the visual tree and then added again later. Your original code doesn't deal with this either, so I avoided the complexity of dealing with that case. If you need that to work, do this instead:
public class SegmentLine : Shape
{
AdornerLayer lastLayer;
TextAdorner adorner;
...
protected override void OnRender(DrawingContext drawingContext)
{
base.OnRender(drawingContext);
var layer = AdornerLayer.GetAdornerLayer(this);
if(layer!=lastLayer)
{
if(adorner==null)
{
adorner = new TextAdorner(this);
... set other adorner properties and events ...
}
if(lastLayer!=null) lastLayer.Remove(adorner);
if(layer!=null) layer.Add(adorner);
lastLayer = layer;
}
adorner.InvalidateVisual();
}
}
How is the line being moved? Does the MeasureOverride or ArrangeOverride of the adorner get invoked after the move? OnRender will only get invoked if the visual is invalidated (e.g. invalidatevisual) so I'm guessing that the render isn't being invalidated.
May be you wanted to use segmentBounds to define midPoint? Otherwise what is it doing there? Looks like you are defining midPoint relative to not rerendered segment.
idiot fix, but it works
AdornerLayer aLayer;
void SegmentLine_Loaded(object sender, RoutedEventArgs e)
{
aLayer = AdornerLayer.GetAdornerLayer(this);
if (aLayer != null)
{
aLayer.Add(new TextAdorner(this));
}
}
protected override void OnRender(DrawingContext drawingContext)
{
base.OnRender(drawingContext);
if (aLayer != null)
{
aLayer.Update();
}
}
Now, the problem is that when I click on a the adorner the control itself does not recieve the hit...