Изображение квадрата Дюрера

ООО АВТОМАТИКА плюс

Rambler's Top100

Рейтинг@Mail.ru

Пишем свой 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 мы не сможем. Даже если мы не хотим открывать какого-либо конкретного документа, мы все равно должны открыть как минимум пустой документ.
Для открытия пустого документа можно воспользоваться следующим кодом:
  wb.Navigate('about:<html><body></body></html>');
Но так делать я не рекомендую. На практике лучше создать HTML-файл, сохранить его на диск и указать имя этого файла в методе Navigate. Это связано с тем, что протокол about игнорирует кодировку страницу, поэтому здесь кодировку указывать бесполезно. Вот пример кода для создания нового документа:
var
  B: Boolean;
  AFile: string;
begin
  AFile := 'temp.html';
  with TStringList.Create do
  try
    Text := '<html><head>'+
    '<META http-equiv=Content-Type content="text/html; '+
    'charset=windows-1251">'+
    '</head>'+
    '<body text=#000000 bgColor=#ffffff>'+
    'Введите текст HTML-страницы'+
    '</body></html>';
    SaveToFile(AFile);
  finally
    Free;
  end;
  B := True;
  wb.Navigate(AFile, B);
end;


Здесь указана русская кодировка HTML-страницы windows-1251. Метод Navigate следует вызывать по крайней с 2-мя параметрами. Второй параметр должен быть True. Иначе на некоторых компьютерах для открытия HTML-страницы запускается отдельное окно Internet Explorer, что для нас крайне нежелательно.

Прежде чем начать какую-либо работу с HTML-страницей, необходимо дождаться, когда она будет полностью загружена. Один из способов сделать это - дождаться вызова метода OnDocumentComplete. В этом методе работать непосредственно с HTML-страницей еще нельзя (вернее некоторые вещи уже работают, а некоторые еще нет). Зато уже здесь можно перевести документ в режим редактирования, например:
  ((pDisp as IWebBrowser).Document as IHTMLDocument2).designMode := 'On';

Кроме того, здесь можно запомнить ссылку на IHTMLDocument2, например:
  var
     Editor: IHTMLDocument2;
  Editor := (pDisp as IWebBrowser).Document as IHTMLDocument2;
В дальнейшем для работы с документом будем использовать переменную Editor.
Выйти из режима редактирования можно в любой момент, если присвоить Editor.designMode := 'Off'.

Стандартные операции

Ниже предлагается ряд команд, с помощью которых можно выполнить стандартные операции с HTML-редактором:

  • wb.ExecWB(OLECMDID_COPY, OLECMDEXECOPT_DONTPROMPTUSER); - копирование выделенного фрагмента в буфер обмена;
  • wb.ExecWB(OLECMDID_CUT, OLECMDEXECOPT_DONTPROMPTUSER); - вырезка выделенного фрагмента в буфер обмена;
  • wb.ExecWB(OLECMDID_PASTE, OLECMDEXECOPT_DONTPROMPTUSER); - вставка в текущую позицию из буфера обмена;
  • wb..ExecWB(OLECMDID_UNDO, OLECMDEXECOPT_DONTPROMPTUSER); - отмена последнего изменения;
  • wb.ExecWB(OLECMDID_REDO, OLECMDEXECOPT_DONTPROMPTUSER); - отмена отмены последнего изменения;
  • wb.ExecWB(OLECMDID_ZOOM, OLECMDEXECOPT_DONTPROMPTUSER, Param); - управляет размерами шрифта на HTML-страницы. Позволяет пользователю увеличить или уменьшить шрифт с целью более удобного просмотра. Значения от 0 (самый мелкий) до 4 (самый крупный). Похоже, что в Internet Explorer эта команда действует только на текст, для которого уже явно задан размер шрифта с помощью тэга <FONT>. Если размер шрифта задан с помощью стилей, то данная команда не действует. Кроме того, если вы хотите, чтобы команда OLECMDID_ZOOM действовала на текст в ячейках таблицы, то в каждой ячейке необходимо разместить тэг <FONT>;
  • wb.ExecWB(OLECMDID_PRINTPREVIEW, OLECMDEXECOPT_DONTPROMPTUSER); - открывает окно предварительного просмотра. Из этого окна можно вывести документ на принтер.
  • wb.ExecWB(OLECMDID_PRINT, OLECMDEXECOPT_DONTPROMPTUSER); - печать документа на принтере;

Иногда перед выполнением команды полезно проверить, применима ли она при данном состоянии документа. Например, если отсутствует выделенный фрагмент текста, то команда "Копировать" не сработает. Если в данный момент в буфере обмена находятся недопустимые данные, то команда "Вставить" не сработает. Выполнить проверку можно следующим образом:
function CanCopy: Boolean; // Проверяет возможность копирования
begin
  Result := wb.QueryStatusWB(OLECMDID_COPY) <> 1;
end;


function CanPaste: Boolean; // Проверяет возможность вставки
begin
  Result := wb.QueryStatusWB(OLECMDID_PASTE) <> 1;
end;


function CanUndo: Boolean; // Проверяет возможность отмены
var
  I: OLECMDF;
begin
  I := wb.QueryStatusWB(OLECMDID_UNDO);
  Result := not (I in [0, 1]);
end;

Операции с текстом

Кроме того, имеется большой набор команд, которые можно выполнить над выделенным фрагментом текста. Перед тем, как воспользоваться такими командами, необходимо получить ссылку на объект IHTMLTxtRange. Это можно выполнить следующим образом:
function DocRange: IHTMLTxtRange;
var
  SelType: string;
begin
  Result := nil;
  SelType := Editor.selection.type_; // None / Text / Control
  if SelType <> 'Control' then
    Result := (Editor.selection.createRange as IHTMLTxtRange);
end;

Если Editor.selection.type_ имеет значение None или Text, это значит, что осуществляется работа с текстом. Значение None означает, что курсор находится внутри текста, но при этом текст не выделен. Значение Text означает, что текст выделен. Значение Control означает, что в данный момент выделен некоторый объект (таблица, блок, кнопка и т.д.). Функция DocRange возвращает NIL, если выделен объект, поэтому при работе с ней результат следует проверять на равенство NIL.

С помощью метода execCommand можно выполнять различные операции над текстовым фрагментом: изменять цвет, размер, наклонность, жирность, наименование и многие другие параметры шрифта, управлять выравниванием текста, нумерованными и маркированными списками, добавлять гиперссылки, картинки, произвольный текст и многое другое. Ниже приведены примеры использования команды execCommand:

  • DocRange.execCommand('bold', false, emptyparam); - включение / выключение жирности шрифта;
  • DocRange.execCommand('italic', false, emptyparam); - включение / выключение наклонности шрифта;
  • DocRange.execCommand('StrikeThrough', false, emptyparam); - включение / выключение зачеркнутости шрифта;
  • DocRange.execCommand('Underline', false, emptyparam); - включение / выключение подчеркнутости шрифта;
  • DocRange.execCommand('JustifyLeft', false, emptyparam); - выравнивание по левому краю;
  • DocRange.execCommand('JustifyCenter', false, emptyparam); - выравнивание по центру;
  • DocRange.execCommand('JustifyRight', false, emptyparam); - выравнивание по правому краю;
  • DocRange.execCommand('JustifyFull', false, emptyparam); - выравнивание по ширине;
  • DocRange.execCommand('JustifyNone', false, emptyparam); - отключение выравнивания;
  • DocRange.execCommand('Subscript', false, emptyparam); - делает текст подстрочным;
  • DocRange.execCommand('Superscript', false, emptyparam); - делает текст надстрочным;
  • DocRange.execCommand('RemoveFormat', false, emptyparam); - сброс всех атрибутов шрифта;
  • DocRange.execCommand('CreateLink', True, emptyparam); - создание гиперссылки;
  • DocRange.execCommand('InsertHorizontalRule', false, emptyparam); - добавление горизонтальной линии;
  • DocRange.execCommand('Indent', false, emptyparam); - увеличивает отступ текста;
  • DocRange.execCommand('Outdent', false, emptyparam); - уменьшает отступ текста;
  • DocRange.execCommand('InsertImage', true, emptyparam); - вставляет изображение;
  • DocRange.execCommand('InsertOrderedList', false, emptyparam); - форматирует выделенный фрагмент как нумерованные список / удаляет форматирование;
  • DocRange.execCommand('InsertUnorderedList', false, emptyparam); - форматирует выделенный фрагмент как маркированный список / удаляет форматирование;
  • DocRange.execCommand('BackColor', false, V); - устанавливает цвет фона. В качестве V должен передаваться цвет, закодированный в текстовом виде в формате #RRGGBB (например #FF0000 - красный);
  • DocRange.execCommand('ForeColor', false, V); - устанавливает цвет текста;
  • DocRange.execCommand('FontName', false, V); - устанавливает имя шрифта. Например V='Times New Roman';
  • DocRange.execCommand('FontSize', false, V); - устанавливает размер шрифта. Размер шрифта может принимать значения от '1' до '7'. Кроме того можно использовать значения '+1', '+2'. . . '-1', '-2' и т.д. Вариантный параметр V должен быть строкой.

С помощью метода queryCommandValue можно узнать параметры шрифта для текстового фрагмента. Например:
function SelectedIsBold(): Boolean; // Возвращает TRUE, если текст жирный
begin
  Result := DocRange.queryCommandValue('bold') = True;
end;

В некоторых случаях метод queryCommandValue не может определить значения требуемого параметра. Например, если размер шрифта задан с помощью стилей (CSS), то метод DocRange.queryCommandValue('FontSize') может вернуть NULL. В таких случаях результат метода queryCommandValue следует проверять с помощью функции VarIsNull().

Будет полезно отметить, что методы DocRange.queryCommandValue('ForeColor') и DocRange.queryCommandValue('BackColor') возвращают строку с числовым значением цвета. Для преобразования такой строки в TColor достаточно вызвать функцию StrToInt().

Метод pasteHTML позволяет вставить в текущую позицию курсора произвольный HTML-текст. При этом если текст был выделен, то выделенный текст будет удален и на его место встанет новый текст. Например:
DocRange.pasteHTML('<MARQUEE>Бегущая строка</MARQUEE>');
В результате в текущей позиции появится бегущая строка. Для ее просмотра в динамике следует выйти из режима редактирования.
DocRange.pasteHTML('<BR>'); - будет добавлен разрыв строки.

Для просмотра HTML-кода выделенного текстового фрагмента служит метод DocRange.htmlText. Одной из особенностей данного метода является то, что он автоматически добавляет в строку-результат открывающие / закрывающие HTML-тэги, если в этом есть необходимость.

С методом pasteHTML существует немало проблем. Он сложнее, чем кажется на первый взгляд. Например, он автоматически заменяет "<" на "&lt;", когда видит, что символ "<" не относится к 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 реализована следующим образом:

function ControlRange: IHTMLControlRange;
var
  SelType: string;
begin
  Result := nil;
  SelType := Editor.selection.type_; // None / Text / Control
  if SelType = 'Control' then
    Result := (Editor.selection.createRange as IHTMLControlRange);
end;

Данные методы (parentElement, commonParentElement) возвращают результат IHTMLElement. Это базовый описатель HTML-тэга. Он позволяет работать с параметрами, свойственными для любых HTML-тэгов. Далее для полученного элемента мы может определить родительский HTML-тэг опять же с помощью метода parentElement. Таким образом для построения пути из HTML-тэгов достаточно одного простенького цикла. Интерфейс IHTMLElement обладает следующими полезными свойствами и методами:

  • className - имя класса (используется для работы с CSS);
  • id - идентификатор тэга. Используется для организации динамических обращений к HTML-элементу (например из JavaScript);
  • tagName - наименование тэга (всегда в верхнем регистре, вероятно регистр зависит от каких-то дополнительных настроек);
  • parentElement - ссылка на родительский HTML-тэг (т.е. элемент IHTMLElement);
  • style - ссылка на встроенную таблицу стилей IHTMLStyle. Для работы в внешними таблицами стилей указывайте className;
  • onclick, ondblclick, onkeydown и т.п. - обработчики соответствующих событий для данного HTML-элемента;
  •  title - всплывающая подсказка для данного HTML-элемента;
  • offsetLeft, offsetTop, offsetWidth, offsetHeight - позволяют определить текущие координаты и размеры данного HTML-элемента;
  • outerHTML - возвращает / изменяет HTML-код элемента вместе со всем его содержимым. Например вызов outerHTML для HTML-элемента <html> позволит получить полный код HTML-страницы. Далеко не каждый элемент можно изменить с помощью outerHTML. В таких случаях попытка изменения приведет к ошибке.
  • innerHTML - возвращает / изменяет HTML-код внутреннего содержимого HTML-элемента. Далеко не у каждого элемента можно изменить содержимое с помощью innerHTML. В таких случаях попытка изменения приведет к ошибке.
  • all - возвращает коллекцию вложенных HTML-тэгов IHTMLElementCollection.
  • setAttribute - устанавливает значение заданного атрибута HTML-тэга. Каждый тэг имеет свой собственный набор атрибутов. К атрибутам не относятся "class", "style". Любой HTML-тэг имеет атрибуты "id" и "title". Большинство тэгов никаких других атрибутов кроме этих двух не имеют. Все элементы формы (кнопки, поля ввода, выпадающие списки), включая саму форму, имеют атрибут "name". Он используется при отправке формы на сервер.
  • getAttribute - позволяет узнать значения заданного атрибута HTML-тэга.
  • removeAttribute - удаляет заданный атрибут. В результате этого атрибут будет иметь значение по умолчанию, либо наследовать атрибуты родительского HTML-тэга. Также позволяет удалить встроенную таблицу стилей (параметр "style").

Для управления атрибутами вы можете использовать методы setAttribute, getAttribute, removeAttribute. Но есть и более удобный способ для этого. В интерфейсном модуле MSHTML.pas есть огромное множество объявлений. И среди этих объявлений нашлось место для описания всех возможных атрибутов для каждого HTML-тэга. Каждому HTML-тэгу соответствует свой интерфейс:

  • IHTMLElement, IHTMLElement2, IHTMLElement3 и т.д. - универсальные интерфейсы, общие для всех HTML-элементов;
  • IHTMLLinkElement, IHTMLLinkElement2, IHTMLLinkElement3 - интерфейсы для управления тэгом гиперссылки <a>;
  • IHTMLFormElement, IHTMLFormElement2, IHTMLFormElement3 - интерфейсы для управления тэгом формы <form>;
  • IHTMLImgElement, IHTMLImgElement2 - интерфейсы для управления тэгом изображения <img>;
  • IHTMLBodyElement, IHTMLBodyElement2 - интерфейсы для управления тэгом <body>;
  • IHTMLFontElement - интерфейс для управления тэгом шрифта <font>;
  • IHTMLUListElement - интерфейс для управления тэгом маркированного списка <ul>;
  • IHTMLOListElement - интерфейс для управления тэгом нумерованного списка <ol>;
  • IHTMLLIElement - интерфейс для управления элементом списка <li>;
  • IHTMLDivElement - интерфейс для управления блоковым тэгом <div>;
  • IHTMLHRElement - интерфейс для управления тэгом горизонтальной линии <hr>;
  • IHTMLParaElement - интерфейс для управления тэгом абзаца <p>;
  • IHTMLSelectElement, IHTMLSelectElement2 - интерфейсы для управления тэгом выпадающего списка <select>;
  • IHTMLOptionElement - интерфейс для управления тэгом элемента выпадающего списка <option>;
  • IHTMLInputElement, IHTMLInputElement2 - общие интерфейсы для управления тэгом поля ввода  <input>;
  • IHTMLInputTextElement - интерфейс для управления полем ввода строки текста <input type="text">;
  • IHTMLInputFileElement - интерфейс для управления полем выбора файла <input type="file"> (используется для отправки файла на сервер);
  • IHTMLInputButtonElement - интерфейс для управления простой кнопкой <input type="button">;
  • IHTMLInputHiddenElement - интерфейс для управления скрытым элементом <input type="hidden"> (используется для хранения текущего состояния HTML-страницы при огранизации обмена данными с сервером);
  • IHTMLTextAreaElement - интерфейс для управления полем ввода многострочного текста <textarea>;
  • IHTMLMarqueeElement - интерфейс для управления тэгом бегущей строки <marquee>;
  • IHTMLHtmlElement - интерфейс для управления корневым тэгом <html>;
  • IHTMLHeadElement - интерфейс для управления тэгом заголовочной информации <head>;
  • IHTMLTitleElement - интерфейс для управления тэгом заголовка страницы <title>;
  • IHTMLMetaElement, IHTMLMetaElement2 - интерфейсы для управления тэгом мета-информации страницы <meta>. В тэге meta хранится кодировка страницы, иногда кукисы и другая информация. Можно прописывать любую информацию в формате Параметр=Значение. Браузер может использовать эту информацию по своему усмотрению. HTML-редакторы часто прописывают в этом тэге информацию о фирме, разработчике и версии программы;
  • IHTMLTable, IHTMLTable2, IHTMLTable3 - интерфейсы для управления тэгом таблицы <table>;
  • IHTMLTableRow, IHTMLTableRow2, IHTMLTableRow3 - интерфейсы для управления тэгом строки таблицы <tr>;
  • IHTMLTableCell, IHTMLTableCell2 - интерфейсы для управления тэгом ячейки строки таблицы <td> или <th>;

Это далеко не полный перечень. Для использования возможностей этих интерфейсов можно выполнить приведение типов, например IHTMLElement as IHTMLTable. Еще раз подчеркнем, что можно не использовать все эти интерфейсы, а обойтись одним лишь IHTMLElement и его методами setAttribute, getAttribute, removeAttribute. Кому как удобнее.

Работа со встроенной таблицей стилей CSS

Для доступа к встроенной таблице стилей интерфейс IHTMLElement предоставляет свойство "style", которое имеет тип IHTMLStyle. Допустим, мы имеем переменную Elem: IHTMLElement., инициализированную следующим образом:
  Elem := DocRange.parentElement.parentElement;
Тогда работа со встроенной таблицей стилей будет осуществляется следующим образом:

  • Elem.style.verticalAlign := 'top' - будет задано выравнивание по вертикали;
  • Elem.style.textAlign := 'left' - будет задано горизонтальное выравнивание;
  • (Elem.style as IHTMLStyle2).borderCollapse := 'collapse' - рамки смежных ячеек таблицы будут в виде одной линии;
  • (Elem.style as IHTMLStyle3).zoom:= '2' - масштаб элемента будет увеличен вдвое. Если элементом является тэг <body>, то будет масштабироваться вся HTML-страница.

Следует более подробно остановиться на составных стилях border, borderColor, borderWidth, borderStyle, borderTop, borderRight, borderBottom, borderLeft. Эти стили использовать программным путем не рекомендуется. Во-первых, они являются составными, и поэтому их сложно обработать. Во-вторых, мы можем получить пустую строку, хотя стили заданы. Лучше работать с отдельными компонентами этих стилей: borderTopColor, borderTopWidth, borderTopStyle и т.д. Это проще и надежнее.

Кстати работать со стилями, которые возвращают цвет, не так-то просто. Например красный цвет может быть задан как минимум тремя способами: 'red', '#FF0000', 'rgb(255,0,0)', так что учитывайте это многообразие.

Обработка событий

При написании HTML-редактора необходимо организовать обработку событии от области работы с текстом. Как минимум следует отлавливать щелчки мышкой и нажатия клавиш клавиатуры. При этом можно обновлять состояние различных элементов на панели инструментов. Вряд ли кто-то без посторонней помощи разберется как это сделать. В данном лучше всего дать готовый кусок кода без лишних объяснений:

Объявляем класс TEventMethod:

  TEventMethod = class(TInterfacedObject, IDispatch)
  public
    function GetTypeInfoCount(out Count: Integer): HResult; stdcall;
    function GetTypeInfo(Index, LocaleID: Integer; out TypeInfo): HResult; stdcall;
    function GetIDsOfNames(const IID: TGUID; Names: Pointer;
      NameCount, LocaleID: Integer; DispIDs: Pointer): HResult; stdcall;
    function Invoke(DispID: Integer; const IID: TGUID; LocaleID: Integer;
      Flags: Word; var Params; VarResult, ExcepInfo, ArgErr: Pointer): HResult; stdcall;
  end;

Жмем Ctrl+Shift+C, в результате чего Delphi автоматически создаст соответствующие методы. В методах GetTypeInfoCount, GetTypeInfo, GetIDsOfNames пишем Result := 0. А в методе Invoke пишем код, которых будет вызываться при возникновении соответствующих событий. В классе TEventMethod больше нельзя объявлять никаких полей, иначе механизм перестанет работать.

Объявляем переменную EvtMethod: IDispatch;

Создаем ее с помощью кода: EvtMethod := TEventMethod.Create;

В событии OnDocumentComplete можно прописать обработчики:

Editor.onmousedown := EvtMethod;
Editor.onmouseup := EvtMethod;
Editor.onkeydown := EvtMethod;
и т.д.

Вот и все дела! :) Идея взята из Королевства Делфи: http://www.delphikingdom.com/asp/articles_forum.asp?ArticleID=1319.

Работа с таблицами

Самое сложное при написании HTML-редактора - работа с таблицами. Начнем с самого простого. Пусть есть переменная Tab: IHTMLTable, и она каким-то образом у нас уже инициализирована. Тогда мы может получить количество строк таблицы: Tab.rows.length. А вот так будет выглядеть цикл перебора строк таблицы:
    for I := 0 to Tab.rows.length - 1 do
      Row := Tab.rows.item(I, 0) as IHTMLTableRow;

Так выглядет цикл перебора ячеек строки таблицы:
    for J := 0 to Row.cells.length - 1 do
      Cell := Row.cells.item(J, 0) as IHTMLTableCell;
Далее некоторые полезные приемы, свойства и методы:

  • Row.rowIndex - индекс строки Row. Индексация начинается с нуля;
  • Cell.cellIndex - индекс ячейки Cell. Индексация начинается с нуля;
  • Cell.colSpan- количество ячеек, которые охватывает ячейка Cell по горизонтали;
  • Cell.rowSpan - количество строк, которые охватывает ячейка Cell по вертикали;
  • Row := Tab.insertRow(Index) as IHTMLTableRow - вставляет новую строку таблицы в указанную позицию. Добавленая строка не содержит ни одной ячейки.
  • Cell := Row.insertCell(Index) as IHTMLTableCell; - вставляет новую ячейку в указанную позицию строки Row.
  • Row.deleteCell(Cell.cellIndex); - удаляет ячейку Cell из строки Row;
  • Tab.deleteRow(Row.rowIndex); - удаляет строку Row из таблицы Tab.

В принципе на основе этого строится вся обработка таблицы. Вроде бы ничего сложного. Однако проблемы возникают. И судя по тому, что практически не один бесплатный HTML-редактор не осиляет сложные таблицы, далеко не все разработчики с этими проблемами справляются.

Для работы с таблицами нужно реализовать, по крайней мере, 9 операций: добавление столбца слева, добавление столбца справа, вставка строки сверху, вставка строки снизу, удаление столбца, удаление строки, объединение с ячейкой справа, объединение с ячейкой снизу, разбивка объединенной ячейки. Пусть это упрощенный набор, но его достаточно для любых манипуляций с таблицей. Посмотрите на типичный вид таблицы:
<TABLE>
<TR>   <TD>1</TD>   <TD>2</TD>   <TD>3</TD>   </TR>
<TR>   <TD>4</TD>   <TD>5</TD>   <TD>6</TD>   </TR>
<TR>   <TD>7</TD>   <TD>8</TD>   <TD>9</TD>   </TR>
</TABLE>
так она выглядит визуально:
1 2 3
4 5 6
7 8 9

Вроде все очевидно. Однако как только появляются объединенные ячейки, так сразу начинаются проблемы. Посмотрите на следующую таблицу:

1245 3
6
7 8 9

Вот ее код:
<TABLE>
<TR>   <TD rowSpan=2 colSpan=2>1245</TD>   <TD>3</TD>   </TR>
<TR>   <TD>6</TD>   </TR>
<TR>   <TD>7</TD>   <TD>8</TD>   <TD>9</TD>   </TR>
</TABLE>

И как с ней работать? Уже не понятно. И это далеко не самая сложная таблица. Посмотрите на 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. Делается это просто:
procedure SaveHTMLToFile(AFile: WideString);
var
  oPF: ActiveX.IPersistFile;
begin
  oPF := (wb.Document as IPersistFile);
  oPF.Save(PWideChar(AFile), True);
end;

Проблема с потерей фокуса

Internet Explorer в режиме редактирования постоянно теряет фокус. Стоит переключиться на другое окно, и все, мигающий курсор в редакторе исчезает! Даже если мы вернет фокус окну редактора, курсор все равно не замигает. Замое замечательное - стандартные команды передачи фокуса (SetFocus, ActiveControl и т.п.) не работают! Решение проблемы очень простое. Следует при необходимости вызывать код:
  (HTMLInfo.Editor.body as IHTMLElement2).focus;
Одно из мест, откуда этот код можно вызывать - это событие WM_ACTIVATE, например:
procedure TEditorForm.WMActivate(var Msg: TWMActivate);
begin
  inherited;

  if Assigned(Editor) then
    if not Msg.Minimized then
      (Editor.body as IHTMLElement2).focus;
end;

Помимо потери фокуса, есть еще масса проблем с курсором. Что делать, если для выбранного элемента открыли окно свойств? Курсор при этом исчезает, и врядли его можно заставить мигать стандартными средствами. В представленном 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 (в самом его начале) добавьте код:
fWBContainer := TWBContainer.Create(wb);
fWBContainer.UseCustomCtxMenu := True;

Разместите на форме компонент TPopupMenu и укажите его в свойстве PopupMenu компонента WebBrowser. Можете запустить приложение и убедиться в том, что механизм работает. За это отвечает следующий код из модуля UContainer.pas:
function TWBContainer.ShowContextMenu(
  const dwID: DWORD;
  const ppt: PPOINT;
  const pcmdtReserved: IInterface;
  const pdispReserved: IDispatch): HResult;
begin
  if fUseCustomCtxMenu then
  begin
    // tell IE we're handling the context menu
    Result := S_OK;
    if Assigned(HostedBrowser.PopupMenu) then
      // browser has a pop up menu so activate it
      HostedBrowser.PopupMenu.Popup(ppt.X, ppt.Y);
  end
  else
    // tell IE to use default action: display own menu
    Result := S_FALSE;
end;

Здесь реализована функция, которую WebBrowser автоматически вызывает при необходимости отобразить контекстное меню.

В принципе, то же самое вы можете найти в ДЕМО-проекте, сопровождающем указанную статью.

Перехватываем нажатие клавиш

Для этого добавляем в класс TWBContainer следующий метод:
    function TranslateAccelerator(const lpMsg: PMSG; const pguidCmdGroup: PGUID;
      const nCmdID: DWORD): HResult; stdcall;

Вот пример его реализации:
function TWBContainer.TranslateAccelerator(const lpMsg: PMSG;
  const pguidCmdGroup: PGUID; const nCmdID: DWORD): HResult;
begin
 Result := S_FALSE;
 if GetKeyState(VK_CONTROL) < 0 then
 begin
   if (lpMsg.wParam = Ord('O')) or (lpMsg.wParam = Ord('L')) or
      (lpMsg.wParam = Ord('S')) or (lpMsg.wParam = Ord('N')) then
   begin
       Result := S_OK;

        if Assigned(FOnPageOpen) then
         if lpMsg.wParam = Ord('O') then
           DoPageOpen();

       if Assigned(FOnPageSave) then
         if lpMsg.wParam = Ord('S') then
           DoPageSave();

       if Assigned(FOnPageNew) then
         if lpMsg.wParam = Ord('N') then
           DoPageNew();
   end;
 end;
end;

Здесь DoPageOpen, DoPageSave, DoPageNew - некие действия, которые можно выполнить при открытии (Ctrl+O) / сохранении (Ctrl+S) / создании  (Ctrl+N) страницы. Результат S_OK говорит о том, что мы сами обрабатываем данное сочетание клавиш, и поэтому WebBrowser на них никак не реагирует.

В принципе, данный метод можно использовать при обработке событий от клавиатуры вместо TEventMethod.

На этом пока все! Статья со временем будет дорабатываться!

1ПТК АЗС - система автоматизации АЗС, АГЗС, МТАЗК от компании ООО "АВТОМАТИКА плюс" www.automatikaplus.ru

Логинов Дмитрий © 2005-2015