Este artigo se concentra em como usar o controle TreeView. O controle TreeView possui funções avançadas e pode ser usado em muitas situações. Resumo: descreve como adicionar funcionalidade de associação de dados ao controle TreeView. Ele faz parte de uma série de exemplos de desenvolvimento de controle do Microsoft Windows. Você pode ler este artigo em conjunto com o artigo de visão geral relacionado.
Introdução
Sempre que possível, você deve começar com controles prontos para uso, pois os controles do Microsoft® Windows® Forms fornecidos incluem tanta codificação e testes que seria um desperdício abandoná-los e começar do zero. Com base nisso, neste exemplo, herdarei um controle existente do Windows Forms, TreeView, e depois o personalizarei. Ao baixar o código do controle TreeView , você também obtém exemplos adicionais de desenvolvimento de controle, bem como um aplicativo de exemplo que demonstra como usar o TreeView aprimorado com outros controles vinculados a dados.
Projetando uma visualização em árvore de vinculação de dados
Para desenvolvedores do Windows, adicionar vinculação de dados ao controle TreeView é um problema comum, mas como há uma grande diferença entre TreeView e outros controles (como ListBox ou DataGrid ) (ou seja, TreeView exibe dados hierárquicos), o controle básico Este recurso é ainda não é suportado (ou seja, ainda temos que usá-lo). Dada uma tabela de dados, fica claro como exibir essas informações em um ListBox ou DataGrid , mas usar a natureza em camadas de um TreeView para exibir os mesmos dados não é tão simples. Pessoalmente, apliquei muitos métodos diferentes ao exibir dados usando TreeView , mas há um método que é mais comumente usado: agrupar os dados na tabela por determinados campos, conforme mostrado na Figura 1.
Figura 1: Exibindo dados no TreeView
Neste exemplo, criarei um controle TreeView no qual posso passar um conjunto de dados simples (mostrado na Figura 2) e produzir facilmente os resultados mostrados na Figura 1.
Figura 2: Conjunto de resultados simples contendo todas as informações necessárias para criar a árvore mostrada na Figura 1
Antes de começar a codificar, criei um design para o novo controle que lidaria com esse conjunto de dados específico e, com sorte, funcionaria em muitas outras situações semelhantes. Adicione uma coleção de grupos grande o suficiente para criar uma hierarquia usando a maioria dos dados simples, nos quais você especifica um campo de agrupamento, um campo de exibição e um campo de valor para cada nível de hierarquia (qualquer um ou todos os campos devem ser iguais). Para transformar os dados mostrados na Figura 2 no TreeView mostrado na Figura 1, meu novo controle requer que você defina dois níveis de agrupamento, Publisher e Title, e defina pub_id como o campo de agrupamento do grupo Publisher e title_id como o campo de agrupamento do grupo Título. Além dos campos de agrupamento, você também precisa especificar campos de exibição e de valor para cada grupo para determinar o texto exibido no nó do grupo correspondente e o valor que identifica exclusivamente o grupo específico. Ao encontrar esse tipo de dados, use pub_name/pub_id e title/title_id como campos de exibição/valor para esses dois grupos. As informações do autor tornam-se os nós folha da árvore (os nós no final da hierarquia de agrupamento), e você também precisa especificar os campos de ID ( au_id ) e exibição ( au_lname ) para esses nós.
Ao construir um controle personalizado, determinar como o programador usará o controle antes de iniciar a codificação ajudará a tornar o controle mais eficiente. Neste caso, gostaria que o programador (dados os dados mostrados anteriormente e o resultado desejado) conseguisse realizar o agrupamento com algumas linhas de código como este:
Com DbTreeControl .ValueMember = au_id .DisplayMember = au_lname .DataSource = minhaDataTable.DefaultView .AddGroup (Editor, pub_id, pub_name, pub_id) .AddGroup(Título, título_id, título, título_id) Terminar com |
Nota: Esta não é a linha final do código que escrevi, mas é semelhante. Ao desenvolver o controle, percebi que precisava associar o índice de imagem no ImageList associado ao TreeView a cada nível de agrupamento, então tive que adicionar um parâmetro extra ao método AddGroup .
Para realmente construir a árvore, examinarei os dados e procurarei alterações nos campos (especificados como os valores de agrupamento para cada agrupamento) enquanto crio novos nós de agrupamento, se necessário, e um nó folha para cada item de dados. Devido ao agrupamento de nós, o número total de nós será maior que o número de itens na fonte de dados, mas haverá exatamente um nó folha para cada item nos dados subjacentes.
Figura 3: Nós de grupo e nós folha
A distinção entre nós folha e nós de grupo (mostrados na Figura 3) será importante para o restante deste artigo. Decidi tratar esses dois tipos de nós de maneira diferente, criar nós personalizados para cada tipo de nó e gerar eventos diferentes com base no tipo de nó selecionado.
Implementar vinculação de dados
A primeira etapa para escrever o código para esse controle é criar o projeto e a classe inicial correspondente. Neste exemplo, primeiro crio uma nova biblioteca de controles do Windows, depois excluo a classe UserControl padrão e a substituo por uma nova classe que herda do controle TreeView :
Classe pública dbTreeControl
Herda System.Windows.Forms.TreeView
Deste ponto em diante, projetarei um controle que pode ser colocado em um formulário e ter a aparência e a funcionalidade de um TreeView normal. A próxima etapa é começar a adicionar o código necessário para lidar com a nova funcionalidade que está sendo adicionada ao TreeView , ou seja, vinculação de dados e agrupamento de dados.
Adicionar propriedade DataSource
Toda a funcionalidade do meu novo controle é importante, mas duas questões principais na criação de controles complexos vinculados a dados são o tratamento das propriedades DataSource e a recuperação de itens individuais de cada objeto da fonte de dados.
Criar rotina de atributos
Primeiro, qualquer controle usado para implementar vinculação de dados complexa precisa implementar uma rotina de propriedade DataSource e manter variáveis de membro apropriadas:
m_DataSource privado como objeto _ Propriedade pública DataSource() como objeto Pegar Retornar m_DataSource Fim Definir (valor ByVal como objeto) Se o valor não é nada, então cm = nada Agrupamento alterado() Outro Se não (o valor TypeOf é IList ou _ O valor TypeOf é IListSource) Então 'não é uma fonte de dados válida para este propósito Lançar novo System.Exception (DataSource inválido) Outro Se o valor TypeOf for IListSource, então Dim myListSource como IListSource minhaListSource = CType(Valor, IListSource) Se myListSource.ContainsListCollection = True então Lançar novo System.Exception (DataSource inválido) Outro 'Sim, sim. é uma fonte de dados válida m_DataSource = Valor cm = CType(Me.BindingContext(Valor), _ Gerenciador de moeda) Agrupamento alterado() Terminar se Outro m_DataSource = Valor cm = CType(Me.BindingContext(Valor), _ Gerenciador de moeda) Agrupamento alterado() Terminar se Terminar se Terminar se Conjunto final Fim da propriedade |
Geralmente há suporte para objetos que podem ser usados como fontes de dados para vinculações de dados complexas. Essa interface expõe os dados como uma coleção de objetos e fornece diversas propriedades úteis, como Count . Meu novo controle TreeView requer um objeto apoiado por IList em sua ligação, mas usar outra interface funciona bem porque fornece uma maneira conveniente de obter um objeto IList ( GetList ). Ao definir a propriedade DataSource , primeiro determino se um objeto válido é fornecido, ou seja, um objeto que suporta IList ou IListSource . O que eu realmente quero é um IList , portanto, se o objeto suportar apenas IListSource (por exemplo, DataTable ), usarei o método GetList() dessa interface para obter o objeto correto.
Alguns objetos que implementam IListSource (como DataSet ) na verdade contêm várias listas representadas pela propriedade ContainsListCollection . Se esta propriedade for True , GetList retornará um objeto IList representando uma lista (contendo múltiplas listas). No meu exemplo, decidi oferecer suporte a conexões diretas com objetos IList ou objetos IListSource que contêm apenas um objeto IList e ignorar objetos que exigem trabalho adicional para especificar uma fonte de dados, como um DataSet .
Nota: Se desejar oferecer suporte a tais objetos ( DataSet ou similar), você poderá adicionar uma propriedade adicional (como DataMember ) para especificar uma sublista específica para ligação.
Se a fonte de dados fornecida for válida, o resultado final será uma instância criada ( cm = Me.BindingContext(Value) ). Como esta instância será usada para acessar a fonte de dados subjacente, as propriedades do objeto e as informações de localização, ela é armazenada em variáveis locais.
Adicionar propriedades de membro de exibição e valor
Ter um DataSource é o primeiro passo na implementação de vinculação de dados complexa, mas o controle precisa saber quais campos ou propriedades específicas dos dados serão usados como membros de exibição e valor. O membro Display será usado como título do nó da árvore, enquanto o membro Value é acessível através da propriedade Value do nó. Essas propriedades são todas strings que representam nomes de campos ou propriedades e podem ser facilmente adicionadas ao controle:
m_ValueMember privado como string m_DisplayMember privado como string _ Propriedade pública ValueMember() como string Pegar Retornar m_ValueMember Fim Definir (valor ByVal como string) m_ValueMember = Valor Conjunto final Propriedade final _ Propriedade pública DisplayMember() como string Pegar Retornar m_DisplayMember Fim Definir (valor ByVal como string) m_DisplayMember = Valor Conjunto final Fim da propriedade |
Neste TreeView , essas propriedades representarão apenas os membros Display e Value dos nós folha, e as informações correspondentes para cada nível de agrupamento serão especificadas no método AddGroup .
Usando o objeto CurrencyManager
Na propriedade DataSource discutida anteriormente, uma instância da classe CurrencyManager é criada e armazenada em uma variável de nível de classe. A classe CurrencyManager acessada por meio deste objeto é uma parte fundamental da implementação da vinculação de dados porque possui propriedades, métodos e eventos que permitem as seguintes funções:
Recuperar valor do atributo/campo
O objeto CurrencyManager permite recuperar valores de propriedades ou campos, como o valor de um campo DisplayMember ou ValueMember , de um item individual em uma fonte de dados por meio de seu método GetItemProperties . Em seguida, use um objeto PropertyDescriptor para obter o valor de um campo ou propriedade específica em um item de lista específico. O trecho de código a seguir mostra como criar esses objetos PropertyDescriptor e como usar a função GetValue para obter o valor da propriedade de um item na fonte de dados subjacente. Observe a propriedade List do objeto CurrencyManager : ela fornece acesso à instância IList à qual o controle está vinculado:
Dim myNewLeafNode como TreeLeafNode Dim currObject como objeto currObject = cm.List(currentListIndex) Se Me.DisplayMember <> AndAlso Me.ValueMember <> Então 'Adicionar nó folha? Dim pdValue As System.ComponentModel.PropertyDescriptor Dim pdDisplay como System.ComponentModel.PropertyDescriptor pdValue = cm.GetItemProperties()(Me.ValueMember) pdDisplay = cm.GetItemProperties()(Me.DisplayMember) meuNewLeafNode = _ Novo TreeLeafNode(CStr(pdDisplay.GetValue(currObject)), _ objeto atual, _ pdValue.GetValue(currObject), _ índiceListaAtual) |
GetValue ignora o tipo de dados subjacente da propriedade ao retornar um objeto, portanto, o valor de retorno precisa ser convertido antes de usá-lo.
Mantenha os controles vinculados a dados sincronizados
O CurrencyManager possui mais um recurso importante: além de fornecer acesso a fontes de dados vinculadas e propriedades de itens, ele permite que o mesmo DataSource seja usado para coordenar a vinculação de dados entre este controle e qualquer outro controle. Esse suporte pode ser usado para garantir que vários controles vinculados à mesma fonte de dados ao mesmo tempo permaneçam no mesmo item da fonte de dados. Para meu controle, quero ter certeza de que quando um item for selecionado na árvore, todos os outros controles vinculados à mesma fonte de dados estejam apontando para o mesmo item (o mesmo registro, linha ou mesmo array, se você desejar pensar em termos de um banco de dados). Para fazer isso, substituo o método OnAfterSelect no TreeView base. Nesse método (que é chamado após selecionar o nó da árvore), defino a propriedade Position do objeto CurrencyManager como o índice do item atualmente selecionado. O aplicativo de exemplo fornecido com o controle TreeView ilustra como os controles de sincronização facilitam a criação de interfaces de usuário vinculadas a dados. Para facilitar a determinação da posição da lista do item atualmente selecionado, usei uma classe TreeNode personalizada ( TreeLeafNode ou TreeGroupNode ) e armazenei o índice da lista de cada nó na propriedade Position que criei:
Substituições protegidas Sub OnAfterSelect _ (ByVal e As System.Windows.Forms.TreeViewEventArgs) Dim tln como TreeLeafNode Se TypeOf e.Node for TreeGroupNode então tln = FindFirstLeafNode(e.Node) Dim groupArgs como novo groupTreeViewEventArgs(e) RaiseEvent AfterGroupSelect(grupoArgs) ElseIf TypeOf e.Node é TreeLeafNode então Dim leafArgs como novo leafTreeViewEventArgs(e) RaiseEvent AfterLeafSelect(leafArgs) tln = CType(e.Node, TreeLeafNode) Terminar se Se não é nada então Se cm.Position <> tln.Position Então cm.Posição = tln.Posição Terminar se Terminar se MinhaBase.OnAfterSelect(e) Finalizar sub |
No trecho de código anterior, você deve ter notado uma função chamada FindFirstLeafNode , que desejo apresentar brevemente aqui. No meu TreeView , apenas os nós folha (os nós finais da hierarquia) correspondem aos itens no DataSource , todos os outros nós são usados apenas para criar a estrutura de agrupamento. Se eu quiser criar um controle vinculado a dados com bom desempenho, sempre preciso selecionar um item correspondente ao DataSource , portanto, sempre que selecionar um nó de grupo, encontro o primeiro nó folha no grupo, como se Este nó fosse o seleção atual. Você pode conferir o exemplo em ação, mas por enquanto fique à vontade para usá-lo.
Função privada FindFirstLeafNode (ByVal currNode As TreeNode) _ Como TreeLeafNode Se TypeOf currNode for TreeLeafNode, então Retornar CType(currNode, TreeLeafNode) Outro Se currNode.Nodes.Count > 0 Então Retornar FindFirstLeafNode(currNode.Nodes(0)) Outro Não retornar nada Terminar se Terminar se Função final |
Definir a propriedade Position do objeto CurrencyManager sincroniza outros controles com a seleção atual, mas quando a posição de outros controles muda, o CurrencyManager também gera eventos para que a seleção mude de acordo. Para ser um bom componente vinculado a dados, a seleção deve se mover conforme o local da fonte de dados muda e a exibição deve ser atualizada quando os dados de um item são modificados. Existem três eventos gerados por CurrencyManager : CurrentChanged , ItemChanged e PositionChanged . O último evento é bastante simples; um dos propósitos do CurrencyManager é manter um indicador de posição atual para a fonte de dados para que vários controles vinculados possam exibir o mesmo registro ou item de lista, e esse evento seja gerado sempre que a posição for alterada. Os outros dois acontecimentos por vezes sobrepõem-se e a distinção é menos clara. A seguir descrevemos como usar esses eventos em controles personalizados: PositionChanged é um evento relativamente simples e não será descrito aqui quando você deseja ajustar o item atualmente selecionado em um controle complexo vinculado a dados (como Tree), use The; evento. O evento ItemChanged é gerado sempre que um item na fonte de dados é modificado, enquanto CurrentChanged é gerado somente quando o item atual é modificado.
No meu TreeView , descobri que sempre que selecionava um novo item, todos os três eventos eram gerados, então decidi tratar o evento PositionChanged alterando o item atualmente selecionado e não fazer nada com os outros dois. Recomenda-se converter a fonte de dados em IBindingList (se a fonte de dados suportar IBindingList ) e usar o evento ListChanged , mas não implementei essa funcionalidade.
Private Sub cm_PositionChanged (ByVal remetente como objeto, _ ByVal e As System.EventArgs) Lida com cm.PositionChanged Dim tln como TreeLeafNode Se TypeOf Me.SelectedNode for TreeLeafNode, então tln = CType(Me.SelectedNode, TreeLeafNode) Outro tln = FindFirstLeafNode(Me.SelectedNode) Terminar se Se tln.Position <> cm.Position Então Me.SelectedNode = FindNodeByPosition(cm.Position) Terminar se Finalizar sub Função de sobrecargas privadas FindNodeByPosition (índice ByVal como inteiro) _ Como TreeNode Retornar FindNodeByPosition(index, Me.Nodes) Função final Função de sobrecargas privadas FindNodeByPosition (índice ByVal como inteiro, _ ByVal NodesToSearch As TreeNodeCollection) As TreeNode Dim i como inteiro = 0 Dim currNode como TreeNode Dim tln como TreeLeafNode Faça enquanto eu <NodesToSearch.Count currNode = NodesToSearch(i) eu += 1 Se TypeOf currNode for TreeLeafNode, então tln = CType(currNode, TreeLeafNode) Se tln.Position = índice Então Retornar currNode Terminar se Outro currNode = FindNodeByPosition(index, currNode.Nodes) Se não currNode não for nada, então Retornar currNode Terminar se Terminar se Laço Não retornar nada Função final |
Converter DataSource em árvore
Depois de escrever o código de vinculação de dados, posso prosseguir e adicionar o código para gerenciar os níveis de agrupamento, construir a árvore de acordo e, em seguida, adicionar alguns eventos, métodos e propriedades personalizados.
Grupo de gerenciamento
Para configurar uma coleção de grupos, o programador deve criar as funções AddGroup , RemoveGroup e ClearGroups . Sempre que a coleção de grupos é modificada, a árvore deve ser redesenhada (para refletir a nova configuração), então criei um procedimento genérico, GroupingChanged , que pode ser chamado por vários códigos no controle quando as circunstâncias mudam e a árvore precisa ser forçada a ser reconstruído:
TreeGroups privados como novo ArrayList() Public Sub RemoveGroup (grupo ByVal como grupo) Se não for treeGroups.Contains (grupo) então treeGroups.Remove(grupo) Agrupamento alterado() Terminar se Finalizar sub Sub AddGroup de sobrecargas públicas (grupo ByVal como grupo) Tentar treeGroups.Add(grupo) Agrupamento alterado() Pegar Finalizar tentativa Finalizar sub Sobrecargas públicas Sub AddGroup (nome ByVal como String, _ ByVal grupoBy As String, _ ByVal displayMember As String, _ ByVal valorMember As String, _ ByVal imageIndex como inteiro, _ ByVal selecionadoImageIndex como inteiro) Dim myNewGroup As New Group(nome, groupBy, _ displayMember, valorMember, _ imageIndex, selectImageIndex) Me.AddGroup(meuNovoGrupo) Finalizar sub Função Pública GetGroups() As Group() Retornar CType(treeGroups.ToArray(GetType(Group)), Group()) Função final |
árvore geradora
A reconstrução real da árvore é feita por dois processos: BuildTree e AddNodes . Como o código desses dois processos é muito longo, este artigo não lista todos eles, mas tenta resumir seu comportamento (é claro, você pode baixar o código completo, se desejar). Conforme mencionado anteriormente, os programadores podem interagir com esse controle configurando uma série de grupos e, em seguida, usando esses grupos no BuildTree para determinar como configurar os nós da árvore. BuildTree limpa a coleção de nós atual e, em seguida, itera por toda a fonte de dados para processar o agrupamento de primeiro nível (Publisher mencionado no exemplo e no diagrama anterior neste artigo), adicionando um nó para cada valor de agrupamento diferente (usando os dados no exemplo, para cada Adicione um nó com um valor pub_id ) e, em seguida, chame AddNodes para preencher todos os nós no agrupamento de primeiro nível. AddNodes chama a si mesmo recursivamente para lidar com quantos níveis forem necessários, adicionando nós de grupo e nós folha conforme necessário. Use duas classes personalizadas baseadas em TreeNode para distinguir nós de grupo e nós folha e fornecer atributos correspondentes para os dois tipos de nós.
Eventos TreeView personalizados
Sempre que um nó é selecionado, TreeView gera dois eventos: BeforeSelect e AfterSelect . Mas, sob meu controle, eu queria diferenciar os eventos dos nós do grupo e dos nós folha, então adicionei meus próprios eventos BeforeGroupSelect/AfterGroupSelect e BeforeLeafSelect/AfterLeafSelect . Além dos eventos básicos, também acionei uma classe de parâmetro de evento personalizada:
Evento público BeforeGroupSelect _ (ByVal remetente As Object, ByVal e As groupTreeViewCancelEventArgs) Evento público AfterGroupSelect _ (ByVal remetente As Object, ByVal e As groupTreeViewEventArgs) Evento públicoBeforeLeafSelect _ (ByVal remetente As Object, ByVal e As leafTreeViewCancelEventArgs) Evento público AfterLeafSelect _ (ByVal remetente As Object, ByVal e As leafTreeViewEventArgs) Substituições protegidas Sub OnBeforeSelect _ (ByVal e As System.Windows.Forms.TreeViewCancelEventArgs) Se TypeOf e.Node for TreeGroupNode então Dim groupArgs como novo groupTreeViewCancelEventArgs(e) RaiseEvent BeforeGroupSelect(CObj(Me),groupArgs) ElseIf TypeOf e.Node é TreeLeafNode então Dim leafArgs como novo leafTreeViewCancelEventArgs(e) RaiseEvent BeforeLeafSelect(CObj(Me), leafArgs) Terminar se MinhaBase.OnBeforeSelect(e) Finalizar sub Substituições protegidas Sub OnAfterSelect _ (ByVal e As System.Windows.Forms.TreeViewEventArgs) Dim tln como TreeLeafNode Se TypeOf e.Node for TreeGroupNode então tln = FindFirstLeafNode(e.Node) Dim groupArgs como novo groupTreeViewEventArgs(e) RaiseEvent AfterGroupSelect(CObj(Me),groupArgs) ElseIf TypeOf e.Node é TreeLeafNode então Dim leafArgs como novo leafTreeViewEventArgs(e) RaiseEvent AfterLeafSelect(CObj(Me), leafArgs) tln = CType(e.Node, TreeLeafNode) Terminar se Se não é nada então Se cm.Position <> tln.Position Então cm.Posição = tln.Posição Terminar se Terminar se MinhaBase.OnAfterSelect(e) Finalizar sub |
Classes de nós customizados ( TreeLeafNode e TreeGroupNode ) e classes de parâmetros de eventos customizados estão incluídas no código para download.
Exemplo de aplicação
Para compreender completamente todo o código neste controle de exemplo, você deve entender como ele funciona em seu aplicativo. O aplicativo de exemplo incluído usa o banco de dados pubs.mdb do Access e ilustra como o controle Tree pode ser usado com outros controles vinculados a dados para criar aplicativos do Windows. Os principais recursos dignos de nota neste caso incluem a sincronização da árvore com outros controles vinculados e a seleção automática de nós da árvore ao realizar uma pesquisa na fonte de dados.
Nota: Este aplicativo de amostra (denominado TheSample) está incluído no download deste artigo.
Figura 4: Aplicativo de demonstração para TreeView vinculado a dados
resumo
O controle Tree vinculado a dados descrito neste artigo não é adequado para todos os projetos que exigem um controle Tree para exibir informações do banco de dados, mas introduz um método para personalizar o controle para fins pessoais. Lembre-se de que qualquer controle complexo vinculado a dados que você deseja criar terá praticamente o mesmo código que o controle Árvore e você poderá simplificar o desenvolvimento futuro do controle modificando o código existente.