vendredi 21 mars 2014

11 - Lire et écrire dans un fichier

0 commentaires


Pourquoi travailler avec des fichiers ?
Parce que c'est la façon la plus simple de stocker des informations à l'extérieur d'un programme. Ensuite, ces informations peuvent être utilisées n'importe quand (sans que le programme qui les a créées ne soit obligé de fonctionner) et par n'importe quel programme qui sait lire le fichier en question.
On a toujours besoin de fichiers ne serait-ce que pour enregistrer du texte.
Il existe un nombre infini de types de fichiers, et vous pouvez même créer les vôtres.
Un fichier possède un nom que se termine par un point suivi de quelques lettres (en général 2 à 4), comme ceci : "exemple.txt".
Ces quelques lettres représentent l'extension du fichier. Votre OS (système d'exploitation) reconnaît les extensions et sait quels programmes y sont associés.
Ainsi, un fichier ".txt" est un fichier texte qui s'ouvrira automatiquement avec un éditeur de texte.
De même, un fichier ".png" représente une image et sera traité comme telle.
Tous les fichiers sont stockés en binaire : une succession de bits (0 ou 1) qui forment des octets (1 octet = 8 bits).
Ces octets sont en fait des nombres écrits en base 2. ils peuvent représenter tout et n'importe quoi : du texte, du son, une image, ...
Nous avons l'habitude de travailler en base 10 : il y a dix chiffres (de 0 à 9) et le nombre abc en base 10(pas a*b*c !) représente a*102 + b*101 + c*100, ce qui fait 100*a + 10*b + c.
De même abc en base 2 représente a*22 + b*21 + c*20, ce qui fait 4*a + 2*b + c. Ainsi, 1001 en base 2correspond à 9 en base 10.
Un octet va de 0000 0000 jusqu'à 1111 1111 en base 2, soit de 0 à 255 (il y a 8 bits : nombre max = 28 - 1) en base 10.
Pour coder les caractères, le code ASCII a à l'origine été mis en place. Il y avait cependant un problème : les accents n'existent pas en anglais et n'ont pas été traduits en code ASCII. Ce code a donc évolué depuis et a été complété par d'autres.
Unicode est le code universel actuel. Vous avez d'ailleurs sûrement déjà entendu parler de l'encodage UTF-8, qui est un format de codage de caractères défini pour les caractères Unicode.
Voici un fichier texte ouvert en mode binaire avec Textpad (ce n'est pas vraiment du binaire, c'est de l'héxadécimal) :
oups l'image a disparu !
Fichier texte ouvert en mode binaire avec Textpad.
Un nombre hexadécimal (hexa = 6, décimal = 10) est écrit en base 16 :
base 2
base 10
base 16
0000 0000
0
00
0000 0001
1
01
0000 0010
2
02
0000 0011
3
03
0000 0100
4
04
0000 0101
5
05
0000 0110
6
06
0000 0111
7
07
0000 1000
8
08
0000 1001
9
09
0000 1010
10
0A
0000 1011
11
0B
0000 1100
12
0C
0000 1101
13
0D
0000 1110
14
0E
0000 1111
15
0F
0001 0000
16
10
0001 0001
17
11
...
...
...
1111 1111
255
FF
Le code de la première lettre est 56 en hexadécimal (l'indice entre parenthèses est la base) :
56(16) = 5.161 + 6.160
56(16) = (4+1).16 + (4+2)
56(16) = (1.22 + 1.20).24 + (1.22 + 1.21)
56(16) = 1.22+4 + 1.20+4 + 1.22 + 1.21
56(16) = 1.26 + 1.24 + 1.22 + 1.21
56(16) = 0101 0110(2)
Vous remarquez au passage que 5(16) = 0101(2) et 6(16) = 0110(2). C'est normal : 24 = 16 donc ce que l'on code avec 4 chiffres en base 2, on le code avec un seul en base 16 : 50(16) = 0101 0000(2).
Les quatre premiers bits (en partant de la droite) représentent un nombre compris entre 0 et 15 : en base 16 cela représente les unités ; les quatre derniers représentent un nombre entre 16 et 255 : en base 16 cela représente les "seizaines".
Dans la page du code ASCII sur Wikipédia (voir lien ci-dessus), vous voyez que cela correspond au caractère 'V' : l'éditeur de texte affiche donc un 'V'.
Si vous changez l'extension d'une image en ".txt", vous pourrez voir plein de caractères qui ne veulent rien dire, eh oui le code binaire décrit des informations concernant l'image, ce n'est pas du texte. L'éditeur affiche les caractères correspondant au code, mais cela ne représente rien pour nous autres humains.
Vous voyez donc que modifier l'extension d'une image en ".txt" va juste faire que le fichier sera affiché comme étant du texte et non comme étant une image ; les données en elles-mêmes ne sont pas affectées.

Lire

Nous allons travailler avec des objets de type Stream ("stream" en anglais = "flux" en français), qui représentent des flux d'octets.
Qu'est-ce qu'un flux ?
Un flux est une connexion entre différents entités ; il transporte les données qu'elles s'échangent. On peut écrire des données dans le flux ou lire celles qui circulent déjà. Ici nous allons effectuer une connexion entre notre programme et l'espace de stockage de notre ordinateur.
Pour lire un fichier en utilisant les boîtes de dialogue, vous allez avoir besoin d'un objet de typeOpenFileDialog, ici nommé openFileDialog1 (ici le nom n'est pas fabuleux car il s'agit simplement d'un exemple ; dans un vrai programme, nommez-le en fonction de ce à quoi il sert).
Pour en créer un, prenez-le dans la boîte à outils et mettez-le sur votre fenêtre. Ce n'est pas un objet qui s'affiche dans la fenêtre, il va donc se mettre en-dessous de votre fenêtre dans le mode Design.
Comme d'habitude, je vous donne d'abord le code (qui est à mettre dans une méthode) et ensuite je vous explique tout :
1string fileName;
2string fileContent;
3
4// On interdit la sélection de plusieurs fichiers.
5openFileDialog1.Multiselect = false;
6
7// On supprime le nom de fichier, qui ici vaut "openFileDialog1" (avant sélection d'un fichier).
8openFileDialog1.FileName = string.Empty;
9
10// On met des filtres pour les types de fichiers : "Nom|*.extension|autreNom|*.autreExtension" (autant de filtres qu'on veut).
11openFileDialog1.Filter = "Fichiers texte|*.txt|Tous les fichiers|*.*";
12
13// Le filtre sélectionné : le 2e (là on ne commence pas à compter à 0).
14openFileDialog1.FilterIndex = 2;
15
16// On affiche le dernier dossier ouvert.
17openFileDialog1.RestoreDirectory = true;
18
19// Si l'utilisateur clique sur "Ouvrir"...
20if (openFileDialog1.ShowDialog() == DialogResult.OK)
21{
22    try
23    {
24        // On récupère le nom du fichier.
25        fileName = openFileDialog1.FileName;
26
27        // On lit le fichier.
28        fileContent = File.ReadAllText(fileName);
29    }
30    // En cas d'erreur...
31    catch (Exception ex)
32    {
33        MessageBox.Show("Une erreur est survenue lors de l'ouverture du fichier : {0}.", ex.Message);
34    }
35}
Les quelques premières lignes ne devraient pas poser trop de problèmes : on modifie simplement quelques propriétés de la boîte de dialogue. C'est dans le if que ça devient intéressant. L'ordinateur évalue openFileDialog1.ShowDialog() == DialogResult.OK pour obtenir un booléen. En fait, tout se passe comme si == était une méthode prenant deux arguments en entrée et retournant un booléen ; on pourrait avoir ==(openFileDialog1.ShowDialog(), DialogResult.OK). Je pense qu'on comprend mieux ce qui se passe avec cette écriture. Donc nous étions en train de dire que nous voulions évaluer cette expression. Pour cela, il faut commencer par évaluer les arguments.DialogResult.OK est déjà une valeur, donc pas de soucis. Par contre ShowDialog est une méthode : il faut donc l'appeler et exécuter le code qu'elle contient pour obtenir un résultat qui servira pour le ==.
Que fait la méthode ShowDialog ?
ShowDialog affiche une boîte de dialogue contenant un explorateur de fichiers et qui attend que vous en choisissiez un ou plusieurs (ici on a interdit d'en choisir plusieurs). Elle retourne un objet de typeDialogResult (une énumération) qui donne des informations sur ce qu'a fait l'utilisateur. Cet objet peut valoir DialogResult.OK si l'utilisateur a bien choisi un fichier et qu'il n'a, par exemple, pas annulé. C'est précisément ce que l'on teste dans le if : on vérifie qu'un fichier a été choisi avant d'essayer d'y accéder.
Récapitulons : dans l'ordre chronologique on commence par afficher la boîte de dialogue et on attend que l'utilisateur fasse quelque chose : le programme est bloqué à la méthode ShowDialog. Ensuite on compare le résultat de cette méthode avec DialogResult.OK à l'aide de l'opérateur == : si l'utilisateur a choisi un fichier alors on exécute le code du bloc if.
Continuons l'analyse : on récupère d'abord le nom du fichier choisi puis on lit le fameux fichier en utilisant la méthode File.ReadAllText qui ouvre le fichier, le lit en entier, le ferme et retourne son contenu. En une seule ligne, on rassemble des actions que nous pouvons séparer.
Dans cet exemple j'ai utilisé la méthode File.ReadAllText. Comme son nom l'indique, on lit les données en faisant comme si c'était du texte ; à nous d'être sûrs que nous lisons du texte, sinon nous obtiendrons une suite de caractères sans aucun sens. Suivant ce que l'on veut faire avec le fichier, on peut utiliser d'autres méthodes ou classes. Très souvent, on utilise des objets de type Stream (ou des types qui en dérivent). Cette classe est aussi située dans l'espace de noms System.IO, il faut donc mettre using System.IO; en début de fichier. Voici comment on peut faire avec cette nouvelle classe : je remplace juste la ligne fileContent = File.ReadAllText(fileName); par :
1Stream myStream = null;
2StreamReader reader = null;
3try
4{
5    // Ouverture
6    myStream = openFileDialog1.OpenFile();
7    reader = new StreamReader(myStream, Encoding.UTF8);
8    
9    // Lecture
10    fileContent = reader.ReadToEnd();
11}
12catch
13{
14    Console.WriteLine("Something went wrong!");
15}
16finally
17{
18    // Fermeture
19    if (reader != null)
20    {
21        reader.Dispose();
22    }
23    
24    if (myStream != null)
25    {
26        myStream.Dispose();
27    }
28}
Ici on a séparé ouverture, lecture et fermeture. Vous pouvez utiliser le mot-clef using pour appeler automatiquement à la fin du bloc correspond la méthode Dispose de l'objet entre parenthèses :
1try
2{
3    // Ouverture
4    using (Stream myStream = openFileDialog1.OpenFile())
5    {
6        using (StreamReader reader = new StreamReader(myStream, Encoding.UTF8))
7        {
8            // Lecture
9            fileContent = reader.ReadToEnd();
10        }
11        // La méthode Dispose de reader vient d'être appelée.
12    }
13    // La méthode Dispose de myStream vient d'être appelée.
14}
15catch
16{
17    Console.WriteLine("Something went wrong!");
18}
Dans ce cas, la fermeture est implicite.
Il est très important de fermer un flux une fois que vous n'en avez plus besoin : cela libère de la mémoire et cela permet de ne pas monopoliser l'accès à la ressource en question (d'autres programmes en ont peut-être besoin).
404 Image not found
Informations concernant la classe Stream (tirées des métadonnées).
Vous voyez que la classe Stream implémente bien l'interface IDsiposable et propose bien une méthode Dispose.
Voici le code complet de la deuxième version (je le répète : ce code est à mettre dans une méthode) :
1string fileName;
2string fileContent;
3
4// On interdit la sélection de plusieurs fichiers.
5openFileDialog1.Multiselect = false;
6
7// On supprime le nom de fichier, qui ici vaut "openFileDialog1" (avant sélection d'un fichier).
8openFileDialog1.FileName = string.Empty;
9
10// On met des filtres pour les types de fichiers : "Nom|*.extension|autreNom|*.autreExtension" (autant de filtres qu'on veut).
11openFileDialog1.Filter = "Fichiers texte|*.txt|Tous les fichiers|*.*";
12
13// Le filtre sélectionné : le 2e (là on ne commence pas à compter à 0).
14openFileDialog1.FilterIndex = 2;
15
16// On affiche le dernier dossier ouvert.
17openFileDialog1.RestoreDirectory = true;
18
19// Si l'utilisateur clique sur "Ouvrir"...
20if (openFileDialog1.ShowDialog() == DialogResult.OK)
21{
22    try
23    {
24        // On récupère le nom du fichier.
25        fileName = openFileDialog1.FileName;
26        
27        Stream myStream =  openFileDialog1.OpenFile();
28        
29        if (myStream != null)
30        {
31            using (myStream)
32            {
33                using (StreamReader reader = new StreamReader(myStream, Encoding.UTF8))
34                {
35                    fileContent = reader.ReadToEnd();
36                }
37            }
38        }
39    }
40    // En cas d'erreur...
41    catch (Exception ex)
42    {
43        MessageBox.Show("Une erreur est survenue lors de l'ouverture du fichier : {0}.", ex.Message);
44    }
45}
Si vous faites un clic-droit sur StreamReader et que vous cliquez ensuite sur "Go To Definition", vous aurez accès aux métadonnées, dans lesquelles vous pourrez lire :
1// Summary:
2//     Implements a System.IO.TextReader that reads characters from a byte stream
3//     in a particular encoding.
4[Serializable]
5[ComVisible(true)]
6public class StreamReader : TextReader
Vous voyez que le commentaire précise que l'on lit des caractères, autrement dit du texte. Si nous voulions lire des données brutes, nous n'aurions pas besoin de StreamReader et nous pourrions nous contenter de l'objet myStream.

Écrire

Pour écrire dans un fichier, le principe va être à peu près le même car nous allons encore utiliser une boîte de dialogue. La différence est qu'au lieu d'utiliser un contrôle de type OpenFileDialog, nous allons utiliser SaveFileDialog.
Comme vous devez vous en douter, il vous suffit encore d'un simple glisser-déposer depuis la boîte à outils pour créer un objet de type SaveFileDialog ; le mien gardera le piètre nomsaveFileDialog1.
Voici maintenant comment écrire la valeur de fileContent dans un fichier (le code suivant va dans une méthode) :
1saveFileDialog1.Filter = "Fichiers texte|*.txt|Tous les fichiers|*.*";
2saveFileDialog1.FilterIndex = 2;
3saveFileDialog1.RestoreDirectory = true;
4
5if (saveFileDialog1.ShowDialog() == DialogResult.OK)
6{
7    try
8    {
9        Stream myStream = saveFileDialog1.OpenFile();
10
11        if (myStream != null)
12        {
13            using (myStream)
14            {
15                using (StreamWriter writer = new StreamWriter(myStream, Encoding.UTF8))
16                {
17                    writer.Write(fileContent);
18                }
19            }
20        }
21    }
22    catch (Exception ex)
23    {
24        MessageBox.Show("Une erreur est survenue lors de l'écriture du fichier: {0}.", ex.Message);
25    }
26}
La seule différence notoire est que nous utilisons ici un StreamWriter au lieu d'un StreamReader et la méthode Write au lieu de ReadToEnd.
Là encore, on s'intéresse à du texte et j'utilise l'encodage UTF-8 (vous pouvez en choisir un autre dans la classe Encoding).
J'ai porté l'accent sur le traitement de fichiers texte, car c'est le plus facile. Sachez que vous pouvez bien évidemment stocker tout ce que vous voulez, du son, des images, des vidéos, des données...

Leave a Reply