How can I Use Connected Animation in WPF App like UWP - c#

I want to implement UWP connected animation in my WPF app. like this......
Is it possible? If it is then how can I do it?

using System;
using System.Collections.Generic;
using System.Linq;
using System.Windows;
using System.Windows.Documents;
using System.Windows.Media;
using System.Windows.Media.Animation;
using Walterlv.Annotations;
namespace Walterlv.Demo.Media.Animation
{
public class ConnectedAnimation
{
internal ConnectedAnimation([NotNull] string key, [NotNull] UIElement source, [NotNull] EventHandler completed)
{
Key = key ?? throw new ArgumentNullException(nameof(key));
_source = source ?? throw new ArgumentNullException(nameof(source));
_reportCompleted = completed ?? throw new ArgumentNullException(nameof(completed));
}
public string Key { get; }
private readonly UIElement _source;
private readonly EventHandler _reportCompleted;
public bool TryStart([NotNull] UIElement destination)
{
return TryStart(destination, Enumerable.Empty<UIElement>());
}
public bool TryStart([NotNull] UIElement destination, [NotNull] IEnumerable<UIElement> coordinatedElements)
{
if (destination == null)
{
throw new ArgumentNullException(nameof(destination));
}
if (coordinatedElements == null)
{
throw new ArgumentNullException(nameof(coordinatedElements));
}
if (Equals(_source, destination))
{
return false;
}
// showing the animation?
// ready to connect the animation。
var adorner = ConnectedAnimationAdorner.FindFrom(destination);
var connectionHost = new ConnectedVisual(_source, destination);
adorner.Children.Add(connectionHost);
var storyboard = new Storyboard();
var animation = new DoubleAnimation(0.0, 1.0, new Duration(TimeSpan.FromSeconds(10.6)))
{
EasingFunction = new CubicEase {EasingMode = EasingMode.EaseInOut},
};
Storyboard.SetTarget(animation, connectionHost);
Storyboard.SetTargetProperty(animation, new PropertyPath(ConnectedVisual.ProgressProperty.Name));
storyboard.Children.Add(animation);
storyboard.Completed += (sender, args) =>
{
_reportCompleted(this, EventArgs.Empty);
//destination.ClearValue(UIElement.VisibilityProperty);
adorner.Children.Remove(connectionHost);
};
//destination.Visibility = Visibility.Hidden;
storyboard.Begin();
return true;
}
private class ConnectedVisual : DrawingVisual
{
public static readonly DependencyProperty ProgressProperty = DependencyProperty.Register(
"Progress", typeof(double), typeof(ConnectedVisual),
new PropertyMetadata(0.0, OnProgressChanged), ValidateProgress);
public double Progress
{
get => (double) GetValue(ProgressProperty);
set => SetValue(ProgressProperty, value);
}
private static bool ValidateProgress(object value) =>
value is double progress && progress >= 0 && progress <= 1;
private static void OnProgressChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
((ConnectedVisual) d).Render((double) e.NewValue);
}
public ConnectedVisual([NotNull] Visual source, [NotNull] Visual destination)
{
_source = source ?? throw new ArgumentNullException(nameof(source));
_destination = destination ?? throw new ArgumentNullException(nameof(destination));
_sourceBrush = new VisualBrush(source) {Stretch = Stretch.Fill};
_destinationBrush = new VisualBrush(destination) {Stretch = Stretch.Fill};
}
private readonly Visual _source;
private readonly Visual _destination;
private readonly Brush _sourceBrush;
private readonly Brush _destinationBrush;
private Rect _sourceBounds;
private Rect _destinationBounds;
protected override void OnVisualParentChanged(DependencyObject oldParent)
{
if (VisualTreeHelper.GetParent(this) == null)
{
return;
}
var sourceBounds = VisualTreeHelper.GetContentBounds(_source);
if (sourceBounds.IsEmpty)
{
sourceBounds = VisualTreeHelper.GetDescendantBounds(_source);
}
_sourceBounds = new Rect(
_source.PointToScreen(sourceBounds.TopLeft),
_source.PointToScreen(sourceBounds.BottomRight));
_sourceBounds = new Rect(
PointFromScreen(_sourceBounds.TopLeft),
PointFromScreen(_sourceBounds.BottomRight));
var destinationBounds = VisualTreeHelper.GetContentBounds(_destination);
if (destinationBounds.IsEmpty)
{
destinationBounds = VisualTreeHelper.GetDescendantBounds(_destination);
}
_destinationBounds = new Rect(
_destination.PointToScreen(destinationBounds.TopLeft),
_destination.PointToScreen(destinationBounds.BottomRight));
_destinationBounds = new Rect(
PointFromScreen(_destinationBounds.TopLeft),
PointFromScreen(_destinationBounds.BottomRight));
}
private void Render(double progress)
{
var bounds = new Rect(
(_destinationBounds.Left - _sourceBounds.Left) * progress + _sourceBounds.Left,
(_destinationBounds.Top - _sourceBounds.Top) * progress + _sourceBounds.Top,
(_destinationBounds.Width - _sourceBounds.Width) * progress + _sourceBounds.Width,
(_destinationBounds.Height - _sourceBounds.Height) * progress + _sourceBounds.Height);
using (var dc = RenderOpen())
{
dc.DrawRectangle(_sourceBrush, null, bounds);
dc.PushOpacity(progress);
dc.DrawRectangle(_destinationBrush, null, bounds);
dc.Pop();
}
}
}
private class ConnectedAnimationAdorner : Adorner
{
private ConnectedAnimationAdorner([NotNull] UIElement adornedElement)
: base(adornedElement)
{
Children = new VisualCollection(this);
IsHitTestVisible = false;
}
internal VisualCollection Children { get; }
protected override int VisualChildrenCount => Children.Count;
protected override Visual GetVisualChild(int index) => Children[index];
protected override Size ArrangeOverride(Size finalSize)
{
foreach (var child in Children.OfType<UIElement>())
{
child.Arrange(new Rect(child.DesiredSize));
}
return finalSize;
}
internal static ConnectedAnimationAdorner FindFrom([NotNull] Visual visual)
{
if (Window.GetWindow(visual)?.Content is UIElement root)
{
var layer = AdornerLayer.GetAdornerLayer(root);
if (layer != null)
{
var adorner = layer.GetAdorners(root)?.OfType<ConnectedAnimationAdorner>().FirstOrDefault();
if (adorner == null)
{
adorner = new ConnectedAnimationAdorner(root);
layer.Add(adorner);
}
return adorner;
}
}
throw new InvalidOperationException("指定的 Visual 尚未连接到可见的视觉树中,找不到用于承载动画的容器。");
}
internal static void ClearFor([NotNull] Visual visual)
{
if (Window.GetWindow(visual)?.Content is UIElement root)
{
var layer = AdornerLayer.GetAdornerLayer(root);
var adorner = layer?.GetAdorners(root)?.OfType<ConnectedAnimationAdorner>().FirstOrDefault();
if (adorner != null)
{
layer.Remove(adorner);
}
}
}
}
}
}
How to use?
private int index;
private void AnimationButton_Click(object sender, RoutedEventArgs e)
{
BeginConnectedAnimation((UIElement)sender, ConnectionDestination);
}
private async void BeginConnectedAnimation(UIElement source, UIElement destination)
{
var service = ConnectedAnimationService.GetForCurrentView(this);
service.PrepareToAnimate($"Test{index}", source);
var animation = service.GetAnimation($"Test{index}");
animation?.TryStart(destination);
// use different Key when click。
index++;
}
See https://walterlv.com/post/connected-animation-of-wpf.html

Related

How to set Xamarin.Forms Elements BindableProperties from a Custom Renderer?

I have been trying to set a bindable property value in my Element from my native control through a custom renderer. My native control is a view (painview) where you can draw and I am trying to get the drawing and set it, as a base64 string, to a bindable property Signature in my Element.
This is my Native Control
public class PaintView : View
{
Canvas _drawCanvas;
Bitmap _canvasBitmap;
readonly Paint _paint;
readonly Dictionary<int, MotionEvent.PointerCoords> _coords = new Dictionary<int, MotionEvent.PointerCoords>();
public Bitmap CanvasBitmap { get => _canvasBitmap; private set => _canvasBitmap = value; }
private readonly string TAG = nameof(PaintView);
public event EventHandler OnLineDrawn;
public PaintView(Context context) : base(context, null, 0)
{
_paint = new Paint() { Color = Color.Blue, StrokeWidth = 5f, AntiAlias = true };
_paint.SetStyle(Paint.Style.Stroke);
}
public PaintView(Context context, IAttributeSet attrs) : base(context, attrs) { }
public PaintView(Context context, IAttributeSet attrs, int defStyle) : base(context, attrs, defStyle) { }
protected override void OnSizeChanged(int w, int h, int oldw, int oldh)
{
base.OnSizeChanged(w, h, oldw, oldh);
_canvasBitmap = Bitmap.CreateBitmap(w, h, Bitmap.Config.Argb8888); // full-screen bitmap
_drawCanvas = new Canvas(_canvasBitmap); // the canvas will draw into the bitmap
}
public override bool OnTouchEvent(MotionEvent e)
{
switch (e.ActionMasked)
{
case MotionEventActions.Down:
{
int id = e.GetPointerId(0);
var start = new MotionEvent.PointerCoords();
e.GetPointerCoords(id, start);
_coords.Add(id, start);
return true;
}
case MotionEventActions.PointerDown:
{
int id = e.GetPointerId(e.ActionIndex);
var start = new MotionEvent.PointerCoords();
e.GetPointerCoords(id, start);
_coords.Add(id, start);
return true;
}
case MotionEventActions.Move:
{
for (int index = 0; index < e.PointerCount; index++)
{
var id = e.GetPointerId(index);
float x = e.GetX(index);
float y = e.GetY(index);
_drawCanvas.DrawLine(_coords[id].X, _coords[id].Y, x, y, _paint);
_coords[id].X = x;
_coords[id].Y = y;
OnLineDrawn?.Invoke(this, EventArgs.Empty);
}
Invalidate();
return true;
}
case MotionEventActions.PointerUp:
{
int id = e.GetPointerId(e.ActionIndex);
_coords.Remove(id);
return true;
}
case MotionEventActions.Up:
{
int id = e.GetPointerId(0);
_coords.Remove(id);
return true;
}
default:
return false;
}
}
protected override void OnDraw(Canvas canvas)
{
// Copy the off-screen canvas data onto the View from it's associated Bitmap (which stores the actual drawn data)
canvas.DrawBitmap(_canvasBitmap, 0, 0, null);
}
public void Clear()
{
_drawCanvas.DrawColor(Color.Black, PorterDuff.Mode.Clear); // Paint the off-screen buffer black
Invalidate(); // Call Invalidate to redraw the view
}
public void SetInkColor(Color color)
{
_paint.Color = color;
}
}
The property PaintView._canvasBitmap is the one I want to be set in my Xamarin.Form Element through my custom renderer.
This is my Custom Renderer
public class SketchViewRenderer : ViewRenderer<SketchView, PaintView>
{
public SketchViewRenderer(Context context) : base(context) { }
protected override void OnElementChanged(ElementChangedEventArgs<SketchView> e)
{
if (Control == null)
{
var paintView = new PaintView(Context);
paintView.SetInkColor(Element.InkColor.ToAndroid());
SetNativeControl(new PaintView(Context));
MessagingCenter.Subscribe<SketchView>(this, nameof(SketchView.OnClear), OnMessageClear);
Control.OnLineDrawn += PaintViewLineDrawn;
}
}
private void PaintViewLineDrawn(object sender, EventArgs e)
{
var sketchCrl = (ISketchViewController)Element;
if (sketchCrl == null) return;
try
{
Element.SetValueFromRenderer(SketchView.SignatureProperty, Utils.Utils.BitmapToBase64(Control.CanvasBitmap));
sketchCrl.SendSketchUpdated(Utils.Utils.BitmapToBase64(Control.CanvasBitmap));
}
catch (Exception ex)
{
Console.WriteLine(ex.Message);
}
}
protected override void OnElementPropertyChanged(object sender, PropertyChangedEventArgs e)
{
base.OnElementPropertyChanged(sender, e);
if (e.PropertyName == SketchView.InkColorProperty.PropertyName)
{
Control.SetInkColor(Element.InkColor.ToAndroid());
}
if (e.PropertyName == SketchView.ClearProperty.PropertyName)
{
if (Element.Clear) OnMessageClear(Element);
}
}
private void OnMessageClear(SketchView sender)
{
if (sender == Element) Control.Clear();
}
protected override void Dispose(bool disposing)
{
if (disposing)
{
MessagingCenter.Unsubscribe<SketchView>(this, nameof(SketchView.OnClear));
Control.OnLineDrawn -= PaintViewLineDrawn;
}
base.Dispose(disposing);
}
}
I have tried changing my Element.Signature property through the SketchViewRenderer.PaintViewLineDrawn(...) method without success. This has been prove when debugging my view model where the property has not been set as expected.
My Xamarin.Forms Element looks as follow
public class SketchView : View, IDoubleTappedController, ISketchViewController
{
public static readonly BindableProperty SignatureProperty = BindableProperty.Create(nameof(Signature), typeof(string), typeof(SketchView), null, defaultBindingMode: BindingMode.TwoWay);
public string Signature
{
get => (string)GetValue(SignatureProperty);
set => SetValue(SignatureProperty, value);
}
public static readonly BindableProperty MultiTouchEnabledProperty = BindableProperty.Create(nameof(MultiTouchEnabled), typeof(bool), typeof(SketchView), false);
public bool MultiTouchEnabled
{
get => (bool)GetValue(MultiTouchEnabledProperty);
set => SetValue(MultiTouchEnabledProperty, value);
}
public static readonly BindableProperty InkColorProperty = BindableProperty.Create(nameof(InkColor), typeof(Xamarin.Forms.Color), typeof(SketchView), Xamarin.Forms.Color.Azure);
public Xamarin.Forms.Color InkColor
{
get => (Xamarin.Forms.Color)GetValue(InkColorProperty);
set => SetValue(InkColorProperty, value);
}
public static readonly BindableProperty ClearProperty = BindableProperty.Create(nameof(Clear), typeof(bool), typeof(SketchView), false, defaultBindingMode: BindingMode.TwoWay);
public bool Clear
{
get => (bool)GetValue(ClearProperty);
set
{
SetValue(ClearProperty, value);
if (value) { OnClear(); }
}
}
public void OnClear()
{
MessagingCenter.Send(this, nameof(OnClear));
}
public void SetSignature(string signature)
{
Signature = signature;
}
void IDoubleTappedController.DoubleTapped()
{
throw new NotImplementedException();
}
void ISketchViewController.SendSketchUpdated(string signature)
{
Clear = false;
Signature = signature;
}
}
I have also tried using the SetValueFromRenderer() method from my Custom renderer, again, without success.
May you suggest to me what is the way to set an Element value from a Custom Renderer?
Thanks and kind regards,
Temo
The problem was that the field in my view model was set to null when comparing it with the value. Then throwing a TargetException letting the source buggy unable to be updated by the target.
public bool SetProperty<T>(ref T field, T value, [CallerMemberName] string propertyName = default)
{
if (value == null) return false;
if (field != null && field.Equals(value)) return false;
field = value;
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
return true;
}
Now, I make sure the field is not null before using the Equals operator.

TestStack White doesn't find TextBox in WPF Application

I use TestStack White framework to automate testing of a WPF Application
It needs to open modal window and access TextBox in it. Everything works well, but White can't find Textbox, although it finds other elements of window
I tried the following lines of code:
TestStack.White.UIItems.TextBox TextBox = CreateBranch.Get<TestStack.White.UIItems.TextBox>(SearchCriteria.byAutomationId("Title"));
where CreateBranch is modal window
I also tried (SearchCriteria.All), (SearchCriteria.ByControlType) and nothing works
Coded UI tool finds this element well by AutomationID, but I need to do it in White
UISpy and other similar tools recognize this control and see its AutomationID
This textbox is custom control, here's code for it, I changed namespace name for privacy:
using System;
using System.ComponentModel;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Input;
using System.Windows.Interactivity;
using System.Windows.Media;
namespace Test.Wpf.Controls.XTextBox
{
[TemplatePart(Name = "PART_Watermark", Type = typeof(TextBlock))]
[TemplatePart(Name = "PART_Pasword", Type = typeof(TextBlock))]
public class XTextBox : TextBox
{
#region Static
static XTextBox()
{
DefaultStyleKeyProperty.OverrideMetadata(typeof(XTextBox), new FrameworkPropertyMetadata(typeof(XTextBox)));
}
#endregion //Static
#region Fields
private TextBlock PART_Watermark;
private TextBlock PART_Pasword;
#endregion //Fields
#region DependencyProperties
public static readonly DependencyProperty WatermarkProperty = DependencyProperty.Register(
"Watermark",
typeof(String),
typeof(XTextBox),
new PropertyMetadata(String.Empty));
public static readonly DependencyProperty WatermarkVerticalAlignmentProperty = DependencyProperty.Register(
"WatermarkVerticalAlignment",
typeof(VerticalAlignment),
typeof(XTextBox),
new PropertyMetadata(VerticalAlignment.Stretch));
public static readonly DependencyProperty WatermarkForegroundProperty = DependencyProperty.Register(
"WatermarkForeground",
typeof(Brush),
typeof(XTextBox),
new PropertyMetadata(new SolidColorBrush(Colors.Black)));
public static readonly DependencyProperty WatermarkFontSizeProperty = DependencyProperty.Register(
"WatermarkFontSize",
typeof(Double),
typeof(XTextBox),
new PropertyMetadata(12.0));
public static readonly DependencyProperty IsFloatingProperty = DependencyProperty.Register(
"IsFloating",
typeof(Boolean),
typeof(XTextBox),
new PropertyMetadata(false));
public static readonly DependencyProperty IsAccessNegativeProperty = DependencyProperty.Register(
"IsAccessNegative",
typeof(Boolean),
typeof(XTextBox),
new PropertyMetadata(true));
public static readonly DependencyProperty IsDigitOnlyProperty = DependencyProperty.Register(
"IsDigitOnly",
typeof(Boolean),
typeof(XTextBox),
new PropertyMetadata(false));
public static readonly DependencyProperty MaxValueProperty = DependencyProperty.Register(
"MaxValue",
typeof(Single),
typeof(XTextBox),
new PropertyMetadata(Single.NaN));
public static readonly DependencyProperty IsPasswordProperty = DependencyProperty.Register(
"IsPassword",
typeof(Boolean),
typeof(XTextBox),
new PropertyMetadata(false));
public static readonly DependencyProperty VisibilityMainTextProperty = DependencyProperty.Register(
"VisibilityMainText",
typeof(Visibility),
typeof(XTextBox),
new PropertyMetadata(Visibility.Visible));
#endregion //DependencyProperties
#region Properties
[Description("Gets or sets the watermark title")]
public String Watermark
{
get { return (String)GetValue(WatermarkProperty); }
set { SetValue(WatermarkProperty, value); }
}
[Description("Gets or sets the watermark vertical alignment")]
public VerticalAlignment WatermarkVerticalAlignment
{
get { return (VerticalAlignment)GetValue(WatermarkVerticalAlignmentProperty); }
set { SetValue(WatermarkVerticalAlignmentProperty, value); }
}
[Description("Gets or sets the watermark title color")]
public Brush WatermarkForeground
{
get { return (Brush)GetValue(WatermarkVerticalAlignmentProperty); }
set { SetValue(WatermarkVerticalAlignmentProperty, value); }
}
[Description("Gets or sets the watermark title font size")]
public Double WatermarkFontSize
{
get { return (Double)GetValue(WatermarkVerticalAlignmentProperty); }
set { SetValue(WatermarkVerticalAlignmentProperty, value); }
}
[Description("Gets or sets the textbox floating mode")]
public Boolean IsFloating
{
get { return (Boolean)GetValue(IsFloatingProperty); }
set { SetValue(IsFloatingProperty, value); }
}
[Description("Gets or sets the textbox access of negative values")]
public Boolean IsAccessNegative
{
get { return (Boolean)GetValue(IsAccessNegativeProperty); }
set { SetValue(IsAccessNegativeProperty, value); }
}
[Description("Gets or sets the textbox chars type")]
public Boolean IsDigitOnly
{
get { return (Boolean)GetValue(IsDigitOnlyProperty); }
set { SetValue(IsDigitOnlyProperty, value); }
}
[Description("Gets or sets the max input value (enable in digit mode only)")]
public Single MaxValue
{
get { return (Single)GetValue(MaxValueProperty); }
set { SetValue(MaxValueProperty, value); }
}
[Description("Gets or sets the textbox is passwordbox")]
public Boolean IsPassword
{
get { return (Boolean)GetValue(IsPasswordProperty); }
set { SetValue(IsPasswordProperty, value); }
}
public Visibility VisibilityMainText
{
get { return (Visibility)GetValue(VisibilityMainTextProperty); }
set { SetValue(VisibilityMainTextProperty, value); }
}
#endregion //Properties
public override void OnApplyTemplate()
{
base.OnApplyTemplate();
PART_Watermark = GetTemplateChild("PART_Watermark") as TextBlock;
PART_Pasword = GetTemplateChild("PART_Pasword") as TextBlock;
SetWatermarkVisibility();
if (IsPassword)
{
VisibilityMainText = Visibility.Collapsed;
if (PART_Pasword != null)
{
PART_Pasword.Visibility = Visibility.Visible;
PART_Pasword.FontSize = 20;
}
}
else
{
VisibilityMainText = Visibility.Visible;
}
DataObject.AddPastingHandler(this, OnPaste);
}
protected void OnPaste(Object sender, DataObjectPastingEventArgs e)
{
try
{
var isText = e.SourceDataObject.GetDataPresent(DataFormats.UnicodeText, true);
if (!isText) return;
var text = e.SourceDataObject.GetData(DataFormats.UnicodeText) as String;
if (!String.IsNullOrEmpty(text))
{
if (IsDigitOnly)
{
if (!IsAccessNegative)
{
var ch = text[0];
if (ch == 45)
{
e.CancelCommand();
}
}
for (int i = 0; i < text.Length; i++)
{
if (i == 0)
{
if (IsAccessNegative && text[i] == 45)
{
continue;
}
}
if (!Char.IsDigit(text[0]))
e.CancelCommand();
}
}
}
}
catch (Exception)
{
// ignored
e.Handled = true;
}
}
protected override void OnTextChanged(TextChangedEventArgs e)
{
base.OnTextChanged(e);
SetWatermarkVisibility();
if (IsPassword)
{
PART_Pasword.Text = new String('•', Text.Length);
}
}
protected override void OnLostKeyboardFocus(KeyboardFocusChangedEventArgs e)
{
base.OnLostKeyboardFocus(e);
SetWatermarkVisibility();
}
protected override void OnGotKeyboardFocus(KeyboardFocusChangedEventArgs e)
{
base.OnGotKeyboardFocus(e);
if (PART_Watermark != null)
{
PART_Watermark.Visibility = Visibility.Hidden;
}
}
protected override void OnPreviewTextInput(TextCompositionEventArgs e)
{
base.OnPreviewTextInput(e);
if (e.Text.Length == 0)
{
e.Handled = true;
return;
}
if (IsDigitOnly)
{
var ch = e.Text[0];
if (!Char.IsDigit(ch) && ch != 8 && ch != 46)
{
if (!(IsAccessNegative && ch == 45))
e.Handled = true;
}
if (IsFloating)
{
if (ch == 46 && Text.IndexOf('.') != -1)
{
e.Handled = true;
return;
}
}
if (!IsAccessNegative)
{
if (ch == 45)
{
e.Handled = true;
}
}
}
}
#region Private
private void SetWatermarkVisibility()
{
if (PART_Watermark != null)
{
PART_Watermark.Visibility = (Text != String.Empty || IsKeyboardFocused)? Visibility.Hidden : Visibility.Visible;
}
}
#endregion
}
}
Screenshot from UISpy
Let me know if that works
TextBox = (TextBox)CreateBranch
.Get(SearchCriteria.ByAutomationId("Title").AndOfFramework(WindowsFramework.Wpf));
Edited after new source added
You have to create a specific AutomationPeer for your custom control and return it via the override of the method OnCreateAutomationPeer().
Your control is a subclass of the TextBox control so you can just return a new TextBoxAutomationPeer instance or create your custom AutomationPeer from it.
public class XTextBox : TextBox
{
...
protected override AutomationPeer OnCreateAutomationPeer()
{
return new XTextBoxAutomationPeer(this);
// or just use the TextBoxAutomationPeer
// return new TextBoxAutomationPeer(this);
}
...
}
The custom automation peer
public class XTextBoxAutomationPeer : TextBoxAutomationPeer
{
public XTextBoxAutomationPeer(XTextBox owner)
: base(owner)
{
}
protected override string GetClassNameCore()
{
return "XTextBox";
}
}
[SetUpFixture]
public class SETUP_THAT_WILL_GET_CALL_LATER
{
[OneTimeSetUp]
public void OneTimeSetUp()
{
var applicationDirectory = TestContext.CurrentContext.TestDirectory;
var applicationPath = Path.Combine(applicationDirectory, #"..\..\..\, "your debug folder path here", "your application.exe here");
Application = Application.Launch(applicationPath);
Thread.Sleep(2000);
Window = Application.GetWindow("Title of your application", InitializeOption.WithCache);
}
[OneTimeTearDown()]
public void OneTimeTearDown()
{
Window.Dispose();
Application.Dispose();
}
public static Application Application;
public static Window Window;
}
Then in your test
[Test]
public void yourtest()
{
var textBox = SETUP_THAT_WILL_GET_CALL_LATER.**Window.Get<TextBox>("Your textbox name here");**
}

Applying MahApps.Metro style to NavigationWindow

Has anyone had any luck applying the MahApps.Metro style to a NavigationWindow? I have implemented it for a Window just fine, but need to apply it to a NavigationWindow with Pages. I tried extending NavigationWindow and adding the modifications from MetroWindow like so, but no luck. The Window has a standard title bar and border, and the content is completely black.
using System;
using System.Windows;
using System.Windows.Input;
using System.Windows.Interop;
using System.Windows.Media;
using System.Windows.Navigation;
using MahApps.Metro.Native;
namespace MahApps.Metro.Controls
{
[TemplatePart(Name = PART_TitleBar, Type = typeof(UIElement))]
[TemplatePart(Name = PART_WindowCommands, Type = typeof(WindowCommands))]
public class MetroNavigationWindow : NavigationWindow
{
private const string PART_TitleBar = "PART_TitleBar";
private const string PART_WindowCommands = "PART_WindowCommands";
public static readonly DependencyProperty ShowIconOnTitleBarProperty = DependencyProperty.Register("ShowIconOnTitleBar", typeof(bool), typeof(MetroNavigationWindow), new PropertyMetadata(true));
public static readonly DependencyProperty ShowTitleBarProperty = DependencyProperty.Register("ShowTitleBar", typeof(bool), typeof(MetroNavigationWindow), new PropertyMetadata(true));
public static readonly DependencyProperty ShowMinButtonProperty = DependencyProperty.Register("ShowMinButton", typeof(bool), typeof(MetroNavigationWindow), new PropertyMetadata(true));
public static readonly DependencyProperty ShowCloseButtonProperty = DependencyProperty.Register("ShowCloseButton", typeof(bool), typeof(MetroNavigationWindow), new PropertyMetadata(true));
public static readonly DependencyProperty ShowMaxRestoreButtonProperty = DependencyProperty.Register("ShowMaxRestoreButton", typeof(bool), typeof(MetroNavigationWindow), new PropertyMetadata(true));
public static readonly DependencyProperty TitlebarHeightProperty = DependencyProperty.Register("TitlebarHeight", typeof(int), typeof(MetroNavigationWindow), new PropertyMetadata(30));
public static readonly DependencyProperty TitleCapsProperty = DependencyProperty.Register("TitleCaps", typeof(bool), typeof(MetroNavigationWindow), new PropertyMetadata(true));
public static readonly DependencyProperty SaveWindowPositionProperty = DependencyProperty.Register("SaveWindowPosition", typeof(bool), typeof(MetroNavigationWindow), new PropertyMetadata(false));
public static readonly DependencyProperty WindowPlacementSettingsProperty = DependencyProperty.Register("WindowPlacementSettings", typeof(IWindowPlacementSettings), typeof(MetroNavigationWindow), new PropertyMetadata(null));
public static readonly DependencyProperty TitleForegroundProperty = DependencyProperty.Register("TitleForeground", typeof(Brush), typeof(MetroNavigationWindow));
public static readonly DependencyProperty IgnoreTaskbarOnMaximizeProperty = DependencyProperty.Register("IgnoreTaskbarOnMaximize", typeof(bool), typeof(MetroNavigationWindow), new PropertyMetadata(false));
public static readonly DependencyProperty GlowBrushProperty = DependencyProperty.Register("GlowBrush", typeof(SolidColorBrush), typeof(MetroNavigationWindow), new PropertyMetadata(null));
public static readonly DependencyProperty FlyoutsProperty = DependencyProperty.Register("Flyouts", typeof(FlyoutsControl), typeof(MetroNavigationWindow), new PropertyMetadata(null));
public static readonly DependencyProperty WindowTransitionsEnabledProperty = DependencyProperty.Register("WindowTransitionsEnabled", typeof(bool), typeof(MetroNavigationWindow), new PropertyMetadata(true));
bool isDragging;
public bool WindowTransitionsEnabled
{
get { return (bool)this.GetValue(WindowTransitionsEnabledProperty); }
set { SetValue(WindowTransitionsEnabledProperty, value); }
}
public FlyoutsControl Flyouts
{
get { return (FlyoutsControl)GetValue(FlyoutsProperty); }
set { SetValue(FlyoutsProperty, value); }
}
public WindowCommands WindowCommands { get; set; }
public bool IgnoreTaskbarOnMaximize
{
get { return (bool)this.GetValue(IgnoreTaskbarOnMaximizeProperty); }
set { SetValue(IgnoreTaskbarOnMaximizeProperty, value); }
}
public Brush TitleForeground
{
get { return (Brush)GetValue(TitleForegroundProperty); }
set { SetValue(TitleForegroundProperty, value); }
}
public bool SaveWindowPosition
{
get { return (bool)GetValue(SaveWindowPositionProperty); }
set { SetValue(SaveWindowPositionProperty, value); }
}
public IWindowPlacementSettings WindowPlacementSettings
{
get { return (IWindowPlacementSettings)GetValue(WindowPlacementSettingsProperty); }
set { SetValue(WindowPlacementSettingsProperty, value); }
}
public bool ShowIconOnTitleBar
{
get { return (bool)GetValue(ShowIconOnTitleBarProperty); }
set { SetValue(ShowIconOnTitleBarProperty, value); }
}
public bool ShowTitleBar
{
get { return (bool)GetValue(ShowTitleBarProperty); }
set { SetValue(ShowTitleBarProperty, value); }
}
public bool ShowMinButton
{
get { return (bool)GetValue(ShowMinButtonProperty); }
set { SetValue(ShowMinButtonProperty, value); }
}
public bool ShowCloseButton
{
get { return (bool)GetValue(ShowCloseButtonProperty); }
set { SetValue(ShowCloseButtonProperty, value); }
}
public int TitlebarHeight
{
get { return (int)GetValue(TitlebarHeightProperty); }
set { SetValue(TitlebarHeightProperty, value); }
}
public bool ShowMaxRestoreButton
{
get { return (bool)GetValue(ShowMaxRestoreButtonProperty); }
set { SetValue(ShowMaxRestoreButtonProperty, value); }
}
public bool TitleCaps
{
get { return (bool)GetValue(TitleCapsProperty); }
set { SetValue(TitleCapsProperty, value); }
}
public SolidColorBrush GlowBrush
{
get { return (SolidColorBrush)GetValue(GlowBrushProperty); }
set { SetValue(GlowBrushProperty, value); }
}
public string WindowTitle
{
get { return TitleCaps ? Title.ToUpper() : Title; }
}
public MetroNavigationWindow()
{
Loaded += this.MetroWindow_Loaded;
}
private void MetroWindow_Loaded(object sender, RoutedEventArgs e)
{
VisualStateManager.GoToState(this, "AfterLoaded", true);
if (!ShowTitleBar)
{
//Disables the system menu for reasons other than clicking an invisible titlebar.
IntPtr handle = new WindowInteropHelper(this).Handle;
UnsafeNativeMethods.SetWindowLong(handle, UnsafeNativeMethods.GWL_STYLE, UnsafeNativeMethods.GetWindowLong(handle, UnsafeNativeMethods.GWL_STYLE) & ~UnsafeNativeMethods.WS_SYSMENU);
}
if (this.Flyouts == null)
{
this.Flyouts = new FlyoutsControl();
}
}
static MetroNavigationWindow()
{
DefaultStyleKeyProperty.OverrideMetadata(typeof(MetroNavigationWindow), new FrameworkPropertyMetadata(typeof(MetroNavigationWindow)));
}
public override void OnApplyTemplate()
{
base.OnApplyTemplate();
if (WindowCommands == null)
WindowCommands = new WindowCommands();
var titleBar = GetTemplateChild(PART_TitleBar) as UIElement;
if (ShowTitleBar && titleBar != null)
{
titleBar.MouseDown += TitleBarMouseDown;
titleBar.MouseUp += TitleBarMouseUp;
titleBar.MouseMove += TitleBarMouseMove;
}
else
{
MouseDown += TitleBarMouseDown;
MouseUp += TitleBarMouseUp;
MouseMove += TitleBarMouseMove;
}
}
protected override void OnStateChanged(EventArgs e)
{
if (WindowCommands != null)
{
WindowCommands.RefreshMaximiseIconState();
}
base.OnStateChanged(e);
}
protected void TitleBarMouseDown(object sender, MouseButtonEventArgs e)
{
var mousePosition = e.GetPosition(this);
bool isIconClick = ShowIconOnTitleBar && mousePosition.X <= TitlebarHeight && mousePosition.Y <= TitlebarHeight;
if (e.ChangedButton == MouseButton.Left)
{
if (isIconClick)
{
if (e.ClickCount == 2)
{
Close();
}
else
{
ShowSystemMenuPhysicalCoordinates(this, PointToScreen(new Point(0, TitlebarHeight)));
}
}
else
{
IntPtr windowHandle = new WindowInteropHelper(this).Handle;
UnsafeNativeMethods.ReleaseCapture();
var wpfPoint = PointToScreen(Mouse.GetPosition(this));
short x = Convert.ToInt16(wpfPoint.X);
short y = Convert.ToInt16(wpfPoint.Y);
int lParam = x | (y << 16);
UnsafeNativeMethods.SendMessage(windowHandle, Constants.WM_NCLBUTTONDOWN, Constants.HT_CAPTION, lParam);
if (e.ClickCount == 2 && (ResizeMode == ResizeMode.CanResizeWithGrip || ResizeMode == ResizeMode.CanResize))
{
WindowState = WindowState == WindowState.Maximized ? WindowState.Normal : WindowState.Maximized;
}
}
}
else if (e.ChangedButton == MouseButton.Right)
{
ShowSystemMenuPhysicalCoordinates(this, PointToScreen(mousePosition));
}
}
protected void TitleBarMouseUp(object sender, MouseButtonEventArgs e)
{
isDragging = false;
}
private void TitleBarMouseMove(object sender, MouseEventArgs e)
{
if (e.LeftButton != MouseButtonState.Pressed)
{
isDragging = false;
}
if (isDragging
&& WindowState == WindowState.Maximized
&& ResizeMode != ResizeMode.NoResize)
{
// Calculating correct left coordinate for multi-screen system.
Point mouseAbsolute = PointToScreen(Mouse.GetPosition(this));
double width = RestoreBounds.Width;
double left = mouseAbsolute.X - width / 2;
// Check if the mouse is at the top of the screen if TitleBar is not visible
if (!ShowTitleBar && mouseAbsolute.Y > TitlebarHeight)
return;
// Aligning window's position to fit the screen.
double virtualScreenWidth = SystemParameters.VirtualScreenWidth;
left = left + width > virtualScreenWidth ? virtualScreenWidth - width : left;
var mousePosition = e.MouseDevice.GetPosition(this);
// When dragging the window down at the very top of the border,
// move the window a bit upwards to avoid showing the resize handle as soon as the mouse button is released
Top = mousePosition.Y < 5 ? -5 : mouseAbsolute.Y - mousePosition.Y;
Left = left;
// Restore window to normal state.
WindowState = WindowState.Normal;
}
}
internal T GetPart<T>(string name) where T : DependencyObject
{
return (T)GetTemplateChild(name);
}
private static void ShowSystemMenuPhysicalCoordinates(Window window, Point physicalScreenLocation)
{
if (window == null) return;
var hwnd = new WindowInteropHelper(window).Handle;
if (hwnd == IntPtr.Zero || !UnsafeNativeMethods.IsWindow(hwnd))
return;
var hmenu = UnsafeNativeMethods.GetSystemMenu(hwnd, false);
var cmd = UnsafeNativeMethods.TrackPopupMenuEx(hmenu, Constants.TPM_LEFTBUTTON | Constants.TPM_RETURNCMD, (int)physicalScreenLocation.X, (int)physicalScreenLocation.Y, hwnd, IntPtr.Zero);
if (0 != cmd)
UnsafeNativeMethods.PostMessage(hwnd, Constants.SYSCOMMAND, new IntPtr(cmd), IntPtr.Zero);
}
}
}
To accomplish this, I am using a MetroWindow as my main navigation window, and a Frame within that MetroWindow which handles the navigation.
Navigation Window
<Controls:MetroWindow x:Class="TestWpfApplicationMahApps.Metro.NavWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:Controls="clr-namespace:MahApps.Metro.Controls;assembly=MahApps.Metro" Title="NavWindow" Height="300" Width="300">
<Window.Resources>
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
<ResourceDictionary Source="pack://application:,,,/MahApps.Metro;component/Styles/Colours.xaml" />
<ResourceDictionary Source="pack://application:,,,/MahApps.Metro;component/Styles/Fonts.xaml" />
<ResourceDictionary Source="pack://application:,,,/MahApps.Metro;component/Styles/Controls.xaml" />
<ResourceDictionary Source="pack://application:,,,/MahApps.Metro;component/Styles/Accents/Blue.xaml" />
<ResourceDictionary Source="pack://application:,,,/MahApps.Metro;component/Styles/Accents/BaseLight.xaml" />
</ResourceDictionary.MergedDictionaries>
</ResourceDictionary>
</Window.Resources>
<Frame Source="Page1.xaml" NavigationUIVisibility="Hidden"></Frame>
</Controls:MetroWindow>
This allows me to not change any of the navigation logic, it can be called from the NavigationService just like it was when using a NavigationWindow.
Page 1 .cs
/// <summary>
/// Interaction logic for Page1.xaml
/// </summary>
public partial class Page1 : Page
{
public Page1()
{
InitializeComponent();
}
private void button1_Click(object sender, RoutedEventArgs e)
{
NavigationService.Navigate(new Page2());
}
}

Access subclass of FrameworkElement from VisualTreeHelper.HitTest

I have a custom element class that is a subclass of FrameworkElement.
public class MyCustomElement : FrameworkElement
{
private VisualCollection children;
public MyCustomElement()
{
this.children = new VisualCollection(this);
this.children.Add(MyDrawingRoutines());
}
private DrawingVisual MyDrawingRoutines()
{
//...
}
protected override int VisualChildrenCount
{
get { return children.Count; }
}
protected override Visual GetVisualChild(int index)
{
if (index < 0 || index >= children.Count)
{
throw new ArgumentOutOfRangeException();
}
return children[index];
}
}
The UI holds a canvas in which these custom drawing elements are added and hit testing is performed.
public partial class MainWindow : Window
{
private MyCustomElement myCustomElement;
public MainWindow()
{
InitializeComponent();
myCustomElement = new MyCustomElement();
myCanvas.Children.Add(myCustomElement);
}
private void myCanvas_MouseLeftButtonUp(object sender, MouseButtonEventArgs e)
{
System.Windows.Point pt = e.GetPosition((UIElement)sender);
VisualTreeHelper.HitTest(this, null, new HitTestResultCallback(leftClickCallback), new PointHitTestParameters(pt));
}
public HitTestResultBehavior leftClickCallback(HitTestResult result)
{
if (result.VisualHit.GetType() == typeof(DrawingVisual))
{
if (((DrawingVisual)result.VisualHit).Opacity == 1.0)
{
((DrawingVisual)result.VisualHit).Opacity = 0.4;
}
else
{
((DrawingVisual)result.VisualHit).Opacity = 1.0;
}
}
return HitTestResultBehavior.Stop;
}
}
}
This code works as expected, but I cannot find a way to determine which MyCustomElement the detected DrawingVisual belongs. Right now, the opacity adjustment is done only superficially correct? I would like to change the opacity property on MyCustomElement, have the MyDrawingRoutines() method apply it, and have only the finished DrawingVisual drawn on the Canvas.
You should be able to cast the Parent property of the DrawingVisual to your MyCustomElement class:
public HitTestResultBehavior leftClickCallback(HitTestResult result)
{
var visual = result.VisualHit as DrawingVisual;
if (visual != null)
{
var element = visual.Parent as MyCustomElement;
if (element != null)
{
if (element.Opacity == 1.0)
{
element.Opacity = 0.4;
}
else
{
element.Opacity = 1.0;
}
}
}
return HitTestResultBehavior.Stop;
}
In case you need to get the parent of any visual (not just a ContainerVisual, which has the Parent property as shown above), you may use VisualTreeHelper.GetParent:
var visual = result.VisualHit;
var element = VisualTreeHelper.GetParent(visual) as MyCustomElement;

How to improve performance of this control?

I needed a marquee for a project and after much Googling and trial and error, I created one. However, the animation itself is a little jittery. I need some pointers on how to improve the performance of this. Thanks.
p.s. Some of the code may be redundant...
public class Marquee : Canvas
{
static Marquee()
{
DefaultStyleKeyProperty.OverrideMetadata(typeof(Marquee), new FrameworkPropertyMetadata(typeof(Marquee)));
}
private IList<string> _lines = new List<string>();
private IList<string> Lines
{
get
{
return _lines;
}
set
{
_lines = value;
}
}
public double FontSize
{
get { return (double)GetValue(FontSizeProperty); }
set { SetValue(FontSizeProperty, value); }
}
// Using a DependencyProperty as the backing store for FontSize. This enables animation, styling, binding, etc...
public static readonly DependencyProperty FontSizeProperty =
DependencyProperty.Register("FontSize", typeof(double), typeof(Marquee));
public Brush FontBrush
{
get { return (Brush)GetValue(FontBrushProperty); }
set { SetValue(FontBrushProperty, value); }
}
// Using a DependencyProperty as the backing store for FontBrush. This enables animation, styling, binding, etc...
public static readonly DependencyProperty FontBrushProperty =
DependencyProperty.Register("FontBrush", typeof(Brush), typeof(Marquee));
public string SourceFile { get; set; }
List<RenderTargetBitmap> _images = new List<RenderTargetBitmap>();
private void CreateBitmaps()
{
foreach (var line in Lines)
{
FormattedText ft = new FormattedText(line,
System.Globalization.CultureInfo.CurrentUICulture,
System.Windows.FlowDirection.LeftToRight,
new Typeface(FontFamily.Source),
FontSize,
FontBrush);
if (ft.Height == 0 || ft.Width == 0)
continue;
DrawingVisual drawingVisual = new DrawingVisual();
DrawingContext drawingContext = drawingVisual.RenderOpen();
drawingContext.DrawText(ft, new Point(0, 0));
drawingContext.Close();
RenderTargetBitmap bmp = new RenderTargetBitmap((int)ft.Width, (int)ft.Height, 72, 72, PixelFormats.Pbgra32);
bmp.Render(drawingVisual);
bmp.Freeze();
_images.Add(bmp);
}
}
private int nextImgIndex = 0;
private Image _Image;
private void GetNextImage()
{
if (_images.Count == 0)
return;
if (nextImgIndex >= _images.Count)
nextImgIndex = 0;
_Image.Source = _images.ElementAt(nextImgIndex++);
}
private string _curStr = null;
private string CurrentString
{
get
{
return _curStr;
}
set
{
_curStr = value;
}
}
TextBlock _textBlock = new TextBlock();
DispatcherTimer timer;
public Marquee()
{
Loaded += Marquee_Loaded;
FontSize = 12;
FontBrush = Brushes.Black;
if (Rate == 0d)
{
Rate = 150d;
}
this.CacheMode = new BitmapCache(2);
FontBrush.Freeze();
_Image = new Image();
_Image.CacheMode = new BitmapCache();
_Image.ClipToBounds = false;
FontFamily = new FontFamily("Calibri");
this.Children.Add(_Image);
}
void Marquee_Loaded(object sender, RoutedEventArgs e)
{
ReadFile();
CreateBitmaps();
CreateAnimation();
//throw new NotImplementedException();
}
//[ValueConversion(typeof(string), typeof(TimeSpan))]
//public class StringFormatConverter : IValueConverter
//{
// public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
// {
// string[] vals = ((string)value).Split(new[] { ':' });
// if (vals.Count() != 3)
// throw new FormatException(string.Format("Invalid timespan format : {0}", value));
// return new TimeSpan(int.Parse(vals[0]), int.Parse(vals[1]), int.Parse(vals[2]));
// }
// public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
// {
// throw new NotImplementedException();
// }
//}
DoubleAnimation anim;
TranslateTransform transform;
Duration duration;
public double Rate { get; set; }
void CreateAnimation()
{
if (CurrentString == null)
return;
GetNextImage();
transform = new TranslateTransform(Application.Current.MainWindow.ActualWidth, 0);
_Image.RenderTransform = transform;
var width = _Image.Source.Width;
double secs = (Application.Current.MainWindow.ActualWidth + width) / Rate;
duration = new Duration(TimeSpan.FromSeconds(secs));
anim = new DoubleAnimation(-width, duration);
anim.Completed += anim_Completed;
transform.BeginAnimation(TranslateTransform.XProperty, anim);
}
void anim_Completed(object sender, EventArgs e)
{
CreateAnimation();
}
double MeasureStringLength(string text)
{
if (text == null)
return 0;
FormattedText ft = new FormattedText(text,
System.Globalization.CultureInfo.CurrentUICulture,
System.Windows.FlowDirection.LeftToRight,
new Typeface(_textBlock.FontFamily.ToString()),
FontSize,
FontBrush);
return ft.Width;
}
public FontFamily FontFamily
{
get { return (FontFamily)GetValue(FontFamilyProperty); }
set { SetValue(FontFamilyProperty, value); }
}
// Using a DependencyProperty as the backing store for FontFamily. This enables animation, styling, binding, etc...
public static readonly DependencyProperty FontFamilyProperty =
DependencyProperty.Register("FontFamily", typeof(FontFamily), typeof(Marquee));
FormattedText GetFormattedText(string text)
{
if (text == null)
return null;
FormattedText ft = new FormattedText(text,
System.Globalization.CultureInfo.CurrentUICulture,
System.Windows.FlowDirection.LeftToRight,
new Typeface(_textBlock.FontFamily.ToString()),
FontSize,
FontBrush);
return ft;
}
void ReadFile()
{
if (SourceFile == null)
return;
StreamReader fR = new StreamReader(SourceFile);
string line;
while ((line = fR.ReadLine()) != null)
{
Lines.Add(line);
}
if (Lines.Count > 0)
CurrentString = Lines[0];
}
}
I'm modified your control in the following manner (removing the transform animation, using direct rendering instead of rendering to image cache):
public static readonly DependencyProperty OffsetProperty = DependencyProperty.Register("Offset", typeof(double),
typeof(Marquee), new FrameworkPropertyMetadata(0.0, FrameworkPropertyMetadataOptions.AffectsRender));
//
public double Offset {
get { return (double)GetValue(OffsetProperty); }
}
protected override void OnRender(DrawingContext dc) {
dc.DrawText(currentText, new Point(Offset, 0)); // direct render
}
int nextTextIndex = 0;
FormattedText currentText;
void GetNextText() {
if(formattedTexts.Count == 0) return;
currentText = formattedTexts[(nextTextIndex++) % formattedTexts.Count];
}
void CreateAnimation() {
if(CurrentString == null)
return;
GetNextText();
double width = currentText.Width;
double secs = (Application.Current.MainWindow.ActualWidth + width) / Rate;
duration = new Duration(TimeSpan.FromSeconds(secs));
anim = new DoubleAnimation(0, -width, duration);
anim.Completed += anim_Completed;
BeginAnimation(OffsetProperty, anim);
}
//
void anim_Completed(object sender, EventArgs e) {
anim.Completed -= anim_Completed;
CreateAnimation();
}
List<FormattedText> formattedTexts = new List<FormattedText>();
void CreateTexts() {
foreach(var line in Lines) {
FormattedText ft = new FormattedText(line,
System.Globalization.CultureInfo.CurrentUICulture,
System.Windows.FlowDirection.LeftToRight,
new Typeface(FontFamily.Source),
FontSize,
FontBrush);
if(ft.Height == 0 || ft.Width == 0)
continue;
formattedTexts.Add(ft);
}
}
Now it is more smoother to me.

Categories