Mémo C#

Michel Llibre Juin 2025

  1. 1 Table des matières 

1 Directives

2 Projets C#

3 Présentation de hello.cs

4 Structure du programme

4.1 Tour des différents types

4.1.1 Les Types valeur

1.1.1.1 Types intégrés ou primitifs

1.1.1.2 Types enum

1.1.1.3 Types struct

1.1.1.4 Types valeur Nullable

4.1.2 Les Types objets et les Types référence.

1.1.1.5 Types classe

1.1.1.6 Types interface

1.1.1.7 Types de tableaux

1.1.1.8 Types délégués

4.2 Le type object

4.3 Mot clé var : inférence de type

4.4 Les types simples

4.4.1 Domaines des valeurs

4.4.2 Valeurs littérales

4.4.3 Priorité des opérateurs

4.4.4 Transtypage

4.5 objets instance de classe

4.6 La classe string

4.6.1 @Chaines littérales

4.6.2 $chaine interpolée et Formatage

4.6.3 Conversions nombre - string - nombre

4.6.4 Comparaison de string

4.7 Le type tableau

4.8 Les tuples

4.9 Le type enum

4.10 Attibut const

4.11 Modificateurs d'accès

4.12 Opérateurs d'accès

4.13 Opérateur ? accès avec nullité conditionnelle

4.14 Opérateur ? ? fusion de valeurs nulles

4.15 opérateurs arithmétiques et logiques

5 Fonctions

5.1 Argument passé en mode : ref

5.2 Argument passé en mode : out

5.3 Nombre d'arguments variables : préfixe params

5.4 Arguments facultatifs prédéfinis ( = valeur)

5.5 Arguments nommés

5.6 Extension de classe (préfixe this)

6 Fonction mathématiques

7 Structures de controle

8 Spécificités du C#

8.1 Opérateurs typeof, is, as

8.1.1 typeof : Information sur un élément : classe Type

8.1.2 is : comparaison de types

8.1.3 as : test et conversion de type

8.2 unsafe

8.3 foreach

8.4 yield

8.5 throw et try

8.6 Checked et unchecked

8.7 lock

8.8 using

8.9 ref et out

9 Class

9.1 Accesseurs get et set

9.2 Extensions statiques ultérieures

10 Délégués, opérateur Lambda et Événements

10.1 Délégué

10.2 Délégués multifontions

10.3 Délégués prédéfinis Func, Action et Predicate

10.3.1 Func<Targs...,Tret>

10.3.2 Action<>

10.3.3 Predicate<>

10.4 Délégué anonyme

10.5 Opérateur =>

10.5.1 Opérateur Lambda

10.5.2 Corps d'expression

10.6 Résumé syntaxes

10.7 Événements

10.8 Methode Invoke

10.9 MethodInvoker

11 Entrées-Sorties

11.1 Clavier - Ecran

11.2 Formatage

11.2.1 Formatage DateTime (tiré de ??)

11.2.2 Formatage DateTime avec Tostring()

11.3 Fichier

12 Un peu de tout

12.1 Main

12.2 Callback associée à un widget

12.3 MessageBox

12.4 ToolTip

12.5 Exit

12.6 Méthodes usuelles des objets

13 Stopwatch

14 Environment (Classe)

15 Les Windows Form

15.1 Référence utilisées

15.2 Windows Form à partir de 0 sans Designer

15.3 Avec le Designer

15.3.1 Propriéts essentielles des widgets/controles

15.3.2 Callbacks associées aux événements

15.3.3 Evénements de gestion de la Form

15.3.4 Evénements courants

15.3.5 Evénements Souris

15.3.6 Evénements Clavier

15.3.7 Différences entre Click, MouseDown et PreviewMouseDown

15.3.8 Exemple : Gérer la fermeture de la fenêtre (FormClosing)

15.4 Forme modale et non modale

15.4.1 Non modale

15.4.2 Modale :

15.5 Un dialogue de lecture de string

15.6 Affichage image

15.6.1 PixelFormat

15.7 Dessiner un graphe

15.8 Graphique Pen et Brush

15.8.1 Pen

15.8.2 Brush

16 Process, Thread, Task

16.1 Processus

16.2 Les Threads

16.2.1 Exemple sans renvoi d'argument

16.2.2 Exemple avec renvoi d'argument

16.2.3 Avant-plan et arrire plan

16.2.4 Informations sur le thread

16.3 Les Tasks

16.3.1 Tâche sans résultat

16.3.2 Tâche avec résultat

16.3.3 Tâche avec paramètres

16.3.4 Task asynchrone (async) et await résultat

16.3.5 Parallélisation

16.3.6 Task.WhenAny

16.3.7 Task.WhenAll

16.3.8 Task.Delay(t_ms)

17 Compléments Visual et C#

17.1 Ajout de référence aux objets

17.2 Accès aux fonctions WIN32

17.3 Appel dll C++ depuis C#

17.3.1 Accès à une structure contenant des entiers

18 WPF et 3D

18.1 Mathématique 3D

18.2 Utiliser WPF dans une Windows Form

18.3 Application minimale WPF

18.4 Usage du conteneur Grid en C#

18.5 Application minimale 3D

18.6 Le Viewport3D et son contenu et la camera

18.6.1 Création et attachement du Viewport3D

18.6.1.1 Code xaml minimal

18.6.1.2 Chargement dans xaml

18.6.1.3 Chargement et Viewport dans xaml

18.6.2 Le contenu du Viewport : ModelVisual3D et Camera

18.7 Construction d'une scène 3D

18.7.1 Le MeshGeometry3D

18.7.1.1 Transformation d'un MeshGeometry3D

18.7.1.2 Animation d'un MeshGeometry3D

18.7.2 Le GeometryModel3D (géometrie + matière)

18.7.3 Le Model3DGroup

18.7.4 Le ModelVisual3D

18.8 Le point de vue (Camera)

18.8.1 Définition du point de vue

18.8.2 Gestion du point de vue

18.9 Les couleurs (Colors) et transparence

18.10 Les éclairages

18.10.1 Lumière ambiante (AmbientLight)

18.10.2 Lumière directionnelle (DirectionalLight)

18.10.3 Lumière ponctuelle (PointLight)

18.10.4 Lumière Spot (SpotLight)

18.11 Revêtement du GeometryModel3D

18.11.1 Les matériaux (Material+Brush)

18.11.1.1 Revêtement uniforme (DiffuseMaterial+ Brush)

18.11.1.2 Image (DiffuseMaterial + ImageBrush)

18.11.1.3 Revêtement par gradient linéaire (DiffuseMaterial + LinearGradientBrush)

18.11.1.4 Revêtement par gradient radial (DiffuseMaterial + LinearGradientBrush)

18.11.1.5 Revêtement spéculaire (SpecularMaterial)

18.11.1.6 Revêtement émissif (SpecularMaterial)

18.11.2 Taille et placage du revêtement (Viewbox, Viewport, Stretch)

18.11.3 Ajustement du revêtement (TextureCoordinate)

18.11.4 Dictionnaire pour l'économie des Positions

18.11.4.1 Cas non texturé

18.11.4.2 Cas texturé

18.12 Index des exemples

 

1 Directives

Le #ifdef de C/C++ est ici un simple #if (valeur)

Les #define valeur doivent être placés en tête de fichier.

2 Projets C#

  1. 1.Application Windows Form (WF): Application windows fenêtrée utilisant la techno Windows Forms. Il y a deux versions :  

  1. 2.Application WPF : Application windows fenêtrée utilisant la techno Windows Presentation Fooundation basée sur une description de type *.xaml. Plus ancienne, mais intéressante car intègre des libraires 3D. Il existe des passerelles permettant d'activer une WPF depuis une WF (voir D:\MesProgs_Langages\MICROSOFT_VISUAL_STUDIO\C#-WPF\WF2WPF et WF2WPF_02) 

  2. 3.Application console : Application utilisable en ligne de commande (.Framework ou .Net) 

  3. 4.Projet partagé : Contient du code utilisable par d'autres projets. 

  4. 5.Bibliotèque de classes : Code de classes utilisables par d'autres exécutables. 

  5. 6.Application de navigateur WPF : Application pouvant être ouverte par un navigateur web. 

  6. 7.Bibliothèque de contrôles utilisateur WPF : Pour étendre ceux offerts par Windows 

  7. 8.Bibliothèque de contrôles personnalisés WPF : Idem. 

  8. 9.Service Windows : programmes qui s'exécutent en tâche de fond. 

F1 direct : Aide sur mot clé.

SHIFT-F1 : Aide Editeur

3 Présentation de hello.cs

 

using System;

class Hello

{

    static void Main()

    {

        Console.WriteLine("Hello, World");

    }

}

La directive using fait référence à un espace de noms donné. Elle permet l’utilisation non qualifiée des types membres de cet espace de noms. En raison de la directive using System,  un programme peut utiliser Console.WriteLine comme raccourci pour System.Console.WriteLine.

 

La classe Hello a un membre unique, la méthode nommée Main déclarée avec le modificateur static. Elle sert de point d’entrée au programme.

 

Les fichiers sources C# ont généralement l’extension de fichier .cs et sont compilés comme ceci :

csc hello.cs

 

4 Structure du programme

Les programmes sont constitués de fichiers sources organisés en namespace (espace de nom). Les espaces de nom ont une structure hiérachque comme les fichiers ou les classes et sous-classes. Les innombrables méthodes fournies par Microsoft sont décrite dans la MSDN que l'on peut consulter ici :  https://docs.microsoft.com/fr-fr/dotnet/api/

Par exemple, la doc sur system.environment.username  se trouve ici :

https://docs.microsoft.com/fr-fr/dotnet/api/system.environment.username?view=net-5.0

Les programmes déclarent des types (class, interface, ...).

Les types ont des membres qui sont des champs, des méthodes, des propriétés, des évènements...

Lorsque les programmes .NET (C#, etc)  sont compilés, ils sont physiquement empaquetés dans des assemblys (assenblages de code executable) qui ont généralement l’extension de fichier .exe ou .dll, selon qu’ils implémentent des applications ou des bibliothèques.

Exemple de fichier acme.cs qui déclare une classe nommée Stack dans un espace de noms appelé Acme.Collections, ce qui fait que le nom complet de cette classe est Acme.Collections. Stack.

using System;

namespace Acme.Collections

{

    public class Stack

    {

        Entry top;

        public void Push(object data)

        {

            top = new Entry(top, data);

        }

 

        public object Pop()

        {

            if (top == null)

            {

                throw new InvalidOperationException();

            }

            object result = top.data;

            top = top.next;

            return result;

        }

 

        class Entry

        {

            public Entry next;

            public object data;

            public Entry(Entry next, object data)

            {

                this.next = next;

                this.data = data;

            }

        }

    }

}

 

La classe Stack contient

La classe interne Entry contient trois membres en plus : deux champs nommés next et data et un constructeur.

 

Pour compiler cette routine en librairie et générer acme.dll faire :

csc /t:library acme.cs

Les assemblys contiennent un langage intermédiaire (IL) et des métadonnées. Au moment de l'utilisation, le compilateur juste à temps (JIT) du Common Language Runtime .NET (CLR) convertit le tout en code spécifique au processeur.

Les métadonnées des assemblys rendent inutiles les directives #include et les fichiers d’en-tête en C# pour accéder depuis un autre programme aux membres et types publics contenus dans un assembly. L'accès est direct par simple référence à cet assembly lors de la compilation du programme.

Ci-après le programme example.cs qui utilise la classe Acme.Collections.Stack à partir de l’assembly acme.dll :

using System;

using Acme.Collections;

class Example

{

    static void Main()

    {

        Stack s = new Stack();

        s.Push(1);

        s.Push(10);

        s.Push(100);

        Console.WriteLine(s.Pop());

        Console.WriteLine(s.Pop());

        Console.WriteLine(s.Pop());

    }

}

On compile en faisant référence à la dll comme suit :

csc /r:acme.dll example.cs

Les types et variables

4.1 Tour des différents types

4.1.1 Les Types valeur

4.1.1.1 Types intégrés ou primitifs

Ils sont tous initialisés par défaut par des  0000000...

4.1.1.2 Types enum

Types définis par l'utilisateur de la forme enum E {...}

4.1.1.3 Types struct

Types définis par l'utilisateur de la forme struct S {...}

4.1.1.4 Types valeur Nullable

Extensions de tous les autres types de valeurs avec une valeur null.  La syntaxe Nullable<T> ou T? étend le type T en lui permettant d'avoir la valeur null, par example lorsqu'il n'est pas défini. Exemples : int? x = 10; ou int? x = null; déclarent une variabe x de type Nullable<int> et l'initialisent respectivement à 10 et null.

Les propriétés HasValue et Value qui sont en lecture seule permettent de n'accéder à la valeur d'une variable nullable que lorsqu'elle existe, par ex : if (x.HasValue) y = x.Value;

Si Value est utilisé alors que HasValue est false a pour effet de lèver une InvalidOperationException. Remarque :  On aurait aussi pu écrire : if (x != null) y = x.Value;

4.1.2 Les Types objets et les Types référence.

Les  types objets simples sont des références sur des types valeur. La zone mémoire associée au nom de la variable contient le type de la variable et null ou l'adresse de la zone ou la valeur sera stockée.

4.1.2.1 Types classe

4.1.2.2 Types interface

Types définis par l'utilisateur de la forme interface I {...}

4.1.2.3 Types de tableaux

Syntaxe très proche du C/C++ :

4.1.2.4 Types délégués

Types définis par l'utilisateur de la forme delegate int D(...)

 

Un delegate est un type référence qui peut être utilisé pour encapsuler une méthode anonyme ou nommée. Les délégués sont comparables aux pointeurs de fonction de C/C++ qui de plus sont sûrs et de type sécurisé. Un délégué peut être instancié en l’associant à une méthode nommée ou anonyme. Ils sont nécessaires pour appeler les méthodes des widgets des Forms depuis un autre thread (timer, ...) car ces méthodes ne sont normalement appelables que depuis le thread de la Form.

 

Voir chapitre 10.1.

4.2 Le type object

Chaque type dans C# dérive directement ou indirectement du type object, et object est la classe de base fondamentale de tous les types. Les valeurs des types référence sont considérées comme des objets simplement en affichant les valeurs en tant que type object. Les valeurs des types valeur sont considérées comme des objets en effectuant des opérations de boxing et d’unboxing. Dans l’exemple suivant, une valeur int est convertie en object et à nouveau en int.

using System;

class BoxingExample

{

    static void Main()

    {

        int i = 123;

        object o = i;    // Boxing : Allocation de o et copie de la valeur 123 à l'intérieur

        int j = (int)o;  // Unboxing (avec vérification de compatibilité avant affectation)

    }

}

4.3 Mot clé var : inférence de type

A l'intérieur d'une fonction, on peut déclarer var une variable locale dont le type est évident. Le compilateur associe à la variable le bon type qu'il déduit du contexte. Exemple :

var x = 10; // x sera déclaré int

var s = Console.ReadLine(); // s sera déclaré string.

 

4.4 Les types simples

4.4.1 Domaines des valeurs

 

Type C#

Type .NET

Donnée représentée

Suffixe
littéral

Nb octets

Domaine de valeurs

char

Char

caractère

 

2

caractère Unicode (UTF-16)

string

String

chaîne de caractères

 

 

référence sur une séquence de caractères Unicode

int

Int32

nombre entier

 

4

[-231, 231-1] [-2147483648, 2147483647]

uint

UInt32

..

U

4

[0, 232-1] [0, 4294967295]

long

Int64

..

L

8

[-263, 263 -1] [-9223372036854775808, 9223372036854775807]

ulong

UInt64

..

UL

8

[0, 264 -1] [0, 18446744073709551615]

sbyte

SByte

..

 

1

[-27 , 27 -1] [-128,+127]

byte

Byte

..

 

1

[0 , 28 -1] [0,255]

short

Int16

..

 

2

[-215, 215-1] [-32768, 32767]

ushort

UInt16

..

 

2

[0, 216-1] [0,65535]

float

Single

nombre réel

F

4

[1.5 10-45, 3.4 10+38] en valeur absolue

double

Double

..

D

8

[-1.7 10+308, 1.7 10+308] en valeur absolue

decimal

Decimal

nombre décimal

M

16

[1.0 10-28,7.9 10+28] en valeur absolue avec 28 chiffres significatifs

bool

Boolean

..

 

1

true, false

object

Object

référence d'objet

 

 

référence d'objet

 

Ce sont tous des structure sauf  string et object qui sont des classes. Le type int est un alias C# qui désigne la structure .NET System.Int32. De même, le type C# string est un alias pour le type .NET System.String qui est une classe et non une structure.

4.4.2 Valeurs littérales

int

245, -7, 0xFF (hexadécimal), 0b1001 (binaire)

long

100000L

double

134.789, -45E-18

float

134.789F, -45E-18F (-45 10-18)

decimal

100000M

char

'A', 'b'

string

"aujourd'hui", "c:\\chap1\\par3", @"c:\chap1\par3"

bool

true, false

date

new DateTime(1954,10,13) (an, mois, jour) pour le 13/10/1954

 

 

Remarques :

4.4.3 Priorité des opérateurs

 

() [] fonction

gauche en premier

! ~ ++ --

droit en premier

new (type) opérateurs cast

droit en premier

* / %

gauche en premier

+ -

gauche en premier

<< >>

gauche en premier

< <= > >= instanceof

gauche en premier

==  !=

gauche en premier

&

gauche en premier

^

gauche en premier

|

gauche en premier

&&

gauche en premier

||

gauche en premier

?  :

droit en premier

= += -= etc. .

droit en premier

 

4.4.4 Transtypage

Opérateur (cast) comme en C/C++.

4.5 objets instance de classe

Un objet est créé par :

Toto toto ; // mais comme i n'est pas instancié (toto is null) est true.

Toto toto = new Toto; // là il est instancié.

toto.Dispose() ; détruit toto, mais (toto is null) est toujours vrai

toto = null; // permet ensuite de tester (toto is null).

 

4.6 La classe string

La classe string est très proche de celle qu'on trouve en java et en Visual Basic, avec :

Le caractère \ est un caractère d'échappement pour :

\'   \"   \\   \a (bip)  \b (backspace)  \f (saut de page)  \n  (saut de ligne) \r  (retour début de ligne)  \t (tab. horiz)  \v  (tab. vert.) \0  (null) et \uXXXX (unicode XXXX).

4.6.1 @Chaines littérales

Dans une chaine préfixée par le caratère @ tous les caractères sont traités littéralement, même les caractères d'échappement, à l'exception du " qui doit être doublé. Exemple :

string nomfich = @"D:\DocsDiverses\Usinage\adresses.txt";

Le préfixe @ permet d'utilser les \ comme des caractères ordinaires.

4.6.2 $chaine interpolée et Formatage

En précédent une chaine par le caracère $ les éléments intérieurs compris entre accolades comme {var} sont évalués et remplacés par leur expression en chaine de caractères.

double pi = 3.141516; string s2 = $"Pi = {pi}";  

Peut éventuellement être plus pratique que le formatage utilié dans Console.WriteLine:

Console.WriteLine("La somme {0} + {1} vaut {2}",3,4,7);

 

Remarque : Dans le deux cas, entre les accolades que ce soit la variable {pi} de la chaine interpolée ou le rang {0}, {1}... de la chaine formatée de Writeline, la variable ou le rang peuvent être suivis d'une expression précisant le Format comme par exemple : {pi,7:F3} qui précise que le string généré va comporté 7 places de caractères, que la conversion en string sera de type F (flottant) et qu'il y aura 3 chiffres après le point décimal.

Le nombre de caractères est facultatif. S'il est absent la virgule aussi  : {pi:F3}

Le nombre de décimales est facultatif : {pi:F}

Le format est facultatif : {pi}

Pour mettre des zéros par devant il faut utiliser le format D suivi du nombre total de caractères, par exemple {45:D5} génère "00045"

 

 

 

4.6.3 Conversions nombre - string - nombre

 

nombre -> chaîne

nombre.ToString()

chaine -> int

int.Parse(chaine) ou System.Int32.Parse

chaîne -> long

long.Parse(chaine) ou System.Int64.Parse

chaîne -> double

double.Parse(chaîne) ou System.Double.Parse(chaîne)

chaîne -> float

float.Parse(chaîne) ou System.Float.Parse(chaîne)

 

Exemple : Lecture d'un tableau de valeurs dans un fichier :

StreamReader sr = new StreamReader("trajnum4.txt");

String line = sr.ReadLine(); // Saute la première ligne

while (true)

{

   line = sr.ReadLine();

   if (line == null) break;

   string[] ss = line.Split(' ');

   string[] vals = new string[6];

   int k = 0; // Enleve les strings vides, ne conserve que ceux qui ont une valeur

   foreach(var s in ss){ if (s.Length > 0){vals[k++] = s; if (k >= 6) break;}

   t = double.Parse(vals[0]);

   ah = double.Parse(vals[1]);

   dec = double.Parse(vals[2]);

   ad = double.Parse(vals[3]);

   va = double.Parse(vals[4]);

   vd = double.Parse(vals[5]);

   Console.WriteLine(String.Format("{0,4:F0} {1,9:F5} {2,9:F5} {3,9:F5} {4,9:F6} {5,9:F6} ", t, ah, dec, ad, va, vd));

}

sr.Close();

La méthode Split retourne un string pour chaque zone ne contenant pas d'espace, mais aussi un string vide pour chaque caractère espace, string qu'il faut éliminer.

4.6.4 Comparaison de string

On peut comparer deux strings par ==, != ou bien chaine1.CompareTo(chaine2) -> int ou encore chaine1.Equals(chaine2) -> bool.

4.7 Le type tableau

int[] entiers = new int[4];

Avec la dimension déduite de l'initialisation :

int[] entiers = new int[] {0,10,20,30};

ou plus simplement

int[] entiers = {0,10,20,30};

 

Les tableaux multidimensionnels existent comme en C/C++ avec la possibilité de dimensions internes variables, mais il existe en plus des tableaux plus simples avec les dimensions internes fixes :

int[,] t1 = new int[2, 3];

int[,] t2 = { { 1, 2, 3 },{ 4, 5, 6 } };

 

Ici t2[1,1] == 5. en ligne 1, c'est-à-dire 2ème ligne, colonne 1, c'est-à-dire 2ème colonne.

Autre exemple :

double[,] tablo = new double[,] {{ 0.5, 1.7}, { 8.4, -6 }, {2.3, 1.2 }};

            Console.WriteLine("\ntablo est de dimension {0}x{1}",tablo.GetLength(0), tablo.GetLength(1));

Résultat :

tablo est de dimension 3x2

4.8 Les tuples

Les tuples sont des structures hyper simplifiées utilisées pour regrouper des éléments divers rangés entre parenthèses. On peut déclarer un tuple comme ceci :

(double taille, int age, int numero) individu;

Généralement on utilise des tuples anonymes comme retour de méthodes qui renvoient pluiseurs paramètres :

        public (double jdec, int mois, int annee) jul2dmy(double JJ)

        {

            double jd; int m, a;

            /* Calcul de jd, m, a */

            return (jd, m, a);

        }

Et on utilise directement les éléments internes, sans passer par un tuple nommé :

            double jde; int mo, an;

            (jde, mo, an) = A.jul2dmy(JJ);

Mais on peut aussi passer par l'intermédiaire d'un tuple nommé:

            (double, int, int) res = A.jul2dmy(JJ);

            Console.WriteLine($"Jdec = {res.Item1}, mois = {res.Item2}, an = {res.Item3}");

et accéder aux éléments par le biais des champs Item1,...

On peut affecter un tuple nommé ou anonyme à un autre tuple nommé ou anonyme de même composition.

4.9 Le type enum

Fonctionne de manière basique comme l'enum C/C++, mais c'est de plus une instance de la classe System.Enum qui offre de nombreuses méthodes assez complexes. Voir :

https://docs.microsoft.com/fr-fr/dotnet/api/system.enum?view=netframework-4.8

4.10 Attibut const

public const double PI = 3.14159;

Be peut plus être modifié.

4.11 Modificateurs d'accès

private : accès réduit à la classe

protected : accès réduit à la classe et ses filles

internal : accès réduit aux classes de l'assembly qui déclarent la variable

protected internal : accès réduit aux classes de l'assembly qui déclarent la variable et aux classes filles de ces classes.

public : accès universel.

4.12 Opérateurs d'accès

L'accès indexé aux membres d'un tableau par [ ] et aux membres d'une classe par . (point) comme en Java.

4.13 Opérateur ? accès avec nullité conditionnelle

Pour éviter l'exception levée par l'accès à une référence nulle de classe ou de tableau, on peut précéder le . ou les [] d'un caractère ?. Ainsi personne?.nom ou tablo?[1,1] renverront la valeur null si personne ou tablo sont eux même null.

4.14 Opérateur ? ? fusion de valeurs nulles

x = y ?? z;

Si y est non null, x vaut y, sinon il vaut z.

4.15 opérateurs arithmétiques et logiques

Comme en C/C++, java...

5 Fonctions

Les arguments des fonctions sont passés par valeur. C'est-à-dire que c'est une copie qui est utilisé dans la fonction.

5.1 Argument passé en mode : ref

Mais le mot clé ref, permet le passage par référence.

            int inc10(int x) { x += 10; return x; }

            int inc20(ref int x) { x += 20; return x; }

 

            int a = 1, b = inc10(a);

            Console.WriteLine($"a = {a}, b = {b}");

 

            int c = 1, d = inc20(ref c);

            Console.WriteLine($"c = {c}, d = {d}");

Résultat :

a = 1, b = 11

c = 21, d = 21

5.2 Argument passé en mode : out

Un argument passé en mode out peut être modifié dans la fonction, même s'il n'est pas initialisé !

            void bizarre(out int x) { x = 12; }

            int a;

            bizarre(out a);

            Console.WriteLine($"a = {a}");

Résultat :

a = 12

5.3 Nombre d'arguments variables : préfixe params

Le mot clé params permet de passer des arguments en nombre variable. Il doit être le dernier de la liste d'arguments.

            void UseParams(int a, params int[] list)

            {

                Console.Write(a + " ");

                for (int i = 0; i < list.Length; i++) Console.Write(list[i] + " ");

                Console.WriteLine();

            }

 

            void UseParams2(params object[] list)

            {

                for (int i = 0; i < list.Length; i++) Console.Write(list[i] + " ");

                Console.WriteLine();

            }

            UseParams(1, 3, 4);

            UseParams2("Bonjour", "Michel");

            Console.ReadKey();

Résultat :

1 3 4

Bonjour Michel

5.4 Arguments facultatifs prédéfinis ( = valeur)

Des arguments prédéfinis peuvent être omis dans la liste d'appel. Attention : Le premier omis force l'omission des suivants.

           void imprime (int a, int b = 12) {Console.WriteLine($"a = {a}, b = {b}");}

            imprime(3, 4);

            imprime(5);

Résultat :

a = 3, b = 4

a = 5, b = 12

5.5 Arguments nommés

On peut utiliser le nom des arguments pour les affecter dans l'appel, en ordre quelconque :

           void imprime (int a, int b = 12) {Console.WriteLine($"a = {a}, b = {b}");}

            imprime(b : 5, a : 10);

            imprime(5);

Résultat :

a = 10, b = 5

a = 5, b = 12

Bien sur, il y a des règles de cohérence à respecter pour que le compilateur s'y retrouve.

5.6 Extension de classe (préfixe this)

On peut ajouter une méthode statique à une classe existante dans une classe d'extension bidon, en préfixant le premier argument (qui est une instance de la classe existante) du mot clé this. Voir chapitre 9.2.

 

6 Fonction mathématiques

Comme dans Java ce sont des méthodes de la classe Math. Elles commencent par une majuscule.

Les classiques du C/C++ avec E/S  doubles : Acos, Asin, Atan, Atan2, Cos, Cosh, Exp, Log, Log10, Pow, Sin, Sqrt, Tan, Tanh.

BigMul : I32 x I32 : I64

Ceiling : E/S double ou Decimal : Arrondi vers + l'∞.

Floor :  E/S double ou Decimal : Arrondi vers - l'

Truncate :  E/S Decimal : Arrondi vers 0.

Round : E/S double ou Decimal : Arrondi vers le plus proche

DivRem  : int quotient = DivRem(int a, int b, out int reste) avec reste = a%b du signe de a.

IEEERemainder : double reste = IEEERemainder (double x, double y) en prenant pour quotient q l'entier le plus proche de x/y.

Abs, Max, Min : Sortie du même type que l'entrée.

Sign : int32 -1, 0 ou +1 selon le nombre.

E, PI.

7 Structures de controle

comme en C/C++, java, mais de plus la condition testée peut être :

(val is v)

(val is TType v) ou TType est int, var, String...

(val is TType v when (v > 7)) ou TType est int, var, String...

Les deuxième arguments (V, TType, TType v when (v > 7)) peuvent intervenir aussi dans les case d'un switch.

En plus :

 

 


8 Spécificités du C#

8.1 Opérateurs typeof, is, as

8.1.1 typeof : Information sur un élément : classe Type

L'information retournée par typeof est de la classe Type. Exemple :

Type t = typeof(String);

C'est le principal moyen d’accéder aux métadonnées. Les membres de Type permettent d'obtenir des informations sur une déclaration de type, sur les membres d’un type (tels que les constructeurs, les méthodes, les champs, les propriétés et les événements d’une classe), ainsi que le module et l’assembly dans lequel la classe est déployée.

8.1.2 is : comparaison de types

Pour comparer le type d'un élément à une classe, on le fait avec is, qui permet en particulier de savoir si un élément est instancié en le comparant à null, pas avec == mais avec "is", comme ceci :

if (toto is null) ....;

E is T renvoie true s'ils sont de même type ou si on peut convertir E en type un élément de type T.

8.1.3 as : test et conversion de type

E as TT est un type est équivalent à l'instruction E is T ? (T)(E) : (T)null  

c'est-à-dire, si E est de type convertible en T on fait un cast de E en type T, sinon on renvoi null de type T, ce qui est plus pratique que d'utiliser directement le cast du langage C : (T)(E) qui peut produire une erreur.

8.2 unsafe

Modificateur mis devant une opération ou définition de fonction dans laquelle des pointeurs sont utilisé. On peut aussi déclarer un bloc unsafe.

8.3 foreach

foreach (Type variable in collection)

    instructions;

}

collection est une collection d'objets énumerables (tableaux, listes, ...)

8.4 yield

Le mot clé yield permet de définir un itérateur sans classe explicite supplémentaire pour générer des énumérateurs. Dans l'example suivant, la fonction Power renvoie une énumération de int grace à l'utilisation de  yield return. cette énumération est utilisée dans l'instruction foreach du Main.

public class PowersOf2

{

    static void Main()

    {

        // Display powers of 2 up to the exponent of 8:

        foreach (int i in Power(2, 8))

        {

            Console.Write("{0} ", i);

        }

    }

 

  public static System.Collections.Generic.IEnumerable<int> Power(int number, int exponent)

    {

        int result = 1;

 

        for (int i = 0; i < exponent; i++)

        {

            result = result * number;

            yield return result;

        }

        yield break; // Inutile ici

    }

 

    // Output: 2 4 8 16 32 64 128 256

}

Les deux formes de l'instruction yield sont :

yield return <expression>; // Pour retourner les éléments un par un

yield break; // Pour terminer l'itération

Il ne peut pas y avoir d'instruction yield return dans un bloc try-catch. Une instruction yield return peut se trouver dans le bloc try d'une instruction try-finally.

Une instruction yield break peut se trouver dans un bloc try ou un bloc catch mais pas dans un bloc finally.

Si le corps foreach (en dehors de la méthode Iterator) lève une exception, un bloc finally de la méthode Iterator est exécuté.

8.5 throw et try

Example :

static double Divide(double x, double y)

{

    if (y == 0)

        throw new DivideByZeroException();

    return x / y;

}

 

static void TryCatch(string[] args)

{

    try

    {

        if (args.Length != 2)

        {

            throw new InvalidOperationException("Two numbers required");

        }

        double x = double.Parse(args[0]);

        double y = double.Parse(args[1]);

        Console.WriteLine(Divide(x, y));

    }

    catch (InvalidOperationException e)

    {

        Console.WriteLine(e.Message);

    }

    finally

    {

        Console.WriteLine("Good bye!");

    }

}

 

Remarque :

8.6 Checked et unchecked

Example :

static void CheckedUnchecked(string[] args)

{

    int x = int.MaxValue;

    unchecked

    {

        Console.WriteLine(x + 1);  // Overflow

    }

    checked

    {

        Console.WriteLine(x + 1);  // Exception

    }

}

8.7 lock

class Account

{

    decimal balance;

    private readonly object sync = new object();

    public void Withdraw(decimal amount)

    {

        lock (sync)

        {

            if (amount > balance)

            {

                throw new Exception("Insufficient funds");

            }

            balance -= amount;

        }

    }

}

8.8 using

 

static void UsingStatement(string[] args)

{

    using (TextWriter w = File.CreateText("test.txt"))

    {

        w.WriteLine("Line one");

        w.WriteLine("Line two");

        w.WriteLine("Line three");

    }

}

8.9 ref et out

Un argument de fonction préfixé par le mot clé ref devient modifiable dans la fonction : il est passé par référence au lieu d'être passé par valeur, et de même avec le mot clé out. La différence est que l'argument préfixé ref doit obligatoirement être initialisé avant l'appel de la fonction.

9 Class

Exposé limité aux apports par rapport aux autres langages.

Classe minimale avec un membre statique :

        class Toto { public int contenu = 10; }

        static void Main(string[] args)

        {

            Toto t = new Toto();

            Console.WriteLine("Contenu = {0}", t.contenu);

        }

Des membres peuvent être initialisés, entre accolades, après le new :

        class Toto { public int aa; public int bb; public int cc; }

        static void Main(string[] args)

        {

            Toto t = new Toto() {cc = 3, aa = 10 };

            Console.WriteLine("aa = {0}, bb = {1}, cc = {2}", t.aa, t.bb, t.cc);

        }

Les membres sont initialisés à 0 par défaut.

Sortie :

aa = 10, bb = 0, cc = 3

Appuyez sur une touche pour continuer...

Idem avec un constructeur qui initiale un membre :

        class Toto

        { public int aa; public int bb; public int cc;

            public Toto(int valb) { bb = valb; }

        }

        static void Main(string[] args)

        {

            Toto t = new Toto(5) {cc = 3, aa = 10 };

            Console.WriteLine("aa = {0}, bb = {1}, cc = {2}", t.aa, t.bb, t.cc);

        }

Sortie :

aa = 10, bb = 5, cc = 3

Appuyez sur une touche pour continuer...

Les membres affectés des attributs get et ou set peuvent être accédés comme les membres publics

9.1 Accesseurs get et set

Pour modifier la valeur ou accéder à la valeur d'un membre ou peut utliser les accessuers set et get, comme ci_après :

        class Toto { private int _aa;

            public int aa { get { return _aa; } set { _aa = value;} }

            public int bb { get; set; }

            public int cc;

            public Toto(int valb) { bb = valb; } }

        static void Main(string[] args)

        {

            Toto t = new Toto(5) {cc = 3, aa = 10 };

            Console.WriteLine("aa = {0}, bb = {1}, cc = {2}", t.aa, t.bb, t.cc);

        }

Ici pour le membres aa, quand on écrit x = t.aa c'est la fonction t.aa.get() qui est appelée, et quand on écrit t.aa = y, c'est la fonction t.aa.set(y) qui est appelée. Le membre interne privée _aa est utilisée pour stocker la valeur (remarquer le mot clé value qui pallie l'absence d'argument.

Il en est exactement de même pour le membre bb, mais ici les fonctions get et set sont créées avec le comportement par défaut comme pour aa, mais la valeur interne de stockage est anonyme.

Le membre cc est accessible directement.

Intérêt des get et set (?):

Le set comme celui utilisé dans le cas de aa, permet vérifier la valeur fournie avant de l'affecter, et de ne pas faire cette affection si elle ne convient pas.

Le get comme celui de aa permet de vérifier si certaines conditions sont réunies avant de délivrer la valeur demandée, ou éventuellement appeler une fonctionnalité pour la rafraichir.

Remarque : On utilise ci-dessous le raccourci => pour le corps d'une méthode qui peut se résumer à un return :

class Toto { private int _aa;

            public int aa { get =>_aa;  set { _aa = value;} }

            public int bb { get; set; }

            public int cc;

            public Toto(int valb) { bb = valb; } }

9.2 Extensions statiques ultérieures

Une calsse existante, et en particulier les classes des API système et cie, peuvent être étendues très facilement par des méthodes statiques. Il suffit de les déclarer en statique dans une classe statique (dont le nom importe peu car il ne sera jamais utilisé) et de leur donner comme premier argument l'objet instance de la classe que l'on veut étendre, précédé du mot clé this dans sa définition.

Exemple : On étend la classe string avec une méthode qui compte les voyelles :

using System;

public static class MonExtension

{

    // La méthode d'extension pour le type string

    public static int CountVowels(this string str)

    {

        if (str == null) throw new ArgumentNullException(nameof(str));

        int count = 0;

        foreach (char c in str.ToLower()) { if ("aeiou".Contains(c)) count++;}

        return count;

    }

}

 

class Program

{

    static void Main()

    {

        string phrase = "Hello, World!";

        int vowelCount = phrase.CountVowels(); // Appel de la méthode d'extension

        Console.WriteLine($"Nombre de voyelles : {vowelCount}");

    }

}

 

Remarque : En mettant la méthode statique CountVowels directement dans la classe Program sans le mot clé this et en appellant int vowelCount = CountVowels(phrase); on obtient le même résultat. Morale !!!

10 Délégués, opérateur Lambda et Événements

10.1 Délégué

Un objet délégué a des similitudes avec les pointeurs de fonction C ou les classes interface Java. Il définit une signature de méthode. Dans .NET, les types System.Action et  System.Func fournissent des définitions génériques pour de nombreux délégués courants qui permettent de créer des instanciations des types génériques fournis. Une instance de cette classe permet de passer une méthode en argument, ce qui ne serait pas possible autrement.

    class Program

    {

        // Nous définissons un type délégué int bidon(int, int)

        // de même signature que les méthodes qu'on va empaqueter

        // à l'EXTERIEUR de la METHODE qui l'utilise (ici Main).

        delegate int MyDelegate(int i, int j);

        delegate void MyPrint(string s);

        static MyPrint print = Console.WriteLine;

 

        static void Main(string[] args)

        {

            // Les méthodes

            int somme(int i, int j){return i + j;}

            int produit(int i, int j) { return i * j; }

            int rapport(int i, int j) { return i / j; }

            MyDelegate op1 = new MyDelegate(somme);

            MyDelegate op2 = produit; // Le new est inutile

            var op3 = new MyDelegate(rapport); // Ici, il l'est

            // Une méthode qui prend une fonction inconnue en argument

            int work(int i, int j, MyDelegate op) {return op(i,j);}

 

            // En argument de work, on peut passer la fonction ou le délégué

            print("Le somme de {0}+{1}={2}", 3, 5, work(3, 5, op1));

            Console.WriteLine("Le produit  {0}+{1}={2}", 5, 7, work(5, 7, produit));

            Console.WriteLine("Le rapport  {0}/{1}={2}", 9, 3, work(9, 3, rapport));

            // On peut utiliser un délégué à la place de la fonction

            print("Le produit  {0}+{1}={2}", 5, 7, produit(5, 7));

            Console.WriteLine("Le rapport  {0}/{1}={2}", 9, 3, op3(9,3));

        }

    }

Remarquons que le délégué de signature int bidon(int, int) est déclaré à l'extérieur de la méthode (ici Main) qui s'en sert, alors que le délégué print qui est déclaré à l'extérieur doit être déclaré static, comme le Main.

On définit ici 3 délégués op1, op2 et op3 des méthodes somme, produit et rapport. de 3 manières différentes. La seconde MyDelegate op2 = produit;  est la plus simple et la plus fréquemment utilisée.

La définition de ce MyDelegate est nécessaire à l'écriture de la fonction

int work(int i, int j, MyDelegate op) qui prend en argument une fonction parmi plusieurs, quelconque a priori. En C#, il n'y a pas d'autre moyen de lui fournir l'argument op.

Par contre lors de l'utilisation de la fonction work dans les Console.WriteLine, on peut mettre en argument le nom du délégué (exemple op1) ou de la fonction (exemple produit et rapport).  

Remarquer la différence entre les arguments op1, produit, rapport des méthodes work qui représente bien une référence sur une fonction et l'argument  work(..) dans Console.WriteLine qui représente la valeur renvoyée par work(..).

Enfin dans l'avant dernier  Console.WriteLine on utilise directement le résultat renvoyé par la fonction produit alors que dans le dernier on utilise le résultat renvoyé par le délégué op3 de la fonction rapport, qui peut lui être substitué.

 

Un délégué peut être réaffecté à une autre fonction de même signature :

    class Program

    {

        public class Person

        {

            public string nom, prenom;

            public Person(string _nom, string _prenom){nom = _nom; prenom = _prenom;}

            public void ShowNom(string msg) {Console.WriteLine(msg + nom);}

            public void ShowPrenom(string msg) {Console.WriteLine(msg + prenom);}

        }

 

        public delegate void MonDelegue(string msg);

 

        static void Main(string[] args)

        {

            var per = new Person("Brassens", "Georges");

            MonDelegue unDelegue = per.ShowNom;

            unDelegue("Nom : ");

            unDelegue = per.ShowPrenom;

            unDelegue("Prénom : ");

        }

    }

Ici unDelegue est d'abord affecté à la méthode per.ShowNom, puis il est affecté à la méthode per.ShowPrenom .

 

Remarque : Void plus loin le delegate Func qui simplifie tout cela !

10.2 Délégués  multifontions

Un type délégué de signature renvoyant un void peut être instancié avec plusieurs méthodes qui seront appelées en séquence à l'invocation du délégué. On ajoute une méthode par += et on retranche une méthode présente par -=. Exemple :

        public class Oper

        {

            public static void Add(int x, int y)

            {Console.WriteLine("{0} + {1} = {2}", x, y, x + y);}

 

            public static void Sub(int x, int y)

            {Console.WriteLine("{0} - {1} = {2}", x, y, x - y);}

        }

        delegate void MyDelegate(int x, int y);

 

        static void Main(string[] args)

        {

            var ops = new MyDelegate(Oper.Add);

            Console.WriteLine("Addition seulement :");

            ops(9, 3);

            Console.WriteLine("Addition et soustraction :");

            ops += new MyDelegate(Oper.Sub);

            ops(6, 4);

            Console.WriteLine("Soustraction seulement :");

            ops -= new MyDelegate(Oper.Add);

            ops(2, 8);

        }

Sortie :

Addition seulement :

9 + 3 = 12

Addition et soustraction :

6 + 4 = 10

6 - 4 = 2

Soustraction seulement :

2 - 8 = -6

 

Remarque : un délégué defini dans une classe ne peut pas être défini à l'extérieur de cette classe par un = . On ne peut qu'utiliser  += ou -= pour lui ajouter ou retrancher des méthodes.

10.3 Délégués prédéfinis Func, Action et Predicate

10.3.1 Func<Targs...,Tret>

Il existe de nombreux délégués prédéfinis pour les méthodes prenant de 0 à 16 arguments et renvoyant un résultat. Ce délégué est invoqué par le type Func<Targ1,...TargN, Tret>.

Exemple avec un délégué ordinaire delegate int MyDelegate(int i) utilisé pour carre et un délégué prédéfini Func<int,int,long>  utilisé pour somme.

    class Program

    {

        delegate int MyDelegate(int i);

 

        static void Main(string[] args)

        {

            int carre(int i) { return i*i; } // Une methode

            MyDelegate degCarre = carre; // Un delegué sur cette méthode

            Console.WriteLine("Le carre de {0}={1}", 7, degCarre(7));

 

            long somme(int i, int j) { return i + j; } // Une methode

            Func<int,int,long> degSomme = somme; // Un delegué sur cette méthode

            Console.WriteLine("Le somme de {0}+{1}={2}", 3, 5, degSomme(3, 5));

        }

    }

Autre exemple : L'ancien code suivant :

        delegate int MyDelegate(int i, int j);

        static void Main(string[] args)

        {

            int somme(int i, int j){return i + j;}

            MyDelegate op1 = new MyDelegate(somme);

            int work(int i, int j, MyDelegate op) {return op(i,j);}

            print("Le somme de {0}+{1}={2}", 3, 5, work(3, 5, op1));

      ....

est avantageusement remplacé par :

        static void Main(string[] args)

        {

            int somme(int i, int j){return i + j;}

            int work(int i, int j, Func<int, int, int> op) {return op(i,j);}

            print("Le somme de {0}+{1}={2}", 3, 5, work(3, 5, op1));

      ....

10.3.2 Action<>

Pour 0 ou 1 paramètre et pas de retour on utilise le type Action<> qui serait l'équivalent du type Func, sans Tret et avec au plus un Targ.

10.3.3 Predicate<>

Ce type défini un délégué sur un prédicat, c'est-à-dire une méthode qui retournent un booléen. Il est invoqué par Predicate<Targ..>. Exemple

        static void Main(string[] args)

        {

            // Le prédicat : méthode qui renvoie un booleen

            bool greaterThanThree(int x) { return x > 3; }

            // Un delegue sur un prédicat qui prend un int en argument

            Predicate<int> myPred = greaterThanThree;

            // Une liste d'entiers

            List<int> vals = new List<int> { 4, 2, 3, 0, 6, 7, 1, 9 };

            // On applique un tri basée sur le prédicat

            List<int> valsupa3 = vals.FindAll(myPred);

            // On imprime le résultat

            foreach (int i in valsupa3) { Console.WriteLine(i); }

        }

 

10.4 Délégué anonyme

Lorsque la méthode qui sera instanciée pour le délégué delegate int MyDelegate(string s);

ne sert qu'à cette instanciation, il est inutile de la définir en tant que méthode nommée, on peut la définir en tant que méthode anonyme comme suit :

MyDelegate op = new MyDelegate(delegate (string s) { Console.WriteLine(s);  return 100; });  

ou plus simplement comme suit :

MyDelegate op = delegate (string s) { Console.WriteLine(s);  return 100; };

 

ou encore plus simplement, avec l'opérateur lambda =>.

MyDelegate op = (string s) => { Console.WriteLine(s); return 100; };

10.5 Opérateur =>

Le symbole => peut être utilisé comme opérateur Lambda de définition d'une instance de délégué (pointeur sur une méthode) anonyme ou pour définir le corps de l'expression d'un constructeur , finaliseur ou setteur d'un membre d'une classse

10.5.1 Opérateur Lambda

L'opérateur Lamda (=>) permet de définir la méthode anonyme, instance d'un délégué de manière plus concise (mais assez cabalistique), en omettant le terme delegate et le nom de la fonction (elle devient anonyme), et en faisant suivre les arguments éventuels entre parenthèse () des symboles => : Plusieurs formes possibles :

MyDelegate op = new MyDelegate((string s) => { Console.WriteLine(s); return 100; });

 

peu utilisée car assez lourde, ou :

MyDelegate op = (string s) => { Console.WriteLine(s); return 100; };  

Cette expression peut encore être simplifiée

10.5.2 Corps d'expression

 

10.6 Résumé syntaxes

Délégué de méthode nommée :

Tref methode(Targs){..., return tref;} // La méthode nommée

delegate Tret MyDel(Targs); // La signature du délégué

 

MyDel op = new MyDel(methode);  // version delayée

MyDel op = methode; // version succinte

 

Délégué de méthode anonyme

delegate Tret MyDel(Targs); // La signature du délégué

 

MyDel op = new MyDel(delegate(Tref) => {..., return tref;});  // version delayée

MyDel op = delegate(Tref){..., return tref;};  // version succinte

MyDel op = (Tref) => {..., return tref;}; // version lambda

10.7 Événements

Pour passer un gestionnaire d'événements en arguments de méthode, on est conduit à définir un délégué retournant un void et prenant deux arguments, un objet et un EventArgs, comme suit :

public delegate void MonDeleg(object sender, EventArgs e);

(on peut mettre autre chose à la place de l'EventArgs, un String par exemple).

Pour que ce délégué soit associé à un événement c'est assez compliqué car il faut passer par l'intermédiaire d'une classe ClassEmettriceEvent à l'intérieur de laquelle on mettra une instance non initialisée de ce délégué, préfixée du mot clé event, comme suit :

public event MonDeleg monDelegEvent;  

Cette classe sera également pourvue d'une méthode qui, le moment sera venu, permettra d'émettre l'événement :       

public void emitTheEvent()

{ if (monDelegEvent != null) monDelegEvent(this, EventArgs.Empty);}  

le délégué est testé à null, au cas où il ne serait pas initialisée lors d'une tentative d'émission de l'événement.

La callback, définie généralement dans une autre partie du code, sera du genre :

void OnMyEventCallback(object sender, EventArgs e)

{ Console.WriteLine("On a reçu ..."); }

 

Dans cette autre partie du code (classe) pour pouvoir émettre l'événement, on créera une instance emetteur de ClassEmettriceEven, et on associera notre callback au délégué de gestion d'événement :

var emetteur = new ClassEmettriceEvent();

emetteur.monDelegEvent += new MonDeleg(OnMyEventCallback);

 

Puis dans cette classe (ou ailleurs ?) on pourra, au moment adéquat, émettre l'événement par :

emetteur.emitTheEvent();

 

Exemple : Tire des nombres au hasard et émet un événement si c'est un 5

    class Program

    {

        public delegate void MonDeleg(object sender, EventArgs e);

 

        public class ClassEmettriceEvent

        {

            public event MonDeleg monDelegEvent;

            public void emitTheEvent()

            { if (monDelegEvent != null) monDelegEvent(this, EventArgs.Empty);}

        }

 

        // Tire des nombres au hasard et émet un événement si c'est un 5

        static void Main(string[] args)

        {

            void OnMyEventCallback(object sender, EventArgs e)

            { Console.WriteLine("On a reçu un 5");}

 

            var emetteur = new ClassEmettriceEvent();

            emetteur.monDelegEvent += new MonDeleg(OnMyEventCallback);

 

            var random = new Random();

            for (int i = 0; i < 20; i++)

            {

                int rn = random.Next(6);

                Console.WriteLine(rn);

                if (rn == 5) emetteur.emitTheEvent();

            }

        }

    }

 

Remarque : Dans l'exemple ci-après, on a remplacé le deuxième argument EventArgs e par un string e pour pouvoir passer une information supplémentaire lors de l'émission de l'événement :

 

    class Program

    {

        public delegate void MonDeleg(object sender, string e);

 

        public class ClassEmettriceEvent

        {

            public event MonDeleg monDelegEvent;

            public void emitTheEvent(string s)

            { if (monDelegEvent != null) monDelegEvent(this, s);}

        }

 

        // Tire des nombres au hasard et émet un événement si c'est un 5

        static void Main(string[] args)

        {

            void OnMyEventCallback(object sender, string e)

            { Console.WriteLine("On a reçu un 5 au tirage n°"+e);}

 

            var emetteur = new ClassEmettriceEvent();

            emetteur.monDelegEvent += new MonDeleg(OnMyEventCallback);

            var random = new Random();

            for (int i = 0; i < 20; i++)

            {

                int rn = random.Next(6);

                Console.WriteLine(rn);

                if (rn == 5) emetteur.emitTheEvent(i.ToString());

            }

        }

    }

Mais il est plus judicieux d'utiliser une classe dérivée de EventsArgs, comme dans l'exemple ci-après :

    public class Program

    {

        public delegate void MonDeleg(object sender, MyEventArgs e);

 

        public class MyEventArgs : EventArgs

        {

            public int count; public DateTime time;

            public MyEventArgs(int _count, DateTime _time)

            { count = _count; time = _time; }

        }

 

        public class ClassEmettriceEvent

        {

            public event MonDeleg monDelegEvent;

            public void emitTheEvent(MyEventArgs e)

            { if (monDelegEvent != null) monDelegEvent(this, e); }

        }

 

        static void Callback(object sender, MyEventArgs e)

        { Console.WriteLine("Tirage d'un 5 n°{0} intervenu à {1}", e.count, e.time); }

 

 

        static void Main(string[] args)

        {

            int count = 0;

            MyEventArgs evArgs;

            var emetteur = new ClassEmettriceEvent();

            emetteur.monDelegEvent += Callback;

 

            var random = new Random();

            for (int i = 0; i < 20; i++)

            {

                int rn = random.Next(6);

                Console.WriteLine(rn);

                if (rn == 5)

                {

                    count++;

                    evArgs = new MyEventArgs(count, DateTime.Now);

                    emetteur.emitTheEvent(evArgs);

                }

            }

        }

    }

 

VOIR https://zetcode.com/csharp/delegate/

 

10.8 Methode Invoke

Invoke est une méthode de certains widgets et en particulier de la classe Form. Ses prototypes sont les suivants :

        public object Invoke(Delegate method);

        public object Invoke(Delegate method, params object[] args);

Dans le premier cas, le délégué représente une méthode qui ne prend aucun arguments, et dans le deuxième cas la méthode peut predre un ou plusieurs arguments.

On ne peut pas appeler appeler une méthode d'un widget d'une Forme depuis un thread différent de celui de la Form, mais depuis ce thread différent, on peut appeler la méthode Invoke, qui sert à cette communication.

On définiera donc un délégué comme suit :

delegate Toto MethodeDelegate(Titi a, Tata b);

et on en crééra une instance, non encore initialisée.

MethodeDelegate methodDelegue;

Supposons que la méthode candidate soit :

Toto methode(Titi a, Tata b)

{ ... }

On initialisera le délégué dans une des méthodes d'initialisation :

  methodDelegue =  methode;

Et dans le thread qui veut appeler methode(a, b), on fera :

object[] args = new object[2];

// On affecte les valeurs des Titi x et Tata y au tableau d'objects args

args[0] = x; args[1] = y; Invoke(methodDelegue, args);

et cela doit marcher !

               

10.9 MethodInvoker

MethodInvoker simplifie légèrement la procédure précédente. Il représente un délégué simple  un qui permet d'exécuter toute méthode retournant un void et n'acceptant aucun paramètre. Ce délégué peut être utilisé lors de l’appel à la méthode d’un contrôle Invoke et lorsqu'on a besoin d’un délégué simple rapidement codé.

Exemple, dans une callback associée à la réception de signaux série, on veut activer un radioButton de la forme this. On fait alors l'appel suivant dans la callback :

this.Invoke(new MethodInvoker(delegate () { radioButtonPPS.Checked = port.CDHolding; }));

que l'on peut simplifier en

this.Invoke((MethodInvoker) delegate () { radioButtonPPS.Checked = port.CDHolding; });

alors que le codage direct de :

radioButtonPPS.Checked = port.CDHolding;

produit ce message d'erreur : System.InvalidOperationException : 'Opération inter-threads non valide : le contrôle 'radioButtonPPS' a fait l'objet d'un accès à partir d'un thread autre que celui sur lequel il a été créé.'

 

11 Entrées-Sorties

11.1 Clavier - Ecran

Les sorties écran se font avec System.Console.Out (sur out = 1) et  System.Console.Out (sur error = 2) au moyen des méthodes Write et WriteLine, etc... On peut abréger  System.Console.Out.Write en  System.Console.Write etc...

Les entrées clavier se font avec avec System.Console.In (sur in = 0)  au moyen de la méthodes ReadLine()...  On peut abréger  System.Console.In.ReadLine() en  System.Console.ReadLine().

Ces entrées sorties peuvent être redirigées depuis ou vers des fichier en ajoutant sur la ligne de commande les classiques chevrons de redirection :

0<in.txt,  1>out.txt ,  1>>out.txt,  2>error.txt,  2>>error.txt,

1>out.txt 2>error.txt, ...

11.2 Formatage

On peut formater chaque paramètre par une expression entre accolades {}. Les types de formatage suivant existent :

            "(C) Currency: . . . . . . . . {0:C}\n" +

            "(D) Decimal:. . . . . . . . . {0:D}\n" +

            "(E) Scientific: . . . . . . . {1:E}\n" +

            "(F) Fixed point:. . . . . . . {1:F}\n" +

            "(G) General:. . . . . . . . . {0:G}\n" +

            "    (default):. . . . . . . . {0} (default = 'G')\n" +

            "(N) Number: . . . . . . . . . {0:N}\n" +

            "(P) Percent:. . . . . . . . . {1:P}\n" +

            "(R) Round-trip: . . . . . . . {1:R}\n" +

            "(X) Hexadecimal:. . . . . . . {0:X}\n",

Exemple : Console.Writeline("Pi = {0:F}",3.1415) va imprimer 3.1415

Dans l'accolade le 1er paramètre est le rang (base 0) de l'argument. Il peut être le seul paramètre.

Après le ":" figure le format (C, D, E, ....)

Le caractère ":" peut être précédé de la longueur du champ qui est alors précédé d'une virgule ","

Le format peut être suivi du nombre de décimales.

 

Le format {x,y:Zz} affiche le paramètre n° x en utilisant exactement y caractères dans le format de type Z avec z caractères après le point décimal pour les types E, F et G.

Console.WriteLine(String.Format("{0,4:F0} {1,9:F5} {2,9:F5} {3,9:F5} {4,9:F6} {5,9:F6} ", t, ah, dec, ad, va, vd));

 

Pour un format entier avec les espaces antérieurs remplacés par des 0, utiliser :Dn.

 

produit :

   3  93.63420  11.23390 -173.09020 -0.013740  0.123890

11.2.1 Formatage DateTime (tiré de ??)

// This code example demonstrates the Console.WriteLine() method.

// Formatting for this example uses the "en-US" culture.

 

using System;

class Sample

{

    enum Color {Yellow = 1, Blue, Green};  

 static DateTime thisDate = DateTime.Now;

 

    public static void Main()

    {

        Console.Clear();

 

        // Format a negative integer or floating-point number in various ways.

        Console.WriteLine("Standard Numeric Format Specifiers");

        Console.WriteLine(

            "(C) Currency: . . . . . . . . {0:C}\n" +

            "(D) Decimal:. . . . . . . . . {0:D}\n" +

            "(E) Scientific: . . . . . . . {1:E}\n" +

            "(F) Fixed point:. . . . . . . {1:F}\n" +

            "(G) General:. . . . . . . . . {0:G}\n" +

            "    (default):. . . . . . . . {0} (default = 'G')\n" +

            "(N) Number: . . . . . . . . . {0:N}\n" +

            "(P) Percent:. . . . . . . . . {1:P}\n" +

            "(R) Round-trip: . . . . . . . {1:R}\n" +

            "(X) Hexadecimal:. . . . . . . {0:X}\n",

            -123, -123.45f);

 

        // Format the current date in various ways.

        Console.WriteLine("Standard DateTime Format Specifiers");

        Console.WriteLine(

            "(d) Short date: . . . . . . . {0:d}\n" +

            "(D) Long date:. . . . . . . . {0:D}\n" +

            "(t) Short time: . . . . . . . {0:t}\n" +

            "(T) Long time:. . . . . . . . {0:T}\n" +

            "(f) Full date/short time: . . {0:f}\n" +

            "(F) Full date/long time:. . . {0:F}\n" +

            "(g) General date/short time:. {0:g}\n" +

            "(G) General date/long time: . {0:G}\n" +

            "    (default):. . . . . . . . {0} (default = 'G')\n" +

            "(M) Month:. . . . . . . . . . {0:M}\n" +

            "(R) RFC1123:. . . . . . . . . {0:R}\n" +

            "(s) Sortable: . . . . . . . . {0:s}\n" +

            "(u) Universal sortable: . . . {0:u} (invariant)\n" +

            "(U) Universal full date/time: {0:U}\n" +

            "(Y) Year: . . . . . . . . . . {0:Y}\n",

            thisDate);

 

        // Format a Color enumeration value in various ways.

        Console.WriteLine("Standard Enumeration Format Specifiers");

        Console.WriteLine(

            "(G) General:. . . . . . . . . {0:G}\n" +

            "    (default):. . . . . . . . {0} (default = 'G')\n" +

            "(F) Flags:. . . . . . . . . . {0:F} (flags or integer)\n" +

            "(D) Decimal number: . . . . . {0:D}\n" +

            "(X) Hexadecimal:. . . . . . . {0:X}\n",

            Color.Green);      

    }

}

/*

This code example produces the following results:

 

Standard Numeric Format Specifiers

(C) Currency: . . . . . . . . ($123.00)

(D) Decimal:. . . . . . . . . -123

(E) Scientific: . . . . . . . -1.234500E+002

(F) Fixed point:. . . . . . . -123.45

(G) General:. . . . . . . . . -123

    (default):. . . . . . . . -123 (default = 'G')

(N) Number: . . . . . . . . . -123.00

(P) Percent:. . . . . . . . . -12,345.00 %

(R) Round-trip: . . . . . . . -123.45

(X) Hexadecimal:. . . . . . . FFFFFF85

Standard DateTime Format Specifiers

(d) Short date: . . . . . . . 6/26/2004

(D) Long date:. . . . . . . . Saturday, June 26, 2004

(t) Short time: . . . . . . . 8:11 PM

(T) Long time:. . . . . . . . 8:11:04 PM

(f) Full date/short time: . . Saturday, June 26, 2004 8:11 PM

(F) Full date/long time:. . . Saturday, June 26, 2004 8:11:04 PM

(g) General date/short time:. 6/26/2004 8:11 PM

(G) General date/long time: . 6/26/2004 8:11:04 PM

    (default):. . . . . . . . 6/26/2004 8:11:04 PM (default = 'G')

(M) Month:. . . . . . . . . . June 26

(R) RFC1123:. . . . . . . . . Sat, 26 Jun 2004 20:11:04 GMT

(s) Sortable: . . . . . . . . 2004-06-26T20:11:04

(u) Universal sortable: . . . 2004-06-26 20:11:04Z (invariant)

(U) Universal full date/time: Sunday, June 27, 2004 3:11:04 AM

(Y) Year: . . . . . . . . . . June, 2004

 

Standard Enumeration Format Specifiers

(G) General:. . . . . . . . . Green

    (default):. . . . . . . . Green (default = 'G')

(F) Flags:. . . . . . . . . . Green (flags or integer)

(D) Decimal number: . . . . . 3

(X) Hexadecimal:. . . . . . . 00000003

 

*/

11.2.2 Formatage DateTime avec Tostring()

On peut formater à notre convenance, jusqu'à la précision du tick (100 nanosecondes, soit 7 chiffres après la virgule des secondes), par exemple :

System.DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fffffff")

11.3 Fichier

La lecture dans un fichier texte se fait avec un objet StreamReader via ses méthodes Read et ReadLine, et les écritures dans un fichier texte se font avec un objet StreamWriter via ses méthodes Write et WriteLine.

12 Un peu de tout

12.1 Main

Elle est déclarée comme suit :

public static void Main(string[] args)

avec un argument facultatif. S'il est présent, on obtient le nombre des éventuels arguments de la ligne de commande par args.Length.

La méthode main est obligatoirement déclaré static ce qui permet à runtime d'activer cette méthode sans créer d'objet de la classe englobante.

12.2 Callback associée à un widget

Pour associer une callback à un widget, le sélectionner dans la fenêtre Form1.cs[Design], puis, à gauche afficher la liste des propriétés du widget, sélectonner la liste des événements (icone éclair) et dans cette liste choisir l'événement adéquat et donner un nom à la callback. Puis remplir son champ d'instructions dans le Form1.cs.

12.3 MessageBox

MessageBox.Show("Texte", "Titre", MessageBoxButtons.OK, MessageBoxIcon.Information);

Le paramètre de MessageBoxButtons peut prendre les valeurs suivantes :
AbortRetryIgnore, Ok, OkCancel, RetryCancel, YesNo, YesNoCancel.

Le paramètre de MessageBoxIcon peut prendre les valeurs suivantes :
Asterisk|Exclamation|Information, Question, Warning

On peut récupérer le résultat et le traiter :

DialogResult res = MessageBox.Show(..);
if (res
== DialogResult.Yes) {...}

Avec les possibilités suivantes pour DialogResult :
Abort, Cancel, Ignore, No, None, OK, Retry, Yes

Pour lire un string voir 15.5.

12.4 ToolTip

La classe ToolTip permet de faire apparaitre un message lorsque l'utilisateur laisse la souris sur un widget.

On peut détourner le Tooltip pour faire l'équivalent des Toast Android, c'est-à-dire un message furtif. Par exemple lors de l'appui sur un bouton, si dans la callback associée au click sur ce bouton, on veut, dans certaines circonstance, envoyer un message furtif, on pourra faire :

new ToolTip().Show("le message a envoyer", this, Cursor.Position.X - this.Location.X, Cursor.Position.Y - this.Location.Y, 1000);

12.5 Exit

Environment.Exit(int status);

ou en windowsForm :

Application.exit().

12.6 Méthodes usuelles des objets

Autocheck : Pour les radioButton, etc... pour permettre ou non l'acces à l'utilisateur (= readonly).

13 Stopwatch

Cette classe permet d'accéder à des intervalles de temps assez précis.

Stopwatch.IsHighResolution est un bool qui indique si le système  offre un compteur de haute résolution (< ms) ou non.

Stopwatch.GetTimestamp() donne le nombre de ticks écoulés depuis le démarrage de l'ordinateur.

 

Stopwatch.Frequency est le nombre de ticks par secondes.

 

On peut créer une Stopwatch : Stopwatch chrono; et le démarrer par :

chrono = Stopwatch.StartNew();  

et l'arrêter par : chrono.stop();.

Les propriétés :

 

Les Stopwatch créés dans différents threads sont indépendants.

14 Environment (Classe)

using System;

Propriétés :

string CommandLine (get)

string CurrentDirectory  (get,set)

int CurrentManagedThreadId { get; }

int ExitCode { get; set; }

bool HasShutdownStarted { get; }

bool Is64BitOperatingSystem { get; }

bool Is64BitProcess { get; }

string MachineName { get; }

string NewLine { get; }

OperatingSystem OSVersion { get; }

        OperatingSystem os = Environment.OSVersion;

        Console.WriteLine("Current OS Information:\n");

        Console.WriteLine("Platform: {0:G}", os.Platform);

        Console.WriteLine("Version String: {0}", os.VersionString);         Console.WriteLine("Version Information:");

        Console.WriteLine(" Major: {0}", os.Version.Major);

        Console.WriteLine(" Minor: {0}", os.Version.Minor);

        Console.WriteLine("Service Pack: '{0}'", os.ServicePack);

int ProcessorCount { get; }

string StackTrace { get; }

string SystemDirectory { get; }

int SystemPageSize { get; }

int TickCount { get; } // nombre de millisecondes écoulées depuis le démarrage du système (signé passe en négatif)

string UserDomainName { get; }

bool UserInteractive { get; } // indique si le processus en cours est exécuté en mode interactif avec l'utilisateur.

string UserName { get; }

Version Version { get; }

long WorkingSet { get; } //quantité de mémoire physique mappée au contexte du processus.

15 Les Windows Form

Une Windows Form est un objet fenêtre qui hérite de la classe Form (parfois traduite en français par Formulaire).

15.1 Référence utilisées

Une windows Form standard utilise les références suivantes :

 

On ajoute une référence par un clic droit sur Référence dans l'Explorateur de solution (Ajouter référence) et en la choississant dans la liste des Assemblys. Par exemple pour compiler l'application Invoke2 récupérée sur Codes-Source, les ajouts suivants (à partir de zéro) ont suffit :

System, System.ComponentModel.Composition, System.Data, System.Drawing et System.Windows.forms.

15.2 Windows Form à partir de 0 sans Designer

Le code minimal suivant fonctionne :

 

using System;

using System.Windows.Forms;

 

namespace WindowsFormsApp1

{

    static class Program

    {

        [STAThread]

        static void Main() { Application.Run(new Form1()); }

    }

    public class Form1 : Form

    {

        private Label label1;

        public Form1()

        {

            label1 = new Label();

            label1.Text = "Bonjour Michel !";

            SuspendLayout();

            Controls.Add(label1);

            ResumeLayout(false);

        }

    }

}

 

Mais il faut ajouter les références aux assemblys System et System.Windows.Forms.

 

Voir documentation ICI : https://learn.microsoft.com/en-us/troubleshoot/developer/visualstudio/csharp/language-compilers/add-controls-to-windows-forms

 

Remarque :

Dans void Main() l'instruction Application.Run(new Form1()); lance la boucle d'attente des événements qui du temps de win32 s'crivait comme suit :

 while (GetMessage (&msg, NULL, 0, 0))

 {

   TranslateMessage (&msg) ;  

   DispatchMessage (&msg) ;

 }

15.3 Avec le Designer

Doc ici : https://bpesquet.developpez.com/tutoriels/csharp/programmation-evenementielle-winforms/

Pour créer Hello world en windowds Form : choisir Créer un nouveau projet c#, windows Form pour .NET (ou plutôt pout .Net Framework ?)

Trois fichiers .cs sont créés : Program.cs et Form1.cs que l'on peut modifier, et Form1.Designer.cs qui est modifié par Visual Studio, qui gère également un fichier Form1.resx et divers autres fichiers projets.

VS s'ouvre avec la fenêtre en question ouverte dans le panneau d'édition, sous forme graphique (concepteur), avec son nom, a priori Form1.cs en onglet. On commute avec sa forme textuelle (code) en donnant le focus à cette partie édition, puis par le menu affichage/code (F7) et on retourne à la forme graphique par affichage/concepteur (Maj+F7). Attention ces lignes de menu ne sont présentes que si la fenêtre d'édition à le focus.

Contenu de Program.cs :

using System;

using System.Windows.Forms;

namespace WindowsFormsApp1

{

    static class Program

    {

        [STAThread]

        static void Main()

        {

            Application.EnableVisualStyles();

            Application.SetCompatibleTextRenderingDefault(false);

            Application.Run(new Form1());

        }

    }

}

Deux appels de méthodes pour assurer des compatibilités (éventuellement non nécessaires), suivis  de Application.Run(new Form1()); qui crée notre classe Form1 et qui la lance.

 

Le code du fichier  Form1.cs qui déclare cette classe est le suivant :

using System.Windows.Forms;

namespace WindowsFormsApp1

{

    public partial class Form1 : Form

    {

        public Form1()

        {

            InitializeComponent();

        }

    }

}

 

Le constructeur ne fait qu'appeler a méthode InitializeComponent() qui est déclarée dans le fichier auto-généré Form1.Designer.cs :

 

namespace WindowsFormsApp1

{

    partial class Form1

    {

        private System.ComponentModel.IContainer components = null;

        protected override void Dispose(bool disposing)

        {

            if (disposing && (components != null)) components.Dispose();          

            base.Dispose(disposing);

        }

 

        private void InitializeComponent()

        {

            this.components = new System.ComponentModel.Container();

            this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;

            this.ClientSize = new System.Drawing.Size(800, 450);

            this.Text = "Form1";

        }

    }

}

 

 

Ici, les trois fichiers appartiennent au même namespace qui a pour nom celui de l'application, a priori.

Le main est dans la static class Program. Il lance la boucle de gestion des évènements en appelant Application.Run() avec comme argument notre windowsForm Form1.

Le code de la Form1 est divisé est deux : une partie gérée par Visual Studio se trouve dans le fichier Form1.Designer.cs sous la rubrique partial class Form1 { ...} et l'autre partie que l'on gère, se trouve dans le fichier Form1.cs sous la rubrique public partial class Form1 : Form {...}.

Au niveau de la windowsForm, en mode designer, on peut faire glisser des widgets qui se trouvent dans la Boite à outils  (Affichage/Boite à outils ou CTRL+ALT+X si on ne la voit pas) sur la forme.

La fenêtre Propriétés permet de gérer ce widget. Cette fenêtre présentent 2 onglets qui organisent ces propriétés diversement :

De plus 2 onglets permettent de les classer différemment :

On change le nom d'un widget par sa propriété  (name) qui apparait en début de liste en mode priétés&alphabétique

Dans Qt, on change le texte d'unt Label par label.setText("bonjour"). Dans c# c'est tout simplement label.Text = "bonjour".

Les widgets héritent pratiquement tous de la classe Control qui leur fournit énormément de propriétés et de méthodes communes et ils héritent aussi d'une autre classe de base relative à leur usage général. Les proprités suivantes sont essentiellement héritées de la classe Control, d'où l'appellation courante de controle pour un widget.

 

Depuis une Form classique on peut créer une nouvelle Form entièrement gérée en code. Exemple :

using System;

using System.Windows.Forms;

namespace Bidon

{

    public partial class Form1 : Form // Classe gérée par Form1.Designer.cs

    {

        public Form1() {InitializeComponent();}

        private void button1_Click(object sender, EventArgs e)

        { new MaWin("Une autre fenêtre").ShowDialog();}

    }

 

    public class MaWin : Form // Classe isolée et complète ici

    {

        public MaWin(string titre)

        {

            Button bouton = new Button();

            bouton.Text = "Créer une nouvelle fenêtre";

            bouton.Size = new System.Drawing.Size(208, 23);

            bouton.Click += new System.EventHandler(this.bouton_Click);

            this.SuspendLayout();

            this.Text = titre;

            this.Controls.Add(bouton);

            this.ResumeLayout(false);

        }

 

        private void bouton_Click(object sender, EventArgs e)

        { MessageBox.Show("Clic sur autre fenêtre"); }

    }

}

15.3.1 Propriéts essentielles des widgets/controles

(Name) : le nom donné à ce widget dans le code.

Dock : Pour spécifier un positionnement qui occupe tout un bord du conteneur. Dock peut valoir DockStyle.Left | Top | Right | Bottom : Le widget est collé à ras sur le bord en question de son conteneur et occupe toute la longueur de ce bord, ou Fill : le widget rempli son conteneur, ou None : le widget n'occupe pas tout un bord du conteneur.

Anchor : Permet de préciser le positionnement dans le conteneur (si Dock.None) ou le compléter en précisant de quel bord préserver la distance lors du redimensionnement : AnchorStyle.None ou < <Top | Bottom> , <Left | Right>. Après un redimensionnement, None => centré,  sinon, la distance au bord du conteneur indiqué par la/les propriété(s) sera conservée.

Enabled : indique si le contrôle est actif ou non ;

Text : le texte affiché par le contrôle ;

Visible : indique si le contrôle est visible ou non.

Tag : propriété de type objet qui permet de mémoriser une propriété ou méthode qcq, à toutes fins utiles, par exemple l'objet qui doit être activé quand un checkbox est coché.

15.3.2 Callbacks associées aux événements

Si on a mis un bouton dans le designer, en double-cliquant dessus, cela ajoute la callback suivante dans le code :

        private void button1_Click(object sender, EventArgs e) { }

qu'il n'y a plus qu'à remplir entre les accolades.

Un double-clic sur un textbox produit les ajouts suivants :

       this.textBox1.TextChanged += new System.EventHandler(this.textBox1_TextChanged);    

dans la zone d'initialisation et :

           private void textBox1_TextChanged(object sender, EventArgs e) { }

dans les méthodes. Mais comme l' EventArgs e n'e pas de membres accessibles, si on veut tester les caractères entrés, on remplacera ces fonctions par :

            this.textBox1.KeyPress += new System.Windows.Forms.KeyPressEventHandler(keypressed);

dans la zone d'initialisation et :

        private void keypressed(Object o, KeyPressEventArgs e)

        {if (e.KeyChar == (char)Keys.Return) blabla();}

dans les méthodes.

Remarque. Il est aussi simple d'ajouter soi-même, les liaisons aux callbacks dans le constructeur de la Form1, après le InitializeComponent(); par les instructions :

       button1.Click += new EventHandler(this.button1_Click);

       textBox1.KeyPress += new KeyPressEventHandler(keypressed);

De plus on peut encore simplifier et écrire directement :

       button1.Click += button1_Click;

       textBox1.KeyPress += keypressed;

15.3.3 Evénements de gestion de la Form

Load : événement émis au chargement de le Form

Shown : événement émis lors du premier affichage de la Form

FormClosing : évènement émis lors de la  demande de fermeture

FormClosed :évènement émis lors de la  fermeture

15.3.4 Evénements courants

Paint : Lorsque le widget doit être redessiné

Layout : Lors de l'ajout ou suppression de widgets enfants

Resize : Lorsque le conteneur du widget a été redimensionné

Click : Lorsque le widget a été cliqué

DoubleClick

15.3.5 Evénements Souris

MouseClick

MouseDoubleClick

MouseDown

PreviewMouseDown

MouseEnter

MouseHover

MouseLeave

MouseMove

MouseUp

MouseWheel

Click

15.3.6 Evénements Clavier

KeyDown

KeyPress (déprécié ?)

KeyUp

PreviewKeyDown

PreviewKeyUp

 

15.3.7 Différences entre Click, MouseDown et  PreviewMouseDown

Click : se produit lorsque le contrôle est cliqué par la souris.
MouseDown : se produit lorsqu'un bouton de la souris est enfoncé alors que  lorsque le pointeur de la souris se trouve sur un contrôle,

PreviewMouseDown : se produit lorsque n'importe quel bouton de la souris est enfoncé relativement à l'élément surveillé.

Preview existe pour preque tous les événements et se produit avant son homologue.

15.3.8 Exemple : Gérer la fermeture de la fenêtre (FormClosing)

Pour ajouter une méthode de gestion de l'évènement de fermeture de la fénêtre, le plus simple est de passer par le designer de la Forme, dans la fenêtre droite des propriétés, sélectionner l'onglet éclair (zigzag) et faire un double clic sur FormClosing dans la rubrique Comportement. Cela ajoute la méthode suivante :
       private void Form1_FormClosing(object sender, FormClosingEventArgs e)

        {

        }

qu'il n'y a qu'à remplir. Par exemple pour éventuellemnt empêcher la fermeture :
       private void Form1_FormClosing(object sender, FormClosingEventArgs e)

        {

            var res = MessageBox.Show(this, "Etes-vous sûr de vouloir quitter?",

                        "Exit", MessageBoxButtons.YesNo, MessageBoxIcon.Warning,                                  MessageBoxDefaultButton.Button2);

            if (res != DialogResult.Yes)

            {

                e.Cancel = true;

                return;

            }

        }

Remarque : la ligne suivante :
           this.FormClosing += new System.Windows.Forms.FormClosingEventHandler(this.Form1_FormClosing);
a été ajoutée dans la méthode InitializeComponent() située dans le fichier Form1.Designer.cs.

On peut effectuer tout cela manuellement, en ajoutant nous-même une ligne équivalente dans le constructeur de la Form1 après l'appel de la méthode et ensuite en ajoutant la méthode.

Exemple :

Après l'appel d' mettre :

        this.FormClosing += this.MaFermeture;

ou bien

        this.FormClosing += new System.Windows.Forms.FormClosingEventHandler(this.MaFermeture);

et ensuite on instancie la méthode :

        private void MaFermeture(object sender, FormClosingEventArgs e)

        {

        }

 

15.4 Forme modale et non modale

Les Formulaires Form peuvent être modal (bloquante) ou non modal (exécution en //).

15.4.1 Non modale

C'est une sous-forme ordinaire qui, après avoir été instanciée, est affichée en appelant sa méthode Show(). Elle a sa vie indépendante à coté des autres formes de l'application.

15.4.2 Modale :

Une sous forme modale est affichée par l'appel de la méthode ShowDialog().

Une sous-forme modale bloque le reste de l'application tant qu'elle est affichée : exemple boite de dialogue.

Elle renvoie un résultat de type DialogResult.

// Affiche le formulaire SubForm (modal)

SubForm subForm = new SubForm();

if (subForm.ShowDialog() == DialogResult.OK)

{

    // ...

}

Cela suppose qu'on ait mis au moins un bouton avec sa propriété DialogResult mise sur OK.

 

15.5 Un dialogue de lecture de string

C# n'offre pas l'équivalent de la fonction InputBox de visual Basic, qui est bien pratique. Voici une méthode qui crée une forme modale qui offre ce service :

    class InputBox

    {

        public static DialogResult getStr(ref string strin, string titre = "")

        {

            int w = 400, h = 70;

            // On crée la forme contenante

            Form forme = new Form();

            forme.FormBorderStyle = FormBorderStyle.FixedDialog;

            forme.ClientSize = new Size(w, h);

            forme.Text = titre;

 

            // Le TextBox d'écriture pour l'usager

            TextBox textBox = new TextBox();

            textBox.Size = new Size(w - 10, 23);

            textBox.Location = new Point(5, 5);

            textBox.Text = strin;

            forme.Controls.Add(textBox);

 

            // Le Bouton Ok

            Button okButton = new Button();

            okButton.DialogResult = DialogResult.OK;

            okButton.Name = "okButton";

            okButton.Size = new Size(75, 23);

            okButton.Text = "&OK";

            okButton.Location = new Point(w - 80 - 80, 39);

            forme.Controls.Add(okButton);

            forme.AcceptButton = okButton; // Le Return produit Ok

 

            // Affiche le widget sous forme de boite de dialogue modale

            DialogResult result = forme.ShowDialog();

            strin = textBox.Text;

            return result; // cad  DialogResult.OK|.Yes|.No|.Cancel|...

        }

    }

On l'appellera comme ceci :

            string entree = "";

            // On lit le string entree avec l'InputBox

            DialogResult rd = InputBox.getStr(ref entree, "Titre de la question");

            if (rd != DialogResult.OK && rd != DialogResult.Yes) return;

            // On affiche le résultat dans un label

            this.label1.Text = entree;

     

On peut également déclarer la fonction getStr ailleurs , dans notre classe Form1 par exemple (avec un autre nom éventuellement) et l'appeler comme une méthode de notre forme.

On peut ajouter un bouton Cancel, mais la croix de fermeture joue déjà ce rôle.

15.6 Affichage image

Un widget approprié pour afficher des graphiques ou des images isus d'un fichier bitmap, icône, jpg, gif, png est une PictureBox.

Exemple de code affectant une image à une PictureBox à partir d'un fichier :

        private Bitmap MyImage;

        public void ShowMyImage(String fileToDisplay, int xSize, int ySize)

        {

            // Sets up an image object to be displayed.

            if (MyImage != null)

            {

                MyImage.Dispose();

            }

 

            // Stretches the image to fit the pictureBox.

            pictureBox1.SizeMode = PictureBoxSizeMode.StretchImage;

            MyImage = new Bitmap(fileToDisplay);

            pictureBox1.ClientSize = new Size(xSize, ySize);

            pictureBox1.Image = (Image)MyImage;

        }

Utilise l'espace de noms System.Drawing .

 

Exemple de code affectant une bitmap créé par des ordres graphiques : (dessine un drapeau : flag)

        void CreateBitmapAtRuntime()

        {

            pictureBox1.Size = new Size(210, 110);

            this.Controls.Add(pictureBox1);

 

            Bitmap flag = new Bitmap(200, 100);

            Graphics flagGraphics = Graphics.FromImage(flag);

            int red = 0;

            int white = 11;

            while (white <= 100)

            {

                flagGraphics.FillRectangle(Brushes.Red, 0, red, 200, 10);

                flagGraphics.FillRectangle(Brushes.White, 0, white, 200, 10);

                red += 20;

                white += 20;

            }

            pictureBox1.Image = flag;

        }

 

15.6.1 PixelFormat

Alpha (262144) : valeurs alpha non prémultipliées.

Canonical (2097152) 32 bits : couleur 24 bits et canal alpha 8 bits.

DontCare (0) non spécifié

Extended (1048576) Réservé.

Format16bppArgb1555 (397319) 16 bits (5R, 5V, 5B et 1 alpha)

Format16bppGrayScale (1052676) 16 bits

Format16bppRgb555 (135173) 16 bits ( 5R, 5V, 5B et 1 inutilisé).

Format16bppRgb565 (135174) 16 bits (5R, 6V, 5B)

Format1bppIndexed (196865) 1 bit + table des couleurs de 2 couleurs.

Format24bppRgb (137224) 24 bits (8R, 8V, 8B)

Format32bppArgb (2498570) 32 bits (8R, 8V, 8B, 8a)

Format32bppPArgb (925707) 32 bits (8R, 8V, 8B, 8a) RVB prémultipliés par a.

Format32bppRgb (139273) 32 bits (8R, 8V, 8B, 8inutilisés)

Format48bppRgb (1060876) 48 bit (16R,16V, 16B).

Format4bppIndexed (197634) 4 bits indexés.

Format64bppArgb (3424269) 64 bits (16a,16R,16V, 16B)

Format64bppPArgb (1851406) 64 bits (16a,16R,16V, 16B) RVB prémultiplié par a.

Format8bppIndexed (198659) 8 bits indexés dans table de 256 couleurs.

Gdi (131072) couleurs GDI indexées ?

Indexed (65536) ? indexées dans table ?

Max (15) Valeur maximale (?)

PAlpha (524288) alpha prémultipliées (?)

Undefined (0) non défini

15.7 Dessiner un graphe

On peut dessiner dans une PictureBox pBox que l'on met dans une Form. Il suffit d'appeler pbox.Invalidate() pour déclencher son dessin. Au préalable il faut écrire la callback de dessin comme une méthode de la classe de la Form, par exemple :
       private void pBox_Paint(object sender, PaintEventArgs e)

        {

            Graphics g = e.Graphics;

            int red = 100;

            int white = red + 11;

            Brush b = isRed ? Brushes.Red : Brushes.Green;

            while (white <= 200)

            {

                g.FillRectangle(b, 50, red, 200, 10);

                g.FillRectangle(Brushes.White, 50, white, 200, 10);

                red += 20;

                white += 20;

            }

        }

que l'on enregistrera dans le constructeur de la classe par l'instruction :
           this.pBox.Paint += new System.Windows.Forms.PaintEventHandler (this.pBox_Paint);

Cet enregistrement peut être fait automatiquement par un double clic sur  la propriété Paint qui figure dans la liste des propriétés de la PictureBox : Vue Design, Onglet Propitété, Liste Evènements (icône éclair). Ce double clic génère aussi ke squelette vide de la callback.

15.8 Graphique Pen et Brush

15.8.1 Pen

The Pen class specifies a number of settings for lines, curves, and outlines:

For information on how to draw lines and curves see the Drawing Lines and Curves topic.

Line Color and Width

The Pen.Color property specifies line color. The property does not support transparency information, so the color alpha channel will be ignored.

The minimum and default line width is 1. You can change width using the Pen.Width property.

To draw a line using a Pen object, you should pass this object as an argument to a drawing method, for example, DrawLine.

The code below draws crossed blue and red lines as shown in the image:

GDI pen.GDI pen.

C#

using (var bitmap = new Bitmap(100, 60, PixelFormat.Format24bppRgb, RgbColor.White))

{

    using (Graphics graphics = bitmap.GetGraphics())

    {

        var bluePen = new Pen(RgbColor.Blue, 8);

        graphics.DrawLine(bluePen, 10, 55, 90, 5);

        var redPen = new Pen(RgbColor.Red, 8);

        graphics.DrawLine(redPen, 10, 5, 90, 55);

        bitmap.Save(@"Images\Output\pen.png");

    }

}

You may notice that lines produced with a Graphics object are not smooth. Also, there is no possibility to draw semitransparent lines. This is a limitation of the Aurigma.GraphicsMill.Drawing.Graphics class. If you need to draw smooth or semitransparent lines, you can use the standard .NET drawing classes, which have similar APIs. Each class, Aurigma.GraphicsMill.Drawing.Graphics and System.Drawing.Graphics, has its own strengths and limitations. We recommend that you read the AdvancedDrawing.Graphics vs Drawing.Graphics vs System.Drawing.Graphics topic before making a decision on which class to use.

The code below also draws crossed blue and red lines, but it uses the System.Drawing.Graphics, and the result is quite different:

GDI+ pen.GDI+ pen.

C#

using (var bitmap = new Bitmap(100, 60, PixelFormat.Format24bppRgb, RgbColor.White))

{

    using (System.Drawing.Graphics graphics = bitmap.GetGdiPlusGraphics())

    {

        graphics.SmoothingMode = System.Drawing.Drawing2D.SmoothingMode.AntiAlias;

        var bluePen = new System.Drawing.Pen(System.Drawing.Color.Blue, 8);

        graphics.DrawLine(bluePen, 10, 55, 90, 5);

        var redPen = new System.Drawing.Pen(System.Drawing.Color.FromArgb(200, 255, 0, 0), 8);

        graphics.DrawLine(redPen, 10, 5, 90, 55);

        bitmap.Save(@"Images\Output\pen.png");

    }

}

Dash Styles

You can draw not only solid lines, but also dashed, dotted, etc. using several predefined patterns of dots and dashes. To set the pattern utilize the Pen.DashStyle property and the System.Drawing.Drawing2D.DashStyle enumeration.

The following code draws four lines of different color and style, as shown in the image:

Line continuity.Line continuity.

C#

using (var bitmap = new Bitmap(100, 40, PixelFormat.Format24bppRgb, RgbColor.White))

{

    using (Graphics graphics = bitmap.GetGraphics())

    {

        //Dashed line

        var pen = new Pen(RgbColor.DarkOrange, 1, System.Drawing.Drawing2D.DashStyle.Dash);

        graphics.DrawLine(pen, 5, 5, 95, 5);

        //Line made of dash-dot patterns

        pen.Color = RgbColor.DarkGreen;

        pen.DashStyle = System.Drawing.Drawing2D.DashStyle.DashDot;

        graphics.DrawLine(pen, 5, 15, 95, 15);

        //Line made of dash-dot-dot patterns

        pen.Color = RgbColor.Brown;

        pen.DashStyle = System.Drawing.Drawing2D.DashStyle.DashDotDot;

        graphics.DrawLine(pen, 5, 25, 95, 25);

        //Dotted line

        pen.Color = RgbColor.DarkRed;

        pen.DashStyle = System.Drawing.Drawing2D.DashStyle.Dot;

        graphics.DrawLine(pen, 5, 35, 95, 35);

        bitmap.Save(@"Images\Output\pen.png");

    }

}

Line Caps

When an opened figure or a line is drawn, you can specify how to draw the line caps, which can be arrowed, circled, squared, and so on. An open line has two caps, the styles of which can be set via the Pen.StartCap and Pen.EndCap properties.

The following code draws lines with rounded, flat, and squared end caps, as shown in the image:

Line caps.Line caps.

C#

using (var bitmap = new Bitmap(100, 70, PixelFormat.Format24bppRgb, RgbColor.White))

{

    using (Graphics graphics = bitmap.GetGraphics())

    {

        //Horizontal line

        var pen = new Pen(RgbColor.Gray, 1);

        graphics.DrawLine(pen, 10, 20, 90, 20);

        graphics.DrawLine(pen, 10, 50, 90, 50);

        pen.Width = 15;

        //Line with rounded cap

        pen.Color = RgbColor.DarkOrange;

        pen.EndCap = LineCap.Round;

        graphics.DrawLine(pen, 25, 50, 25, 20);

        //Line with flat cap

        pen.Color = RgbColor.DarkGreen;

        pen.EndCap = LineCap.Flat;

        graphics.DrawLine(pen, 50, 50, 50, 20);

        //Line with square cap

        pen.Color = RgbColor.Brown;

        pen.EndCap = LineCap.Square;

        graphics.DrawLine(pen, 75, 50, 75, 20);

        bitmap.Save(@"Images\Output\pen.png");

    }

}

Line Joins

When drawing polylines, the junctions can be painted in different ways: rounded, sharp-edged, beveled, etc., depending on the Pen.LineJoin property value.

The image below shows three possible line joins. The corner in the middle could more or less sharper, depending on the Pen.MiterLimit property value.

Line joins.Line joins.

The LineJoin and MiterLimit properties are also useful if you are styling polylines.

The following code draws the polylines shown in the image above:

C#

using (var bitmap = new Bitmap(105, 35, PixelFormat.Format24bppRgb, RgbColor.White))

{

    using (Graphics graphics = bitmap.GetGraphics())

    {

        var pen = new Pen(RgbColor.DarkOrange, 9);

        //Round corner

        pen.LineJoin = System.Drawing.Drawing2D.LineJoin.Round;

        System.Drawing.Point[] points = {

            new System.Drawing.Point(10, 25),

            new System.Drawing.Point(30, 25),

            new System.Drawing.Point(30, 5)

        };

        graphics.DrawLines(pen, points);

        //Sharp corner

        pen.Color = RgbColor.DarkGreen;

        pen.LineJoin = System.Drawing.Drawing2D.LineJoin.Miter;

        points[0] = new System.Drawing.Point(40, 25);

        points[1] = new System.Drawing.Point(60, 25);

        points[2] = new System.Drawing.Point(60, 5);

        graphics.DrawLines(pen, points);

        //Diagonal corner

        pen.Color = RgbColor.Brown;

        pen.LineJoin = System.Drawing.Drawing2D.LineJoin.Bevel;

        points[0] = new System.Drawing.Point(70, 25);

        points[1] = new System.Drawing.Point(90, 25);

        points[2] = new System.Drawing.Point(90, 5);

        graphics.DrawLines(pen, points);

        bitmap.Save(@"Images\Output\pen.png");

    }

}

15.8.2 Brush

As mentioned above, brushes are used to specify how to fill shapes. Graphics Mill has two brush classes that provide different filling features: solid and hatch brush.

Solid brush is represented by the SolidBrush class, which has only one setting, Color.

Hatch brush is represented by the HatchBrush class. This brush has more settings:

The following code draws the blue ellipse shown in the image:

GDI brushesGDI brushes

C#

using (var bitmap = new Bitmap(100, 60, PixelFormat.Format24bppRgb, RgbColor.White))

{

    using (Graphics graphics = bitmap.GetGraphics())

    {

        var brush = new SolidBrush(RgbColor.Blue);

        graphics.FillEllipse(brush, 10, 5, 80, 50);

        bitmap.Save(@"Images\Output\brush.png");

    }

}

The System.Drawing.Graphics class provides wider support for filling shapes. In particular it has more than 50 hatch styles. For example, the following code draws the shape shown in the image:

GDI+ brushGDI+ brush

C#

using (var bitmap = new Bitmap(100, 60, PixelFormat.Format24bppRgb, RgbColor.White))

{

    using (System.Drawing.Graphics graphics = bitmap.GetGdiPlusGraphics())

    {

        graphics.SmoothingMode = System.Drawing.Drawing2D.SmoothingMode.AntiAlias;

        var brush = new System.Drawing.Drawing2D.HatchBrush(

            System.Drawing.Drawing2D.HatchStyle.LargeConfetti,

            System.Drawing.Color.Blue, System.Drawing.Color.Yellow);

        graphics.FillEllipse(brush, 10, 5, 80, 50);

        var pen = new System.Drawing.Pen(

            System.Drawing.Color.FromArgb(200, 255, 0, 0), 2);

        graphics.DrawEllipse(pen, 10, 5, 80, 50);

        bitmap.Save(@"Images\Output\brush.png");

    }

}

 

16 Process, Thread, Task

16.1 Processus

https://zetcode.com/csharp/process/

On peut lancer un process avec la commande  Process.Start de System.Diagnostics.

Exemples :

Process.Start("notepad.exe");  

ou bien avec un argument :

Process.Start("notepad.exe", @"D:\DocsDiverses\Usinage\adresses.txt");

 

Process.Start("cmd.exe", @"/k dir c:\");

 

De manière plus classique, on crée un nouveau process, on lui attribue le nom du fichier à exécuter et on le lance :

            var pr = new Process();

            pr.StartInfo.FileName = "notepad.exe";

            pr.Start();  

 

Dans ce cas, on peut donner l'argment par :

            pr.StartInfo.Arguments = @"D:\DocsDiverses\Usinage\adresses.txt";  

On tue un processus avec sa méthode Kill(), par exemple après de 3 secondes :

Thread.Sleep(3000);

pr.Kill();  

On accède à tous processus en cours par GetProcesses() :

Process[] tabPr = Process.GetProcesses();

Console.WriteLine("Il y a {0} processus en cours!", tabPr.Length);

Array.ForEach(tabPr, (pr) =>

{

  Console.WriteLine("Process: {0} Id: {1}", pr.ProcessName, pr.Id);

});

Donne :

Il y a 227 processus en cours!

Process: svchost Id: 9048

Process: chrome Id: 15752

Process: svchost Id: 1284

Process: SecurityHealthService Id: 6884

Process: SearchApp Id: 9900

Process: AVerScheduleService Id: 4848

Process: svchost Id: 1708

Process: svchost Id: 2568

Process: chrome Id: 6876

Process: conhost Id: 8168

Process: explorer Id: 7736

Process: svchost Id: 864

Process: Skype Id: 13764

Process: svchost Id: 8160

........

Process: NVDisplay.Container Id: 3024

Process: services Id: 868

Process: svchost Id: 5608

Process: System Id: 4

Process: svchost Id: 11208

Process: Idle Id: 0

 

On accède aux handle des processus en cours d'exécution à partir de leur nom par  GetProcessesByName(..) :

Process[] tabPr = Process.GetProcessesByName("Firefox");

Console.WriteLine("{0} processus Firefox", tabPr.Length);

Array.ForEach(tabPr, (pr) =>

{

  Console.WriteLine("Process: {0} Id: {1}", pr.ProcessName, pr.Id);

});

affiche la liste suivante (alors que le gestionnaire de processus n'en signale qu'un seul) :

11 processus Firefox

Process: firefox Id: 1204

Process: firefox Id: 8500

Process: firefox Id: 12800

Process: firefox Id: 6200

Process: firefox Id: 15880

Process: firefox Id: 11368

Process: firefox Id: 13740

Process: firefox Id: 7632

Process: firefox Id: 13272

Process: firefox Id: 12724

Process: firefox Id: 13856  

Quand il y a beaucoup d'options, on a intérêt à créer en premier un nouveau ProcessStartInfo, puis à remplir tous les paramètres.

Exemple de redirection de la sortie d'un processus de stdout vers un fichier :

var psi = new ProcessStartInfo();

psi.FileName = "cmd.exe";

psi.Arguments = @"/C dir c:\tmp"; // "/C" pour quitter le cmd à la fin

psi.UseShellExecute = false;      // Pas d'UI graphique pour rediriger

psi.RedirectStandardOutput = true;

var pr = Process.Start(psi);

StreamReader reader = pr.StandardOutput;

string data = reader.ReadToEnd(); // On lit la sortie

File.WriteAllText("output.txt", data);  // On l'enregistre dans un fichier  

16.2 Les Threads

Alors que les différents processus s'exécutent chacun dans une mémoire séparée et isolée des autres, et que les processus achevés peuvent perdurer comme zombie (le système ne sait pas qu'ils sont terminés et n'a pas recupéré leurs ressources), un processus donné peut exécuter différents threads (unités de travail) qui vont se partager la mémoire allouée au processus. Les threads achevés ne peuvent pas devenir des zombies. Les threads sont rapides à créer et à détruire.

Un Thread est une unité de base de travail à laquelle le processus alloue de sa mémoire et de son temps processeur. Le système réalise une pseudo-parallélisation du travail entre les divers threads, Mais avec les threads les opérations sont effectuées séquentiellement quel que soit le nombre de cœurs disponibles.

Pour créer un nouveau thread qui va exécuter la méthode void maClasse.vasy() (qui ne prend pas d'argument et ne renvoie rien) on utilise l'instruction :

Thread t = new Thread(new ThreadStart(classe.vasy));  

L'argument new ThreadStart(methode) transforme  void methode() en public delegate void methode() évitant ainsi d'avoir à définir un délégué de signature void methode(), sans argument. La méthode utilisée (ThreadStart) pour faire cette transformation est définie par :

public delegate void ThreadStart() .

Remarque : Une telle méthode qui transforme son argument dans un autre type s'appele un wrapper. Il semble qu'il y a une mise en place automatique du wrapper car son omission ne pose pas de problème. En effet Thread t = new Thread(classe.vasy);  // fonctionne parfaitement

Si la méthode à exécuter par le thread prend un argument, on utilisera le wrapper suivant :

public delegate void ParameterizedThreadStart(object obj) qui prend un objet en argument.

L'omission du wrapper ParameterizedThreadStart est également possible. S'il y a un argument, l'argument obj est passé à thread.Start(valObj);

L'appel thread.Start(); met le thread dans le pool des threads (ThreadPool) d'avant plan où il s'exécute en pseudo-parallèle avec le thread en cours (qui est un thread comme les autres). L'appel t.Join(); donnera au thread t la possibilté de prendre la main en bloquant l'appelant jusqu'à ce qu'il se termine ou soit préempté ou bloqué à son tour.

16.2.1 Exemple sans renvoi d'argument

using System;

using System.Diagnostics;

using System.Threading;

 

public class Example

{

    public static void Main()

    {

        Console.WriteLine("Main Thread {0}: {1}, Priority {2}",

                          Thread.CurrentThread.ManagedThreadId,

                          Thread.CurrentThread.ThreadState,

                          Thread.CurrentThread.Priority);

        var th = new Thread(ArrierePlan);

        th.Start(4500);

        Thread.Sleep(700);

        var chrono = Stopwatch.StartNew();

        Console.WriteLine("Main thread ({0}) démarre chrono",

                  Thread.CurrentThread.ManagedThreadId);

        Thread.Sleep(800);

        Console.WriteLine("Main thread ({0}) exiting...Elapsed {1:N2} seconds",

                          Thread.CurrentThread.ManagedThreadId, chrono.ElapsedMilliseconds / 1000.0);

    }

 

    private static void ArrierePlan(Object _duree)

    {

        int duree = (int) _duree;

        var montre = Stopwatch.StartNew();

        Console.WriteLine("Thread {0}: {1}, Priority {2}",

                          Thread.CurrentThread.ManagedThreadId,

                          Thread.CurrentThread.ThreadState,

                          Thread.CurrentThread.Priority);

        do

        {

            Console.WriteLine("Thread {0}: Elapsed {1:N2} seconds",

                              Thread.CurrentThread.ManagedThreadId,

                              montre.ElapsedMilliseconds / 1000.0);

            Thread.Sleep(500);

        } while (montre.ElapsedMilliseconds <= duree);

        montre.Stop();

    }

}

 

On remarquera que la méthode exécutée par le thread ne renvoie pas d'argument et n'en prend qu'un. D'où des argument d'entrée de type classe pour pouvoir y mettre des données et en récupérer. On peut aussi en donner et en récupérer par des globals, mais ce n'est pas de la bonne programmation.

16.2.2 Exemple avec renvoi d'argument

using System;

using System.Threading;

 

namespace Thread03

{

    class Program

    {

        class Maclasse {

            public int valeur;

            public void add3() {valeur += 3; }

        }

        static void Main(string[] args)

        {

            Maclasse classe = new Maclasse();

            classe.valeur = 5;

            Thread thread = new System.Threading.Thread(new ThreadStart(classe.add3));

            thread.Start(); thread.Join();

            Console.WriteLine(classe.valeur);

        }

    }

}

 

16.2.3 Avant-plan et arrire plan

Normalement les threads crées par un new thread sont, comme le thread principal des threads d'avant-plan. Mais on peut les placer en arrière plan comme les threads du pool (impression,...). Les threads créés par du code non managé (du C par exemple) s'exécutent en arrière plan.

On modifie cette propriété à l'aide de l'attribut IsBackGround (en get/set).

On peut ajouter un thread dans le ThreadPool à l'aide de la méthode :

public static bool ThreadPool.QueueUserWorkItem (System.Threading.WaitCallback callBack, object? state); ou l'argument object? state est facultatif.

Les threads d'arrière-plan ne s'exécutent que quand tous les threads d'avant plan sont bloqués. Quans le thread principal est terminé (ainsi que les threads d'avant plan qu'il a lancés), le programme est fini : les threads d'arrière plan qu'il a lancés et qui ne sont pas finis sont perdus.

16.2.4 Informations sur le thread

var th = Thread.CurrentThread; // permet d'obtenir une référence du thread courant avec laquelle on peut obtenir les informations suivantes  sur le thread :

16.3 Les Tasks

(https://zetcode.com/csharp/task/)

Une Task est un moyen plus récent, utilisé pour exécuter le travail en parallèle de manière asynchrone. Plus simple (?) et plus efficace (?) les tasks exploitent éventuellement la capacité multi-coeur de la machine.

16.3.1 Tâche sans résultat

Quand on veut lancer un travail en arrière plan et qu'on n'a pas besoin de résultat, il suffit de le lancer par un Task.Run. Il sera lancé dans un autre thread sans qu'on ait besoin de le créer, comme ci-dessous :

static void Main(string[] args)

{

  void methode()

  {

    Console.WriteLine($"Thread {Thread.CurrentThread.ManagedThreadId} begin");

    Thread.Sleep(2000);

    Console.WriteLine($"Thread {Thread.CurrentThread.ManagedThreadId} end");

  }

  Action taskMethode = methode;

  Console.WriteLine($"Main thread {Thread.CurrentThread.ManagedThreadId} begin");

  Task t = Task.Run(taskMethode);

  Console.WriteLine($"Main thread {Thread.CurrentThread.ManagedThreadId} end. Taper une touche pour sortir.");

  Console.ReadLine(); // Si tapé avant 5 secondes la tâche ne terminera pas !!!

}

On lance une tâche qui ne fait qu'imprimer le numéro du thread dans lequel elle s'exécute, avec un sommeil de 5 secondes entre 2 impressions. La sortie du main se fait après la frappe d'une touche par l'usager

Main thread 1 begin

Main thread 1 end. Taper une touche pour sortir.

Thread 3 begin

Thread 3 end

 

Appuyez sur une touche pour continuer...

La dernière ligne est générée par le système (exécution par Ctrl+F5). Ci-dessus la frappe de l'usager est intervenue tard, laissant à la tâche le temps de se terminer. Si elle intervient plus tôt la tâche ne se termine pas :

Main thread 1 begin

Main thread 1 end. Taper une touche pour sortir.

Thread 3 begin

 

Appuyez sur une touche pour continuer...

Ci-dessus pas d'impression de "Thread 3 end".

Remarques :

Les méthodes Task.Start, Task.Run... mettent le travail spécifié en file d'attente dans le ThreadPool et renvoie un handle sur la Task .

16.3.2 Tâche avec résultat

Task est associé à une opération qui ne renvoie aucun résultat et ne prend pas d'argument. Dans le cas où on veut utiliser une opération qui renvoie un résultat on utilisera Task<TResult>.

Exemple :

Task<Tret> t = new Task<Tret>(methode); t.Start();

ou plus simple

var t = Task<Tret>.Run(methode);

et le résultat sera donné par l'appel de t.Result. qui va bloquer le thread jusqu'à la réception du résultat. ce qui revient à appeler t.Wait().

Remarque : Task<Tret>.Run(methode) est équivalent à  Task<Tret>.Factory.StartNew(methode)

16.3.3 Tâche avec paramètres

Mauvaise solution : On peut utiliser Task<Tret>.Factory.StartNew(methode, obj) pour lancer la méthode de signature Tret methode(object obj) en batch,  ce qui permet de passer un objet quelconque en argument, objet qui empaquête les paramètres effectifs.

Bonne Solution : On passe par l'intermédiaire d'une méthode anonyme qui ne prend pas d'argument et qui appelle la méthode avec les  paramètres effectifs en arguments :

var t1 = Task<int>.Run(() => methode(arg1, arg2..));  

C'est cette solution qui est à privilégier dans tous les cas, même pour définir la tâche sans la lancer, puis la lancer plus tard, ou bien en utilisant .Factory.StartNew à la place de .Run si on désire ajouter des paramètres de gestion des tâches.

16.3.4 Task asynchrone (async) et await résultat

Tous les exemples précédents qui récupèrent un résultat par t.Result correspondent à déroulement synchrone des opérations qui met le thread courant en attente de la fin de la tâche avant de poursuivre l'éxécution des opérations. Ceci est une très mauvaise utilisation des Tasks qui ont été conçu pour permettre un fonctionnement en mode asynchrone. Pour cela, au lieu de mettre le thread courant en attente en appelant t.Result on utilise le préfixe await devant l'identifiant de la tâche qui renverra le résultat de type Task<Tresult>. Par exemple une méthode qui renvoie un int sera empaquetée, puis lancée comme suit :

Task<int> t = new Task<int>(methode); t.Start();

 et le résultat sera attendu comme ceci :

int val = await t;  

Il est plus simple et recommandé d'écrire tout ceci sous la forme suivante :

int val = await Task.Run(methode);

.NET contient de nombreuses méthodes telles que StreamReader.ReadLineAsync qui lit des enregistrements qui peuvent prendre du temps à arriver ou HttpClient.GetAsync. Ces méthodes exécutent du code d'E/S de manière en mode tache asynchrone. Ils doivent être  utilisés précédés de await.

La procédure qui contient du code avec un ou des appels effectués avec await va prendre du temps et si elle est elle-même appelée, elle peut faire attendre. Cette procédure doit donc, elle-même être appelée par l'intermédiaire d'un await et on signale cela en précisant qu'elle est elle même une tache asynchrone en la préfixant par async Task, et ainsi de suite jusqu'à la procédure main qui au lieu d'être déclaré static void Main() doit être déclaré : static async Task Main()

Dans l'exemple suivant, on lance attent le résultat de la méthode Maclasse.add(int n). Cet exemple montre deux manières de fournir des arguments :

Exemple :

using System;

using System.Threading.Tasks;

 

namespace Thread03

{

    class Program

    {

       class Maclasse {

            public int valeur;

            public int add(int n) { return (valeur+n); }

        }

        static async Task Main(string[] args)

        {

            Maclasse classe = new Maclasse();

            classe.valeur = 5;

            int val = await Task.Run(() => classe.add(10));

            Console.WriteLine(val);

        }

    }

}

 

Remarque : La ligne : int val = await Task.Run(() => classe.add(10));

est un raccourci des instructions suivantes :

  Task<int> t = new Task<int>(() => classe.add(10));

  t.Start();

  int val = await t;  

où on crée la tâche ordinaire t, on la lance et on se met en attente asynchrone sur elle.

16.3.5 Parallélisation

Pour bénéficier de la parallélisation, au lieu d'attendre le résultat dès l'appel de la méthode, comme dans les exemples précédents, il vaut mieux d'abord créer toutes les instances des tâches à effectuer, ce qui permet de démarrer tous les opérations longues le plus tôt possible, puis se mettre en await des résultats qu'après cela.

 

Etant donné une méthode de longue durée) qui prend des arguments, comme celle-ci :

Toto Methode(int arg) {... }  

on transforme cette méthode en Task de la manière suivante :

async Task<Toto> tache(int arg)

{ return await Task.Run(() => Methode(arg)); }

 

qui sera appelée ensuite comme ceci :

Toto val = await tache(2);

Dans le cas où on a plusieurs méthodes lourdes, Toto MethodeO(int), Titi MethodeI(int), Tutu MethodeU(int)

// On crée les instances des tâches

async Task<Toto> tasko(int n){return await Task.Run(() => MethodeO(n),); }

async Task<Toti> tasko(int n){return await Task.Run(() => MethodeI(n),); }

async Task<Totu> tasko(int n){return await Task.Run(() => MethodeU(n),); }

// On attend les résulats

Toto valo = await tasko(2);

Titi vali = await taski(3);

Tutu valu = await tasku(4);

 

Si la méthode ne prend pas d'argument, et n'incorpore pas de await on peut simplement les créer par  Task<Toto>  tache = methode;

16.3.6 Task.WhenAny

 

Pour faire un certain travail dès qu'une des tâches est finie, on peut créer une tâche auxilliaire avec la méthode Task.WhenAny(listeTaches), exemple :

var listeTaches = new List<Task> {tasko, taski, tasku};

while (listeTaches.Count > 0)

{

   Task tachefinie = await Task.WhenAny(listeTaches);

   if (tachefinie == tasko) {...}

   else if (tachefinie == taski) {...}

   else if (tachefinie == tasku) {...}

   await tachefinie; // Conseillé même si semble redondant.

   listeTaches.Remove(tachefinie);

}

Dans les champs {...} on peux récupérer le résultat éventuel de taskx par :

var valx = await taskx;

16.3.7 Task.WhenAll

 

Pour n'effectuer la suite du travail que lorsque toutes les tâches sont finies, à la place de Task.WhenAny ou peut faire appel à la tache auxilliaire Task.WhenAll(tache1, tache2,..), exemple :

await Task.WhenAll(tasko, taski, taski);

...

De même on peux ensuite récupérer le résultat éventuel de taskx par var valx = await taskx;

16.3.8 Task.Delay(t_ms)

Task.Delay(t_ms) crée une nouvelle tâche, qui dort pendant t_ms  millisecondes. On l'utilise dans une méthode de type async Task en se mettant en attente de sa terminaison :

await Task.Delay(2000);  

C'est l'équivalent de Thread.Sleep(t_ms); qui bloque le thread courant, mais ne permet pas la parallélisation. Dans un cadre de Tak, pour accélérer l'exécution globale, il y a intérêt à utiliser await Task.Delay(t_ms);

17 Compléments Visual et C#

17.1 Ajout de référence aux objets

Pour accéder aux bibliothèques externes on passe par le menu contextuel nomduProjet/Ajouter référence C# dans l'explorateur de solutions.

Dans la fenêtre qui s'ouvre on trouvera les bibliothèque dans les rubriques suivantes :

17.2 Accès aux fonctions WIN32

On peut accéder à certaines fonctions W32 comme par exemple GetProcessTimes de kernel32.dll

en rajoutant dans le contexte :

[DllImport("kernel32.dll")]

static extern bool GetProcessTimes(IntPtr hProcess,

 out FILETIME lpCreationTime,

 out FILETIME lpExitTime,

 out FILETIME lpKernelTime,

 out FILETIME lpUserTime);

 

struct FILETIME

{

 public uint DateTimeLow;

 public uint DateTimeHigh;

}

C'est-a-dire, après l'imporatation de la dll, la déclaration du prototype de la fonction à utiliser et éventuellement la définition des types particuliers utilisés, comme ici la struct FILETIME.

Ne pas oublier le :

using System.Runtime.InteropServices;

où ce trouve ce kernel32.

17.3 Appel dll C++ depuis C#

Documentation exhaustive dans : https://learn.microsoft.com/fr-fr/dotnet/framework/interop/marshalling-data-with-platform-invoke

Tuto tiré de https://nico-pyright.developpez.com/tutoriel/vc2005/interop2

Voir aussi partie 1 : https://nico-pyright.developpez.com/tutoriel/vc2005/interop/

ftp://ftp-developpez.com/nico-pyright/tutorial/interop/interop.pdf

ftp://ftp-developpez.com/nico-pyright/tutorial/interop2/interop2.pdf

 

17.3.1 Accès à une structure contenant des entiers

On crée un projet C++ de bibliothèque de liens dynamique (dll), nommé par exemple LibCStruct.

Dans le fichier source pré-généré dllmain.cpp on ajoute  le code qui figure en fin de fichier (après la méthode DllMain).

// dllmain.cpp : Définit le point d'entrée de l'application DLL.

#include "pch.h"

 

BOOL APIENTRY DllMain( HMODULE hModule,

                       DWORD  ul_reason_for_call,

                       LPVOID lpReserved

                     )

{

    switch (ul_reason_for_call)

    {

    case DLL_PROCESS_ATTACH:

    case DLL_THREAD_ATTACH:

    case DLL_THREAD_DETACH:

    case DLL_PROCESS_DETACH:

        break;

    }

    return TRUE;

}

 

typedef struct {

    int telfixe;

    int telPort;

} MASTRUCTURETEL;

 

extern "C" __declspec(dllexport) MASTRUCTURETEL * GetUneStructure()

{

    MASTRUCTURETEL* maStruct = new MASTRUCTURETEL;

    maStruct->telfixe = 123;

    maStruct->telPort = 456;

    return maStruct;

}

On y définit la structure MASTRUCTURETEL et la fonction MASTRUCTURETEL * GetUneStructure().

La génération de la solution génère les fichiers LibCStruct.dll et LibCStruct.lib.

Pour les utiliser depuis C#, on définit un projet C# Application Console (.Net Framework), nommé par exemple CSClientCpp avec le code ci-dessous :

using System;

using System.Runtime.InteropServices;

 

namespace CSclientCpp

{

    class Program

    {

        [DllImport("LibCStruct.dll")]

        public static extern IntPtr GetUneStructure();

 

        [StructLayout(LayoutKind.Sequential)]

        public class MaStructTelCSharp

        {

            public Int32 telFixe;

            public Int32 telMobile;

        }

 

        static void Main(string[] args)

        {

            IntPtr maStructureCUnmanaged = GetUneStructure();

            MaStructTelCSharp maStructureCSharp = new MaStructTelCSharp();

            Marshal.PtrToStructure(maStructureCUnmanaged, maStructureCSharp);

            Console.WriteLine(maStructureCSharp.telFixe);

            Console.WriteLine(maStructureCSharp.telMobile);

            Console.ReadKey();

        }

    }

}


L'entête :

using System.Runtime.InteropServices;

permet l'accès aux methodes de conversion.

La déclaration :

[DllImport("LibCStruct.dll")]

indique l'utilisation de cette librairie

La déclaration :

public static extern IntPtr GetUneStructure();

est le pendant C# de la déclaration

extern "C" __declspec(dllexport) MASTRUCTURETEL * GetUneStructure()

dans le fichier C++.

De même la déclaration de la structure C#

        [StructLayout(LayoutKind.Sequential)]

        public class MaStructTelCSharp

        {

            public Int32 telFixe;

            public Int32 telMobile;

        }

est le pendant de la structure C++ :

typedef struct {

    int telfixe;

    int telPort;

} MASTRUCTURETEL;

Ensuite  on appelle la méthode C++ qui en renvoie un pointeur sur la structure qui est stockée dans un pointeur générique  IntPtr. On crée une structure C# et on utilise le convertisseur spécialisé de structures (pointées)  Marshal.PtrToStructure pour récupérer les bons champs.

La classe Marshal fournit une pléthore de méthodes de conversion. Ici nous avons utilisé :

public static void PtrToStructure<T>(IntPtr ptr, T stDest);

qui convertit les données d’un bloc de mémoire non managée vers un objet managé du type spécifié.
ptr : est le pointeur vers un bloc de mémoire non managée fourni par la DLL.

stDest: est l'objet C# dans lequel les données seront recopiées

T : indique une classe formatée C# quelconque.

 

Tableau de correspondance des types entre API Win32, C standard et C#

 

API Win32

 C standard

C#

 

VOID

void

System.Void

 

HANDLE

void *

System.IntPtr
System.UIntPtr

32 bits Windows 32 bits,
64 bits Windows 64 bits.

BYTE

unsigned char

System.Byte

8 bits

SHORT

short

System.Int16

16 bits

WORD

unsigned short

System.UInt16

16 bits

INT

int

System.Int32

32 bits

UINT

unsigned int

System.UInt32

32 bits

LONG

long

System.Int32

32 bits

BOOL

long

System.Boolean

System.Int32

32 bits

DWORD

unsigned long

System.UInt32

32 bits

ULONG

unsigned long

System.UInt32

32 bits

CHAR

char

System.Char

ANSI.

WCHAR

wchar_t

System.Char

Unicode.

LPSTR

char *

System.String
System.Text.StringBuilder

 ANSI.

LPCSTR

const char *

System.String
System.Text.StringBuilder

ANSI.

LPWSTR

wchar_t *

System.String
System.Text.StringBuilder

Unicode.

LPCWSTR

const wchar_t *

System.String System.Text.StringBuilder

Unicode.

FLOAT

float

System.Single

32 bits

DOUBLE

double

System.Double

64 bits

18 WPF et 3D

Livre Kindle sur WPF 3D de Rod Stephens ici : C:\Users\Utilisateur\Documents\My Kindle Content, mais lisible avec C:\Users\Utilisateur\AppData\Local\Amazon\Kindle\application\Kindle.exe

Les WPF constituent un ensemble de programmation d'application de bureau pour les PCs sous Windows, qui est concurent de Windows Form. Il est plus ancien, semble-t-il, que Windows Form et il a pour particularité de favoriser la séparation entre l'apparence visuelle qui serait plutot programmée en xaml et les fonctionalités qui seraient programmées en c#.

Un des intérêts de WPF est qu'il possède des API puissantes pour le graphique 3D.

Remarque : Les classes Vector3D, Points3D... sont accessibles dans les applications Console et Windows Form en ajoutant using System.Windows.Media.Media3D; et en ajoutant une référence à l'assembly (Framework) PresentationCore.

18.1 Mathématique 3D

Espace vectoriel : Vector3D

Addition/Soustraction : Vector3D ± Vector3D => Vector3D

Multiplication/division scalaire : n * Vector3D * m => Vectord3D , Vector3D / m => Vectord3D

Vector3D.AngleBetween(u, v); => angle en degrés

Vector3D cross = Vector3D.CrossProduct(u, v);

double dot = Vector3D.DotProduct(u, v);

v.LengthSquared => ||v||2

v.Length => ||v||

v.Normalize() => v / ||v||

Espace Ponctuel : Point3D

Point3D ± Vector3D => Point3D

Rotation : Rotation3D

Les rotations sont gérées par la classe générique Rotation3D, dont le principal représentant est :

Rotation3D AxisAngleRotation3D(Vector3D axis, double angle);

 

 

Transformation :  Transform3D

C'est une classe générique qui gère les transformations dont l'action s'apparente à celle d'une matrice homogène 4x4 (Matrix3D), à savoir : rotation, placement, grandissement, cisaillement,...

Les transformations héritées de Transform3D s'appliquent au moyen de la méthode Transform(-) à :

Une rotation dont le centre n'est pas spécifié fera une rotation autour de l'origine O. Quand on fait une rotation d'un morceau de forme de centre A, il faut spécifier le point A comme centre de rotation pour faire tourner les points M autour de A (rotation du vecteur AM), sinon on fera tourner les points M autour de O (rotation du vecteur OM), ainsi que le point A qui sera donc déplacé.

Rotation :

Transform3D RotateTransform3D(Rotation3D rotation, Point3D center);  

Translation :

Transform3D TranslateTransform3D(double dx, double dy, double dz)  

Mise à l'échelle :

Transform3D st = ScaleTransform3D(double gX, double gY, double gZ);  

Composition :

Pour composer plusieurs transformations successives, on crée un Transform3DGroup auquel on ajoute les transformation3D élémentaires par le biais de la méthode Add() :

Transform3DGroup groupT = new Transform3DGroup();

groupT.Add(t3D_1); groupT.Add(t3D_2); ...

18.2 Utiliser WPF dans une Windows Form

(Voir D:\MesProgs_Langages\MICROSOFT_VISUAL_STUDIO\C#\WF2WPF)

On peut utiliser les classes WPF dans une Windows Form, sans code xaml, mais c'est assez complexe. Il existe pour cela dans Windows Form une classe ElementHost qui permet d'héberger un élément de WPF. Voir les 3 étapes suivantes :

1) Ajouter les références suivantes au projet (on les trouve dans la liste Assemblys/Framework)

 

2) Créer la classe Form suivante :

    public class MaFormWPF : Form

    {

        public MaFormWPF()

        {

            var elementHost = new ElementHost();

            elementHost.Dock = DockStyle.Fill;

 

            var myWpfControl = new MyWpfControl();

            elementHost.Child = myWpfControl;

 

            this.Controls.Add(elementHost);

            this.Text = "Windows Forms avec WPF 3D";

        }

    }

Cette Form crée un  elementHost qu'elle ajoute dans la Form (elementHost.Dock = DockStyle.Fill;). A cet elementHost on ajoute comme enfant la fenêtre WPF qui hérite de System.Windows.Controls.UserControl :

    public class MyWpfControl : System.Windows.Controls.UserControl

    {

        public MyWpfControl()

        {

            // Exemple avec un objet 3D

            var viewport = new Viewport3D();

            ModelVisual3D visual3d = new ModelVisual3D();

            Model3DGroup group3d = new Model3DGroup();

            visual3d.Content = group3d;

            viewport.Children.Add(visual3d);

 

            // camera, lights, and model.

            DefineCamera(viewport);

            DefineLights(group3d);

            DefineModel(group3d);

            this.Content = viewport;

        }

 

        // Define the camera.

        private void DefineCamera(Viewport3D viewport) {...}

 

        // Define the lights.

        private void DefineLights(Model3DGroup group){...}

 

        // Define the model.

        private void DefineModel(Model3DGroup group){...}

    }

3) Lancer l'éxécution de la Windows Form :

Pour cela on peut la lancer à la place de la Form1 créée par défaut, en la supprimant et en mettant le nom de notre Form dans la class Program :

    static class Program

    {

        [STAThread]

        static void Main()

        {

            Application.EnableVisualStyles();

            Application.SetCompatibleTextRenderingDefault(false);

            Application.Run(new MaFormWPF());

        }

    }

(voir exemple D:\MesProgs_Langages\MICROSOFT_VISUAL_STUDIO\C#\WF2WPF)

Ou bien, on laisse la Form1 telle que et on ajoute un bouton dans cette Form1 pour lancer notre WPF, en créant  dans la callback comme ceci :

        private void button1_Click(object sender, EventArgs e)

        {

            new MaFormWPF().Show(); // Modale (ne bloque pas. On peut en créer plusieurs)

            // new MaFormWPF().ShowDialog(); // Non modal (bloque l'appelante)

        }

(voir exemple D:\MesProgs_Langages\MICROSOFT_VISUAL_STUDIO\C#\WF2WPF_02).

18.3 Application minimale WPF

Une application classique WPF comprend en premier un couple de fichiers App.xaml et App.xaml.cs :

Fichier App.xaml :

<Application x:Class="WpfMinimale.App"

             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"

             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"

             xmlns:local="clr-namespace:WpfMinimale"

             StartupUri="FenetrePpale.xaml">

    <Application.Resources>

         

    </Application.Resources>

</Application>

Il déclare les adresses des patrons et fournit le nom de la classe à démarrer :  FenetrePpale

Fichier App.xaml.cs :

using System.Windows;

namespace WpfMinimale

{

 

    public partial class App : Application

    {

    }

}

Il ne fait que déclarer que l'application hérite de la classe Application.

La classe à démarrer FenetrePpale est généralement décrite par le couple FenetrePpale.xaml et  FenetrePpale.xaml.cs.

FenetrePpale.xaml :

<Window x:Class="WpfMinimale.MainWindow"

        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"

        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"

        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"

        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"

        xmlns:local="clr-namespace:WpfMinimale"

        mc:Ignorable="d"

        Title="Fenêtre principale" Height="450" Width="800">

    <Grid>        

    </Grid>

</Window>

Le fichier déclare les patrons, la dimension de la fenêtre principale et le contenu de la fenêtre entre les balises <Grid> ... </Grid> qui ici ne contiennent rien.

FenetrePpale.xaml .cs:

using System.Windows;

namespace WpfMinimale

{

    public partial class MainWindow : Window

    {

        public MainWindow()

        {

            InitializeComponent();

        }

    }

}

Le fichier déclare que cette fenêtre hérite de Window. Le constructeur appelle la fonction d'initialisation standard.

Voici le résultat :

18.4 Usage du conteneur Grid en C#

La mainWindow ne peut avoir qu'un contenu qui lui est affecté par :

this.Content = element;  

Si on veut mettre plusieur éléments on va mettre en premier un conteneur, par exemple un Grid, un StackPanel, un Canvas... à l'intérieur duquel on pourra mettre de nombreux autres éléments. Le Grid est un des plus employés, mais son utilisation est assez complexe. Les instructions :

grd = new Grid(); this.Content = grd;  

permettent de le créer et de l'affecter à la mainWindow. Ensuite il faut définir toutes les lignes et les colonnes de notre grille (la première est définie par défaut, si elle est seule)

RowDefinition l0 = new RowDefinition();

RowDefinition l1 = new RowDefinition();

....

ColumnDefinition c0 = new ColumnDefinition();

ColumnDefinition c1 = new ColumnDefinition();

...

Si les dimensions des cellules ne sont pas égales, on peut différencer les dimensions en utilisant :

l0.Height = new GridLength(1, GridUnitType.Star);

l1.Height = new GridLength(5, GridUnitType.Star);

pour que la ligne l1 soit 5 fois plus haute que la ligne l0, et de même pour la largeur des colonnes.

Pour mettre un élément dans la cellule (li, co), on fera :

Grid.SetRow(element, li); Grid.SetColumn(element, co);

grd.Children.Add(element);

où on précise à l'aide des propriétés statiques SetRow et SetColumn la cellule destination (étrange mais c'est comme ça !).

18.5 Application minimale 3D

Modifier le fichier MainWindows.xaml comme suit :

Dans le champ <Window ...> ajouter la ligne :

Loaded="AuChargement"

qui spécifie la méthode à activer au chargement de la fenêtre

Modifier le fichier MainWindows.xaml.cs comme suit :

using System;

using System.Windows;

using System.Windows.Media;

using System.Windows.Media.Media3D;

 

namespace _3dMini

{

    public partial class MainWindow : Window

    {

        Viewport3D mainViewport;

        public MainWindow()

        {

            InitializeComponent();  

            Grid grid = new Grid();

            this.AddChild(grid);

            mainViewport = new Viewport3D();

            grid.Children.Add(mainViewport);

        }

 

        private void AuChargement(object sender, RoutedEventArgs e)

        {

          // Création des objets ModelVisual3D et Model3DGroup.

            ModelVisual3D visual3d = new ModelVisual3D();

            Model3DGroup group = new Model3DGroup();

          // Le  Model3DGroup est mis dans le ModelVisual3D

            visual3d.Content = group;

          // Le  ModelVisual3D est mis dans le Viewport3D

            monViewport.Children.Add(visual3d);

 

          // Definition Camera

            Point3D p = new Point3D(1.5, 2, 3);

            Vector3D lookDirection = new Vector3D(-p.X, -p.Y, -p.Z);

            Vector3D upDirection = new Vector3D(0, 1, 0);

            double fieldOfView = 60;

            PerspectiveCamera camera =

                new PerspectiveCamera(p, lookDirection, upDirection, fieldOfView);

            monViewport.Camera = camera;

          // Définition des lumières

            group.Children.Add(new AmbientLight(Colors.Gray));

            Vector3D direction = new Vector3D(1, -2, -3);

            group.Children.Add(new DirectionalLight(Colors.Gray, direction));

          // Définition géométrie du modèle qui sera insérée dans un maillage MeshGeometry3D

            // Liste des points

            Point3D[] points =

            {

                new Point3D(0, 1, 0),  // Point indice 0

                new Point3D(0, 0, 1), // Point indice 1

                new Point3D(0.87, 0, -0.5), // Point indice 2

                new Point3D(-0.87,0, -0.5) // Point indice 3

            };

            // Liste des triangles (index des points aux sommets)

            Tuple<int, int, int>[] triangles =

            {

                 new Tuple<int, int, int>(0, 1, 2),

                 new Tuple<int, int, int>(0, 2, 3),

                 new Tuple<int, int, int>(0, 3, 1),

                 new Tuple<int, int, int>(1, 3, 2),

            };

            // Le maillage du tout

            MeshGeometry3D mesh = new MeshGeometry3D();

            // Ajout des points au maillage

            foreach (Point3D point in points) mesh.Positions.Add(point);

            // Ajout des triangles au maillage

            foreach (Tuple<int, int, int> tuple in triangles)

            {

                mesh.TriangleIndices.Add(tuple.Item1);

                mesh.TriangleIndices.Add(tuple.Item2);

                mesh.TriangleIndices.Add(tuple.Item3);

            }

          // Le MeshGeometry3D est inséré dans un GeometryModel3D

            GeometryModel3D model = new GeometryModel3D(mesh, new DiffuseMaterial(Brushes.LightBlue));

          // qui est ajouté au Model3DGroup (contenu dans le visual3d.Content)

            group.Children.Add(model);

        }

    }

}

 

Si on isole les créations des divers contenus dans des modules, on a la structure générale suivante pour la méthode chargée :

        private void AuChargement(object sender, RoutedEventArgs e)

        {

            // Définition des objets WPF

            ModelVisual3D visual3d = new ModelVisual3D();

            Model3DGroup group = new Model3DGroup();

            visual3d.Content = group;

            mainViewport.Children.Add(visual3d);

 

            // Défiition caméra, lumière et modèle

            DefineCamera(monViewport);

            DefineLights(group);

            DefineModel(group);

        }

18.6 Le Viewport3D et son contenu et la camera

18.6.1 Création et attachement du Viewport3D

Pour dessiner en 3D il faut créer un Viewport3D et l'affecter à la MainWindow ou à son conteneur racine.

18.6.1.1 Code xaml minimal

Dans les exemple suivants le code xaml est réduit à son minimum:

<Window x:Class="SDKexample.MainWindow"

        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"

        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"

        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"

        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"

        xmlns:local="clr-namespace:SDKexample"

        mc:Ignorable="d"

        Title="MainWindow">

</Window>

Ensuite on aura le code c# suivant avec le viewport directement dans la MainWindow :

        Viewport3D mainViewport;

        public MainWindow()

        {

            InitializeComponent();  

            viewport = new Viewport3D();

            this.Content = viewport;

            .....

        }

ou bien dans un conteneur racine. Ci-dessous il est mis dans un conteneur Grid.

        Viewport3D mainViewport;

        public MainWindow()

        {

            InitializeComponent();  

            Grid grid = new Grid();

            this.Content = grid;

            viewport = new Viewport3D();

            grid.Children.Add(viewport);

        }

Au lieu de mettre ce code dans le constructeur de la MainWindow, on peut le mettre dans une méthode appelée par le constructeur au chargement :

        public MainWindow()

        {

            InitializeComponent();

            this.Loaded += AuChargement;

        }

 

        public void AuChargement(object sender, RoutedEventArgs e)

        {

            Viewport3D myViewport3D = new Viewport3D();

            this.Content = myViewport3D;

            ModelVisual3D myModelVisual3D = new ModelVisual3D();

            myViewport3D.Children.Add(myModelVisual3D);

 

On peut également créer le viewport dans une autre fenêtre de type Dialog :

    public partial class MainWindow : Window

    {

        public MainWindow()

        {

            InitializeComponent();

            Window maWin = new Window();

            maWin.Title = "WPF dans un Dialog";

            maWin.Width = 400;

            maWin.Height = 300;

            maWin.Content = CScene3D.MonViewport();

            maWin.ShowDialog();

        }

    }

 

    class CScene3D

    {

        public static Viewport3D MonViewport()

        {

            Viewport3D myViewport3D = new Viewport3D();

            ModelVisual3D myModelVisual3D = new ModelVisual3D();

            myViewport3D.Children.Add(myModelVisual3D);

            ...

         }

    }

 

18.6.1.2 Chargement dans xaml

<Window x:Class="_3dMini.MainWindow"

        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"

        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"

        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"

        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"

        xmlns:local="clr-namespace:_3dMini"

        mc:Ignorable="d"

        Title="MainWindow" Height="450" Width="800"

        Loaded="AuChargement">

</Window>

Code c# :

       public MainWindow()

        {

            InitializeComponent();

        }

        private void AuChargement(object sender, RoutedEventArgs e)

        {

            Viewport3D myViewport3D = new Viewport3D();

            this.Content = myViewport3D;

            ModelVisual3D visual3d = new ModelVisual3D();

            myViewport3D.Children.Add(visual3d);

      ....

        }

18.6.1.3 Chargement et Viewport dans xaml

<Window x:Class="_3dMini.MainWindow"

        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"

        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"

        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"

        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"

        xmlns:local="clr-namespace:_3dMini"

        mc:Ignorable="d"

        Title="MainWindow" Height="450" Width="800"

        Loaded="AuChargement">

    <Grid>

        <Viewport3D Name="monViewport" />

    </Grid>

</Window>

Code c# :

       public MainWindow()

        {

            InitializeComponent();

        }

        private void AuChargement(object sender, RoutedEventArgs e)

        {

            ModelVisual3D visual3d = new ModelVisual3D();

            monViewport.Children.Add(visual3d);

      ....

        }

18.6.2 Le contenu du Viewport :  ModelVisual3D et Camera

La scène qui sera affichée dans le Viewport3D est construite à partir de deux objets : la scène proprement dite construite dans un (ou plusieurs) objet(s) ModelVisua3D et le système de projection qui est soit une  OrthographicCamera ou une PerspectiveCamera.

Le(s)  ModelVisua3D est(sont) attaché(s) au Viewport3D en tant qu'enfant :

  ModelVisual3D visual3d = new ModelVisual3D();

  viewport.Children.Add(visual3d);  

 

La camera directemente affectée à la propriété Camera du Viewport3D :

  PerspectiveCamera camera = new PerspectiveCamera(...);

  viewport.Camera = camera;

 

18.7 Construction d'une scène 3D

La scène 3D qui sera affectée au ModelVisual3D est constituée d'une hiérachie de classes dont les principales sont :

Attention, la propriété Content = remplace tout le contenu existant, y compris le contenu incorporé via des Children.Add().

La figure suivante résume la hiérarchie d'une scène 3D :

Certaines éléments sont passés directement en argument au constructeur comme Geometry et Material au GeometryModel3D ou la Camera au Viewport3D.

La Camera peut également être affectée plus tard au Viewport3D.

La plupart des éléments sont passés via un Children.Add(enfant) aux parents qui acceptent plusieurs enfants.

Comme on peut le voir dans l'arborescence un modèle GeometryModel3D est constitué d'un maillage MeshGeometry3D et d'un matériau qui peut intégré plusieurs éléments primaires. Un modèle n'a qu'un maillage, plus ou moins complexe, et un matériau également plus ou moins complexe. Dès qu'on veut utiliser un autre matériau de revêtement on est obligé de définir un autre modèle GeometryModel3D, qui aura son maillage et son matériau.

18.7.1 Le MeshGeometry3D

C'est la classe de base qui permet de construire les formes les plus complexes à partir de facettes tringulaires décrites par des éléments primaires simples (Point3D, Vector3D...). On ne peut pas lui ajouter un enfant du même type qu'elle comme c'est le cas pour des élémnts de plus haut niveau.

Sa propriété Position permet de stocker les Point3D qui sont utilisés pour définir sa géométrie :

        foreach (Point3D point in points) mesh.Positions.Add(point);

et sa propriété TriangleIndices est utilisée pour stocker les Tuple des index des points qui définissent les facettes (triangles) :

        foreach (Tuple<int, int, int> tuple in triangles)

        {

          mesh.TriangleIndices.Add(tuple.Item1);

          mesh.TriangleIndices.Add(tuple.Item2);

          mesh.TriangleIndices.Add(tuple.Item3);  

        }  

Ses principales propriétés sont :

Positions : Une Point3DCollection dans laquelle on va ajouter tous les sommets Point3D de notre forme. Ces points sont indicés de 0 à Positions.Count.

TrianglesIndices : une Int32Collection des index des sommets des triangles (pris dans les Positions) . TrianglesIndices.Count doit être un multiple de 3 ! Ces 3 indices servent à calculer la normale extérieure à la face par le produit AB^AC où A, B et C sont les 3 points correspondants aux 3 indices consécutifs. Cette normale extérieure n'est pas référencée, on ne peut dons pas y accéder, mais c'est elle qui détermine si la face est dessinée ou non. Si l'angle entre sa direction et celle de la caméra est aigu, la surface n'est pas dessinée, quel que soit l'éclairage.

Pour qu'un triangle soit visible des 2 cotés, il faut spécifier 2 fois l'ordre de ses 3 sommets, une fois dans chaque sens.

En plus des Positions et TrianglesIndices on éventuellement peut spécifier :

Normals : Une Vector3DCollection des directions des normales à la forme en chaque sommet Positions qui seront utilisées pour calculer l'intensité de la couleur du rendu de la zone autour de chaque sommet. Si l'utilisateur ne les définit pas, elle n'existent pas, mais le rendu utilise les normales extérieures pour calculer l'intensité de la couleur de chaque face visible, qui sera noire en l'absence de lumière ambiante et si la face visible reçoit pas de lumière directe.

Si un sommet est commun à 2 faces, ses normales (fournies par l'usager ou calculées pour les 2 faces) seront moyennées. Si on ne veut pas de l'effet de lissage que produit ce moyennage, il faut définir plusieurs fois le sommet pour chacune des faces. Un cube par exemple doit avoir des normales différenciées en ses sommets, alors que dans le cas du maillage serré d'une surface, ce n'est pas nécessaire et même déconseillé car cela nuit au lissage.

Si on double une face en ajoutant 3 indices en sens inverses sans ajouter de nouveaux sommets, on aura une incohérence dans le rendu de la couleur des 2 faces :

 

TextureCoordinates : Une PointCollection (2D) des coordonnées (u,v) affectées à chaque point Positions, en considérant qu'ils appartiennent à une surface paramétrée par ces coordonnées, avec 0= u <= 1 et 0 <= v <= 1. On doit donc avoir autant de TextureCoordinates que de Positions : (TextureCoordinates.Count = Positions.Count). A titre d'exemple, dans le cas de 4 points A, B, C, D constituants un rectangle, on peut leur attribuer les coordonnées suivantes aux 4 TextureCoordinates : Point(0,0), Point(1,0), Point(1,1) et Point(0,1).

Cela permet de coller une image ou une texture à une surface 3D en l'adaptant à la surface de manière à faire coïncider :

18.7.1.1 Transformation d'un MeshGeometry3D

Contrairement aux type de plus haut niveau, le MeshGeometry3D ne possède pas de propriété Transform qui permettrait de déplacer tout le mesh.

Pour déplacer tout un mesh il faut déplacer tous ses points et toutes ses normales par le biais d'une Transform3D ou d'une Matrix3D. Pour cela on peut opérer comme suit :

Il est commode de convertir les Positions de  Point3DCollection en tableau Point3D[] (et éventuellement ses normales) comme ceci :

Point3D[] points = mesh.Positions.ToArray();
Vector3D[] normals = mesh.Normals.ToArray();

puis on peut appliquer la Transform3D/Matrix3D  :

transfmat.Transform(points);
transfmat.Transform(normals);

et ensuite on réintègre ces points (et éventuellement les normales) au mesh :

mesh.Positions = new Point3DCollection(points);
mesh.Normals = new Vector3DCollection(normals);

18.7.1.2 Animation d'un MeshGeometry3D

Pour animer un MeshGeometry3D il faut :

 DispatcherTimer timer = new DispatcherTimer();

 timer.Tick += maCallback;

 timer.Interval = new TimeSpan(0, 0, 0, 0, 20);// ttes les 20 ms

 timer.Start();

Exemple de transformation qui va faire tourner le mesh : Une rotation rotation de 2° par rapport à l'origine autour de l'axe y, à chaque tick.

CubeRotator = RotateTransform3D(new AxisAngleRotation3D(new Vector3D(0, 1, 0), 2), new Point3D(0, 0, 0));

18.7.2 Le GeometryModel3D (géometrie + matière)

C'est une classe dont l'objet peut-être  ajouté en tant qu'enfant à un ModelVisual3D ou à un Model3DGroup.

Les deux principales propriétés d'un GeometryModel3D sont sa Geometry de type MeshGeometry3D et son Material qui lui sont passés à sa construction :

MeshGeometry3D mesh = new MeshGeometry3D();

Material material = new DiffuseMaterial(Brushes.LightBlue);

GeometryModel3D model3d= new GeometryModel3D(mesh, material);  

Ici le corps est composé d'une seule matière. Lorsque son aspect plus complexe fait appel à plusieurs matières, celles-ci son agglomérées dans un MaterialGroup qui est passé au  GeometryModel3D.

Le GeometryModel3D possède une propriété Transform à laquelle on peut affecter une Transform3D qui lui sera appliquée. Exemple pour faire tourner tout le groupe d'un angle (ang)  autour de l'axe (new Vector3D(1, 0, 0)) passant par le point (new Point3D()) :

monModel.Transform = new RotateTransform3D(

        new AxisAngleRotation3D(new Vector3D(1, 0, 0), ang), new Point3D());

18.7.3 Le Model3DGroup

C'est une classe dont l'objet peut-être  ajouté en tant qu'enfant à un ModelVisual3D et qui peut contenir en tant qu'enfant d'autres Model3DGroup, des géométries GeometryModel3D et des éclairages. Généralement on n'ajoute des éclairages qu'au Model3DGroup racine.

On peut vérifier la présence de l'éclairage dans un Model3DGroup en appelant:

bool group.Children.Contains(light)  

et retirer cet éclairage en appelant group.Children.Remove(light);

De plus chaque Model3DGroup possède, comme les GeometryModel3D, une propriété Transform à laquelle on peut affecter une Transform3D qui est intéressante dans le cas des articulations d'une chaine articulée.  La transformation s'applique à la scène définie dans le Model3DGroup et à tous ses enfants.

18.7.4 Le ModelVisual3D

C'est une classe dont l'objet peut-être  ajouté en tant qu'enfant à un Viewport3D et qui n'a qu'un contenu qui lui est affecté par sa propriété Content. Mais un ModelVisual3D parent peut contenir plusieurs ModelVisual3D enfants ajoutés à l'aide de Children.Add(), mais la scène véritablement ajoutée à un enfant (ou a un parent sans enfant) l'est par le biais de la proprieté Content = . Ce qui est affecté par cette propriété remplace les éventuels ajouts effectués par des Children.Add().

A l'aide de Content on ajoute soit un modèle complexe de type Model3DGroup (avec éclairage), soit un modèle  élémentaire de type GeometryModel3D (sans éclairage).

Exemple : On ajoute à visual3D soit un simple  GeometryModel3D

  GeometryModel3D model3D = new GeometryModel3D();

  visual3d.Content = model3D;

soit un objet composite de type  Model3DGroup :

  Model3DGroup group = new Model3DGroup();

    visual3d.Content = group;

auquel on ajoutera le GeometryModel3D simple :

  GeometryModel3D model3D = new GeometryModel3D();

  group.Children.Add(model3D);

et l'éclairage du modèle :

  group.Children.Add(new AmbientLight(Colors.Gray));

  group.Children.Add(new DirectionalLight(Colors.Gray, new Vector3D(0, 0, -1)));

18.8 Le point de vue (Camera)

18.8.1 Définition du point de vue

Nous avons précédemment qu'une camera devait être affectée au Viewport  via sa propriété Camera :

viewport.Camera = camera;

 

Il y a deux principales caméra de projection : l'OrthographicCamera et la PerspectiveCamera.

L'OrthographicCamera  réalise une projection orthogonale. Elle est crée par :

OrthographicCamera camera =

    new OrthographicCamera(position, lookDirection, upDirection, width);

 

Le constructeur spécifie :

Ces propriétés sont également accessibles en get et set. On peut également spécifier camera.NearPlaneDistance et camera.FarPlaneDistance qui sont des plans de coupe avant et arrière.

La  PerspectiveCamera réalise une projection en perspective. Elle est crée par :

PerspectiveCamera camera =

    new PerspectiveCamera(position, lookDirection, upDirection, fieldOfView);

 

Le constructeur spécifie :

Ces propriétés sont également accessibles en get et set. Comme dans le cas précédent on peut également spécifier camera.NearPlaneDistance et camera.FarPlaneDistance qui sont les plans de coupe avant et arrière.

18.8.2 Gestion du point de vue

Pour gérer le point de vue, on modifie les propriétés de la caméra (les paramètres passés ou non au constructeur) par le biais d'écouteurs d'événements générés par les actions de l'utilisateur. Ces d'événements sont classés en UIElement parmi lesquels on utilisera principalement :

Remarque : Pour voir si la touche Shift ou Ctrl... est pressée en même temps qu'un autre événement, on peut le savoir en calculant le booléen suivant (dans le cas du Shift) :

bool s = Keyboard.IsKeyDown(Key.LeftShift) || Keyboard.IsKeyDown(Key.RightShift);

puis en testant la valeur de s lors du traitement de l'autre événement.

Les gestionnaires d'événements sont installés comme suit :

Widget.PreviewKeyDown += Callback_KeyDown;

Widget.PreviewMouseWheel += Callback_MouseWheel;

Widget.MouseDown += Callback_MouseDown;

Widget.MouseMove += Callback_MouseMove;

Widget.MouseUp += Callback_MouseUp;

et désintallés en mettant un "-" à la place du "+".

Les gestionnaires peuvent être associés à pratiquement tous les widgets, mais généralement on associe les événements clavier à la classe Window mère (le this de la MainWindow) et pour la souris ce sera généralement le conteneur racine de la fenêtre, c'est ce qui est fait par Rod Stephens dans ces exemples, mais bizarrement les actions sur la souris ne sont pris en compte que lorsqu'un moèle3D se trouve à l'endroit du clic, sinon il est ignoré. On peut éviter cela en associant les événements souris à la MainWindow, comme pour le clavier.

18.9 Les couleurs (Colors) et transparence

Le rendu de la couleur dépend de trois éléments : La couleur de la lumière, la couleur de l'objet, la direction de la lumière par rapport à l'objet.

La couleur elle même est un objet de la classe Colors qui peut être défini par 3 ou 4 bytes (0 à 255), qui sont :

Color.FromArgb(64, 128, 255, 128);    // vert pâle très transparent (64)

Color.FromArgb(192, 128, 255, 128);   // vert pâle relativement opaque (192)

Color.FromArgb(255, 128, 255, 128);   // vert pâle totalement opaque (255)

Color.FromRgb(128, 255, 128);         // vert pâle totalement opaque (255)

 

Pour que la transparence soit bien gérée, il faut insérer les objets transparents en dernier.

 

Il y a également beaucoup de couleurs précédfinie comme  Colors.Gray,  Colors.Red,  Colors.LightBlue...

Général, on utilise la couleur  Colors.Gray, Colors.LightGray, Colors.DarkGray ou Colors.White pour les lumières afin qu'elle ne dénature pas la couleur des objets. En effet si on éclaire en bleu un objet rouge, il ressortira totalement noir !

On peut utiliser tous les différents types de lumières dans une scène et même utiliser plusieurs instances du même type de lumière. Toutes les lumières d'une scène s'additionnent sur les surfaces qu'elles affectent.

Le niveau des composantes R,V, B de la couleur d'un objet indiquent la proportion de lumière qu'elle renvoit de chaque composante qu'elle reçoit. S'il reçoit du blanc, sa teinte (hue) naturelle sera perçue.

18.10 Les éclairages

Il y a quatre type d'éclairage : l'éclairage par la lumière ambiante, par une lumière directionnelle, par une lumière ponctuelle, par un spot:

18.10.1 Lumière ambiante (AmbientLight)

L' AmbientLight est une lumière ambiante présente partout qui n'a qu'une seule propriété, sa couleur. On l'ajoute directement au  Model3DGroup.

group.Children.Add(new AmbientLight(Colors.Gray));

 

Elle éclaire uniformément dans toutes les directions tous les objets de la scène.

 

18.10.2 Lumière directionnelle (DirectionalLight)

Un DirectionalLight est un flux lumineux de rayons parallèles provenant de l'infini et dirigé selon une direction specifiée par Vector3D. On l'ajoute directement au  Model3DGroup.

group.Children.Add(new DirectionalLight(Colors.Gray, direction));

 

La couleur reflétée par un corps dépend de sa couleur propre, de la couleur de la lumière et de l'angle d'incidence de la direction de la lumière sur la surface du corps.

18.10.3 Lumière ponctuelle (PointLight)

Un PointLight émet de la lumière dans toute les directions, à partir d'une source ponctuelle invisible. On l'ajoute directement au  Model3DGroup.

group.Children.Add(new PointLight(color, new Point3D(x, y, z)));

Elle agit comme un ampoule omnidirectionnelle mais qu'on ne voit pas. Pour simuler sa présence, il faut mettre un objet à son emplacement avec des propriétés adéquates.

L'intensité reçue par les objets diminue avec la distance à la source. L'atténuation est fonction des propriétés ConstantAttenuation (C), LinearAttenuation (L) et QuadraticAttenuation (Q) selon la formule :

C + L·d + Q·d2

ou d est la distance d'un triangle  à la source avec par défaut C = 1, L = 0 et Q = 0, alors que dans le monde réel ce serait plutot  C = 0, L = 0 et Q = 1.

En fait , la couleur rendue est uniforme par facette (triangle). Si on veut voir l'effet de la distance à la source sur une surface éclairée par une lumière ponctuelle, il faut subdiviser cette surface en un grand nombre de triangles.

Il existe également la propriété Range qui permet de limiter la distance d d'action de cette source lumineuse.

18.10.4 Lumière Spot (SpotLight)

Un Spotlight émet depuis une source ponctuelle de la lumière limitée dans un cone. On l'ajoute directement au  Model3DGroup.

group.Children.Add(new SpotLight(couleur, position, direction, angE, angI));

 

angE est l'angle au sommet Extérieur  du cône en degrés (45° par exemple) et angI est l'angle au sommet Intérieur de la partie intense du faisceau (30° par exemple). Le constructeur par défaut ne prend aucun paramètre et crée une lumière par défaut avec InnerConeAngle = 180 et OuterConeAngle = 90.

Les propriétés ConstantAttenuation, LinearAttenuation, QuadraticAttenuation et  Range s'appliquent au Spotlight.

18.11 Revêtement du GeometryModel3D

Les corps représentés sur la scène sont des GeometryModel3D. Un GeometryModel3D est constitué d'une MeshGeomtry3D qui décrit sa géométrie et d'un Material ou MaterialGroup qui est le materiau de son revêtement.

MeshGeometry3D mesh = new MeshGeometry3D();

Material material = new DiffuseMaterial(Brushes.LightBlue);

GeometryModel3D model3d= new GeometryModel3D(mesh, material);  

Si on veut attribuer à une grande surface différents revêtements, soit on construit un revêtement composite (avec un éditeur d'image par exemple) et on associe ce revêtement unique au GeometryModel3D, soit on ajoute au Model3DGroup plusieurs GeometryModel3D ayant chacun son propre revêtement , car chaque GeometryModel3D ne peut en avoir qu'un seul  Material ou MaterialGroup. Cet objet MaterialGroup peut prêter à confusion, car bien qu'il permette de combiner plusieurs Material(s) au niveau rendu, il ne permet pas de les juxtaposer.

18.11.1 Les matériaux (Material+Brush)

L’apparence finale du revêtement dépend à la fois des lumières qui l’illuminent et de son matériau. WPF fournit trois classes de matériaux différentes :

Chacun de ces types de matériaux peut être utilisé avec six différents types de pinceaux :

ce qui produit 18 combinaisons possibles de matériaux et de pinceaux.

Si plusieurs matériaux sont présents sur une surface, les derniers ajoutés surpassent et remplacent totalement les autres, sauf s'ils ont un peu transparence par le biais du canal alpha de leur Brush. Il y a donc intérêt à placer Specular et Emissive en dernier.

Dans le cas où le revêtement n'est pas uniforme, la correspondance avec le dessin du revêtement et les Points3D de la MeshGeometry3D est faite par le biais des TexturesCoordinates de cette dernière. Rappelons leur utilisation. Les  TexturesCoordinates sont une PointCollection (2D) des coordonnées (u,v) affectées à chaque Points3D Positions, en considérant qu'ils appartiennent à une surface paramétrée par ces coordonnées, avec 0= u <= 1 et 0 <= v <= 1. On doit avoir autant de TextureCoordinates que de Positions : (TextureCoordinates.Count = Positions.Count). Par exemple, dans le cas d'un rectangle de sommets A, B, C, D, on peut leur attribuer les TextureCoordinates suivantes : Point(0,0), Point(1,0), Point(1,1) et Point(0,1). en associant ainsi :

18.11.1.1 Revêtement uniforme (DiffuseMaterial+ Brush)

C'est le plus classique :

Material mat = new DiffuseMaterial(Brushes.Red);

Il donne un fond d'une couleur uniforme.

Brushes.Red est un des SolidColorBrush prédéfinis.

18.11.1.2 Image (DiffuseMaterial +  ImageBrush)

Pour créer un Material à partir d'une image on procède comme suit :

ImageBrush smileyBrush = new ImageBrush();

smileyBrush.ImageSource = new BitmapImage(new Uri("Smiley.png", UriKind.Relative));

Material mat = new DiffuseMaterial(smileyBrush);

 

18.11.1.3 Revêtement par gradient linéaire (DiffuseMaterial + LinearGradientBrush)

On crée une brosse linéaire vide :

LinearGradientBrush linear = new LinearGradientBrush();

On définit les couleurs pour certaines abscisses linéaires entre 0 et 1. La couleur sera interpolée aux abscisses intermédiaires.

linear.GradientStops.Add(new GradientStop(Colors.Blue, 0.0));

linear.GradientStops.Add(new GradientStop(Colors.White, 0.4));

linear.GradientStops.Add(new GradientStop(Colors.Green, 0.5));

linear.GradientStops.Add(new GradientStop(Colors.White, 0.6));

linear.GradientStops.Add(new GradientStop(Colors.Red, 1.0));

On fait correspondres les extrémités linéaires à 2 points des coordonnées de texture.

linear.StartPoint = new Point(1, 0);

linear.EndPoint = new Point(0, 1);

On crée un DiffuseMaterial avec cette brosse :

Material mat = new DiffuseMaterial(linear);

Par défaut la correspondance est :

linear.StartPoint = new Point(0, 0);

linear.EndPoint = new Point(1, 1);

18.11.1.4 Revêtement par gradient radial (DiffuseMaterial + LinearGradientBrush)

On crée une brosse radiale vide :

RadialGradientBrush radial = new RadialGradientBrush();

On définit les couleurs pour certaines rayons linéaires entre 0 et 1. La couleur sera interpolée aux rayons intermédiaires.

radial.GradientStops.Add(new GradientStop(Colors.Blue, 0.0));

radial.GradientStops.Add(new GradientStop(Colors.White, 0.25));

radial.GradientStops.Add(new GradientStop(Colors.Green, 0.5));

radial.GradientStops.Add(new GradientStop(Colors.White, 0.75));

radial.GradientStops.Add(new GradientStop(Colors.Red, 1.0));

On crée un DiffuseMaterial avec cette brosse :

Material mat = new DiffuseMaterial(radial);

 

Ce qui suit est inutile, car ce sont les valeurs par défaut :

radial.GradientOrigin = new Point(0.5, 0.5);

radial.Center = new Point(0.5, 0.5); // Centre du gradient

radial.RadiusX = 0.5; // Portée du gradient en X

radial.RadiusY = 0.5;

18.11.1.5 Revêtement spéculaire (SpecularMaterial)

Le matériau spéculaire est généralement associé à un des SolidColorBrush prédéfinis.

Material mat = new SpecularMaterial(Brushes.LightBlue, 100);

Le deuxième argument est l'exposant de Phong : 100 => réflection intense des éclairages dans la direction de l'angle de réflexion (la lumière ambiante n'est pas rénvoyée puisqu'elle n'a pas de direction). S'il est plus petit il y a une diffusion autour de l'angle de réflexion.

La réflexion spéculaire est particulièrement intéressante pour les surfaces avec de petites mailles, car avec de grandes mailles, seules les faces qui réfléchissent exactement la lumière directionnelle ou ponctuelle ou spot seront visibles.

18.11.1.6 Revêtement émissif (SpecularMaterial)

Le matériau émissif est généralement associé à un des SolidColorBrush prédéfinis.

Material mat = new EmissiveMaterial(Brushes.Yellow);

Cette lumière donne une couleur au corps qui l'émet mais n'est pas perçue par les autres corps.

18.11.2 Taille et placage du revêtement (Viewbox, Viewport, Stretch)

Avant de plaquer la texture sur le maillage on peut sélectionner un sous rectangle origine avec la proprié brush.Viewbox et un sous-rectangle destination avec la propriété brush.Viewport qui sont des rectangles Rect définis par les coordonnées du coin left-top et par les dimensions width-height. Par défaut ce sont deux Rect(0,0,1,1), ce qui fait que toute la texture initiale est affecté au maillage dans son intégralité.
Si on défini
t un brush.Viewbox = new Rect(b_u, b_v, b_du, b_dv) seul ce rectangle de la brosse sera utilisé sur le maillage.

Si on définit brush.Viewport = new Rect(p_u, p_v, p_du, p_dv),  ces coordonnées seront attribuées à la partie Viewbox sélectionnée.

Si on définit brush.TileMode = TileMode.Tile et que la brosse résultante ne couvre pas tout le maillage, elle est dupliquée vers la droite et vers le bas autant de fois qu'il faut pour recouvrir tout le maillage.  On a également les options TileMode.FlipX, FlipY ,FlipXY qui produisent un effet mirroir avnat chaque duplication et TileMode.None par défaut.

Si les ratios w/h des coordonnées du Viewbox et du Viewport ne concordent pas, la propriété brush.Stretch définit le comportement :

Par défaut, les coordonnées du brush.Viewport sont plaquées sur un BoundBox du maillage de type (0,0,1,1), ce qui correspont à la propriété :

brush.ViewportUnits = BrushMappingMode.RelativeToBoundingBox;

ignorant de ce fait les TextureCoordinates. Si on veux que les TextureCoordinates soient prises en compte, il faut affecter la propriété Absolute au brush.ViewportUnits :

brush.ViewportUnits = BrushMappingMode.Absolute;

18.11.3 Ajustement du revêtement (TextureCoordinate)

L'espace du revêtement est repéré par des Point TextureCordinates (u,v)  variant dans un intervalle min - max qui est généralement 0 - 1. Les propriétés Point TexturesCoordinates du mesh permettent d'associer à chaque propriétés Point3D Positions une coordonnées Point TextureCordinates.

Voici un exemple définissant une méthode pour associer un révêtement à un rectangle défini par 3 points (le quatrième est calculé).

        // Ajout d'un rectangle à un Mesh avec des coordonnées de texture

        private void AddRect2Mesh(MeshGeometry3D mesh,

            Point3D p1, Point3D p2, Point3D p3)

        {

            Point3D p4 = p1 + (p3 - p2);

            int index = mesh.Positions.Count;

            mesh.Positions.Add(p1);

            mesh.Positions.Add(p2);

            mesh.Positions.Add(p3);

            mesh.Positions.Add(p4);

 

            mesh.TextureCoordinates.Add(new Point(0, 0));

            mesh.TextureCoordinates.Add(new Point(0, 1));

            mesh.TextureCoordinates.Add(new Point(1, 1));

            mesh.TextureCoordinates.Add(new Point(1, 0));

 

            mesh.TriangleIndices.Add(index);

            mesh.TriangleIndices.Add(index + 1);

            mesh.TriangleIndices.Add(index + 2);

 

            mesh.TriangleIndices.Add(index);

            mesh.TriangleIndices.Add(index + 2);

            mesh.TriangleIndices.Add(index + 3);

        }

 

        // Ajout d'un materiau et Rectangle à un groupe

        private void AddMaterialRect2Group(Model3DGroup group, Material mat, Point3D p1, Point3D p2, Point3D p3)

        {

            MeshGeometry3D mesh = new MeshGeometry3D();

            AddRect2Mesh(mesh, p1, p2, p3);

            GeometryModel3D model = new GeometryModel3D(mesh, mat);

            group.Children.Add(model);

        }

 

        // Ajout d'une image et Rectangle à un groupe

        private void AddImageRect2Group(Model3DGroup group, String image, Point3D p1, Point3D p2, Point3D p3)

        {

            ImageBrush brush = new ImageBrush();

            brush.ImageSource = new BitmapImage(new Uri(image, UriKind.Relative));

            Material material = new DiffuseMaterial(brush);

            AddMaterialRect2Group(group, material, p1, p2, p3);

        }

Exemple d'utilisation de ces méthodes :

ModelVisual3D visual3d = new ModelVisual3D(); mainViewport.Children.Add(visual3d);

Model3DGroup group = new Model3DGroup(); visual3d.Content = group;

AddImageRect2Group(group, "water.jpg",

                new Point3D(-1, 0, -1),

                new Point3D(-1, 0, +1),

                new Point3D(+1, 0, +1));    

ATTENTION :

Le repère 2D des coordonnées de texture a son axe y descendant.

Considérons un rectangle du plan XY défini par les 4 points p1 (0,0,0), p2(1,0,0), p3(1,1,0) et p4(0,1,0) et les deux triangles directs {p1, p2, p3} et {p1, p3, p4}. Si on veut que la texture soit plaquée à l'endroit, il faut la définir par les 4 points 2D suivants : (0,0), (0,1), (1,1), (1,0) . Ces coordonnées ne correspondent pas aux coordonnées x,y des points du parallélogramme dans l'ordre direct. Elles correspondent aux coordonnées x,y des points du parallélogramme dans l'ordre p1,p4, p3, p2.
Quand on fait la correspondance entre les coordonnées XYZ d'un rectangle et les coordonnées (x,y) d'une texture, il faut bien se rappeler que pour la texture l'axe y est descendant d'où l'ordre fréquemment rencontré (0,0), (0,1), (1,1), (1,0), et non pas (0,0), (1,0), (1,1), (0,1)

18.11.4 Dictionnaire pour l'économie des Positions

18.11.4.1 Cas non texturé

Dans le cas d'un maillage sans texture, on peut appliquer la procédure suivante pour économiser le nombre de Point3D Positions enregistrés pour le maillage.

Deux triangles adjacents partagent 2 positions, A et B par exemple. Si on veut que l'arête soit bien apparente, il faut que les deux triangles aient chacun leurs normales propres en ces points. Ces points seront donc dupliqués, et ainsi chaque point aura sa normale, ce qui devrait être le cas pour une pyramide ou un cube par exemple. Par contre si on désire que les arêtes ne soient pas apparentes (cas d'une sphère par exemple), il faut que les points soient uniques afin qu'il y ait qu'une normale en A et en B, ce qui donne un aspect lisse à la transition entre les 2 triangles. Dans les algorithmes qui calculent les positions des sommets des facettes, il faut donc vérifier lorsqu'on calcule les coordonnées  si elles correspondent à une Positions déjà existante avant de l'ajouter à la collection des Positions. Ceci est effectué en associant au mesh un dictionnaire de ses Positions Point3D  :
Dictionary<Point3D, int> dico = new Dictionary<Point3D, int>();  

qui permettra de n'ajouter que les points qui n'y sont pas. Pour pouvoir comparer les points sans risque à cause de la variabilité dans le calcul des coordonnées on effectue un arrondi au 1/1000 près sur toutes les coordonnées en utilisant la fonction suivante :

public static Point3D RoundPoint3D(Point3D point, int decimals = 3)

{

    double x = Math.Round(point.X, decimals);

    double y = Math.Round(point.Y, decimals);

    double z = Math.Round(point.Z, decimals);

    return new Point3D(x, y, z);

}

Ensuite avant d'ajouter un nouveau Point3D pt = RoundPoint3D(newPt), on procède comme suit pour l'ajouter éventuellement au mesh :

int ix;
if ( dico.ContainsKey(pt)) ix = dico[pt];
else {
   ix = mesh.Positions.Count;
   
mesh.Positions.Add(pt);
   mesh.TextureCoordinates.Add(textureCoord); // si présente
   dico.Add(pt, ix);
}

L'index ix obtenu sera utilisé pour définir la facette (dans l'ordre adéquat)

mesh.TriangleIndices.Add(ix);

18.11.4.2 Cas texturé

 

Dans le cas où le maillage possède une texture ce procédé peut être gênant lorsque le maillage possède des frontières avec lui-même. Par exemple dans le cas d'une bande circulaire, il faut différencier les points de la frontière 0° qui ont pour TextureCoordonnée u = 0 de ceux de la frontière 360° qui ont pour coordonnée u = 1. C'est ainsi le cas pour une sphère où tous les points du méridien de démarcation doivent être dupliqués. Par ailleurs, toujours dans le cas de la sphère, le point du pôle Nord doit être autant de fois présent en Positions qu'il y a de méridiens dans le maillage, et de même pour le pôle Sud.

18.12 Index des exemples

Transparence :  18-Tétrahedrom, 18-Dualtetrahedrom, 19-Cube, 20-Octahedrom, 20-DualCubeOctahedrom, 21-Dodecahedrom, 22-Icosahedrom, 22-DualDodecahedronIcosahedron

Sphère-ico : 24-GeodesicSphere

Faces avant-arrière : 26-ParametricSurface, 26-ParametricSurface2

Terrain : 29-RandomSurface, 29-FractalLandscape

Texte3D : 30-FittedText, 30-SameSizedText, 30-TextTextures

Video : 30-VideoTexture

Marques visibles 3D : 31-CirclePoints

Labels Visibles 3D : 31-LabelPoints

Texte 2D (front/back) : 31-TextLayers

Souris Select-Déplacer objet/Déplacer Camera (clic gauche/droit) : 32-SelectBox 32-MoveBox

Chargement d'Objet3D : 33-LoadObj

graphique 3D : 34

robot bras/humain : 35