Sources
Delphi Russian Knowledge Base
DRKB - база знаний по Дельфи в рунете, составленная Виталием Невзоровым

Визуальный HTM-редактор своими руками

27.07.2004
samum2000@mail15.com

Здравствуйте, дорогие друзья. Понадобилась мне недавно компонента визуального html редактора. Сколько в internete не искал я информации по этому поводу - не нашел. В смысле, не нашел приемлемого решения, ведь платить $19.99 за одну компоненточку жалко. Поэтому сейчас я отмниму хлеб у некоторых компоненто-писателей и расскажу вам, как можно сделать полноценный html редактор своими руками, тем более, что для этого практически ничего не нужно.

Нам понадобится самая малость. В первую очередь - delphi 5-7 (у меня стоит 7-я версия, и весь код тестировался именно в этой версии). Такое ограничение версий вызвано тем, что компонент twebbrowser впервые "прописался" на вкладке internet именно в 5-й версии (в 4-й его надо было устанавливать как компонент activex). Еще необходимо, чтобы в системе был установлен internet explorer 4 и выше, по тем причинам, что именно его части являются основой webbrowser'a.

Сначала нам надо перевести webbrowser в режим редактирования. Для этого у каждого документа (согласно объектной модели это document) существует свойство designmode. Если установить его в 'on', то наша компонента автоматически переключается в режим редактирования, а если установить его в "off", то компонент вернется в режим просмотра.

Проверим это! Создадим новую форму, разместим на ней компоненту twebbrowser и несколько компонент tspeedbutton. Затем напишем такой код:

unit main;

interface
...

var
  form1: tform1;
  disp: idispatch;
  editor: ihtmldocument2;

implementation

{$r *.dfm}

procedure tform1.webbrowser1documentcomplete(
    sender: tobject;
    const pdisp: idispatch;
    var url: olevariant);
var
  currentwb: iwebbrowser;
  editor: ihtmldocument2;
begin
  disp:=pdisp;
end;

procedure tform1.speedbutton1click(sender: tobject);
var
  currentwb: iwebbrowser;
begin
  currentwb := disp as iwebbrowser;
  editor:=(currentwb.document as ihtmldocument2);
  editor.designmode := 'on';
end;

procedure tform1.formcreate(sender: tobject);
begin
    webbrowser1.navigate('about:<html><body></body></html>');
end;

Теперь по порядку о том, что мы написали. В событии oncreate формы мы загружаем в браузер простую страницу (напомню, что протокол about: позволяет загружать в браузер html строку). Это необходимо для того, чтобы в последующем мы могли обращаться к документу. Сразу после этого будет вызван обработчик события ondocumentcomplete. Но пока еще ничего не произошло. Внимательный читатель мог обратить внимание, что для перевода браузера в режим редактирования надо нажать кнопку 1. editor - это экземпляр нашего документа (document). Его свойство designmode устанавливается в 'on'. Теперь наш редактор практически готов. Он уже умеет править текст, копировать/вырезать/вставлять текст и картинки, делать текст жирным/подчеркнутым/наклонным. Для этого есть соответствующие комбинации клавиш.

Стандартые сочетания клавиш

Клавиша Действие
ctrl + c Копировать
ctrl + b Жирный текст
ctrl + z Отменить
ctrl + f Найти
ctrl + x Вырезать
ctrl + i Наклонный текст
ctrl + y Повторить
ctrl + a Выделить всё
ctrl + v Вставить
ctrl + u Подчеркнутый текст
ctrl + k Гиперссылка
ctrl + left-click Выделить блок

"Это, конечно, хорошо, что есть горячие клавиши, но мне не хотелось бы все их запоминать" - можете сказать вы. Хорошо. Тогда давайте разберем, как из delphi заставить webbrowser выполнять все эти действия. Для этого есть метод execcommand интерфейса ihtmltxtrange (он описан в модуле mshtml_tlb). Рассмотрим простой пример.

procedure tform1.speedbutton2click(sender: tobject);
var
range: ihtmltxtrange;
begin
range:=(editor.selection.createrange as ihtmltxtrange);
range.execcommand('bold',false,emptyparam)
end;

Сначала в этой процедуре создается объект range. После этого вызывается метод execcommand:

function execcommand(cmdid: widestring; showui: wordbool;
                     value: olevariant): wordbool;

cmdid - это строка идентификатор команды (в нашем примере "bold" заставляет редактор переключаться между жирным и обычным начертанием текста); полный список команд смотри в приложении.

showui - show user interface - показывать интерфейс пользователя (если таковой имеется, как правило это различные диалоговые окна). Если параметр равен false, то команда выполняется без предупреждения.

value - содержит дополнительную информацию в зависимости от команды.

Несколько слов об объекте range. Помимо уже знакомого нам execcommand этот объект обладает еще рядом свойств и методов, некоторые из которых сейчас рассмотрим.

+----------------------+------------+-----------------------+
| text                 | widestring | Содержит текст        |
|                      |            | выделения (без тегов  |
|                      |            | html)                 |
+----------------------+------------+-----------------------+
| htmltext             | widestring | Полный текст          |
|                      |            | выделения             |
+----------------------+------------+-----------------------+
| movestart(const      | procedure  | Перемещает начальную  |
| unit_:widestring;    |            | позицию выделения на  |
|                      |            | count символов вправо |
| count:integer)       |            | (если count<0, т о    |
|                      |            | влево),               |
|                      |            | unit_-единицы         |
|                      |            | измерения смещения    |
|                      |            | (чаще всего           |
|                      |            | используется          |
|                      |            | 'character': 1        |
|                      |            | символ). При этом     |
|                      |            | конечная позиция не   |
|                      |            | смещается.            |
+----------------------+------------+-----------------------+
| movestart(const      | procedure  | То же самое, только   |
| unit_:widestring;    |            | для конечной позиции  |
| count:integer)       |            | выделения.            |
+----------------------+------------+-----------------------+
| pastehtml(const      | procedure  | Вставляет html-строку |
| html: widestring);   |            |                       |
+----------------------+------------+-----------------------+
| execcommandshowhelp( | function,  | Отображает помощь по  |
| cmdid:               |            | команде, указанной в  |
| widestring);         | wordbool   | cmdid                 |
+----------------------+------------+-----------------------+

Пожалуй, на сегодня всё. Об остальных объектах (картинки, таблицы, элементы управления) поговорим в другой раз.

Будут вопросы - пишите: samum2000@mail15.com.

Приложение.

Доступные команды

+-----------------------+-----------------------------------+
| backcolor             | Устанавливает или получает цвет   |
|                       | фона текущего выделения. value    |
|                       | должно содержать имя цвета или    |
|                       | его шеснадцитиричный rgb          |
|                       | эквивалент (например, #ffcc00).   |
+-----------------------+-----------------------------------+
| bold                  | Переключает начертание текста     |
|                       | текущего выделения между          |
|                       | полужирным и нормальным.          |
+-----------------------+-----------------------------------+
| copy                  | Копирует выделение в буфер обмена |
+-----------------------+-----------------------------------+
| createbookmark        | Получает имя якоря или создает    |
|                       | его для текущего выделения. value |
|                       | - строка, содержащая имя якоря.   |
+-----------------------+-----------------------------------+
| createlink            | Получает url ссылки или создает   |
|                       | новую ссылку. Параметр value      |
|                       | должен содержать url.             |
+-----------------------+-----------------------------------+
| cut                   | Вырезает текущее выделение в      |
|                       | буфер обмена.                     |
+-----------------------+-----------------------------------+
| delete                | Очищает текущее выделение         |
|                       | (удаляет всё его содержимое).     |
+-----------------------+-----------------------------------+
| find                  | Находит текст, заданный в         |
|                       | параметре value в текущем         |
|                       | выделении.                        |
+-----------------------+-----------------------------------+
| fontname              | Устанавливает шрифт для текущего  |
|                       | выделения. value содержит         |
|                       | описание этого шрифта (как в теге |
|                       | font).                            |
+-----------------------+-----------------------------------+
| fontsize              | Устанавливает размер шрифта.      |
|                       | value - число от 1 до 7           |
|                       | включительно.                     |
+-----------------------+-----------------------------------+
| forecolor             | Устанавливает цвет текста. value  |
|                       | должно содержать имя цвета или    |
|                       | его шеснадцитиричный rgb          |
|                       | эквивалент (например, #ffcc00)    |
+-----------------------+-----------------------------------+
| formatblock           | Устанавливает или получает        |
|                       | форматирование текущего блока.    |
|                       | value может содержать             |
|                       | теги-описатели.                   |
+-----------------------+-----------------------------------+
| indent                | Увеличивает отступ выделенного    |
|                       | текста на одну единицу приращения |
+-----------------------+-----------------------------------+
| insertbutton          | Записывает идентификатор кнопки   |
|                       | вместо текущего выделения. value  |
|                       | - строка, содержащая              |
|                       | идентификатор кнопки.             |
+-----------------------+-----------------------------------+
| insertfieldset        | То же для поля ввода.             |
+-----------------------+-----------------------------------+
| inserthorizontalrule  | То же для горизонтальной полосы.  |
+-----------------------+-----------------------------------+
| insertiframe          | То же для встроеных фреймов       |
|                       | (iframe).                         |
+-----------------------+-----------------------------------+
| insertimage           | То же для изображений.            |
+-----------------------+-----------------------------------+
| insertinputbutton     | То же для кнопки.                 |
+-----------------------+-----------------------------------+
| insertinputcheckbox   | То же для чекбоксов (checkbox).   |
+-----------------------+-----------------------------------+
| insertinputfileupload | То же для элемента выбора файла.  |
+-----------------------+-----------------------------------+
| insertinputhidden     | То же для скрытого поля (hidden)  |
+-----------------------+-----------------------------------+
| insertinputimage      | То же для изображения.            |
+-----------------------+-----------------------------------+
| insertinputpassword   | То же для поля ввода пароля.      |
+-----------------------+-----------------------------------+
| insertinputradio      | То же для радио-кнопок (radio)    |
+-----------------------+-----------------------------------+
| insertinputreset      | То же для кнопки reset.           |
+-----------------------+-----------------------------------+
| insertinputsubmit     | То же для кнопки submit.          |
+-----------------------+-----------------------------------+
| insertinputtext       | То же для поля ввода текста.      |
+-----------------------+-----------------------------------+
| insertparagraph       | Вставляет новый раздел (абзац).   |
+-----------------------+-----------------------------------+
| insertorderedlist     | Переключает стиль текущего        |
|                       | выделения между списком и простым |
|                       | текстом.                          |
+-----------------------+-----------------------------------+
| insertunorderedlist   | То же самое.                      |
+-----------------------+-----------------------------------+
| insertselectdropdown  | Записывает элемент drop-down      |
|                       | вместо текущего выделения. value  |
|                       | должно содержать идентификатор    |
|                       | элемента.                         |
+-----------------------+-----------------------------------+
| inserttextarea        | То же для элемента textarea.      |
+-----------------------+-----------------------------------+
| italic                | Переключает начертание текста     |
|                       | текущего выделения между          |
|                       | наклонным и обычным.              |
+-----------------------+-----------------------------------+
| justifycenter         | Устанавливает выравнивание по     |
|                       | центру для всего блока, в котором |
|                       | расположено текущее выделение.    |
+-----------------------+-----------------------------------+
| justifyleft           | Устанавливает выравнивание по     |
|                       | левому краю для всего блока, в    |
|                       | котором расположено текущее       |
|                       | выделение.                        |
+-----------------------+-----------------------------------+
| justifyright          | Устанавливает выравнивание по     |
|                       | правому краю для всего блока, в   |
|                       | котором расположено текущее       |
|                       | выделение.                        |
+-----------------------+-----------------------------------+
| outdent               | Уменьшает отступ для всего блока, |
|                       | в котором расположено выделение,  |
|                       | на одну единицу.                  |
+-----------------------+-----------------------------------+
| overwrite             | Переключается между режимами      |
|                       | вставки текста и замены текста    |
|                       | при вводе. value: true - замена,  |
|                       | false - вставка.                  |
+-----------------------+-----------------------------------+
| paste                 | Вставляет текст из буфера обмена  |
|                       | вместо текущего выделения.        |
+-----------------------+-----------------------------------+
| refresh               | Обновляет текущий документ.       |
+-----------------------+-----------------------------------+
| removeformat          | Удаляет из текущего фрагмента все |
|                       | теги форматирования               |
+-----------------------+-----------------------------------+
| selectall             | Выделяет все содержимое           |
|                       | документа.                        |
+-----------------------+-----------------------------------+
| unbookmark            | Удаляет все закладки из текущего  |
|                       | выделения.                        |
+-----------------------+-----------------------------------+
| underline             | Переключает начертание текста     |
|                       | текущего выделения между          |
|                       | подчеркнутым и обычным.           |
+-----------------------+-----------------------------------+
| unlink                | Удаляет все гиперссылки из        |
|                       | текущего выделенного фрагмента.   |
+-----------------------+-----------------------------------+
| unselect              | Снимает выделение.                |
+-----------------------+-----------------------------------+

Часть II.

В прошлый раз речь шла о том, как работать с текстом в html редакторе. В этот раз мы поговорим о том, как работать с другими объектами html страниц - контролами. К ним относятся всевозможные элементы управления, изображения, фреймы, таблицы.

Рассмотрим общий принцип работы с этими элементами. Как и в случае с текстом, прежде всего надо создать объект-выделение (назовем его range):

range: ihtmlcontrolrange;

Интерфейс ihtmlcontrolrange предназначен специально для выполнения различных операций с выделенными объектами страницы, однако, его совершенно невозможно применять для работы с текстовым выделением - вы получите исключительную ситуацию eintfcasterror с сообщением о том, что выбраннй интерфейс не поддерживается (тоже самое будет, если использовать ihtmltxtrange для работы с контролами). Чтобы избегать подобных ситуаций, в интерфейсе ihtmlselectionobject введено поле type_: widestring. В зависимости от типа выделения оно будет содержать "control" или "text" (если ничего не выделено, то это поле будет содежать "none"). Вот простой пример того, как можно вставить картинку в определенное место документа (как открыть документ в режиме редактирования было описано в первой статейке):

procedure tform1.speedbutton13click(sender: tobject);
var
  ctrlrange: ihtmlcontrolrange;
  textrange: ihtmltxtrange;
begin
  if editor.selection.type_='control' then
  begin
    ctrlrange:=(editor.selection.createrange as ihtmlcontrolrange);
    if not ctrlrange.querycommandenabled('insertimage') then
      application.messagebox('not supported!','');
    else
      ctrlrange.execcommand('insertimage',false,'c:\my files\porshe1.jpg')
  end
  else
  begin
    textrange:=(editor.selection.createrange as ihtmltxtrange);
    textrange.execcommand('insertimage',false,'c:\my files\porshe1.jpg')
  end;
end;

Обратите внимание на то, что когда веделен объект, мы используем метод querycommandenabled чтобы убедиться, что данную комманду можно выполнить над выделенным контролом. Это связано с тем, что, например, встроенный фрейм нельзя заменить на картинку. На самом деле это проверка необязательная, но я все же рекомендую её проводить во избежание неприятных последствий.

Еще один метод - querycommandsupported(cmdid: widestring): boolean позволяет выянить, поддерживается ли данная комманда данным типом выделения. Такие же методы есть и у интерфейса ihtmltxtrange, но в данном случае в них нет необходимости.

С таблицами дело обстоит гораздо сложнеее. Контролы типа htmltable, htmlrow и htmlcell, согласно документации от microsoft, предназначены для создания таблиц при формировании страницы на стороне сервера. Соответсвенно, в нашем случае возникают некоторые трудности: в частности, как добавить полученную таблицу в документ (во всяком случае, у меня ничего не вышло). Как вариант я предлагаю следующее: создавать таблицу типа htmltable, работать с ней так, как будто мы формируем документ на сервере, а затем, использовать свойство outerhtml. Это поле содержит текстовое представление таблицы в формате html. Рассмотрим подробнее этот способ на примере:

procedure tform1.speedbutton14click(sender: tobject);
var 
  table: htmltable; 
  textrange: ihtmltxtrange; 
  row: htmltablerow; 
  col: htmltablecol; 
  i: integer; 
begin 
  if editor.selection.type_<>'control' then 
  begin 
    table:=(editor.createelement('table') as htmltable); 
    for i:=0 to 3 do 
    begin 
      row:=(table.insertrow(i) as disphtmltablerow); 
      col:=(row.insertcell(0) as disphtmltablecol); 
      col.width:='200'; 
      col.style.bordercolor:='#ff0000'; 
      col.innertext:='Ячейка #'+inttostr(i); 
    end; 
    table.style.bordercolor:='#00ff00'; 
    textrange:=(editor.selection.createrange as ihtmltxtrange); 
    textrange.pastehtml(table.outerhtml); 
  end; 
end; 

На мой взгляд, этот пример достаточно информативен. Очевидное преимущество использования объекта htmltable и сопутствующих ему объектов состоит в том, что программисту не надо беспокоится о том, как описать на html то или иное свойство таблицы, нет необходимости работать со строками, писать парсеры и т.п. - таблица сама себя опишет. Однако, очевидным недостатком такого метода является то, что в последствии невозможно будет обратиться к созданной таблице как к объекту, и изменить её програмным методом (использование парсеров и интерпритаторов кода в расчет брать не будем).

Дополнение от 27.07.2004:

Я еще несколько раз прочитал msdn и нашел таки способ нормально работать с таблицами и ячейками. Вот простой пример того, как можно заменить текст в уже созданной таблице:

var
  i, j: integer;
  ovtable: olevariant;
  t: htmltable;
begin
  // В документе должна быть таблица, описанная примерно так:
  //<table ... class="mytable">

  ovtable := webbrowser1.oleobject.document.getelementsbyname('mytable').item(0);
  //webbrowser1.oleobject.document.getelementsbyname('mytable') - 
  //это коллекция элементов (ведь несколько элементов могут иметь 
  //id равный "mytable"
  for i := 0 to (ovtable.rows.length - 1) do
    for j := 0 to (ovtable.rows.item(i).cells.length - 1) do
      ovtable.rows.item(i).cells.item(j).innertext:='new text!';
end;

То есть теперь у нас есть возможность как получать данные из таблицы, так и заносить их туда в любой момент времени. Все свойства соответствуют свойствам dom. Остается только сказать, что таким образом можно работать и с формами, и с изображениями, в общем со всем, что поддерживается в Объектной модели докумета (dom).] Если вы знаете более изящный способ работы с таблицами, или можете чем-то дополнить изложенное выше, очень прошу вас написать мне на e-mail: samum2000@mail15.com

Previous page:
Компонента HTML-редактора
Top:
DRKB
Next page:
Веб-страничка внутри Delphi-приложения