Démarrer avec les Winforms (partie 2) en C++/CLI avec Visual Studio 2005

Nico-pyright(c)

À travers ce tutoriel, vous trouverez la suite de l'introduction à la programmation des Winforms grâce au framework .net 2.0 à travers l'utilisation du langage C++/CLI avec Visual C++ 2005. Vous y découvrirez l'utilisation de certains contrôles des Winforms. ♪

- Titre : Démarrer avec les Winforms (partie 2) en C++/CLI avec Visual Studio 2005

- Auteur : Nico-pyright(c)

- Parution : 12 juin 2006

- Licence : Les sources présentées sur cette page sont libres de droits et vous pouvez les utiliser à votre convenance. Par contre, la page de présentation constitue une œuvre intellectuelle protégée par les droits d'auteur. Copyright ® 2006 Nico-pyright(c). Aucune reproduction, même partielle, ne peut être faite de ce site et de l'ensemble de son contenu : textes, documents, images, etc. sans l'autorisation expresse de l'auteur. Sinon vous encourez selon la loi jusqu'à trois ans de prison et jusqu'à 300 000 € de dommages et intérêts.

Sommaire

- I. Introduction

- II. Les menus

- II-A. Le menu de la fenêtre

- II-B. Le menu contextuel

- III. Le contrôle NotifyIcon

- IV. Le contrôle ToolTip

- V. Le contrôle ToolStrip

- VI. Les boites de dialogues standards

- VI-A. La boite de sélection d'un répertoire

- VI-B. Les boites d'ouvertures et de sauvegardes de fichiers

- VI-C. Les autres boites de dialogues

- VII. Les Timers

- VIII. Les ImageList

- IX. Les tabControl

- X. Les splitContainer

- XI. Les StatusStrip

- XII. Implémenter un drag and drop

- XIII. Conclusion

- XIV. Remerciements

- XV. Contact

I - Introduction

Vous êtes un développeur, vous avez des bases dans le langage C/C++ et vous souhaitez acquérir des connaissances en programmation Windows dans le domaine du développement d'interfaces homme-machine en .Net grâce au framework 2.0 et le C++/CLI ?
Alors ce tutoriel est pour vous.

Vous trouverez dans ce cours la suite de l'introduction à la programmation des Winforms (Windows Forms). Je vous invite à consulter le tutoriel Démarrer avec les winforms en préambule.

Je vais vous présenter dans ce tutoriel certains contrôles en rentrant un peu plus dans les détails.

À travers ce tutoriel, j'utiliserai le logiciel Visual Studio 2005 de Microsoft et plus particulièrement Visual C++ 2005 ainsi que le framework dotnet 2.0.

II - Les menus

II-A - Le menu de la fenêtre

Pour créer un menu, rien de plus simple : faites glisser le composant Menustrip dans la fenêtre.
Vous pouvez constater qu'une barre s'est ajoutée, dans laquelle vous allez pouvoir écrire directement le nom de vos menus.

Remplissez le contenu comme il vous convient.
Il est possible d'associer une image à un menu : dans l'éditeur de la propriété Items, il suffit de renseigner la propriété Image.

Notez que ce qui est applicable pour un menu l'est aussi pou un sous-menu. Pour y accéder, il faut alors rentrer dans l'éditeur de propriétés de DropDownItems.

Pour être averti d'un clic sur le menu, il faut intercepter l'événement ItemClicked. Pour intercepter un clic sur un sous-menu, il faut intercepter de la même façon l'événement DropDownItemClicked, ces événements devant être interceptés pour chaque composant MenuStrip et ToolStripMenuItem qui compose le menu. Ceci implique donc d'avoir une fonction qui intercepte l'événement par élément à intercepter.

Remarque : Il est possible d'utiliser une seule fonction pour intercepter tous les événements dans la mesure où le prototype de la fonction est le même à chaque fois. Il faut dans ce cas modifier par code chaque ajout de handler pour tous les objets composant le menu. On s'en réfère à l'objet ToolStripItemClickedEventArgs et à la propriété ClickedItem pour identifier sur quel élément on a cliqué.

Un menu dispose des propriétés Checked et RadioCheck qui permettent de cocher un élément de menu.
On peut égaler associer facilement un raccourci grâce à la propriété Shortcut ; celui-ci pouvant être affiché ou non grâce à la propriété ShowShortCut.

L'ajout d'un shortcut par code se fait ainsi (ici Control + B) :

menuItem->ShortcutKeys = static_cast<Keys>(Keys::Control | Keys::B);

On peut noter une propriété intéressante : LayoutStyle, qui permet par exemple d'orienter le menu verticalement.

II-B - Le menu contextuel

Je vais vous montrer dans ce paragraphe comment créer un menu contextuel par code.
On utilise les classes ContextMenu et MenuItem.

On va faire un menu contextuel contenant deux éléments (ouvrir et supprimer), on l'affichera lors du clic droit sur un listview par exemple.

On déclare trois variables, un ContextMenu, et deux MenuItem :

ContextMenu ^ monMenu;
MenuItem ^ eltOuvrir;
MenuItem ^ eltSupprimer;

Dans le constructeur de la form, on initialise le menu et on déclare les deux fonctions qui vont permettre de réceptionner les clics sur les éléments du menu :

monMenu = gcnew System::Windows::Forms::ContextMenu();
eltOuvrir = gcnew MenuItem("Ouvrir");
eltOuvrir->Click += gcnew EventHandler(this, &Form1::OnMenuOuvrir);
eltSupprimer = gcnew MenuItem("Supprimer");
eltSupprimer->Click += gcnew EventHandler(this, &Form1::OnMenuSupprimer);

...

private: System::Void OnMenuOuvrir(Object ^sender, EventArgs ^e)
{
MessageBox::Show("Ouverture");
}
private: System::Void OnMenuSupprimer(Object ^sender, EventArgs ^e)
{
MessageBox::Show("Suppression");
}

On peut alors afficher le menu :

if (e->Button == System::Windows::Forms::MouseButtons::Right)
{
monMenu->MenuItems->AddRange(gcnew array<MenuItem ^> {eltOuvrir, eltSupprimer});
monMenu->Show(listView1, e->Location);
}

III - Le contrôle NotifyIcon

Il s'agit d'un contrôle très connu, plus ou moins lié au contrôle menu. Ce contrôle sert à avoir une icône dans la barre des tâches (généralement utilisée pour réduire l'application), de pouvoir agir dessus par un menu contextuel, ou d'obtenir une information grâce à une bulle.

Son utilisation est très aisée. Ajoutez un contrôle NotifyIcon sur votre form. Dans les propriétés du contrôle, renseignez la propriété Icon qui correspondra à l'icône que nous verrons affichée dans la barre des tâches.
Et pour afficher une bulle comme ci-dessus, il suffit d'appeler la méthode ShowBalloonTip :

notifyIcon1->ShowBalloonTip(2000, "titre", "text", ToolTipIcon::Info);

Le dernier paramètre correspond à l'icône qui sera affichée dans la bulle. On la trouve dans une énumération qui contient entre autres : Error, Info, None et Warning.

On peut ensuite ajouter très facilement un menu contextuel qui s'affichera automatiquement lors d'un clic droit sur l'icône dans la barre des tâches.
Pour ce faire, il suffit de déposer sur la form un contrôle ContextMenuStrip. Un menu apparaît sur la form que l'on peut soit remplir directement, soit depuis la fenêtre de propriétés.

Il ne reste plus qu'à implémenter l'événement ItemClicked et à différencier nos éléments de menu, comme ceci par exemple :

private: System::Void contextMenuStrip1_ItemClicked(System::Object^ sender,
System::Windows::Forms::ToolStripItemClickedEventArgs^ e)
{
if (e->ClickedItem->Text == "Ouvrir")
MessageBox::Show("Ouvrir");
else
MessageBox::Show("Quitter");
}

Remarque : toutes les propriétés classiques d'un ContextMenuStrip sont applicables, par exemple la possibilité de mettre des images dans le menu.

IV - Le contrôle ToolTip

Le contrôle ToolTip correspond à la bulle d'aide bien connue que l'on peut apercevoir sur un bouton par exemple lorsqu'on laisse le curseur de la souris immobile dessus pendant 1 à 2 secondes.
Il s'utilise ici encore très facilement. Imaginons que je veuille avoir une bulle d'aide sur un bouton. En déposant un contrôle ToolTip sur la form, il s'ajoute automatiquement une propriété supplémentaire à mon bouton dans la fenêtre des propriétés.
Elle s'appelle ToolTip on toolTip1, toolTip1 étant le nom par défaut de mon contrôle ToolTip déposé.

On renseigne dans cette propriété le texte que l'on souhaite voir affiché, et le tour est joué.

Ici, la photo d'écran ne prend pas le curseur de la souris, il est bien sûr positionné sur le bouton.

Une autre méthode pour implémenter une bulle d'aide est d'intercepter l'événement MouseHover du bouton, et d'utiliser la méthode Show d'un objet ToolTip. Cette méthode prend le texte en paramètre ainsi que le contrôle sur lequel va se mettre la bulle d'aide.

monToolTip->Show("Le message en question", this->button1);

V - Le contrôle ToolStrip

Voici encore un contrôle très facile à utiliser grâce à l'IDE Visual Studio 2005. On peut grâce à celui-ci créer une barre d'outils entièrement personnalisable, avec des boutons (avec image), du texte, des zones de saisies, des combo box, des menus, et j'en passe… En termes d'objets, ces contrôles sont des ToolStripButton, des ToolStripLabels, etc.
Tout ce que vous avez à faire, est de déposer le contrôle ToolStrip sur la form et à personnaliser votre barre directement sur la form, comme ci-dessous :

Évidemment, on peut associer des événements à chacun des contrôles ajoutés grâce à l'IDE, comme l'événement Click, ou TextChanged pour un contrôle éditable.
Encore une fois, l'IDE nous fournit une aide considérable pour créer ce genre de composant qui s'avérerait plus laborieux à créer dynamiquement.

Pour créer une barre d'outils dynamiquement, il faut déjà créer les contrôles un par un.

myToolStripComboBox->Items->AddRange(
gcnew cli::array< System::Object^  >(3) {"Elt 1", "Elt 2", "elt 3"});
myToolStripComboBox->toolStripComboBox1->Name = "Ma Combo";
myToolStripComboBox->toolStripComboBox1->Size = System::Drawing::Size(121, 25);

Puis utiliser la méthode AddRange (ou Add) de l'objet ToolStrip pour ajouter les contrôles.

monToolStrip->Items->AddRange(
gcnew cli::array< System::Windows::Forms::ToolStripItem^  >(2)
{monToolStripButton, monToolStripLabel});

VI - Les boites de dialogues standards

VI-A - La boite de sélection d'un répertoire

On utilise le contrôle FolderBrowserDialog pour faire une sélection d'un répertoire.
On peut lui faire choisir un emplacement racine avec la propriété RootFolder et récupérer le répertoire sélectionné avec SelectedPath.
On peut aussi optionnellement permettre la création de répertoires avec la propriété booléenne ShowNewFolderButton.

folderBrowserDialog1->Description = "Choisissez le répertoire";
folderBrowserDialog1->ShowNewFolderButton = true;
folderBrowserDialog1->RootFolder = Environment::SpecialFolder::Desktop;
folderBrowserDialog1->ShowDialog();
if (folderBrowserDialog1->SelectedPath != String::Empty)
MessageBox::Show("Répertoire sélectionné : " + folderBrowserDialog1->SelectedPath);

VI-B - Les boites d'ouvertures et de sauvegardes de fichiers

Les contrôles OpenFileDialog et SaveFileDialog fonctionnent grosso modo de la même façon. Ils permettent respectivement l'ouverture d'une boite de dialogue pour effectuer la lecture ou l'écriture d'un fichier.
On peut bien sûr définir une extension par défaut avec la propriété DefaultExt ainsi qu'un filtre sur une ou plusieurs extensions avec la propriété Filter.
Il existe aussi quelques propriétés intéressantes qui permettent l'apparition d'un warning si par exemple le fichier ou le répertoire n'existe pas, avec CheckFileExists ou CheckPathExists. On peut aussi autoriser la sélection multiple avec Multiselect.

openFileDialog1->Title = "Sélectionnez un fichier";
openFileDialog1->FileName = nullptr;
openFileDialog1->DefaultExt = "txt";
openFileDialog1->Filter = "Fichiers Texte (*.txt)|*.txt|Tous (*.*)|*.*";
openFileDialog1->CheckFileExists = true;
if (openFileDialog1->ShowDialog() == System::Windows::Forms::DialogResult::OK)
{
Stream ^s = openFileDialog1->OpenFile();
// ...
}

Remarque : Ici, la fonction OpenFile utilise le fichier sélectionné dans la propriété FileName. De même, cette propriété a été initialisée à nullptr pour ne pas avoir de nom prédéfini dans la boite de dialogue.

VI-C - Les autres boites de dialogues

Sans rentrer dans les détails, on peut citer :

- la boite de dialogue PrintDialog qui permet d'ouvrir la boite d'impression ;

- la boite de dialogue ColorDialog qui permet d'ouvrir une boite de sélection de couleur ;

- la boite de dialogue FontDialog qui permet d'ouvrir une boite de sélection de fontes.

VII - Les Timers

Le contrôle Timer est très pratique. Il permet d'exécuter une fonction à intervalle de temps régulier.
On définit un intervalle de temps en millisecondes grâce à la propriété Interval, et on démarre le Timer avec la méthode Start().
Il suffit alors d'implémenter l'événement Tick du Timer pour exécuter la fonction toutes les x millisecondes, défini dans l'intervalle de temps. À tout moment on peut arrêter le timer grâce à la méthode Stop().
Cet exemple nous permet de mettre à jour un textbox avec l'heure toutes les secondes.

private: System::Void Form1_Load(System::Object^  sender, System::EventArgs^  e)
{
timer1->Interval = 1000;
timer1->Start();
}

private: System::Void timer1_Tick(System::Object^  sender, System::EventArgs^  e)
{
textBox1->Text = Convert::ToString(System::DateTime::Now);
}

Remarque : la fonction est exécutée uniquement si l'application n'est pas bloquée à faire autre chose et peut recevoir la notification du Timer. Si ce n'est pas le cas, l'événement sera différé jusqu'à ce que le programme reprenne la main et puisse traiter l'événement Timer. (Ce qui veut dire que la fonction n'est pas exécutée dans un thread indépendant).

VIII - Les ImageList

Un contrôle ImageList est utilisé pour stocker des images. Son intérêt, par rapport à stocker directement le fichier en ressources, est sa facilité d'utilisation ainsi que son intégration avec d'autres contrôles (ListView, TreeView, Toolbar…).
Déposez un contrôle ImageList sur la form et utilisez la propriété Images pour ajouter des images. N'oubliez pas de renseigner la propriété Name de chaque image ajoutée, afin d'y accéder facilement par la suite.
On peut alors accéder à une image très facilement, comme cet exemple qui permet d'afficher l'image dans un pictureBox.

pictureBox1->Image = imageList1->Images["arbre"];

Comme je le disais, outre cette accessibilité évidente, un autre intérêt est son interaction avec d'autres contrôles, par exemple le listView.
Je peux lui associer directement dans l'éditeur de propriété, par exemple dans la propriété largeImageList le contrôle ImageList que nous avons créé et qui contient nos images.

Dès lors, je pourrais ajouter un élément dans la propriété Items et associer directement une image en utilisant la propriété ImageKey ou ImageIndex, comme ci-dessous :

IX - Les tabControl

Le contrôle tabControl permet de créer ce que l'on appelle communément des onglets.
Il s'agit d'un contrôle très simple à utiliser. Déposez le contrôle sur la form et ajoutez les contrôles directement sur chacune des pages des onglets. On ajoute un onglet grâce à la propriété tabPages.
On peut contrôler le positionnement des onglets grâce à la propriété alignment. Ainsi, il est possible de les positionner en haut, à droite, à gauche ou en bas.

On peut rajouter un ToolTip sur l'intitulé de l'onglet grâce à la propriété ToolTip. On peut aussi rajouter une image dans l'onglet en utilisant un contrôle ImageList associé à la propriété ImageList. Il suffit alors d'utiliser la propriété ImageIndex ou ImageKey pour aller récupérer les images du contrôle ImageList.

On utilise l'événement SelectedIndexChanged pour détecter un changement d'onglet, par un clic dessus. On identifie sur quel onglet on se trouve grâce à l'objet Sender, que l'on caste en TabControl et en utilisant les propriétés SelectedTab et SelectedIndex :

private: System::Void tabControl1_SelectedIndexChanged(System::Object^  sender, System::EventArgs^  e)
{
TabControl ^tab = safe_cast<TabControl ^>(sender);
MessageBox::Show("Je suis sur l'onglet " + tab->SelectedTab->Text +
"(index : " + Convert::ToString(tab->SelectedIndex) + ")");
}

X - Les splitContainer

On peut utiliser le contrôle splitContainer pour créer un splitter, qui permet de partager une fenêtre en deux parties redimensionnables.
Sur l'image ci-dessous, on observe deux splitContainer, le deuxième étant contenu dans la partie droite du premier.

On constate ici que l'on peut changer l'orientation du splitter. Ceci est faisable grâce à la propriété Orientation. J'ai aussi positionné la propriété BorderStyle à Fixed3D. Il existe d'autres styles pour cette propriété (FixedSingle et None). Je trouve cependant que Fixed3D est le style qui se voit le plus et qui facilite le redimensionnement.
Il est possible de définir l'épaisseur de la barre qui sert au redimensionnement grâce à la propriété SplitterWidth. Si l'on veut fixer la barre, on utilise alors la propriété IsSplitterFixed.

Le splitContainer est composé de deux panels. Il s'agit des contrôles panels standards (System::Windows::Forms::Panel) qui se comportent donc de manière standard. Cela veut dire qu'ils se comportent en tant que container de contrôles et donc que l'on peut ajouter des contrôles dedans, comme le button ou le textbox que l'on voit sur l'image.

On intercepte le redimensionnement d'un splitter grâce à l'événement SplitterMoved. L'argument SplitterEventArgs nous permet d'obtenir des renseignements sur le redimensionnement. Notamment les attributs X et Y qui nous permettent de connaître la taille des nouveaux panels.

XI - Les StatusStrip

Le contrôle StatusStrip représente la barre d'état que l'on peut voir dans certaines applications. Elle est généralement ancrée dans la partie basse d'une fenêtre.

De la même façon que pour le contrôle ToolStrip, il est possible de rajouter des contrôles dans la barre. La différence ici est que l'on peut ajouter uniquement des labels, des progressbar, des dropdownbuttons ou des splitbuttons.

On utilise la propriété Dock avec les valeurs Bottom, Left, Right ou Top pour ancrer la barre respectivement en bas, à gauche, à droite ou en haut de la fenêtre.
On utilise la propriété RightToLeft si l'on veut voir commencer à s'afficher les contrôles depuis la droite de la barre. On peut également changer le style de la barre avec la propriété RenderMode, le style par défaut étant le style System. Une autre propriété intéressante est la possibilité de choisir l'orientation du texte avec la propriété TextDirection. Il peut ainsi être Horizontal, Vertical90 ou Vertical270.

Comme d'autres contrôles, il est facile d'associer un menu contextuel grâce à la propriété ContextMenuStrip.

XII - Implémenter un drag and drop

Je vais vous montrer dans ce paragraphe comment implémenter un drag and drop entre deux contrôles PictureBox sur une form, qui va permettre l'échange des contenus des images.
Faites glisser deux pictureBox et choisissez une image par PictureBox. Autorisez la réception de données « déposées », en mettant à true la propriété AllowDrop (ceci ne pouvant se faire que par code, dans le form_load par exemple).

pictureBox1->AllowDrop = true;
pictureBox2->AllowDrop = true;

Générez les événements DragDrop, DragEnter et MouseMove.
Dans le mouseMove de l'image source (ici pictureBox1), on envoie l'image en drag and drop :

if (e->Button == System::Windows::Forms::MouseButtons::Left)
pictureBox1->DoDragDrop(pictureBox1->Image, DragDropEffects::All );

Dans le DragEnter de l'image destination (ici pictureBox2), on vérifie que ce qui est envoyé est bien une image, dans ce cas, on effectue une copie :

if (e->Data->GetDataPresent(DataFormats::Bitmap))
e->Effect = DragDropEffects::Copy;
else
e->Effect = DragDropEffects::None;

Enfin, on vient récupérer l'image envoyée dans le dragDrop de l'image destination, et on intervertit les images :

if ( e->Data->GetDataPresent(DataFormats::Bitmap))
{
pictureBox1->Image = pictureBox2->Image;
pictureBox2->Image = safe_cast<Image ^>(e->Data->GetData(DataFormats::Bitmap));
}

Il ne reste plus qu'à faire la même chose pour gérer le drag and drop de l'image 2 vers l'image 1.

Remarque : ceci est une méthode appliquée aux images, en effet, on contrôle la validité de l'envoi avec le type DataFormats::Bitmap.
Il est bien sûr possible de faire transiter autre chose que des images entre des contrôles, par exemple du texte. Il suffirait d'utiliser le type DataFormats::Text.

Là où cela se corse, c'est lorsqu'on veut faire transiter des objets personnels. Ceci est quand même possible en changeant un peu la méthode de réception.
Imaginons que nous voulions envoyer un objet MonObjet.
Il faut commencer par envoyer l'objet en lui-même. (Dans mon exemple, j'ai stocké mon objet MonObjet dans la propriété Tag de la pictureBox. C'est lui que je vais envoyer.)

private: System::Void PictureMouseMove(System::Object^  sender, MouseEventArgs^  e)
{
if (e->Button == System::Windows::Forms::MouseButtons::Left)
safe_cast<PictureBox ^>(sender)->DoDragDrop(
safe_cast<MonObjet^>(safe_cast<PictureBox ^>
(sender)->Tag), DragDropEffects::All );
}

Ensuite, on ne peut pas tester le format de l'objet envoyé, comme c'était le cas pour un Bitmap. Une solution consiste à parcourir un tableau des formats disponibles et de le comparer à notre objet passé.

private: System::Void PictureDragEnter(System::Object^  sender, DragEventArgs^  e)
{
array<String ^> ^formats = e->Data->GetFormats();
if (formats->Length >0)
if (formats[0] == "MonObjet")
e->Effect = DragDropEffects::Copy;
else
e->Effect = DragDropEffects::None;
else
e->Effect = DragDropEffects::None;
}

Enfin, il ne reste plus qu'à récupérer l'objet envoyé et éventuellement l'objet d'origine.

private: System::Void PictureDragDrop(System::Object^  sender, DragEventArgs^  e)
{
MonObjet ^org = safe_cast<MonObjet^>(safe_cast<PictureBox ^>(sender)->Tag);
MonObjet ^dest = safe_cast<MonObjet^>(e->Data->GetData("MonObjet"));
// ...
}

Pour illustrer cette méthode, j'ai élaboré un petit jeu très connu, dont le principe est de réorganiser une image dont les morceaux se sont mélangés en faisant glisser une case vide. (Ce jeu s'appelle dans mon téléphone « image magique », mais peut-être a-t-il un vrai nom …). Vous pouvez télécharger les sources ainsi qu'une version compilée : Télécharger ici (55 Ko).

XIII - Conclusion

Voilà, à travers ce tutoriel vous avez pu découvrir un peu plus précisément certains contrôles des winforms.
Bien sûr, je n'ai pas répertorié toutes les propriétés de chaque contrôle, mais j'espère vous avoir permis de démarrer un peu plus sereinement et j'espère vous avoir aussi donné envie d'aller plus loin dans la découverte du monde merveilleux des Winforms.

XIV - Remerciements

Je remercie toute l'équipe C++ et Dotnet pour leur relecture attentive du document.

XV - Contact

Si vous constatez une erreur dans le tutoriel, dans le source, dans la programmation ou pour toutes informations, n'hésitez pas à me contacter par mail, ou par le forum.