2009-04-16 9 views
1

Je suis en train d'écrire un simple contrôle personnalisé de ListBox dessiné par le propriétaire et de me taper la tête contre le mur en essayant de comprendre comment implémenter ce qui me semble être une fonction directe. Mon ListBox personnalisé est censé fonctionner de manière similaire à la liste "Ajout/Suppression de programmes" dans Windows XP. Autrement dit, il affiche une liste d'éléments comme d'habitude, mais lorsque l'utilisateur sélectionne un élément dans la liste, un bouton cliquable doit apparaître à côté du texte de l'élément. Dans mon cas, j'essaie d'afficher un bouton "Importer" à côté de chaque article dans mon ListBox. Afin de garder le ListBox personnalisé encapsulé, j'essaye d'afficher le bouton en remplaçant la méthode ListBox.OnDrawItem (ie le bouton fait conceptuellement partie de l'élément de la zone de liste), mais je n'arrive pas à le faire fonctionner tout à fait raison.Quelle est la meilleure façon de dessiner des contrôles "enfants" (tels que des boutons) dans chaque élément d'une boîte aux lettres WinForms?

Je dois noter que j'essaie d'utiliser un seul bouton pour l'ensemble ListBox. Lorsque l'utilisateur sélectionne un élément dans la liste, la méthode OnDrawItem ne fait que repositionner ce bouton unique pour qu'il apparaisse à côté de l'élément sélectionné. Je l'ai 99% de travail: le problème est maintenant que lorsque le ListBox défile et que l'élément sélectionné sort de l'écran, le bouton est toujours dessiné à sa position précédente, de sorte qu'il dessine au-dessus du mauvais élément. Je suppose que c'est parce que Windows n'essaiera pas de redessiner l'élément sélectionné s'il est hors écran, et donc le code de repositionnement ne sera pas appelé.

Voici la version coupé vers le bas de ce que j'ai en ce moment:

public partial class EventListBox : ListBox 
{ 
    private Button _importButton; 

    public EventListBox() 
    { 
     InitializeComponent(); 
     this.DrawMode = DrawMode.OwnerDrawVariable; 

    // set up the button that will appear next to the currently-selected item 
     _importButton = new Button(); 
     _importButton.Text = "Import..."; 
     _importButton.AutoSize = true; 
     _importButton.Visible = false; 

    // Add the button as a child control of this ListBox 
     Controls.Add(_importButton); 
    } 


    protected override void OnDrawItem(DrawItemEventArgs e) 
    { 
     if (this.Items.Count > 0) 
     { 
      e.DrawBackground(); 

    // draw item here (omitted) 

    // if drawing the selected item, re-position the "Import" button so that appears 
    // inside the current item, and make it visible if it is hidden. 
    // These checks prevent the resulting repaint that will occur from causing an infinite loop 

    // The problem seems to be that if the ListBox is scrolled such that the selected item 
    // moves off-screen , this code won't run, because it won't repaint the selected item anymore... 
    // This means the button will be painted in its previous position. 

    // The real question is: Is there a better way to approach the whole notion of 
    // rendering buttons within ListBox items? 

      if (e.State & DrawItemState.Selected == DrawItemState.Selected) 
      { 
       _importButton.Top = e.Bounds.Bottom - _importButton.Height - 20; 
       _importButton.Left = e.Bounds.Left; 
       if(!_importButton.Visible) _importButton.Visible = true; 
      } 
     } 

     base.OnDrawItem(e); 
    } 

    protected override void OnMeasureItem(MeasureItemEventArgs e) 
    { 
     base.OnMeasureItem(e); 
     e.ItemHeight = 100; //hard-coded for now... 
    } 

} 

Justification de l'aide d'un seul bouton

Je préférerais créer un bouton distinct pour chaque élément dans la ListBox, mais je ne trouve aucun moyen de savoir quand des éléments sont ajoutés/supprimés de la liste. Je n'ai trouvé aucune méthode pertinente ListBox que je pourrais remplacer, et je ne peux pas réaffecter la propriété ListBox.Items à un objet de collection personnalisé, puisque ListBox.Items est en lecture seule. Pour cette raison, je suis allé avec l'approche ci-dessus d'utiliser un seul bouton et le repositionnement au besoin, mais comme je l'ai mentionné, ce n'est pas très robuste et se casse facilement. Je pense actuellement qu'il serait plus judicieux de créer un nouveau bouton au moment où de nouveaux éléments sont ajoutés au ListBox, et de supprimer les boutons lorsque les éléments ont été supprimés.

Voici quelques solutions que j'ai trouvées, mais existe-t-il une meilleure façon de mettre en œuvre cela?

  • je pouvais créer mes propres AddItem et méthodes RemoveItem directement sur ma classe dérivée ListBox. Je pourrais créer un bouton correspondant chaque fois qu'un nouvel article est ajouté, et enlever le bouton de l'article dans RemoveItem. Cependant, pour moi, c'est un hack moche, car il me force à appeler ces méthodes spéciales d'ajout/suppression au lieu d'utiliser simplement ListBox.Items.
  • Je pourrais dessiner un nouveau bouton manuellement en OnDrawItem en utilisant System.Windows.Forms.ButtonRenderer, mais alors je dois faire beaucoup de travail supplémentaire pour le faire agir comme un vrai bouton, puisque ButtonRenderer ne fait rien de plus que dessiner le bouton. Déterminer quand l'utilisateur survole ce "bouton", et quand il est cliqué, semble qu'il serait difficile d'obtenir raison.
  • Lorsque OnDrawItem est appelée, je peux créer un nouveau bouton si l'élément en cours de dessin n'a pas encore de bouton associé (je pourrais garder une trace de ceci avec un Dictionary<Item, Button>), mais j'ai encore besoin d'un moyen de supprimer inutilisé boutons lorsque leurs éléments correspondants sont supprimés de la liste. Je suppose que je pourrais itérer sur mon dictionnaire de mappages de bouton d'article et supprimer des éléments qui n'existent plus dans le ListBox, mais alors je suis itérer sur deux listes chaque fois qu'un élément dans le ListBox est redessiné (ack!).

Alors, existe-t-il un meilleur moyen d'inclure des boutons cliquables à l'intérieur d'un ListBox? C'est évidemment déjà fait, mais je ne trouve rien d'utile sur Google. Les seuls exemples que j'ai vus qui ont des boutons dans un ListBox étaient des exemples WPF, mais je cherche comment faire avec WinForms.

Répondre

0

Une solution possible

J'ai trouvé une solution viable, et a fini par garder l'approche unique bouton pour l'instant. Je vais poster ma solution de contournement ici, mais si quelqu'un a une réponse plus élégante à ma question initiale, ne hésitez pas à l'afficher.

Ma petite épiphanie

Après avoir expérimenté un peu plus, il semble que la question du bouton ne se repositionne correctement sur le défilement ne se produit que lorsque vous utilisez la molette de la souris pour faire défiler les ListBox. Faire défiler la manière "normale" ne semble pas reproduire le comportement, mais comme je ne pouvais pas être sûr à 100%, j'ai aussi inclus un correctif pour le défilement normal dans ma solution de contournement.

Mon bidouille horrible d'une solution de contournement

Depuis que je savais que la molette de la souris (et le défilement en général) semblait être au cœur de la question, j'ai décidé d'invalider juste mon ListBox chaque fois que le ListBox défile ou chaque fois que la molette de la souris est déplacée. Cela produit un scintillement disgracieux, dont je voudrais me débarrasser, mais je peux vivre avec les résultats pour l'instant.

J'ai ajouté les méthodes suivantes à ma dérivée EventListBox classe:

protected override void WndProc(ref Message m) 
    { 
     const int WM_VSCROLL = 277; 

     if (m.Msg == WM_VSCROLL) 
     { 
      this.Invalidate(); 
     } 

     base.WndProc(ref m); 
    } 

    protected override void OnMouseWheel(MouseEventArgs e) 
    { 
     this.Invalidate(); 
     base.OnMouseWheel(e); 
    } 

j'étais un peu surpris que ListBox ne possèdes pas de ScrollableControl et il n'y avait pas quelque chose comme une méthode OnScroll que je pouvais passer outre, donc j'ai fait la vérification de défilement en remplaçant la méthode WndProc. La détection de la molette de la souris était plus simple, car il existait déjà une méthode à prioriser. Comme je l'ai dit, c'est moins qu'idéal car l'invalidation de la liste à chaque fois que la liste défile provoque un scintillement, et je soupçonne que les performances se dégraderaient de manière significative quand la listbox contient beaucoup d'éléments.

Je n'accepterai pas encore cette réponse, car je suis curieux de voir si quelqu'un a une meilleure solution.

Questions connexes