Ультрашвидкий підсвічування синтаксису в delphi trichedit

Коли я писав Scriptaint в PaintCAD 4Windows, то довелося зробити підсвічування скриптів. За кілька тижнів я знаходив в інтернеті ряд початкових кодів, але всі вони не підійшли. Довелося зібрати з них свого Франкенштейна.

Коли я написав цю процедуру, то на підсвічування невеликого скрипта розміром в 200 рядків Windows XP витрачав приблизно півсекунди. Тому після кожного натискання клавіші або після зміни скрипта можна було тут же викликати підсвічування. Я зробив таймер, який оновлювався кожного разу при зміні тексту TRichEdit і якщо протягом 2 секунд ніхто не міняв текст - то запускалася підсвічування.

Коли я почав тестувати це в Windows 8 - то побачив, що замість півсекунди на підсвічування того ж скрипта стало йти 5-6 секунд (мабуть в win8 щось намудрували з бідним richeditом)! І не можна було взагалі нічого редагувати, з жахом чекаючи що ось запуститься підсвічування і все залипне на п'ять секунд.

Виявилося, що довго працює trichedit, який лежить на формі, і дуже швидко працює trichedit в пам'яті. Мабуть windows 8 робить щось страшне при кожному перемальовуванні. І навіть після моїх блокувань відтворення компоненти richedit купою функцій WinAPI, понадерганих з чужих початкових кодів підсвічування, що лежить на формі richedit все одно продовжував працювати дуже довго.

Тому нова процедура копіювала вміст richedit в невидимий richedit в пам'яті, в ньому робила все обробки і перекидала отримане вміст назад, відновлюючи після цього старе положення смуг прокрутки і положення курсора.

Але після тестів в Windows 98 виявилося, що вертикальна смуга прокрутки не слухається установки в старе положення. Довелося додати посилку повідомлення EN_LINESCROLL на richedit і відновлювати таки його положення в win 98 теж.

Ось вона, процедура підсвічування річедіта RichEdit1 і супутні процедури, що працюють миттєво (напевно 0.1-0.2 сек), хоча на товстих текстах можуть і вони гальмувати (тоді вам доведеться писати підсвічування в межах показуваного в richedit фрагмента тексту, а це вже інша історія):

delim - рядок символів, які можуть бути з боків зарезервованих слів. Алгоритм їх відловлює з боків від знайденого слова, тому ", function," виділиться, а "efunction" - немає.

Reswords - масив слів, які будуть жирними в тексті, якщо з боків від них символи з delim.

FillChar (tsih, sizeof (tsih), 0);
tsih.cbSize: = SizeOf (tsih);
tsih.fMask: = SIF_POS;
// запам'ятовуємо в них положення смуг прокрутки
GetScrollInfo (RichEdit1.Handle, SB_VERT, tsiv);
GetScrollInfo (RichEdit1.Handle, SB_HORZ, tsih);

// починаємо городити підсвічування в невидимому TRichEdit
try
// чистимо всю попередню підсвічування
ri.SelStart: = 0;
ri.SelLength: = length (ri.Text);
ri.SelAttributes: = RichEdit1.DefAttributes;
ri.SelAttributes.Color: = clBlack;
ri.SelAttributes.Style: = [];

i: = 0;
// весь текст скидаємо в змінну work, додаючи в кінці символ роздільник, щоб шукати зарезервовані слова і в кінці тексту теж
work: = ri.Text + # $ D # $ A;
wl: = length (work);
while (i<=wl) do
begin
i: = i + 1;
// біжимо по тексту

// знайшли одинарні лапки і до цього знайшли ще одну (прапорець insidestr1 встановлений) - фарбуємо від першої лапки до знайденої все в фіолетовий
if insidestr1 and (work = '' '') then
begin
ri.SelStart: = is1;
ri.SelLength: = i-is1;
ri.SelAttributes.Color: = clPurple;
insidestr1: = false;
continue;
end;

// знайшли прямі подвійні лапки і до цього знайшли ще одну (прапорець insidestr2 встановлений) - фарбуємо від першої лапки до знайденої все в синій
if insidestr2 and (work = ' "') then
begin
ri.SelStart: = is2;
ri.SelLength: = i-is2;
ri.SelAttributes.Color: = RGB (0,0,128);
insidestr2: = false;
continue;
end;

// якщо це не останній символ і він з наступним утворює * /, і до цього знаходили / * (прапорець longcomm встановлений) - то фарбуємо це все в зелений
if i if longcomm and (work = '*') and (work [i + 1] = '/') then
begin
ri.SelStart: = ls;
ri.SelLength: = i-ls + 1;
ri.SelAttributes.Color: = clGreen;
i: = i + 1;
longcomm: = false;
continue;
end;

// якщо біжимо по тексту і ми не всередині жодного з виділень - то можна шукати початкові символи цих виділень і зарезервовані слова
if (not htmlcomm) and (not longcomm) and (not insidestr1) and (not insidestr2) then
begin
// одинарна лапка - запам'ятовуємо де була і ставимо прапорець початку фарбування
if work = '' '' then
begin
insidestr1: = true;
is1: = i-1;
continue;
end;

// лапки - запам'ятовуємо де була і ставимо прапорець початку фарбування
if work = ' "' then
begin
insidestr2: = true;
is2: = i-1;
continue;
end;

// закінчили пробіг по тексту

// дивимося чи рвані виділення (незакінчені) - тоді просто фарбуємо текст до самого кінця

i: = wl-1;
if insidestr1 then
begin
ri.SelStart: = is1;
ri.SelLength: = i-is1;
ri.SelAttributes.Color: = clPurple;
insidestr1: = false;
end;

if insidestr2 then
begin
ri.SelStart: = is2;
ri.SelLength: = i-is2;
ri.SelAttributes.Color: = RGB (0,0,128);
insidestr2: = false;
end;

if longcomm then
begin
ri.SelStart: = ls;
ri.SelLength: = i-ls;
ri.SelAttributes.Color: = clGreen;
longcomm: = false;
end;

if htmlcomm then
begin
ri.SelStart: = hs;
ri.SelLength: = i-hs;
ri.SelAttributes.Color: = clGreen;
htmlcomm: = false;
end;

// всі пофарбували, копіюємо назад
ms: = TMemoryStream.Create;
ri.PlainText: = false;
RichEdit1.PlainText: = false;
try
ri.Lines.SaveToStream (ms);
ms.Seek (0, soFromBeginning);
RichEdit1.Lines.LoadFromStream (ms);
finally
ms.Free;
end;

// відновлюємо позиції скролів
RichEdit1.Perform (WM_VSCROLL, SB_THUMBPOSITION + tsiv.nPos * 65536,0);
RichEdit1.Perform (WM_HSCROLL, SB_THUMBPOSITION + tsih.nPos * 65536,0);

// для win98 і інших випадків - дивимося, якщо по вертикалі скролл не туди потрапив, то змушуємо скролл туди куди треба
st: = SendMessage (RichEdit1.Handle, EM_GETFIRSTVISIBLELINE, 0,0);
if st<>en then SendMessage (RichEdit1.Handle, EM_LINESCROLL, 0, en-st);

// richedit в пам'яті відслужив своє, зносимо його
ri.Free;

// ставимо курсор і виділення куди треба
RichEdit1.SelStart: = ss;
RichEdit1.SelLength: = sl;

// включаємо вимкнуту перерисовку назад і перемальовували компоненту
SendMessage (RichEdit1.Handle, WM_SETREDRAW, 1,0);
InvalidateRect (RichEdit1.Handle, 0, true);
SendMessage (RichEdit1.Handle, WM_USER + 69,0, ema);
RichEdit1.DoubleBuffered: = false;
RichEdit1.Repaint;

Виклик процедури підсвічування по таймеру

procedure TForm37.Timer1Timer (Sender: TObject);
begin
if not ((Word (GetAsyncKeyState (VK_SHIFT)) and $ 8000)<>0) then
begin
Timer1.Enabled: = false;
Podsvet ();
end;
end;

У сам RichEdit1 на подію OnChange призначаємо цю процедуру, щоб оновити таймер і завжди чекати з моменту останньої зміни тексту до виклику підсвічування рівно 2 секунди:

procedure TForm37.RichEdit1Change (Sender: TObject);
begin
Timer1.Enabled: = false;
Timer1.Enabled: = true;
end;

От і все. Виходить швидка підсвічування RichEdit через 2 секунди після останньої зміни тексту.

Збільшення обсягу тексту в RichEdit

У RichEdit є властивість MaxLength, яке за замовчуванням дорівнює нулю. І тоді максимальна кількість символів в RichEdit стає рівним 65536.

Як радять в інеті, збільшити це значення можна, якщо в програму додати рядок:
RichEdit1.MaxLength: = System.MaxInt-2;

Або прямо на формі при розробці виділити RichEdit і записати в властивість MaxLength число 2147483645.

Повернутись до початку

Одноденна експлуатація підсвічування показала що:
1) при великих обсягах тексту все псується, richedit захлинається при захопленні тексту в пам'ять перед підсвічуванням. Тому в процедурі були виправлені деякі місця. Текст тепер забирається як PlainText:

// копіюємо з нашого відомого TRichEdit весь вміст в створений TRichEdit
ms: = TMemoryStream.Create;
RichEdit1.PlainText: = true;
ri.PlainText: = true;
try
RichEdit1.Lines.SaveToStream (ms);
ms.Seek (0, soFromBeginning);
ri.Lines.LoadFromStream (ms);
finally
ms.Free;
end;

2) а атрибути для тексту копіюються перед в процесі колишнього скидання атрибутів:

ri.SelStart: = 0;
ri.SelLength: = length (ri.Text);
ri.SelAttributes: = RichEdit1.DefAttributes;
ri.SelAttributes.Color: = clBlack;
ri.SelAttributes.Style: = [];

3) в кінці теж трохи поміняв код копіювання з ri назад в richedit

// всі пофарбували, копіюємо назад
ms: = TMemoryStream.Create;
ri.PlainText: = false;
RichEdit1.PlainText: = false;
try
ri.Lines.SaveToStream (ms);
ms.Seek (0, soFromBeginning);
RichEdit1.Lines.LoadFromStream (ms);
finally
ms.Free;
end;

4) максимальний розмір richedit по дефолту поставлений в 65536 (при maxlength = 0), тому виставив його максимальним:

см. в першому пості розділ "Збільшення обсягу тексту в RichEdit".

Всі ці зміни з 1 по 4 пункт вже додані в скрипті в першому пості.

5) Випробування показали, що на тексти обсягом в 5-10 тисяч рядків йде час на підсвічування 1-2 секунди. Що непогано, особливо для Скріптаінта, там 10000-строковий скрипт це дуже потужна буде штука.

Тільки тоді доведеться робити підсвічування після 2 секунд по тому від не тільки зміни тексту, а й навіть просто після зміни en: = SendMessage (RichEdit1.Handle, EM_GETFIRSTVISIBLELINE, 0,0); на інше значення.

Повернутись до початку