Système de mise à jour en C#

C#Français

October 21, 2016

Warning This content is 1 year old or more. Please, read this content keeping its age in mind.

Aujourd’hui nous allons développer un lanceur de jeu aussi dit Launcher en anglais. Le code que je vais vous proposer n’est pas parfait, si vous avez des améliorations n’hésitez pas à les partager dans les commentaires.

Le logiciel aura pour but de mettre à jour un jeu ou logiciel. Des boutons permettront aussi d’accéder à des sites internet, lancé le logiciel/jeu ou alors de quitter le lanceur.

Pour faire le lanceur je me suis inspiré de celui de The Elder Scrolls V : Skyrim et de Tomb Raider.
Voir image ci-dessous:

TombRaider & Skyrim launcher

Nous allons rester dans le même esprit: une image de fond, enlever les bordures des fenêtres Windows et personnalisaient nos boutons de façons à ce qu’il ressemble à du texte simple.
Voici le résultat final:

Final Launcher

Le projet:

Avant de commencer le développement pur et dur il nous faut définir ce que doit pouvoir faire le lanceur, comment il le fait et avec quoi nous allons le faire.
Pour l’exemple je vais utiliser VS Express 2013 pour Desktop, le langage C# avec la librairie Windows Presentation Foundation (WPF) pour l’interface graphique.

Fonctionnalités du lanceur:

  • Lancer le jeu.
  • Mettre à jour le jeu si besoin.
  • Unzip, décompressé le fichier .zip contenant la mise à jour.
  • Ouvrir le navigateur internet par défaut pour aller sur le site officiel de votre logiciel/jeu.
  • Pouvoir quitter le lanceur.

Cette liste nous permet de définir les boutons et leurs actions. Rien de bien compliquer, le plus dur étant le téléchargement et dézipage des fichiers téléchargés. Nous allons voir via ce schéma comment va fonctionner le système de mise à jour.

Network

Comme vous pouvez le voir, notre lanceur va au démarrage récupérer des informations dans un ordre précis: url du .txt puis url du .zip(du jeu) et enfin le nom du dossier de destination le tout séparé par un seul espace. Une fois ces informations dans des variables, l’on va vérifier et comparer le version.txt local et celui en ligne. S’il y à une mise à jour alors le fichier .zip est téléchargé puis décompresser sur l’ordinateur de l’utilisateur.
Maintenant que tout est bien défini, nous allons pouvoir attaquer le code !

La mise en forme

WPF utilise pour la mise en forme du xaml. Ne vous inquiétez pas, une connaisance parfaite du langage n’est pas nécessaire, l’interface graphique proposée par Visual studio va nous simplifier la vie.

Voilà le code de notre fenêtre, je vais vous l’expliquer après:

<Window x:Class="Launcher.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="MainWindow" Height="445" Width="790" ResizeMode="NoResize" WindowStyle="None" WindowStartupLocation="CenterScreen">

        <Window.Resources>
            <Style x:Key="buttonText" TargetType="Button">
                    <Setter Property="Template">
                        <Setter.Value>
                                <ControlTemplate TargetType="Button">
                                <ControlTemplate.Resources>
                                        <Style x:Key="contentStyle">
                                            <Setter Property="Control.Background" Value="{x:Null}" />
                                            <Setter Property="Control.Foreground" Value="#FFE2E2E2" />
                                            <Setter Property="Control.BorderBrush" Value="{x:Null}" />
                                            <Setter Property="Control.FontSize" Value="32" />
                                        </Style>
                                </ControlTemplate.Resources>
                                <Grid>
                                        <ContentPresenter HorizontalAlignment="Center" VerticalAlignment="Center" Name="content" Style="{StaticResource contentStyle}">
                                        </ContentPresenter>
                                </Grid>
                            </ControlTemplate>
                        </Setter.Value>
                    </Setter>
                    <Style.Triggers>
                        <Trigger Property="IsMouseOver" Value="True">
                                <Setter Property="Foreground" Value="#FFC5C5C5" />
                                <Setter Property="BorderBrush" Value="{x:Null}" />
                                <Setter Property="Background" Value="{x:Null}" />
                        </Trigger>
                    </Style.Triggers>
            </Style>
        </Window.Resources>

        <Window.Background>
            <ImageBrush ImageSource="background.png"/>
        </Window.Background>

        <Grid>
            <Button x:Name="Play" Style="{StaticResource buttonText}" Content="Jouer" HorizontalAlignment="Left" Margin="602,94,0,0" VerticalAlignment="Top" Cursor="Hand" IsDefault="True" Click="Play_Click"/>
            <Button x:Name="Options" Style="{StaticResource buttonText}" Content="Options" HorizontalAlignment="Left" Margin="566,147,0,0" VerticalAlignment="Top" Cursor="Hand"/>
            <Button x:Name="OfficialWebSite" Style="{StaticResource buttonText}" Content="Site officiel" HorizontalAlignment="Left" Margin="523,200,0,0" VerticalAlignment="Top" Cursor="Hand" Click="OfficialWebSite_Click"/>
            <Button x:Name="MySite" Style="{StaticResource buttonText}" Content="Mon site" HorizontalAlignment="Left" Margin="555,253,0,0" VerticalAlignment="Top" Cursor="Hand"/>
            <Button x:Name="Exit" Style="{StaticResource buttonText}" Content="Quitter" HorizontalAlignment="Left" Margin="578,306,0,0" VerticalAlignment="Top" Cursor="Hand" Click="Exit_Click" IsCancel="True"/>

            <Label x:Name="LabelVersion" Content="Version 1.0.0.0" HorizontalAlignment="Left" Margin="703,419,0,0" VerticalAlignment="Top"/>
        </Grid>
</Window>

Je ne vais pas m’attarder pour ce qui est du début du code, déclaration de la fenêtre ainsi que de la taille. Important, la fenêtre a pour argument supplémentaire ResizeMode="NoResize" WindowStyle="None" WindowStartupLocation="CenterScreen", NoResize comme son nom l’indique empêche le redimentionnement de toute façon impossible car WindowStyle est désactiver, ce qui signifie que les bordures des fenêtres sont désactivées. WindowStartupLocation permet de définir la position de la fenêtre au démarrage, la valeur est actuellement à CenterScreen de façon à ce que notre fenêtre soit centrée au milieu de l’écran.

Nous allons maintenant parler de <Window.Resources>, c’est ici que nous déclarons le style de nos nouveaux boutons. Ils doivent ressembler à un Label, c’est-à-dire à du texte simple. Nous déclarons donc <Style x:Key="buttonText" TargetType="Button">, nous allons à l’intérieur définir le style de notre bouton.

<Style x:Key="contentStyle">
    <Setter Property="Control.Background" Value="{x:Null}" />
    <Setter Property="Control.Foreground" Value="#FFE2E2E2" />
    <Setter Property="Control.BorderBrush" Value="{x:Null}" />
    <Setter Property="Control.FontSize" Value="32" />
</Style>

Le fond du bouton (background) est défini à Null afin d’avoir de la transparence, le texte est de couleurs grise #FFE2E2E2, les bordures sont enlevées et la taille de la police est à 32px. Maintenant il nous faut définir le style de notre bouton quand on clique dessus. Il suffit de reprendre le schéma vue plus haut mais de changer <Style> en <Style.Triggers> et d’ajouter <Trigger Property="IsMouseOver" Value="True"> ce qui donne:

<Style.Triggers>
    <Trigger Property="IsMouseOver" Value="True">
        <Setter Property="Foreground" Value="#FFC5C5C5" />
        <Setter Property="BorderBrush" Value="{x:Null}" />
        <Setter Property="Background" Value="{x:Null}" />
    </Trigger>
</Style.Triggers>

Ensuite, <Window.Background> permet de définir une image de fond pour la fenêtre. Puis se trouve la liste des boutons. Là encore rien de compliquer, il suffit de déclarer un bouton et de lui donné comme Style celui que nous avons créé précédemment. Voici un exemple:, l’ensemble des boutons sont cassis identique:

<Button x:Name="Play" Style="{StaticResource buttonText}" Content="Jouer" HorizontalAlignment="Left" Margin="602,94,0,0" VerticalAlignment="Top" Cursor="Hand" IsDefault="True" Click="Play_Click"/>

Voila le code XAML, l’interface est maintenant terminée. Le plus gros de l’application et surtout le plus important reste à faire. Le système de mise à jour. Cette fois nous utiliserons le langage C# et quelques librairies .NET.

Le code C#

Les choses sérieuses commence, comme d’habitude, je vais vous exposer une grosse partie de code puis, je vais vous l’expliquer. Si cette méthode ne vous convient pas, n’hésitez surtout pas à le dire dans les commentaires.

using ICSharpCode.SharpZipLib.Zip;
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Net;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;

namespace Launcher
{
    /// <summary>
    /// Logique d'interaction pour MainWindow.xaml
    /// </summary>
    enum UpdateState { search, available, download, update };

    public partial class MainWindow : Window
    {
        private string m_confUrl = "http://aubega.com/launcher/conf.txt";

        //Variable de configuration
        string m_versionUrl = null, m_zipUrl = null, m_unziPath = null;
        private UpdateState m_updateState;

        public MainWindow()
        {
            InitializeComponent();
            m_updateState = UpdateState.search;

            //Si l'évenement this.Close() est appeler.
            this.Closing += delegate
            {
                if (m_updateState == UpdateState.download) //Si le téléchargement est en cours.
                {
                    if (MessageBox.Show("Le téléchargement sera annuler si vous quittez.", "Voulez vous quitter ?", MessageBoxButton.YesNo) == MessageBoxResult.Yes)
                    {
                        this.Close();
                    }
                }
            };

            //Téléchargement du fichier de configuration
            WebClient request = new WebClient();
            request.DownloadFile(m_confUrl, "conf.txt");

            //Lecture du fichier
            StreamReader initReader;
            initReader = new StreamReader("conf.txt");

            string lineRead = initReader.ReadLine();
            if (lineRead != null)
            {
                string[] settingLine = lineRead.Split(' ');

                m_versionUrl = settingLine[0];
                m_zipUrl = settingLine[1];
                m_unziPath = settingLine[2];
            }
            else
                MessageBox.Show("Erreur: Fichier de configuration illisible", "ERREUR", MessageBoxButton.OK, MessageBoxImage.Stop);

            initReader.Close();

            //Vérification de la version, mise à jour ou non.
            if (File.Exists("version.txt")) // Voir si le fichier version.txt existe sur l'ordinateur.
            {
                Stream data = request.OpenRead(m_versionUrl); // Lire la version en ligne du version
                StreamReader reader = new StreamReader(data);
                string onlineVersion = reader.ReadLine();
                reader.Close();

                StreamReader localVersionStream;//Local
                localVersionStream = new StreamReader("version.txt");
                string localVersion = localVersionStream.ReadLine();
                localVersionStream.Close();

                if (onlineVersion == localVersion) //Si il existe, vérifier que le client est à jour
                {
                    //Tout est à jour
                    m_updateState = UpdateState.update;
                }
                else
                {
                    //Mise à jour
                    m_updateState = UpdateState.available;
                    Download();
                }
            }
            else //Si il n'existe pas, alors mise à jour.
            {
                //Mise à jour
                m_updateState = UpdateState.available;
                Download();
            }
        }

        //Update
        private bool Download()
        {
            if (m_updateState == UpdateState.available)
            {
                m_updateState = UpdateState.download;
                Play.Content = "Mise à jour en cours ..."; //On change le texte
                Play.Margin = new Thickness(304, 104, 0, 0);

                WebClient webClient = new WebClient();
                webClient.DownloadFileCompleted += new AsyncCompletedEventHandler(Completed);
                webClient.DownloadProgressChanged += new DownloadProgressChangedEventHandler(ProgressChanged);
                webClient.DownloadFileAsync(new Uri(m_zipUrl), "update.zip");
            }

            return true;
        }

        private void Completed(object sender, AsyncCompletedEventArgs e)
        {
            Play.Margin = new Thickness(602, 104, 0, 0);
            Play.Content = "Jouer";

            if (Directory.Exists(m_unziPath)) //On supprime l'ancienne version.
            {
                string[] filePaths = Directory.GetFiles(m_unziPath);
                foreach (string filePath in filePaths)
                    File.Delete(filePath);
            }

            Decompress("update.zip", m_unziPath, true);

            //Mettre à jour le version.txt local
            WebClient webClient = new WebClient();
            webClient.DownloadFile(new Uri(m_versionUrl), "version.txt");

            m_updateState = UpdateState.update;
        }

        private void ProgressChanged(object sender, DownloadProgressChangedEventArgs e)
        {
            Play.Content = "Mise à jour en cours (" + e.ProgressPercentage + "%)";
        }

        //Button Event
        private void OfficialWebSite_Click(object sender, RoutedEventArgs e)
        {
            Process.Start("https://minecraft.net/");
        }

        private void Exit_Click(object sender, RoutedEventArgs e)
        {
            this.Close();
        }

        private void Play_Click(object sender, RoutedEventArgs e)
        {
            if(m_updateState == UpdateState.update)
            {
                //Lancement de votre exécutable
                System.Diagnostics.ProcessStartInfo start = new System.Diagnostics.ProcessStartInfo();
                start.FileName = ".exe";
                start.WorkingDirectory = m_unziPath;
                System.Diagnostics.Process.Start(start);
            }
        }

        //Unzip
        public bool Decompress(string source, string destinationDirectory, bool deleteOriginal = false)
        {
            //Ouverture d'un nouveau contexte ZipInputStream (Librairie)
            using (var zipStream = new ZipInputStream(File.OpenRead(source)))
            {
                ZipEntry entry = null;
                string tempEntry = string.Empty;

                while ((entry = zipStream.GetNextEntry()) != null)
                {
                    string fileName = System.IO.Path.GetFileName(entry.Name);

                    if (destinationDirectory != string.Empty)
                        Directory.CreateDirectory(destinationDirectory);

                    if (!string.IsNullOrEmpty(fileName))
                    {
                        if (entry.Name.IndexOf(".ini") < 0)
                        {
                            string path = destinationDirectory + @"\" + entry.Name;
                            path = path.Replace("\\ ", "\\");
                            string dirPath = System.IO.Path.GetDirectoryName(path);
                            if (!Directory.Exists(dirPath))
                                Directory.CreateDirectory(dirPath);
                            using (var stream = File.Create(path))
                            {
                                int size = 2048;
                                byte[] data = new byte[2048];

                                byte[] buffer = new byte[size];

                                while (true)
                                {
                                    size = zipStream.Read(buffer, 0, buffer.Length);
                                    if (size > 0)
                                        stream.Write(buffer, 0, size);
                                    else
                                        break;
                                }
                            }
                        }
                    }
                }
            }

            if (deleteOriginal)
                File.Delete(source);

            return true;
        }
    }
}

Au niveau du header et des using rien de spécial, sauf peut-être using ICSharpCode.SharpZipLib.Zip;, ceci est le nom de la librairie que nous utiliserons pour décompresser le fichier .zip: ICSharpCode.

/// <summary>
/// Logique d'interaction pour MainWindow.xaml
/// </summary>
enum UpdateState { search, available, download, update };

Cette ligne permet de déclarer une énumération, son utilité sera de donner des valeurs à notre variable de même type m_updateState qui définit le statut du système de mise à jour.

  • search correspond à l’état ou le système cherche la mise à jour,
  • available que le système doit être mis à jour,
  • download que le téléchargement de la nouvelle version est en cours,
  • update signifie-lui, que le logiciel est à jour. Si aucune mise à jour n’est disponible, update sera la valeur assignée par défaut.

Comme vue plus haut sur le schéma, le lanceur au démarrage va chercher un fichier conf.txt contenant ces configurations (adresse du fichier version et du zip). Pour modifier l’adresse rien de plus simple, il suffit dans le code de modifier cette ligne: private string m_confUrl = "http://aubega.com/launcher/conf.txt"; avec votre adresse.

La fonction public MainWindow() est le noyau de notre application, c’est ici que nous vérifions au démarrage du lanceur si oui ou non il faut faire une mises à jour. Nous allons étudier le code pas à pas.

InitializeComponent();
m_updateState = UpdateState.search;

//Si l'évenement this.Close() est appeler.
this.Closing += delegate
{
    if (m_updateState == UpdateState.download) //Si le téléchargement est en cours.
    {
        if (MessageBox.Show("Le téléchargement sera annuler si vous quittez.", "Voulez vous quitter ?", MessageBoxButton.YesNo) == MessageBoxResult.Yes)
        {
            this.Close();
        }
    }
};

Ici nous initialisons les variables de notre application, puis nous ajoutons un évènement via ‘this.Closing += delegate’ qui indique que lors de la fermeture de l’application, il est vérifié qu’aucun téléchargement est en cours, dans le cas contraire une fenêtre d’alerte s’ouvrira et demandera à l’utilisateur via ‘MessageBoxButton.YesNo’ s’il souhaite vraiment quitter et donc arrêter le téléchargement.

//Téléchargement du fichier de configuration
WebClient request = new WebClient();
request.DownloadFile(m_confUrl, "conf.txt");

//Lecture du fichier
StreamReader initReader;
initReader = new StreamReader("conf.txt");

string lineRead = initReader.ReadLine();
if (lineRead != null)
{
    string[] settingLine = lineRead.Split(' ');

    m_versionUrl = settingLine[0];
    m_zipUrl = settingLine[1];
    m_unziPath = settingLine[2];
 }
 else
    MessageBox.Show("Erreur: Fichier de configuration illisible", "ERREUR", MessageBoxButton.OK, MessageBoxImage.Stop);

initReader.Close();

Ici pas grand-chose à dire, les commentaires expliquent assez bien le code. Nous téléchargons le fichier de configuration puis une fois en local nous le lisons pour récupérer: l’adresse du fichier de version, du .zip et l’endroit ou le .zip sera décompressé.

//Vérification de la version, mise à jour ou non.
if (File.Exists("version.txt")) // Voir si le fichier version.txt existe sur l'ordinateur.
{
    Stream data = request.OpenRead(m_versionUrl); // Lire la version en ligne du version
    StreamReader reader = new StreamReader(data);
    string onlineVersion = reader.ReadLine();
    reader.Close();

    StreamReader localVersionStream;//Local
    localVersionStream = new StreamReader("version.txt");
    string localVersion = localVersionStream.ReadLine();
    localVersionStream.Close();

    if (onlineVersion == localVersion) //Si il existe, vérifier que le client est à jour
    {
        //Tout est à jour
        m_updateState = UpdateState.update;
    }
    else
    {
        //Mise à jour
        m_updateState = UpdateState.available;
        Download();
    }
 }
 else //Si il n'existe pas, alors mise à jour.
 {
    //Mise à jour
    m_updateState = UpdateState.available;
    Download();
 }

Nous vérifions si oui ou non il faut faire une mise à jour. Si la version local et identique à la version en ligne alors dans ce cas pas besoin de mise à jour sinon on la lance. Si le fichier version.txt n’existe pas alors mise à jour.

Nous avons fini de voir le fonctionnement de la fonction principale MainWindow(). Il nous reste à étudier la fonction qui gère le téléchargement, puis nous verrons pour finir comment ajouté des actions à nos boutons pour, par exemple lancer notre .exe ou ouvrir une page internet.

private bool Download()
{
   if (m_updateState == UpdateState.available)
   {
       m_updateState = UpdateState.download;
       Play.Content = "Mise à jour en cours ..."; //On change le texte
       Play.Margin = new Thickness(304, 104, 0, 0);

       WebClient webClient = new WebClient();
       webClient.DownloadFileCompleted += new AsyncCompletedEventHandler(Completed);
       webClient.DownloadProgressChanged += new DownloadProgressChangedEventHandler(ProgressChanged);
       webClient.DownloadFileAsync(new Uri(m_zipUrl), "update.zip");
    }

    return true;
}

La fonction Download est appelée dès qu’une mise à jour est disponible, la variable m_updateState est mise à l’état available indiquant qu’une mise à jour est disponible, puis à l’état download pour indiquer que la mise à jour est en cours de téléchargement. Avec WebClient nous lançons un téléchargement de type Asynchrone: DownloadFileAsync pour ne pas bloquer l’application pendant le téléchargement. DownloadProgressChanged permet de mettre à jour le rendu graphique de notre application en fonction du pourcentage de téléchargement et pour finir DownloadFileCompleted sera exécuté une fois le téléchargement effectué et terminer.

private void ProgressChanged(object sender, DownloadProgressChangedEventArgs e)
{
    Play.Content = "Mise à jour en cours (" + e.ProgressPercentage + "%)";
}

Dans cette partie, nous affichons sur le bouton Play un texte avec le pourcentage du téléchargement. Cette fonction est appelée via webClient.DownloadProgressChanged += new DownloadProgressChangedEventHandler(ProgressChanged); vue plus haut.

private void Completed(object sender, AsyncCompletedEventArgs e)
{
    Play.Margin = new Thickness(602, 104, 0, 0);
    Play.Content = "Jouer";

    if (Directory.Exists(m_unziPath)) //On supprime l'ancienne version.
    {
        string[] filePaths = Directory.GetFiles(m_unziPath);
        foreach (string filePath in filePaths)
            File.Delete(filePath);
    }

    Decompress("update.zip", m_unziPath, true); //Unzip file

    //Mettre à jour le version.txt local
    WebClient webClient = new WebClient();
    webClient.DownloadFile(new Uri(m_versionUrl), "version.txt");

    m_updateState = UpdateState.update;
}

Cette fonction s’effectue une fois le téléchargement terminé. Le code est assez simple à comprendre, nous changeons la position du bouton ‘Play’ de façon qu’il soit à ça place puis nous changeons le texte par Jouer puis nous mettons à jour, une fois le téléchargement terminer le version.txt.

Nous allons rapidement voir comment ajouter des actions à nos beaux boutons stylisés précédemment en XML. Rien de plus simple, il suffit pour cela, dans l’interface graphique de Visual studio lors de l’édition du XML de double-cliquer sur votre bouton, une fonction liée à celui-ci sera automatiquement créée. Il ne vous reste maintenant plus qu’à la remplir.

En ouvrant un ‘Process’ par exemple pour ouvrir une page internet ou alors votre application .exe.

    //Button Event
    private void OfficialWebSite_Click(object sender, RoutedEventArgs e)
    {
        Process.Start("https://minecraft.net/");
    }

    private void Exit_Click(object sender, RoutedEventArgs e)
    {
        this.Close();
    }

    private void Play_Click(object sender, RoutedEventArgs e)
    {
        if(m_updateState == UpdateState.update)
        {
            //Lancement de votre exécutable
            System.Diagnostics.ProcessStartInfo start = new System.Diagnostics.ProcessStartInfo();
            start.FileName = ".exe";
            start.WorkingDirectory = m_unziPath;
            System.Diagnostics.Process.Start(start);
        }
    }

Je ne vais pas vous expliquer dans cet article le code qui permet de décompresser les archives .zip, cela serait trop long. Je ferai peut-être un autre article sur ce thème.

Voilà, vous savez maintenant comment crée vôtre système de mise à jour pour vos jeux/logiciels. N’hésitez surtout pas à partager dans les commentaires vos avis, ainsi que vos créations !

Télécharger le projet Visual Studio (GitHub)