TextRange constructor performance in RichTextBox - c#

I am currently working on syntax highlighting and code completion project, and making user control based on RichTextBox. I've had some problems adapting to the way RTB works and everything, but I have managed to make simple syntax highlighting.
Simple means that I highlight entire text every time user types a character. It's not supposed to be fast or anything, but it is too slow. Performance issues become visible when I have about 500 chars worth of text, and I do only one pass through the text for each typed character('colorInterval' function gets called about 100 times in one pass).
Performance analysis says the problem is TextRange constructor that takes about 80%+ of the time, and I use it every time I need to color an interval of text:
private void colorInterval(TextPointer start, TextPointer end)
{
TextRange range = new TextRange(start, end);
if(isFunction(range.Text)) colorAsFunction(range);
if(isInQuotes(range.Text)) colorAsQuoted(range);
...
}
So here goes my question:
Am I doing something wrong doing everything this way, or is there a way to boost performance of TextRange, recycle the 'range' object or something like that? What other solutions are there.

The simplest avenue is to (as you suggest) reuse the TextRange object, if it really is the constructor that is taking up most of your time. The TextRange properties Start and End are read only, but there is a public method Select which will update both, taking two TextPointer objects just like the constructor you have been using.
protected TextRange range;
private void colorInterval(TextPointer start, TextPointer end)
{
if (range == null)
range = new TextRange(start, end);
else
range.Select(start, end);
...
}
(N.B. checking for a null reference before deciding whether to initialise the variable isn't as neat as just instantiating a TextRange in the declaration. Unfortunately, TextRange has no public empty constructor and TextPointer no public constructors at all. You could create it with some dummy values in your class constructor to avoid this check.)
Above, I said 'if it really is the constructor'. Obviously, the profiling you've rightfully done has highlighted the constructor, but it could just as easily be a routine common to the constructor and the Select method.
Assuming you don't call colorInterval from more than one thread, I would say this is a better approach than you have currently whatever the time saving, because (I would guess that) colorInterval is being called frequently and constant creation and garbage collection of the subsequent TextRange objects it leaves behind is certainly an inefficiency.
Having made this suggestion, I strongly suggest you move away from the model where you scan the entire document every time you want to react to (for example) a single character change. Assuming you are targetting >= .net 3.5, the RichTextBox provides a TextChanged event that reports a list of TextChange objects from which you can work out the location of (and characters added or removed by) changes.
Naturally, there will be some work here because any change is unlikely to completely encapsulate a highlighted range. The TextRange class has a method for finding the paragraphs in which the start and end of a range can be found, in case that helps. There's probably a case for storing details of each highlighted range so you can quickly check for intersection.

Related

Gettings information about stairsruns and stairs from elements containing such information

Hello I have the following code:
public static void HandleStairs(Document doc)
List<TransitionPoint> ret = new List<TransitionPoint>();
FilteredElementCollector collector = new FilteredElementCollector(doc);
ICollection<Element> stairs = collector.OfCategory(BuiltInCategory.OST_Stairs).ToElements();
foreach (var stair in stairs)
{
var st= stair as Stairs;
if(st!=null)
{
%code that is never executed
}
}
return ret;
}
Now the problem is that no matter what it appears stairs are always null, I heard that another programmer had the same problem.
the stairs variable does receive a number of object with stairlike properties (being named staircase, having risers and platforms ext.) but does not actually appear to accept being cast to a stair. Anybody know how to actually cast this into stairs (or otherwise obtain all stairs in an document?)
Note that stairs is an element with the following properties:
Riser to Tread Connection
Monolithic Material
Apply Nosing Profile
Stringer Material
Text Size
Begin with Riser
Stringer Carriage Height
URL
Open Stringer Offset
Right Stringer
Riser Type
Cost
Left Stringer
Underside of Winder
Stringer Height
Nosing Profile
Manufacturer
Middle Stringers
Keynote
Riser Material
Minimum Tread Depth
Text Font
Monolithic Stairs
Maximum Riser Height
Landing Carriage Height
Break Symbol in Plan
Landing Overlap
Extend Below Base
Nosing Length
Assembly Description
End with Riser
Description
Function
Type Image
Type Comments
Stringer Thickness
Assembly Code
Calculation Rules
Trim Stringers at Top
Model
Tread Thickness
Tread Material
Riser Thickness
I mostly need the stair objects to get the runs assosiated with the stair objects, or actually I need the paths the runs follow.
This can hopefully be used to do the following:
var tesselated = new List<XYZ>();
var stairPath = run.GetStairsPath();
foreach (Curve curve in stairPath)
{
tesselated.AddRange(curve.Tessellate());
}
Because I need the XYZ locations for the positions any stairs attached to the geometry of the building.
First of all, you absolutely have to find out and tell us what kind of element you are talking about. Otherwise, this discussion is completely pointless. One very easy way to determine that is to explore the 'stair-like' element using RevitLookup:
https://github.com/jeremytammik/RevitLookup
If you do not know what RevitLookup is, you should stop absolutely everything else you are doing with the Revit API right away and start off fresh by working through the Revit API getting started material, especially installing and starting to use RevitLookup:
http://thebuildingcoder.typepad.com/blog/about-the-author.html#2
The filtered element collector you show retrieves all elements of the 'Stairs' category. This 'stair-like object' could be a DirectShape, in which case you can assign it the 'Stairs' category. Then it will be retrieved by your filtered element collector above.
Here is an example of a 'stair-like' extruded roof, which is and will always remain a roof, with the 'Roofs' category, and thus can never be identified by your filtered element collector:
http://thebuildingcoder.typepad.com/blog/2014/09/events-again-and-creating-an-extrusion-roof.html#7
Sorry for the confusing answer, but I must say your question is pretty confusing too. Never heard anything like it before. I hope this helps.
What you've done looks reasonable, although as others have pointed out, obviously somehow you are getting back an element which is not a Stair element.
I would suggest - in order to make sure you get back what you want, that you use:
.OfClass(typeof(Stairs))
with the FilteredElementCollector. With this, you can probably drop the WhereElementIsNotElementType() and OfCategory() methods, because it is implicit in the above statement.
This way - whatever you get back should be cast-able.
The as operator returns null if the cast fails, so whatever doc.GetElement(stairId) returns is not of type Stairs or one of its sub-types.
I'm guessing doc is some kind of 'storage' document so you probably need to create a new instance of Stairs and fill it with information you get from whatever doc.GetElement(stairId) returns.

Declaring variables slow down program

I have a program which uses a RichTextBox (which is part of Tab Control) and in the TextChanged event I these variable declarations:
RichTextBox programTextBox = (RichTextBox)tabControl.TabPages[tabControl.SelectedIndex].Controls[0];
int selectStart = programTextBox.SelectionStart;
int programCurrentLine = programTextBox.GetLineFromCharIndex(programTextBox.SelectionStart);
int programCurrentLineIndex = programTextBox.GetFirstCharIndexFromLine(programCurrentLine);
int programCurrentLineLength = programTextBox.Lines[programCurrentLine].Length;
string programCurrentLineText = programTextBox.Lines[programCurrentLine].ToString();
All of them are important in that particular event and I use them multiple times for multiple purposes. However, recalculating every single time slows down my program.
For example, I've noticed that if I open a somewhat large file (with my RichTextBox) and then start pressing e.g. the 'a' button, there is some noticable lag. Deleting every single piece of code except the declaration of these variables does not help at all with the lag, but also deleting the code above, completely solves the problem.
I have two questions: 1) Why do these declarations slow down that much the TextChanged event and 2) What can I do? (Is there a faster way to calculate these variables?)
It's not the variable declarations - it's the code you're using to initialize them. I wouldn't be at all surprised to find that GetLineFromCharIndex and GetFirstCharIndexFromLine were expensive - and currently you're calling the Lines property twice.
You could probably improve matters at least slightly just by removing one of those Lines calls, simply by fetching the line first and then looking at its length:
// No need to call ToString() - Lines is a string[]
string programCurrentLineText = programTextBox.Lines[programCurrentLine];
int programCurrentLineLength = programCurrentLineText.Length;
It's a shame there isn't some way of saying "Get all the information about the position of the given index: its line, first char index from line, and the line itself" in one call :(
If it is not important to recalculate on every key press you might be better off implementing a boolean variable which is set on keypress then has a timer to reset it. For example adding a 1.2 second delay on the event. I am not sure the needs of your app so that time might be a bit much.

Problem with C# multiline textbox memory usage

I am using a multiline text box in C# to just log some trace information. I simply use AppendText("text-goes-here\r\n") as I need to add lines.
I've let this program run for a few days (with a lot of active trace) and I noticed it was using a lot of memory. Long story short, it appears that even with the maxlength value to something very small (256) the content of the text box just keeps expanding.
I thought it worked like a FIFO (throwing away the oldest text that exceeds the maxlength size). It doesn't, it just keeps increasing in size. This is apparently the cause of my memory waste. Anybody know what I'm doing wrong?
Added a few hours after initial question...
Ok, I tried the suggested code below. To quickly test it, I simply added a timer to my app and from that timer tick I now call a method that does essentially the same thing as the code below. The tick rate is high so that I can observe the memory usage of the process and quickly determine if there is a leak. There wasn't. That was good; however, I put this in my application and memory usage did not change (still leaking). That sure seems to imply that I have a leak somwehere else :-( however, if I simply add a return at the top of that method, the usage drops back to stable. Any thoughts on this? The timer-tick-invoked code did not accumulate memory but my real code (same method) does. The difference is that I'm calling the method from a variety of different places in the real code. Can the context of the call affect this somehow? (note, if it isn't already obvious, I'm not a .NET expert by any means)...
TextBox will allow you to append text regardless of MaxLength value - it's only used to control user entry. You can create a method that will be adding new text after verifying that maxlength is not reached, and if it is, just remove x lines from the beginning.
You could use a simple function to append text:
int maxLength = 256;
private void AppendText(string text)
{
textBox1.AppendText(text);
if(textBox1.Text.Length > maxLength)
textBox1.Text = textBox1.Text.Substring(textBox1.Text.Length - maxLength);
}

Speeding up listbox insertion time

I need to add 950 strings that are 2500 characters in length to a listbox. The method I am using below takes 2.5 seconds and ideally it needs to happen in less then 500ms.
Stopwatch sw = Stopwatch.StartNew();
listBox1.BeginUpdate();
listBox1.Items.AddRange(items.ToArray());
listBox1.EndUpdate();
sw.Stop();
What would be the best way to optimize the insertion time?
Thanks.
One thing you could try is changing this line:
listBox1.Items.AddRange(items.ToArray());
to something like this:
foreach (var item in items)
{
listBox1.Items.Add(item);
}
That way, you do not have the overhead of creating a whole new array (the ToArray() call) before putting the items into your ListBox.
Listbox is dealing with 2500 characters. That is what is slow. All that data, including converting to/from arrays, is peanuts in memory. Hence skipping the ToArray step not making a difference. If your users have to scroll horizontally to see this info, chances are, you're stuck with 'slow.'
If not, consider refactoring a tiny bit. Strategy: only put as many characters - about 100 - as are viewable in a regular width listbox. Full strings are retained behind the scenes.
Make a helper class like so (you can convert the public string to a property if you're that anal;-):
Class TruncatedListItem
Public Content as string
Overrides sub ToString() as string ' Pardon me if this is wrong I always use intellisense
return Mid(Content,1,100)
end sub
end class
Add those items to the listbox. When an item in a listbox isn't of type string, it calls the item's ToString method (which hey, we just tailored to give the listbox a break) and adds that as a string, then the items collection appears as the items you've added. (keep the begin/end update too)
For each each itm as string in Items
dim tli as new TruncatedListItem
tli.Content = itm
listbox.add(tli)
next
When you want to see what the user picked, instead of getting the string like this:
MyString = Ctype(Listbox.SelectedItem,string)
do this
MyString = Ctype(ListBox.SelectedItem,TruncatedListItem).Content
Now I am assumming the user, at some point, still needs to see all 2500 chars before selecting. Unless they are a serious stick in the mud, they should settle for this alternative (In fact there are advantages to scrolling).
When they double click an item, in the handler for double click, show them the full text in a messagebox. You could tell them to do that in a tooltip. For example, in the double-click handler: msgbox Ctype(ListBox.SelectedItem,TruncatedListItem).Content,,"Full Item Text"
Good luck!

What's a good way to perform hit testing on a FormattedText?

I'm rendering text using FormattedText, but there does appear to be any way to perform per-char hit testing on the rendered output. It's read-only, so I basically only need selection, no editing.
I'd use RichTextBox or similar, but I need to output text based on control codes embed in the text itself, so they don't always nest, which makes building the right Inline elements very complex. I'm also a bit worried about performance with that solution; I have a large number of lines, and new lines are appended often.
I've looked at GlyphRun, it appears I could get hit-testing from it or a related class, but I'd be reimplementing a lot of functionality, and it seems like there should be a simpler way...
Does anyone know of a good way to implement this?
You can get the geometry of each character from a FormattedText object and use the bounds of each character to do your hit testing.
var geometry = (GeometryGroup)((GeometryGroup)text.BuildGeometry(new Point(0, 0))).Children[0];
foreach (var c in geometry.Children)
{
if (c.Bounds.Contains(point))
return index;
index++;
}
In OnRender you can render these geometry objects instead of the formatted text.
The best way is to design a good data structure for storing your text and which also considers hit-testing. One example could be to split the text into blocks (words, lines or paragraphs depending on what you need). Then each such block should have a bounding-box which should be recomputed in any formatting operations. Also consider caret positions in your design.
Once you have such facility it becomes very easy to do hit-testing, just use the bounding boxes. It will also help in subsequent operations like highlighting a particular portion of text.
Completely agree with Sesh - the easiest way you're going to get away with not re-implementing a whole load of FormattedText functionality is going to be by splitting up the individual items you want to hit-test into their own controls/inlines.
Consider using a TextBlock and adding each word as it's own Inline ( or ), then either bind to the inline's IsMouseDirectlyOver property, our add delegates to the MouseEnter & MouseLeave events.
If you want to do pixel-level hit testing of the actual glyphs (i.e. is the mouse exactly in the dot of this 'i'), then you'll need to use GlyphRuns and do manual hit testing on the glyphs (read: hard work).
I'm very late to the party--if the party is not over, and you don't need the actual character geometry, I found something like this useful:
for (int i = 0; i < FormattedText.Text.Length; i++)
{
characterHighlightGeometry = FormattedText.BuildHighlightGeometry(new Point(), i, 1);
CharacterHighlightGeometries.Children.Add(characterHighlightGeometry);
}
BuildGeometry() only includes the actual path geometry of a character. BuildHighlightGeometry() generates the outer bounds of all characters--including
spaces, so an index to a space can be located by:
foreach (var c in CharacterHighlightGeometries.Children)
{
if (c.Bounds.Contains(centerpoint))
{
q = c;
cpos = index;
break;
}
index++;
}
Hope this helps.

Categories

Resources