我有一个自动换行算法,基本上可以生成符合文本宽度的文本行。不幸的是,当我添加太多文本时它会变慢。
我想知道我是否监督了可以做出的任何重大优化。此外,如果任何人的设计仍然允许更好的线条或字符串指针的线条,我将打开以重写算法。
由于
void AguiTextBox::makeLinesFromWordWrap()
{
textRows.clear();
textRows.push_back("");
std::string curStr;
std::string curWord;
int curWordWidth = 0;
int curLetterWidth = 0;
int curLineWidth = 0;
bool isVscroll = isVScrollNeeded();
int voffset = 0;
if(isVscroll)
{
voffset = pChildVScroll->getWidth();
}
int AdjWidthMinusVoffset = getAdjustedWidth() - voffset;
int len = getTextLength();
int bytesSkipped = 0;
int letterLength = 0;
size_t ind = 0;
for(int i = 0; i < len; ++i)
{
//get the unicode character
letterLength = _unicodeFunctions.bringToNextUnichar(ind,getText());
curStr = getText().substr(bytesSkipped,letterLength);
bytesSkipped += letterLength;
curLetterWidth = getFont().getTextWidth(curStr);
//push a new line
if(curStr[0] == '\n')
{
textRows.back() += curWord;
curWord = "";
curLetterWidth = 0;
curWordWidth = 0;
curLineWidth = 0;
textRows.push_back("");
continue;
}
//ensure word is not longer than the width
if(curWordWidth + curLetterWidth >= AdjWidthMinusVoffset &&
curWord.length() >= 1)
{
textRows.back() += curWord;
textRows.push_back("");
curWord = "";
curWordWidth = 0;
curLineWidth = 0;
}
//add letter to word
curWord += curStr;
curWordWidth += curLetterWidth;
//if we need a Vscroll bar start over
if(!isVscroll && isVScrollNeeded())
{
isVscroll = true;
voffset = pChildVScroll->getWidth();
AdjWidthMinusVoffset = getAdjustedWidth() - voffset;
i = -1;
curWord = "";
curStr = "";
textRows.clear();
textRows.push_back("");
ind = 0;
curWordWidth = 0;
curLetterWidth = 0;
curLineWidth = 0;
bytesSkipped = 0;
continue;
}
if(curLineWidth + curWordWidth >=
AdjWidthMinusVoffset && textRows.back().length() >= 1)
{
textRows.push_back("");
curLineWidth = 0;
}
if(curStr[0] == ' ' || curStr[0] == '-')
{
textRows.back() += curWord;
curLineWidth += curWordWidth;
curWord = "";
curWordWidth = 0;
}
}
if(curWord != "")
{
textRows.back() += curWord;
}
updateWidestLine();
}
答案 0 :(得分:2)
我认为,有两个主要因素会让它变慢。
第一个,也可能不那么重要:当你构建每一行时,你会在行上附加单词。每个这样的操作可能需要重新分配行并复制其旧内容。对于长线来说,这是低效的。但是,我猜测在实际使用中你的线很短(比如60-100个字符),在这种情况下,成本不太可能很大。不过,那里可能还有一些效率。
第二个,也许更重要的是:你显然在某种GUI中使用它作为文本区域,我猜它正在被输入。如果你为每个输入的字符重新计算,一旦文本变长,这真的会受到伤害。
只要用户只在最后添加字符 - 这肯定是最常见的情况 - 你可以有效地利用这样一个事实,即你的“贪婪”换行算法改变永远不会影响更早的事情lines:所以只需从最后一行的开头重新计算。
如果您希望即使用户在文本中间的某个位置键入(或删除或其他)时也要快速,您的代码将需要做更多的工作并存储更多信息。例如:无论何时构建一条线,请记住“如果您使用此字开始一行,则以 字开头,而此是整个结果线“。当该行内的任何变化时,使此信息无效。现在,经过一些编辑后,大多数更改都不需要重新计算。你应该自己弄清楚这个细节,因为(1)这是一个很好的练习,(2)我现在需要上床睡觉。
(为了节省内存,你可能根本不想存储整行 - 无论你是否实现我刚才描述的那种技巧。相反,只需存储这里的下一行换行信息和构建你的UI需要渲染它们。)
它可能比您现在想要的更复杂,但您也应该查看Donald Knuth基于动态编程的断行算法。它比你的复杂得多,但仍然可以很快地完成,并且它产生明显更好的结果。例如,参见http://defoe.sourceforge.net/folio/knuth-plass.html。
答案 1 :(得分:2)
算法问题通常会带来数据结构问题。
首先让我们做一些观察:
段落
我首先介绍段落的概念,它由用户引入的换行符确定。在进行编辑时,您需要找到哪个是相关段落,这需要查找结构。
这里的“理想”结构将是一个Fenwick树,对于一个小文本框,但这似乎有点过分。我们只会让每个段落存储构成其表示的显示行数,并且您将从头开始计算。请注意,访问最后显示的行是对最后一段的访问。
这些段落因此以C ++术语存储为连续序列,很可能采用间接命中(即存储指针)来保存在中间的段落被移除时移动它们。
每个段落都会存储:
std::string
。每个段落都会缓存其显示,只要进行编辑,此段落缓存就会失效。
实际渲染一次只能进行几个段落(更好的是,显示几行):那些可见的。
显示行
段落可能至少显示一行,但没有最大值。我们需要以可编辑的形式存储“显示”,这是一种适合版本的形式。
投入\n
的单个字符块不适合。更改意味着移动大量字符,用户假设正在更改文本,因此我们需要更好。
使用长度而不是字符,我们实际上可能只存储4个字节(如果字符串超过3GB ......我不保证这个算法有多少。)
我的第一个想法是使用字符索引,但是在版本的情况下,所有后续索引都会更改,并且传播容易出错。长度是偏移量,因此我们有一个相对于前一个单词位置的索引。它确实构成了一个单词(或令牌)的问题。值得注意的是,你是否会折叠多个空格?你怎么处理它们?在这里,我假设单词由一个空格分开。
对于“快速”检索,我也会存储整个显示行的长度。这允许在段落的字符503处进行编辑时快速跳过第一个显示的行。
显示的行将由以下内容组成:
这个序列应该可以在两端有效地编辑(因为对于包装,我们将在两端按下/弹出单词,具体取决于编辑是否添加或删除了单词)。如果在中间我们效率不高,那就不那么重要了,因为在中间只编辑一行。
在C ++中,vector
或deque
应该没问题。虽然理论上list
是“完美的”,但实际上它的不良内存局部性和高内存开销将抵消其渐近保证。一条线由几个单词组成,因此渐近行为无关紧要,而高常数则无关紧要。
渲染
对于渲染,选择一个已经足够长度的缓冲区(std::string
调用reserve
即可)。通常,你只需要clear
并重写缓冲区,所以不会发生内存分配。
您无需显示无法看到的内容,但需要知道有多少行,以获取正确的段落。
一旦你得到段落:
offset
设为0
offset
(后面的空格为+ 1)_content
的子字符串进行访问,您可以使用insert
上的buffer
方法:buffer.insert(buffer.end(), _content[offset], _content[offset+length])
困难在于维持offset
,但这就是使算法有效的原因。
结构
struct LineDisplay: private boost::noncopyable
{
Paragraph& _paragraph;
uint32_t _length;
std::vector<uint16_t> _words; // copying around can be done with memmove
};
struct Paragraph:
{
std::string _content;
boost::ptr_vector<LineDisplay> _lines;
};
使用这种结构,实现应该是直截了当的,并且在内容增长时不应该减慢速度。
答案 2 :(得分:0)
算法的一般更改 -
这允许你移除/减少几乎每个字符上运行的测试if(!isVscroll && isVScrollNeeded())
- isVScroll可能不会吱吱作响,示例代码似乎没有将行知识传递给函数所以不能看看它是否需要它。
假设textRows
是vector<string>
- textrows.back() +=
有点贵,查看后面的内容不是+ =字符串上的字符串效率不高。我将更改为使用ostrstream
收集行并在完成后将其推入。
getFont()。getWidth()可能很昂贵 - 字体有变化吗?固定宽度字体的最小和最大快捷方式的宽度有多大差异。
尽可能使用原生方法来获取单词的大小,因为您不想破坏它们 - GetTextExtentPoint32
当你在两者之间进行切换时,通常会有足够的空间来容纳VScroll。从测量开始重新启动可能会花费您两倍的时间。存储每行的行宽,以便跳过仍然适合的行。 或者不要直接构建线条,保持单词大小分开。
它真正需要的准确度是多少? 应用一些实用主义......
只是假设需要VScroll,大部分包装都不会有太大变化,即使它不是(在一行的结尾/开头有1个字母的单词)
尝试使用单词而不是字母更多地工作 - 检查每个字母的剩余空间可能会浪费时间。假设字符串中的每个字母是最长的字母,字母x最长&lt;空间然后把它放进去。