Mémo C#
Michel Llibre Juin 2025
Le #ifdef de C/C++ est ici un simple #if (valeur)
Les #define valeur doivent être placés en tête de fichier.
1.Application Windows Form (WF): Application windows fenêtrée utilisant la techno Windows Forms. Il y a deux versions :
◦.Framework qui accède à toutes les fonctionnalités du Framework sans avoir rien à préciser
◦.NET pour lequel il faut ajouter une à une toutes les dépendances nécessaires à l'application.
La majorité des applications c# Windows non Console que j'écris sont des WF.
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)
3.Application console : Application utilisable en ligne de commande (.Framework ou .Net)
4.Projet partagé : Contient du code utilisable par d'autres projets.
5.Bibliotèque de classes : Code de classes utilisables par d'autres exécutables.
6.Application de navigateur WPF : Application pouvant être ouverte par un navigateur web.
7.Bibliothèque de contrôles utilisateur WPF : Pour étendre ceux offerts par Windows
8.Bibliothèque de contrôles personnalisés WPF : Idem.
9.Service Windows : programmes qui s'exécutent en tâche de fond.
F1 direct : Aide sur mot clé.
SHIFT-F1 : Aide Editeur
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
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
•une classe imbriquée nommée Entry.
•un champ nommé top du type de la classe Entry,
•deux méthodes nommées Push et Pop
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
•Entier signé : sbyte, short, int, long
•Entier non signé : byte, ushort, uint, ulong
•Caractères UTF-16 : char
•Virgule flottante pour valeur binaire IEEE : float, double
•Virgule flottante pour valeur décimale haute précision : decimal
•Booléen : bool : true ou false
Ils sont tous initialisés par défaut par des 0000000...
Types définis par l'utilisateur de la forme enum E {...}
Types définis par l'utilisateur de la forme struct S {...}
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;
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.
•Classe de base fondamentale de tous les autres types : object
•Chaînes Unicode : string
•Types définis par l'utilisateur de la forme class C {...}
Types définis par l'utilisateur de la forme interface I {...}
Syntaxe très proche du C/C++ :
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.
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)
}
}
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.
Type C# |
Type .NET |
Donnée représentée |
Suffixe |
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.
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 :
•pas de suffixe pour int et double
•suffixe L pour long, F pour float et M pour décimal
•la chaine verbatim @"c:\chap1\par3" commençant par @ où les caractères qui suivent les \ ne sont pas interprêtés.
•On peut utiliser le caractère _ pour séparer les groupes de milliers dans les décimaux, les octets dans les hexa et les blocs de 4 bits dans les binaires comme dans 0b0100_0100_0111_1100_0010.
() [] 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 |
Opérateur (cast) comme en C/C++.
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).
La classe string est très proche de celle qu'on trouve en java et en Visual Basic, avec :
•la concatenation offerte par l'opérateur + : "Lac Titi" + "caca";
•conversion automatique de tous les objets concaténés en string par le biais de leur méthode ToString() qui peut prendre 0, 1 ou 2 arguments. S'il est présent le premier spécifie le type de sortie désirée :
◦"G" : général
◦"C" : currentie (monnaie -> $, £, € à la fin.
◦"Dn" : "D3" cad 3 chiffres avec des 0 par devant
◦"En" : "E2" cad format XX.ddE+003 par exemple avec n d après le .
◦"F" : décimal ordinaire
◦"N" : décimal avec regroupement des chiffres par groupes de 3
◦"P" : suivi de %
◦"Xn" : "X8" par exemple hexadécimal avec 8 chiffres
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).
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.
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"
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.
On peut comparer deux strings par ==, != ou bien chaine1.CompareTo(chaine2) -> int ou encore chaine1.Equals(chaine2) -> bool.
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
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.
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
public const double PI = 3.14159;
Be peut plus être modifié.
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.
L'accès indexé aux membres d'un tableau par [ ] et aux membres d'une classe par . (point) comme en Java.
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.
x = y ?? z;
Si y est non null, x vaut y, sinon il vaut z.
Comme en C/C++, java...
Les arguments des fonctions sont passés par valeur. C'est-à-dire que c'est une copie qui est utilisé dans la fonction.
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
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
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
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
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.
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.
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.
•if... else
•switch
•for
•do while
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 :
•foreach(<type> <nomVar> in <collection>) { traitement de nomVar }
•goto etiquette; fait un saut à la ligne qui est commence par etiquette :
•using (...) { ... } garantit que le Dispose() sur la ressource non gérée sera effectué.
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.
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.
E as T où T 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.
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.
foreach (Type variable in collection)
instructions;
}
où collection est une collection d'objets énumerables (tableaux, listes, ...)
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é.
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 :
•catch (Exception e) : on traite toutes les exceptions
•catch (IndexOutOfRangeException e) : on ne traite que les débordements de tableau
•il peut y avoir plusieurs catch à la suite pour les différents types d'exception.
•la clause finally (facultative) a son code exécuté dans tous les cas, exception ou non.
•on peut ne pas mettre l'argument e si on ne l'utilise pas.
•on a intérêt au minimum d'imprimer le string e.Message.
Example :
static void CheckedUnchecked(string[] args)
{
int x = int.MaxValue;
unchecked
{
Console.WriteLine(x + 1); // Overflow
}
checked
{
Console.WriteLine(x + 1); // Exception
}
}
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;
}
}
}
static void UsingStatement(string[] args)
{
using (TextWriter w = File.CreateText("test.txt"))
{
w.WriteLine("Line one");
w.WriteLine("Line two");
w.WriteLine("Line three");
}
}
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.
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
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; } }
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 !!!
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 !
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.
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));
....
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.
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); }
}
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; };
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
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
•en supprimant les parenthèses autour de l'argument de la méthode s'il est unique (on met les parenthèses pour 0 argument ou > =2)
MyDelegate op = s => { Console.WriteLine(s); return 100; };
•en supprimant les {} s'il n'y a qu'une expression dans le corps de la méthode
•et de plus en supprimant le mot clé return et le ; dans le cas d'un simple transfert de données, exemple :
MyDelegate op = a => b; dans le cas d'une méthode prenant un argument du même type que celui de a et renvoyant un élément du même type que celui de b.
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
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/
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 !
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éé.'
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, ...
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
// 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
*/
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")
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.
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.
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.
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.
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);
ou en windowsForm :
Application.exit().
Autocheck : Pour les radioButton, etc... pour permettre ou non l'acces à l'utilisateur (= readonly).
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 :
•chrono.ElapsedTicks fournit le nombre de ticks écoulés depuis le dernier Stopwatch.StartNew();
•chrono.ElapsedMilliseconds fournit le nombre de millisecondes écoulées depuis le dernier Stopwatch.StartNew();
Les Stopwatch créés dans différents threads sont indépendants.
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.
Une Windows Form est un objet fenêtre qui hérite de la classe Form (parfois traduite en français par Formulaire).
Une windows Form standard utilise les références suivantes :
•Analyseur,
•Microsoft.Csharp
•System
•System.Core
•System.Data
•System.Data.DataSetExtensions
•System.Deployment
•System.Drawing
•System.Net.Http
•System.Windows.Forms
•System.Xml
•System.Xml.Linq
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.
•Créer un projet/Projet vide (.Net Framework)
•Projet/Ajouter nouvel élément/Fichier de code (puis le renommer à sa guise)
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) ;
}
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 :
•Propriétés (clé plate) : présentation des propriétés
•Événements (éclair zigzag) : présentation des évèment associables
De plus 2 onglets permettent de les classer différemment :
•Catégories : organisation en catégories
•Alphabétique : organisation alphabétique
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"); }
}
}
(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é.
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;
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
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
MouseClick
MouseDoubleClick
MouseDown
PreviewMouseDown
MouseEnter
MouseHover
MouseLeave
MouseMove
MouseUp
MouseWheel
Click
KeyDown
KeyPress (déprécié ?)
KeyUp
PreviewKeyDown
PreviewKeyUp
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.
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)
{
}
Les Formulaires Form peuvent être modal (bloquante) ou non modal (exécution en //).
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.
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.
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.
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;
}
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
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.
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.
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:
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:
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");
}
}
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:
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");
}
}
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:
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");
}
}
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.
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");
}
}
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:
•Hatch pattern specified by the HatchStyle property. There are six predefined hatch styles.
•Hatch fore color specified by the ForegroundColor property. The fore color does not support transparency, so the alpha channel of the color will be ignored.
•Hatch background. To set background color use the BackgroundColor property. To draw the pattern only, set the TransparentBackground property to true.
The following code draws the blue ellipse shown in the image:
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:
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");
}
}
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
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.
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.
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);
}
}
}
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.
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 :
•ManagedThreadId : son numéro d'ordre
•ThreadState : son état (combinaison des états suivants : Unstarted, Stopped, WaitSleepJoin, Aborted, Background, Running, StopRequested, Suspended, SuspenRequested)
•Priority : (ThreadPriority.BelowNormal, ThreadPriority.AboveNormal , ThreadPriority.Normal )
•IsBackground : true/false
•IsThreadPoolThread : true/false
•CurrentCulture.Name : fr-FR...
(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.
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 :
•Task t = new Task(taskMethode); planifie la tâche, mais ne l'exécute pas. Il faut appeler t.Start(); pour lancer son exécution.
•En théorie Task.run(taskMethode) prend en argument une Action, c'est-à-dire un délégué sur une méthode qui renvoie un void et prend 0 ou 1 argument. Mais en fait il semble qu'il y ait un cast automatique et qu'on puisse se passer de l'intermédiaire de taskMethode et écrire directement Task t = Task.Run(methode);
•Il est recommandé d'utiliser Task.Run au lieu de Task t = new Task(methode); t.Start();
•Task.Run est une version simplifiée de Task.Factory.StarNew qui permet de nombreux autres paramètres de configuration comme l'annulation, la liaison d'enfants, la planification...
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 .
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)
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.
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 :
•en appelant la méthode avec ses arguments dans le champ de la classe anonyme, comme ici la valeur 10,
•en utilisant des membres de la classe, qui ici ne sert qu'à cela (mémoriser des paramètres) : la valeur 5 fournie avant l'appel de la tâche.
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.
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;
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;
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;
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);
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 :
•.NET : permet de référencer une assembly présente dans le GAC (global assembly cache) : c’est qu'on cherche les assemblys du framework .NET
•COM : permet de référencer une dll COM (l’ancêtre de l’assembly). Techniquement, on ne référence pas directement une dll COM, mais Visual C# génère un wrapper permettant d’utiliser la dll COM comme si c’était une assembly.
•Projets : permet de référencer des assemblys qui se trouvent dans notre solution. Si notre solution ne contient qu’un seul projet, l’onglet sera vide.
•Parcourir : va permettre de référencer une assembly depuis un emplacement sur le disque dur.
•Récent : permet de référencer des assemblys récemment référencées
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.
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
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 |
| |
HANDLE |
void * |
System.IntPtr |
32 bits Windows 32 bits, |
BYTE |
unsigned char |
8 bits | |
SHORT |
short |
16 bits | |
WORD |
unsigned short |
16 bits | |
INT |
int |
32 bits | |
UINT |
unsigned int |
32 bits | |
LONG |
long |
32 bits | |
BOOL |
long |
32 bits | |
DWORD |
unsigned long |
32 bits | |
ULONG |
unsigned long |
32 bits | |
CHAR |
char |
ANSI. | |
WCHAR |
wchar_t |
Unicode. | |
LPSTR |
char * |
System.String |
ANSI. |
LPCSTR |
const char * |
System.String |
ANSI. |
LPWSTR |
wchar_t * |
System.String |
Unicode. |
LPCWSTR |
const wchar_t * |
Unicode. | |
FLOAT |
float |
32 bits | |
DOUBLE |
double |
64 bits |
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.
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, déplacement, grandissement, cisaillement,...
Les transformations héritées de Transform3D s'appliquent au moyen de la méthode Transform(-) à :
•Point3D
•Point3D[]
•Point4D
•Point4D[]
•Vector3D
•Vector3D[]
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); ...
(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)
•PresentationCore.dll
•PresentationFramework.dll
•WindowsBase.dll
•WindowsFormsIntegration.dll
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).
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 :
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 !).
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);
}
Pour dessiner en 3D il faut créer un Viewport3D et l'affecter à la MainWindow ou à son conteneur racine.
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);
...
}
}
<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);
....
}
<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);
....
}
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;
La scène 3D qui sera affectée au ModelVisual3D est constituée d'une hiérachie de classes dont les principales sont :
•Des MeshGeometry3D qui permettent de définir les formes à partir d'un maillage en facettes
•Des MaterialGroup (matériau) qui définissent l'aspect de la surface des facettes à l'aide d'enfants de type matériau élémentaire (DiffuseMaterial...) ou MaterialGroup.
•Des GeometryModel3D qui regroupent un MeshGeometry3D et un MaterialGroup.
•Des Model3DGroup qui regroupent des enfants de leur propre type et des GeometryModel3D et des éclairages (AmbientLight...) . Généralement on ne met des éclairages qu'à un Model3DGroup racine.
• Un ModelVisual3D racine qui contient un Model3DGroup racine. Il peut contenir également d'autres ModelVisual3D enfants.
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.
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 :
•si on fournit des normales, on ne pourra fournir qu'un jeu (puisqu'il n'y a qu'un jeu de sommets), les deux faces auraont donc le même rendu,
•si on ne fournit pas de normales, les 2 faces seront noires (pourquoi ?) s'il n'y a pas d'éclairage ambiant.
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 :
•le Point3D associé à la TextureCoordinate (0,0) au coin left-top de l'image ou de la texture,
•le Point3D associé à la TextureCoordinate (1,1) au coin bottom-right de l'image ou de la texture.
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);
Pour animer un MeshGeometry3D il faut :
•Déclarer accessibles dans la MainWindow les mesh à modifier et les Tranformations3D qui seront variables :
private MeshGeometry3D CubeMesh = null;
private RotateTransform3D CubeRotator = null;
•Définir la callback qui appliquera la transformation variable :
private void maCallback(object sender, EventArgs e)
{
CubeMesh.ApplyTransformation(CubeRotator);
}
• Lancer un Timer au chargement de l'application, par exemple à la fin de la méthode Window_Loaded :
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));
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());
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.
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)));
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 :
•Position : Un Point3D spécifiant la position de l'objectif dans le repère scène,
•LookDirection : Un Vector3D spécifiant la direction de la visée dans le repère scène,
•UpDirection : Un Vector3D spécifiant la direction vers le haut de la caméra dans le repère scène,
•Width : La largeur du champ vue dans le repère scène
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 :
•Position : Un Point3D spécifiant la position de l'objectif dans le repère scène,
•LookDirection : Un Vector3D spécifiant la direction de la visée dans le repère scène,
•UpDirection : Un Vector3D spécifiant la direction vers le haut de la caméra dans le repère scène,
•FieldOfView : La largeur du champ vue en degrés.
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.
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 :
•PreviewKeyDown (précède Keydown) : quand une touche clavier a été appuyée. e.Key fournit le caratère ou le code de la touche (Key.Left, Key.A,..)
•PreviewMouseWheel (précède MouseWheel) quand la molette de la souris est actionnée. L'événement e.Delta fournit le Delta de rotation
•MouseDown : quand un bouton de la souris est enfoncé. On capture la souris, sa position donnée par Point e.GetPosition(widget) et on installe un gestionnaire pour gérer les deux événement suivants :
•MouseMove : quand la souris se déplace on récupère la position courante Point e.GetPosition(widget)
•MouseUp : relevé du bouton : On relache la capture et on désintalle les 2 gestionnaire de MouseMove et MouseUp.
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.
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 :
•l'opacité (facultative) avec 0 : transparence totale et 255 : totale opacité
•rouge, vert et bleue.
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.
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:
•AmbientLight
•DirectionalLight
•PointLight
•SpotLight
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.
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.
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.
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.
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.
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 :
•DiffuseMaterial,
•SpecularMaterial,
•EmissiveMaterial.
Chacun de ces types de matériaux peut être utilisé avec six différents types de pinceaux :
•SolidColorBrush,
•LinearGradientBrush,
•RadialGradientBrush,
•ImageBrush, DrawingBrush,
•VisualBrush
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 :
•le Point3D left-top à la TextureCoordinate (0,0) de l'image,
•le Point3D bottom-right à la TextureCoordinate (1,1) de l'image.
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.
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);
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);
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;
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.
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.
Avant de plaquer la texture sur le maillage on peut sélectionner un sous rectangle origine avec la propriété 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éfinit 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 :
•brush.Stretch = Stretch.Fill (appliqué par défaut) : le Viewbox est déformé pour couvrir tout Viewport
•brush.Stretch = Stretch.Uniform : le Viewbox est étirée uniformément jusqu'à ce qu'un bord du Viewport soit atteint sur une dimension. Si les 2 W/H ne sont pas égaux, il y aura une partie non couverte dans l'autre dimension.
•brush.Stretch = Stretch.UniformToFill : le Viewbox est étirée uniformement pour couvrir tout Viewport. Si les 2 W/H ne sont pas égaux, une partie de la texture du Viewbox sera masquée par les autres tuiles.
•brush.Stretch = Stretch.None : le Viewport reste vierge
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;
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)
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);
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.
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