I have an GridItem that has a fixed Height/Width.
It contains a textblock that has the max line set.
How can I determine if this text is trimmed?
I want to add special functionality if it is trimmed.
Old Way - when TextWrapping is set to None
To know if a TextBlock is trimmed, we can subscribe to its SizeChanged event and compare its ActualWidth to the MaxWidth you specified. To get the right ActualWidth of the TextBlock, we will need to leave the TextTrimming to its default value (i.e. TextTrimming.None), and set it to trimmed once the width goes over.
New Way - when TextWrapping is set to Wrap
Now that I know because the TextWrapping is set to Wrap and assume the VirticalAlignment is not specified (default to Stretch), the Width will always stay the same. We only need to monitor the SizeChanged event when the actual height of the TextBlock exceeds the height of its parent.
Let's use a Behavior to encapsulate all the logic above. What needs to be mentioned here is that a static helper class with a bunch of attached properties or a new control that inherits from TextBlock can do exactly the same thing; but being a big Blend fan, I prefer to use Behaviors whenever possible.
The Behavior
public class TextBlockAutoTrimBehavior : DependencyObject, IBehavior
{
public bool IsTrimmed
{
get { return (bool)GetValue(IsTrimmedProperty); }
set { SetValue(IsTrimmedProperty, value); }
}
public static readonly DependencyProperty IsTrimmedProperty =
DependencyProperty.Register("IsTrimmed", typeof(bool), typeof(TextBlockAutoTrimBehavior), new PropertyMetadata(false));
public DependencyObject AssociatedObject { get; set; }
public void Attach(DependencyObject associatedObject)
{
this.AssociatedObject = associatedObject;
var textBlock = (TextBlock)this.AssociatedObject;
// subscribe to the SizeChanged event so we will know when the Width of the TextBlock goes over the MaxWidth
textBlock.SizeChanged += TextBlock_SizeChanged;
}
private void TextBlock_SizeChanged(object sender, SizeChangedEventArgs e)
{
// ignore the first time height change
if (e.PreviousSize.Height != 0)
{
var textBlock = (TextBlock)sender;
// notify the IsTrimmed dp so your viewmodel property will be notified via data binding
this.IsTrimmed = true;
// unsubscribe the event as we don't need it anymore
textBlock.SizeChanged -= TextBlock_SizeChanged;
// then we trim the TextBlock
textBlock.TextTrimming = TextTrimming.WordEllipsis;
}
}
public void Detach()
{
var textBlock = (TextBlock)this.AssociatedObject;
textBlock.SizeChanged += TextBlock_SizeChanged;
}
}
The XAML
<Grid HorizontalAlignment="Center" Height="73" VerticalAlignment="Center" Width="200" Background="#FFD2A6A6" Margin="628,329,538,366">
<TextBlock x:Name="MyTextBlock" TextWrapping="Wrap" Text="test" FontSize="29.333">
<Interactivity:Interaction.Behaviors>
<local:TextBlockAutoTrimBehavior IsTrimmed="{Binding IsTrimmedInVm}" />
</Interactivity:Interaction.Behaviors>
</TextBlock>
</Grid>
Note that the Behavior exposes a dependency property IsTrimmed, you can data bind it to a property in your viewmodel (i.e. IsTrimmedInVm in this case).
P.S. There's no FormattedText function in WinRT otherwise the implementation could be a little bit different.
We ended up making a static function
// Ensure block does not have MAXLINES or text trimming set prior to checking
public static bool IsTruncated(TextBlock block, int maxLines)
{
if (block == null)
{
throw new ArgumentNullException("block");
}
//the cut-off height is the height at which text will be cut off in the UI
var cutOffHeight = maxLines * block.LineHeight;
//determine whether the actual height of the TextBlock is greater than the cut-off height
return block.ActualHeight > cutOffHeight;
}
The trick is to make sure that Maxlines and text trimming is NOT set on the Textblock prior to running this function. After this function returns, that is when the Maxlines is set. In my case I just saved the returned Boolean in a containing object so I knew it was longer. Then I set maxlines and another button to see the extended content based on that Boolean.
Related
I tried using the slider component for the first time and immediately some question came up:
How do I make the slider snap to the positions of the TickMarks?
If that's not possible, what are they for?
How do I change the amount of marks or remove all of them if I want to slider without stepping?
Why, when I change the Slider Track settings the visuals do not adjust?
I already see this going into a feature request ;)
These questions are probably best filed as issues on the MRTK github page. here's a link to file an issue.
But, answering your questions here for future visitors:
How do I make the slider snap to the positions of the TickMarks?
The default PinchSlider does not provide that functionality, but here is an example of how to do this by extending pinchslider (reference: issue 4140
public class SliderWithSnapPoints : PinchSlider
{
[SerializeField]
[Tooltip("The number of snap points")]
float snapPoints = 100;
float lastSnapPoint;
float snapPointSize;
public override void OnPointerDown(MixedRealityPointerEventData eventData)
{
base.OnPointerDown(eventData);
if (eventData.used)
{
lastSnapPoint = SliderValue;
snapPointSize = 1f / snapPoints;
}
}
/// <summary>
/// Handle slider value changes by dragging, and commit these changes.
/// </summary>
/// <remarks>
/// Note, this requires the MRTK pinch slider to implement this function signature,
/// and the pinch slider needs to call this function instead of instead setting SliderValue
/// directly.
/// </remarks>
protected override void OnPointerDragged(float newSliderValue)
{
var valueChange = Mathf.Abs(lastSnapPoint - newSliderValue);
if (valueChange >= snapPointSize)
{
lastSnapPoint = SliderValue = newSliderValue;
}
}
}
How do I change the amount of marks or remove all of them if I want to slider without stepping?
You can just delete or disable the tick marks in the scene hierarchy.
Why, when I change the Slider Track settings the visuals do not adjust?
The visuals do not adjust because they are not tied to the number of snap points, they are just provided for visual guidance.
I modified this implementation slightly:
using Microsoft.MixedReality.Toolkit.UI;
using UnityEngine;
public class SliderWithSnapPoints : PinchSlider
{
#pragma warning disable 649
[Tooltip("The number of snap points")]
[Min(2)]
[SerializeField] private int snapPoints = 5;
#pragma warning restore 649
/// <summary>
/// Handle slider value changes by dragging, and commit snap point aligned changes.
/// </summary>
/// <remarks>
/// Note, this requires the MRTK pinch slider to implement this function signature,
/// and the pinch slider needs to call this function instead of setting SliderValue
/// directly.
/// </remarks>
protected override void OnPointerDragged(float newSliderValue)
{
float gap = 1f / (snapPoints - 1f);
newSliderValue = 0.5f * gap + newSliderValue;
newSliderValue -= (newSliderValue % gap);
SliderValue = newSliderValue;
}
}
As the comment suggests you also have to edit the main PinchSlider script from:
public void OnPointerDragged(MixedRealityPointerEventData eventData)
{
if (eventData.Pointer == activePointer && !eventData.used)
{
var delta = activePointer.Position - startPointerPosition;
var handDelta = Vector3.Dot(SliderTrackDirection.normalized, delta);
SliderValue = Mathf.Clamp(startSliderValue + handDelta / SliderTrackDirection.magnitude, 0, 1);
// Mark the pointer data as used to prevent other behaviors from handling input events
eventData.Use();
}
}
To:
public void OnPointerDragged(MixedRealityPointerEventData eventData)
{
if (eventData.Pointer == activePointer && !eventData.used)
{
var delta = activePointer.Position - startPointerPosition;
var handDelta = Vector3.Dot(SliderTrackDirection.normalized, delta);
OnPointerDragged(Mathf.Clamp(startSliderValue + handDelta / SliderTrackDirection.magnitude, 0, 1));
// Mark the pointer data as used to prevent other behaviors from handling input events
eventData.Use();
}
}
protected virtual void OnPointerDragged(float newSliderValue)
{
SliderValue = newSliderValue;
}
I want to write a new class that extends Region containing a StackPane inside. But I'm getting into trouble when I add insets to it like padding or a border. Here is a simplified example of the class:
public class CustomPane extends Region
{
private ToggleButton testControl = new ToggleButton("just a test control");
private StackPane rootPane = new StackPane(testControl);
public CustomPane()
{
getChildren().add(rootPane);
setStyle("-fx-border-color: #257165; -fx-border-width: 10;");
}
}
And that is how the result looks like:
If I try to move the StackPane by calling
rootPane.setLayoutX(10);
rootPane.setLayoutY(10);
then the Region just grows:
But really I wanted it to look like this:
(The third image was created by extending StackPane instead of Region, which already manages the layouting stuff correctly. Unfortunately I have to extend Region since I want to keep getChildren() protected.)
Okay, I tried to handle the layout calculation but I didn't come up with it. Could experts give me some advise?
StackPane uses the insets for layouting the (managed) children. Region doesn't do this by default. Therefore you need to override the layoutChildren with something that uses these insets, e.g.:
#Override
protected void layoutChildren() {
Insets insets = getInsets();
double top = insets.getTop(),
left = insets.getLeft(),
width = getWidth() - left - insets.getRight(),
height = getHeight() - top - insets.getBottom();
// layout all managed children (there's only rootPane in this case)
layoutInArea(rootPane,
left, top, // offset of layout area
width, height, // available size for content
0,
HPos.LEFT,
VPos.TOP);
}
It is necessary to make the support of Right to Left style (both text and layout-s). I understand that when you set parent Grid's properties FlowDirection = "RightToLeft" in all child controls it inherited.
The question is - is there any default setting, which will shift all we need in app? Or should I set every parent greeds FlowDirection by some king of flag and set this flag as FlowDirection = "RightToLeft" if we, for example in in Arab countries?
If you are going to support any right to left language will need to have a right to left layout too. You don't need to change FlowDirection property of all of the elements since it is inherited by child elements.
MSDN:
An object inherits the FlowDirection value from its parent in the
object tree. Any element can override the value it gets from its
parent. If not specified, the default FlowDirection is LeftToRight
So usually you need to set the property once for root element/frame of the Window.
However, some elements like FontIcon and Image does not mirror automatically. FontIcon has a MirroredWhenRightToLeft property:
You can set the MirroredWhenRightToLeft property to have the glyph
appear mirrored when the FlowDirection is RightToLeft. You typically
use this property when a FontIcon is used to display an icon as part
of a control template and the icon needs to be mirrored along with the
rest of the control
For Image, you need to flip the image by transforms.
Edit:
You can set the property in the Application class where you create the main frame/page:
// Part of the App.xaml.cs in default UWP project template:
protected override void OnLaunched(LaunchActivatedEventArgs e) {
#if DEBUG
if (System.Diagnostics.Debugger.IsAttached) {
this.DebugSettings.EnableFrameRateCounter = true;
}
#endif
Frame rootFrame = Window.Current.Content as Frame;
// Do not repeat app initialization when the Window already has content,
// just ensure that the window is active
if (rootFrame == null) {
// Create a Frame to act as the navigation context and navigate to the first page
rootFrame = new Frame();
rootFrame.NavigationFailed += OnNavigationFailed;
if (e.PreviousExecutionState == ApplicationExecutionState.Terminated) {
//TODO: Load state from previously suspended application
}
//**********************
// Set flow direction
// *********************
if (System.Globalization.CultureInfo.CurrentCulture.TextInfo.IsRightToLeft) {
rootFrame.FlowDirection = FlowDirection.RightToLeft;
}
// Place the frame in the current Window
Window.Current.Content = rootFrame;
}
...
...
If you don't want to use code behind (I think its OK to use it for this scenario), you can implement IValueConverter (Not recommended):
public class RightToLeftConverter : IValueConverter {
public object Convert(object value, Type targetType,
object parameter, string language) {
if (System.Globalization.CultureInfo.CurrentCulture.TextInfo.IsRightToLeft) {
return FlowDirection.RightToLeft;
}
return FlowDirection.LeftToRight;
}
public object ConvertBack(object value, Type targetType,
object parameter, string language)
{
throw new NotImplementedException();
}
}
and use it in XAML:
<Page
...
...
FlowDirection="{Binding Converter={StaticResource RightToLeftConverter}}">
I'm changing the height and the width of my CustomView which extends Android View in runtime like this:
#Override
public void onGlobalLayout() {
if(!initialized) {
int containerHeight = instance.getHeight();
int containerWidth = instance.getWidth();
myView.getLayoutParams().height = (int) (containerHeight * HEIGHT_RATIO);
myView.getLayoutParams().width = (int) (containerWidth * WIDTH_RATIO);
instance.getViewTreeObserver().removeGlobalOnLayoutListener(this);
initialized = true;
}
}
this code is in the container view Constructor.
In addition my CustomView onMeasure() is as follows:
#Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
// maximum width we should use
int width = MeasureSpec.getSize(widthMeasureSpec);
int height = MeasureSpec.getSize(heightMeasureSpec);
setMeasuredDimension(width, height);
}
This is the result:
Where the width and height I specified are at the same size of the green rectangle.
My Question is: why does the actual size of my custom view (red rectangle) is not at the same size as I gave as input in the LayoutParams ?
not sure what object you're working with for your view, but you might try something like this:
CustomViewget.Window().setLayout(370, 480); //Controls width and height
My problem was in the drawing of the CustomView. In onDraw(), I took the width and height of the canvas instead of the View itself.
The sizes might not be calculated properly at the time of calling, so you may need to place your code in a handler and run it on post so that it gets done after everything else on the UI thread is done. Try the following code.
Handler handler = new Handler();
handler.post(new Runnable() {
public void run() {
//check sizes and update sizes here
});
I am new to WPF and have not been able to find the solution to this yet.
We are trying to make a UserControl that provides a progressbar that will change its style as the percentage goes up (basically red when like less then 50%, yellow to 30%, etc.)
The control seems to work exceot for the style getting updated. When the window is first brought up the value is always 0 even if the progressbar is being started at 50% or so. To me it seems I have messed up the PropertyChanged code or not connected the data up right somewhere. Here is the code so far:
XAML file comsuming the UserControl (TaskListStatus.xaml)
<ssw:ColoredProgressBar x:Name="pbCompleted" Value="{Binding PercentCompleted}" Height="40"/>
ColoredProgressBar.xaml:
<UserControl.Resources>
<this:ProgressBarStyleConverter x:Key="pbStyleConverter"/>
<!-- Progress Bar Styles-->
........
</UserControl.Resources>
<Grid>
<ProgressBar x:Name="pb" Value="{Binding Path=Value, ElementName=coloredBar}">
<ProgressBar.Style>
<Binding Converter="{StaticResource pbStyleConverter}"
RelativeSource="{RelativeSource Self}"/>
</ProgressBar.Style>
</ProgressBar>
</Grid>
ColoredProgressBar.xaml.cs
public partial class ColoredProgressBar : UserControl, INotifyPropertyChanged {
public ColoredProgressBar() {
InitializeComponent();
}
public static DependencyProperty ValueProperty =
DependencyProperty.Register("Value", typeof(double),
typeof(ColoredProgressBar), new PropertyMetadata(null));
public double Value {
get { return Convert.ToDouble(GetValue(ValueProperty)); }
set {
SetValue(ValueProperty, value);
if (PropertyChanged != null) {
PropertyChanged(this, new PropertyChangedEventArgs("Value"));
}
}
}
public event PropertyChangedEventHandler PropertyChanged;
}
ProgressBarStyleConverter.cs
public class ProgressBarStyleConverter : IValueConverter {
private const int RED_CUTOFF = 40;
private const int YELLOW_CUTOFF = 100;
private enum ProgressBarColor {
Green,
Yellow,
Red
}
public object Convert(object value, Type targetType, object parameter, CultureInfo culture) {
FrameworkElement targetElement = value as FrameworkElement;
double progressBarValue = ((ProgressBar)targetElement).Value;
string styleName = "AeroProgressBarStyle";
ProgressBarColor color;
if (progressBarValue < RED_CUTOFF) {
color = ProgressBarColor.Red;
} else if (progressBarValue < YELLOW_CUTOFF) {
color = ProgressBarColor.Yellow;
} else {
color = ProgressBarColor.Green;
}
switch (color) {
case ProgressBarColor.Green:
styleName = "AeroProgressBarStyle"; break;
case ProgressBarColor.Yellow:
styleName = "YellowAeroProgressBarStyle"; break;
case ProgressBarColor.Red:
styleName = "RedAeroProgressBarStyle"; break;
}
return (Style)targetElement.TryFindResource(styleName);
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) {
return null;
}
}
If anyone can help it would be greatly appreciated.
So far this is the way we have gotten it to work. I am not 100% sure this is the ideal way but it does work:
ColoredProgressBar.xaml:
<UserControl.Resources>
<this:ProgressBarStyleConverter x:Key="pbStyleConverter"/>
<!-- Progress Bar Styles-->
........
</UserControl.Resources>
<Grid>
<ProgressBar x:Name="pb" Value="{Binding Path=Value, ElementName=coloredBar}">
<ProgressBar.Style>
<MultiBinding Converter="{StaticResource pbStyleConverter}">
<Binding RelativeSource="{RelativeSource Self}" />
<Binding ElementName="coloredBar" Path="Value" />
</MultiBinding>
</ProgressBar.Style>
</ProgressBar>
</Grid>
We could not figure out how to get the binding down to a single binding. The reason we have to pass in Self is because that is where all of the temples are defined for setting up the color. I tried passing Self in as a ConverterParameter but I could never seem to get it to work. So by passing it in as a MultiBinding we can get to self and the temples defined there and since value is passed in, when value updates, it updates the temple of the progressbar.
Bind the style to the Value property of the ProgressBar rather than the ProgressBar itself. So add a Path=Value attribute to the binding, and modify the converter code accordingly.