Визуальный 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

