Precisely locating glyph text in WPF - 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.

Related

GlyphRun and the exact position of the cursor (WPF)

I would like to draw a text exactly in the place of the mouse cursor.
Because I need very high performance, I would like to use GlyphRun.
Everything works almost well, but unfortunately my text is slightly below the cursor.
Can someone help me modify this method to eliminate this vertical shift?
Now it looks like this
My expectation (the text touches the cursor)
My code:
void MyDrawer_MouseMove(object sender, MouseEventArgs e)
{
Test1();
}
void Test1()
{
MyDrawer.DeleteVisual(Dv);
MyDrawer.Cursor = Cursors.Cross;
string text = "Hello Word";
double size = 40;
Dv = new DrawingVisual();
using (var dc = Dv.RenderOpen())
{
Typeface typeface = new Typeface("Arial");
if (typeface.TryGetGlyphTypeface(out GlyphTypeface glyphTypeface))
{
ushort[] glyphIndexes = new ushort[text.Length];
double[] advanceWidths = new double[text.Length];
for (int i = 0; i < text.Length; i++)
{
ushort glyphIndex = glyphTypeface.CharacterToGlyphMap[text[i]];
glyphIndexes[i] = glyphIndex;
double width = glyphTypeface.AdvanceWidths[glyphIndex] * size;
advanceWidths[i] = width;
}
Point origin = Mouse.GetPosition(MyDrawer);
//Move text belowe the cursor
origin = new Point { X = origin.X, Y = origin.Y + (glyphTypeface.Baseline * size) };
GlyphRun glyphRun = new GlyphRun(glyphTypeface, 0, false, size,
glyphIndexes, origin, advanceWidths, null, null, null, null,
null, null);
dc.DrawGlyphRun(Brushes.Red, glyphRun);
MyDrawer.AddVisual(Dv);
}
}
}
Of course, this is only a test, in practice it will not affect the cursor, but the indicated point and text will be much more than in this example.
I bumped into this question in search for the same issue.
Glyph content (GlyphRun for that matter) is wrapped in a black-box.
The black-box contain a bit of padding on the top.
Like so : Rought skecth of the glyph run
// To get the box size
var blackBoxHeight = glyphTypeface.Height * fontSize;
// To get the actual character height
var characterHeight = glyphTypeface.Baseline * fontSize;
// To get the padding inside the black box
var blackBoxPadding = blackBoxHeight - characterHeight;
// set the point to draw a little bit up (skip the padding)
origin.Y -= blackBoxPadding;
// Create the glyph
var run = new GlyphRun
(
glyphTypeface: glyphTypeface,
bidiLevel: 0,
isSideways: false,
renderingEmSize: fontSize,
pixelsPerDip: 1.0f,
glyphIndices: indices,
baselineOrigin: origin, /* The point containing the padding offset */
advanceWidths: widths,
glyphOffsets: null,
characters: null,
deviceFontName: null,
clusterMap: null,
caretStops: null,
language: null
);
The other answer lacks of some details.
There is actually no magic or unreasoned paddings. The picture shows relations of glyphTypeface height parameters to the displayed GlyphRun.
The code that calculates corresponding line vertical positions based on the GlyphRun origin Y is shown below:
var baseLine = origin.Y;
var boxTop = baseLine - glyphTypeface.Baseline * FontSize;
var boxBottom = boxTop + glyphTypeface.Height * FontSize;
var capsHeight = baseLine - glyphTypeface.CapsHeight * FontSize;
var lowHeight = baseLine - glyphTypeface.XHeight * FontSize;

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.

Is there a more efficient way to detect polygon overlap/intersection than PathGeometry.FillContainsWithDetail()?

I have a method that is gobbling up 25% of my cpu time. I call this method about 27,000 times per second. (Yup, lots of calls since it's updating frequently). I am wondering if anybody knows a faster way to detect if 2 polygons overlap. Basically, I have to check the moving objects on the screen against stationary objects on the screen. I am using PathGeometry and the two calls below are using up 25% of the cpu time used by my program. The PointCollection objects I am passing just contain 4 points representing 4 corners of a polygon. They may not create a rectangular area, but all the points are connected. I guess a trapazoid would be the shape.
These methods are short and were very easy to implement, but I think I might want to opt for a more complicated solution if I can have it run more quickly than the code below. Any ideas?
public static bool PointCollectionsOverlap(PointCollection area1, PointCollection area2)
{
PathGeometry pathGeometry1 = GetPathGeometry(area1);
PathGeometry pathGeometry2 = GetPathGeometry(area2);
return pathGeometry1.FillContainsWithDetail(pathGeometry2) != IntersectionDetail.Empty;
}
public static PathGeometry GetPathGeometry(PointCollection polygonCorners)
{
List<PathSegment> pathSegments = new List<PathSegment>
{ new PolyLineSegment(polygonCorners, true) };
PathGeometry pathGeometry = new PathGeometry();
pathGeometry.Figures.Add(new PathFigure(polygonCorners[0], pathSegments, true));
return pathGeometry;
}
Ok, after lots of research and finding many partial answers, but none that fully answered the question, I have found a faster way and it is actually about 4.6 times faster than the old way.
I created a special test app to test the speed this. You can find the test app here. If you download it, you can see a checkbox at the top of the app. Check and uncheck it to switch back and forth between the old way and the new way. The app generates a bunch of random polygons and the borders of the polygons change to white when they intersect another polygon. The numbers to the left of the 'Redraw' button are to allow you to enter the Number of Polygons, Max Length of a side, and Max offset from square (to make them less square and more odd shaped). Push 'Refresh' to clear and regenerate new polygons with the settings you've entered.
Anyway, here is the code for the two different implementations. You pass in a collection of the points that make up each polygon. The old way uses less code, but is 4.6 times slower than the new way.
Oh, one quick note. The new way has a couple calls to 'PointIsInsidePolygon'. These were necessary because without it, the method returned false when one polygon was entirely contained within a different polygon. But the PointIsInsidePolygon method fixes that problem.
Hope this all helps somebody else out with polygon intercepts and overlaps.
Old Way (4.6 times slower. YES REALLY 4.6 TIMES slower):
public static bool PointCollectionsOverlap_Slow(PointCollection area1, PointCollection area2)
{
PathGeometry pathGeometry1 = GetPathGeometry(area1);
PathGeometry pathGeometry2 = GetPathGeometry(area2);
bool result = pathGeometry1.FillContainsWithDetail(pathGeometry2) != IntersectionDetail.Empty;
return result;
}
public static PathGeometry GetPathGeometry(PointCollection polygonCorners)
{
List<PathSegment> pathSegments = new List<PathSegment> { new PolyLineSegment(polygonCorners, true) };
PathGeometry pathGeometry = new PathGeometry();
pathGeometry.Figures.Add(new PathFigure(polygonCorners[0], pathSegments, true));
return pathGeometry;
}
New Way (4.6 times faster. YES REALLY 4.6 TIMES faster):
public static bool PointCollectionsOverlap_Fast(PointCollection area1, PointCollection area2)
{
for (int i = 0; i < area1.Count; i++)
{
for (int j = 0; j < area2.Count; j++)
{
if (lineSegmentsIntersect(area1[i], area1[(i + 1) % area1.Count], area2[j], area2[(j + 1) % area2.Count]))
{
return true;
}
}
}
if (PointCollectionContainsPoint(area1, area2[0]) ||
PointCollectionContainsPoint(area2, area1[0]))
{
return true;
}
return false;
}
public static bool PointCollectionContainsPoint(PointCollection area, Point point)
{
Point start = new Point(-100, -100);
int intersections = 0;
for (int i = 0; i < area.Count; i++)
{
if (lineSegmentsIntersect(area[i], area[(i + 1) % area.Count], start, point))
{
intersections++;
}
}
return (intersections % 2) == 1;
}
private static double determinant(Vector vector1, Vector vector2)
{
return vector1.X * vector2.Y - vector1.Y * vector2.X;
}
private static bool lineSegmentsIntersect(Point _segment1_Start, Point _segment1_End, Point _segment2_Start, Point _segment2_End)
{
double det = determinant(_segment1_End - _segment1_Start, _segment2_Start - _segment2_End);
double t = determinant(_segment2_Start - _segment1_Start, _segment2_Start - _segment2_End) / det;
double u = determinant(_segment1_End - _segment1_Start, _segment2_Start - _segment1_Start) / det;
return (t >= 0) && (u >= 0) && (t <= 1) && (u <= 1);
}

Finding specific pixel colors of a BitmapImage

I have a WPF BitmapImage which I loaded from a .JPG file, as follows:
this.m_image1.Source = new BitmapImage(new Uri(path));
I want to query as to what the colour is at specific points. For example, what is the RGB value at pixel (65,32)?
How do I go about this? I was taking this approach:
ImageSource ims = m_image1.Source;
BitmapImage bitmapImage = (BitmapImage)ims;
int height = bitmapImage.PixelHeight;
int width = bitmapImage.PixelWidth;
int nStride = (bitmapImage.PixelWidth * bitmapImage.Format.BitsPerPixel + 7) / 8;
byte[] pixelByteArray = new byte[bitmapImage.PixelHeight * nStride];
bitmapImage.CopyPixels(pixelByteArray, nStride, 0);
Though I will confess there's a bit of monkey-see, monkey do going on with this code.
Anyway, is there a straightforward way to process this array of bytes to convert to RGB values?
Here is how I would manipulate pixels in C# using multidimensional arrays:
[StructLayout(LayoutKind.Sequential)]
public struct PixelColor
{
public byte Blue;
public byte Green;
public byte Red;
public byte Alpha;
}
public PixelColor[,] GetPixels(BitmapSource source)
{
if(source.Format!=PixelFormats.Bgra32)
source = new FormatConvertedBitmap(source, PixelFormats.Bgra32, null, 0);
int width = source.PixelWidth;
int height = source.PixelHeight;
PixelColor[,] result = new PixelColor[width, height];
source.CopyPixels(result, width * 4, 0);
return result;
}
usage:
var pixels = GetPixels(image);
if(pixels[7, 3].Red > 4)
{
...
}
If you want to update pixels, very similar code works except you will create a WriteableBitmap, and use this:
public void PutPixels(WriteableBitmap bitmap, PixelColor[,] pixels, int x, int y)
{
int width = pixels.GetLength(0);
int height = pixels.GetLength(1);
bitmap.WritePixels(new Int32Rect(0, 0, width, height), pixels, width*4, x, y);
}
thusly:
var pixels = new PixelColor[4, 3];
pixels[2,2] = new PixelColor { Red=128, Blue=0, Green=255, Alpha=255 };
PutPixels(bitmap, pixels, 7, 7);
Note that this code converts bitmaps to Bgra32 if they arrive in a different format. This is generally fast, but in some cases may be a performance bottleneck, in which case this technique would be modified to match the underlying input format more closely.
Update
Since BitmapSource.CopyPixels doesn't accept a two-dimensional array it is necessary to convert the array between one-dimensional and two-dimensional. The following extension method should do the trick:
public static class BitmapSourceHelper
{
#if UNSAFE
public unsafe static void CopyPixels(this BitmapSource source, PixelColor[,] pixels, int stride, int offset)
{
fixed(PixelColor* buffer = &pixels[0, 0])
source.CopyPixels(
new Int32Rect(0, 0, source.PixelWidth, source.PixelHeight),
(IntPtr)(buffer + offset),
pixels.GetLength(0) * pixels.GetLength(1) * sizeof(PixelColor),
stride);
}
#else
public static void CopyPixels(this BitmapSource source, PixelColor[,] pixels, int stride, int offset)
{
var height = source.PixelHeight;
var width = source.PixelWidth;
var pixelBytes = new byte[height * width * 4];
source.CopyPixels(pixelBytes, stride, 0);
int y0 = offset / width;
int x0 = offset - width * y0;
for(int y=0; y<height; y++)
for(int x=0; x<width; x++)
pixels[x+x0, y+y0] = new PixelColor
{
Blue = pixelBytes[(y*width + x) * 4 + 0],
Green = pixelBytes[(y*width + x) * 4 + 1],
Red = pixelBytes[(y*width + x) * 4 + 2],
Alpha = pixelBytes[(y*width + x) * 4 + 3],
};
}
#endif
}
There are two implementations here: The first one is fast but uses unsafe code to get an IntPtr to an array (must compile with /unsafe option). The second one is slower but does not require unsafe code. I use the unsafe version in my code.
WritePixels accepts two-dimensional arrays, so no extension method is required.
Edit: As Jerry pointed out in the comments, because of the memory layout, the two-dimensional array has the vertical coordinate first, in other words it must be dimensioned as Pixels[Height,Width] not Pixels[Width,Height] and addressed as Pixels[y,x].
I'd like to add to Ray´s answer that you can also declare PixelColor struct as a union:
[StructLayout(LayoutKind.Explicit)]
public struct PixelColor
{
// 32 bit BGRA
[FieldOffset(0)] public UInt32 ColorBGRA;
// 8 bit components
[FieldOffset(0)] public byte Blue;
[FieldOffset(1)] public byte Green;
[FieldOffset(2)] public byte Red;
[FieldOffset(3)] public byte Alpha;
}
And that way you'll also have access to the UInit32 BGRA (for fast pixel access or copy), besides the individual byte components.
The interpretation of the resulting byte array is dependent upon the pixel format of the source bitmap, but in the simplest case of a 32 bit, ARGB image, each pixel will be composed of four bytes in the byte array. The first pixel would be interpreted thusly:
alpha = pixelByteArray[0];
red = pixelByteArray[1];
green = pixelByteArray[2];
blue = pixelByteArray[3];
To process each pixel in the image, you would probably want to create nested loops to walk the rows and the columns, incrementing an index variable by the number of bytes in each pixel.
Some bitmap types combine multiple pixels into a single byte. For instance, a monochrome image packs eight pixels into each byte. If you need to deal with images other than 24/32 bit per pixels (the simple ones), then I would suggest finding a good book that covers the underlying binary structure of bitmaps.
I'd like to improve upon Ray's answer - not enough rep to comment. >:( This version has the best of both safe/managed, and the efficiency of the unsafe version. Also, I've done away with passing in the stride as the .Net documentation for CopyPixels says it's the stride of the bitmap, not of the buffer. It's misleading, and can be computed inside the function anyway. Since the PixelColor array must be the same stride as the bitmap (to be able to do it as a single copy call), it makes sense to just make a new array in the function as well. Easy as pie.
public static PixelColor[,] CopyPixels(this BitmapSource source)
{
if (source.Format != PixelFormats.Bgra32)
source = new FormatConvertedBitmap(source, PixelFormats.Bgra32, null, 0);
PixelColor[,] pixels = new PixelColor[source.PixelWidth, source.PixelHeight];
int stride = source.PixelWidth * ((source.Format.BitsPerPixel + 7) / 8);
GCHandle pinnedPixels = GCHandle.Alloc(pixels, GCHandleType.Pinned);
source.CopyPixels(
new Int32Rect(0, 0, source.PixelWidth, source.PixelHeight),
pinnedPixels.AddrOfPinnedObject(),
pixels.GetLength(0) * pixels.GetLength(1) * 4,
stride);
pinnedPixels.Free();
return pixels;
}
I took all examples and created a slightly better one - tested it too
(the only flaw was that magic 96 as DPI which really bugged me)
I also compared this WPF tactic versus:
GDI by using Graphics (system.drawing)
Interop by directly invoking GetPixel from GDI32.Dll
To my supprise,
This works x10 faster than GDI, and around x15 times faster then Interop.
So if you're using WPF - much better to work with this to get your pixel color.
public static class GraphicsHelpers
{
public static readonly float DpiX;
public static readonly float DpiY;
static GraphicsHelpers()
{
using (var g = Graphics.FromHwnd(IntPtr.Zero))
{
DpiX = g.DpiX;
DpiY = g.DpiY;
}
}
public static Color WpfGetPixel(double x, double y, FrameworkElement AssociatedObject)
{
var renderTargetBitmap = new RenderTargetBitmap(
(int)AssociatedObject.ActualWidth,
(int)AssociatedObject.ActualHeight,
DpiX, DpiY, PixelFormats.Default);
renderTargetBitmap.Render(AssociatedObject);
if (x <= renderTargetBitmap.PixelWidth && y <= renderTargetBitmap.PixelHeight)
{
var croppedBitmap = new CroppedBitmap(
renderTargetBitmap, new Int32Rect((int)x, (int)y, 1, 1));
var pixels = new byte[4];
croppedBitmap.CopyPixels(pixels, 4, 0);
return Color.FromArgb(pixels[3], pixels[2], pixels[1], pixels[0]);
}
return Colors.Transparent;
}
}
A little remark:
If you are trying to use this code (Edit: provided by Ray Burns), but get the error about the array's rank, try to edit the extension methods as follows:
public static void CopyPixels(this BitmapSource source, PixelColor[,] pixels, int stride, int offset, bool dummy)
and then call the CopyPixels method like this:
source.CopyPixels(result, width * 4, 0, false);
The problem is, that when the extension method doesn't differ from the original, the original one is called. I guess this is because PixelColor[,] matches Array as well.
I hope this helps you if you got the same problem.
If you want just one Pixel color:
using System.Windows.Media;
using System.Windows.Media.Imaging;
...
public static Color GetPixelColor(BitmapSource source, int x, int y)
{
Color c = Colors.White;
if (source != null)
{
try
{
CroppedBitmap cb = new CroppedBitmap(source, new Int32Rect(x, y, 1, 1));
var pixels = new byte[4];
cb.CopyPixels(pixels, 4, 0);
c = Color.FromRgb(pixels[2], pixels[1], pixels[0]);
}
catch (Exception) { }
}
return c;
}
Much simpler. There's no need to copy the data around, you can get it directly. But this comes at a price: pointers and unsafe. In a specific situation, decide whether it's worth the speed and ease for you (but you can simply put the image manipulation into its own separate unsafe class and the rest of the program won't be affected).
var bitmap = new WriteableBitmap(image);
data = (Pixel*)bitmap.BackBuffer;
stride = bitmap.BackBufferStride / 4;
bitmap.Lock();
// getting a pixel value
Pixel pixel = (*(data + y * stride + x));
bitmap.Unlock();
where
[StructLayout(LayoutKind.Explicit)]
protected struct Pixel {
[FieldOffset(0)]
public byte B;
[FieldOffset(1)]
public byte G;
[FieldOffset(2)]
public byte R;
[FieldOffset(3)]
public byte A;
}
The error checking (whether the format is indeed BGRA and handling the case if not) will be left to the reader.
You can get color components in a byte array. First copy the pixels in 32 bit to an array and convert that to 8-bit array with 4 times larger size
int[] pixelArray = new int[stride * source.PixelHeight];
source.CopyPixels(pixelArray, stride, 0);
// byte[] colorArray = new byte[pixelArray.Length];
// EDIT:
byte[] colorArray = new byte[pixelArray.Length * 4];
for (int i = 0; i < colorArray.Length; i += 4)
{
int pixel = pixelArray[i / 4];
colorArray[i] = (byte)(pixel >> 24); // alpha
colorArray[i + 1] = (byte)(pixel >> 16); // red
colorArray[i + 2] = (byte)(pixel >> 8); // green
colorArray[i + 3] = (byte)(pixel); // blue
}
// colorArray is an array of length 4 times more than the actual number of pixels
// in the order of [(ALPHA, RED, GREEN, BLUE), (ALPHA, RED...]

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