WPF-Rendering with TranslateTransform and Label - wpf

i do have the following Code:
private static void AddElements(Canvas canvas)
{
double canvasHeight = canvas.Height;
double canvasWidth = canvas.Width;
double y0 = canvasHeight / 2;
double x0 = canvasWidth / 2;
// Defining the new Coordinate-Point (0,0) to mid auf Canvas
TranslateTransform tt = new TranslateTransform(x0, y0);
Line line1 = new Line();
line1.X1 = -350;
line1.Y1 = 0;
line1.X2 = 350;
line1.Y2 = 0;
line1.Stroke = Brushes.Black;
line1.StrokeThickness = 2.0;
line1.RenderTransform = tt;
canvas.Children.Add(line1);
Line line2 = new Line();
line2.X1 = 0;
line2.Y1 = -350;
line2.X2 = 0;
line2.Y2 = 350;
line2.Stroke = Brushes.Black;
line2.StrokeThickness = 2.0;
line2.RenderTransform = tt;
canvas.Children.Add(line2);
Label lblN = new Label();
lblN.Width = 50;
lblN.Background = Brushes.Red;
lblN.Margin = new System.Windows.Thickness(0, -350, 0, 0);
lblN.Content = $"N";
lblN.HorizontalContentAlignment = System.Windows.HorizontalAlignment.Center;
lblN.VerticalContentAlignment = System.Windows.VerticalAlignment.Center;
lblN.RenderTransform = tt;
lblN.Padding = new System.Windows.Thickness(0);
lblN.BorderBrush = Brushes.Black;
lblN.BorderThickness = new System.Windows.Thickness(2.0);
lblN.RenderTransform = tt;
canvas.Children.Add(lblN);
Label lblS = new Label();
lblS.Width = 50;
lblS.Background = Brushes.Red;
lblS.Margin = new System.Windows.Thickness(0, 350, 0, 0);
lblS.Content = $"S";
lblS.HorizontalContentAlignment = System.Windows.HorizontalAlignment.Center;
lblS.VerticalContentAlignment = System.Windows.VerticalAlignment.Center;
lblS.RenderTransform = tt;
lblS.Padding = new System.Windows.Thickness(0);
lblS.BorderBrush = Brushes.Black;
lblS.BorderThickness = new System.Windows.Thickness(2.0);
lblS.RenderTransform = tt;
canvas.Children.Add(lblS);
}
this method is called on an Menu-Eventhandler and it shows an coordinate system with (0,0) in the mid of the canvas. It should show a label with "N" at the top and a label with "S" at the bottom.
But i shows the attached image
Does anyone know, why lblN looks different than lblS ?
best regards
Volkhard
=============
if i set the height of both Label-Objects to 15
lblN.Height=15
:
lblS.Height=15
i get the following:
i expected the lblN to be more upper on the y-coordinate.

What's causing it
Through a bit of testing, I can definitely say that it's the lblN.Margin = new System.Windows.Thickness(0, -350, 0, 0); that's causing the problem. Apparently, when you give a Label a negative margin like that, it will move upwards only as far is it's Height, and then it will start expanding instead of just continuing to move. So you end up with a Label that's 350 tall. We could try to figure out why that is, but really, that would be missing the point.
Admittedly, I don't have any direct documentation to back up the following statement this, but from years of experience in WPF I feel I can say:
Margin is intended to be used to give space between elements in a dynamic layout, not to give an element an absolute position.
This behavior of the Label seems to strengthen the idea that using Margin in this way was not something that was planed for by the designers.
What you should do instead
Canvas has tools for giving an element a set position, yet nowhere do you use Canvas's SetLeft, SetTop, SetRight, or SetBottom. Take a look at the example on MSDN. You shouldn't need to use a TranslateTransform or set Margin at all. Instead, you should calculate where you want the element to be and use one of the above four listed methods to assign that position.
Extra Tip
Don't use canvas.Height and canvas.Width, use canvas.ActualHeight and canvas.ActualWidth instead. The first pair only work if you are explicitly setting the size of the Canvas (which it seems you are). But in a senario where the Canvas is dynamically sized, the first pair will be NaN. The second pair always return the actual size that the Canvas is.
This doesn't make a difference in your current use case, but it might later on. If you're doing calculations based on the actual size of an element (as opposed to the size you might want it to be), always use ActualHeight and ActualWidth.

Related

Precisely locating glyph text in WPF

I am writing a chemical molecule editor for Windows. As it has to be used in a Word Add-In I am restricted to using WPF for rendering structures. This is working quite well, apart from one tiny niggling point.
I use GlyphRuns for rendering atom labels and they are always displaced slightly to the right. If you look on the screenshot you can see there is a leading whitespace, especially with the H2N, and Hg atom labels. Why? The white background is what you get when you get the outline geometry of the glyph run.
The GlyphRun class is so badly documented that I cannot see which of the properties to amend to precisely locate the text where I want it. So any suggestions to try would be welcome.
UPDATE: I've been asked to provide a sample. The code is complex, but not gratuitously so, so I'm cutting it down to focus on the essentials:
public void MeasureAtCenter(Point center)
{
GlyphInfo = GlyphUtils.GetGlyphsAndInfo(Text, PixelsPerDip, out GlyphRun groupGlyphRun, center, _glyphTypeface, TypeSize);
//compensate the main offset vector for any descenders
Vector mainOffset = GlyphUtils.GetOffsetVector(groupGlyphRun, AtomShape.SymbolSize) + new Vector(0.0, -MaxBaselineOffset) + new Vector(-FirstBearing(groupGlyphRun), 0.0);
TextRun = groupGlyphRun;
TextMetrics = new AtomTextMetrics
{
BoundingBox = groupGlyphRun.GetBoundingBox(center + mainOffset),
Geocenter = center,
TotalBoundingBox = groupGlyphRun.GetBoundingBox(center + mainOffset),
OffsetVector = mainOffset
};
}
public static GlyphInfo GetGlyphs(string symbolText, GlyphTypeface glyphTypeFace, double size)
{
ushort[] glyphIndexes = new ushort[symbolText.Length];
double[] advanceWidths = new double[symbolText.Length];
double[] uprightBaselineOffsets = new double[symbolText.Length];
double totalWidth = 0;
for (int n = 0; n < symbolText.Length; n++)
{
ushort glyphIndex = glyphTypeFace.CharacterToGlyphMap[symbolText[n]];
glyphIndexes[n] = glyphIndex;
double width = glyphTypeFace.AdvanceWidths[glyphIndex] * size;
advanceWidths[n] = width;
double ubo = glyphTypeFace.DistancesFromHorizontalBaselineToBlackBoxBottom[glyphIndex] * size;
uprightBaselineOffsets[n] = ubo;
totalWidth += width;
}
return new GlyphInfo { AdvanceWidths = advanceWidths, Indexes = glyphIndexes, Width = totalWidth, UprightBaselineOffsets = uprightBaselineOffsets };
}
public static GlyphUtils.GlyphInfo GetGlyphsAndInfo(string symbolText, float pixelsPerDip, out GlyphRun hydrogenGlyphRun, Point point, GlyphTypeface glyphTypeFace, double symbolSize)
{
//measure the H atom first
var glyphInfo = GlyphUtils.GetGlyphs(symbolText, glyphTypeFace, symbolSize);
hydrogenGlyphRun = GlyphUtils.GetGlyphRun(glyphInfo, glyphTypeFace,
symbolSize, pixelsPerDip, point);
//work out exactly how much we should offset from the center to get to the bottom left
return glyphInfo;
}
public static Vector GetOffsetVector(GlyphRun glyphRun, double symbolSize)
{
Rect rect = glyphRun.ComputeInkBoundingBox();
//Vector offset = (rect.BottomLeft - rect.TopRight) / 2;
Vector offset = new Vector(-rect.Width / 2, glyphRun.GlyphTypeface.CapsHeight * symbolSize / 2);
return offset;
}
Indeed the GlyphRun class is a lot of work to use. I would suggest working with FormattedText objects instead. If there are performance issues, you can consider converting the FormattedText to Geometry once and reusing that. The MSDN docs provide a comparison of the different approaches.

Drawing multiple lines with DrawLines and DrawLine produces different result

I am trying to draw multiple lines on a winforms panel using it's graphics object in paint event. I am actually drawing a number of lines joining given points. So, first of all I did this,
private void panel1_Paint(object sender, PaintEventArgs e)
{
e.Graphics.DrawLines(new Pen(new SolidBrush(Color.Crimson), 3), PointFs.ToArray());
float width = 10;
float height = 10;
var circleBrush = new SolidBrush(Color.Crimson);
foreach (var point in PointFs)
{
float rectangleX = point.X - width / 2;
float rectangleY = point.Y - height / 2;
var r = new RectangleF(rectangleX, rectangleY, width, height);
e.Graphics.FillEllipse(circleBrush, r);
}
}
Which produces a result like the image below,
As you can see lines are drawn with having a little bit of extension at sharp turns, which is not expected. So, I changed the drawlines code to,
var pen = new Pen(new SolidBrush(Color.Crimson), 3);
for (int i = 1; i < PointFs.Count; i++)
{
e.Graphics.DrawLine(pen, PointFs[i - 1], PointFs[i]);
}
And now the drawing works fine.
Can anyone tell the difference between the two approaches?
I have just had the same problem (stumbled upon this question during my research), but I have now found the solution.
The problem is caused by the LineJoin property on the Pen used. This DevX page explains the different LineJoin types (see Figure 1 for illustrations). It seems that Miter is the default type, and that causes the "overshoot" when you have sharp angles.
I solved my problem by setting the LineJoin property to Bevel:
var pen = new Pen(new SolidBrush(Color.Crimson), 3);
pen.LineJoin = Drawing2D.LineJoin.Bevel;
Now DrawLines no longer overshoot the points.

Getting new position of a line after rotation

I need to find out new coordinates of line after rotation using RotateTransform method on a line.
For example, after this line:
line.RenderTransform = new RotateTransform(25, 0, 0);
line.X1 and the three other properties don't change. I have found some solution for shapes like rectangular, but it doesn't work for line. Line has different way how to treat with it.
EDIT: Thanks for your help H.B. The second way is exactly what I've been looking for.
SOLUTION:
Line line = new Line();
line.X1 = 10;
line.Y1 = 10;
line.X2 = 20;
line.Y2 = 30;
RotateTransform rotation = new RotateTransform();
Point p1 = rotation.Transform(new Point(line.X1, line.Y1));
Point p2 = rotation.Transform(new Point(line.X2, line.Y2));
line.X1 = p1.X;
line.Y1 = p1.Y;
line.X2 = p2.X;
line.Y2 = p2.Y;
This code rotates the line and sets a new values of coordinates to attached properties of this line.
You can transform points on their own:
var transform = new RotateTransform(25, 0, 0);
var newP1 = transform.Transform(new Point(line.X1,line.Y1));
//...
If you want a permanent transformation you can transform the start and end points and assign the new values to the respective properties.

WPF transform confusion

I Have a canvas full of objects that I zoom and pan using
this.source = VisualTreeHelper.GetChild(this, 0) as FrameworkElement;
this.zoomTransform = new ScaleTransform();
this.transformGroup = new TransformGroup();
this.transformGroup.Children.Add(this.zoomTransform);
this.transformGroup.Children.Add(this.translateTransform);
this.source.RenderTransform = this.transformGroup;
I then have a method that moves the canvas to a a certain point (in the original coordinates) to the center of the screen:
public void MoveTo(Point p)
{
var parent= VisualTreeHelper.GetParent(this) as FrameworkElement;
Point centerPoint = new Point(parent.ActualWidth / 2, parent.ActualHeight / 2);
double x = centerPoint.X - p.X;
double y = centerPoint.Y - p.Y;
x *= this.zoomTransform.ScaleX;
y *= this.zoomTransform.ScaleY;
this.translateTransform.BeginAnimation(TranslateTransform.XProperty, CreatePanAnimation(x), HandoffBehavior.Compose);
this.translateTransform.BeginAnimation(TranslateTransform.YProperty, CreatePanAnimation(y), HandoffBehavior.Compose);
}
private DoubleAnimation CreatePanAnimation(double toValue)
{
var da = new DoubleAnimation(toValue, new Duration(TimeSpan.FromMilliseconds(300)));
da.AccelerationRatio = 0.1;
da.DecelerationRatio = 0.9;
da.FillBehavior = FillBehavior.HoldEnd;
da.Freeze();
return da;
}
Everything works great until I actually have a zoom animation active after which the pan animation is inaccurate. I've tried different ways of calculation x,y and the centerpoint but can't seem to get it right. Any help appreciated, should be simple :)
I'd also like to make a method that both animates zooming and pans to a point, a little unsure on the ordering to accomplish that
Nevermind, I'm stupid
Point centerPoint = new Point(parent.ActualWidth / 2 / this.zoomTransform.ScaleX, parent.ActualHeight / 2 / this.zoomTransform.ScaleY);
I am still interested in how I can combine the scale and zoom animations though
this.translateTransform.BeginAnimation(TranslateTransform.XProperty, CreatePanAnimation(x), HandoffBehavior.Compose);
this.translateTransform.BeginAnimation(TranslateTransform.YProperty, CreatePanAnimation(y), HandoffBehavior.Compose);
this.zoomTransform.BeginAnimation(ScaleTransform.ScaleXProperty, CreateZoomAnimation(factor));
this.zoomTransform.BeginAnimation(ScaleTransform.ScaleYProperty, CreateZoomAnimation(factor));
wont work since the scale and pan values wont be synced...

How do you determine the width of the text in a WPF TreeViewItem at run time?

How do you determine the width of the text in a WPF TreeViewItem at run time?
I need to calculate an offset so I can draw a line from one leaf to the leaf of a different TreeView. All the 'width' properties return a size that is way bigger than the space taken up by the actual text of the node. It must be possible because the Select feature doesn't highlight the entire row. I'm writing the client in WPF and Silverlight.
You weren't very specific on the text or the tags, so I'm assuming you're taking about the .Net Framework's TreeViewItem.
There might be easier ways, but one possibility is to use the Graphics.MeasureString method. It gives you the size in pixels of a text when drawn using a specific font.
#mrphil: Sweet aborted fetus, that's scary
myTreeViewItem.Measure(new Size(Double.PositiveInfinity, Double.PositiveInfinity));
Size s = myTreeViewItem.DesiredSize;
return s.Width;
I have two solutions:
A) Uses the visual tree
TreeViewItem selected = (TreeViewItem)dataSourceTreeView.SelectedItem;
double textWidth = 0;
double expanderWidth = 0;
Grid grid = (Grid)VisualTreeHelper.GetChild(selected, 0);
ToggleButton toggleButton = (ToggleButton)VisualTreeHelper.GetChild(grid, 0);
expanderWidth = toggleButton.ActualWidth;
Border bd = (Border)VisualTreeHelper.GetChild(grid, 1);
textWidth = bd.ActualWidth;
B) If you don't want to use the visual tree
TreeViewItem selected = (TreeViewItem)dataSourceTreeView.SelectedItem;
double textWidth = 0;
Typeface typeface = new Typeface(selected.FontFamily,
selected.FontStyle, selected.FontWeight, selected.FontStretch);
GlyphTypeface glyphTypeface;
if (!typeface.TryGetGlyphTypeface(out glyphTypeface))
throw new InvalidOperationException("No glyphtypeface found");
string headerText = (string)selected.Header;
double size = selected.FontSize;
ushort[] glyphIndexes = new ushort[headerText.Length];
double[] advanceWidths = new double[headerText.Length];
for (int n = 0; n < headerText.Length; n++)
{
ushort glyphIndex = glyphTypeface.CharacterToGlyphMap[headerText[n]];
glyphIndexes[n] = glyphIndex;
double width = glyphTypeface.AdvanceWidths[glyphIndex] * size;
advanceWidths[n] = width;
textWidth += width;
}

Resources