Building custom TextBlock control in WPF - wpf

I have built custom WPF Control which unique function is displaying text. I tried using TextBlock from System.Windows.Controls namespace but it's not working for me (I have ~10000 strings with different position and too much memory loss). So I tried making my own control by inheriting FrameworkElement, overriding OnRender method which now contain single line:
drawingContext.DrawText(...);
But...
I get a little confusing result.
After comparing performance for 10000 objects, I realized that the time needed for creating and adding to Canvas is still ~10 sec, and memory usage for my application raises from ~32MB to ~60MB !!!
So no benefits at all.
Can anyone explain why this happens, and what is the other way to create simple (simple = allocate less memory, take less time to create) visual with two functions:
display text
set position (using thickness or TranslateTransform)
Thanks.

Check out AvalonEdit
Also not sure how you are storing the strings, but have you used StringBuilder before?

Here is my code (a little bit modified):
public class SimpleTextBlock : FrameworkElement
{
#region Static
private const double _fontSize = 12;
private static Point _emptyPoint;
private static Typeface _typeface;
private static LinearGradientBrush _textBrush;
public readonly static DependencyProperty TextWidthProperty;
static SimpleTextBlock()
{
_emptyPoint = new Point();
_typeface = new Typeface(new FontFamily("Sergoe UI"), FontStyles.Normal, FontWeights.Normal, FontStretches.Normal);
GradientStopCollection GSC = new GradientStopCollection(2);
GSC.Add(new GradientStop(Color.FromArgb(160, 255, 255, 255), 0.0));
GSC.Add(new GradientStop(Color.FromArgb(160, 180, 200, 255), 0.7));
_textBrush = new LinearGradientBrush(GSC, 90);
_textBrush.Freeze();
SimpleTextBlock.TextWidthProperty = DependencyProperty.Register(
"TextWidth",
typeof(double),
typeof(SimpleTextBlock),
new FrameworkPropertyMetadata(0.0d, FrameworkPropertyMetadataOptions.AffectsRender));
}
#endregion
FormattedText _formattedText;
public SimpleTextBlock(string text)
{
_formattedText = new FormattedText(text, System.Globalization.CultureInfo.InvariantCulture, FlowDirection.LeftToRight, _typeface, _fontSize, _textBrush);
}
public SimpleTextBlock(string text, FlowDirection FlowDirection)
{
_formattedText = new FormattedText(text, System.Globalization.CultureInfo.InvariantCulture, FlowDirection, _typeface, _fontSize, _textBrush);
}
protected override void OnRender(DrawingContext drawingContext)
{
_formattedText.MaxTextWidth = (double)GetValue(TextWidthProperty);
drawingContext.DrawText(_formattedText, _emptyPoint);
}
public double TextWidth
{
get { return (double)base.GetValue(TextWidthProperty); }
set { base.SetValue(TextWidthProperty, value); }
}
public double ActualTextWidth
{
get { return _formattedText.Width; }
}
public double ActualTextHeight
{
get { return _formattedText.Height; }
}
}

Since it sounds like we determined you should stylize a control like listbox, here are some examples of different things you can do:
Use Images as Items
Stylized and Binding
Honestly it all depends on what you want it to look like. WPF is great in how much control it gives you on how something looks.
Crazy example using a listbox to make the planet's orbits

Related

GMAP.NET adding labels underneath markers

I have just started using gmap.net and I was looking for the functionality of adding labels under the markers. I see there's tooltips but I would like to have a constant label under my marker with a one word description.
I searched for docs or other answers but I cannot find anything which leads me to believe that it is not implemented. If someone can verify this I would appreciate it.
You need to create your own custom marker.
Based on the source of GMapMarker and the derived GMarkerGoogle I came up with this simplified example:
public class GmapMarkerWithLabel : GMapMarker, ISerializable
{
private Font font;
private GMarkerGoogle innerMarker;
public string Caption;
public GmapMarkerWithLabel(PointLatLng p, string caption, GMarkerGoogleType type)
: base(p)
{
font = new Font("Arial", 14);
innerMarker = new GMarkerGoogle(p, type);
Caption = caption;
}
public override void OnRender(Graphics g)
{
if (innerMarker != null)
{
innerMarker.OnRender(g);
}
g.DrawString(Caption, font, Brushes.Black, new PointF(0.0f, innerMarker.Size.Height));
}
public override void Dispose()
{
if(innerMarker != null)
{
innerMarker.Dispose();
innerMarker = null;
}
base.Dispose();
}
#region ISerializable Members
void ISerializable.GetObjectData(SerializationInfo info, StreamingContext context)
{
base.GetObjectData(info, context);
}
protected GmapMarkerWithLabel(SerializationInfo info, StreamingContext context)
: base(info, context)
{
}
#endregion
}
Usage (assuming a GMap instance named gm):
GMapOverlay markerOverlay = new GMapOverlay("markers");
gm.Overlays.Add(markerOverlay);
var labelMarker = new GmapMarkerWithLabel(new PointLatLng(53.3, 9), "caption text", GMarkerGoogleType.blue);
markerOverlay.Markers.Add(labelMarker)
I'll answer here because this is the first question that pops up when looking to display a text marker for the WPF GMAP.NET library. Displaying a text marker with the WPF version of the library is actually much easier than in WinForms, or at least than the accepted answer.
The GMapMarker in WPF has a Shape property of type UIElement, which means you can provide a System.Windows.Controls.TextBlock object to display a text marker :
Markers.Add(new GMapMarker(new PointLatLng(latitude, longitude))
{
Shape = new System.Windows.Controls.TextBlock(new System.Windows.Documents.Run("Label"))
});
Since the marker displays the top left portion of the shape at the given position, you can play with the GMapMarker.Offset property to adjust the text position according to its dimensions. For instance, if you want the text to be horizontally centered on the marker's position :
var textBlock = new TextBlock(new Run("Label"));
textBlock.Measure(new Size(double.PositiveInfinity, double.PositiveInfinity));
textBlock.Arrange(new Rect(textBlock.DesiredSize));
Markers.Add(new GMapMarker(new PointLatLng(request.Latitude, request.Longitude))
{
Offset = new Point(-textBlock.ActualWidth / 2, 0),
Shape = textBlock
});
The solution to get the TextBlock's dimensions was quickly taken from this question, so if you need a more accurate way of getting the block's dimensions to play with the offset I suggest you start from there.

Displaying about a Meg of Text in WPF

I have a barebones WPF app that has about a Meg of ASCII text to display. I initially put a TextBlock in a WrapPanel in a ScrollViewer. This correctly scrolled and resized when I resized the window, but it was super slow! I needed something faster.
So I put the text in FormattedText, and rendered that using a custom control. That was much faster, but it didn't resize. So I made my custom control resize. But it would ReDraw too many times a second, so I made it only redraw every 100ms.
Much better. Rendering and Resizing still isn't great but it's much better than it was. But I lost scrolling.
Eventually I need a solution that does a lot - but for now I'm trying to have a solution that does a little: show a mem of text, wrap, have a scrollbar, and be performant. Eventually, I'd like it to scale to a gig of text, have colors inline, some mouseover/click events for portions of the text...
How can I make FormattedText (or perhaps more accurately, a DrawingVisual) have a Vertical Scrollbar?
Here's my FrameworkElement that shows my FormattedText:
using System;
using System.Windows;
using System.Windows.Media;
namespace Recall
{
public class LightweightTextBox : FrameworkElement
{
private VisualCollection _children;
private FormattedText _formattedText;
private System.Threading.Timer _resizeTimer;
private const int _resizeDelay = 100;
public double MaxTextWidth
{
get { return this._formattedText.MaxTextWidth; }
set { this._formattedText.MaxTextWidth = value; }
}
public LightweightTextBox(FormattedText formattedText)
{
this._children = new VisualCollection(this);
this._formattedText = formattedText;
DrawingVisual drawingVisual = new DrawingVisual();
DrawingContext drawingContext = drawingVisual.RenderOpen();
drawingContext.DrawText(this._formattedText, new Point(0, 0));
drawingContext.Close();
_children.Add(drawingVisual);
this.SizeChanged += new SizeChangedEventHandler(LightweightTextBox_SizeChanged);
}
void LightweightTextBox_SizeChanged(object sender, SizeChangedEventArgs e)
{
this.MaxTextWidth = e.NewSize.Width;
if (_resizeTimer != null)
_resizeTimer.Change(_resizeDelay, System.Threading.Timeout.Infinite);
else
_resizeTimer = new System.Threading.Timer(new System.Threading.TimerCallback(delegate(object state)
{
ReDraw();
if (_resizeTimer == null) return;
_resizeTimer.Dispose();
_resizeTimer = null;
}), null, _resizeDelay, System.Threading.Timeout.Infinite);
}
public void ReDraw()
{
this.Dispatcher.Invoke((Action)(() =>
{
var dv = _children[0] as DrawingVisual;
DrawingContext drawingContext = dv.RenderOpen();
drawingContext.DrawText(this._formattedText, new Point(0, 0));
drawingContext.Close();
}));
}
//===========================================================
//Overrides
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];
}
}
}
For simple text a readonly TextBox is pretty good. For more complex matters you can use FlowDocuments (which can be hosted in a FlowDocumentScrollViewer), TextBlocks also host flow content but are not intended for larger amounts.
MSDN:
TextBlock is not optimized for scenarios that need to display more than a few lines of content; for such scenarios, a FlowDocument coupled with an appropriate viewing control is a better choice than TextBlock, in terms of performance. After TextBlock, FlowDocumentScrollViewer is the next lightest-weight control for displaying flow content, and simply provides a scrolling content area with minimal UI. FlowDocumentPageViewer is optimized around "page-at-a-time" viewing mode for flow content. Finally, FlowDocumentReader supports the richest set functionality for viewing flow content, but is correspondingly heavier-weight.

Animate WPF Drag Adorner movement

in our application we use a adorner for some fake drag & drop. The adorner doesn't follow to mouse but is set to some specific coordinates on the screen when the mouse moves.
double xPosLocation = (int)(dividerDistanceXAxis * virtualPosition) + YAxisData.SpacingLeft;
double yPosLocation = CalculateValueToYPosition(VirtualPriceOfVehicle);
DragAdorner.UpdatePosition(xPosLocation - offsetX, yPosLocation + offsetY);
This works fine so far. But with a code like this the adorner jumps from one location to another. This is how it was supposed to work but it would be nice to have some smooth movement transitions when the adorner it set to a new position.
Can it be done? Since I see no position property I guess it is not possible to do a double-animation or something like that.
You have to use a DependencyProperty for animation
public class DragAdorner
{
public Point Position
{
get { return (Point)GetValue(PositionProperty); }
set { SetValue(PositionProperty, value); }
}
// Using a DependencyProperty as the backing store for PositionProperty. This enables animation, styling, binding, etc...
public static readonly DependencyProperty PositionProperty =
DependencyProperty.Register("Position", typeof(Point), typeof(DragAdorner), new UIPropertyMetadata(PositionChanged));
private static void PositionChanged(DependencyObject obj, DependencyPropertyChangedEventArgs e)
{
UpdatePosition(Position);
}
}
Then you could do something like this (not tested)
double xPosLocation = (int)(dividerDistanceXAxis * virtualPosition) + YAxisData.SpacingLeft;
double yPosLocation = CalculateValueToYPosition(VirtualPriceOfVehicle);
Point newLocation = new Point(xPosLocation, yPosLocation);
PointAnimation myPointAnimation = new PointAnimation();
myPointAnimation.From = DragAdorner.Position;
myPointAnimation.To = newLocation;
myPointAnimation.Duration = new Duration(TimeSpan.FromSeconds(1));
Storyboard myStoryboard = new Storyboard();
myStoryboard.Children.Add(myPointAnimation);
Storyboard.SetTargetName(myPointAnimation, DragAdorner.Position);
myStoryboard.Begin();

How to draw connecting lines between two controls on a grid WPF

I am creating controls (say button) on a grid. I want to create a connecting line between controls.
Say you you do mousedown on one button and release mouse over another button. This should draw a line between these two buttons.
Can some one help me or give me some ideas on how to do this?
Thanks in advance!
I'm doing something similar; here's a quick summary of what I did:
Drag & Drop
For handling the drag-and-drop between controls there's quite a bit of literature on the web (just search WPF drag-and-drop). The default drag-and-drop implementation is overly complex, IMO, and we ended up using some attached DPs to make it easier (similar to these). Basically, you want a drag method that looks something like this:
private void onMouseLeftButtonDown(object sender, MouseButtonEventArgs e)
{
UIElement element = sender as UIElement;
if (element == null)
return;
DragDrop.DoDragDrop(element, new DataObject(this), DragDropEffects.Move);
}
On the target, set AllowDrop to true, then add an event to Drop:
private void onDrop(object sender, DragEventArgs args)
{
FrameworkElement elem = sender as FrameworkElement;
if (null == elem)
return;
IDataObject data = args.Data;
if (!data.GetDataPresent(typeof(GraphNode))
return;
GraphNode node = data.GetData(typeof(GraphNode)) as GraphNode;
if(null == node)
return;
// ----- Actually do your stuff here -----
}
Drawing the Line
Now for the tricky part! Each control exposes an AnchorPoint DependencyProperty. When the LayoutUpdated event is raised (i.e. when the control moves/resizes/etc), the control recalculates its AnchorPoint. When a connecting line is added, it binds to the DependencyProperties of both the source and destination's AnchorPoints. [EDIT: As Ray Burns pointed out in the comments the Canvas and grid just need to be in the same place; they don't need to be int the same hierarchy (though they may be)]
For updating the position DP:
private void onLayoutUpdated(object sender, EventArgs e)
{
Size size = RenderSize;
Point ofs = new Point(size.Width / 2, isInput ? 0 : size.Height);
AnchorPoint = TransformToVisual(node.canvas).Transform(ofs);
}
For creating the line class (can be done in XAML, too):
public sealed class GraphEdge : UserControl
{
public static readonly DependencyProperty SourceProperty = DependencyProperty.Register("Source", typeof(Point), typeof(GraphEdge), new FrameworkPropertyMetadata(default(Point)));
public Point Source { get { return (Point) this.GetValue(SourceProperty); } set { this.SetValue(SourceProperty, value); } }
public static readonly DependencyProperty DestinationProperty = DependencyProperty.Register("Destination", typeof(Point), typeof(GraphEdge), new FrameworkPropertyMetadata(default(Point)));
public Point Destination { get { return (Point) this.GetValue(DestinationProperty); } set { this.SetValue(DestinationProperty, value); } }
public GraphEdge()
{
LineSegment segment = new LineSegment(default(Point), true);
PathFigure figure = new PathFigure(default(Point), new[] { segment }, false);
PathGeometry geometry = new PathGeometry(new[] { figure });
BindingBase sourceBinding = new Binding {Source = this, Path = new PropertyPath(SourceProperty)};
BindingBase destinationBinding = new Binding { Source = this, Path = new PropertyPath(DestinationProperty) };
BindingOperations.SetBinding(figure, PathFigure.StartPointProperty, sourceBinding);
BindingOperations.SetBinding(segment, LineSegment.PointProperty, destinationBinding);
Content = new Path
{
Data = geometry,
StrokeThickness = 5,
Stroke = Brushes.White,
MinWidth = 1,
MinHeight = 1
};
}
}
If you want to get a lot fancier, you can use a MultiValueBinding on source and destination and add a converter which creates the PathGeometry. Here's an example from GraphSharp. Using this method, you could add arrows to the end of the line, use Bezier curves to make it look more natural, route the line around other controls (though this could be harder than it sounds), etc., etc.
See also
http://social.msdn.microsoft.com/Forums/en-US/wpf/thread/dd246675-bc4e-4d1f-8c04-0571ea51267b
http://www.codeproject.com/KB/WPF/WPFDiagramDesigner_Part1.aspx
http://www.codeproject.com/KB/WPF/WPFDiagramDesigner_Part2.aspx
http://www.codeproject.com/KB/WPF/WPFDiagramDesigner_Part3.aspx
http://www.codeproject.com/KB/WPF/WPFDiagramDesigner_Part4.aspx
http://www.syncfusion.com/products/user-interface-edition/wpf/diagram
http://www.mindscape.co.nz/products/wpfflowdiagrams/

Creating SelectionBorder: Bit in the face by decimal rounding?

I am currently implementing a class called SelectionBorder in WPF. It's derived from the Shape class.
It basically looks like this:
public class SelectionBorder : Shape
{
public Point StartPoint {get; set;}
public PointCollection Points {get; set;}
public double StrokeLength {get; set;}
protected override Geometry DefiningGeometry{
get{
//Magic!
}
}
}
The StartPoint and Points properties determine the corners of the border. The border is a typical stroked line border (one black stroke, one invisible stroke like that: - - - -)
The problem that I have now is that since the corner points are freely choosable it's pretty common that the count of strokes (meaning black and invisible strokes) is not even (in fact not even an integer) and therefore the first stroke looks longer than the others (visible in the picture). This maybe doesn't seem to be a big deal but I later want to animate the border so that the strokes circle round the content. When doing this animation the tiny flaw in the static view becomes clearly visible and in my opinion is highly disturbing.
alt text http://img14.imageshack.us/img14/2874/selectionborder.png
The problem is that I tried to determine a StrokeLength that gets as close to the original StrokeLength as possible and creates an even number of strokes. However the problem I've run into is that WPF (obviously) can't display the whole precision of a double decimal StrokeLength and therefore the resulting stroke number is uneven once again.
Is there any workaround for this problem? Do you probably have another solution for my problem?
Thanks in advance!
EDIT: I retested and reviewed the code after a little break for fitness today and after all it happens only on very big StrokeLengths. I plan to use StrokeLengths of 2 where the little animation jumping does matter much less than I originally thought.
You could make more than one corner "un-matched" in that regard. For example, instead of having one point be the "source" and "destination" of the animated dashes, you could pick 2 points. One would be the "source", dashes appearing to march away from it in 2 directions, and another point be the "destination", where dashes converge and disappear.
GIMP, for example, animates selection dashed lines in this way and seems to pick a point closest to the lower-left for the "source" and a point closest to the upper-right for the "destination".
You could come up with some other scheme, as well.
Just remember that while it may look disturbing to you, most users will not care.
I just found a way that makes it way easier to create such an animated SelectionBorder.
Instead of creating the animation by moving an self-created AnimationPoint through animation I just animated the StrokeDashOffset property natively provided by the Shape class and setting the StrokeDashArray to define the StrokeLength.
It would look like this in XAML:
<namespace:SelectionBorder StrokeDashArray="2" AnimationDuration="0:0:1" Stroke="Black" />
The class looks like this:
public class SelectionBorder : Shape
{
private DoubleAnimation m_Animation;
private bool m_AnimationStarted;
public SelectionBorder()
{
IsVisibleChanged += OnIsVisibleChanged;
}
protected void OnIsVisibleChanged(object sender, DependencyPropertyChangedEventArgs e)
{
if (Visibility == Visibility.Visible)
{
StartAnimation();
}
else
{
StopAnimation();
}
}
public void StartAnimation()
{
if (m_AnimationStarted)
return;
if (m_Animation == null)
{
m_Animation = CreateAnimation();
}
BeginAnimation(StrokeDashOffsetProperty, m_Animation);
m_AnimationStarted = true;
}
protected virtual DoubleAnimation CreateAnimation()
{
DoubleAnimation animation = new DoubleAnimation();
animation.From = 0;
if (StrokeDashArray.Count == 0)
animation.To = 4;
else
animation.To = StrokeDashArray.First() * 2;
animation.Duration = AnimationDuration;
animation.RepeatBehavior = RepeatBehavior.Forever;
return animation;
}
public void StopAnimation()
{
if (m_AnimationStarted)
{
BeginAnimation(StrokeDashOffsetProperty, null);
m_AnimationStarted = false;
}
}
#region Dependency Properties
public Duration AnimationDuration
{
get { return (Duration)GetValue(AnimationDurationProperty); }
set { SetValue(AnimationDurationProperty, value); }
}
public static readonly DependencyProperty AnimationDurationProperty =
DependencyProperty.Register("AnimationDuration", typeof(Duration), typeof(SelectionBorder), new UIPropertyMetadata(new Duration(TimeSpan.FromSeconds(0.5))));
#endregion Dependency Properties
protected override Geometry DefiningGeometry
{
get
{
double width = (double.IsNaN(Width)) ? ((Panel)Parent).ActualWidth : Width;
double height = (double.IsNaN(Height)) ? ((Panel)Parent).ActualHeight : Height;
RectangleGeometry geometry = new RectangleGeometry(new Rect(0, 0, width, height));
return geometry;
}
}
}

Resources