Objeto TShellListView

12/12/2003

1

Olá pessoal!

Estu precisando de alguma documentação mais técnica sobre os objetos: TShellListView e TShellTreeView.

Motivo: Descobrir como estes componentes trabalham com ícones do sistema e pastas especiais (como Desktop).

Serve dica, apostila, qualquer informação útil.

Um abraço!


Responder

Posts

12/12/2003

Deus

Usando os Controles do Shell no Delphi

Por Dave Murray <irongut @ vodafone.net>
Tradução: Demian Lessa


A partir do Delphi 6, a Borland oferece controles do shell, incluindo TShellTreeView e TShellListView que imitam a funcionalidade do Windows Explorer mas estão escondidos na página Samples da paleta de componentes, não possuem documentação alguma, e até mesmo seus fontes são difíceis de encontrar; estão em Delphi\Demos\ShellControls. Você está perdoado se pensar que esses controles são uma idéia posterior e que seu uso não é esperado.

Recentemente, eu quis construir meu próprio cliente FTP porque eu não gostava de qualquer um dos gratuitos que já havia testado. Eu pensei ´Eu tenho Indy´ então, quão difícil pode ser? Verifiquei o exemplo do TIdFTP, o componente cliente FTP do Indy, e a parte de rede pareceu fácil o suficiente, então comecei a pensar a respeito da interface. Eu queria algo simples e optei por uma visão do sistema de arquivos local acima e uma visão do sistema de arquivos remoto abaixo com uma barra de ferramentas no meio. Cada visão iria conter um TreeView e um ListView com alguns botões para navegação simplificada, como o Explorer. Queria poder arrastar e soltar entre os controles e também com o Explorer. Nesse ponto, fui procurar componentes para implementar a parte local e encontrei os controles shell da Borland. Decidi que esse tipo de layout é algo que poderia ser reutilizado, então comecei a trabalhar num frame genérico.

Então, como eles funcionam? Alguma funcionalidade é fácil de implementar mas, de outra forma, esses controles podem ser estranhos e confusos. A maioria dos métodos que se esperaria encontrar não existem ou retornam parâmetros que são de valores duvidosos. Frequentemente, são de tipos incorretos para outras chamadas que se deseja realizar. O que deveria ter sido um trabalho de poucas horas e fácil programação transformou-se rapidamente em várias noites de leitura de código, experimentação e esticar de cabelos. Em algum ponto durante o processo, decidi fazer desse trabalho um artido de modo que eu pudesse compartilhar minha dor com vocês. ;)

Vamos começar com a parte fácil. Conectei meu TShellTreeView com um
TShellListView e então iniciei a barra de ferramentas. O primeiro botão que queria era um que levasse ao diretório imediamtamente superior na árvore de diretórios; após alguma busca, verifiquei que o método TShellListView.Back faz exatamente isso. A maioria dos demais botões trouxe alguma complicação, então voltarei a esses mais tarde. Mas um deles foi fácil- o botão de visões para o TShellListView. Apenas criei um menu de popup para o botão que definia o TShellListView.ViewStyles. Nesse ponto, eu tinha um gerenciador de arquivos simples que oferecia características básicas de navegação e menus de contexto padrão do Explorer.

Considerei a possibilidade de acrescentar um TShellComboBox sobre a lista de arquivos. Queria que ele fosse redimensionado com o frame assim como os demais controles mas ele não possui a propriedade Align. Tentei utilizar âncoras mas não consegui o efeito desejado então decidi abandonar a idéia.

Agora vamos à parte complicada. Os controles do shell não oferecem métodos que auxiliem na manipulação dos arquivos, então foi preciso usar a API do Windows. A função SHFileOperation() pode copiar, mover, excluir e renomear, então escrevi o seguinte wrapper para facilitar seu uso.

  function TconExplorerFrame.FileOperation(const source, dest : string;
    op, flags : Integer) : boolean;
  // copia, move, exclui e renomeia arquivos e pastas usando a WinAPI
  var
    Structure : TSHFileOpStruct;
    src, dst : string;
    OpResult : integer;
  begin
    // inicializar a estrutura FileOp
    FillChar(Structure, SizeOf (Structure), #0);
    src := source + 00;
    dst := dest + 00;
    Structure.Wnd := 0;
    Structure.wFunc := op;
    Structure.pFrom := PChar(src);
    Structure.pTo := PChar(dst);
    Structure.fFlags := flags;
    case op of
      // definir o título do diálogo de progresso
      FO_COPY : Structure.lpszProgressTitle := ´Copying...´;
      FO_DELETE : Structure.lpszProgressTitle := ´Deleting...´;
      FO_MOVE : Structure.lpszProgressTitle := ´Moving...´;
      FO_RENAME : Structure.lpszProgressTitle := ´Renaming...´;
      end; // case op of..
    OpResult := 1;
    try
      // realizar a operação
      OpResult := SHFileOperation(Structure);
    finally
      // informar sucesso / falha
      result := (OpResult = 0);
      end; // try..finally..
  end; // function TconExplorerFrame.FileOperation


Essa função retorna true se a operação é bem sucedida e exibe um diálogo de progresso se necessário. Procure por SHFILEOPSTRUCT na ajuda da WinAPI e veja os possíveis valores de op e flags. Como ainda não sei se precisarei utilizar esse função fora do frame, deixarei-a por hora como um método privado- mas isso pode ser mudado no futuro.

Um botão de exclusão agora era simples- tudo a fazer era determinar que arquivo ou pasta está selecionado e exclui-lo utilizando FileOperation().

Enquanto fazia o botão Atualizar, decidi escrever uma função genérica que poderia ser chamada a partir de outros métodos e atualizaria ambos os controles. TShellTreeView.Refresh toma um nó como parâmetro- mas qual nó passar? Tentei passar a pasta atual, mas isso nem sempre funcionou (isso também parece ser um problema com o Explorer). Então tentei passar o nó raiz e isso funcionou corretamente. TShellListView pisca quando atualizamos o TShellTreeView ao qual está conectado, então o desconecto antes. Veja a rotina TconExplorerFrame.Refresh nos fontes.

Na criação de uma nova pasta, precisamos atribuir um nome único. O procedimento usual é chamá-lo de ´Nova Pasta´ e acrescentar um número ao nome se a pasta com esse nome já existir. Escrevi GetNewFolderName() para retornar um nome único; precisei da função DirectoryExists() de
SysUtils.pas e de um loop while. Meu botão de Criar Pasta chama essa função e então usa CreateDir() de SysUtils.pas.

Queria oferecer um botão de propriedades mas os controles do shell não possuem métodos interessantes para isso. Como ao pressionar Alt+Enter num TShellListView funciona, mergulhei em ShellCtrls.pas e verifiquei o fonte. Inicialmente, pareceu simples, tudo que era preciso era uma chamada a DoContextMenuVerb. Ou não, uma vez que DoContextMenuVerb não é um método de TShellListView, mas uma rotina privada em ShellCtrls.pas. Nesse ponto, decidi que o plágio era o melhor caminho e então copiei esse código na unidade do meu frame.

Clicar duplo em arquivos no TShellListView não funciona (no Win2k, pelo menos), mas selecionar Open pelo menu de contexto funciona. Ao verificar o código, o controle possui um método DblClick que chama ShellExecute(). Nesse instante, notei TShellFolder.ExecuteDefault. Já que TShellFolder pode ser um arquivo ou pasta e podemos obetr o item selecionado como um TShellFolder chamando TShellListView.SelectedFolder, escrever um
manipulador para o evento OnDblClick foi simples. Isso também garante que o clicar duplo não apenas tenta abrir o arquivo, mas executa a ação padrão do seu menu de contexto- que é exatamente o que o Explorer faz.
Veja TconExplorerFrame.shlllstvwFilesDblClick.

Nesse ponto, tinha tudo que queria exceto pelo arrastar e soltar. Eu jamais havia implementado esse tipo de funcionalidade antes então tive que ler um pouco antes de começar. O ideal seria alterar o cursor no pressionamento do Ctrl, como o Explorer faz. Isso implica em utilizar um TDragControlObject para fornecer uma lista de imagens de arrasto, então decidi manter as coisas simples por hora e deixar esse efeito de lado.

Comecei arrastando do TShellListView para o TShellTreeView. Os métodos
e propriedades necessários (com os valores de retorno corretos) não parecem existir até que você percebe que a propriedade SelectedFolder pode retornar arquivos assim como pastas. Escrevi um evento OnDragOver para o TShellTreeView de modo que ele aceitass itens do TShellListView e então iniciei com seu evento OnDragDrop. Rapidamente descobri que não poderia acessar o arquivo sendo arrastado nesse evento, então decidi por armazenar esse valor numa variável global ao frame durante o evento TShellListView.OnStartDrag e então limpá-la no TShellListView.OnEndDrag. Também tive problemas com a pasta de destino- TTShellTreeView.GetNodeAt e TTShellTreeView.DropTarget retornam um TTreeNode mas, para conseguir o caminho para a operação de arquivo eu precisava de um TShellFolder; então seleciono o DropTarget para recuperar o SelectedFolder (um TShellFolder) e então seleciono a pasta anterior uma vez mais. Isso faz o TShellListView piscar terrivelmente (você pode ver a mudança de diretórios) então tentei utilizar TShellListView.Items.BeginUpdate e EndUpdate mas isso também não funcionou, então tive que desconectar o TShellTreeView, realizar as ações, e então reconectá-lo. Isso é uma verdadeira gambiarra, eu não gosto, mas funciona. O evento OnDragDrop não oferece informação acerca do estado do teclado e eu queria implementar a operação de cópia se o usuário estivesse pressionando Ctrl ao final do arrasto. Eu usei GetKeyState() da biblioteca Jedi (JCLSysInfo.pas) para isso.

Tendo colocado o arrasto para funcionar a partir do TShellListView eu alterei os eventos OnDragOver e OnDragDrop para também aceitarem uma pasta arrastada do TShellTreeView e acrescentei os eventos OnStartDrag e OnEndDrag a ele. Esses seis eventos oferecem todas as funcionalidades necessárias para soltar um arquivo no TShellTreeView a partir do frame. Para manter simples, eu apenas permito ao usuário arrastar um item de cada vez.

O Explorer permite que você arraste uma pasta da árvore para a lista de
arquivos e arquivos para pastas dentro da lista. Mas arrastar uma pasta
do TShellTreeView seleciona e exibe o conteúdo da pasta e, em ambos os
casos, TShellListView.DropTarget sempre retorna nil! Por isso, não pude
encontrar uma forma de implementar essas funcionalidades. :(

Tendo feito o possível para oferecer a funcionalidade de arrastar e soltar no frame, agora precisava fazer que funcionasse também com o Explorer. Para aceitar um arquivo arrastado do Explorer, é preciso usar mensagens do Windows. Eu estava incerto a respeito do efeito que isso teria sobre os eventos de arrastar e soltar já implementados; fiquei satisfeito ao verificar que não houve interferência. Tive problemas para chegar às configurações corretas, no entanto. É preciso chamar a função DragAcceptFiles() com o handle do controle que irá aceitar os arquivos para permitir que o Windows saiba como enviar a mensagem de soltar arquivos. Mas TFrame não possui um evento OnCreate e não podemos usar referências a seus componentes ou a ele próprio na seção de inicialização. Quis que meu frame fosse totalmente auto-contido, mas tive que aceitar ter que chamar DragAcceptFiles() no evento OnCreate do formulário que o contém. Inicialmente, pensei em passar o handle para o
TShellListView de modo que os arquivos pudessem ser soltos apenas nesse controle, mas para isso necessitaria de uma mensagem WMDROPFILES para TShellListView então decidi aceitar que os arquivos fossem soltos em qualquer lugar do frame passando seu handle. Uma vez contornados esses problemas o resto foi fácil.

A procedure TconExplorerFrame.WMDROPFILES lida com a mensagem de soltar. Ela usa DragQueryFile() da WinAPI para determinar o número de itens sendo soltos e então a chama de novo para recuperar o caminho completo de cada item na medida em que faz a iteração pela lista. O Windows nos oferece de forma automática com um cursor de cópia e, pressionar Ctrl ou Shift não tem qualquer efeito, então eu copio os itens para a pasta exibida no TShellListView.

Também quis implementar arrastar e soltar do programa para o Explorer e entre instâncias do próprio aplicativo, mas não consegui encontrar artigos ou dicas que explicassem como fazer isso. Esperei que, ao habilitar o arrasto a partir do Explorer, poderia obter, como efeito colateral, uma dessas funcionalidades; ou, pelo menos, um indicativo de como implementá-las, mas não foi esse o caso. Acho que preciso criar meu próprio descendente de TCustomShellListView habilitado para arrasto com o shell, mas não estou certo de como começar.

Então esse é o fim de minha exploração dos controles shell da Borland.
Se algum de vocês souber como arrastar do Delphi para outras aplicações
ou podem sugerir melhorias para o meu frame, por favor entrem me
contato. Os fontes para meu frame e o programa de teste estão no arquivo em anexo - estejam à vontade para utilizar em seus próprios programas.


Responder
×
+1 DevUP
Acesso diário, +1 DevUP
Parabéns, você está investindo na sua carreira