Criação de layouts de aplicativos inteligentes com o Windows Forms 2.0

Visual Studio 2005

Jay Allen, Microsoft Corporation

Publicado em: 1 de julho de 2006

Aplicável a:

  • Microsoft Visual Studio 2005

  • Microsoft .NET Framework 2.0

  • Microsoft Windows Forms 2.0

Resumo: Saiba como usar os novos controles do Windows Forms 2.0 para criar layouts de aplicativos inteligentes e extensíveis. (15 páginas impressas).

Baixe exemplos de código no C# e Visual Basic (903 KB) na Central de Download da Microsoft.

Nesta página

Introdução Introdução
Limites do TabControl Limites do TabControl
Tab-Style Tool Strip Tab-Style Tool Strip
Outlook-Style Tool Strip Outlook-Style Tool Strip
Menus Recolhíveis Menus Recolhíveis
Painel Flutuante Painel Flutuante
Conclusão Conclusão

Introdução

O Microsoft Windows Forms 2.0 permite que você organize a funcionalidade de seu aplicativo de maneiras exclusivas que são fáceis de seus clientes usarem. Usando novos controles, como ToolStrip, FlowLayoutPanel e TableLayoutPanel, será possível criar layouts de aplicativos inteligentes e extensíveis. Este artigo descreve quatro layouts de aplicativo: um tab-style tool strip, um Outlook-style tool strip, menus recolhíveis e um painel flutuante. Criar todos esses layouts é uma tarefa simples com o Windows Forms 2.0. Este artigo supõe que você tenha noções básicas sobre o Windows Forms e esteja familiarizado com os UserControls.

Limites do TabControl

Recentemente, desejei criar um aplicativo simples que servisse como um temporizador de contagem regressiva para várias atividades, como exercícios e meditação. A principal funcionalidade seria um temporizador que reproduziria um som repetidamente após o tempo definido (digamos, 30 minutos) ter expirado. Eu sabia que esse aplicativo simples teria três seções principais: Um painel Iniciar exibido na inicialização, um painel Contagem Regressiva em que você define e executa o relógio de contagem regressiva e um painel Configurações em que você define um som personalizado e verifica se deseja manter um registro de suas sessões.

Decidi que a melhor forma de fazer isso seria uma abordagem baseada em guias para navegação. Defini as principais áreas de funcionalidade em três guias diferentes. Na primeira, o TabControl, que você recebe gratuitamente com o Windows Forms, parecia a escolha natural. Mas, então, surgiu o problema. Eu não queria que esse aplicativo tivesse a aparência de um aplicativo normal do Windows, com os esquemas de cores cinza padrão. E não queria usar a guias alinhadas na parte superior, que são o padrão. Eu queria que as guias se esticassem para baixo à direita do TabControl. Eu queria que tudo fosse branco, sem bordas confusas e que as três guias se estendessem da parte superior para a inferior do aplicativo.

Logo percebi que o TabControl não faria o que eu queria. Embora o TabControl suportasse guias de alinhamento à direita e à esquerda, por padrão, isso faria com que o texto fosse apresentado verticalmente e não horizontalmente. Pensei que se o Visual Styles fosse habilitado, nenhum texto seria apresentado nas guias alinhadas lateralmente!

Descobri uma maneira de chegar perto do efeito que eu desejava usando o desenho proprietário. (Para obter detalhes, consulte meu TabControl do Window Forms: Usando Guias Alinhadas à Direita e à Esquerda postada no blog Windows Forms Documentation Updates.) Entretanto, existiam outros obstáculos que me impediam de usar o TabControl. Especificamente, o desenho proprietário redesenhava somente o conteúdo das guias. Eu não podia personalizar ou eliminar juntamente as bordas da guia, nas guias ou ao redor dos painéis da guia. Isso limitou a personalização que eu poderia efetuar. Para efetuar tudo isso com êxito, os controles nas próprias guias, às vezes, não são dispostos corretamente quando você aplica os Estilos Visuais. Dados todos esses problemas e a falta de capacidade de personalização, eu já não estava mais animado a usar o TabControl.

Tab-Style Tool Strip

Voltei um pouco e percebi que havia outra maneira mais criativa de abordar esse problema. Lembrei-me, a partir de minhas últimas correções, de que o novo controle ToolStrip do Windows Forms era muito flexível. Será que eu poderia usá-lo para criar exatamente a aparência que eu estava querendo?

Assim que descobri isso, pude fazê-lo — e somente com um mínimo de programação. A Figura 1 exibe o resultado acabado.


Figura 1. O aplicativo concluído, que usa um tab-style tool strip.

Segue como eu criei o aplicativo. O formulário principal é dividido em duas partes. No lado esquerdo está um UserControl personalizado que escrevi, chamado Slideshow (incluído no exemplo da Contagem Regressiva). Esse é apenas um controle "vistoso" que usa o controle de invólucro WebBrowser gerenciado para desaparecer entre uma série de imagens usando o HTML Dinâmico.

O que é interessante para este artigo é o lado direito, onde posicionei um controle SplitContainer. O SplitContainer também é um novo controle no Windows Forms. Ele substitui o antigo controle Splitter, fornecendo maior funcionalidade para o número e a direcionalidade de painéis divididos. O SplitContainer tem dois painéis divididos, Painel1 e Painel2. O Painel1 é o painel de conteúdo que conterá diversos controles para cada botão. O Painel2 incluirá botões que permitirão a navegação entre os três painéis: Iniciar, Contagem Regressiva e Configurações. O Painel2 hospedará o controle ToolStrip. Para meu aplicativo, mantive a propriedade Orientation em SplitContainer definido como Vertical (o padrão) e redimensionei os painéis no Microsoft Visual Studio para deixar o esquerdo maior que o direito. A Figura 2 mostra o aplicativo Contagem Regresssiva antes da adição do controle ToolStrip no Painel2.


Figura 2. O aplicativo Contagem Regressiva com um UserControl e um SplitContainer personalizados do Slideshow.

Depois de adicionar o UserControl e o SplitContainer do Slideshow personalizado, adicionei um controle ToolStrip no Painel2. Por padrão, a propriedade Dock do ToolStrip é definido como Top. Faz sentido que o padrão para a maioria dos aplicativos é usar o ToolStrip para criar as barras de ferramentas do comando, como no Microsoft Word e em outros aplicativos. Mas para meus objetivos, precisei definir a propriedade Dock para Fill. Eu também não queria a apresentação lisa parecida com o Office que o ToolStrip usa por padrão, então defini RenderMode como System e BackColor como White. Defini GripStyle como Hidden, por isso, o grip usado para capturar o ToolStrip e realocá-lo para outra área da tela não era visível. Finalmente, como eu queria que os botões de navegação fluíssem da parte superior para inferior, um sobre o outro, mudei o valor do LayoutStyle para VerticalStackWithOverflow.

Em seguida vieram os botões de navegação reais. Como eu teria três painéis, criei três botões. Usei o Items Collection Editor no designer visual para criar três objetos ToolStripButton no ToolStrip. Para abrir o Items Collection Editor, cliquei no botão elipse ao lado da propriedade Items para o ToolStrip. (Para abrir o Items Collection Editor, eu também poderia ter clicado na marca inteligente do ToolStrip e no menu Tarefas do ToolStrip e clicado em Editar Itens.)

Depois, precisei estilizar a aparência dos botões. Criei uma imagem para cada botão com stock photography e redimensionei as imagens com a mesma altura e largura. Cliquei no botão elipse ao lado da propriedade Image para cada ToolStripButton e usei a caixa de diálogo Selecionar Recurso para importar a imagem para cada botão. Por padrão, o ToolStripButton dimensionará a imagem para o tamanho padrão do botão. Como eu queria a imagem em seu tamanho real, defini a propriedade ImageScaling em cada botão para None.

Inseri o texto adequado na propriedade Text de cada ToolStripButton. Para fazer o texto aparecer sob a imagem, tive que alterar duas propriedades. Primeiramente, tive que definir a propriedade DisplayStyle como ImageAndText. Isso fez o texto aparecer no centro à direita da imagem. Para colocar o texto sob a imagem, defini a propriedade TextImageRelation como ImageAboveText. Em seguida, alterei a fonte do ToolStripButton por isso o texto ficou do meu gosto.

Para meu último truque de mágica, decidi que queria que o botão aparecesse "selecionado" quando fosse selecionado, então o usuário teria um indicador visual rápido de qual painel estava ativo no momento. (O efeito de selecionado aparece como uma borda ao redor do segundo botão na Figura 1.) Para isso, defini a propriedade CheckOnClick para cada botão como True.

Tornando o Tool Strip Tabs funcional

Esse era o limite do que eu podia fazer com o designer. Como você pode ver, fiz muita coisa sem escrever um código. Agora, chegou o momento de projetar painéis individuais e conectar tudo. Para meu tab-style tool strip, procurei implementar a seguinte lógica:

  • Exibir o painel Iniciar por padrão quando o aplicativo fosse iniciado.

  • Detectar quando um botão ToolStrip fosse clicado. Se o painel correspondente para aquele botão ainda não tivesse sido exibido, criá-lo e exibi-lo.

  • Desmarcar o botão selecionado anteriormente.

Para cada um dos painéis, criei um UserControl separado: um para o painel Iniciar, um para o painel principal Contagem Regressiva e um para o painel Configurações. nomeei esses painéis como CDHomePanel, CDCountdownPanel eCDSettingsPanel, respectivamente.

Depois de ter implementado a estrutura desses UserControls, comecei a escrever a lógica para exibi-la no Painel1 do SplitContainer. Como eu tinha somente três painéis, eu poderia ter embutido no código um manipulador de eventos separado para que cada botão criasse uma instância e exibisse o painel adequado. Entretanto, essa é uma abordagem muito extensível. Adequada para três botões — mas e se fossem seis botões? E se eu quisesse usar essa mesma abordagem em um aplicativo maior que precisasse de 10 a 20 botões? Decidi escrever um único manipulador de eventos para manipular todos os botões sem embutir no código nenhum valor. Eu queria que o código fosse abstrato o suficiente para que, no futuro, eu pudesse adicionar outro botão e outro painel com alterações mínimas no código do manipulador de eventos ToolStripButton.

Primeiramente, voltei para o designer e, para cada ToolStripButton, defini a propriedade Tag para o nome do UserControl, prefixado com seu namespace, correspondente ao botão. Fiz isso para que meu manipulador de eventos pudesse usar a propriedade Tag do botão que foi clicado a fim de deduzir para qual UserControl ele deveria criar uma instância. Por exemplo, para meu CDHomePanel UserControl, que existe no namespace Countdown, o nome completamente qualificado é Countdown.CDHomePanel. Atribui "Countdown.CDHomePanel" à propriedade Tag do botão Iniciar. Similarmente, defini a propriedade Tag do botão Contagem Regressiva como "Countdown.CDCountdownPanel" e a propriedade Tag do botão Configurações como "Countdown.CDSettingsPanel".

Depois, adicionei alguns códigos que meu formulário precisaria para o manipulador de eventos. Como ia instanciar os painéis do UserControl dinamicamente, precisaria usar classes definidas nos namespaces System.Reflection e System.Runtime.Remoting. Também defini duas variáveis particulares de nível de classe que meu manipulador de eventos precisaria: _CurrentControl, uma referência ao painel atualmente visível para o usuário e _CurrentClickedButton, uma referência ao ToolStripButton correspondente ao painel visível.

Imports System.Threading
Imports System.Drawing.Drawing2D
Imports System.Reflection
Imports System.Runtime.Remoting

Public Class Form1
	Private _CurrentControl As Control
	Private _CurrentClickedButton As ToolStripButton = Nothing
	

A seguir, adicionei o código ao manipulador de eventos Load de meu formulário para instanciar o painel Iniciar, exibi-lo ao usuário e inicializar as variáveis _CurrentControl e _CurrentClickedButton com referências de objetos válidos. Como eu tinha que fazer isso somente uma vez durante a vida útil do aplicativo, criei diretamente uma instância do CDHomePanel. Quando o painel foi instanciado, adicionei-o ao Painel1 do meu SplitContainer.

Dim HomePanel As New CDHomePanel()
HomePanel.Tag = "Countdown.CDHomePanel"
HomePanel.Dock = DockStyle.Fill
SplitContainer1.Panel1.Controls.Add(HomePanel)
_CurrentControl = HomePanel
_CurrentClickedButton = HomeButton
	

Finalmente, escrevi um método chamado Navigate, que é projetado para ser chamado a partir do evento MouseDown de cada ToolStripButton. (Explicarei posteriormente na seção Incorporação de prompts ao alternar painéis porque eu fatorei esse código em um método separado.) Como eu estava criando no Visual Basic, pude usar a palavra-chave Handles para especificar que esse manipulador de eventos é aplicável a todos os três botões. O manipulador analisa o conjunto de Controls do Painel1 de meu SplitContainer e verifica se o UserControl correspondente a esse botão já foi criado. Em caso negativo, ele o cria enviando a propriedade Tag do ToolStripButton para o método CreateInstance, que é definido na classe AppDomain.

Private Sub Navigate(ByVal sender As Object)
    Dim _NewControl As Control
    Dim _CurrentButton As ToolStripButton = CType(sender, ToolStripButton)
    Dim _ControlName As String = _CurrentButton.Tag.ToString()

    ' Uncheck the previous clicked button.
    _CurrentClickedButton.Checked = False
    _CurrentClickedButton = _CurrentButton

    ' First, make sure this isn't a redundant event - are we already 
    ' displaying the control?
    If (Not (_ControlName = _CurrentControl.Name)) Then
        ' Get the control to use, and instantiate it dynamically 
        ' if it isn't already defined.
        _NewControl = SplitContainer1.Panel1.Controls(_ControlName)
        If (_NewControl Is Nothing) Then
            ' Control not found - instantiate it.
            Dim _Oh As ObjectHandle = _
                AppDomain.CurrentDomain.CreateInstance( _
                Assembly.GetExecutingAssembly().FullName, _ControlName)
            _NewControl = _Oh.Unwrap()
            _NewControl.Name = _ControlName
            _NewControl.Dock = DockStyle.Fill
            SplitContainer1.Panel1.Controls.Add(_NewControl)
        End If

        ' Hide the old control, and show the new one. 
        _CurrentControl.Visible = False
        _NewControl.Visible = True
        _CurrentControl = _NewControl
    End If
End Sub	
	

Com isso, concluí e pude compilar e executar o aplicativo com êxito. Devido à forma como escrevi o código, seria fácil adicionar um novo botão e um painel em quatro etapas.

  1. Criar UserControl

  2. Criar o ToolStripButton.

  3. Atribuir o nome do UserControl à propriedade Tag.

  4. Adicionar o ToolStripButton à cláusula Handles do manipulador de eventos.

Como pode ver, a etapa mais difícil aqui é criar o novo UserControl. Depois disso, tudo o que será necessário é configurar o botão como fiz anteriormente e modificar uma linha do código. Agora você têm uma substituição para o TabControl que é fácil de estender e mais aberto à personalização.

Incorporação de prompts ao alternar painéis

Depois de concluir tudo isso, algumas perguntas me ocorreram. O que aconteceria quando os usuários clicassem no botão Configurações quando eles estivessem no painel Contagem Regressiva e o relógio estivesse sendo executado? E o que aconteceria se eles estivessem no painel Configurações e clicassem no botão Contagem Regressiva antes de salvar suas configurações? Deveria manter as alterações? Descartá-las? Perguntar ao usuário?

Ficou claro que os painéis UserControl precisavam de um mecanismo por meio do qual seu formulário pai poderia informar os painéis UserControl que uma navegação estava prestes a ocorrer. Semelhantemente, cada um dos painéis precisava de uma forma para permitir ou proibir essa navegação, com base no estado do painel. Para conseguir isso, defini uma interface chamada IPanelNavigating, e declarei um único método nomeado CanNavigate para as classes a serem implementadas. O método CanNavigate retorna um valor Booleano que indica se o painel pode ser alternado.

Public Interface IPanelNavigating
    Function CanNavigate() As Boolean
End Interface
	

Como eu nunca teria a necessidade de parar a navegação no painel Iniciar, não o implementei para essa classe. Eu o implementei em meu painel Contagem Regressiva e usei uma caixa de mensagem para perguntar aos usuários se eles desejavam finalizar a contagem regressiva. Em caso afirmativo, eu redefiniria o temporizador e retornaria True a partir do CanNavigate. Caso eles não quisessem finalizar a sessão, eu retornaria False.

Para implementar isso em meu formulário principal, adicionei alguma lógica no manipulador de eventos MouseDown que defini para todos os três objetos ToolStripButton.

Private Sub ToolStripButton_MouseDown(ByVal sender As System.Object, _
ByVal e As MouseEventArgs) Handles HomeButton.MouseDown, _
CountdownButton.MouseDown, SettingsButton.MouseDown
    ' Attempt to cast to an IPanelNavigating. If not implemented, 
    ' just navigate.
    If (TypeOf _CurrentControl Is IPanelNavigating) Then
        Dim _CanNavigate As IPanelNavigating = _
            CType(_CurrentControl, IPanelNavigating)
        If (_CanNavigate.CanNavigate()) Then
            Navigate(sender)
        End If
    Else
        ' Just navigate.
        Navigate(sender)
    End If
End Sub
	

Concluí o aplicativo elaborando a programação dos painéis UserControl e adicionando suporte para uma barra de títulos personalizada, que me forneceu outro uso exclusivo do controle ToolStrip. (Para obter detalhes sobre como implementar isso, consulte minha postagem Usar ToolStrip para criar uma barra de títulos personalizada no blog Windows Forms Documentation Updates)

Outlook-Style Tool Strip

A grande novidade sobre a arquitetura do aplicativo Contagem Regressiva é que ele é fácil de ser reutilizado em outros aplicativos. Graças ao poder do controle ToolStrip, você poderá personalizar radicalmente a aparência para ajustá-la às necessidades de seu aplicativo. A Figura 3 mostra outro exemplo, uma imitação de um aplicativo bancário. Aqui a navegação tem um Outlook-style tool strip.


Figura 3. Um layout de imitação simples que tem um Outlook-style tool strip.

Para esse aplicativo, coloquei o ToolStrip à esquerda e deixei o painel direito do SplitContainer disponível para os painéis UserControl. Formatei os objetos ToolStripButton definindo DisplayStyle como ImageAndText, definindo ImageAlign como MiddleCenter, definindo TextAlign como MiddleRight, e definindo TextImageRelation como Overlay. Toda a lógica que escrevi para o aplicativo Contagem Regressiva funciona muito bem nesse aplicativo e precisa somente de ajustes mínimos par funcionar com um número arbitrário de controles ToolStripButton.

Você pode estender este conceito adicionando botões com submenus. O ToolStrip suporta um controle chamado ToolStripDropDownButton, que permite a adição de controles filhos que serão exibidos como um submenu quando o usuário clicar no botão. A Figura 4 mostra o exemplo bancário de imitação modificado de forma que o botão Configurações seja um ToolStripDropDownButton com vários itens de submenu denotando os diferentes tipos de configurações que o usuário pode modificar.


Figura 4. O layout de imitação simples da Figura 3, com os controles ToolStripDropDownButton que exibem submenus.

Menus Recolhíveis

Depois de concluir meu aplicativo Contagem Regressiva, decidi achar outras formas de implementar menus complexos e sistemas de navegação usando os novos controles no Windows Forms. A primeira coisa que percebi como "similar, ainda que diferente" foi a janela Caixa de Ferramentas do Visual Studio. Se você tiver feito qualquer programação no Windows Forms ou ASP.NET com o Visual Studio, estará familiarizado com o modo em que a Caixa de Ferramentas classifica os controles em uma série de menus recolhíveis. A Figura 5 mostra um exemplo dos menus recolhíveis na Caixa de Ferramentas.


Figura 5. Menus recolhíveis na janela Caixa de Ferramentas do Visual Studio.

Nas versões anteriores do Windows Forms, a emulação da Caixa de Ferramentas teria envolvido a adição de uma série de códigos para o reposicionamento dos menus recolhíveis. No Windows Forms, a tarefa é significativamente simplificada pelo controle FlowLayoutPanel. FlowLayoutPanel hospeda um número arbitrário de controles de Windows Forms em um fluxo seqüencial, um controle posicionado após o outro. A grande novidade sobre o FlowLayoutPanel é que ele é dinâmico. Se você remover ou ocultar um controle em tempo de execução, os outros controles após ele "se recolherão" no espaço deixado. Isto é exatamente o que será necessário para implementar os menus recolhíveis.

Dividi o controle recolhível em dois controles separados:

  • Um controle nomeado como ListHeader para representar o elemento de interface de usuário sombreado que exibe o texto do cabeçalho e o indicador recolhível mais/menos no lado esquerdo. ListHeader define um evento que surge sempre que o indicador recolhível é clicado.

  • Um controle nomeado CollapsibleControl que combina ListHeader com outro controle arbitrário (que chamarei de "controle de conteúdo"). CollapsibleControl contém o FlowLayoutPanel, que exibe um número arbitrário de seções de conteúdo que podem ser recolhidos e expandidos.

Não quero entrar em detalhes sobre como implementei o controle ListHeader; os leitores interessados podem examinar o código. O fato importante aqui é que defini um evento ListHeaderStateChanged para sinalizar para o CollapsibleControl se o controle de conteúdo tiver de ser recolhido ou exibido. ListHeaderStateChanged envia um ListHeaderEventArgs, que define uma propriedade State do tipo ListHeaderState. ListHeaderState é uma enumeração com dois valores possíveis: ListHeaderState.Expanded ou ListHeaderState.Collapsed.

Com o ListHeader concluído, comecei a criar CollapsibleControl. O CollapsibleControl é dividido em duas partes.

Primeiramente, criei uma classe CollapsibleControlSection. O CollapsibleControlSection associa um controle de conteúdo a um nome de cabeçalho.

Public Class CollapsingControlSection
    Inherits Object

    Private _SectionName As String = Nothing
    Private _Control As Control = Nothing


    Public Sub New(ByVal sectionName As String, ByVal control As Control)
        _SectionName = sectionName
        _Control = control
    End Sub


    Public Property SectionName() As String
        Get
            Return _SectionName
        End Get
        Set(ByVal value As String)
            _SectionName = value
        End Set
    End Property

    Public Property SectionControl() As Control
        Get
            Return _Control
        End Get
        Set(ByVal value As Control)
            _Control = value
        End Set
    End Property
End Class	
	

A classe CollapsibleControl usa o controle ListHeader e um ou mais objetos CollapsibleControlSection para exibir um conjunto de controles de conteúdo separados por cabeçalhos. Quando um controle de conteúdo for recolhido ou oculto, o outro controle preencherá o espaço deixado. Para esse truque dar certo, primeiramente abri o CollapsibleControlSection no designer do Visual Studio e adicionei um FlowLayoutPanel ao controle. Defini a propriedade Dock do meu FlowLayoutPanel como Fill, por isso ele ocupou toda a área do controle.

Segundo, criei o CollapsibleControl. A lógica necessária para fazer esse controle funcionar é surpreendentemente simples. Importei o namespace System.Collections.Generic de forma que eu pudesse usar a classe genérica List(Of T) para armazenar os objetos CollapsibleControlSection. Essa List é preenchida pelo método AddSection, que realiza as seguintes operações:

  • Cria um ListHeader para o controle de conteúdo e o adiciona juntamente com o controle de conteúdo no final do FlowLayoutPanel.

  • Usa a propriedade Tag do controle ListHeader para associar o controle de conteúdo a seu controle ListHeader, de forma que o CollapsibleControl saiba qual controle de conteúdo deverá mostrar ou ocultar.

  • Adiciona um manipulador ao evento do controle ListHeaderStateChanged do ListHeader, de forma que ele possa mostrar ou ocultar o painel de conteúdo adequado quando o usuário clicar no indicador recolhível no ListHeader.

Segue o código para a classe CollapsibleControl.

Public Class CollapsibleControl
    Dim SectionList As List(Of CollapsibleControlSection)

	Public Sub New()
        ' This call is required by the Windows Form Designer.
        InitializeComponent()

        SectionList = New List(Of CollapsibleControlSection)()
    End Sub

    Public Sub AddSection(ByVal NewSection As CollapsibleControlSection)
        SectionList.Add(NewSection)
        ' Add a new row. 
        Dim Header As New ListHeader()
        Header.Text = NewSection.SectionName
        Header.Width = Me.Width
        AddHandler Header.ListHeaderStateChanged,
            AddressOf Header_ListHeaderStateChanged
        FlowLayoutPanel1.Controls.Add(Header)
        ' Get the position of the control we're going to add,
        ' so that we can show and hide it when needed.
        Header.Tag = FlowLayoutPanel1.Controls.Count

        Dim c As Control = NewSection.SectionControl
        c.Width = Me.Width
        FlowLayoutPanel1.Controls.Add(c)
    End Sub

    Sub Header_ListHeaderStateChanged(ByVal sender As Object, _
    ByVal e As ListHeaderStateChangedEventArgs)
        Dim header As ListHeader = CType(sender, ListHeader)
        Dim c As Control = FlowLayoutPanel1.Controls(CInt(header.Tag))

        If e.State = ListHeaderState.Collapsed Then
            c.Hide()
        Else
            c.Show()
        End If
    End Sub
End Class	
	

Como eu não estava preocupado em fazer o controle designável, não implementei um método RemoveSection nem forneci nenhuma outra infra-estrutura necessária para permitir a adição ou remoção de seções no designer do Visual Studio. O controle, como escrito, deverá ser criado e preenchido por meio de código.

Para testar esse código, criei um UserControl chamado ToolboxContentControl, que servirá como meu controle de conteúdo. O ToolboxContentControl contém um controle ToolStrip que tem dois controles ToolStripButton configurados para se assemelharem aos itens da Caixa de Ferramenta do Visual Studio. (Se eu fosse criar um aplicativo na vida real usando o CollapsibleControl, teria uma série de controles de conteúdo, cada um com várias opções. Para meu aplicativo de teste, entretanto, usei quatro instâncias de ToolboxContentControl.) Em seguida, adicionei um CollapsibleControl à minha classe de formulário principal do aplicativo e um pouco de código para preenchê-lo com quatro seções:

Private Sub Form1_Load(ByVal sender As System.Object, _
ByVal e As System.EventArgs) Handles MyBase.Load
    Dim NewSection As New CollapsibleControlSection( _
        "Toolbox Controls", New ToolboxContentControl())
    Me.CollapsibleControl1.AddSection(NewSection)

    Dim NewSection2 As New CollapsibleControlSection( _
        "Toolbox Controls 2", New ToolboxContentControl())
    Me.CollapsibleControl1.AddSection(NewSection2)

    Dim NewSection3 As New CollapsibleControlSection( _
        "Toolbox Controls 3", New ToolboxContentControl())
    Me.CollapsibleControl1.AddSection(NewSection3)

    Dim NewSection4 As New CollapsibleControlSection( _
        "Toolbox Controls 4", New ToolboxContentControl())
    Me.CollapsibleControl1.AddSection(NewSection4)
End Sub	
	

O resultado foi exatamente o que eu visava, um conjunto de menus recolhíveis que foram reposicionados quando um ou mais menus foram recolhidos — graças ao poder do FlowLayoutPanel. A Figura 6 mostra um exemplo dos menus recolhíveis.


Figura 6. Exemplo de menus recolhíveis em ação.

O exemplo de menu recolhível imita os menus na Caixa de Ferramentas do Visual Studio, mas a janela Caixa de Ferramentas também tem a capacidade de entrar e sair da exibição. O tipo de janela é, às vezes, chamado de painel flutuante.

Painel Flutuante

Um painel flutuante é um painel que entra na exibição quando solicitado pelo usuário e sai da exibição quando não é utilizado. A janela Caixa de Ferramentas do Visual Studio entra na exibição quando você posiciona o ponteiro do mouse sobre o ícone Caixa de Ferramentas. Quando o ponteiro do mouse sair da região do ícone Caixa de Ferramentas e do painel, o painel sai automaticamente da exibição.

Você pode implementar esse mesmo comportamento no Windows Forms, mais uma vez empregando nosso bom amigo, o controle ToolStrip. Para minha primeira etapa, criei um novo UserControl chamado ToolboxPanel. Fiz a mesma coisa para o ToolboxPanel que fiz para o formulário de teste do menu recolhível; adicionei um CollapsibleControl e alguns códigos para preenchê-lo com quatro seções para fins de teste.

Depois, criei um novo formulário para meu projeto e o defini como o formulário de partida. Em seguida, adicionei um ToolStrip e defini a propriedade Dock como Left. Criei um único ícone ToolStripLabel, atribui a ele um imagem parecida com a Caixa de Ferramentas e defini a propriedade Text para a palavra "Toolbox". Para fazer com que o texto fosse exibido verticalmente, defini a propriedade TextDirection do botão ToolStrip como Vertical90.

Para meu formulário de teste, tudo o que eu queria era mostrar um painel flutuante simples. Entretanto, na vida real, você provavelmente desejaria uma série de painéis flutuantes, como o Visual Studio usa. Escrevi meu código de uma forma que suportaria vários painéis flutuantes (embora não sem aprimoramentos futuros). Emprestando de um truque que usei anteriormente com o meu aplicativo Contagem Regressiva, defini a propriedade Tag do meu ToolStripLabel como "TestPanelFlyoutVB.ToolboxPanel", que é um nome inteiramente qualificado do controle ToolboxPanel que criei anteriormente. O nome inteiramente qualificado é usado no evento MouseEnter do ToolStripLabel para instanciar o controle ToolboxPanel se ele ainda não existir.

Coloquei dois controles Timer no formulário para ajudar com a animação de flutuação. PanelTimer é ativado quando o painel de flutuação precisa ser exibido ou ocultado. MouseLeaveTimer fornece o atraso de um segundo na retração do painel quando o mouse sai do ToolStripLabel. Isso ocorre, porque nossa lógica de painel flutuante tem que acomodar duas possibilidades:

  • O usuário coloca o mouse novamente sobre o ToolStripLabel quase imediatamente, caso em que o painel flutuante deve permanecer exibido.

  • O mouse sai do ToolStripLabel, mas insere a área do painel flutuante, caso em que o painel flutuante deve permanecer em exibição de forma que o usuário possa interagir com ele. Há um caso de limite interessante aqui, em que o mouse pode ser momentaneamente posicionado em uma borda fina do controle ToolStrip que reside entre o ToolStripLabel e o painel flutuante em si. O atraso de um segundo dá ao usuário tempo para concluir a movimentação do cursor no painel flutuante.

Com isso feito, escrevi o código a seguir para juntar tudo.

Imports System.Runtime.Remoting
Imports System.Reflection

Public Class FlyoutForm

    Dim _DoFade As Boolean = False
    Dim _HaveProcessedMouseEnter As Boolean = False
    Dim _CurrentControl As Control = Nothing
    Dim _CachedControlPoint As Point = Nothing

    Sub ToolBoxLabel_MouseEnter(ByVal sender As Object, _
    ByVal e As EventArgs) Handles ToolBoxLabel.MouseEnter
        If Not _HaveProcessedMouseEnter Then
            _HaveProcessedMouseEnter = True

            If (_DoFade) Then
                PanelTimer.Stop()
                _DoFade = False
            ElseIf (_CurrentControl Is Nothing) Then
                ' Get the control to use, and instantiate it dynamically.
                Dim controlName As String = CType(sender, _
                    ToolStripLabel).Tag.ToString()
                Dim oh As ObjectHandle = _
                    AppDomain.CurrentDomain.CreateInstance( _
                    Assembly.GetExecutingAssembly().FullName, controlName)
                _CurrentControl = CType(oh.Unwrap(), Control)
                _CurrentControl.Height = Me.Height
                _CurrentControl.Location = New Point( _
                    toolStrip1.Location.X + toolStrip1.Width - _
                    _CurrentControl.Width, 0)
                _CachedControlPoint = _CurrentControl.Location
                Me.Controls.Add(_CurrentControl)

                ' The following calls make the panel appear above all 
                ' other controls on the form,
                ' *except* the ToolStrip with the panel buttons.
                _CurrentControl.BringToFront()
                toolStrip1.BringToFront()
            End If

            PanelTimer.Start()
        End If
    End Sub 'toolBoxLabel_MouseEnter

    Sub ToolBoxLabel_MouseLeave(ByVal sender As Object, _
    ByVal e As EventArgs) Handles ToolBoxLabel.MouseLeave
        ' Slight problem: We could be transitioning between the ToolStrip 
        'and the flyout panel. If the
        ' mouse is in an inbetween state where it's not over the 
        ' ToolStripLabel but *is* over the
        ' thin wedge of the ToolStrip itself, we'll fade the panel 
        ' erroneously. So let's kick off this
        ' timer to give the user time to transition. The delay built into 
        ' VS is about 1 second; that 
        ' should work for us.
        MouseLeaveTimer.Start()
    End Sub


    Private Sub PanelTimer_Tick(ByVal sender As Object, _
    ByVal e As EventArgs) Handles PanelTimer.Tick
        If _DoFade Then
            ' Hide the panel.
            If _CachedControlPoint.X + _CurrentControl.Size.Width < _
            toolStrip1.Location.X + toolStrip1.Width Then
                _CachedControlPoint.Offset(-20, 0)
                _CurrentControl.Location = _CachedControlPoint
            Else
                PanelTimer.Stop()
                _HaveProcessedMouseEnter = False
                _DoFade = False
            End If
        Else
            ' Show the panel.
            If _CachedControlPoint.X < toolStrip1.Location.X + _
            toolStrip1.Width Then
                _CachedControlPoint.Offset(20, 0)
                _CurrentControl.Location = _CachedControlPoint
            Else
                PanelTimer.Stop()
                _HaveProcessedMouseEnter = False
                _DoFade = True
            End If
        End If
    End Sub

    Private Sub MouseLeaveTimer_Tick(ByVal sender As Object, _
    ByVal e As EventArgs) Handles MouseLeaveTimer.Tick
        ' If we're over the flyout panel, don't trigger the leave event. 
        Dim controlUnderMouse As Control = _
            Me.GetChildAtPoint(Me.PointToClient( _
            System.Windows.Forms.Cursor.Position))

        ' This may get us whatever child control is under the ToolStrip. 
        ' We need to inspect
        ' parent controls until we reach the Form. If we don't find a 
        ' control that matches
        ' _CurrentControl in the parenting chain, we fade.
        Dim overPanel As Boolean = False
        While controlUnderMouse IsNot Me
            ' If we're over the blank form area, controlOverMouse 
            ' will be null.
            If controlUnderMouse Is Nothing Then
                overPanel = False
                Exit While
            End If

            If controlUnderMouse Is _CurrentControl Then
                overPanel = True
                Exit While
            End If

            controlUnderMouse = controlUnderMouse.Parent
        End While
        If Not overPanel Then
            ' Since the mouse must leave the panel area SOMETIME, 
            ' keep checking until we've left. 
            MouseLeaveTimer.Stop()
            PanelTimer.Start()
        End If
    End Sub
End Class	
	

Com esse código incorporado, o exemplo de meu painel flutuante funcionou uniformemente. A Figura 7 mostra o painel flutuante na exibição completa.


Figura 7. Exemplo de painel flutuante em ação.

Observação:O aplicativo de exemplo executa mais lentamente sob o depurador do Visual Studio. Especificamente, o painel flutuante reduz cerca de metade de sua velocidade esperada. O exemplo é executado como esperado quando ele é executado fora do depurador.

Obviamente há mais trabalho que pode ser feito aqui em termos de estilização do painel flutuante, incorporando vários painéis e implementando esses recursos como fixação com alfinetes (onde o painel flutuante permanece visível em vez de aparecer e desaparecer). Também, para suportar a adição de outras guias do painel flutuante, eu precisaria adicionar a lógica para verificar um painel flutuante aberto e retraí-lo antes de exibir o novo painel.

Conclusão

Conforme demonstrado neste artigo, o Windows Forms 2.0 facilita a criação de layouts de navegação e dinâmica avançados. Para obter mais informações sobre o Windows Forms, verifique:

Windows Forms no Microsoft .NET Framework Developer Center

WindowsForms.NET

Blog do Windows Forms Documentation Updates

Mostrar: