29 de março de 2010

Design Patterns com Delphi : Comando - parte II

Uma vez tendo modelado as relações entre as classes envolvidas, implementar uma solução com o Design Pattern Comando (introduzido no post anterior) é tarefa relativamente simples. Acaba se tornando uma questão de prestar atenção em que classe deve instanciar outras, quais tem relação de herança e quais terão internamente instâncias de outros.
Para falar sobre a implementação em si, vou usar o exemplo do diagrama abaixo, também apresentado no último post:
Diagrama UML para o padrão Command
Neste exemplo, as classes herdadas de TWImportableObj são capazes de carregar para dentro do sistema os dados gravados por terceiros em tabelas específicas para esse fim, aplicando-lhes as regras de negócio pertinentes para que se tornem dados válidos no sistema. Essas operações podem ser comandadas a partir de pontos distintos do sistema - através de uma solicitação do usuário ou como processos agendados, por exemplo.

Para implementar tais classes é o bastante ter uma relação de herança entre elas. Assim, a classe base TWImportableObj estabelece o comportamento padrão para cargas através da definição de métodos e funções, de modo que poderá ser usada mais adiante como repositório para quaisquer de suas heranças. É recomendável, então, que a criação de instâncias dessas classes seja feita por um pattern como o Factory Method, garantindo a flexibilização e robustez na criação.
TWImportableObj = class
protected
_DtDe, _DtAte : TDateTime;
public
procedure ConfigImportacao(ADtDe, ADtAte : TDateTime);
procedure ImportaRegistros;virtual;abstract;
end;

TWApontamentos = class(TWImportableObj)
public
procedure ImportaRegistros;override;
end;

TWClientes = class(TWImportableObj)
public
procedure ImportaRegistros;override;
end;

{ Factory Method }
TWTipoImport = (tiApontamentos, tiClientes);
function GetImportable (ATipo: TWTipoImport;
ADtDe, ADtAte: TDateTime): TWImportableObj;

implementation

function GetImportable (ATipo: TWTipoImport;
ADtDe, ADtAte: TDateTime): TWImportableObj;
begin
case ATipo of
tiApontamentos: Result := TWApontamentos.Create;
tiClientes: Result := TWClientes.Create;
end;

Result.ConfigImportacao (ADtDe, ADtAte);
end;

procedure TWApontamentos.ImportaRegistros;
begin
{ Ações específicas para carregar apontamentos de produção }
end;

procedure TWClientes.ImportaRegistros;
begin
{ Ações específicas para carregar dados de clientes }
end;

A seguir, a teoria diz que temos que ter uma interface para definir o comportamento esperado para os tipos de comandos. No entanto, como há uma série de considerações a levar em conta quando usando interfaces no Delphi, prefiro implementar essa parte usando uma classe abstrata já que assim o efeito desejado é obtido. Isto é, teremos funções e métodos que devem ser obrigatoriamente implementados nas heranças:
TWComandoBase = class
public
procedure Execute;virtual;abstract;
procedure Undo;virtual;abstract;
function IsUndoable : boolean;virtual;abstract;
end;

TWComandoCarga = class(TWComandoBase)
protected
_Obj : TWImportableObj;
public
Constructor Create (AObj: TWImportableObj);
procedure Execute;override;
procedure Undo;override;
function IsUndoable : boolean;override;
end;

implementation

Constructor TWComandoCarga.Create (AObj: TWImportableObj);
begin
{ Configura o "Receiver" para esse comando: }
_Obj := AObj;
end;

procedure TWComandoCarga.Execute;
begin
{ Repassa a ação para o "Receiver" : }
_Obj.ImportaRegistros;
end;

procedure TWComandoCarga.Undo;
begin
{ Neste exemplo, o Undo não está implementado. Para implementá-lo, o Receiver teria que salvar informações suficientes para permitir que as operações realizadas sejam desfeitas }
end;

function TWComandoCarga.IsUndoable : boolean;
begin
Result := False;
end;
Aqui vale chamar a atenção para o fato da classe TWComandoCarga possuir o membro _Obj do tipo TWImportableObj. Conforme foi exposto no post anterior sobre o pattern Comando, o papel de um ConcreteCommand - como o TWComandoCarga - é fornecer uma ligação entre o Comando e o Receiver (no caso, qualquer herança do TWImportableObj). O membro _Obj é quem estabele a ligação nesse exemplo, sendo que a requisição para execução é repassada para ele através do TWComandoCarga. É importante ressaltar também que essa característica faz com que o TWComandoCarga em si sirva apenas para executar comandos de carga.

A próxima etapa é criar o Invoker, que nada mais é do que uma lista com os comandos que devem ser executados em sequência - embora para o exemplo fosse suficiente uma única ocorrência de Comando.
TWComandos = class
protected
_Lista: TList;
public
procedure Add (ACmd: TWComandoBase);
procedure Clear;
procedure ExecuteAll;
end;

{ ... }

procedure TWComandos.ExecuteAll;
var i : integer;
lCmd : TWComandoBase;
begin
for i := 0 to _Lista.Count - 1 do
begin
lCmd := (TWComandoBase)Lista.Item[i];
lCmd.Execute;
end;
end;
Obviamente, o código acima está simplificado para facilitar a compreensão da teoria por trás do pattern. O correto seria embutir nele tratamento dos erros que poderão acontecer durante uma execução qualquer. Também não estão representados o construtor nem o destrutor da classe.

Para finalizar, a classe Client junta todas as peças e as põe para funcionar. O que vai abaixo seria a resposta a um evento produzido pelo usuário (clique num botão ou seleção de um item de menu) requisitando que uma carga de Apontamentos de produção seja executada:
var lCarga: TWImportableObj;
lCmd: TWComandoBase;
begin
lCarga := GetImportable (tiApontamentos, _DtDe, _DtAte);
lCmd := TWComandoCarga.Create (lCarga);

_Comandos.Add (lCmd);
_Comandos.ExecuteAll;
{ ... }
end;
Neste exemplo, o comando criado é local na função mas isto não é uma obrigação. Pelo contrário. O ideal é criar o comando globalmente numa classe e adicioná-lo localmente ao Invoker (_Comandos) associado ao cenário. Assim, o mesmo comando pode ser utilizado para executar a carga a partir de um menu, de um clique de botão, agendado, num executor de macros ou qualquer outra opção que se queira dar ao usuário. Lembre-se apenas que o Receiver (o TWImportableObj) é externo ao comando e deve haver meios de se passar a ele as configurações de execução desejadas pelo usuário.

24 de março de 2010

Design Patterns com Delphi : Comando - parte I

É bastante comum no desenvolvimento de software a situação em que um mesmo tipo de comando pode ser ativado por diferentes meios. Por exemplo, no ERP Omega da ABC71 um mesmo processo ou relatório pode ser executado diretamente pelo usuário ou essa execução pode ser agendada para executar mais tarde, num horário mais apropriado.

Esse tipo de situação é abordada pelo Design Pattern comportamental Comando. Nesse Pattern, uma requisição é encapsulada como uma objeto, facilitando a configuração de chamadas a essa requisição em diversas circunstâncias. Tendo comandos como um objeto, é possível criar filas de execução de comandos (como num Wizard), implementar o recurso de "desfazer" em um programa (percorrendo a lista de comandos executados e chamando um método "undo" que deve existir em cada objeto) ou até mesmo permitir a gravação de macros em seu programa (na prática, uma lista de comandos criada e mantida diretamente pelo usuário).

No ERP da ABC71, temos um conceito de "tabelas integradoras" onde softwares de terceiros podem armazenar informações que devem ser importadas por nós. O processo de importação lê os registros gravados nessas tabelas e aplica a eles nossas regras de negócio de modo que os dados são corretamente incorporados ao ERP. Cada tipo de importação pode ser encarado como um comando separado e o usuário pode escolher numa tela quais os comandos e respectivos parâmetros. Os comandos poderiam também ser acessados individualmente, fora da tela. O diagrama abaixo mostra uma visão simplificada da solução para esse cenário usando o padrão Command:
Diagrama UML para o padrão Command
De acordo com o objetivo pretendido, pode haver pequenas variações nas relações entre as classes mas o que está representado no diagrama é uma boa base para o Command. No quadro abaixo aparece a nomenclatura usual para as classes participantes da solução:
Comand é uma interface que estabelece as operações que estarão disponíveis num comando. No exemplo acima, a interface TWComandoBase detem esse papel ao introduzir as operações Execute e Undo. Prevendo que nem todo comando poderá ser desfeito, há também a função IsUndoable, permitindo que cada comando estabeleça se poderá ou não ser desfeito.

O Receiver é qualquer classe que tenha o conhecimento de como executar a ação associada a um comando. Trata-se, portanto, do objeto que executa de fato a ação requisitada. No diagrama, esse é o papel da classe TWImportableObj e suas heranças.

Um ConcreteCommand provê a ligação entre o objeto Receiver e a ação esperada para o comando. Isto implica que, ao criarmos a instância do ConcreteCommand, devemos vincular um Receiver a ela, promovendo o desacoplamento entre este Receiver e quem irá invocar o comando (o Invoker). Quem invoca o comando apenas espera que ele seja executado, sem se preocupar com os detalhes da implementação; quando se requisita ao comando que execute a ação, o ConcreteCommand redireciona a requisição de forma transparente para o Receiver. No diagrama acima, aparecem duas classes que agem como ConcreteComand : TWComandoCarga e TWComandoExecProc.

A classe Invoker é quem solicita a um comando que execute a ação associada. No nosso exemplo, a classe TWComandos executa esse papel de uma forma uma pouco mais elaborada. Aqui, ele aceita uma lista de comandos - quaisquer objetos que implementem o TWComandoBase - e os executa sequencialmente sem precisar ter conhecimento sobre como cada comando realiza sua operação. Ou seja, o Invoker está desacoplado do Receiver.

A responsabilidade do Client é criar corretamente todos os ConcreteComand necessários e ajustar o Receiver apropriado para cada um. Os objetos assim criados são adicionados à lista de comandos representada pelo Invoker e este é acionado para executar cada comando. No exemplo, o papel de Client é reservado para a classe TWFormImportaRegs, que neste caso é uma tela que oferece ao usuário a oportunidade de selecionar os comandos que ele deseja executar bem como suas respectivas configurações.
Note que é possível criar instâncias isoladas das heranças de TWImportableObj e requisitar a importação separadamente a partir delas. A vantagem de fazer isso usando o Invoker é que este flexibiliza o uso dos comandos, possibilitando, por exemplo, a execução de importações (e quaisquer outros comandos que implementem a interface TWComandoBase) em agendamentos, em qualquer ordem.

Volto no próximo post para mostrar um mapeamento das classes envolvidas usando o Delphi.

19 de março de 2010

Criando um editor de HTML com o TWebBrowser - parte II

Escrevi um post há alguns dias onde mostrei o básico sobre as técnicas para construir um editor de documentos HTML usando o componente TWebBrowser do Delphi. Perguntaram, então, por que é que alguém construiria um editor próprio se já há editores bons disponíveis - como o Word ou o BrOffice ? A principal razão é que construindo o seu próprio editor você tem a liberdade de estabelecer os padrões que serão usados. Assim, você pode forçar que apenas um determinado tipo de fonte ou uma determinada cor seja usada, pode impedir que certos itens sejam adicionados (imagens) e assim por diante. Além disso, seu programa pode armazenar os documentos diretamente no seu banco de dados, evitando que a documentação fique espalhada e se perca.

Dando continuidade àquele post, mostro aqui outras formas de interação que você pode disponibilizar ao usuário do editor, permitindo que se crie documentos mais ricos.

É possível, por exemplo, inserir tags HTML complexas usando apenas texto. Isso dá a flexibilidade de tratar em um único comando quaisquer atributos que a tag suportar, bastando que ela seja montada como um texto comum (ao invés de usar estrutura do DOM). Assim, do ponto de vista do usuário final do Editor, somente um clique de botão é suficiente para incluir a tag complexa no documento; e ele não precisa ter qualquer conhecimento de HTML para realizar a tarefa.

Usei o código abaixo como reposta ao clique de um botão que insere uma linha horizontal (tag HR) no ponto do editor onde o cursor está localizado:
procedure TForm1.tbLinhaHrClick(Sender: TObject);
var lComando : String;
vo : OleVariant;
begin
lComando := '<hr style="width:50%;color:silver;height:1px;" />';

if (DOMInterface.Selection.Type_ = 'Control') then
FBrowser.ExecWB (OLECMDID_DELETE, OLECMDEXECOPT_DODEFAULT);

vo := DOMInterface.Selection.CreateRange;
vo.pasteHTML(lComando);
end;
Algumas das tags HTML são tratadas de forma especial pelo browser pois permitem interações mais elaboradas com o usuário - imagens e a linha horizontal, por exemplo, podem ser redimensionadas dentro do editor. Para obter esse efeito, a representação dessas tags é feita por controles visuais inseridos no editor. Caso um desses objetos visuais esteja selecionado, o método CreateRange da seleção retorna uma interface ControlRange ao invés de TextRange. Acontece que a interface ControlRange não possui o método pasteHTML de que necessitamos e, por isso, se a seleção atual for do tipo 'Control', a seleção (o controle visual) deve ser removido manualmente antes de se poder inserir o novo HTML no lugar. É isso que faz o "if" e o ExecWB existentes no código anterior.

Ainda no código anterior, o método pasteHTML é quem insere no documento o trecho HTML preformatado com a linha horizontal. Veja que montei a tag HR usando estilos fixos cujos valores diferem dos valores padrões da tag, forçando o uso de um padrão próprio a ser seguido por todos os documentos mantidos com esse editor.

Há também um mecanismo que dá acesso aos mesmos comandos disponíveis nos menus do Internet Explorer. Para atingir esse objetivo, o TWebBrowser implementa uma outra interface - esta de uso genérico - que está definida na unit ActiveX e chama-se IOleCommandTarget. Essa interface introduz a capacidade de um objeto OLE (como o WebBrowser) receber comandos da aplicação onde ele está contido, tais como "Imprimir", "Copiar", "Salvar", etc. Para submeter os comandos, a aplicação deve usar o método Exec dessa interface.

O trecho de código abaixo usa esse mecanismo para exibir o preview da impressão do documento que estiver sendo mostrado no TWebBrowser. Para ver a lista de comandos aceitos, acesse este link do MSDN.
procedure TForm1.tbImprimirClick(Sender: TObject);
var lCommandTarget : IOleCommandTarget;
pVarIn, pVarOut : OleVariant;
begin
lCommandTarget := FBrowser.Document As IOleCommandTarget;
lCommandTarget.Exec(Nil,
OLECMDID_PRINTPREVIEW,
OLECMDEXECOPT_DODEFAULT,
pVarIn, pVarOut);
end;
A constante OLECMDID_PRINTPREVIEW identifica o comando desejado, que neste caso é o preview de impressão. Já o parâmetro seguinte se refere a se o comando irá interagir com o usuário se isso fizer sentido. No exemplo, informei a constante OLECMDEXECOPT_DODEFAULT, o que significa que o WebBrowser deve usar o comportamento padrão. No caso da função de preview, o padrão é pedir a intervenção do usuário.

16 de março de 2010

Projeto Mono leva .NET para o Android

Desenvolvedores de software que queiram criar programas para o sistema operacional Android do Google vão ganhar uma opção extra de plataforma de desenvolvimento. Atualmente, o SDK do Android - que roda para equipamentos móveis, como celulares - é voltado para programadores Java. No entanto, o projeto Mono da Novell pretende lançar ainda em 2010 uma versão de seu runtime capaz de executar no sistema do Google.

Esse lançamento foi notícia hoje no site da Infoworld.com, junto com o anúncio de outras ferramentas envolvendo o projeto Mono. O texto abaixo é uma tradução do post original, que pode ser encontrado aqui.
MonoDroid, que permitirá que celulares com o sistema Android do Google executem aplicações baseadas na plataforma .Net, está em desenvolvimento na Novell, com uma versão de preview planejada para ser liberada em Agosto, disse na segunda-feira o chefe do Projeto.

O projeto para esse runtime project é parte do esforço do projeto Mono da Novell, que tem sido responsável por colocar as tecnologias do .NET da Microsoft em outros sistemas operacionais, como o Linux.

"[O MonoDroid é] o Mono executando no Android, mas é também o conjunto de APIs de forma que é possível conversar com as APIs do Android," disse Miguel de Icaza, vice presidente da plataforma para desenvolvedores na Novell e líder do projeto Mono, em uma entrevista na conferência Mix10 em Las Vegas.

"Há demanda para execção de aplicações .Net no Android", completa De Icaza.

Os desenvolvedores do projeto Mono também estão concluindo uma versão do Silverlight para o IPhone da Apple, com um preview marcado para agosto. O projeto, chamado Moonlight e baseado no próprio Mono, anteriormente já havia colocado o Silverlight no Linux, Macintosh e Unix. De Icaza disse que não sabia se o produto continuaria com o nome de Moonlight.

Também está prestes a ser liberado para os desenvolvedores Mono a ferramenta Mono Tools for Visual Studio 2.0, que se integra ao IDE do Visual Studio para permitir o desenvolvimento de aplicações compatíveis com o runtime do Mono. Há planos dessa pltafaforma eventualmente suportar o desenvolvimento de aplicações para o Android e o iPhone via tecnologias Mono.

Um desenvolvedor presente na conferência à conferência endossou o Mono e declarou bemvinda a adição da capacidade de executar no Android.

"O Mono é incrível. É um ambiente surpreendentemente flexível", disse o desenvolvedor Ethan Nagel, presidente da Nagel Technologies.

"É mais rápido do que reescrever seu código toda vez, e o que eu mais gosto nisso é que você não o compromete," disse Nagel.

Curiosamente, também hoje a Microsoft confirmou que seu sistema operacional para equipamentos móveis, o Windows Phone 7, não executará código nativo. A notícia, também publicada pela Infoworld.com, revela que a Microsoft optou por pemitir a execução apenas de programas construídos com o Silverlight ou que usem o XNA, que é a plataforma de jogos da empresa. O texto completo dessa notícia pode ser acessado nesse link.


12 de março de 2010

Criando um editor de HTML com o TWebBrowser - parte I

Como eu disse em outro post, o componente TWebBrowser existente no Delphi e no C++ Builder pode ser usado para exibir páginas HTML num programa seu mas também tem recursos para se transformar num editor de HTML no estilo WYSWYG. Isto significa que o usuário final de seu programa pode editar visualmente um conteúdo que esteja disponível em formato HTML mesmo sem conhecer essa linguagem. Ou seja, durante a edição do texto, o usuário enxergará o documento com a mesma aparência que quando ele é exibido pelo browser.

Colocar o componente TWebBrowser em modo de edição é relativamente fácil, bastando modificar o valor de uma propriedade. No entanto, o fonte que é distribuido junto com o IDE não trás todos os tipos de dados, de modo que algumas funcionalidades ficam escondidas. Para podermos usar esses tipos, teremos que importar a Type Library que os contêm, chamada Microsoft HTML Object Library. Há um post aqui explicando como realizar a importação de uma type library no Delphi.

Agora que temos todos os tipos necessários, podemos usar a propriedade Document do WebBrowser para iniciar o modo de edição. O Document implementa um série de interfaces para trabalhar com o editor e o conteúdo HTML propriamente dito. A interface que mais utilizaremos é a IHTMLDocument2, que representa a estrutura DOM do documento HMTL, permitindo que se obtenha informações sobre os elementos HTML e os textos contidos no documento.

Uma das propriedades dessa interface é a designMode, cuja função é determinar se o documento em exibição pode ou não ser editado. Então, para iniciar o modo de edição basta atribuir o valor 'on' para a propriedade:
Uses MSHTML_TLB;

procedure TForm1.Editar;
var lDoc : IHtmlDocument2;
begin
FBrowser.Navigate('about:blank');
lDoc := FBrowser.Document as IHtmlDocument2;
lDoc.designMode := 'on';
end;
No código acima, a unit MSHTML_TLB na cláusula Uses disponibiliza para nosso fonte a type libray importada no começo. Antes de ativar o modo de edição, é obrigatório que haja um documento ativo no WebBrowser. Por isso, uma página HTML em branco é carregada através da navegação para o endereço 'about:blank'. Por fim, é feito um cast com o Document para obter acesso às propriedades da interface IHtmlDocument2 e poder iniciar o modo de edição.

Ao executar esse código, já temos um editor HTML básico que usa valores padronizados para fonte, formatações e cores. Se quisermos alterar esse valores, podemos carregar uma página HTML mais robusta, com alguma formatação pré criada. Veja os outros posts sobre o WebBrowser para ver como fazer essa carga.

Algumas das propriedades podem ser modificadas diretamente na interface IHtmlDocument2, como por exemplo a cor de fundo da página :
{ ... }
lDoc.designMode := 'on';
lDoc.bgColor := 'yellow';
end;
Veja que a cor informada deve ser compatível com o HTML. Isso implica que se um TColor no Delphi for atribuído, a cor será interpretada incorretamente.

Com relação aos comandos aceitos por esse editor, é seguido o padrão do Windows. Por exemplo, use CTRL-C e CTRL-V para operações copiar/colar, CTRL-Z para desfazer a última operação, etc.

Para deixar esse editor um pouco mais flexível, é interessante oferecer ao usuário meios intuitivos de se adicionar formatações HTML, tais como negrito, cores, tipos de fonte, etc. Ou ainda, permitir a criação de listas, tabelas e hiperlinks, adição de imagens, vídeos e outros objetos. Apesar do Document representar a estrutura do HTML, adicionar uma tag é um pouco mais complexo do que seria se tentássemos simplesmente acrescentar um novo nó à estrutura DOM - como num XML. Isso se dá porque é permitido aplicar formatações que abranjam mais de uma tag. Um bom exemplo é aplicar negrito a uma seleção que envolva mais de um parágrafo ou linhas numa tabela.

A chave para implementar esse tipo de recurso é a propriedade Selection do IHtmlDocument2. De acordo com a documentação no MSDN, o Selection representa um texto selecionado, o ponto atual para inserção de novo conteúdo no documento ou ainda um elemento no HTML onde se pode realizar alguma ação específica. O Selection possui um método chamado createRange que é capaz de recuperar a seleção atual de uma forma que permite aplicar corretamente qualquer formatação, mesmo que o texto selecionado englobe mais de uma tag. Veja como funciona para negrito:
procedure TForm1.btBoldClick(Sender: TObject);
var lSel: IHTMLSelectionObject;
lRange : OleVariant;
lDoc : IHtmlDocument2;
begin
lDoc := FBrowser.Document as IHtmlDocument2;
lSel := lDoc.selection;
lRange := lSel.createRange;
lRange.execCommand('BOLD', False, True);
end;
O objeto criado com createRange é do tipo TextRange (se a seleção contiver apenas texto) ou controlRange (se contiver múltiplos elementos selecionados). Em ambos os casos, há um método que permite aplicar comandos na seleção - o execCommand. Mesmo podendo estar em tipos diferentes, o método possui os mesmos parâmetros; isso nos permite usar um OleVariant para executar o método - o Delphi determinará em tempo de execução qual o método certo a ser chamado, usando uma técnica denominada late binding.

O efeito desse comando no texto selecionado é envolvê-lo internamente com a tag STRONG, resultando na exibição do texto em negrito. Caso o texto já esteja em negrito, o comando removerá essa formatação, voltando a exibir sem negrito. A lista de comandos aceitos pelo execCommand pode ser encontrada neste link do MSDN.

Volto em outro post com outros exemplos de facilitadores para a edição.

5 de março de 2010

Usando "streams" para navegação no TWebBrowser

Postei em Fevereiro a respeito do componente TWebBrowser, que é um encapsulamento do navegador Internet Explorer da Microsoft. Naquele post, eu apresentei a função básica do componente (que permite navegar para uma determinada página HTML), além dos eventos mais comuns.

Aquela solução mostrada lá, no entanto, exige que se tenha uma página HTML externa ao programa, seja esta página existente em disco ou disponível através da Internet. Isso pode ser um inconveniente, já que temos que garantir que o arquivo HTML esteja distribuído junto com a aplicação.

Então, fiz uma pesquisa para tentar descobrir se havia jeito de se inserir o texto HTML diretamente no componente, sem ter que passar pelo arquivo externo. Nessa pesquisa eu encontrei no site Delphi Dabbler um artigo descrevendo como fazer isso através de streams. O artigo na verdade é um pouco mais complexo do que eu precisava, de modo que extraí dele apenas a parte relativa a como passar um stream para o WebBrowser e como obter de volta o stream em uso. Com isso, posso ler o stream com o HTML para exibição a partir de um banco de dados ou usar o componente que permite embutir um arquivo diretamente na aplicação.

O Web Browser implementa uma interface COM chamada IPersistStreamInit cuja principal função é interagir com streams, disponibilizando métodos para carregar e salvar conteúdos. Teremos que usar o método QueryInterface existente em todo COM para obter acesso às funções dessa interface. A interface, no entanto, só está acessível se por acaso você já tiver navegado para alguma página. Para resolver isso, podemos antes solicitar ao Browser que exiba uma página especial em branco. A URL dessa página é 'about:blank' :
var PersistStreamInit: IPersistStreamInit;
begin
FWebBrowserInt.Navigate('about:blank');

if FWebBrowserInt.Document.QueryInterface(IPersistStreamInit, PersistStreamInit) = S_OK then
{ ... }
A declaração para IPersistStreamInit está na unit ActiveX, sendo portanto obrigatória a inclusão dela na cláusula uses do nosso programa. A propriedade Document (usada para obter a instância da interface) representa o documento HTML ativo e dá acesso a toda a sua estrutura DOM.

Agora que temos como acessar as funções da IPersistStreamInit, temos mais um problema. A carga de um stream exige que esse stream implemente a interface IStream (também disponível na unit ActiveX) mas a classe da VCL TStream e suas descendentes não têm esse recurso. Para contornar essa limitação, há uma classe chamada TStreamAdapter que pega um TStream e implementa para ele o comportamento exigido pela interface IStream. O código para carga do HTML, então, fica assim:
var lStream : TStream;
StreamAdapter: IStream;
begin
{ ... }
lStream := GetHTMLStreamFromDB;

StreamAdapter:= TStreamAdapter.Create(lStream);

PersistStreamInit.Load(StreamAdapter);
{ ... }
A função GetHTMLStreamFromDB é minha e busca um stream no banco de dados com o HTML que deve ser carregado.

Salvar o documento atual de volta para um stream é similar ao que já foi exposto, bastando substituir o Load pelo Save. Como já disse num outro post, o WebBrowser também tem recursos para edição do documento que está em exibição; é nesse contexto que salvar o documento atual faz mais sentido.
lStream := TMemoryStream.Create;
StreamAdapter := TStreamAdapter.Create(lStream);
PersistStreamInit.Save(StreamAdapter, True);
Quando se está editanto um HTML com o WebBrowser, ele mantém internamente um status indicando se o contéudo foi alterado. O parâmetro True passado para a função Save instrui o componente a limpar esse status para refletir a condição de que o conteúdo já foi salvo.

Da forma como está, essa solução ainda apresenta um inconveniente: se o HTML carregado como stream fizer referência a objetos externos (imagens, vídeos, etc.) eles não serão exibidos corretamente, mesmo que distribuídos junto com a aplicação. Volto numa outra oportunidade para falar disso.