Проекты Логинова Дмитрия | ||||||||||||||||||||||||||
|
Пишем свой HTML-редактор (wysiwyg HTML editor)Версия от 26.07.2010
ВведениеСтандартный компонент TWebBrowser, находящийся с составе Delphi - это мощное средство, несущее в себе огромное множество возможностей. В частности, в нем есть все, что необходимо для написания визуального HTML-редактора. Преимуществом такого редактора является возможность открытия и правки практически любой HTML-страницы, даже весьма сложной. В качестве наглядного примера к этой статье вы можете использовать готовый HTML-редактор. Данный редактор был разработан как удобное средство для редактирования шаблонов HTML-отчетов системы ПТК АЗС1. Поэтому не ставилось задачи дать доступ ко всем возможностям HTML / CSS / JavaScript. Да и дело это практически бесконечное, учитывая количество этих самых возможностей. Основной упор делался на интерфейсе. Интерфейс должен быть русским, должен позволять работать со всеми стандартными тэгами, атрибутами и стилями, но не требовать от пользователя каких-либо познаний в HTML, ведь обычный пользователь HTML не осилит. Все текстовые константы атрибутов и стилей должны быть русифицированы и доступны в виде списков. А самое главное для отчетности - это таблицы. Соответственно редактор должен обеспечивать все, что нужно для работы с таблицами. Ни один бесплатный редактор не умеет работать с таблицами на уровне, приемлемом для отчетности ПТК АЗС. Неплохая работа с таблицами реализована в платной программе MS Word, но и она является неприемлемой, поскольку со сложными таблицами MS Word в некоторых случаях работает некорректно. О программе OpenOffice Writer серьезно говорить и подавно нельзя, т.к. работа с таблицами реализована отвратительно. Поскольку бесплатного редактора, удовлетворяющего этим требованиям в природе не существовало, назрела очевидная необходимость разработать такой редактор собственными силами. В данной статье описаны основные приемы, которые были использованы при написании HTML-редактора. Поскольку никакой более-менее толковой документации с описанием интерфейса работы с WebBrowser по всей видимости не существует, информацию приходится собирать по всему интернету буквально по крупицам. Отправной точкой при написании HTML редактора были статьи Визуальный HTML редактор своими руками Часть I и Часть II. Здесь очень мало материала, но для меня он оказался очень важен, т.к. послужил стартовым импульсом. Режим редактированияИтак, предлагаю создать в Delphi новый VCL-проект и кинуть на форму компонент TWebBrowser. Он находится в палитре компонентов на вкладке Internet. Для краткости имя компонента будет "wb" (просто писать каждый раз WebBrowser1.XXX лениво). Для начала работы с любым документом его нужно открыть. До этого никакой полезной работы совершить с компонентом TWebBrowser мы не сможем. Даже если мы не хотим открывать какого-либо конкретного документа, мы все равно должны открыть как минимум пустой документ.
Прежде чем начать какую-либо работу с HTML-страницей, необходимо дождаться, когда она будет полностью загружена. Один из способов сделать это - дождаться вызова метода OnDocumentComplete. В этом методе работать непосредственно с HTML-страницей еще нельзя (вернее некоторые вещи уже работают, а некоторые еще нет). Зато уже здесь можно перевести документ в режим редактирования, например: Стандартные операцииНиже предлагается ряд команд, с помощью которых можно выполнить стандартные операции с HTML-редактором:
Иногда перед выполнением команды полезно проверить, применима ли она при данном состоянии документа. Например, если отсутствует выделенный фрагмент текста, то команда "Копировать" не сработает. Если в данный момент в буфере обмена находятся недопустимые данные, то команда "Вставить" не сработает. Выполнить проверку можно следующим образом: Операции с текстомКроме того, имеется большой набор команд, которые можно выполнить над выделенным фрагментом текста. Перед тем, как воспользоваться такими командами, необходимо получить ссылку на объект IHTMLTxtRange. Это можно выполнить следующим образом: Если Editor.selection.type_ имеет значение None или Text, это значит, что осуществляется работа с текстом. Значение None означает, что курсор находится внутри текста, но при этом текст не выделен. Значение Text означает, что текст выделен. Значение Control означает, что в данный момент выделен некоторый объект (таблица, блок, кнопка и т.д.). Функция DocRange возвращает NIL, если выделен объект, поэтому при работе с ней результат следует проверять на равенство NIL. С помощью метода execCommand можно выполнять различные операции над текстовым фрагментом: изменять цвет, размер, наклонность, жирность, наименование и многие другие параметры шрифта, управлять выравниванием текста, нумерованными и маркированными списками, добавлять гиперссылки, картинки, произвольный текст и многое другое. Ниже приведены примеры использования команды execCommand:
С помощью метода queryCommandValue можно узнать параметры шрифта для текстового фрагмента. Например: В некоторых случаях метод queryCommandValue не может определить значения требуемого параметра. Например, если размер шрифта задан с помощью стилей (CSS), то метод DocRange.queryCommandValue('FontSize') может вернуть NULL. В таких случаях результат метода queryCommandValue следует проверять с помощью функции VarIsNull(). Будет полезно отметить, что методы DocRange.queryCommandValue('ForeColor') и DocRange.queryCommandValue('BackColor') возвращают строку с числовым значением цвета. Для преобразования такой строки в TColor достаточно вызвать функцию StrToInt(). Метод pasteHTML позволяет вставить в текущую позицию курсора произвольный HTML-текст. При этом если текст был выделен, то выделенный текст будет удален и на его место встанет новый текст. Например: Для просмотра HTML-кода выделенного текстового фрагмента служит метод DocRange.htmlText. Одной из особенностей данного метода является то, что он автоматически добавляет в строку-результат открывающие / закрывающие HTML-тэги, если в этом есть необходимость. С методом pasteHTML существует немало проблем. Он сложнее, чем кажется на первый взгляд. Например, он автоматически заменяет "<" на "<", когда видит, что символ "<" не относится к HTML-тэгу. Он автоматически добавляет закрывающий тэг, если вы его пропустили. Одним словом данный метод делает все для обеспечения целостности кода HTML-документа. Однако иногда использование этого метода в паре с DocRange.htmlText приводит к появлению лишних вложенных HTML-тэгов в результате ненужного дублирования. Например если выделить фрагмент "<b>Текст</b>", т.е. Текст, извлечь данный фрагмент с помощью метода DocRange.htmlText, а затем передать его в метод DocRange.pasteHTML(), то в результате в HTML-коде мы получим "<b><b>Текст</b></b>". Работа с тэгами HTMLВ HTML-редакторе, приведенном по ссылке выше, для любого выбранного элемента вы можете определить полный путь к элементу, состоящий из вложенных друг в друга HTML-тэгов. Для начала построения этого пути необходимо определить HTML-тэг, в котором находится выбранный фрагмент. Это делается с помощью метода DocRange.parentElement. Результат функции DocRange следует проверять на равенство NIL. Вероятно, что выделен не текст, а тэг объекта (к ним относятся тэги <TABLE>, <IMG>, <HR> и др.). Тогда нужно получать ссылку на HTML-элемент с помощью кода ControlRange.commonParentElement, где функция ControlRange реализована следующим образом:
Для управления атрибутами вы можете использовать методы setAttribute, getAttribute, removeAttribute. Но есть и более удобный способ для этого. В интерфейсном модуле MSHTML.pas есть огромное множество объявлений. И среди этих объявлений нашлось место для описания всех возможных атрибутов для каждого HTML-тэга. Каждому HTML-тэгу соответствует свой интерфейс:
Это далеко не полный перечень. Для использования возможностей этих интерфейсов можно выполнить приведение типов, например IHTMLElement as IHTMLTable. Еще раз подчеркнем, что можно не использовать все эти интерфейсы, а обойтись одним лишь IHTMLElement и его методами setAttribute, getAttribute, removeAttribute. Кому как удобнее.Работа со встроенной таблицей стилей CSSДля доступа к встроенной таблице стилей интерфейс IHTMLElement предоставляет свойство "style", которое имеет тип IHTMLStyle. Допустим, мы имеем переменную Elem: IHTMLElement., инициализированную следующим образом:
Следует более подробно остановиться на составных стилях border, borderColor, borderWidth, borderStyle, borderTop, borderRight, borderBottom, borderLeft. Эти стили использовать программным путем не рекомендуется. Во-первых, они являются составными, и поэтому их сложно обработать. Во-вторых, мы можем получить пустую строку, хотя стили заданы. Лучше работать с отдельными компонентами этих стилей: borderTopColor, borderTopWidth, borderTopStyle и т.д. Это проще и надежнее. Кстати работать со стилями, которые возвращают цвет, не так-то просто. Например красный цвет может быть задан как минимум тремя способами: 'red', '#FF0000', 'rgb(255,0,0)', так что учитывайте это многообразие. Обработка событийПри написании HTML-редактора необходимо организовать обработку событии от области работы с текстом. Как минимум следует отлавливать щелчки мышкой и нажатия клавиш клавиатуры. При этом можно обновлять состояние различных элементов на панели инструментов. Вряд ли кто-то без посторонней помощи разберется как это сделать. В данном лучше всего дать готовый кусок кода без лишних объяснений: Объявляем класс TEventMethod: TEventMethod = class(TInterfacedObject, IDispatch) Жмем Ctrl+Shift+C, в результате чего Delphi автоматически создаст соответствующие методы. В методах GetTypeInfoCount, GetTypeInfo, GetIDsOfNames пишем Result := 0. А в методе Invoke пишем код, которых будет вызываться при возникновении соответствующих событий. В классе TEventMethod больше нельзя объявлять никаких полей, иначе механизм перестанет работать. Объявляем переменную EvtMethod: IDispatch; Создаем ее с помощью кода: EvtMethod := TEventMethod.Create; В событии OnDocumentComplete можно прописать обработчики: Editor.onmousedown := EvtMethod; Вот и все дела! :) Идея взята из Королевства Делфи: http://www.delphikingdom.com/asp/articles_forum.asp?ArticleID=1319. Работа с таблицамиСамое сложное при написании HTML-редактора - работа с таблицами. Начнем с самого простого. Пусть есть переменная Tab: IHTMLTable, и она каким-то образом у нас уже инициализирована. Тогда мы может получить количество строк таблицы: Tab.rows.length. А вот так будет выглядеть цикл перебора строк таблицы:
В принципе на основе этого строится вся обработка таблицы. Вроде бы ничего сложного. Однако проблемы возникают. И судя по тому, что практически не один бесплатный HTML-редактор не осиляет сложные таблицы, далеко не все разработчики с этими проблемами справляются. Для работы с таблицами нужно реализовать, по крайней мере, 9 операций: добавление столбца слева, добавление столбца справа, вставка строки сверху, вставка строки снизу, удаление столбца, удаление строки, объединение с ячейкой справа, объединение с ячейкой снизу, разбивка объединенной ячейки. Пусть это упрощенный набор, но его достаточно для любых манипуляций с таблицей. Посмотрите на типичный вид таблицы:
Вроде все очевидно. Однако как только появляются объединенные ячейки, так сразу начинаются проблемы. Посмотрите на следующую таблицу:
Вот ее код: И как с ней работать? Уже не понятно. И это далеко не самая сложная таблица. Посмотрите на HTML-код второй строки. Разве можно по нему сказать, где на самом деле эта ячейка находится? Она может быть как слева, так и в центре, так и справа. Т.е. для уточнения нужно смотреть строку, стоящую выше. А эта строка в свою очередь может потребовать точно такого же уточнения. Самый логичный выход из ситуации - составить маску таблицы перед началом любой ее обработки. Первая строка таблицы - самая важная. По ней мы можем узнать реальное число ячеек (мы видим одну ячейку, а на самом деле она может быть объединением десятка ячеек). По остальным строкам реальное число ячеек достоверно определить нельзя. Мы точно знаем число строк таблицы (Tab.rows.length). Создаем массив нужных размеров и постепенно заполняем его путем обхода строк таблицы сверху вниз. Каков формат элементов массива маски таблицы - решайте сами. Главное, чтобы вы смогли его эффективно использовать в дальнейшем. В результате формирования маски мы уже четко видим реальную картину таблицы и дальше можем без особых усилий (но без них тут все равно не обойдешься) реализовать остальные операции по работе с таблицей. Путаница с выравниванием по центруДля выравнивания текста по горизонтали и вертикали используются соответственно атрибуты align и valign. Выравнивание по центру по горизонтали должно задаваться так: align=center. Выравнивание по центру по вертикали должно задаваться так: valign=middle. В случае с компонентом WebBrowser получается так, что при запросе значения атрибута методом getAttribute все работает правильно, а если сохранить документ и посмотреть полученный HTML-код, то мы увидим, что align=middle, valign=middle. Это неправильно! Это влияет на отрисовку HTML-страницы в разных браузерах. На самом деле проблема не является столь серьезной, поскольку выравнивание по центру по вертикали обычно установлено по умолчанию, и задавать его вручную не требуется. Сохранение HTML-страницы в файлВ принципе достаточно получить код тэга <html> и сохранить его в файл с помощью любого известного способа. При этом мы можем выполнить необходимую обработку HTML-страницы перед ее сохранением (например, исправить ошибку выравнивания по центру). Однако при таком подходе следует понимать, что метод outerHTML возвращает HTML-код в формате Unicode, и дальше, в зависимости от версии Delphi (юникодная или неюникодная) мы можем запросто сохранить HTML-страницу в неправильной кодировке, а также потерять некоторые юникодные символы. Представленный HTML-редактор на данный момент может работать только с HTML-страницами в кодировке "windows-1251". В любом случае, необхомимо иметь возможность сохранять HTML-страницу средствами WebBrowser. Делается это просто: Проблема с потерей фокусаInternet Explorer в режиме редактирования постоянно теряет фокус. Стоит переключиться на другое окно, и все, мигающий курсор в редакторе исчезает! Даже если мы вернет фокус окну редактора, курсор все равно не замигает. Замое замечательное - стандартные команды передачи фокуса (SetFocus, ActiveControl и т.п.) не работают! Решение проблемы очень простое. Следует при необходимости вызывать код: if Assigned(Editor) then Помимо потери фокуса, есть еще масса проблем с курсором. Что делать, если для выбранного элемента открыли окно свойств? Курсор при этом исчезает, и врядли его можно заставить мигать стандартными средствами. В представленном HTML-редакторе вместо стандартного курсора на экран выводится панель TPanel, поэтому вы можете и не столкнуться с проблемами курсора. Вешаем собственное контекстное менюПростейший способ заменить стандартное контекстное меню Internet Explorer своим собственным - ловить сообщение WM_MOUSEACTIVATE и по этому событию вызывать собственное контектное меню. Этот метод многие используют, однако он является ненадежным и в некоторых случаях не работает, или работает не правильно. Разработчики WebBrowser предусмотрели специальный механизм, позволяющий надежно показать свое меню в нужном месте и в нужное время. Для того, чтобы ваше приложение могло использовать этот механизм, вам нужно реализовать интерфейсы IDocHostUIHandler и IOleClientSite. Сразу хочу обрадовать: в Delphi нет объявления интерфейса IDocHostUIHandler. Зато к большому счастью, в Интернете имеются готовые модули, в которых есть и объявление и реализация этих интерфейсов. Есть хорошая статься на эту тему: How to customise the TWebBrowser user interface. А необходимые модули можно скачать с этой страницы. Думаю, здесь не стоит давать лишних пояснений, тем более указанная статья достаточно подробно почти что на родном английском все объясняет. Просто скопируйте модули IntfDocHostUIHandler.pas, UNulContainer.pas, UContainer.pas в каталог своего проекта и добавьте в проект модуль UContainer.pas (в этот модуль вы можете при необходимости внести любые изменения). Добавьте модуль UContainer в список uses модуля формы, на которой лежит компонент WebBrowser. Объявите поле формы fWBContainer: TWBContainer. В обработчике события FormCreate (в самом его начале) добавьте код: Разместите на форме компонент TPopupMenu и укажите его в свойстве PopupMenu компонента WebBrowser. Можете запустить приложение и убедиться в том, что механизм работает. За это отвечает следующий код из модуля UContainer.pas: Здесь реализована функция, которую WebBrowser автоматически вызывает при необходимости отобразить контекстное меню. В принципе, то же самое вы можете найти в ДЕМО-проекте, сопровождающем указанную статью. Перехватываем нажатие клавишДля этого добавляем в класс TWBContainer следующий метод:function TranslateAccelerator(const lpMsg: PMSG; const pguidCmdGroup: PGUID; const nCmdID: DWORD): HResult; stdcall; Вот пример его реализации: if Assigned(FOnPageOpen) then if Assigned(FOnPageSave) then if Assigned(FOnPageNew) then Здесь DoPageOpen, DoPageSave, DoPageNew - некие действия, которые можно выполнить при открытии (Ctrl+O) / сохранении (Ctrl+S) / создании (Ctrl+N) страницы. Результат S_OK говорит о том, что мы сами обрабатываем данное сочетание клавиш, и поэтому WebBrowser на них никак не реагирует. В принципе, данный метод можно использовать при обработке событий от клавиатуры вместо TEventMethod. На этом пока все! Статья со временем будет дорабатываться! 1ПТК АЗС - система автоматизации АЗС, АГЗС, МТАЗК от компании ООО "АВТОМАТИКА плюс" www.automatikaplus.ru | |||||||||||||||||||||||||
Логинов Дмитрий © 2005-2015 |